[time series table] visual improvements (#3957)

* [time series table] visual improvements

* [time series table] don't set cell color if text color isn't set
This commit is contained in:
Chris Williams 2017-11-30 20:48:17 -08:00 committed by Maxime Beauchemin
parent 9904593dc3
commit 76a2f95231
5 changed files with 96 additions and 53 deletions

View File

@ -33,7 +33,7 @@ const colTypeOptions = [
export default class TimeSeriesColumnControl extends React.Component { export default class TimeSeriesColumnControl extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const state = Object.assign({}, props); const state = { ...props };
delete state.onChange; delete state.onChange;
this.state = state; this.state = state;
this.onChange = this.onChange.bind(this); this.onChange = this.onChange.bind(this);
@ -61,7 +61,7 @@ export default class TimeSeriesColumnControl extends React.Component {
return ( return (
<Row style={{ marginTop: '5px' }}> <Row style={{ marginTop: '5px' }}>
<Col md={5}> <Col md={5}>
{label}{' '} {`${label} `}
<InfoTooltipWithTrigger <InfoTooltipWithTrigger
placement="top" placement="top"
tooltip={tooltip} tooltip={tooltip}
@ -75,7 +75,7 @@ export default class TimeSeriesColumnControl extends React.Component {
renderPopover() { renderPopover() {
return ( return (
<Popover id="ts-col-popo" title="Column Configuration"> <Popover id="ts-col-popo" title="Column Configuration">
<div style={{ width: '280px' }}> <div style={{ width: 300 }}>
{this.formRow( {this.formRow(
'Label', 'Label',
'The column header label', 'The column header label',
@ -166,13 +166,11 @@ export default class TimeSeriesColumnControl extends React.Component {
/>, />,
)} )}
{this.state.colType !== 'spark' && this.formRow( {this.state.colType !== 'spark' && this.formRow(
'Bounds', 'Color bounds',
( (
'Number bounds used for color coding from red to green. ' + `Number bounds used for color encoding from red to blue.
'Reverse the number for green to red. To get boolean ' + Reverse the numbers for blue to red. To get pure red or blue,
'red or green without spectrum, you can use either only ' + you can enter either only min or max.`
'min, or max, depending on whether small or big should be ' +
'green or red.'
), ),
'bounds', 'bounds',
<BoundsControl <BoundsControl
@ -181,14 +179,25 @@ export default class TimeSeriesColumnControl extends React.Component {
/>, />,
)} )}
{this.formRow( {this.formRow(
'D3 format', 'Number format',
'D3 format string', 'Optional d3 number format string',
'd3-format', 'd3-format',
<FormControl <FormControl
value={this.state.d3format} value={this.state.d3format}
onChange={this.onTextInputChange.bind(this, 'd3format')} onChange={this.onTextInputChange.bind(this, 'd3format')}
bsSize="small" bsSize="small"
placeholder="D3 format string" placeholder="Number format string"
/>,
)}
{this.state.colType === 'spark' && this.formRow(
'Date format',
'Optional d3 date format string',
'date-format',
<FormControl
value={this.state.dateFormat}
onChange={this.onTextInputChange.bind(this, 'dateFormat')}
bsSize="small"
placeholder="Date format string"
/>, />,
)} )}
</div> </div>

View File

@ -66,10 +66,23 @@ export const formatDate = function (dttm) {
const d = UTC(new Date(dttm)); const d = UTC(new Date(dttm));
return tickMultiFormat(d); return tickMultiFormat(d);
}; };
export const fDuration = function (t1, t2, f = 'HH:mm:ss.SS') {
export const formatDateThunk = function (format) {
if (!format) {
return formatDate;
}
const formatter = d3.time.format(format);
return (dttm) => {
const d = UTC(new Date(dttm));
return formatter(d);
};
};
export const fDuration = function (t1, t2, format = 'HH:mm:ss.SS') {
const diffSec = t2 - t1; const diffSec = t2 - t1;
const duration = moment(new Date(diffSec)); const duration = moment(new Date(diffSec));
return duration.utc().format(f); return duration.utc().format(format);
}; };
export const now = function () { export const now = function () {

View File

@ -39,8 +39,8 @@
}, },
"homepage": "http://superset.apache.org/", "homepage": "http://superset.apache.org/",
"dependencies": { "dependencies": {
"@data-ui/event-flow": "0.0.8", "@data-ui/event-flow": "^0.0.8",
"@data-ui/sparkline": "0.0.47", "@data-ui/sparkline": "^0.0.49",
"babel-register": "^6.24.1", "babel-register": "^6.24.1",
"bootstrap": "^3.3.6", "bootstrap": "^3.3.6",
"brace": "^0.10.0", "brace": "^0.10.0",

View File

@ -218,6 +218,10 @@ div.widget {
float: left; float: left;
} }
table.table-no-hover tr:hover {
background-color: initial;
}
.editable-title input { .editable-title input {
padding: 2px 6px 3px 6px; padding: 2px 6px 3px 6px;
} }

View File

@ -7,8 +7,8 @@ import Mustache from 'mustache';
import { Sparkline, LineSeries, PointSeries, VerticalReferenceLine, WithTooltip } from '@data-ui/sparkline'; import { Sparkline, LineSeries, PointSeries, VerticalReferenceLine, WithTooltip } from '@data-ui/sparkline';
import MetricOption from '../javascripts/components/MetricOption'; import MetricOption from '../javascripts/components/MetricOption';
import { d3format, brandColor } from '../javascripts/modules/utils'; import { d3format } from '../javascripts/modules/utils';
import { formatDate } from '../javascripts/modules/dates'; import { formatDateThunk } from '../javascripts/modules/dates';
import InfoTooltipWithTrigger from '../javascripts/components/InfoTooltipWithTrigger'; import InfoTooltipWithTrigger from '../javascripts/components/InfoTooltipWithTrigger';
import './time_table.css'; import './time_table.css';
@ -18,6 +18,13 @@ const SPARKLINE_MARGIN = {
bottom: 8, bottom: 8,
left: 8, left: 8,
}; };
const sparklineTooltipProps = {
style: {
opacity: 0.8,
},
offsetTop: 0,
};
const ACCESSIBLE_COLOR_BOUNDS = ['#ca0020', '#0571b0']; const ACCESSIBLE_COLOR_BOUNDS = ['#ca0020', '#0571b0'];
function FormattedNumber({ num, format }) { function FormattedNumber({ num, format }) {
@ -65,16 +72,16 @@ function viz(slice, payload) {
leftCell = url ? <a href={url} target="_blank">{metric}</a> : metric; leftCell = url ? <a href={url} target="_blank">{metric}</a> : metric;
} }
const row = { metric: leftCell }; const row = { metric: leftCell };
fd.column_collection.forEach((c) => { fd.column_collection.forEach((column) => {
if (c.colType === 'spark') { if (column.colType === 'spark') {
let sparkData; let sparkData;
if (!c.timeRatio) { if (!column.timeRatio) {
sparkData = data.map(d => d[metric]); sparkData = data.map(d => d[metric]);
} else { } else {
// Period ratio sparkline // Period ratio sparkline
sparkData = []; sparkData = [];
for (let i = c.timeRatio; i < data.length; i++) { for (let i = column.timeRatio; i < data.length; i++) {
const prevData = data[i - c.timeRatio][metric]; const prevData = data[i - column.timeRatio][metric];
if (prevData && prevData !== 0) { if (prevData && prevData !== 0) {
sparkData.push(data[i][metric] / prevData); sparkData.push(data[i][metric] / prevData);
} else { } else {
@ -82,13 +89,16 @@ function viz(slice, payload) {
} }
} }
} }
row[c.key] = { const formatDate = formatDateThunk(column.dateFormat);
row[column.key] = {
data: sparkData[sparkData.length - 1], data: sparkData[sparkData.length - 1],
display: ( display: (
<WithTooltip <WithTooltip
tooltipProps={sparklineTooltipProps}
hoverStyles={null}
renderTooltip={({ index }) => ( renderTooltip={({ index }) => (
<div> <div>
<strong>{d3format(c.d3format, sparkData[index])}</strong> <strong>{d3format(column.d3format, sparkData[index])}</strong>
<div>{formatDate(data[index].iso)}</div> <div>{formatDate(data[index].iso)}</div>
</div> </div>
)} )}
@ -96,8 +106,8 @@ function viz(slice, payload) {
{({ onMouseLeave, onMouseMove, tooltipData }) => ( {({ onMouseLeave, onMouseMove, tooltipData }) => (
<Sparkline <Sparkline
ariaLabel={`spark-${metric}`} ariaLabel={`spark-${metric}`}
width={parseInt(c.width, 10) || 300} width={parseInt(column.width, 10) || 300}
height={parseInt(c.height, 10) || 50} height={parseInt(column.height, 10) || 50}
margin={SPARKLINE_MARGIN} margin={SPARKLINE_MARGIN}
data={sparkData} data={sparkData}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
@ -105,7 +115,7 @@ function viz(slice, payload) {
> >
<LineSeries <LineSeries
showArea={false} showArea={false}
stroke={brandColor} stroke="#767676"
/> />
{tooltipData && {tooltipData &&
<VerticalReferenceLine <VerticalReferenceLine
@ -116,7 +126,7 @@ function viz(slice, payload) {
{tooltipData && {tooltipData &&
<PointSeries <PointSeries
points={[tooltipData.index]} points={[tooltipData.index]}
fill={brandColor} fill="#767676"
strokeWidth={1} strokeWidth={1}
/>} />}
</Sparkline> </Sparkline>
@ -127,47 +137,53 @@ function viz(slice, payload) {
} else { } else {
const recent = reversedData[0][metric]; const recent = reversedData[0][metric];
let v; let v;
if (c.colType === 'time') { if (column.colType === 'time') {
// Time lag ratio // Time lag ratio
v = reversedData[parseInt(c.timeLag, 10)][metric]; v = reversedData[parseInt(column.timeLag, 10)][metric];
if (c.comparisonType === 'diff') { if (column.comparisonType === 'diff') {
v = recent - v; v = recent - v;
} else if (c.comparisonType === 'perc') { } else if (column.comparisonType === 'perc') {
v = recent / v; v = recent / v;
} else if (c.comparisonType === 'perc_change') { } else if (column.comparisonType === 'perc_change') {
v = (recent / v) - 1; v = (recent / v) - 1;
} }
} else if (c.colType === 'contrib') { } else if (column.colType === 'contrib') {
// contribution to column total // contribution to column total
v = recent / Object.keys(reversedData[0]) v = recent / Object.keys(reversedData[0])
.map(k => k !== 'iso' ? reversedData[0][k] : null) .map(k => k !== 'iso' ? reversedData[0][k] : null)
.reduce((a, b) => a + b); .reduce((a, b) => a + b);
} else if (c.colType === 'avg') { } else if (column.colType === 'avg') {
// Average over the last {timeLag} // Average over the last {timeLag}
v = reversedData v = reversedData
.map((k, i) => i < c.timeLag ? k[metric] : 0) .map((k, i) => i < column.timeLag ? k[metric] : 0)
.reduce((a, b) => a + b) / c.timeLag; .reduce((a, b) => a + b) / column.timeLag;
} }
let color; let color;
if (c.bounds && c.bounds[0] !== null && c.bounds[1] !== null) { if (column.bounds && column.bounds[0] !== null && column.bounds[1] !== null) {
const scaler = d3.scale.linear() const scaler = d3.scale.linear()
.domain([ .domain([
c.bounds[0], column.bounds[0],
c.bounds[0] + ((c.bounds[1] - c.bounds[0]) / 2), column.bounds[0] + ((column.bounds[1] - column.bounds[0]) / 2),
c.bounds[1]]) column.bounds[1],
])
.range([ACCESSIBLE_COLOR_BOUNDS[0], 'grey', ACCESSIBLE_COLOR_BOUNDS[1]]); .range([ACCESSIBLE_COLOR_BOUNDS[0], 'grey', ACCESSIBLE_COLOR_BOUNDS[1]]);
color = scaler(v); color = scaler(v);
} else if (c.bounds && c.bounds[0] !== null) { } else if (column.bounds && column.bounds[0] !== null) {
color = v >= c.bounds[0] ? ACCESSIBLE_COLOR_BOUNDS[1] : ACCESSIBLE_COLOR_BOUNDS[0]; color = v >= column.bounds[0] ? ACCESSIBLE_COLOR_BOUNDS[1] : ACCESSIBLE_COLOR_BOUNDS[0];
} else if (c.bounds && c.bounds[1] !== null) { } else if (column.bounds && column.bounds[1] !== null) {
color = v < c.bounds[1] ? ACCESSIBLE_COLOR_BOUNDS[1] : ACCESSIBLE_COLOR_BOUNDS[0]; color = v < column.bounds[1] ? ACCESSIBLE_COLOR_BOUNDS[1] : ACCESSIBLE_COLOR_BOUNDS[0];
} }
row[c.key] = { row[column.key] = {
data: v, data: v,
display: ( display: (
<span style={{ color }}> <div style={{ color }}>
<FormattedNumber num={v} format={c.d3format} /> <FormattedNumber num={v} format={column.d3format} />
</span>), </div>
),
style: color && {
boxShadow: `inset 0px -2.5px 0px 0px ${color}`,
borderRight: '2px solid #fff',
},
}; };
} }
}); });
@ -175,7 +191,7 @@ function viz(slice, payload) {
}); });
ReactDOM.render( ReactDOM.render(
<Table <Table
className="table table-condensed" className="table table-no-hover"
defaultSort={defaultSort} defaultSort={defaultSort}
sortBy={defaultSort} sortBy={defaultSort}
sortable={fd.column_collection.map(c => c.key)} sortable={fd.column_collection.map(c => c.key)}
@ -201,6 +217,7 @@ function viz(slice, payload) {
column={c.key} column={c.key}
key={c.key} key={c.key}
value={row[c.key].data} value={row[c.key].data}
style={row[c.key].style}
> >
{row[c.key].display} {row[c.key].display}
</Td>))} </Td>))}