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

View File

@ -66,10 +66,23 @@ export const formatDate = function (dttm) {
const d = UTC(new Date(dttm));
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 duration = moment(new Date(diffSec));
return duration.utc().format(f);
return duration.utc().format(format);
};
export const now = function () {

View File

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

View File

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