You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/08/13 23:19:22 UTC
[incubator-superset] branch master updated: [feature] Allow min/max
value for the sparkline in time series table (#5603)
This is an automated email from the ASF dual-hosted git repository.
ccwilliams pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new 536478e [feature] Allow min/max value for the sparkline in time series table (#5603)
536478e is described below
commit 536478e96d020cae2385f981e28f03d30ff1f240
Author: Krist Wongsuphasawat <kr...@gmail.com>
AuthorDate: Mon Aug 13 16:19:19 2018 -0700
[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 +-
.../assets/src/visualizations/SparklineCell.jsx | 173 +++++++++++++++++++++
superset/assets/src/visualizations/time_table.jsx | 70 +++------
4 files changed, 221 insertions(+), 53 deletions(-)
diff --git a/superset/assets/src/explore/components/controls/TimeSeriesColumnControl.jsx b/superset/assets/src/explore/components/controls/TimeSeriesColumnControl.jsx
index 2634dd7..4c1f0d1 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',
+ <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',
(
diff --git a/superset/assets/src/modules/visUtils.js b/superset/assets/src/modules/visUtils.js
index 62e5725..c1f2a69 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 0000000..9ca272e
--- /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 <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;
diff --git a/superset/assets/src/visualizations/time_table.jsx b/superset/assets/src/visualizations/time_table.jsx
index 900fc5f..c34e5d0 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: (
- <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"