feat: custom d3 number locale (#20075)

This commit is contained in:
Etienne Baratte 2023-05-02 20:29:33 +02:00 committed by GitHub
parent f2fc4a03dc
commit a170ae4368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 144 additions and 22 deletions

View File

@ -50,6 +50,7 @@
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
"@superset-ui/preset-chart-xy": "file:./plugins/preset-chart-xy",
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
"@types/d3-format": "^3.0.1",
"@visx/axis": "^3.0.1",
"@visx/grid": "^3.0.1",
"@visx/responsive": "^3.0.0",
@ -5613,6 +5614,11 @@
"d3-time-format": "^3.0.0"
}
},
"node_modules/@encodable/format/node_modules/@types/d3-format": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz",
"integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ=="
},
"node_modules/@encodable/format/node_modules/d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
@ -20465,9 +20471,9 @@
"integrity": "sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA=="
},
"node_modules/@types/d3-format": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz",
"integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz",
"integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg=="
},
"node_modules/@types/d3-interpolate": {
"version": "1.4.2",
@ -60389,6 +60395,11 @@
"react": ">=16.13.1"
}
},
"packages/superset-ui-core/node_modules/@types/d3-format": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz",
"integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ=="
},
"packages/superset-ui-core/node_modules/@types/d3-time": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
@ -66037,6 +66048,11 @@
"d3-time-format": "^3.0.0"
},
"dependencies": {
"@types/d3-format": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz",
"integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ=="
},
"d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
@ -76715,6 +76731,11 @@
}
}
},
"@types/d3-format": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz",
"integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ=="
},
"@types/d3-time": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
@ -78303,9 +78324,9 @@
"integrity": "sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA=="
},
"@types/d3-format": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz",
"integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz",
"integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg=="
},
"@types/d3-interpolate": {
"version": "1.4.2",

View File

@ -115,6 +115,7 @@
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
"@superset-ui/preset-chart-xy": "file:./plugins/preset-chart-xy",
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
"@types/d3-format": "^3.0.1",
"@visx/axis": "^3.0.1",
"@visx/grid": "^3.0.1",
"@visx/responsive": "^3.0.0",

View File

@ -16,7 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t, smartDateFormatter, NumberFormats } from '@superset-ui/core';
import {
t,
smartDateFormatter,
NumberFormats,
getNumberFormatter,
} from '@superset-ui/core';
// D3 specific formatting config
export const D3_FORMAT_DOCS = t(
@ -30,22 +35,26 @@ export const D3_NUMBER_FORMAT_DESCRIPTION_PERCENTAGE_TEXT = t(
'Only applies when "Label Type" is not set to a percentage.',
);
const d3Formatted: [string, string][] = [
',d',
'.1s',
'.3s',
',.1%',
'.2%',
'.3%',
'.4r',
',.1f',
',.2f',
',.3f',
'+,',
'$,.2f',
].map(fmt => [fmt, `${fmt} (${getNumberFormatter(fmt).preview()})`]);
// input choices & options
export const D3_FORMAT_OPTIONS: [string, string][] = [
[NumberFormats.SMART_NUMBER, t('Adaptive formatting')],
['~g', t('Original value')],
[',d', ',d (12345.432 => 12,345)'],
['.1s', '.1s (12345.432 => 10k)'],
['.3s', '.3s (12345.432 => 12.3k)'],
[',.1%', ',.1% (12345.432 => 1,234,543.2%)'],
['.2%', '.2% (12345.432 => 1234543.20%)'],
['.3%', '.3% (12345.432 => 1234543.200%)'],
['.4r', '.4r (12345.432 => 12350)'],
[',.1f', ',.1f (12345.432 => 12,345.4)'],
[',.2f', ',.2f (12345.432 => 12,345.43)'],
[',.3f', ',.3f (12345.432 => 12,345.432)'],
['+,', '+, (12345.432 => +12,345.432)'],
['$,.2f', '$,.2f (12345.432 => $12,345.43)'],
...d3Formatted,
['DURATION', t('Duration in ms (66000 => 1m 6s)')],
['DURATION_SUB', t('Duration in ms (1.40008 => 1ms 400µs 80ns)')],
];

View File

@ -0,0 +1,26 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { FormatLocaleDefinition } from 'd3-format';
export const DEFAULT_D3_FORMAT: FormatLocaleDefinition = {
decimal: '.',
thousands: ',',
grouping: [3],
currency: ['$', ''],
};

View File

@ -16,7 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import { FormatLocaleDefinition } from 'd3-format';
import { RegistryWithDefaultKey, OverwritePolicy } from '../models';
import { DEFAULT_D3_FORMAT } from './D3FormatConfig';
import createD3NumberFormatter from './factories/createD3NumberFormatter';
import createSmartNumberFormatter from './factories/createSmartNumberFormatter';
import NumberFormats from './NumberFormats';
@ -26,6 +28,8 @@ export default class NumberFormatterRegistry extends RegistryWithDefaultKey<
NumberFormatter,
NumberFormatter
> {
d3Format: FormatLocaleDefinition;
constructor() {
super({
name: 'NumberFormatter',
@ -41,6 +45,12 @@ export default class NumberFormatterRegistry extends RegistryWithDefaultKey<
createSmartNumberFormatter({ signed: true }),
);
this.setDefaultKey(NumberFormats.SMART_NUMBER);
this.d3Format = DEFAULT_D3_FORMAT;
}
setD3Format(d3Format: Partial<FormatLocaleDefinition>) {
this.d3Format = { ...DEFAULT_D3_FORMAT, ...d3Format };
return this;
}
get(formatterId?: string) {
@ -59,6 +69,7 @@ export default class NumberFormatterRegistry extends RegistryWithDefaultKey<
// Create new formatter if does not exist
const formatter = createD3NumberFormatter({
formatString: targetFormat,
locale: this.d3Format,
});
this.registerValue(targetFormat, formatter);

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { FormatLocaleDefinition } from 'd3-format';
import { makeSingleton } from '../utils';
import NumberFormatterRegistry from './NumberFormatterRegistry';
@ -27,6 +28,10 @@ export function getNumberFormatter(format?: string) {
return getInstance().get(format);
}
export function setD3Format(d3Format: Partial<FormatLocaleDefinition>) {
getInstance().setD3Format(d3Format);
}
export function formatNumber(
format: string | undefined,
value: number | null | undefined,

View File

@ -19,10 +19,12 @@
export { default as NumberFormats } from './NumberFormats';
export { default as NumberFormatter, PREVIEW_VALUE } from './NumberFormatter';
export { DEFAULT_D3_FORMAT } from './D3FormatConfig';
export {
default as getNumberFormatterRegistry,
formatNumber,
setD3Format,
getNumberFormatter,
} from './NumberFormatterRegistrySingleton';

View File

@ -20,6 +20,7 @@
import {
NumberFormatterRegistry,
getNumberFormatterRegistry,
setD3Format,
getNumberFormatter,
formatNumber,
} from '@superset-ui/core';
@ -55,4 +56,23 @@ describe('NumberFormatterRegistrySingleton', () => {
expect(formatNumber(undefined, 1000)).toEqual('1k');
});
});
describe('setD3Format()', () => {
it('sets a specific FormatLocaleDefinition', () => {
setD3Format({
decimal: ';',
thousands: '-',
currency: ['€', ''],
grouping: [2],
});
const formatter = getNumberFormatter('$,.2f');
expect(formatter.format(12345.67)).toEqual('€1-23-45;67');
});
it('falls back to default value for unspecified locale format parameters', () => {
setD3Format({
currency: ['€', ''],
});
const formatter = getNumberFormatter('$,.1f');
expect(formatter.format(12345.67)).toEqual('€12,345.7');
});
});
});

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@superset-ui/core';
import { t, DEFAULT_D3_FORMAT } from '@superset-ui/core';
import { BootstrapData, CommonBootstrapData } from './types/bootstrapTypes';
@ -184,6 +184,7 @@ export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
color: '',
},
},
d3_format: DEFAULT_D3_FORMAT,
};
export const DEFAULT_BOOTSTRAP_DATA: BootstrapData = {

View File

@ -57,7 +57,7 @@ setupColors(
);
// Setup number formatters
setupFormatters();
setupFormatters(bootstrapData.common.d3_format);
setupDashboardComponents();

View File

@ -25,9 +25,13 @@ import {
smartDateFormatter,
smartDateVerboseFormatter,
} from '@superset-ui/core';
import { FormatLocaleDefinition } from 'd3-format';
export default function setupFormatters() {
export default function setupFormatters(
d3Format: Partial<FormatLocaleDefinition>,
) {
getNumberFormatterRegistry()
.setD3Format(d3Format)
// Add shims for format strings that are deprecated or common typos.
// Temporary solution until performing a db migration to fix this.
.registerValue(',0', getNumberFormatter(',.4~f'))

View File

@ -6,6 +6,7 @@ import {
Locale,
SequentialSchemeConfig,
} from '@superset-ui/core';
import { FormatLocaleDefinition } from 'd3-format';
import { isPlainObject } from 'lodash';
import { Languages } from 'src/features/home/LanguagePicker';
import { FlashMessage } from '../components/FlashProvider';
@ -150,6 +151,7 @@ export interface CommonBootstrapData {
extra_sequential_color_schemes: SequentialSchemeConfig[];
theme_overrides: JsonObject;
menu_data: MenuData;
d3_format: Partial<FormatLocaleDefinition>;
}
export interface BootstrapData {

View File

@ -378,6 +378,25 @@ LANGUAGES = {
# incomplete and not well maintained.
LANGUAGES = {}
# Override the default d3 locale format
# Default values are equivalent to
# D3_FORMAT = {
# "decimal": ".", # - decimal place string (e.g., ".").
# "thousands": ",", # - group separator string (e.g., ",").
# "grouping": [3], # - array of group sizes (e.g., [3]), cycled as needed.
# "currency": ["$", ""] # - currency prefix/suffix strings (e.g., ["$", ""])
# }
# https://github.com/d3/d3-format/blob/main/README.md#formatLocale
class D3Format(TypedDict, total=False):
decimal: str
thousands: str
grouping: List[int]
currency: List[str]
D3_FORMAT: D3Format = {}
# ---------------------------------------------------
# Feature flags
# ---------------------------------------------------

View File

@ -428,6 +428,7 @@ def cached_common_bootstrap_data(user: User) -> Dict[str, Any]:
"conf": frontend_config,
"locale": locale,
"language_pack": get_language_pack(locale),
"d3_format": conf.get("D3_FORMAT"),
"feature_flags": get_feature_flags(),
"extra_sequential_color_schemes": conf["EXTRA_SEQUENTIAL_COLOR_SCHEMES"],
"extra_categorical_color_schemes": conf["EXTRA_CATEGORICAL_COLOR_SCHEMES"],