[feature] Allow min/max value for the sparkline in time series table (#5603)

* Allow min/max value for the sparkline in time series table

* show bound lines

* User can choose to show y-axis bounds

* show label for the bounds

* compute necessary padding for the bound label

* extract sparkline code to another component

* can show y-axis in sparkline without setting bounds

* reorder option rows
This commit is contained in:
Krist Wongsuphasawat 2018-08-13 16:19:19 -07:00 committed by Chris Williams
parent e0c02be14e
commit 536478e96d
4 changed files with 221 additions and 53 deletions

View File

@ -7,6 +7,7 @@ import Select from 'react-select';
import InfoTooltipWithTrigger from '../../../components/InfoTooltipWithTrigger';
import BoundsControl from './BoundsControl';
import CheckboxControl from './CheckboxControl';
const propTypes = {
onChange: PropTypes.func,
@ -47,9 +48,15 @@ export default class TimeSeriesColumnControl extends React.Component {
onTextInputChange(attr, event) {
this.setState({ [attr]: event.target.value }, this.onChange);
}
onCheckboxChange(attr, value) {
this.setState({ [attr]: value }, this.onChange);
}
onBoundsChange(bounds) {
this.setState({ bounds }, this.onChange);
}
onYAxisBoundsChange(yAxisBounds) {
this.setState({ yAxisBounds }, this.onChange);
}
setType() {
}
textSummary() {
@ -165,6 +172,28 @@ export default class TimeSeriesColumnControl extends React.Component {
options={comparisonTypeOptions}
/>,
)}
{this.state.colType === 'spark' && this.formRow(
'Show Y-axis',
(
'Show Y-axis on the sparkline. Will display the manually set min/max if set or min/max values in the data otherwise.'
),
'show-y-axis-bounds',
<CheckboxControl
value={this.state.showYAxis}
onChange={this.onCheckboxChange.bind(this, 'showYAxis')}
/>,
)}
{this.state.colType === 'spark' && this.formRow(
'Y-axis bounds',
(
'Manually set min/max values for the y-axis.'
),
'y-axis-bounds',
<BoundsControl
value={this.state.yAxisBounds}
onChange={this.onYAxisBoundsChange.bind(this)}
/>,
)}
{this.state.colType !== 'spark' && this.formRow(
'Color bounds',
(

View File

@ -18,7 +18,7 @@ export function getTextDimension({
}
if (isDefined(style)) {
['font', 'fontWeight', 'fontStyle', 'fontSize', 'fontFamily']
['font', 'fontWeight', 'fontStyle', 'fontSize', 'fontFamily', 'letterSpacing']
.filter(field => isDefined(style[field]))
.forEach((field) => {
textNode.style[field] = style[field];

View File

@ -0,0 +1,173 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Sparkline, LineSeries, PointSeries, HorizontalReferenceLine, VerticalReferenceLine, WithTooltip } from '@data-ui/sparkline';
import { d3format } from '../modules/utils';
import { getTextDimension } from '../modules/visUtils';
const propTypes = {
className: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
data: PropTypes.array.isRequired,
ariaLabel: PropTypes.string,
numberFormat: PropTypes.string,
yAxisBounds: PropTypes.array,
showYAxis: PropTypes.bool,
renderTooltip: PropTypes.func,
};
const defaultProps = {
className: '',
width: 300,
height: 50,
ariaLabel: '',
numberFormat: undefined,
yAxisBounds: [null, null],
showYAxis: false,
renderTooltip() { return <div />; },
};
const MARGIN = {
top: 8,
right: 8,
bottom: 8,
left: 8,
};
const tooltipProps = {
style: {
opacity: 0.8,
},
offsetTop: 0,
};
function getSparklineTextWidth(text) {
return getTextDimension({
text,
style: {
fontSize: '12px',
fontWeight: 200,
letterSpacing: 0.4,
},
}).width + 5;
}
function isValidBoundValue(value) {
return value !== null && value !== undefined && value !== '' && !Number.isNaN(value);
}
class SparklineCell extends React.Component {
renderHorizontalReferenceLine(value, label) {
return (
<HorizontalReferenceLine
reference={value}
labelPosition="right"
renderLabel={() => label}
stroke="#bbb"
strokeDasharray="3 3"
strokeWidth={1}
/>
);
}
render() {
const {
width,
height,
data,
ariaLabel,
numberFormat,
yAxisBounds,
showYAxis,
renderTooltip,
} = this.props;
const yScale = {};
let hasMinBound = false;
let hasMaxBound = false;
if (yAxisBounds) {
const [minBound, maxBound] = yAxisBounds;
hasMinBound = isValidBoundValue(minBound);
if (hasMinBound) {
yScale.min = minBound;
}
hasMaxBound = isValidBoundValue(maxBound);
if (hasMaxBound) {
yScale.max = maxBound;
}
}
let min;
let max;
let minLabel;
let maxLabel;
let labelLength = 0;
if (showYAxis) {
const [minBound, maxBound] = yAxisBounds;
min = hasMinBound
? minBound
: data.reduce((acc, current) => Math.min(acc, current), data[0]);
max = hasMaxBound
? maxBound
: data.reduce((acc, current) => Math.max(acc, current), data[0]);
minLabel = d3format(numberFormat, min);
maxLabel = d3format(numberFormat, max);
labelLength = Math.max(
getSparklineTextWidth(minLabel),
getSparklineTextWidth(maxLabel),
);
}
const margin = {
...MARGIN,
right: MARGIN.right + labelLength,
};
return (
<WithTooltip
tooltipProps={tooltipProps}
hoverStyles={null}
renderTooltip={renderTooltip}
>
{({ onMouseLeave, onMouseMove, tooltipData }) => (
<Sparkline
ariaLabel={ariaLabel}
width={width}
height={height}
margin={margin}
data={data}
onMouseLeave={onMouseLeave}
onMouseMove={onMouseMove}
{...yScale}
>
{showYAxis &&
this.renderHorizontalReferenceLine(min, minLabel)}
{showYAxis &&
this.renderHorizontalReferenceLine(max, maxLabel)}
<LineSeries
showArea={false}
stroke="#767676"
/>
{tooltipData &&
<VerticalReferenceLine
reference={tooltipData.index}
strokeDasharray="3 3"
strokeWidth={1}
/>}
{tooltipData &&
<PointSeries
points={[tooltipData.index]}
fill="#767676"
strokeWidth={1}
/>}
</Sparkline>
)}
</WithTooltip>
);
}
}
SparklineCell.propTypes = propTypes;
SparklineCell.defaultProps = defaultProps;
export default SparklineCell;

View File

@ -1,30 +1,17 @@
import ReactDOM from 'react-dom';
import React from 'react';
import propTypes from 'prop-types';
import PropTypes from 'prop-types';
import { Table, Thead, Th, Tr, Td } from 'reactable';
import d3 from 'd3';
import Mustache from 'mustache';
import { Sparkline, LineSeries, PointSeries, VerticalReferenceLine, WithTooltip } from '@data-ui/sparkline';
import MetricOption from '../components/MetricOption';
import { d3format } from '../modules/utils';
import { formatDateThunk } from '../modules/dates';
import { d3format } from '../modules/utils';
import InfoTooltipWithTrigger from '../components/InfoTooltipWithTrigger';
import SparklineCell from './SparklineCell';
import './time_table.css';
const SPARKLINE_MARGIN = {
top: 8,
right: 8,
bottom: 8,
left: 8,
};
const sparklineTooltipProps = {
style: {
opacity: 0.8,
},
offsetTop: 0,
};
const ACCESSIBLE_COLOR_BOUNDS = ['#ca0020', '#0571b0'];
function FormattedNumber({ num, format }) {
@ -37,8 +24,8 @@ function FormattedNumber({ num, format }) {
}
FormattedNumber.propTypes = {
num: propTypes.number,
format: propTypes.string,
num: PropTypes.number,
format: PropTypes.string,
};
function viz(slice, payload) {
@ -93,49 +80,27 @@ function viz(slice, payload) {
}
}
}
const formatDate = formatDateThunk(column.dateFormat);
row[column.key] = {
data: sparkData[sparkData.length - 1],
display: (
<WithTooltip
tooltipProps={sparklineTooltipProps}
hoverStyles={null}
<SparklineCell
width={parseInt(column.width, 10) || 300}
height={parseInt(column.height, 10) || 50}
data={sparkData}
ariaLabel={`spark-${metricLabel}`}
numberFormat={column.d3format}
yAxisBounds={column.yAxisBounds}
showYAxis={column.showYAxis}
renderTooltip={({ index }) => (
<div>
<strong>{d3format(column.d3format, sparkData[index])}</strong>
<strong>{d3format(column.d3Format, sparkData[index])}</strong>
<div>{formatDate(data[index].iso)}</div>
</div>
)}
>
{({ onMouseLeave, onMouseMove, tooltipData }) => (
<Sparkline
ariaLabel={`spark-${metricLabel}`}
width={parseInt(column.width, 10) || 300}
height={parseInt(column.height, 10) || 50}
margin={SPARKLINE_MARGIN}
data={sparkData}
onMouseLeave={onMouseLeave}
onMouseMove={onMouseMove}
>
<LineSeries
showArea={false}
stroke="#767676"
/>
{tooltipData &&
<VerticalReferenceLine
reference={tooltipData.index}
strokeDasharray="3 3"
strokeWidth={1}
/>}
{tooltipData &&
<PointSeries
points={[tooltipData.index]}
fill="#767676"
strokeWidth={1}
/>}
</Sparkline>
)}
</WithTooltip>
/>
),
};
} else {
@ -200,6 +165,7 @@ function viz(slice, payload) {
});
return row;
});
ReactDOM.render(
<Table
className="table table-no-hover"