From 536478e96d020cae2385f981e28f03d30ff1f240 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Mon, 13 Aug 2018 16:19:19 -0700 Subject: [PATCH] [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 --- .../controls/TimeSeriesColumnControl.jsx | 29 +++ superset/assets/src/modules/visUtils.js | 2 +- .../src/visualizations/SparklineCell.jsx | 173 ++++++++++++++++++ .../assets/src/visualizations/time_table.jsx | 70 ++----- 4 files changed, 221 insertions(+), 53 deletions(-) create mode 100644 superset/assets/src/visualizations/SparklineCell.jsx diff --git a/superset/assets/src/explore/components/controls/TimeSeriesColumnControl.jsx b/superset/assets/src/explore/components/controls/TimeSeriesColumnControl.jsx index 2634dd77b5..4c1f0d1a4c 100644 --- a/superset/assets/src/explore/components/controls/TimeSeriesColumnControl.jsx +++ b/superset/assets/src/explore/components/controls/TimeSeriesColumnControl.jsx @@ -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', + , + )} + {this.state.colType === 'spark' && this.formRow( + 'Y-axis bounds', + ( + 'Manually set min/max values for the y-axis.' + ), + 'y-axis-bounds', + , + )} {this.state.colType !== 'spark' && this.formRow( 'Color bounds', ( diff --git a/superset/assets/src/modules/visUtils.js b/superset/assets/src/modules/visUtils.js index 62e5725eeb..c1f2a692d0 100644 --- a/superset/assets/src/modules/visUtils.js +++ b/superset/assets/src/modules/visUtils.js @@ -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]; diff --git a/superset/assets/src/visualizations/SparklineCell.jsx b/superset/assets/src/visualizations/SparklineCell.jsx new file mode 100644 index 0000000000..9ca272e9c3 --- /dev/null +++ b/superset/assets/src/visualizations/SparklineCell.jsx @@ -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
; }, +}; + +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 ( + 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 ( + + {({ onMouseLeave, onMouseMove, tooltipData }) => ( + + {showYAxis && + this.renderHorizontalReferenceLine(min, minLabel)} + {showYAxis && + this.renderHorizontalReferenceLine(max, maxLabel)} + + {tooltipData && + } + {tooltipData && + } + + )} + + ); + } +} + +SparklineCell.propTypes = propTypes; +SparklineCell.defaultProps = defaultProps; + +export default SparklineCell; diff --git a/superset/assets/src/visualizations/time_table.jsx b/superset/assets/src/visualizations/time_table.jsx index 900fc5f529..c34e5d01c7 100644 --- a/superset/assets/src/visualizations/time_table.jsx +++ b/superset/assets/src/visualizations/time_table.jsx @@ -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: ( - (
- {d3format(column.d3format, sparkData[index])} + {d3format(column.d3Format, sparkData[index])}
{formatDate(data[index].iso)}
)} - > - {({ onMouseLeave, onMouseMove, tooltipData }) => ( - - - {tooltipData && - } - {tooltipData && - } - - )} -
+ /> ), }; } else { @@ -200,6 +165,7 @@ function viz(slice, payload) { }); return row; }); + ReactDOM.render(