Skip to content

Commit a6cd01b

Browse files
committed
feat(playground): Use format API from client-core
1 parent 9fee612 commit a6cd01b

7 files changed

Lines changed: 119 additions & 153 deletions

File tree

packages/cubejs-client-core/src/format-d3-numeric-locale.ts

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,22 @@ import { formatLocale } from 'd3-format';
22

33
import type { FormatLocaleDefinition, FormatLocaleObject } from 'd3-format';
44

5-
import enUS from 'd3-format/locale/en-US.json';
6-
import enGB from 'd3-format/locale/en-GB.json';
7-
import zhCN from 'd3-format/locale/zh-CN.json';
8-
import esES from 'd3-format/locale/es-ES.json';
9-
import esMX from 'd3-format/locale/es-MX.json';
10-
import deDE from 'd3-format/locale/de-DE.json';
11-
import jaJP from 'd3-format/locale/ja-JP.json';
12-
import frFR from 'd3-format/locale/fr-FR.json';
13-
import ptBR from 'd3-format/locale/pt-BR.json';
14-
import koKR from 'd3-format/locale/ko-KR.json';
15-
import itIT from 'd3-format/locale/it-IT.json';
16-
import nlNL from 'd3-format/locale/nl-NL.json';
17-
import ruRU from 'd3-format/locale/ru-RU.json';
18-
195
// Pre-built d3 locale definitions for the most popular locales.
206
// Used as a fallback when Intl is unavailable (e.g. some edge runtimes).
21-
export const formatD3NumericLocale: Record<string, FormatLocaleDefinition> = {
22-
'en-US': enUS as unknown as FormatLocaleDefinition,
23-
'en-GB': enGB as unknown as FormatLocaleDefinition,
24-
'zh-CN': zhCN as unknown as FormatLocaleDefinition,
25-
'es-ES': esES as unknown as FormatLocaleDefinition,
26-
'es-MX': esMX as unknown as FormatLocaleDefinition,
27-
'de-DE': deDE as unknown as FormatLocaleDefinition,
28-
'ja-JP': jaJP as unknown as FormatLocaleDefinition,
29-
'fr-FR': frFR as unknown as FormatLocaleDefinition,
30-
'pt-BR': ptBR as unknown as FormatLocaleDefinition,
31-
'ko-KR': koKR as unknown as FormatLocaleDefinition,
32-
'it-IT': itIT as unknown as FormatLocaleDefinition,
33-
'nl-NL': nlNL as unknown as FormatLocaleDefinition,
34-
'ru-RU': ruRU as unknown as FormatLocaleDefinition,
7+
export const formatD3NumericLocale: Record<string, Omit<FormatLocaleDefinition, 'currency'>> = {
8+
'en-US': { decimal: '.', thousands: ',', grouping: [3] },
9+
'en-GB': { decimal: '.', thousands: ',', grouping: [3] },
10+
'zh-CN': { decimal: '.', thousands: ',', grouping: [3] },
11+
'es-ES': { decimal: ',', thousands: '.', grouping: [3] },
12+
'es-MX': { decimal: '.', thousands: ',', grouping: [3] },
13+
'de-DE': { decimal: ',', thousands: '.', grouping: [3] },
14+
'ja-JP': { decimal: '.', thousands: ',', grouping: [3] },
15+
'fr-FR': { decimal: ',', thousands: '\u00a0', grouping: [3], percent: '\u202f%' },
16+
'pt-BR': { decimal: ',', thousands: '.', grouping: [3] },
17+
'ko-KR': { decimal: '.', thousands: ',', grouping: [3] },
18+
'it-IT': { decimal: ',', thousands: '.', grouping: [3] },
19+
'nl-NL': { decimal: ',', thousands: '.', grouping: [3] },
20+
'ru-RU': { decimal: ',', thousands: '\u00a0', grouping: [3] },
3521
};
3622

3723
const currencySymbols: Record<string, string> = {
@@ -45,7 +31,7 @@ const currencySymbols: Record<string, string> = {
4531
RUB: '₽',
4632
};
4733

48-
function getCurrencySymbol(locale: string | undefined, currencyCode: string): [string, string] {
34+
function getCurrencyOverride(locale: string | undefined, currencyCode: string): [string, string] {
4935
try {
5036
const cf = new Intl.NumberFormat(locale, { style: 'currency', currency: currencyCode });
5137
const currencyParts = cf.formatToParts(1);
@@ -88,7 +74,7 @@ function getD3NumericLocaleFromIntl(locale: string, currencyCode = 'USD'): Forma
8874
decimal: find('decimal') || '.',
8975
thousands: find('group') || ',',
9076
grouping: deriveGrouping(locale),
91-
currency: getCurrencySymbol(locale, currencyCode),
77+
currency: getCurrencyOverride(locale, currencyCode),
9278
};
9379
}
9480

@@ -103,14 +89,17 @@ export function getD3NumericLocale(locale: string, currencyCode = 'USD'): Format
10389
let definition: FormatLocaleDefinition;
10490

10591
if (formatD3NumericLocale[locale]) {
106-
definition = { ...formatD3NumericLocale[locale], currency: getCurrencySymbol(locale, currencyCode) };
92+
definition = { ...formatD3NumericLocale[locale], currency: getCurrencyOverride(locale, currencyCode) };
10793
} else {
10894
try {
10995
definition = getD3NumericLocaleFromIntl(locale, currencyCode);
11096
} catch (e: unknown) {
11197
console.warn('Failed to generate d3 local via Intl, failing back to en-US', e);
11298

113-
definition = formatD3NumericLocale['en-US'];
99+
definition = {
100+
...formatD3NumericLocale['en-US'],
101+
currency: getCurrencyOverride(locale, currencyCode)
102+
};
114103
}
115104
}
116105

packages/cubejs-client-core/src/format.ts

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,29 @@ function detectLocale() {
2424

2525
const currentLocale = detectLocale();
2626

27-
const DEFAULT_DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S';
28-
const DEFAULT_DATE_FORMAT = '%Y-%m-%d';
29-
const DEFAULT_DATE_MONTH_FORMAT = '%Y-%m';
30-
const DEFAULT_DATE_QUARTER_FORMAT = '%Y-Q%q';
31-
const DEFAULT_DATE_YEAR_FORMAT = '%Y';
32-
33-
function getTimeFormatByGrain(grain: string | undefined): string {
34-
switch (grain) {
35-
case 'day':
36-
case 'week':
37-
return DEFAULT_DATE_FORMAT;
38-
case 'month':
39-
return DEFAULT_DATE_MONTH_FORMAT;
40-
case 'quarter':
41-
return DEFAULT_DATE_QUARTER_FORMAT;
42-
case 'year':
43-
return DEFAULT_DATE_YEAR_FORMAT;
44-
case 'second':
45-
case 'minute':
46-
case 'hour':
47-
default:
48-
return DEFAULT_DATETIME_FORMAT;
49-
}
27+
// d3-time-format patterns by granularity.
28+
const DATETIME_FORMAT_BY_GRANULARITY: Record<string, string> = {
29+
second: '%Y-%m-%d %H:%M:%S',
30+
minute: '%Y-%m-%d %H:%M',
31+
hour: '%Y-%m-%d %H:00',
32+
day: '%Y-%m-%d',
33+
week: '%Y-%m-%d W%V',
34+
month: '%Y %b',
35+
quarter: '%Y Q%q',
36+
year: '%Y',
37+
};
38+
39+
const DEFAULT_DATETIME_FORMAT = DATETIME_FORMAT_BY_GRANULARITY.second;
40+
41+
export function formatDateByGranularity(
42+
timestamp: Date | string | number,
43+
granularity?: string
44+
): string {
45+
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
46+
if (Number.isNaN(date.getTime())) return 'Invalid date';
47+
48+
const pattern = (granularity && DATETIME_FORMAT_BY_GRANULARITY[granularity]) || DEFAULT_DATETIME_FORMAT;
49+
return timeFormat(pattern)(date);
5050
}
5151

5252
function parseNumber(value: any): number {
@@ -136,10 +136,7 @@ export function formatValue(
136136

137137
// No explicit format — infer from type
138138
if (type === 'time') {
139-
const date = new Date(value);
140-
if (Number.isNaN(date.getTime())) return 'Invalid date';
141-
142-
return timeFormat(getTimeFormatByGrain(granularity))(date);
139+
return formatDateByGranularity(value, granularity);
143140
}
144141

145142
if (type === 'number') {

packages/cubejs-client-core/test/format.test.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2-
import { formatValue } from '../src/format';
2+
import { formatValue, formatDateByGranularity } from '../src/format';
33

44
describe('formatValue', () => {
55
it('format null', () => {
@@ -64,11 +64,12 @@ describe('formatValue', () => {
6464

6565
it('type-based fallback: time with grain', () => {
6666
expect(formatValue('2024-03-15T00:00:00.000', { type: 'time', granularity: 'day' })).toBe('2024-03-15');
67-
expect(formatValue('2024-03-01T00:00:00.000', { type: 'time', granularity: 'month' })).toBe('2024-03');
67+
expect(formatValue('2024-03-01T00:00:00.000', { type: 'time', granularity: 'month' })).toBe('2024 Mar');
6868
expect(formatValue('2024-01-01T00:00:00.000', { type: 'time', granularity: 'year' })).toBe('2024');
69-
expect(formatValue('2024-03-11T00:00:00.000', { type: 'time', granularity: 'week' })).toBe('2024-03-11');
70-
expect(formatValue('2024-03-01T00:00:00.000', { type: 'time', granularity: 'quarter' })).toBe('2024-Q1');
71-
expect(formatValue('2024-03-15T14:00:00.000', { type: 'time', granularity: 'hour' })).toBe('2024-03-15 14:00:00');
69+
expect(formatValue('2024-03-11T00:00:00.000', { type: 'time', granularity: 'week' })).toBe('2024-03-11 W11');
70+
expect(formatValue('2024-03-01T00:00:00.000', { type: 'time', granularity: 'quarter' })).toBe('2024 Q1');
71+
expect(formatValue('2024-03-15T14:00:00.000', { type: 'time', granularity: 'hour' })).toBe('2024-03-15 14:00');
72+
expect(formatValue('2024-03-15T14:30:00.000', { type: 'time', granularity: 'minute' })).toBe('2024-03-15 14:30');
7273
expect(formatValue('2024-03-15T14:30:45.000', { type: 'time' })).toBe('2024-03-15 14:30:45');
7374
});
7475

@@ -116,3 +117,33 @@ describe('formatValue', () => {
116117
expect(formatValue('0', { type: 'boolean' })).toBe('false');
117118
});
118119
});
120+
121+
describe('formatDateByGranularity', () => {
122+
it('formats each predefined granularity', () => {
123+
const iso = '2024-03-15T14:30:45.000';
124+
expect(formatDateByGranularity(iso, 'second')).toBe('2024-03-15 14:30:45');
125+
expect(formatDateByGranularity(iso, 'minute')).toBe('2024-03-15 14:30');
126+
expect(formatDateByGranularity(iso, 'hour')).toBe('2024-03-15 14:00');
127+
expect(formatDateByGranularity(iso, 'day')).toBe('2024-03-15');
128+
expect(formatDateByGranularity(iso, 'week')).toBe('2024-03-15 W11');
129+
expect(formatDateByGranularity(iso, 'month')).toBe('2024 Mar');
130+
expect(formatDateByGranularity(iso, 'quarter')).toBe('2024 Q1');
131+
expect(formatDateByGranularity(iso, 'year')).toBe('2024');
132+
});
133+
134+
it('accepts Date, ISO string, and epoch-number inputs', () => {
135+
const date = new Date('2024-03-15T00:00:00.000');
136+
expect(formatDateByGranularity(date, 'day')).toBe('2024-03-15');
137+
expect(formatDateByGranularity(date.getTime(), 'day')).toBe('2024-03-15');
138+
expect(formatDateByGranularity('2024-03-15T00:00:00.000', 'day')).toBe('2024-03-15');
139+
});
140+
141+
it('falls back to second-grain format for missing or unknown granularity', () => {
142+
expect(formatDateByGranularity('2024-03-15T14:30:45.000')).toBe('2024-03-15 14:30:45');
143+
expect(formatDateByGranularity('2024-03-15T14:30:45.000', 'decade' as any)).toBe('2024-03-15 14:30:45');
144+
});
145+
146+
it('returns "Invalid date" on bad input', () => {
147+
expect(formatDateByGranularity('not-a-date', 'day')).toBe('Invalid date');
148+
});
149+
});

packages/cubejs-playground/src/QueryBuilderV2/QueryBuilderResults.tsx

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
RightOutlined,
3131
} from '@ant-design/icons';
3232
import { QueryOrder, TimeDimensionGranularity } from '@cubejs-client/core';
33+
import { formatValue, formatDateByGranularity } from '@cubejs-client/core/format';
3334
import {
3435
AriaOptionProps,
3536
DroppableCollectionReorderEvent,
@@ -54,13 +55,11 @@ import {
5455
} from 'react-stately';
5556

5657
import { PREDEFINED_GRANULARITIES } from './values';
57-
import { formatCurrency, formatNumber } from './utils/formatters';
5858
import { useDeepMemo, useIntervalEffect } from './hooks';
5959
import { OutdatedLabel } from './components/OutdatedLabel';
6060
import { CopyButton } from './components/CopyButton';
6161
import { ListMemberButton } from './components/ListMemberButton';
6262
import { useQueryBuilderContext } from './context';
63-
import { formatDateByGranularity } from './utils/format-date-by-granularity';
6463
import { MemberBadge } from './components/Badge';
6564
import { MemberLabel } from './components/MemberLabel';
6665
import { areQueriesRelated } from './utils/query-helpers';
@@ -776,32 +775,19 @@ export function QueryBuilderResults({ forceMinHeight }: { forceMinHeight?: boole
776775
const type = typeof value !== 'string' ? typeof value : member.type;
777776

778777
switch (type) {
779-
case 'number':
780-
// @ts-ignore
781-
switch (member.format) {
782-
case 'currency':
783-
return [
784-
formatCurrency(typeof value === 'string' ? parseFloat(value) : value),
785-
'number',
786-
];
787-
case 'percent':
788-
return [
789-
`${formatNumber((typeof value === 'string' ? parseFloat(value) : value) * 100)}%`,
790-
'percent',
791-
];
792-
default:
793-
return [formatNumber(typeof value === 'string' ? parseFloat(value) : value), 'number'];
794-
}
778+
case 'number': {
779+
const kind = member.format === 'percent' ? 'percent' : 'number';
780+
return [
781+
formatValue(value, {
782+
type: 'number',
783+
format: member.format,
784+
currency: member.currency,
785+
}),
786+
kind,
787+
];
788+
}
795789
case 'time':
796-
try {
797-
if (granularity) {
798-
return [formatDateByGranularity(new Date(value), granularity), 'time'];
799-
} else {
800-
return [formatDateByGranularity(new Date(value), 'second'), 'time'];
801-
}
802-
} catch (e: any) {
803-
return [value, 'unknown'];
804-
}
790+
return [formatValue(value, { type: 'time', granularity }), 'time'];
805791
case 'boolean':
806792
return [value && value !== '0' && value !== 'false' ? '{{TRUE}}' : '{{FALSE}}', 'boolean'];
807793
default:

packages/cubejs-playground/src/QueryBuilderV2/components/ChartRenderer.tsx

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
minGranularityForIntervals,
66
isPredefinedGranularity,
77
} from '@cubejs-client/core';
8+
import { formatValue, formatDateByGranularity } from '@cubejs-client/core/format';
89
import { UseCubeQueryResult } from '@cubejs-client/react';
910
import { Skeleton, Tag, tasty } from '@cube-dev/ui-kit';
1011
import { ComponentType, memo, useCallback, useMemo } from 'react';
@@ -33,7 +34,7 @@ import {
3334
getChartColorByIndex,
3435
getChartSolidColorByIndex,
3536
} from '../utils/chart-colors';
36-
import { formatDateByGranularity, formatDateByPattern } from '../utils/index';
37+
import { formatDateByPattern } from '../utils/index';
3738

3839
import { LocalError } from './LocalError';
3940

@@ -455,23 +456,16 @@ const TypeToChartComponent = {
455456
}
456457
}
457458
: (text: any) => {
458-
switch (typeof text) {
459-
case 'boolean':
460-
return text ? 'true' : 'false';
461-
case 'undefined':
462-
case 'object':
463-
return text === null ? <Tag>NULL</Tag> : <Tag>OBJECT</Tag>;
464-
default:
465-
if (c.type === 'boolean') {
466-
return text && text !== '0' ? 'true' : 'false';
467-
}
468-
469-
if (c.format === 'percent' && text != null) {
470-
return `${(parseFloat(text) * 100).toFixed(2)}%`;
471-
}
472-
473-
return text;
474-
}
459+
if (text === null) return <Tag>NULL</Tag>;
460+
if (typeof text === 'object') return <Tag>OBJECT</Tag>;
461+
if (text === undefined) return '';
462+
463+
return formatValue(text, {
464+
type: c.type,
465+
format: c.format,
466+
currency: c.currency,
467+
granularity: c.granularity,
468+
});
475469
},
476470
};
477471
})}
Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,5 @@
1-
import { TimeDimensionGranularity } from '@cubejs-client/core';
21
import formatDate from 'date-fns/format';
32

4-
const FORMAT_MAP = {
5-
second: 'yyyy-LL-dd HH:mm:ss',
6-
minute: 'yyyy-LL-dd HH:mm',
7-
hour: 'yyyy-LL-dd HH:00',
8-
day: 'yyyy-LL-dd',
9-
week: "yyyy-LL-dd 'W'w",
10-
month: 'yyyy LLL',
11-
quarter: 'yyyy QQQ',
12-
year: 'yyyy',
13-
};
14-
15-
export function formatDateByGranularity(timestamp: Date, granularity?: TimeDimensionGranularity) {
16-
return formatDate(
17-
timestamp,
18-
FORMAT_MAP[(granularity as Exclude<TimeDimensionGranularity, string>) ?? 'second'] ??
19-
FORMAT_MAP['second']
20-
);
21-
}
22-
233
export function formatDateByPattern(timestamp: Date, format?: string) {
24-
return formatDate(timestamp, format ?? FORMAT_MAP['second']);
4+
return formatDate(timestamp, format ?? 'yyyy-LL-dd HH:mm:ss');
255
}

0 commit comments

Comments
 (0)