diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 71a007404a..3c7d41bed6 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -47,6 +47,7 @@ "@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud", "@superset-ui/switchboard": "file:./packages/superset-ui-switchboard", "@types/d3-format": "^3.0.1", + "@types/d3-time-format": "^3.0.1", "@visx/axis": "^3.8.0", "@visx/grid": "^3.5.0", "@visx/responsive": "^3.0.0", @@ -5361,6 +5362,11 @@ "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/@types/d3-time-format": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", + "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" + }, "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", @@ -22612,9 +22618,9 @@ "integrity": "sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw==" }, "node_modules/@types/d3-time-format": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", - "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-3.0.1.tgz", + "integrity": "sha512-5GIimz5IqaRsdnxs4YlyTZPwAMfALu/wA4jqSiuqgdbCxUZ2WjrnwANqOtoBJQgeaUTdYNfALJO0Yb0YrDqduA==" }, "node_modules/@types/d3-voronoi": { "version": "1.1.12", @@ -67693,6 +67699,11 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==" }, + "packages/superset-ui-core/node_modules/@types/d3-time-format": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", + "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" + }, "packages/superset-ui-core/node_modules/@types/math-expression-evaluator": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/math-expression-evaluator/-/math-expression-evaluator-1.3.3.tgz", @@ -74967,6 +74978,11 @@ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz", "integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ==" }, + "@types/d3-time-format": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", + "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" + }, "d3-array": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", @@ -86338,6 +86354,11 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==" }, + "@types/d3-time-format": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", + "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" + }, "@types/math-expression-evaluator": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/math-expression-evaluator/-/math-expression-evaluator-1.3.3.tgz", @@ -89668,9 +89689,9 @@ "integrity": "sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw==" }, "@types/d3-time-format": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", - "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-3.0.1.tgz", + "integrity": "sha512-5GIimz5IqaRsdnxs4YlyTZPwAMfALu/wA4jqSiuqgdbCxUZ2WjrnwANqOtoBJQgeaUTdYNfALJO0Yb0YrDqduA==" }, "@types/d3-voronoi": { "version": "1.1.12", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 0bfb898666..3d488537fb 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -112,6 +112,7 @@ "@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud", "@superset-ui/switchboard": "file:./packages/superset-ui-switchboard", "@types/d3-format": "^3.0.1", + "@types/d3-time-format": "^3.0.1", "@visx/axis": "^3.8.0", "@visx/grid": "^3.5.0", "@visx/responsive": "^3.0.0", diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts index 3ea4ee80ad..a8fd6312cb 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts @@ -18,7 +18,7 @@ */ import { t, - smartDateFormatter, + SMART_DATE_ID, NumberFormats, getNumberFormatter, } from '@superset-ui/core'; @@ -64,7 +64,7 @@ export const D3_TIME_FORMAT_DOCS = t( ); export const D3_TIME_FORMAT_OPTIONS: [string, string][] = [ - [smartDateFormatter.id, t('Adaptive formatting')], + [SMART_DATE_ID, t('Adaptive formatting')], ['%d/%m/%Y', '%d/%m/%Y | 14/01/2019'], ['%m/%d/%Y', '%m/%d/%Y | 01/14/2019'], ['%Y-%m-%d', '%Y-%m-%d | 2019-01-14'], diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/D3FormatConfig.ts b/superset-frontend/packages/superset-ui-core/src/time-format/D3FormatConfig.ts new file mode 100644 index 0000000000..d6e5cea44f --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/time-format/D3FormatConfig.ts @@ -0,0 +1,65 @@ +/** + * 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 { TimeLocaleDefinition } from 'd3-time-format'; + +export const DEFAULT_D3_TIME_FORMAT: TimeLocaleDefinition = { + dateTime: '%x, %X', + date: '%-m/%-d/%Y', + time: '%-I:%M:%S %p', + periods: ['AM', 'PM'], + days: [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + ], + shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + months: [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ], + shortMonths: [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ], +}; diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/TimeFormatterRegistry.ts b/superset-frontend/packages/superset-ui-core/src/time-format/TimeFormatterRegistry.ts index e99517e8de..b66ad013a9 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-format/TimeFormatterRegistry.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-format/TimeFormatterRegistry.ts @@ -4,19 +4,22 @@ * 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 + * '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 + * '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 { TimeLocaleDefinition } from 'd3-time-format'; import { RegistryWithDefaultKey, OverwritePolicy } from '../models'; +import { DEFAULT_D3_TIME_FORMAT } from './D3FormatConfig'; import TimeFormats, { LOCAL_PREFIX } from './TimeFormats'; import createD3TimeFormatter from './factories/createD3TimeFormatter'; import TimeFormatter from './TimeFormatter'; @@ -25,12 +28,21 @@ export default class TimeFormatterRegistry extends RegistryWithDefaultKey< TimeFormatter, TimeFormatter > { + d3Format: TimeLocaleDefinition; + constructor() { super({ initialDefaultKey: TimeFormats.DATABASE_DATETIME, name: 'TimeFormatter', overwritePolicy: OverwritePolicy.Warn, }); + + this.d3Format = DEFAULT_D3_TIME_FORMAT; + } + + setD3Format(d3Format: Partial) { + this.d3Format = { ...DEFAULT_D3_TIME_FORMAT, ...d3Format }; + return this; } get(format?: string) { @@ -47,7 +59,13 @@ export default class TimeFormatterRegistry extends RegistryWithDefaultKey< // Create new formatter if does not exist const useLocalTime = targetFormat.startsWith(LOCAL_PREFIX); const formatString = targetFormat.replace(LOCAL_PREFIX, ''); - const formatter = createD3TimeFormatter({ formatString, useLocalTime }); + const locale = this.d3Format; + const formatter = createD3TimeFormatter({ + formatString, + useLocalTime, + locale, + }); + this.registerValue(targetFormat, formatter); return formatter; diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/factories/createMultiFormatter.ts b/superset-frontend/packages/superset-ui-core/src/time-format/factories/createMultiFormatter.ts index 69331536cf..bbd5fc2c42 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-format/factories/createMultiFormatter.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-format/factories/createMultiFormatter.ts @@ -16,8 +16,12 @@ * specific language governing permissions and limitations * under the License. */ - -import { utcFormat, timeFormat } from 'd3-time-format'; +import { + timeFormatLocale, + TimeLocaleDefinition, + timeFormat, + utcFormat, +} from 'd3-time-format'; import { utcUtils, localTimeUtils } from '../utils/d3Time'; import TimeFormatter from '../TimeFormatter'; @@ -38,12 +42,14 @@ export default function createMultiFormatter({ description, formats = {}, useLocalTime = false, + locale, }: { id: string; label?: string; description?: string; formats?: FormatsByStep; useLocalTime?: boolean; + locale?: TimeLocaleDefinition; }) { const { millisecond = '.%L', @@ -56,16 +62,23 @@ export default function createMultiFormatter({ year = '%Y', } = formats; - const format = useLocalTime ? timeFormat : utcFormat; + let formatFunc; - const formatMillisecond = format(millisecond); - const formatSecond = format(second); - const formatMinute = format(minute); - const formatHour = format(hour); - const formatDay = format(day); - const formatFirstDayOfWeek = format(week); - const formatMonth = format(month); - const formatYear = format(year); + if (typeof locale === 'undefined') { + formatFunc = useLocalTime ? timeFormat : utcFormat; + } else { + const formatLocale = timeFormatLocale(locale); + formatFunc = useLocalTime ? formatLocale.format : formatLocale.utcFormat; + } + + const formatMillisecond = formatFunc(millisecond); + const formatSecond = formatFunc(second); + const formatMinute = formatFunc(minute); + const formatHour = formatFunc(hour); + const formatDay = formatFunc(day); + const formatFirstDayOfWeek = formatFunc(week); + const formatMonth = formatFunc(month); + const formatYear = formatFunc(year); const { hasMillisecond, diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDate.ts b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDate.ts index 0aafb2a893..ecc3f66057 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDate.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDate.ts @@ -17,21 +17,25 @@ * under the License. */ +import { TimeLocaleDefinition } from 'd3-time-format'; import createMultiFormatter from '../factories/createMultiFormatter'; -const smartDateFormatter = createMultiFormatter({ - id: 'smart_date', - label: 'Adaptative Formatting', - formats: { - millisecond: '.%Lms', - second: ':%Ss', - minute: '%I:%M', - hour: '%I %p', - day: '%a %d', - week: '%b %d', - month: '%B', - year: '%Y', - }, -}); +export const SMART_DATE_ID = 'smart_date'; -export default smartDateFormatter; +export function createSmartDateFormatter(locale?: TimeLocaleDefinition) { + return createMultiFormatter({ + id: SMART_DATE_ID, + label: 'Adaptative Formatting', + formats: { + millisecond: '.%Lms', + second: ':%Ss', + minute: '%I:%M', + hour: '%I %p', + day: '%a %d', + week: '%b %d', + month: '%B', + year: '%Y', + }, + locale, + }); +} diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDateDetailed.ts b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDateDetailed.ts index d8a30e9a1c..966ad2d15d 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDateDetailed.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDateDetailed.ts @@ -17,21 +17,27 @@ * under the License. */ +import { TimeLocaleDefinition } from 'd3-time-format'; import createMultiFormatter from '../factories/createMultiFormatter'; -const smartDateDetailedFormatter = createMultiFormatter({ - id: 'smart_date_detailed', - label: 'Detailed adaptive formatter', - formats: { - millisecond: '%Y-%m-%d %H:%M:%S.%L', - second: '%Y-%m-%d %H:%M:%S', - minute: '%Y-%m-%d %H:%M', - hour: '%Y-%m-%d %H:%M', - day: '%Y-%m-%d', - week: '%Y-%m-%d', - month: '%Y-%m-%d', - year: '%Y', - }, -}); +export const SMART_DATE_DETAILED_ID = 'smart_date_detailed'; -export default smartDateDetailedFormatter; +export function createSmartDateDetailedFormatter( + locale?: TimeLocaleDefinition, +) { + return createMultiFormatter({ + id: 'smart_date_detailed', + label: 'Detailed adaptive formatter', + formats: { + millisecond: '%Y-%m-%d %H:%M:%S.%L', + second: '%Y-%m-%d %H:%M:%S', + minute: '%Y-%m-%d %H:%M', + hour: '%Y-%m-%d %H:%M', + day: '%Y-%m-%d', + week: '%Y-%m-%d', + month: '%Y-%m-%d', + year: '%Y', + }, + locale, + }); +} diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDateVerbose.ts b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDateVerbose.ts index 4178322864..e564a3ff94 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDateVerbose.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/smartDateVerbose.ts @@ -17,21 +17,25 @@ * under the License. */ +import { TimeLocaleDefinition } from 'd3-time-format'; import createMultiFormatter from '../factories/createMultiFormatter'; -const smartDateFormatter = createMultiFormatter({ - id: 'smart_date_verbose', - label: 'Verbose Adaptative Formatting', - formats: { - millisecond: '.%L', - second: '%a %b %d, %I:%M:%S %p', - minute: '%a %b %d, %I:%M %p', - hour: '%a %b %d, %I %p', - day: '%a %b %-e', - week: '%a %b %-e', - month: '%b %Y', - year: '%Y', - }, -}); +export const SMART_DATE_VERBOSE_ID = 'smart_date_verbose'; -export default smartDateFormatter; +export function createSmartDateVerboseFormatter(locale?: TimeLocaleDefinition) { + return createMultiFormatter({ + id: SMART_DATE_VERBOSE_ID, + label: 'Verbose Adaptative Formatting', + formats: { + millisecond: '.%L', + second: '%a %b %d, %I:%M:%S %p', + minute: '%a %b %d, %I:%M %p', + hour: '%a %b %d, %I %p', + day: '%a %b %-e', + week: '%a %b %-e', + month: '%b %Y', + year: '%Y', + }, + locale, + }); +} diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/index.ts b/superset-frontend/packages/superset-ui-core/src/time-format/index.ts index b0d95c1433..5b01bbca77 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-format/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-format/index.ts @@ -19,6 +19,7 @@ export { default as TimeFormats, LOCAL_PREFIX } from './TimeFormats'; export { default as TimeFormatter, PREVIEW_TIME } from './TimeFormatter'; +export { DEFAULT_D3_TIME_FORMAT } from './D3FormatConfig'; export { default as getTimeFormatterRegistry, @@ -32,9 +33,18 @@ export { export { default as createD3TimeFormatter } from './factories/createD3TimeFormatter'; export { default as createMultiFormatter } from './factories/createMultiFormatter'; -export { default as smartDateFormatter } from './formatters/smartDate'; -export { default as smartDateDetailedFormatter } from './formatters/smartDateDetailed'; -export { default as smartDateVerboseFormatter } from './formatters/smartDateVerbose'; +export { + SMART_DATE_ID, + createSmartDateFormatter, +} from './formatters/smartDate'; +export { + SMART_DATE_DETAILED_ID, + createSmartDateDetailedFormatter, +} from './formatters/smartDateDetailed'; +export { + SMART_DATE_VERBOSE_ID, + createSmartDateVerboseFormatter, +} from './formatters/smartDateVerbose'; export { default as finestTemporalGrainFormatter } from './formatters/finestTemporalGrain'; export { default as normalizeTimestamp } from './utils/normalizeTimestamp'; diff --git a/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatterRegistry.test.ts b/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatterRegistry.test.ts index 5ff3953d04..57b659d740 100644 --- a/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatterRegistry.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatterRegistry.test.ts @@ -17,8 +17,10 @@ * under the License. */ +import { TimeLocaleDefinition } from 'd3-time-format'; import { TimeFormats, TimeFormatter, PREVIEW_TIME } from '@superset-ui/core'; import TimeFormatterRegistry from '../../src/time-format/TimeFormatterRegistry'; +import { DEFAULT_D3_TIME_FORMAT } from '../../src/time-format'; describe('TimeFormatterRegistry', () => { let registry: TimeFormatterRegistry; @@ -78,4 +80,108 @@ describe('TimeFormatterRegistry', () => { ); }); }); + describe('.setD3Format(d3Format)', () => { + describe('when partial value is specified', () => { + const timeFormat: Partial = { + days: [ + 'Domingo', + 'Segunda', + 'Terça', + 'Quarta', + 'Quinta', + 'Sexta', + 'Sábado', + ], + }; + + beforeEach(() => { + registry.setD3Format(timeFormat); + }); + + it('sets the specified value and default', () => { + expect(registry.d3Format).toEqual({ + ...DEFAULT_D3_TIME_FORMAT, + ...timeFormat, + }); + }); + + it('does not change short days of week name format', () => { + expect(registry.format('%a', PREVIEW_TIME)).toEqual('Tue'); + }); + + it('changes full days of week name format', () => { + expect(registry.format('%A', PREVIEW_TIME)).toEqual('Terça'); + }); + + it('does not change months format', () => { + expect(registry.format('%b', PREVIEW_TIME)).toEqual('Feb'); + expect(registry.format('%B', PREVIEW_TIME)).toEqual('February'); + }); + }); + + describe('when full value is specified', () => { + const timeFormat: TimeLocaleDefinition = { + dateTime: '%A, %e de %B de %Y. %X', + date: '%d/%m/%Y', + time: '%H:%M:%S', + periods: ['AM', 'PM'], + days: [ + 'Domingo', + 'Segunda', + 'Terça', + 'Quarta', + 'Quinta', + 'Sexta', + 'Sábado', + ], + shortDays: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'], + months: [ + 'Janeiro', + 'Fevereiro', + 'Março', + 'Abril', + 'Maio', + 'Junho', + 'Julho', + 'Agosto', + 'Setembro', + 'Outubro', + 'Novembro', + 'Dezembro', + ], + shortMonths: [ + 'Jan', + 'Fev', + 'Mar', + 'Abr', + 'Mai', + 'Jun', + 'Jul', + 'Ago', + 'Set', + 'Out', + 'Nov', + 'Dez', + ], + }; + + beforeEach(() => { + registry.setD3Format(timeFormat); + }); + + it('sets the specified value ignoring default', () => { + expect(registry.d3Format).toEqual(timeFormat); + }); + + it('changes days of week format', () => { + expect(registry.format('%a', PREVIEW_TIME)).toEqual('Ter'); + expect(registry.format('%A', PREVIEW_TIME)).toEqual('Terça'); + }); + + it('changes months format', () => { + expect(registry.format('%b', PREVIEW_TIME)).toEqual('Fev'); + expect(registry.format('%B', PREVIEW_TIME)).toEqual('Fevereiro'); + }); + }); + }); }); diff --git a/superset-frontend/packages/superset-ui-core/test/time-format/factories/createMultiFormatter.test.ts b/superset-frontend/packages/superset-ui-core/test/time-format/factories/createMultiFormatter.test.ts index 197120b919..deaf00ff16 100644 --- a/superset-frontend/packages/superset-ui-core/test/time-format/factories/createMultiFormatter.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/time-format/factories/createMultiFormatter.test.ts @@ -18,38 +18,187 @@ */ import { createMultiFormatter } from '@superset-ui/core'; +import { TimeLocaleDefinition } from 'd3-time-format'; describe('createMultiFormatter()', () => { describe('creates a multi-step formatter', () => { - const formatter = createMultiFormatter({ - id: 'my_format', - useLocalTime: true, + describe('when locale is undefined', () => { + describe('and use local time is false', () => { + const formatter = createMultiFormatter({ + id: 'my_format', + useLocalTime: false, + }); + it('formats millisecond', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22, 33, 100))).toEqual( + '.100', + ); + }); + it('formats second', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22, 33))).toEqual(':33'); + }); + it('format minutes', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22))).toEqual('04:22'); + }); + it('format hours', () => { + expect(formatter(new Date('2018-11-18 11:00 UTC'))).toEqual('11 AM'); + }); + it('format first day of week', () => { + expect(formatter(new Date('2018-11-18 UTC'))).toEqual('Nov 18'); + }); + it('format other day of week', () => { + expect(formatter(new Date('2018-11-20 UTC'))).toEqual('Tue 20'); + }); + it('format month', () => { + expect(formatter(new Date('2018-11-1 UTC'))).toEqual('November'); + }); + it('format year', () => { + expect(formatter(new Date('2018-1-1 UTC'))).toEqual('2018'); + }); + }); + describe('and use local time is true', () => { + const formatter = createMultiFormatter({ + id: 'my_format', + useLocalTime: true, + }); + it('formats millisecond', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22, 33, 100))).toEqual( + '.100', + ); + }); + it('formats second', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22, 33))).toEqual(':33'); + }); + it('format minutes', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22))).toEqual('11:22'); + }); + it('format hours', () => { + expect(formatter(new Date(2018, 10, 20, 11))).toEqual('11 AM'); + }); + it('format first day of week', () => { + expect(formatter(new Date(2018, 10, 18))).toEqual('Nov 18'); + }); + it('format other day of week', () => { + expect(formatter(new Date(2018, 10, 20))).toEqual('Tue 20'); + }); + it('format month', () => { + expect(formatter(new Date(2018, 10))).toEqual('November'); + }); + it('format year', () => { + expect(formatter(new Date(2018, 0))).toEqual('2018'); + }); + }); }); - it('formats millisecond', () => { - expect(formatter(new Date(2018, 10, 20, 11, 22, 33, 100))).toEqual( - '.100', - ); + }); + describe('when locale is not default', () => { + const locale: TimeLocaleDefinition = { + dateTime: '%A, %e de %B de %Y. %X', + date: '%d/%m/%Y', + time: '%H:%M:%S', + periods: ['AM', 'PM'], + days: [ + 'Domingo', + 'Segunda', + 'Terça', + 'Quarta', + 'Quinta', + 'Sexta', + 'Sábado', + ], + shortDays: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'], + months: [ + 'Janeiro', + 'Fevereiro', + 'Março', + 'Abril', + 'Maio', + 'Junho', + 'Julho', + 'Agosto', + 'Setembro', + 'Outubro', + 'Novembro', + 'Dezembro', + ], + shortMonths: [ + 'Jan', + 'Fev', + 'Mar', + 'Abr', + 'Mai', + 'Jun', + 'Jul', + 'Ago', + 'Set', + 'Out', + 'Nov', + 'Dez', + ], + }; + describe('and use local time is false', () => { + const formatter = createMultiFormatter({ + id: 'my_format', + useLocalTime: false, + locale, + }); + it('formats millisecond', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22, 33, 100))).toEqual( + '.100', + ); + }); + it('formats second', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22, 33))).toEqual(':33'); + }); + it('format minutes', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22))).toEqual('04:22'); + }); + it('format hours', () => { + expect(formatter(new Date('2018-11-18 11:00 UTC'))).toEqual('11 AM'); + }); + it('format first day of week', () => { + expect(formatter(new Date('2018-11-18 UTC'))).toEqual('Nov 18'); + }); + it('format other day of week', () => { + expect(formatter(new Date('2018-11-20 UTC'))).toEqual('Ter 20'); + }); + it('format month', () => { + expect(formatter(new Date('2018-11-1 UTC'))).toEqual('Novembro'); + }); + it('format year', () => { + expect(formatter(new Date('2018-1-1 UTC'))).toEqual('2018'); + }); }); - it('formats second', () => { - expect(formatter(new Date(2018, 10, 20, 11, 22, 33))).toEqual(':33'); - }); - it('format minutes', () => { - expect(formatter(new Date(2018, 10, 20, 11, 22))).toEqual('11:22'); - }); - it('format hours', () => { - expect(formatter(new Date(2018, 10, 20, 11))).toEqual('11 AM'); - }); - it('format first day of week', () => { - expect(formatter(new Date(2018, 10, 18))).toEqual('Nov 18'); - }); - it('format other day of week', () => { - expect(formatter(new Date(2018, 10, 20))).toEqual('Tue 20'); - }); - it('format month', () => { - expect(formatter(new Date(2018, 10))).toEqual('November'); - }); - it('format year', () => { - expect(formatter(new Date(2018, 0))).toEqual('2018'); + describe('and use local time is true', () => { + const formatter = createMultiFormatter({ + id: 'my_format', + useLocalTime: true, + locale, + }); + it('formats millisecond', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22, 33, 100))).toEqual( + '.100', + ); + }); + it('formats second', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22, 33))).toEqual(':33'); + }); + it('format minutes', () => { + expect(formatter(new Date(2018, 10, 20, 11, 22))).toEqual('11:22'); + }); + it('format hours', () => { + expect(formatter(new Date(2018, 10, 20, 11))).toEqual('11 AM'); + }); + it('format first day of week', () => { + expect(formatter(new Date(2018, 10, 18))).toEqual('Nov 18'); + }); + it('format other day of week', () => { + expect(formatter(new Date(2018, 10, 20))).toEqual('Ter 20'); + }); + it('format month', () => { + expect(formatter(new Date(2018, 10))).toEqual('Novembro'); + }); + it('format year', () => { + expect(formatter(new Date(2018, 0))).toEqual('2018'); + }); }); }); }); diff --git a/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDate.test.ts b/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDate.test.ts index 04354b0c80..3109da0fb6 100644 --- a/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDate.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDate.test.ts @@ -17,26 +17,98 @@ * under the License. */ -import { TimeFormatter, smartDateFormatter } from '@superset-ui/core'; +import { TimeLocaleDefinition } from 'd3-time-format'; +import { TimeFormatter, createSmartDateFormatter } from '@superset-ui/core'; -describe('smartDateFormatter', () => { - it('is a function', () => { - expect(smartDateFormatter).toBeInstanceOf(TimeFormatter); +describe('createSmartDateFormatter', () => { + describe('when locale is default', () => { + const smartDateFormatter = createSmartDateFormatter(); + + it('is a function', () => { + expect(smartDateFormatter).toBeInstanceOf(TimeFormatter); + }); + + it('shows only year when 1st day of the year', () => { + expect(smartDateFormatter(new Date('2020-01-01'))).toBe('2020'); + }); + + it('shows only month when 1st of month', () => { + expect(smartDateFormatter(new Date('2020-03-01'))).toBe('March'); + }); + + it('does not show day of week when it is Sunday', () => { + expect(smartDateFormatter(new Date('2020-03-15'))).toBe('Mar 15'); + }); + + it('shows weekday when it is not Sunday (and no ms/sec/min/hr)', () => { + expect(smartDateFormatter(new Date('2020-03-03'))).toBe('Tue 03'); + }); }); + describe('when different locale is not default', () => { + const locale: TimeLocaleDefinition = { + dateTime: '%A, %e de %B de %Y. %X', + date: '%d/%m/%Y', + time: '%H:%M:%S', + periods: ['AM', 'PM'], + days: [ + 'Domingo', + 'Segunda', + 'Terça', + 'Quarta', + 'Quinta', + 'Sexta', + 'Sábado', + ], + shortDays: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'], + months: [ + 'Janeiro', + 'Fevereiro', + 'Março', + 'Abril', + 'Maio', + 'Junho', + 'Julho', + 'Agosto', + 'Setembro', + 'Outubro', + 'Novembro', + 'Dezembro', + ], + shortMonths: [ + 'Jan', + 'Fev', + 'Mar', + 'Abr', + 'Mai', + 'Jun', + 'Jul', + 'Ago', + 'Set', + 'Out', + 'Nov', + 'Dez', + ], + }; + const smartDateFormatter = createSmartDateFormatter(locale); - it('shows only year when 1st day of the year', () => { - expect(smartDateFormatter(new Date('2020-01-01'))).toBe('2020'); - }); + it('is a function', () => { + expect(smartDateFormatter).toBeInstanceOf(TimeFormatter); + }); - it('shows only month when 1st of month', () => { - expect(smartDateFormatter(new Date('2020-03-01'))).toBe('March'); - }); + it('shows only year when 1st day of the year', () => { + expect(smartDateFormatter(new Date('2020-01-01'))).toBe('2020'); + }); - it('does not show day of week when it is Sunday', () => { - expect(smartDateFormatter(new Date('2020-03-15'))).toBe('Mar 15'); - }); + it('shows only month when 1st of month', () => { + expect(smartDateFormatter(new Date('2020-03-01'))).toBe('Março'); + }); - it('shows weekday when it is not Sunday (and no ms/sec/min/hr)', () => { - expect(smartDateFormatter(new Date('2020-03-03'))).toBe('Tue 03'); + it('does not show day of week when it is Sunday', () => { + expect(smartDateFormatter(new Date('2023-10-15'))).toBe('Out 15'); + }); + + it('shows weekday when it is not Sunday (and no ms/sec/min/hr)', () => { + expect(smartDateFormatter(new Date('2020-03-03'))).toBe('Ter 03'); + }); }); }); diff --git a/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDateDetailed.test.ts b/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDateDetailed.test.ts index 7fd5201d75..c47c468052 100644 --- a/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDateDetailed.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDateDetailed.test.ts @@ -17,40 +17,125 @@ * under the License. */ -import { TimeFormatter, smartDateDetailedFormatter } from '@superset-ui/core'; +import { TimeLocaleDefinition } from 'd3-time-format'; +import { + TimeFormatter, + createSmartDateDetailedFormatter, +} from '@superset-ui/core'; describe('smartDateDetailedFormatter', () => { - const formatter = smartDateDetailedFormatter; + describe('when locale is default', () => { + const formatter = createSmartDateDetailedFormatter(); - it('is a function', () => { - expect(formatter).toBeInstanceOf(TimeFormatter); + it('is a function', () => { + expect(formatter).toBeInstanceOf(TimeFormatter); + }); + + it('shows only year when 1st day of the year', () => { + expect(formatter(new Date('2020-01-01T00:00:00.000+00:00'))).toBe('2020'); + }); + + it('shows full date when a regular date', () => { + expect(formatter(new Date('2020-03-01T00:00:00.000+00:00'))).toBe( + '2020-03-01', + ); + }); + + it('shows full date including time of day without seconds when hour precision', () => { + expect(formatter(new Date('2020-03-01T13:00:00.000+00:00'))).toBe( + '2020-03-01 13:00', + ); + }); + + it('shows full date including time of day when minute precision', () => { + expect(formatter(new Date('2020-03-10T13:10:00.000+00:00'))).toBe( + '2020-03-10 13:10', + ); + }); + + it('shows full date including time of day when subsecond precision', () => { + expect(formatter(new Date('2020-03-10T13:10:00.100+00:00'))).toBe( + '2020-03-10 13:10:00.100', + ); + }); }); + describe('when locale is specified', () => { + const locale: TimeLocaleDefinition = { + dateTime: '%A, %e de %B de %Y. %X', + date: '%d/%m/%Y', + time: '%H:%M:%S', + periods: ['AM', 'PM'], + days: [ + 'Domingo', + 'Segunda', + 'Terça', + 'Quarta', + 'Quinta', + 'Sexta', + 'Sábado', + ], + shortDays: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'], + months: [ + 'Janeiro', + 'Fevereiro', + 'Março', + 'Abril', + 'Maio', + 'Junho', + 'Julho', + 'Agosto', + 'Setembro', + 'Outubro', + 'Novembro', + 'Dezembro', + ], + shortMonths: [ + 'Jan', + 'Fev', + 'Mar', + 'Abr', + 'Mai', + 'Jun', + 'Jul', + 'Ago', + 'Set', + 'Out', + 'Nov', + 'Dez', + ], + }; + const formatter = createSmartDateDetailedFormatter(locale); - it('shows only year when 1st day of the year', () => { - expect(formatter(new Date('2020-01-01T00:00:00.000+00:00'))).toBe('2020'); - }); + it('is a function', () => { + expect(formatter).toBeInstanceOf(TimeFormatter); + }); - it('shows full date when a regular date', () => { - expect(formatter(new Date('2020-03-01T00:00:00.000+00:00'))).toBe( - '2020-03-01', - ); - }); + it('shows only year when 1st day of the year', () => { + expect(formatter(new Date('2020-01-01T00:00:00.000+00:00'))).toBe('2020'); + }); - it('shows full date including time of day without seconds when hour precision', () => { - expect(formatter(new Date('2020-03-01T13:00:00.000+00:00'))).toBe( - '2020-03-01 13:00', - ); - }); + it('shows full date when a regular date', () => { + expect(formatter(new Date('2020-03-01T00:00:00.000+00:00'))).toBe( + '2020-03-01', + ); + }); - it('shows full date including time of day when minute precision', () => { - expect(formatter(new Date('2020-03-10T13:10:00.000+00:00'))).toBe( - '2020-03-10 13:10', - ); - }); + it('shows full date including time of day without seconds when hour precision', () => { + expect(formatter(new Date('2020-03-01T13:00:00.000+00:00'))).toBe( + '2020-03-01 13:00', + ); + }); - it('shows full date including time of day when subsecond precision', () => { - expect(formatter(new Date('2020-03-10T13:10:00.100+00:00'))).toBe( - '2020-03-10 13:10:00.100', - ); + it('shows full date including time of day when minute precision', () => { + expect(formatter(new Date('2020-03-10T13:10:00.000+00:00'))).toBe( + '2020-03-10 13:10', + ); + }); + + it('shows full date including time of day when subsecond precision', () => { + expect(formatter(new Date('2020-03-10T13:10:00.100+00:00'))).toBe( + '2020-03-10 13:10:00.100', + ); + }); }); }); diff --git a/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDateVerbose.test.ts b/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDateVerbose.test.ts index 1fde19a6d7..aaf6e76a13 100644 --- a/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDateVerbose.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/time-format/formatters/smartDateVerbose.test.ts @@ -17,25 +17,95 @@ * under the License. */ -import { TimeFormatter, smartDateVerboseFormatter } from '@superset-ui/core'; +import { TimeLocaleDefinition } from 'd3-time-format'; +import { + TimeFormatter, + createSmartDateVerboseFormatter, +} from '@superset-ui/core'; describe('smartDateVerboseFormatter', () => { - const formatter = smartDateVerboseFormatter; + describe('when locale is default', () => { + const formatter = createSmartDateVerboseFormatter(); - it('is a function', () => { - expect(formatter).toBeInstanceOf(TimeFormatter); + it('is a function', () => { + expect(formatter).toBeInstanceOf(TimeFormatter); + }); + + it('shows only year when 1st day of the year', () => { + expect(formatter(new Date('2020-01-01'))).toBe('2020'); + }); + + it('shows month and year when 1st of month', () => { + expect(formatter(new Date('2020-03-01'))).toBe('Mar 2020'); + }); + + it('shows weekday when any day of the month', () => { + expect(formatter(new Date('2020-03-03'))).toBe('Tue Mar 3'); + expect(formatter(new Date('2020-03-15'))).toBe('Sun Mar 15'); + }); }); + describe('when locale is not default', () => { + const locale: TimeLocaleDefinition = { + dateTime: '%A, %e de %B de %Y. %X', + date: '%d/%m/%Y', + time: '%H:%M:%S', + periods: ['AM', 'PM'], + days: [ + 'Domingo', + 'Segunda', + 'Terça', + 'Quarta', + 'Quinta', + 'Sexta', + 'Sábado', + ], + shortDays: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'], + months: [ + 'Janeiro', + 'Fevereiro', + 'Março', + 'Abril', + 'Maio', + 'Junho', + 'Julho', + 'Agosto', + 'Setembro', + 'Outubro', + 'Novembro', + 'Dezembro', + ], + shortMonths: [ + 'Jan', + 'Fev', + 'Mar', + 'Abr', + 'Mai', + 'Jun', + 'Jul', + 'Ago', + 'Set', + 'Out', + 'Nov', + 'Dez', + ], + }; + const formatter = createSmartDateVerboseFormatter(locale); - it('shows only year when 1st day of the year', () => { - expect(formatter(new Date('2020-01-01'))).toBe('2020'); - }); + it('is a function', () => { + expect(formatter).toBeInstanceOf(TimeFormatter); + }); - it('shows month and year when 1st of month', () => { - expect(formatter(new Date('2020-03-01'))).toBe('Mar 2020'); - }); + it('shows only year when 1st day of the year', () => { + expect(formatter(new Date('2020-01-01'))).toBe('2020'); + }); - it('shows weekday when any day of the month', () => { - expect(formatter(new Date('2020-03-03'))).toBe('Tue Mar 3'); - expect(formatter(new Date('2020-03-15'))).toBe('Sun Mar 15'); + it('shows month and year when 1st of month', () => { + expect(formatter(new Date('2020-04-01'))).toBe('Abr 2020'); + }); + + it('shows weekday when any day of the month', () => { + expect(formatter(new Date('2020-03-03'))).toBe('Ter Mar 3'); + expect(formatter(new Date('2020-03-15'))).toBe('Dom Mar 15'); + }); }); }); diff --git a/superset-frontend/packages/superset-ui-core/test/time-format/index.test.ts b/superset-frontend/packages/superset-ui-core/test/time-format/index.test.ts index ab2f3649fc..1ac7719c0a 100644 --- a/superset-frontend/packages/superset-ui-core/test/time-format/index.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/time-format/index.test.ts @@ -25,8 +25,12 @@ import { getTimeFormatterRegistry, LOCAL_PREFIX, PREVIEW_TIME, - smartDateFormatter, - smartDateVerboseFormatter, + SMART_DATE_ID, + SMART_DATE_VERBOSE_ID, + SMART_DATE_DETAILED_ID, + createSmartDateFormatter, + createSmartDateVerboseFormatter, + createSmartDateDetailedFormatter, TimeFormats, TimeFormatter, } from '@superset-ui/core'; @@ -41,8 +45,12 @@ describe('index', () => { getTimeFormatterRegistry, LOCAL_PREFIX, PREVIEW_TIME, - smartDateFormatter, - smartDateVerboseFormatter, + SMART_DATE_ID, + SMART_DATE_VERBOSE_ID, + SMART_DATE_DETAILED_ID, + createSmartDateFormatter, + createSmartDateVerboseFormatter, + createSmartDateDetailedFormatter, TimeFormats, TimeFormatter, ].forEach(x => expect(x).toBeDefined()); diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js index 81287b18ec..a454fe28b4 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js @@ -29,7 +29,7 @@ import { getTimeFormatter, isDefined, NumberFormats, - smartDateVerboseFormatter, + SMART_DATE_VERBOSE_ID, t, } from '@superset-ui/core'; @@ -82,6 +82,8 @@ const NO_DATA_RENDER_DATA = [ }, ]; +const smartDateVerboseFormatter = getTimeFormatter(SMART_DATE_VERBOSE_ID); + // Override the noData render function to make a prettier UX // Code adapted from https://github.com/novus/nvd3/blob/master/src/utils.js#L653 nv.utils.noData = function noData(chart, container) { diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.js index c3ef5a972e..e31f39be23 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.js @@ -19,7 +19,11 @@ import d3 from 'd3'; import d3tip from 'd3-tip'; import dompurify from 'dompurify'; -import { smartDateFormatter, getNumberFormatter } from '@superset-ui/core'; +import { + SMART_DATE_ID, + getTimeFormatter, + getNumberFormatter, +} from '@superset-ui/core'; // Regexp for the label added to time shifted series // (1 hour offset, 2 days offset, etc.) const TIME_SHIFT_PATTERN = /\d+ \w+ offset/; @@ -42,8 +46,8 @@ export function cleanColorInput(value) { * @param {*} format */ export function getTimeOrNumberFormatter(format) { - return format === 'smart_date' - ? smartDateFormatter + return format === SMART_DATE_ID + ? getTimeFormatter(SMART_DATE_ID) : getNumberFormatter(format); } diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.js index 6745ecdeb3..c4bb078fc7 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.js @@ -16,6 +16,12 @@ * specific language governing permissions and limitations * under the License. */ +import { + getTimeFormatterRegistry, + SMART_DATE_ID, + createSmartDateFormatter, +} from '@superset-ui/core'; + import { computeStackedYDomain, computeYDomain, @@ -111,6 +117,13 @@ const DATA_WITH_DISABLED_SERIES = [ ]; describe('nvd3/utils', () => { + beforeEach(() => { + getTimeFormatterRegistry().registerValue( + SMART_DATE_ID, + createSmartDateFormatter(), + ); + }); + describe('getTimeOrNumberFormatter(format)', () => { it('is a function', () => { expect(typeof getTimeOrNumberFormatter).toBe('function'); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts index f949182a51..981fd9fc0a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { GenericDataType, smartDateFormatter, t } from '@superset-ui/core'; +import { GenericDataType, SMART_DATE_ID, t } from '@superset-ui/core'; import { ControlPanelConfig, D3_FORMAT_DOCS, @@ -71,7 +71,7 @@ export default { renderTrigger: true, choices: D3_TIME_FORMAT_OPTIONS, description: D3_FORMAT_DOCS, - default: smartDateFormatter.id, + default: SMART_DATE_ID, }, }, ], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx index c596e94aef..04c6e74c69 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx @@ -20,7 +20,8 @@ import { PureComponent, MouseEvent } from 'react'; import { t, getNumberFormatter, - smartDateVerboseFormatter, + getTimeFormatter, + SMART_DATE_VERBOSE_ID, computeMaxFontSize, BRAND_COLOR, styled, @@ -45,7 +46,7 @@ class BigNumberVis extends PureComponent { static defaultProps = { className: '', headerFormatter: defaultNumberFormatter, - formatTime: smartDateVerboseFormatter, + formatTime: getTimeFormatter(SMART_DATE_VERBOSE_ID), headerFontSize: PROPORTION.HEADER, kickerFontSize: PROPORTION.KICKER, mainColor: BRAND_COLOR, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx index 86f8216d26..869583f847 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { smartDateFormatter, t } from '@superset-ui/core'; +import { SMART_DATE_ID, t } from '@superset-ui/core'; import { ControlPanelConfig, ControlSubSectionHeader, @@ -144,7 +144,7 @@ const config: ControlPanelConfig = { renderTrigger: true, choices: D3_TIME_FORMAT_OPTIONS, description: D3_FORMAT_DOCS, - default: smartDateFormatter.id, + default: SMART_DATE_ID, }, }, ], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/utils.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/utils.ts index b30e13d539..84fe31691b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/utils.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/utils.ts @@ -21,7 +21,7 @@ import moment from 'moment'; import { getTimeFormatter, getTimeFormatterForGranularity, - smartDateFormatter, + SMART_DATE_ID, TimeGranularity, } from '@superset-ui/core'; @@ -41,6 +41,6 @@ export const getDateFormatter = ( granularity?: TimeGranularity, fallbackFormat?: string | null, ) => - timeFormat === smartDateFormatter.id + timeFormat === SMART_DATE_ID ? getTimeFormatterForGranularity(granularity) : getTimeFormatter(timeFormat ?? fallbackFormat); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/formatters.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/formatters.ts index a8f9d2aa31..8d93783c8a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/formatters.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/formatters.ts @@ -25,12 +25,21 @@ import { isSavedMetric, NumberFormats, QueryFormMetric, - smartDateDetailedFormatter, - smartDateFormatter, + SMART_DATE_DETAILED_ID, + SMART_DATE_ID, + SMART_DATE_VERBOSE_ID, TimeFormatter, ValueFormatter, } from '@superset-ui/core'; +export const getSmartDateDetailedFormatter = () => + getTimeFormatter(SMART_DATE_DETAILED_ID); + +export const getSmartDateFormatter = () => getTimeFormatter(SMART_DATE_ID); + +export const getSmartDateVerboseFormatter = () => + getTimeFormatter(SMART_DATE_VERBOSE_ID); + export const getPercentFormatter = (format?: string) => getNumberFormatter( !format || format === NumberFormats.SMART_NUMBER @@ -68,8 +77,8 @@ export const getYAxisFormatter = ( export function getTooltipTimeFormatter( format?: string, ): TimeFormatter | StringConstructor { - if (format === smartDateFormatter.id) { - return smartDateDetailedFormatter; + if (format === SMART_DATE_ID) { + return getSmartDateDetailedFormatter(); } if (format) { return getTimeFormatter(format); @@ -80,7 +89,7 @@ export function getTooltipTimeFormatter( export function getXAxisFormatter( format?: string, ): TimeFormatter | StringConstructor | undefined { - if (format === smartDateFormatter.id || !format) { + if (format === SMART_DATE_ID || !format) { return undefined; } if (format) { diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx index 9f6164e944..d0042547f9 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx @@ -21,7 +21,7 @@ import { isAdhocColumn, isPhysicalColumn, QueryFormMetric, - smartDateFormatter, + SMART_DATE_ID, t, validateNonEmpty, } from '@superset-ui/core'; @@ -298,7 +298,7 @@ const config: ControlPanelConfig = { type: 'SelectControl', freeForm: true, label: t('Date format'), - default: smartDateFormatter.id, + default: SMART_DATE_ID, renderTrigger: true, choices: D3_TIME_FORMAT_OPTIONS, description: t('D3 time format for datetime columns'), diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts index b7f4451ce4..d8fd463bc0 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts @@ -24,7 +24,7 @@ import { getTimeFormatter, getTimeFormatterForGranularity, QueryFormData, - smartDateFormatter, + SMART_DATE_ID, TimeFormats, } from '@superset-ui/core'; import { getColorFormatters } from '@superset-ui/chart-controls'; @@ -120,7 +120,7 @@ export default function transformProps(chartProps: ChartProps) { temporalColname: string, ) => { let formatter: DateFormatter | undefined; - if (dateFormat === smartDateFormatter.id) { + if (dateFormat === SMART_DATE_ID) { if (granularity) { // time column use formats based on granularity formatter = getTimeFormatterForGranularity(granularity); diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index 41ca0a58a1..cd86dbbe88 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -25,7 +25,7 @@ import { isPhysicalColumn, QueryFormColumn, QueryMode, - smartDateFormatter, + SMART_DATE_ID, t, } from '@superset-ui/core'; import { @@ -361,7 +361,7 @@ const config: ControlPanelConfig = { type: 'SelectControl', freeForm: true, label: t('Timestamp format'), - default: smartDateFormatter.id, + default: SMART_DATE_ID, renderTrigger: true, clearable: false, choices: D3_TIME_FORMAT_OPTIONS, diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts index 92b23307ad..ec86814f98 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts @@ -28,7 +28,7 @@ import { getTimeFormatterForGranularity, NumberFormats, QueryMode, - smartDateFormatter, + SMART_DATE_ID, TimeFormats, TimeFormatter, } from '@superset-ui/core'; @@ -140,7 +140,7 @@ const processColumns = memoizeOne(function processColumns( const customFormat = config.d3TimeFormat || savedFormat; const timeFormat = customFormat || tableTimestampFormat; // When format is "Adaptive Formatting" (smart_date) - if (timeFormat === smartDateFormatter.id) { + if (timeFormat === SMART_DATE_ID) { if (granularity) { // time column use formats based on granularity formatter = getTimeFormatterForGranularity(granularity); diff --git a/superset-frontend/src/constants.ts b/superset-frontend/src/constants.ts index ed7d62dbe8..b7d34ad738 100644 --- a/superset-frontend/src/constants.ts +++ b/superset-frontend/src/constants.ts @@ -16,7 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { t, DEFAULT_D3_FORMAT } from '@superset-ui/core'; +import { + t, + DEFAULT_D3_FORMAT, + DEFAULT_D3_TIME_FORMAT, +} from '@superset-ui/core'; import { BootstrapData, CommonBootstrapData } from './types/bootstrapTypes'; @@ -184,6 +188,7 @@ export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = { }, }, d3_format: DEFAULT_D3_FORMAT, + d3_time_format: DEFAULT_D3_TIME_FORMAT, }; export const DEFAULT_BOOTSTRAP_DATA: BootstrapData = { diff --git a/superset-frontend/src/preamble.ts b/superset-frontend/src/preamble.ts index 52fa959905..c43afc76f8 100644 --- a/superset-frontend/src/preamble.ts +++ b/superset-frontend/src/preamble.ts @@ -61,7 +61,10 @@ setupColors( ); // Setup number formatters -setupFormatters(bootstrapData.common.d3_format); +setupFormatters( + bootstrapData.common.d3_format, + bootstrapData.common.d3_time_format, +); setupDashboardComponents(); diff --git a/superset-frontend/src/setup/setupFormatters.ts b/superset-frontend/src/setup/setupFormatters.ts index 964c94b794..e18aeba9dc 100644 --- a/superset-frontend/src/setup/setupFormatters.ts +++ b/superset-frontend/src/setup/setupFormatters.ts @@ -22,16 +22,22 @@ import { getNumberFormatterRegistry, NumberFormats, getTimeFormatterRegistry, - smartDateFormatter, - smartDateVerboseFormatter, + SMART_DATE_ID, + SMART_DATE_DETAILED_ID, + SMART_DATE_VERBOSE_ID, + createSmartDateFormatter, + createSmartDateVerboseFormatter, + createSmartDateDetailedFormatter, } from '@superset-ui/core'; import { FormatLocaleDefinition } from 'd3-format'; +import { TimeLocaleDefinition } from 'd3-time-format'; export default function setupFormatters( - d3Format: Partial, + d3NumberFormat: Partial, + d3TimeFormat: Partial, ) { getNumberFormatterRegistry() - .setD3Format(d3Format) + .setD3Format(d3NumberFormat) // 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')) @@ -72,8 +78,21 @@ export default function setupFormatters( createDurationFormatter({ formatSubMilliseconds: true }), ); - getTimeFormatterRegistry() - .registerValue('smart_date', smartDateFormatter) - .registerValue('smart_date_verbose', smartDateVerboseFormatter) - .setDefaultKey('smart_date'); + const timeFormatterRegistry = getTimeFormatterRegistry(); + + timeFormatterRegistry + .setD3Format(d3TimeFormat) + .registerValue( + SMART_DATE_ID, + createSmartDateFormatter(timeFormatterRegistry.d3Format), + ) + .registerValue( + SMART_DATE_VERBOSE_ID, + createSmartDateVerboseFormatter(timeFormatterRegistry.d3Format), + ) + .registerValue( + SMART_DATE_DETAILED_ID, + createSmartDateDetailedFormatter(timeFormatterRegistry.d3Format), + ) + .setDefaultKey(SMART_DATE_ID); } diff --git a/superset-frontend/src/types/bootstrapTypes.ts b/superset-frontend/src/types/bootstrapTypes.ts index 6c910b334c..d983f2378a 100644 --- a/superset-frontend/src/types/bootstrapTypes.ts +++ b/superset-frontend/src/types/bootstrapTypes.ts @@ -7,6 +7,7 @@ import { SequentialSchemeConfig, } from '@superset-ui/core'; import { FormatLocaleDefinition } from 'd3-format'; +import { TimeLocaleDefinition } from 'd3-time-format'; import { isPlainObject } from 'lodash'; import { Languages } from 'src/features/home/LanguagePicker'; import { FlashMessage } from '../components/FlashProvider'; @@ -152,6 +153,7 @@ export interface CommonBootstrapData { theme_overrides: JsonObject; menu_data: MenuData; d3_format: Partial; + d3_time_format: Partial; } export interface BootstrapData { diff --git a/superset/config.py b/superset/config.py index cb77982994..fa4ad4a29f 100644 --- a/superset/config.py +++ b/superset/config.py @@ -400,6 +400,38 @@ class D3Format(TypedDict, total=False): D3_FORMAT: D3Format = {} + +# Override the default d3 locale for time format +# Default values are equivalent to +# D3_TIME_FORMAT = { +# "dateTime": "%x, %X", +# "date": "%-m/%-d/%Y", +# "time": "%-I:%M:%S %p", +# "periods": ["AM", "PM"], +# "days": ["Sunday", "Monday", "Tuesday", "Wednesday", +# "Thursday", "Friday", "Saturday"], +# "shortDays": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], +# "months": ["January", "February", "March", "April", +# "May", "June", "July", "August", +# "September", "October", "November", "December"], +# "shortMonths": ["Jan", "Feb", "Mar", "Apr", +# "May", "Jun", "Jul", "Aug", +# "Sep", "Oct", "Nov", "Dec"] +# } +# https://github.com/d3/d3-time-format/tree/main#locales +class D3TimeFormat(TypedDict, total=False): + date: str + dateTime: str + time: str + periods: list[str] + days: list[str] + shortDays: list[str] + months: list[str] + shortMonths: list[str] + + +D3_TIME_FORMAT: D3TimeFormat = {} + CURRENCIES = ["USD", "EUR", "GBP", "INR", "MXN", "JPY", "CNY"] # --------------------------------------------------- diff --git a/superset/views/base.py b/superset/views/base.py index be3af99147..dc90b07728 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -419,6 +419,7 @@ def cached_common_bootstrap_data( # pylint: disable=unused-argument "locale": language, "language_pack": get_language_pack(language), "d3_format": conf.get("D3_FORMAT"), + "d3_time_format": conf.get("D3_TIME_FORMAT"), "currencies": conf.get("CURRENCIES"), "feature_flags": get_feature_flags(), "extra_sequential_color_schemes": conf["EXTRA_SEQUENTIAL_COLOR_SCHEMES"],