[formats] add better defaults for time + number formatting (#4843)

* [formats] add better defaults for time + number formatting

* [formatDate] add tests for concise formatDate

* [nvd3] use verbose time format in tooltips

* [number format] improve number format description

* [formats] revert to .3s defaults, tweak number format preview

* [formats] default number vis to .3s
This commit is contained in:
Chris Williams
2018-04-20 15:55:25 -07:00
committed by GitHub
parent 89347172d2
commit 66fcf9b687
7 changed files with 111 additions and 34 deletions

View File

@@ -3,6 +3,7 @@ import { expect } from 'chai';
import {
tickMultiFormat,
formatDate,
formatDateVerbose,
fDuration,
now,
epochTimeXHoursAgo,
@@ -25,13 +26,35 @@ describe('formatDate', () => {
expect(formatDate(new Date('2020-01-01'))).to.equal('2020');
});
it('shows only month when 1st of month', () => {
expect(formatDate(new Date('2020-03-01'))).to.equal('March');
});
it('does not show day of week when it is Sunday', () => {
expect(formatDate(new Date('2020-03-15'))).to.equal('Mar 15');
});
it('shows weekday when it is not Sunday (and no ms/sec/min/hr)', () => {
expect(formatDate(new Date('2020-03-03'))).to.equal('Tue 03');
});
});
describe('formatDateVerbose', () => {
it('is a function', () => {
assert.isFunction(formatDateVerbose);
});
it('shows only year when 1st day of the year', () => {
expect(formatDateVerbose(new Date('2020-01-01'))).to.equal('2020');
});
it('shows month and year when 1st of month', () => {
expect(formatDate(new Date('2020-03-01'))).to.equal('Mar 2020');
expect(formatDateVerbose(new Date('2020-03-01'))).to.equal('Mar 2020');
});
it('shows weekday when any day of the month', () => {
expect(formatDate(new Date('2020-03-03'))).to.equal('Tue Mar 3');
expect(formatDate(new Date('2020-03-15'))).to.equal('Sun Mar 15');
expect(formatDateVerbose(new Date('2020-03-03'))).to.equal('Tue Mar 3');
expect(formatDateVerbose(new Date('2020-03-15'))).to.equal('Sun Mar 15');
});
});

View File

@@ -1,8 +1,13 @@
import { it, describe } from 'mocha';
import { expect } from 'chai';
import {
tryNumify, slugify, formatSelectOptionsForRange, d3format,
d3FormatPreset, d3TimeFormatPreset, defaultNumberFormatter,
tryNumify,
slugify,
formatSelectOptionsForRange,
d3format,
d3FormatPreset,
d3TimeFormatPreset,
defaultNumberFormatter,
mainMetric,
} from '../../../src/modules/utils';
@@ -53,12 +58,13 @@ describe('utils', () => {
expect(d3FormatPreset('smart_date')(0)).to.equal('1970');
});
});
describe('d3TimeFormatPreset', () => {
describe('defaultNumberFormatter', () => {
expect(defaultNumberFormatter(10)).to.equal('10');
expect(defaultNumberFormatter(1)).to.equal('1');
expect(defaultNumberFormatter(1.0)).to.equal('1');
expect(defaultNumberFormatter(10.0)).to.equal('10');
expect(defaultNumberFormatter(10001)).to.equal('10.0k');
expect(defaultNumberFormatter(10100)).to.equal('10.1k');
expect(defaultNumberFormatter(111000000)).to.equal('111M');
expect(defaultNumberFormatter(0.23)).to.equal('230m');
@@ -67,6 +73,7 @@ describe('utils', () => {
expect(defaultNumberFormatter(-1.0)).to.equal('-1');
expect(defaultNumberFormatter(-10.0)).to.equal('-10');
expect(defaultNumberFormatter(-10001)).to.equal('-10.0k');
expect(defaultNumberFormatter(-10101)).to.equal('-10.1k');
expect(defaultNumberFormatter(-111000000)).to.equal('-111M');
expect(defaultNumberFormatter(-0.23)).to.equal('-230m');
});

View File

@@ -56,14 +56,14 @@ const D3_FORMAT_DOCS = 'D3 format syntax: https://github.com/d3/d3-format';
// input choices & options
const D3_FORMAT_OPTIONS = [
['.1s', '.1s | 12k'],
['.3s', '.3s | 12.3k'],
['.1%', '.1% | 12.3%'],
['.3%', '.3% | 1234543.210%'],
['.4r', '.4r | 12350'],
['.3f', '.3f | 12345.432'],
['+,', '+, | +12,345.4321'],
['$,.2f', '$,.2f | $12,345.43'],
['.1s', '.1s (12345.432 => 10k)'],
['.3s', '.3s (12345.432 => 12.3k)'],
[',.1%', ',.1% (12345.432 => 1,234,543.2%)'],
['.3%', '.3% (12345.432 => 1234543.200%)'],
['.4r', '.4r (12345.432 => 12350)'],
[',.3f', ',.3f (12345.432 => 12,345.432)'],
['+,', '+, (12345.432 => +12,345.432)'],
['$,.2f', '$,.2f (12345.432 => $12,345.43)'],
];
const ROW_LIMIT_OPTIONS = [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000];
@@ -1537,7 +1537,7 @@ export const controls = {
type: 'CheckboxControl',
label: t('Rich Tooltip'),
renderTrigger: true,
default: true,
default: false,
description: t('The rich tooltip shows a list of all series for that ' +
'point in time'),
},

View File

@@ -12,7 +12,40 @@ export function UTC(dttm) {
dttm.getUTCSeconds(),
);
}
export const tickMultiFormat = d3.time.format.multi([
export const tickMultiFormat = (() => {
const formatMillisecond = d3.time.format('.%Lms');
const formatSecond = d3.time.format(':%Ss');
const formatMinute = d3.time.format('%I:%M');
const formatHour = d3.time.format('%I %p');
const formatDay = d3.time.format('%a %d');
const formatWeek = d3.time.format('%b %d');
const formatMonth = d3.time.format('%B');
const formatYear = d3.time.format('%Y');
return function tickMultiFormatConcise(date) {
let formatter;
if (d3.time.second(date) < date) {
formatter = formatMillisecond;
} else if (d3.time.minute(date) < date) {
formatter = formatSecond;
} else if (d3.time.hour(date) < date) {
formatter = formatMinute;
} else if (d3.time.day(date) < date) {
formatter = formatHour;
} else if (d3.time.month(date) < date) {
formatter = d3.time.week(date) < date ? formatDay : formatWeek;
} else if (d3.time.year(date) < date) {
formatter = formatMonth;
} else {
formatter = formatYear;
}
return formatter(date);
};
})();
export const tickMultiFormatVerbose = d3.time.format.multi([
[
'.%L',
function (d) {
@@ -74,6 +107,11 @@ export const formatDate = function (dttm) {
return tickMultiFormat(d);
};
export const formatDateVerbose = function (dttm) {
const d = UTC(new Date(dttm));
return tickMultiFormatVerbose(d);
};
export const formatDateThunk = function (format) {
if (!format) {
return formatDate;

View File

@@ -13,6 +13,7 @@ import AnnotationTypes, {
applyNativeColumns,
} from '../modules/AnnotationTypes';
import { customizeToolTip, d3TimeFormatPreset, d3FormatPreset, tryNumify } from '../modules/utils';
import { formatDateVerbose } from '../modules/dates';
import { isTruthy } from '../utils/common';
import { t } from '../locales';
@@ -136,7 +137,7 @@ export default function nvd3Vis(slice, payload) {
};
const vizType = fd.viz_type;
const f = d3.format('.3s');
const formatter = d3.format('.3s');
const reduceXTicks = fd.reduce_x_ticks || false;
let stacked = false;
let row;
@@ -156,8 +157,6 @@ export default function nvd3Vis(slice, payload) {
if (fd.x_ticks_layout === 'auto') {
if (['column', 'dist_bar'].indexOf(vizType) >= 0) {
xLabelRotation = 45;
} else if (isTimeSeries) {
staggerLabels = true;
}
} else if (fd.x_ticks_layout === 'staggered') {
staggerLabels = true;
@@ -187,8 +186,6 @@ export default function nvd3Vis(slice, payload) {
} else {
chart = nv.models.lineChart();
}
// To alter the tooltip header
// chart.interactiveLayer.tooltip.headerFormatter(function(){return '';});
chart.xScale(d3.time.scale.utc());
chart.interpolate(fd.line_interpolation);
break;
@@ -303,9 +300,9 @@ export default function nvd3Vis(slice, payload) {
`<tr><td style="color: ${p.color};">` +
`<strong>${p[fd.entity]}</strong> (${p.group})` +
'</td></tr>');
s += row(fd.x, f(p.x));
s += row(fd.y, f(p.y));
s += row(fd.size, f(p.size));
s += row(fd.x, formatter(p.x));
s += row(fd.y, formatter(p.y));
s += row(fd.size, formatter(p.size));
s += '</table>';
return s;
});
@@ -375,6 +372,8 @@ export default function nvd3Vis(slice, payload) {
let xAxisFormatter = d3FormatPreset(fd.x_axis_format);
if (isTimeSeries) {
xAxisFormatter = d3TimeFormatPreset(fd.x_axis_format);
// In tooltips, always use the verbose time format
chart.interactiveLayer.tooltip.headerFormatter(formatDateVerbose);
}
if (chart.x2Axis && chart.x2Axis.tickFormat) {
chart.x2Axis.tickFormat(xAxisFormatter);
@@ -397,6 +396,13 @@ export default function nvd3Vis(slice, payload) {
chart.y2Axis.tickFormat(yAxisFormatter);
}
if (chart.yAxis) {
chart.yAxis.ticks(5);
}
if (chart.y2Axis) {
chart.y2Axis.ticks(5);
}
// Set showMaxMin for all axis
function setAxisShowMaxMin(axis, showminmax) {
@@ -404,10 +410,12 @@ export default function nvd3Vis(slice, payload) {
axis.showMaxMin(showminmax);
}
}
setAxisShowMaxMin(chart.xAxis, fd.x_axis_showminmax);
setAxisShowMaxMin(chart.x2Axis, fd.x_axis_showminmax);
setAxisShowMaxMin(chart.yAxis, fd.y_axis_showminmax);
setAxisShowMaxMin(chart.y2Axis, fd.y_axis_showminmax);
// If these are undefined, they register as truthy
setAxisShowMaxMin(chart.xAxis, fd.x_axis_showminmax || false);
setAxisShowMaxMin(chart.x2Axis, fd.x_axis_showminmax || false);
setAxisShowMaxMin(chart.yAxis, fd.y_axis_showminmax || false);
setAxisShowMaxMin(chart.y2Axis, fd.y_axis_showminmax || false);
if (vizType === 'time_pivot') {
chart.color((d) => {
@@ -425,10 +433,11 @@ export default function nvd3Vis(slice, payload) {
chart.useInteractiveGuideline(true);
if (vizType === 'line') {
// Custom sorted tooltip
// use a verbose formatter for times
chart.interactiveLayer.tooltip.contentGenerator((d) => {
let tooltip = '';
tooltip += "<table><thead><tr><td colspan='3'>"
+ `<strong class='x-value'>${xAxisFormatter(d.value)}</strong>`
+ `<strong class='x-value'>${formatDateVerbose(d.value)}</strong>`
+ '</td></tr></thead><tbody>';
d.series.sort((a, b) => a.value >= b.value ? -1 : 1);
d.series.forEach((series) => {

View File

@@ -43,8 +43,8 @@ function sunburstVis(slice, payload) {
return Math.sqrt(d.y + d.dy);
});
const formatNum = d3.format('.3s');
const formatPerc = d3.format('.3p');
const formatNum = d3.format('.1s');
const formatPerc = d3.format('.1p');
container.select('svg').remove();

View File

@@ -39,7 +39,7 @@ function worldMapChart(slice, payload) {
mapData[d.country] = d;
});
const f = d3.format('.3s');
const formatter = d3.format('.3s');
container.show();
@@ -58,7 +58,7 @@ function worldMapChart(slice, payload) {
highlightFillColor: '#005a63',
highlightBorderWidth: 1,
popupTemplate: (geo, d) => (
`<div class="hoverinfo"><strong>${d.name}</strong><br>${f(d.m1)}</div>`
`<div class="hoverinfo"><strong>${d.name}</strong><br>${formatter(d.m1)}</div>`
),
},
bubblesConfig: {
@@ -68,7 +68,7 @@ function worldMapChart(slice, payload) {
popupOnHover: true,
radius: null,
popupTemplate: (geo, d) => (
`<div class="hoverinfo"><strong>${d.name}</strong><br>${f(d.m2)}</div>`
`<div class="hoverinfo"><strong>${d.name}</strong><br>${formatter(d.m2)}</div>`
),
fillOpacity: 0.5,
animate: true,