diff --git a/superset/assets/package.json b/superset/assets/package.json index f1940c70670..466ad3e60ad 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 3f39343abcb..d9f97e7f889 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 ca16d868303..79a2044cb77 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 43a824e89d7..623be1f0744 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 aea9bc48bac..2eeb80d353e 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 = ( {limitReached && diff --git a/superset/assets/src/modules/dates.js b/superset/assets/src/modules/dates.js index 311340bda65..f5063b0c909 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 9fbd1c8f7c2..11d2eb3308e 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 9d80dc9e59d..3e96e60a59b 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 00000000000..dd1f39fdda8 --- /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 9c80e04871e..b6c79e67897 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 92a88f3b157..cb5dcc24107 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 6b7aadae38d..0dd39443d40 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 05d416e8a3c..84e399f68d7 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 ff22bcf3173..830ff2892e4 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 4c8f6aa881c..3e7d7d7f3c0 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 539c024df3a..dbb2e4290c8 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 71d0cfad1f7..b5326bc8a63 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 097c918795a..a99d03f6137 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 f80d032136a..5e4c6ebb7dc 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 29496a693aa..5e13c198a9a 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 7056235e713..23cdc07d3ba 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 = `${dompurify.sanitize(val)}`; } 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 eabbb0ed762..a4751e5f400 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 ( - {d3format(format, num)} + {formatNumber(format, num)} ); } return {num}; diff --git a/superset/assets/src/visualizations/TimeTable/SparklineCell.jsx b/superset/assets/src/visualizations/TimeTable/SparklineCell.jsx index 1a49e3534dc..bc58c104cdb 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 38bf058b273..667b3771177 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 (