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(