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"