Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[time series table] visual improvements #3957

Merged
merged 2 commits into from Dec 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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);
Expand Down Expand Up @@ -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}
Expand All @@ -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',
Expand Down Expand Up @@ -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
Expand All @@ -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>
Expand Down
17 changes: 15 additions & 2 deletions superset/assets/javascripts/modules/dates.js
Expand Up @@ -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 () {
Expand Down
4 changes: 2 additions & 2 deletions superset/assets/package.json
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions superset/assets/stylesheets/superset.less
Expand Up @@ -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;
}
Expand Down
91 changes: 54 additions & 37 deletions superset/assets/visualizations/time_table.jsx
Expand Up @@ -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';

Expand All @@ -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 }) {
Expand Down Expand Up @@ -65,47 +72,50 @@ 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 {
sparkData.push(null);
}
}
}
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>
)}
>
{({ 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}
onMouseMove={onMouseMove}
>
<LineSeries
showArea={false}
stroke={brandColor}
stroke="#767676"
/>
{tooltipData &&
<VerticalReferenceLine
Expand All @@ -116,7 +126,7 @@ function viz(slice, payload) {
{tooltipData &&
<PointSeries
points={[tooltipData.index]}
fill={brandColor}
fill="#767676"
strokeWidth={1}
/>}
</Sparkline>
Expand All @@ -127,55 +137,61 @@ 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',
},
};
}
});
return row;
});
ReactDOM.render(
<Table
className="table table-condensed"
className="table table-no-hover"
defaultSort={defaultSort}
sortBy={defaultSort}
sortable={fd.column_collection.map(c => c.key)}
Expand All @@ -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>))}
Expand Down