This is an automated email from the ASF dual-hosted git repository. michellet pushed a commit to branch 0.30 in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
commit 8b2b4621feada9a7f7ca8861278fc8fa25d624a1 Author: Krist Wongsuphasawat <[email protected]> AuthorDate: Tue Dec 4 13:24:07 2018 -0800 Use @superset-ui/number-format and @superset-ui/time-format for formatting. (#6470) refactor: proxy all d3 number and time formatting calls (cherry picked from commit fcec748b62b333fbfbba23c6d49cdf98f7d6a280) --- superset/assets/package.json | 2 + .../assets/spec/javascripts/modules/dates_spec.js | 52 +--------- .../assets/spec/javascripts/modules/utils_spec.jsx | 52 ---------- .../javascripts/visualizations/nvd3/utils_spec.js | 26 ++++- .../src/explore/components/RowCountLabel.jsx | 5 +- superset/assets/src/modules/dates.js | 112 --------------------- superset/assets/src/modules/utils.js | 64 ------------ superset/assets/src/preamble.js | 4 + superset/assets/src/setup/setupFormatters.js | 35 +++++++ .../src/visualizations/BigNumber/BigNumber.jsx | 4 +- .../src/visualizations/BigNumber/transformProps.js | 7 +- .../assets/src/visualizations/Calendar/Calendar.js | 8 +- superset/assets/src/visualizations/Chord/Chord.js | 3 +- .../src/visualizations/CountryMap/CountryMap.js | 4 +- .../assets/src/visualizations/Heatmap/Heatmap.js | 5 +- .../src/visualizations/Partition/Partition.js | 7 +- .../src/visualizations/PivotTable/PivotTable.js | 5 +- superset/assets/src/visualizations/Rose/Rose.js | 7 +- .../assets/src/visualizations/Sankey/Sankey.js | 3 +- .../assets/src/visualizations/Sunburst/Sunburst.js | 5 +- superset/assets/src/visualizations/Table/Table.js | 13 +-- .../visualizations/TimeTable/FormattedNumber.jsx | 4 +- .../src/visualizations/TimeTable/SparklineCell.jsx | 6 +- .../src/visualizations/TimeTable/TimeTable.jsx | 10 +- .../assets/src/visualizations/Treemap/Treemap.js | 3 +- .../assets/src/visualizations/WorldMap/WorldMap.js | 3 +- superset/assets/src/visualizations/nvd3/NVD3Vis.js | 27 ++--- superset/assets/src/visualizations/nvd3/utils.js | 15 ++- superset/assets/yarn.lock | 17 +++- 29 files changed, 162 insertions(+), 346 deletions(-) diff --git a/superset/assets/package.json b/superset/assets/package.json index f1940c7..466ad3e 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -55,6 +55,8 @@ "@superset-ui/color": "^0.7.0", "@superset-ui/connection": "^0.5.0", "@superset-ui/core": "^0.7.0", + "@superset-ui/number-format": "^0.7.2", + "@superset-ui/time-format": "^0.7.2", "@superset-ui/translation": "^0.7.0", "@vx/legend": "^0.0.170", "@vx/responsive": "0.0.172", diff --git a/superset/assets/spec/javascripts/modules/dates_spec.js b/superset/assets/spec/javascripts/modules/dates_spec.js index 3f39343..d9f97e7 100644 --- a/superset/assets/spec/javascripts/modules/dates_spec.js +++ b/superset/assets/spec/javascripts/modules/dates_spec.js @@ -1,60 +1,10 @@ import { - tickMultiFormat, - formatDate, - formatDateVerbose, fDuration, now, epochTimeXHoursAgo, epochTimeXDaysAgo, epochTimeXYearsAgo, - } from '../../../src/modules/dates'; - -describe('tickMultiFormat', () => { - it('is a function', () => { - expect(typeof tickMultiFormat).toBe('function'); - }); -}); - -describe('formatDate', () => { - it('is a function', () => { - expect(typeof formatDate).toBe('function'); - }); - - it('shows only year when 1st day of the year', () => { - expect(formatDate(new Date('2020-01-01'))).toBe('2020'); - }); - - it('shows only month when 1st of month', () => { - expect(formatDate(new Date('2020-03-01'))).toBe('March'); - }); - - it('does not show day of week when it is Sunday', () => { - expect(formatDate(new Date('2020-03-15'))).toBe('Mar 15'); - }); - - it('shows weekday when it is not Sunday (and no ms/sec/min/hr)', () => { - expect(formatDate(new Date('2020-03-03'))).toBe('Tue 03'); - }); -}); - -describe('formatDateVerbose', () => { - it('is a function', () => { - expect(typeof formatDateVerbose).toBe('function'); - }); - - it('shows only year when 1st day of the year', () => { - expect(formatDateVerbose(new Date('2020-01-01'))).toBe('2020'); - }); - - it('shows month and year when 1st of month', () => { - expect(formatDateVerbose(new Date('2020-03-01'))).toBe('Mar 2020'); - }); - - it('shows weekday when any day of the month', () => { - expect(formatDateVerbose(new Date('2020-03-03'))).toBe('Tue Mar 3'); - expect(formatDateVerbose(new Date('2020-03-15'))).toBe('Sun Mar 15'); - }); -}); +} from '../../../src/modules/dates'; describe('fDuration', () => { it('is a function', () => { diff --git a/superset/assets/spec/javascripts/modules/utils_spec.jsx b/superset/assets/spec/javascripts/modules/utils_spec.jsx index ca16d86..79a2044 100644 --- a/superset/assets/spec/javascripts/modules/utils_spec.jsx +++ b/superset/assets/spec/javascripts/modules/utils_spec.jsx @@ -1,9 +1,5 @@ import { formatSelectOptionsForRange, - d3format, - d3FormatPreset, - d3TimeFormatPreset, - defaultNumberFormatter, mainMetric, roundDecimal, } from '../../../src/modules/utils'; @@ -25,54 +21,6 @@ describe('utils', () => { }); }); - describe('d3format', () => { - it('returns a string formatted number as specified', () => { - expect(d3format('.3s', 1234)).toBe('1.23k'); - expect(d3format('.3s', 1237)).toBe('1.24k'); - expect(d3format('', 1237)).toBe('1.24k'); - expect(d3format('.2efd2.ef.2.e', 1237)).toBe('1237 (Invalid format: .2efd2.ef.2.e)'); - }); - }); - - describe('d3FormatPreset', () => { - it('is a function', () => { - expect(typeof d3FormatPreset).toBe('function'); - }); - it('returns a working formatter', () => { - expect(d3FormatPreset('.3s')(3000000)).toBe('3.00M'); - }); - }); - - describe('d3TimeFormatPreset', () => { - it('is a function', () => { - expect(typeof d3TimeFormatPreset).toBe('function'); - }); - it('returns a working formatter', () => { - expect(d3FormatPreset('smart_date')(0)).toBe('1970'); - expect(d3FormatPreset('%%GIBBERISH')(0)).toBe('0 (Invalid format: %%GIBBERISH)'); - }); - }); - - describe('defaultNumberFormatter', () => { - expect(defaultNumberFormatter(10)).toBe('10'); - expect(defaultNumberFormatter(1)).toBe('1'); - expect(defaultNumberFormatter(1.0)).toBe('1'); - expect(defaultNumberFormatter(10.0)).toBe('10'); - expect(defaultNumberFormatter(10001)).toBe('10.0k'); - expect(defaultNumberFormatter(10100)).toBe('10.1k'); - expect(defaultNumberFormatter(111000000)).toBe('111M'); - expect(defaultNumberFormatter(0.23)).toBe('230m'); - - expect(defaultNumberFormatter(-10)).toBe('-10'); - expect(defaultNumberFormatter(-1)).toBe('-1'); - expect(defaultNumberFormatter(-1.0)).toBe('-1'); - expect(defaultNumberFormatter(-10.0)).toBe('-10'); - expect(defaultNumberFormatter(-10001)).toBe('-10.0k'); - expect(defaultNumberFormatter(-10101)).toBe('-10.1k'); - expect(defaultNumberFormatter(-111000000)).toBe('-111M'); - expect(defaultNumberFormatter(-0.23)).toBe('-230m'); - }); - describe('mainMetric', () => { it('is null when no options', () => { expect(mainMetric([])).toBeUndefined(); diff --git a/superset/assets/spec/javascripts/visualizations/nvd3/utils_spec.js b/superset/assets/spec/javascripts/visualizations/nvd3/utils_spec.js index 43a824e..623be1f 100644 --- a/superset/assets/spec/javascripts/visualizations/nvd3/utils_spec.js +++ b/superset/assets/spec/javascripts/visualizations/nvd3/utils_spec.js @@ -1,11 +1,26 @@ -import { formatLabel, tryNumify } from '../../../../src/visualizations/nvd3/utils'; +import { getTimeOrNumberFormatter, formatLabel, tryNumify } from '../../../../src/visualizations/nvd3/utils'; describe('nvd3/utils', () => { - const verboseMap = { - foo: 'Foo', - bar: 'Bar', - }; + describe('getTimeOrNumberFormatter(format)', () => { + it('is a function', () => { + expect(typeof getTimeOrNumberFormatter).toBe('function'); + }); + it('returns a date formatter if format is smart_date', () => { + const time = new Date(Date.UTC(2018, 10, 21, 22, 11)); + expect(getTimeOrNumberFormatter('smart_date')(time)).toBe('10:11'); + }); + it('returns a number formatter otherwise', () => { + expect(getTimeOrNumberFormatter('.3s')(3000000)).toBe('3.00M'); + expect(getTimeOrNumberFormatter()(3000100)).toBe('3.00M'); + }); + }); + describe('formatLabel()', () => { + const verboseMap = { + foo: 'Foo', + bar: 'Bar', + }; + it('formats simple labels', () => { expect(formatLabel('foo')).toBe('foo'); expect(formatLabel(['foo'])).toBe('foo'); @@ -22,6 +37,7 @@ describe('nvd3/utils', () => { expect(formatLabel(['foo', 'bar', 'baz', '2 hours offset'], verboseMap)).toBe('Foo, Bar, baz, 2 hours offset'); }); }); + describe('tryNumify()', () => { it('tryNumify works as expected', () => { expect(tryNumify(5)).toBe(5); diff --git a/superset/assets/src/explore/components/RowCountLabel.jsx b/superset/assets/src/explore/components/RowCountLabel.jsx index aea9bc4..2eeb80d 100644 --- a/superset/assets/src/explore/components/RowCountLabel.jsx +++ b/superset/assets/src/explore/components/RowCountLabel.jsx @@ -1,12 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Label } from 'react-bootstrap'; +import { getNumberFormatter } from '@superset-ui/number-format'; import { t } from '@superset-ui/translation'; -import { defaultNumberFormatter } from '../../modules/utils'; import TooltipWrapper from '../../components/TooltipWrapper'; - const propTypes = { rowcount: PropTypes.number, limit: PropTypes.number, @@ -21,7 +20,7 @@ const defaultProps = { export default function RowCountLabel({ rowcount, limit, suffix }) { const limitReached = rowcount === limit; const bsStyle = (limitReached || rowcount === 0) ? 'danger' : 'default'; - const formattedRowCount = defaultNumberFormatter(rowcount); + const formattedRowCount = getNumberFormatter()(rowcount); const tooltip = ( <span> {limitReached && diff --git a/superset/assets/src/modules/dates.js b/superset/assets/src/modules/dates.js index 311340b..f5063b0 100644 --- a/superset/assets/src/modules/dates.js +++ b/superset/assets/src/modules/dates.js @@ -1,4 +1,3 @@ -import d3 from 'd3'; import moment from 'moment'; export function UTC(dttm) { @@ -12,117 +11,6 @@ export function UTC(dttm) { ); } -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) { - return d.getMilliseconds(); - }, - ], - // If there are millisections, show only them - [ - ':%S', - function (d) { - return d.getSeconds(); - }, - ], - // If there are seconds, show only them - [ - '%a %b %d, %I:%M %p', - function (d) { - return d.getMinutes() !== 0; - }, - ], - // If there are non-zero minutes, show Date, Hour:Minute [AM/PM] - [ - '%a %b %d, %I %p', - function (d) { - return d.getHours() !== 0; - }, - ], - // If there are hours that are multiples of 3, show date and AM/PM - [ - '%a %b %e', - function (d) { - return d.getDate() >= 10; - }, - ], - // If not the first of the month: "Tue Mar 2" - [ - '%a %b%e', - function (d) { - return d.getDate() > 1; - }, - ], - // If >= 10th of the month, compensate for padding : "Sun Mar 15" - [ - '%b %Y', - function (d) { - return d.getMonth() !== 0 && d.getDate() === 1; - }, - ], - // If the first of the month: 'Mar 2020' - [ - '%Y', - function () { - return true; - }, - ], // fall back on just year: '2020' -]); -export const formatDate = function (dttm) { - const d = UTC(new Date(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 formatDateVerbose; - } - - 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)); diff --git a/superset/assets/src/modules/utils.js b/superset/assets/src/modules/utils.js index 9fbd1c8..11d2eb3 100644 --- a/superset/assets/src/modules/utils.js +++ b/superset/assets/src/modules/utils.js @@ -1,70 +1,6 @@ /* eslint camelcase: 0 */ import $ from 'jquery'; -import { format as d3Format } from 'd3-format'; import { select as d3Select } from 'd3-selection'; -import { timeFormat as d3TimeFormat } from 'd3-time-format'; -import { formatDate, UTC } from './dates'; - -const siFormatter = d3Format('.3s'); - -export function defaultNumberFormatter(n) { - let si = siFormatter(n); - // Removing trailing `.00` if any - if (si.slice(-1) < 'A') { - si = parseFloat(si).toString(); - } - return si; -} - -export function d3FormatPreset(format) { - // like d3Format, but with support for presets like 'smart_date' - if (format === 'smart_date') { - return formatDate; - } - if (format) { - try { - return d3Format(format); - } catch (e) { - // eslint-disable-next-line no-console - console.warn(e); - return value => `${value} (Invalid format: ${format})`; - } - } - return defaultNumberFormatter; -} - -export const d3TimeFormatPreset = function (format) { - const effFormat = format || 'smart_date'; - if (effFormat === 'smart_date') { - return formatDate; - } - const f = d3TimeFormat(effFormat); - return function (dttm) { - const d = UTC(new Date(dttm)); - return f(d); - }; -}; - -const formatters = {}; - -export function d3format(format, number) { - format = format || '.3s'; - // Formats a number and memoizes formatters to be reused - if (!(format in formatters)) { - try { - formatters[format] = d3Format(format); - } catch (e) { - // eslint-disable-next-line no-console - console.warn(e); - return `${number} (Invalid format: ${format})`; - } - } - try { - return formatters[format](number); - } catch (e) { - return `${number} (Invalid format: ${format})`; - } -} /* Utility function that takes a d3 svg:text selection and a max width, and splits the diff --git a/superset/assets/src/preamble.js b/superset/assets/src/preamble.js index 9d80dc9..3e96e60 100644 --- a/superset/assets/src/preamble.js +++ b/superset/assets/src/preamble.js @@ -2,6 +2,7 @@ import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'; import { configure } from '@superset-ui/translation'; import setupClient from './setup/setupClient'; import setupColors from './setup/setupColors'; +import setupFormatters from './setup/setupFormatters'; // Configure translation if (typeof window !== 'undefined') { @@ -22,3 +23,6 @@ setupClient(); // Setup color palettes setupColors(); + +// Setup number formatters +setupFormatters(); diff --git a/superset/assets/src/setup/setupFormatters.js b/superset/assets/src/setup/setupFormatters.js new file mode 100644 index 0000000..dd1f39f --- /dev/null +++ b/superset/assets/src/setup/setupFormatters.js @@ -0,0 +1,35 @@ +import { getNumberFormatter, getNumberFormatterRegistry, createSiAtMostNDigitFormatter, NumberFormats } from '@superset-ui/number-format'; +import { getTimeFormatterRegistry, smartDateFormatter, smartDateVerboseFormatter } from '@superset-ui/time-format'; + +export default function setupFormatters() { + const defaultNumberFormatter = createSiAtMostNDigitFormatter({ n: 3 }); + + getNumberFormatterRegistry() + .registerValue(defaultNumberFormatter.id, defaultNumberFormatter) + .setDefaultKey(defaultNumberFormatter.id) + // Add shims for format strings that are deprecated or common typos. + // Temporary solution until performing a db migration to fix this. + .registerValue('+,', getNumberFormatter(NumberFormats.INTEGER_CHANGE)) + .registerValue(',0', getNumberFormatter(',.4~f')) + .registerValue('.', getNumberFormatter('.4~f')) + .registerValue(',#', getNumberFormatter(',.4~f')) + .registerValue(',2f', getNumberFormatter(',.4~f')) + .registerValue(',g', getNumberFormatter(',.4~f')) + .registerValue('int', getNumberFormatter(NumberFormats.INTEGER)) + .registerValue(',.', getNumberFormatter(',.4~f')) + .registerValue('.0%f', getNumberFormatter('.1%')) + .registerValue('.1%f', getNumberFormatter('.1%')) + .registerValue('.r', getNumberFormatter('.4~f')) + .registerValue(',0s', getNumberFormatter(',.4~f')) + .registerValue('%%%', getNumberFormatter('.0%')) + .registerValue(',0f', getNumberFormatter(',.4~f')) + .registerValue(',1', getNumberFormatter(',.4~f')) + .registerValue('$,0', getNumberFormatter('$,.4f')) + .registerValue('$,0f', getNumberFormatter('$,.4f')) + .registerValue('$,.f', getNumberFormatter('$,.4f')); + + getTimeFormatterRegistry() + .registerValue('smart_date', smartDateFormatter) + .registerValue('smart_date_verbose', smartDateVerboseFormatter) + .setDefaultKey('smart_date'); +} diff --git a/superset/assets/src/visualizations/BigNumber/BigNumber.jsx b/superset/assets/src/visualizations/BigNumber/BigNumber.jsx index 9c80e04..b6c79e6 100644 --- a/superset/assets/src/visualizations/BigNumber/BigNumber.jsx +++ b/superset/assets/src/visualizations/BigNumber/BigNumber.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import shortid from 'shortid'; import { XYChart, AreaSeries, CrossHair, LinearGradient } from '@data-ui/xy-chart'; import { BRAND_COLOR } from '@superset-ui/color'; -import { formatDateVerbose } from '../../modules/dates'; +import { smartDateVerboseFormatter } from '@superset-ui/time-format'; import { computeMaxFontSize } from '../../modules/visUtils'; import './BigNumber.css'; @@ -26,7 +26,7 @@ const PROPORTION = { export function renderTooltipFactory(formatValue) { return function renderTooltip({ datum }) { // eslint-disable-line const { x: rawDate, y: rawValue } = datum; - const formattedDate = formatDateVerbose(rawDate); + const formattedDate = smartDateVerboseFormatter(rawDate); const value = formatValue(rawValue); return ( diff --git a/superset/assets/src/visualizations/BigNumber/transformProps.js b/superset/assets/src/visualizations/BigNumber/transformProps.js index 92a88f3..cb5dcc2 100644 --- a/superset/assets/src/visualizations/BigNumber/transformProps.js +++ b/superset/assets/src/visualizations/BigNumber/transformProps.js @@ -1,6 +1,5 @@ import * as color from 'd3-color'; -import { format as d3Format } from 'd3-format'; -import { d3FormatPreset } from '../../modules/utils'; +import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format'; import { renderTooltipFactory } from './BigNumber'; const TIME_COLUMN = '__timestamp'; @@ -43,7 +42,7 @@ export default function transformProps(chartProps) { const compareValue = sortedData[compareIndex][metricName]; percentChange = compareValue === 0 ? 0 : (bigNumber - compareValue) / Math.abs(compareValue); - const formatPercentChange = d3Format('+.1%'); + const formatPercentChange = getNumberFormatter(NumberFormats.PERCENT_CHANGE_1_POINT); formattedSubheader = `${formatPercentChange(percentChange)} ${compareSuffix}`; } } @@ -62,7 +61,7 @@ export default function transformProps(chartProps) { className = 'negative'; } - const formatValue = d3FormatPreset(yAxisFormat); + const formatValue = getNumberFormatter(yAxisFormat); return { width, diff --git a/superset/assets/src/visualizations/Calendar/Calendar.js b/superset/assets/src/visualizations/Calendar/Calendar.js index 6b7aada..0dd3944 100644 --- a/superset/assets/src/visualizations/Calendar/Calendar.js +++ b/superset/assets/src/visualizations/Calendar/Calendar.js @@ -2,8 +2,10 @@ import PropTypes from 'prop-types'; import { extent as d3Extent, range as d3Range } from 'd3-array'; import { select as d3Select } from 'd3-selection'; import { getSequentialSchemeRegistry } from '@superset-ui/color'; +import { getNumberFormatter } from '@superset-ui/number-format'; +import { getTimeFormatter } from '@superset-ui/time-format'; import CalHeatMap from '../../../vendor/cal-heatmap/cal-heatmap'; -import { d3TimeFormatPreset, d3FormatPreset } from '../../modules/utils'; import { UTC } from '../../modules/dates'; +import { UTC } from '../../modules/dates'; import '../../../vendor/cal-heatmap/cal-heatmap.css'; import './Calendar.css'; @@ -53,8 +55,8 @@ function Calendar(element, props) { verboseMap, } = props; - const valueFormatter = d3FormatPreset(valueFormat); - const timeFormatter = d3TimeFormatPreset(timeFormat); + const valueFormatter = getNumberFormatter(valueFormat); + const timeFormatter = getTimeFormatter(timeFormat); const container = d3Select(element) .style('height', height); diff --git a/superset/assets/src/visualizations/Chord/Chord.js b/superset/assets/src/visualizations/Chord/Chord.js index 05d416e..84e399f 100644 --- a/superset/assets/src/visualizations/Chord/Chord.js +++ b/superset/assets/src/visualizations/Chord/Chord.js @@ -2,6 +2,7 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import { CategoricalColorNamespace } from '@superset-ui/color'; +import { getNumberFormatter } from '@superset-ui/number-format'; import './Chord.css'; const propTypes = { @@ -28,7 +29,7 @@ function Chord(element, props) { const div = d3.select(element); const { nodes, matrix } = data; - const f = d3.format(numberFormat); + const f = getNumberFormatter(numberFormat); const colorFn = CategoricalColorNamespace.getScale(colorScheme); const outerRadius = Math.min(width, height) / 2 - 10; diff --git a/superset/assets/src/visualizations/CountryMap/CountryMap.js b/superset/assets/src/visualizations/CountryMap/CountryMap.js index ff22bcf..830ff28 100644 --- a/superset/assets/src/visualizations/CountryMap/CountryMap.js +++ b/superset/assets/src/visualizations/CountryMap/CountryMap.js @@ -1,8 +1,8 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import { extent as d3Extent } from 'd3-array'; -import { format as d3Format } from 'd3-format'; import { getSequentialSchemeRegistry } from '@superset-ui/color'; +import { getNumberFormatter } from '@superset-ui/number-format'; import './CountryMap.css'; const propTypes = { @@ -32,7 +32,7 @@ function CountryMap(element, props) { } = props; const container = element; - const format = d3Format(numberFormat); + const format = getNumberFormatter(numberFormat); const colorScale = getSequentialSchemeRegistry() .get(linearColorScheme) .createLinearScale(d3Extent(data, v => v.metric)); diff --git a/superset/assets/src/visualizations/Heatmap/Heatmap.js b/superset/assets/src/visualizations/Heatmap/Heatmap.js index 4c8f6aa..3e7d7d7 100644 --- a/superset/assets/src/visualizations/Heatmap/Heatmap.js +++ b/superset/assets/src/visualizations/Heatmap/Heatmap.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import 'd3-svg-legend'; import d3tip from 'd3-tip'; import { getSequentialSchemeRegistry } from '@superset-ui/color'; +import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format'; import '../../../stylesheets/d3tip.css'; import './Heatmap.css'; @@ -85,7 +86,7 @@ function Heatmap(element, props) { bottom: 35, left: 35, }; - const valueFormatter = d3.format(numberFormat); + const valueFormatter = getNumberFormatter(numberFormat); // Dynamically adjusts based on max x / y category lengths function adjustMargins() { @@ -152,7 +153,7 @@ function Heatmap(element, props) { const hmWidth = width - (margin.left + margin.right); const hmHeight = height - (margin.bottom + margin.top); - const fp = d3.format('.2%'); + const fp = getNumberFormatter(NumberFormats.PERCENT); const xScale = ordScale('x', null, sortXAxis); const yScale = ordScale('y', null, sortYAxis); diff --git a/superset/assets/src/visualizations/Partition/Partition.js b/superset/assets/src/visualizations/Partition/Partition.js index 539c024..dbb2e42 100644 --- a/superset/assets/src/visualizations/Partition/Partition.js +++ b/superset/assets/src/visualizations/Partition/Partition.js @@ -3,7 +3,8 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import { hierarchy } from 'd3-hierarchy'; import { CategoricalColorNamespace } from '@superset-ui/color'; -import { d3TimeFormatPreset } from '../../modules/utils'; +import { getNumberFormatter } from '@superset-ui/number-format'; +import { getTimeFormatter } from '@superset-ui/time-format'; import './Partition.css'; // Compute dx, dy, x, y for each node and @@ -93,8 +94,8 @@ function Icicle(element, props) { // Chart options const chartType = timeSeriesOption; const hasTime = ['adv_anal', 'time_series'].indexOf(chartType) >= 0; - const format = d3.format(numberFormat); - const timeFormat = d3TimeFormatPreset(dateTimeFormat); + const format = getNumberFormatter(numberFormat); + const timeFormat = getTimeFormatter(dateTimeFormat); const colorFn = CategoricalColorNamespace.getScale(colorScheme); div.selectAll('*').remove(); diff --git a/superset/assets/src/visualizations/PivotTable/PivotTable.js b/superset/assets/src/visualizations/PivotTable/PivotTable.js index 71d0cfa..b5326bc 100644 --- a/superset/assets/src/visualizations/PivotTable/PivotTable.js +++ b/superset/assets/src/visualizations/PivotTable/PivotTable.js @@ -2,7 +2,8 @@ import dt from 'datatables.net-bs'; import 'datatables.net-bs/css/dataTables.bootstrap.css'; import $ from 'jquery'; import PropTypes from 'prop-types'; -import { d3format, fixDataTableBodyHeight } from '../../modules/utils'; +import { formatNumber } from '@superset-ui/number-format'; +import { fixDataTableBodyHeight } from '../../modules/utils'; import './PivotTable.css'; dt(window, $); @@ -59,7 +60,7 @@ function PivotTable(element, props) { const format = columnFormats[metric] || numberFormat || '.3s'; const tdText = $(this)[0].textContent; if (!Number.isNaN(tdText) && tdText !== '') { - $(this)[0].textContent = d3format(format, tdText); + $(this)[0].textContent = formatNumber(format, tdText); $(this).attr('data-sort', tdText); } }); diff --git a/superset/assets/src/visualizations/Rose/Rose.js b/superset/assets/src/visualizations/Rose/Rose.js index 097c918..a99d03f 100644 --- a/superset/assets/src/visualizations/Rose/Rose.js +++ b/superset/assets/src/visualizations/Rose/Rose.js @@ -3,7 +3,8 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import nv from 'nvd3'; import { CategoricalColorNamespace } from '@superset-ui/color'; -import { d3TimeFormatPreset } from '../../modules/utils'; +import { getNumberFormatter } from '@superset-ui/number-format'; +import { getTimeFormatter } from '@superset-ui/time-format'; import './Rose.css'; const propTypes = { @@ -58,8 +59,8 @@ function Rose(element, props) { .sort((a, b) => a - b); const numGrains = times.length; const numGroups = datum[times[0]].length; - const format = d3.format(numberFormat); - const timeFormat = d3TimeFormatPreset(dateTimeFormat); + const format = getNumberFormatter(numberFormat); + const timeFormat = getTimeFormatter(dateTimeFormat); const colorFn = CategoricalColorNamespace.getScale(colorScheme); d3.select('.nvtooltip').remove(); diff --git a/superset/assets/src/visualizations/Sankey/Sankey.js b/superset/assets/src/visualizations/Sankey/Sankey.js index f80d032..5e4c6eb 100644 --- a/superset/assets/src/visualizations/Sankey/Sankey.js +++ b/superset/assets/src/visualizations/Sankey/Sankey.js @@ -3,6 +3,7 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import { sankey as d3Sankey } from 'd3-sankey'; import { CategoricalColorNamespace } from '@superset-ui/color'; +import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format'; import './Sankey.css'; const propTypes = { @@ -16,7 +17,7 @@ const propTypes = { colorScheme: PropTypes.string, }; -const formatNumber = d3.format(',.2f'); +const formatNumber = getNumberFormatter(NumberFormats.FLOAT); function Sankey(element, props) { const { diff --git a/superset/assets/src/visualizations/Sunburst/Sunburst.js b/superset/assets/src/visualizations/Sunburst/Sunburst.js index 29496a6..5e13c19 100644 --- a/superset/assets/src/visualizations/Sunburst/Sunburst.js +++ b/superset/assets/src/visualizations/Sunburst/Sunburst.js @@ -2,6 +2,7 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import { CategoricalColorNamespace } from '@superset-ui/color'; +import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format'; import { wrapSvgText } from '../../modules/utils'; import './Sunburst.css'; @@ -79,8 +80,8 @@ function Sunburst(element, props) { .innerRadius(d => Math.sqrt(d.y)) .outerRadius(d => Math.sqrt(d.y + d.dy)); - const formatNum = d3.format('.3s'); - const formatPerc = d3.format('.3p'); + const formatNum = getNumberFormatter(NumberFormats.SI_3_DIGIT); + const formatPerc = getNumberFormatter(NumberFormats.PERCENT_3_POINT); container.select('svg').remove(); diff --git a/superset/assets/src/visualizations/Table/Table.js b/superset/assets/src/visualizations/Table/Table.js index 7056235..23cdc07 100644 --- a/superset/assets/src/visualizations/Table/Table.js +++ b/superset/assets/src/visualizations/Table/Table.js @@ -4,8 +4,9 @@ import PropTypes from 'prop-types'; import dt from 'datatables.net-bs'; import 'datatables.net-bs/css/dataTables.bootstrap.css'; import dompurify from 'dompurify'; -import { format as d3Format } from 'd3-format'; -import { fixDataTableBodyHeight, d3TimeFormatPreset } from '../../modules/utils'; +import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format'; +import { getTimeFormatter } from '@superset-ui/time-format'; +import { fixDataTableBodyHeight } from '../../modules/utils'; import './Table.css'; dt(window, $); @@ -46,8 +47,8 @@ const propTypes = { ]), }; -const formatValue = d3Format(',.0d'); -const formatPercent = d3Format('.3p'); +const formatValue = getNumberFormatter(NumberFormats.INTEGER); +const formatPercent = getNumberFormatter(NumberFormats.PERCENT_3_POINT); function NOOP() {} function TableVis(element, props) { @@ -96,7 +97,7 @@ function TableVis(element, props) { } } - const tsFormatter = d3TimeFormatPreset(tableTimestampFormat); + const tsFormatter = getTimeFormatter(tableTimestampFormat); const div = d3.select(element); div.html(''); @@ -130,7 +131,7 @@ function TableVis(element, props) { html = `<span class="like-pre">${dompurify.sanitize(val)}</span>`; } if (isMetric) { - html = d3Format(format || '0.3s')(val); + html = getNumberFormatter(format)(val); } if (key[0] === '%') { html = formatPercent(val); diff --git a/superset/assets/src/visualizations/TimeTable/FormattedNumber.jsx b/superset/assets/src/visualizations/TimeTable/FormattedNumber.jsx index eabbb0e..a4751e5 100644 --- a/superset/assets/src/visualizations/TimeTable/FormattedNumber.jsx +++ b/superset/assets/src/visualizations/TimeTable/FormattedNumber.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { d3format } from '../../modules/utils'; +import { formatNumber } from '@superset-ui/number-format'; const propTypes = { num: PropTypes.number, @@ -15,7 +15,7 @@ const defaultProps = { function FormattedNumber({ num, format }) { if (format) { return ( - <span title={num}>{d3format(format, num)}</span> + <span title={num}>{formatNumber(format, num)}</span> ); } return <span>{num}</span>; diff --git a/superset/assets/src/visualizations/TimeTable/SparklineCell.jsx b/superset/assets/src/visualizations/TimeTable/SparklineCell.jsx index 1a49e35..bc58c10 100644 --- a/superset/assets/src/visualizations/TimeTable/SparklineCell.jsx +++ b/superset/assets/src/visualizations/TimeTable/SparklineCell.jsx @@ -1,7 +1,7 @@ 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 { formatNumber } from '@superset-ui/number-format'; import { getTextDimension } from '../../modules/visUtils'; const propTypes = { @@ -110,8 +110,8 @@ class SparklineCell extends React.Component { ? maxBound : data.reduce((acc, current) => Math.max(acc, current), data[0]); - minLabel = d3format(numberFormat, min); - maxLabel = d3format(numberFormat, max); + minLabel = formatNumber(numberFormat, min); + maxLabel = formatNumber(numberFormat, max); labelLength = Math.max( getSparklineTextWidth(minLabel), getSparklineTextWidth(maxLabel), diff --git a/superset/assets/src/visualizations/TimeTable/TimeTable.jsx b/superset/assets/src/visualizations/TimeTable/TimeTable.jsx index 38bf058..667b377 100644 --- a/superset/assets/src/visualizations/TimeTable/TimeTable.jsx +++ b/superset/assets/src/visualizations/TimeTable/TimeTable.jsx @@ -3,10 +3,10 @@ import PropTypes from 'prop-types'; import Mustache from 'mustache'; import { scaleLinear } from 'd3-scale'; import { Table, Thead, Th, Tr, Td } from 'reactable'; +import { formatNumber } from '@superset-ui/number-format'; +import { formatTime } from '@superset-ui/time-format'; import MetricOption from '../../components/MetricOption'; -import { formatDateThunk } from '../../modules/dates'; -import { d3format } from '../../modules/utils'; import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger'; import FormattedNumber from './FormattedNumber'; import SparklineCell from './SparklineCell'; @@ -113,8 +113,6 @@ class TimeTable extends React.PureComponent { sparkData = entries.map(d => d[valueField]); } - const formatDate = formatDateThunk(column.dateFormat); - return ( <Td column={column.key} @@ -131,8 +129,8 @@ class TimeTable extends React.PureComponent { showYAxis={column.showYAxis} renderTooltip={({ index }) => ( <div> - <strong>{d3format(column.d3format, sparkData[index])}</strong> - <div>{formatDate(entries[index].time)}</div> + <strong>{formatNumber(column.d3format, sparkData[index])}</strong> + <div>{formatTime(column.dateFormat, entries[index].time)}</div> </div> )} /> diff --git a/superset/assets/src/visualizations/Treemap/Treemap.js b/superset/assets/src/visualizations/Treemap/Treemap.js index 17669d6..7ac0e1b 100644 --- a/superset/assets/src/visualizations/Treemap/Treemap.js +++ b/superset/assets/src/visualizations/Treemap/Treemap.js @@ -2,6 +2,7 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import { CategoricalColorNamespace } from '@superset-ui/color'; +import { getNumberFormatter } from '@superset-ui/number-format'; import './Treemap.css'; // Declare PropTypes for recursive data structures @@ -67,7 +68,7 @@ function Treemap(element, props) { treemapRatio, } = props; const div = d3.select(element); - const formatNumber = d3.format(numberFormat); + const formatNumber = getNumberFormatter(numberFormat); const colorFn = CategoricalColorNamespace.getScale(colorScheme); const data = clone(rawData); diff --git a/superset/assets/src/visualizations/WorldMap/WorldMap.js b/superset/assets/src/visualizations/WorldMap/WorldMap.js index b2cb2c0..91f654b 100644 --- a/superset/assets/src/visualizations/WorldMap/WorldMap.js +++ b/superset/assets/src/visualizations/WorldMap/WorldMap.js @@ -1,6 +1,7 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import Datamap from 'datamaps/dist/datamaps.world.min'; +import { getNumberFormatter } from '@superset-ui/number-format'; import './WorldMap.css'; const propTypes = { @@ -17,7 +18,7 @@ const propTypes = { showBubbles: PropTypes.bool, }; -const formatter = d3.format('.3s'); +const formatter = getNumberFormatter(); function WorldMap(element, props) { const { diff --git a/superset/assets/src/visualizations/nvd3/NVD3Vis.js b/superset/assets/src/visualizations/nvd3/NVD3Vis.js index 526406b..5fbfed0 100644 --- a/superset/assets/src/visualizations/nvd3/NVD3Vis.js +++ b/superset/assets/src/visualizations/nvd3/NVD3Vis.js @@ -6,11 +6,11 @@ import moment from 'moment'; import PropTypes from 'prop-types'; import { t } from '@superset-ui/translation'; import { CategoricalColorNamespace } from '@superset-ui/color'; +import { getNumberFormatter, formatNumber, NumberFormats } from '@superset-ui/number-format'; +import { getTimeFormatter, smartDateVerboseFormatter } from '@superset-ui/time-format'; import 'nvd3/build/nv.d3.min.css'; import ANNOTATION_TYPES, { applyNativeColumns } from '../../modules/AnnotationTypes'; -import { formatDateVerbose } from '../../modules/dates'; -import { d3TimeFormatPreset, d3FormatPreset } from '../../modules/utils'; import { isTruthy } from '../../utils/common'; import { cleanColorInput, @@ -20,6 +20,7 @@ import { generateMultiLineTooltipContent, generateRichLineTooltipContent, getMaxLabelSize, + getTimeOrNumberFormatter, hideTooltips, tipFactory, tryNumify, @@ -175,7 +176,7 @@ const propTypes = { }; const NOOP = () => {}; -const formatter = d3.format('.3s'); +const formatter = getNumberFormatter(); function nvd3Vis(element, props) { const { @@ -342,7 +343,7 @@ function nvd3Vis(element, props) { if (pieLabelType !== 'key_percent' && pieLabelType !== 'key_value') { chart.labelType(pieLabelType); } else if (pieLabelType === 'key_value') { - chart.labelType(d => `${d.data.x}: ${d3.format('.3s')(d.data.y)}`); + chart.labelType(d => `${d.data.x}: ${formatNumber(NumberFormats.SI, d.data.y)}`); } if (pieLabelType === 'percent' || pieLabelType === 'key_percent') { @@ -377,8 +378,8 @@ function nvd3Vis(element, props) { xField, yField, sizeField, - xFormatter: d3FormatPreset(xAxisFormat), - yFormatter: d3FormatPreset(yAxisFormat), + xFormatter: getTimeOrNumberFormatter(xAxisFormat), + yFormatter: getTimeOrNumberFormatter(yAxisFormat), sizeFormatter: formatter, })); chart.pointRange([5, maxBubbleSize ** 2]); @@ -458,11 +459,11 @@ function nvd3Vis(element, props) { let xAxisFormatter; if (isTimeSeries) { - xAxisFormatter = d3TimeFormatPreset(xAxisFormat); + xAxisFormatter = getTimeFormatter(xAxisFormat); // In tooltips, always use the verbose time format - chart.interactiveLayer.tooltip.headerFormatter(formatDateVerbose); + chart.interactiveLayer.tooltip.headerFormatter(smartDateVerboseFormatter); } else { - xAxisFormatter = d3FormatPreset(xAxisFormat); + xAxisFormatter = getTimeOrNumberFormatter(xAxisFormat); } if (chart.x2Axis && chart.x2Axis.tickFormat) { chart.x2Axis.tickFormat(xAxisFormatter); @@ -472,11 +473,11 @@ function nvd3Vis(element, props) { chart.xAxis.tickFormat(xAxisFormatter); } - let yAxisFormatter = d3FormatPreset(yAxisFormat); + let yAxisFormatter = getTimeOrNumberFormatter(yAxisFormat); if (chart.yAxis && chart.yAxis.tickFormat) { if (contribution || comparisonType === 'percentage') { // When computing a "Percentage" or "Contribution" selected, we force a percentage format - yAxisFormatter = d3.format('.1%'); + yAxisFormatter = getNumberFormatter(NumberFormats.PERCENT_1_POINT); } chart.yAxis.tickFormat(yAxisFormatter); } @@ -519,8 +520,8 @@ function nvd3Vis(element, props) { } if (isVizTypes(['dual_line', 'line_multi'])) { - const yAxisFormatter1 = d3.format(yAxisFormat); - const yAxisFormatter2 = d3.format(yAxis2Format); + const yAxisFormatter1 = getNumberFormatter(yAxisFormat); + const yAxisFormatter2 = getNumberFormatter(yAxis2Format); chart.yAxis1.tickFormat(yAxisFormatter1); chart.yAxis2.tickFormat(yAxisFormatter2); const yAxisFormatters = data.map(datum => ( diff --git a/superset/assets/src/visualizations/nvd3/utils.js b/superset/assets/src/visualizations/nvd3/utils.js index 52e0c3e..86ee61f 100644 --- a/superset/assets/src/visualizations/nvd3/utils.js +++ b/superset/assets/src/visualizations/nvd3/utils.js @@ -1,6 +1,8 @@ import d3 from 'd3'; import d3tip from 'd3-tip'; import dompurify from 'dompurify'; +import { getNumberFormatter } from '@superset-ui/number-format'; +import { smartDateFormatter } from '@superset-ui/time-format'; // Regexp for the label added to time shifted series // (1 hour offset, 2 days offset, etc.) @@ -14,8 +16,19 @@ export function cleanColorInput(value) { .join(', '); } +/** + * If format is smart_date, format date + * Otherwise, format number with the given format name + * @param {*} format + */ +export function getTimeOrNumberFormatter(format) { + return (format === 'smart_date') + ? smartDateFormatter + : getNumberFormatter(format); +} + export function drawBarValues(svg, data, stacked, axisFormat) { - const format = d3.format(axisFormat || '.3s'); + const format = getNumberFormatter(axisFormat); const countSeriesDisplayed = data.length; const totalStackedValues = stacked && data.length !== 0 ? diff --git a/superset/assets/yarn.lock b/superset/assets/yarn.lock index dec3789..9cdccb9 100644 --- a/superset/assets/yarn.lock +++ b/superset/assets/yarn.lock @@ -432,6 +432,21 @@ dependencies: lodash "^4.17.11" +"@superset-ui/number-format@^0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@superset-ui/number-format/-/number-format-0.7.2.tgz#c193181d4bdc6eb63a48996012fae7baac5ad802" + dependencies: + "@superset-ui/core" "^0.7.0" + d3-format "^1.3.2" + +"@superset-ui/time-format@^0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@superset-ui/time-format/-/time-format-0.7.2.tgz#f5d21a8c46d76fc9603f2377f927ebf11593b4e1" + dependencies: + "@superset-ui/core" "^0.7.0" + d3-time "^1.0.10" + d3-time-format "^2.1.3" + "@superset-ui/translation@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@superset-ui/translation/-/translation-0.7.0.tgz#8b9426a97d523df5aefe9242084264897efe252c" @@ -3546,7 +3561,7 @@ d3-time-format@2, d3-time-format@^2.1.3: dependencies: d3-time "1" -d3-time@1: +d3-time@1, d3-time@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.10.tgz#8259dd71288d72eeacfd8de281c4bf5c7393053c"
