Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update BigNumber #5469

Merged
merged 1 commit into from
Aug 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"dependencies": {
"@data-ui/event-flow": "^0.0.54",
"@data-ui/sparkline": "^0.0.54",
"@data-ui/xy-chart": "^0.0.61",
"@vx/responsive": "0.0.153",
"babel-register": "^6.24.1",
"bootstrap": "^3.3.6",
Expand Down
113 changes: 102 additions & 11 deletions superset/assets/src/modules/visUtils.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,105 @@
export function getTextWidth(text, fontDetails = '12px Roboto') {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (context) {
// Won't work outside of a browser context (ie unit tests)
context.font = fontDetails;
return context.measureText(text).width;
const SVG_NS = 'http://www.w3.org/2000/svg';

function isDefined(x) {
return x !== null && x !== undefined;
}

export function getTextDimension({
text,
className,
style,
container = document.body,
}) {
const textNode = document.createElementNS(SVG_NS, 'text');
textNode.textContent = text;

if (isDefined(className)) {
textNode.setAttribute('class', className);
}

if (isDefined(style)) {
['font', 'fontWeight', 'fontStyle', 'fontSize', 'fontFamily']
.filter(field => isDefined(style[field]))
.forEach((field) => {
textNode.style[field] = style[field];
});
}

const svg = document.createElementNS(SVG_NS, 'svg');
svg.style.position = 'absolute'; // so it won't disrupt page layout
svg.style.opacity = 0; // and not visible
svg.appendChild(textNode);
container.appendChild(svg);
let result;
if (textNode.getBBox) {
const bbox = textNode.getBBox();
// round up
result = {
width: Math.ceil(bbox.width),
height: Math.ceil(bbox.height),
};
} else {
// Handle when called from non-browser and do not support getBBox()
result = {
width: 100,
height: 100,
};
}
return 100;
container.removeChild(svg);
return result;
}

/**
* Shim to support legacy calls
*/
export function getTextWidth(text, font = '12px Roboto') {
return getTextDimension({ text, style: { font } }).width;
}

export default {
getTextWidth,
};
export function computeMaxFontSize({
text,
idealFontSize,
maxWidth,
maxHeight,
className,
style,
container,
}) {
let size = idealFontSize;
if (!isDefined(idealFontSize)) {
if (isDefined(maxHeight)) {
size = Math.floor(maxHeight);
} else {
throw new Error('You must specify at least one of maxHeight or idealFontSize');
}
}

function computeDimension(fontSize) {
return getTextDimension({
text,
className,
style: { ...style, fontSize },
container,
});
}

let textDimension = computeDimension(size);

// Decrease size until textWidth is less than maxWidth
if (isDefined(maxWidth)) {
while (textDimension.width > maxWidth) {
size -= 2;
textDimension = computeDimension(size);
}
}

// Decrease size until textHeight is less than maxHeight
if (isDefined(maxHeight)) {
while (textDimension.height > maxHeight) {
size -= 2;
textDimension = computeDimension(size);
}
}

return size;
}
274 changes: 274 additions & 0 deletions superset/assets/src/visualizations/BigNumber.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { XYChart, AreaSeries, CrossHair, LinearGradient } from '@data-ui/xy-chart';

import { brandColor } from '../modules/colors';
import { d3FormatPreset } from '../modules/utils';
import { formatDateVerbose } from '../modules/dates';
import { computeMaxFontSize } from '../modules/visUtils';

import './big_number.css';

const CHART_MARGIN = {
top: 4,
right: 4,
bottom: 4,
left: 4,
};

const PROPORTION = {
HEADER: 0.4,
SUBHEADER: 0.14,
HEADER_WITH_TRENDLINE: 0.3,
SUBHEADER_WITH_TRENDLINE: 0.125,
TRENDLINE: 0.3,
};

function renderTooltipFactory(formatValue) {
return function renderTooltip({ datum }) { // eslint-disable-line
const { x: rawDate, y: rawValue } = datum;
const formattedDate = formatDateVerbose(rawDate);
const value = formatValue(rawValue);

return (
<div style={{ padding: '4px 8px' }}>
{formattedDate}
<br />
<strong>{value}</strong>
</div>
);
};
}

function identity(x) {
return x;
}

const propTypes = {
className: PropTypes.string,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
bigNumber: PropTypes.number.isRequired,
formatBigNumber: PropTypes.func,
subheader: PropTypes.string,
showTrendline: PropTypes.bool,
trendlineData: PropTypes.array,
mainColor: PropTypes.string,
gradientId: PropTypes.string,
renderTooltip: PropTypes.func,
};
const defaultProps = {
className: '',
formatBigNumber: identity,
subheader: '',
showTrendline: false,
trendlineData: null,
mainColor: brandColor,
gradientId: '',
renderTooltip: renderTooltipFactory(identity),
};

class BigNumberVis extends React.Component {
getClassName() {
const { className, showTrendline } = this.props;
const names = `big_number ${className}`;
if (showTrendline) {
return names;
}
return `${names} no_trendline`;
}

createTemporaryContainer() {
const container = document.createElement('div');
container.className = this.getClassName();
container.style.position = 'absolute'; // so it won't disrupt page layout
container.style.opacity = 0; // and not visible
return container;
}

renderHeader(maxHeight) {
const { bigNumber, formatBigNumber, width } = this.props;
const text = formatBigNumber(bigNumber);

const container = this.createTemporaryContainer();
document.body.appendChild(container);
const fontSize = computeMaxFontSize({
text,
maxWidth: width,
maxHeight,
className: 'header_line',
container,
});
document.body.removeChild(container);

return (
<div
className="header_line"
style={{
fontSize,
height: maxHeight,
}}
>
<span>{text}</span>
</div>
);
}

renderSubheader(maxHeight) {
const { subheader, width } = this.props;
let fontSize = 0;
if (subheader) {
const container = this.createTemporaryContainer();
document.body.appendChild(container);
fontSize = computeMaxFontSize({
text: subheader,
maxWidth: width,
maxHeight,
className: 'subheader_line',
container,
});
document.body.removeChild(container);
}

return (
<div
className="subheader_line"
style={{
fontSize,
height: maxHeight,
}}
>
{subheader}
</div>
);
}

renderTrendline(maxHeight) {
const {
width,
trendlineData,
mainColor,
subheader,
renderTooltip,
gradientId,
} = this.props;
return (
<XYChart
ariaLabel={`Big number visualization ${subheader}`}
xScale={{ type: 'timeUtc' }}
yScale={{ type: 'linear' }}
width={width}
height={maxHeight}
margin={CHART_MARGIN}
renderTooltip={renderTooltip}
snapTooltipToDataX
>
<LinearGradient
id={gradientId}
from={mainColor}
to="#fff"
/>
<AreaSeries
data={trendlineData}
fill={`url(#${gradientId})`}
stroke={mainColor}
/>
<CrossHair
stroke={mainColor}
circleFill={mainColor}
circleStroke="#fff"
showHorizontalLine={false}
fullHeight
strokeDasharray="5,2"
/>
</XYChart>
);
}

render() {
const { showTrendline, height } = this.props;
const className = this.getClassName();

if (showTrendline) {
const chartHeight = Math.floor(PROPORTION.TRENDLINE * height);
const allTextHeight = height - chartHeight;
return (
<div className={className}>
<div
className="text_container"
style={{ height: allTextHeight }}
>
{this.renderHeader(Math.ceil(PROPORTION.HEADER_WITH_TRENDLINE * height))}
{this.renderSubheader(Math.ceil(PROPORTION.SUBHEADER_WITH_TRENDLINE * height))}
</div>
{this.renderTrendline(chartHeight)}
</div>
);
}
return (
<div
className={className}
style={{ height }}
>
{this.renderHeader(Math.ceil(PROPORTION.HEADER * height))}
{this.renderSubheader(Math.ceil(PROPORTION.SUBHEADER * height))}
</div>
);
}
}

BigNumberVis.propTypes = propTypes;
BigNumberVis.defaultProps = defaultProps;

function adaptor(slice, payload) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ the term adaptor for this + code style here

const { formData, containerId } = slice;
const { data, subheader, compare_suffix: compareSuffix } = payload.data;
const compareLag = Number(payload.data.compare_lag);
const supportTrendline = formData.viz_type === 'big_number';
const showTrendline = supportTrendline && formData.show_trend_line;
const formatValue = d3FormatPreset(formData.y_axis_format);
const bigNumber = supportTrendline ? data[data.length - 1][1] : data[0][0];

let percentChange = 0;
let formattedSubheader = subheader;

if (supportTrendline && compareLag > 0) {
const compareIndex = data.length - (compareLag + 1);
if (compareIndex >= 0) {
const compareValue = data[compareIndex][1];
percentChange = compareValue === 0
? 0 : (bigNumber - compareValue) / Math.abs(compareValue);
const formatPercentChange = d3.format('+.1%');
formattedSubheader = `${formatPercentChange(percentChange)} ${compareSuffix}`;
}
}

const trendlineData = showTrendline ? data.map(([x, y]) => ({ x, y })) : null;

let className = '';
if (percentChange > 0) {
className = 'positive';
} else if (percentChange < 0) {
className = 'negative';
}

ReactDOM.render(
<BigNumberVis
className={className}
width={slice.width()}
height={slice.height()}
bigNumber={bigNumber}
formatBigNumber={formatValue}
subheader={formattedSubheader}
showTrendline={showTrendline}
trendlineData={trendlineData}
mainColor={brandColor}
gradientId={`big_number_${containerId}`}
renderTooltip={renderTooltipFactory(formatValue)}
/>,
document.getElementById(containerId),
);
}

export default adaptor;
Loading