@opendata-ai/openchart-core 8.0.0-rc.9 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +363 -43
- package/dist/index.js +39 -18
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +1 -2
- package/schema/chart.schema.json +373 -67
- package/schema/table.schema.json +35 -1
- package/schema/vizspec.schema.json +950 -152
- package/src/accessibility/__tests__/alt-text.test.ts +0 -110
- package/src/accessibility/__tests__/aria.test.ts +0 -125
- package/src/accessibility/alt-text.ts +0 -155
- package/src/accessibility/aria.ts +0 -75
- package/src/accessibility/index.ts +0 -6
- package/src/colors/__tests__/colorblind.test.ts +0 -75
- package/src/colors/__tests__/contrast.test.ts +0 -119
- package/src/colors/__tests__/palettes.test.ts +0 -74
- package/src/colors/colorblind.ts +0 -131
- package/src/colors/contrast.ts +0 -143
- package/src/colors/index.ts +0 -36
- package/src/colors/palettes.ts +0 -206
- package/src/helpers/__tests__/deep-merge.test.ts +0 -54
- package/src/helpers/__tests__/spec-builders.test.ts +0 -338
- package/src/helpers/deep-merge.ts +0 -29
- package/src/helpers/spec-builders.ts +0 -479
- package/src/index.ts +0 -215
- package/src/labels/__tests__/collision.test.ts +0 -310
- package/src/labels/collision.ts +0 -249
- package/src/labels/index.ts +0 -13
- package/src/layout/__tests__/chrome.test.ts +0 -208
- package/src/layout/__tests__/text-measure.test.ts +0 -57
- package/src/layout/chrome.ts +0 -470
- package/src/layout/index.ts +0 -18
- package/src/layout/text-measure.ts +0 -404
- package/src/layout/text-wrap.ts +0 -93
- package/src/locale/__tests__/format.test.ts +0 -606
- package/src/locale/format.ts +0 -459
- package/src/locale/index.ts +0 -21
- package/src/responsive/__tests__/breakpoints.test.ts +0 -146
- package/src/responsive/__tests__/metrics.test.ts +0 -52
- package/src/responsive/breakpoints.ts +0 -177
- package/src/responsive/index.ts +0 -53
- package/src/responsive/metrics.ts +0 -299
- package/src/schema/__tests__/generated-artifacts.test.ts +0 -42
- package/src/schema/__tests__/schema.test.ts +0 -179
- package/src/styles/__tests__/reduced-motion-selectors.test.ts +0 -28
- package/src/styles/animation.css +0 -213
- package/src/styles/base.css +0 -102
- package/src/styles/chrome.css +0 -174
- package/src/styles/dark.css +0 -44
- package/src/styles/graph.css +0 -135
- package/src/styles/index.css +0 -28
- package/src/styles/keyframes.css +0 -151
- package/src/styles/legend.css +0 -18
- package/src/styles/reduced-motion.css +0 -67
- package/src/styles/series-search.css +0 -153
- package/src/styles/sparkline.css +0 -16
- package/src/styles/table-animation.css +0 -93
- package/src/styles/table.css +0 -398
- package/src/styles/token-definitions.ts +0 -199
- package/src/styles/tokens.css +0 -156
- package/src/styles/tooltip.css +0 -126
- package/src/styles/you-draw-it.css +0 -126
- package/src/theme/__tests__/dark-mode.test.ts +0 -232
- package/src/theme/__tests__/defaults.test.ts +0 -73
- package/src/theme/__tests__/resolve.test.ts +0 -277
- package/src/theme/dark-mode.ts +0 -258
- package/src/theme/defaults.ts +0 -110
- package/src/theme/index.ts +0 -8
- package/src/theme/presets.ts +0 -64
- package/src/theme/resolve.ts +0 -312
- package/src/types/__tests__/encoding.test.ts +0 -368
- package/src/types/__tests__/gradient.test.ts +0 -95
- package/src/types/__tests__/spec.test.ts +0 -499
- package/src/types/encoding.ts +0 -280
- package/src/types/events.ts +0 -236
- package/src/types/index.ts +0 -290
- package/src/types/layout.ts +0 -2024
- package/src/types/schema-roots.ts +0 -32
- package/src/types/spec.ts +0 -2925
- package/src/types/table.ts +0 -108
- package/src/types/theme.ts +0 -209
|
@@ -1,606 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
abbreviateNumber,
|
|
4
|
-
buildCompactStepFormatter,
|
|
5
|
-
buildD3Formatter,
|
|
6
|
-
buildTemporalFormatter,
|
|
7
|
-
computeFieldFormatContext,
|
|
8
|
-
defaultNumberFormatter,
|
|
9
|
-
formatCurrency,
|
|
10
|
-
formatDate,
|
|
11
|
-
formatNumber,
|
|
12
|
-
formatOrdinal,
|
|
13
|
-
formatPercent,
|
|
14
|
-
isYearContext,
|
|
15
|
-
isYearLikeValues,
|
|
16
|
-
resolveNumberFormatter,
|
|
17
|
-
} from '../format';
|
|
18
|
-
|
|
19
|
-
describe('formatNumber', () => {
|
|
20
|
-
it('formats integers with commas', () => {
|
|
21
|
-
expect(formatNumber(1500000)).toBe('1,500,000');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('formats decimals to 2 places', () => {
|
|
25
|
-
expect(formatNumber(42.567)).toBe('42.57');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('handles zero', () => {
|
|
29
|
-
expect(formatNumber(0)).toBe('0');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('handles negative numbers', () => {
|
|
33
|
-
// d3-format uses unicode minus sign (U+2212), not ASCII hyphen-minus
|
|
34
|
-
expect(formatNumber(-1234)).toBe('−1,234');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('handles Infinity', () => {
|
|
38
|
-
expect(formatNumber(Infinity)).toBe('Infinity');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('handles NaN', () => {
|
|
42
|
-
expect(formatNumber(NaN)).toBe('NaN');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('preserves small decimals instead of rounding to 0.00', () => {
|
|
46
|
-
expect(formatNumber(0.0034)).toBe('0.0034');
|
|
47
|
-
expect(formatNumber(0.000012)).toBe('0.000012');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('still uses ,.2f for normal-sized decimals', () => {
|
|
51
|
-
expect(formatNumber(0.5)).toBe('0.50');
|
|
52
|
-
expect(formatNumber(3.14567)).toBe('3.15');
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('abbreviateNumber', () => {
|
|
57
|
-
it('abbreviates millions', () => {
|
|
58
|
-
expect(abbreviateNumber(1500000)).toBe('1.5M');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('abbreviates billions', () => {
|
|
62
|
-
expect(abbreviateNumber(2300000000)).toBe('2.3B');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('abbreviates thousands with lowercase k', () => {
|
|
66
|
-
expect(abbreviateNumber(2300)).toBe('2.3k');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('drops trailing .0', () => {
|
|
70
|
-
expect(abbreviateNumber(1000000)).toBe('1M');
|
|
71
|
-
expect(abbreviateNumber(2000)).toBe('2k');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('does not abbreviate small numbers', () => {
|
|
75
|
-
expect(abbreviateNumber(42)).toBe('42');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('handles negative numbers', () => {
|
|
79
|
-
expect(abbreviateNumber(-1500000)).toBe('-1.5M');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('abbreviates trillions', () => {
|
|
83
|
-
expect(abbreviateNumber(1_200_000_000_000)).toBe('1.2T');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('trims 1020000 to 1M', () => {
|
|
87
|
-
expect(abbreviateNumber(1020000)).toBe('1M');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('renders 18900 as 18.9k', () => {
|
|
91
|
-
expect(abbreviateNumber(18900)).toBe('18.9k');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('renders 1000 as 1k', () => {
|
|
95
|
-
expect(abbreviateNumber(1000)).toBe('1k');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('rolls up 999999 to 1M', () => {
|
|
99
|
-
expect(abbreviateNumber(999999)).toBe('1M');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('rolls up 999999999 to 1B', () => {
|
|
103
|
-
expect(abbreviateNumber(999999999)).toBe('1B');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('rolls up 999950 to 1M (rounding boundary)', () => {
|
|
107
|
-
expect(abbreviateNumber(999950)).toBe('1M');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('keeps 999949 as 999.9k (below roll-up boundary)', () => {
|
|
111
|
-
expect(abbreviateNumber(999949)).toBe('999.9k');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('handles -1500000', () => {
|
|
115
|
-
expect(abbreviateNumber(-1500000)).toBe('-1.5M');
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('formatPercent', () => {
|
|
120
|
-
it('formats fractions (default) with .1~%', () => {
|
|
121
|
-
expect(formatPercent(0.847)).toBe('84.7%');
|
|
122
|
-
expect(formatPercent(0.5)).toBe('50%');
|
|
123
|
-
expect(formatPercent(1)).toBe('100%');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('formats pre-scaled values with fraction: false', () => {
|
|
127
|
-
expect(formatPercent(84.7, { fraction: false })).toBe('84.7%');
|
|
128
|
-
expect(formatPercent(1200, { fraction: false })).toBe('1,200%');
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe('formatCurrency', () => {
|
|
133
|
-
it('compact mode abbreviates >= 1000', () => {
|
|
134
|
-
expect(formatCurrency(1900, { compact: true })).toBe('$1.9k');
|
|
135
|
-
expect(formatCurrency(12666, { compact: true })).toBe('$12.7k');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('compact mode uses ASCII minus for negatives', () => {
|
|
139
|
-
expect(formatCurrency(-1900, { compact: true })).toBe('-$1.9k');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('compact mode keeps small numbers full', () => {
|
|
143
|
-
expect(formatCurrency(42, { compact: true })).toBe('$42');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('full mode (default) formats integers without cents', () => {
|
|
147
|
-
expect(formatCurrency(12666)).toBe('$12,666');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('full mode uses ASCII minus for negatives', () => {
|
|
151
|
-
expect(formatCurrency(-12666)).toBe('-$12,666');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('full mode includes cents for non-integers', () => {
|
|
155
|
-
expect(formatCurrency(12666.5)).toBe('$12,666.50');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('handles negative non-integer in full mode', () => {
|
|
159
|
-
expect(formatCurrency(-42.5)).toBe('-$42.50');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('handles negative non-integer in compact mode', () => {
|
|
163
|
-
expect(formatCurrency(-42.5, { compact: true })).toBe('-$42.50');
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('isYearLikeValues', () => {
|
|
168
|
-
it('returns true for year-like integer arrays', () => {
|
|
169
|
-
expect(isYearLikeValues([1500, 2024, 2500])).toBe(true);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('returns false for empty arrays', () => {
|
|
173
|
-
expect(isYearLikeValues([])).toBe(false);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('returns false when values are below 1500', () => {
|
|
177
|
-
expect(isYearLikeValues([1499])).toBe(false);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('returns false when values are above 2500', () => {
|
|
181
|
-
expect(isYearLikeValues([2501])).toBe(false);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('returns false for non-integer values', () => {
|
|
185
|
-
expect(isYearLikeValues([2024.5])).toBe(false);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('returns false when range includes zero', () => {
|
|
189
|
-
expect(isYearLikeValues([0, 2024])).toBe(false);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('returns false for negative values', () => {
|
|
193
|
-
expect(isYearLikeValues([-2000])).toBe(false);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe('computeFieldFormatContext', () => {
|
|
198
|
-
it('computes extent and allIntegers from numeric values', () => {
|
|
199
|
-
const ctx = computeFieldFormatContext([10, 20, 30]);
|
|
200
|
-
expect(ctx.extent).toEqual([10, 30]);
|
|
201
|
-
expect(ctx.allIntegers).toBe(true);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('detects non-integers', () => {
|
|
205
|
-
const ctx = computeFieldFormatContext([10, 20.5, 30]);
|
|
206
|
-
expect(ctx.allIntegers).toBe(false);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('coerces numeric strings', () => {
|
|
210
|
-
const ctx = computeFieldFormatContext(['10', '20', '30']);
|
|
211
|
-
expect(ctx.extent).toEqual([10, 30]);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('skips non-finite values', () => {
|
|
215
|
-
const ctx = computeFieldFormatContext([10, NaN, null, undefined, 30]);
|
|
216
|
-
expect(ctx.extent).toEqual([10, 30]);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('returns empty context for empty input', () => {
|
|
220
|
-
const ctx = computeFieldFormatContext([]);
|
|
221
|
-
expect(ctx.extent).toBeUndefined();
|
|
222
|
-
expect(ctx.allIntegers).toBeUndefined();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('sets surface when provided', () => {
|
|
226
|
-
const ctx = computeFieldFormatContext([1, 2], 'table');
|
|
227
|
-
expect(ctx.surface).toBe('table');
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
describe('isYearContext', () => {
|
|
232
|
-
it('returns true for year-like integer ranges', () => {
|
|
233
|
-
expect(isYearContext({ extent: [1990, 2024], allIntegers: true })).toBe(true);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('returns false when non-integers', () => {
|
|
237
|
-
expect(isYearContext({ extent: [1990, 2024], allIntegers: false })).toBe(false);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('returns false when out of range', () => {
|
|
241
|
-
expect(isYearContext({ extent: [0, 2024], allIntegers: true })).toBe(false);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('returns false for undefined ctx', () => {
|
|
245
|
-
expect(isYearContext(undefined)).toBe(false);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
describe('resolveNumberFormatter', () => {
|
|
250
|
-
it('returns null for empty string', () => {
|
|
251
|
-
expect(resolveNumberFormatter('')).toBeNull();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('returns null for undefined', () => {
|
|
255
|
-
expect(resolveNumberFormatter(undefined)).toBeNull();
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('resolves ordinal keyword', () => {
|
|
259
|
-
const fmt = resolveNumberFormatter('ordinal');
|
|
260
|
-
expect(fmt!(1)).toBe('1st');
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('resolves percent keyword with fraction detection (extent <= 1)', () => {
|
|
264
|
-
const fmt = resolveNumberFormatter('percent', { extent: [0, 0.9] });
|
|
265
|
-
expect(fmt!(0.847)).toBe('84.7%');
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('resolves percent keyword with pre-scaled detection (extent > 1)', () => {
|
|
269
|
-
const fmt = resolveNumberFormatter('percent', { extent: [0, 98] });
|
|
270
|
-
expect(fmt!(84.7)).toBe('84.7%');
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('resolves currency keyword for tables (full precision)', () => {
|
|
274
|
-
const fmt = resolveNumberFormatter('currency', { surface: 'table' });
|
|
275
|
-
expect(fmt!(12666)).toBe('$12,666');
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('resolves currency keyword for charts (compact)', () => {
|
|
279
|
-
const fmt = resolveNumberFormatter('currency');
|
|
280
|
-
expect(fmt!(1900)).toBe('$1.9k');
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('resolves d3 format strings', () => {
|
|
284
|
-
const fmt = resolveNumberFormatter('$,.0f');
|
|
285
|
-
expect(fmt!(1234)).toBe('$1,234');
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('currency with step uses step-derived decimals', () => {
|
|
289
|
-
const fmt = resolveNumberFormatter('currency', {
|
|
290
|
-
step: 500000,
|
|
291
|
-
stepReference: 2000000,
|
|
292
|
-
});
|
|
293
|
-
expect(fmt!(1500000)).toBe('$1.5M');
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
describe('defaultNumberFormatter', () => {
|
|
298
|
-
it('contextless: per-value year check (ARIA path)', () => {
|
|
299
|
-
const fmt = defaultNumberFormatter();
|
|
300
|
-
expect(fmt(2024)).toBe('2024');
|
|
301
|
-
expect(fmt(2600)).toBe('2.6k');
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('year context returns bare year string', () => {
|
|
305
|
-
const fmt = defaultNumberFormatter({ extent: [1990, 2024], allIntegers: true });
|
|
306
|
-
expect(fmt(2024)).toBe('2024');
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it('table surface returns full precision', () => {
|
|
310
|
-
const fmt = defaultNumberFormatter({ surface: 'table' });
|
|
311
|
-
expect(fmt(1020000)).toBe('1,020,000');
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it('step-aware compact when step and reference >= 1000', () => {
|
|
315
|
-
const fmt = defaultNumberFormatter({ step: 500000, stepReference: 2000000 });
|
|
316
|
-
expect(fmt(500000)).toBe('500k');
|
|
317
|
-
expect(fmt(1000000)).toBe('1M');
|
|
318
|
-
expect(fmt(1500000)).toBe('1.5M');
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('per-value compact/formatNumber for chart surface', () => {
|
|
322
|
-
const fmt = defaultNumberFormatter({ extent: [0, 2000000] });
|
|
323
|
-
expect(fmt(1020000)).toBe('1M');
|
|
324
|
-
expect(fmt(42)).toBe('42');
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('does not apply per-value year check when context has extent', () => {
|
|
328
|
-
const fmt = defaultNumberFormatter({ extent: [-2000, 2000], allIntegers: true });
|
|
329
|
-
expect(fmt(2000)).toBe('2k');
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
describe('buildCompactStepFormatter', () => {
|
|
334
|
-
it('formats per-value units with step-derived decimals', () => {
|
|
335
|
-
const fmt = buildCompactStepFormatter(500000);
|
|
336
|
-
expect(fmt(0)).toBe('0');
|
|
337
|
-
expect(fmt(500000)).toBe('500k');
|
|
338
|
-
expect(fmt(1000000)).toBe('1M');
|
|
339
|
-
expect(fmt(1500000)).toBe('1.5M');
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('step-derived decimals prevent misleading rounding', () => {
|
|
343
|
-
const fmt = buildCompactStepFormatter(1250000);
|
|
344
|
-
expect(fmt(1250000)).toBe('1.25M');
|
|
345
|
-
expect(fmt(2500000)).toBe('2.5M');
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it('degenerate narrow domain falls back to comma-grouped', () => {
|
|
349
|
-
const fmt = buildCompactStepFormatter(1);
|
|
350
|
-
expect(fmt(2021)).toBe('2,021');
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it('handles non-finite values', () => {
|
|
354
|
-
const fmt = buildCompactStepFormatter(500000);
|
|
355
|
-
expect(fmt(Infinity)).toBe('Infinity');
|
|
356
|
-
expect(fmt(NaN)).toBe('NaN');
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it('sub-1k ticks render plain', () => {
|
|
360
|
-
const fmt = buildCompactStepFormatter(200);
|
|
361
|
-
expect(fmt(200)).toBe('200');
|
|
362
|
-
expect(fmt(400)).toBe('400');
|
|
363
|
-
expect(fmt(1000)).toBe('1k');
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
describe('buildD3Formatter', () => {
|
|
368
|
-
it('returns a formatter for a valid d3 format string', () => {
|
|
369
|
-
const fmt = buildD3Formatter('$,.0f');
|
|
370
|
-
expect(fmt).not.toBeNull();
|
|
371
|
-
expect(fmt!(1234)).toBe('$1,234');
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it('handles tilde trim modifier', () => {
|
|
375
|
-
const fmt = buildD3Formatter('$,.2~f');
|
|
376
|
-
expect(fmt).not.toBeNull();
|
|
377
|
-
expect(fmt!(3.1)).toBe('$3.1');
|
|
378
|
-
expect(fmt!(3.75)).toBe('$3.75');
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it('handles literal alpha suffix after d3 format', () => {
|
|
382
|
-
const fmt = buildD3Formatter('$,.2~fT');
|
|
383
|
-
expect(fmt).not.toBeNull();
|
|
384
|
-
expect(fmt!(3.75)).toBe('$3.75T');
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it('handles non-alpha suffix like %', () => {
|
|
388
|
-
const fmt = buildD3Formatter('.0f%');
|
|
389
|
-
expect(fmt).not.toBeNull();
|
|
390
|
-
expect(fmt!(50)).toBe('50%');
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it('returns null for undefined input', () => {
|
|
394
|
-
expect(buildD3Formatter(undefined)).toBeNull();
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it('returns null for empty string', () => {
|
|
398
|
-
expect(buildD3Formatter('')).toBeNull();
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('returns null for completely invalid format', () => {
|
|
402
|
-
expect(buildD3Formatter('not-a-format!!!')).toBeNull();
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it('$~s formats low thousands with SI suffix', () => {
|
|
406
|
-
const fmt = buildD3Formatter('$~s');
|
|
407
|
-
expect(fmt).not.toBeNull();
|
|
408
|
-
expect(fmt!(6000)).toBe('$6k');
|
|
409
|
-
expect(fmt!(7000)).toBe('$7k');
|
|
410
|
-
expect(fmt!(14000)).toBe('$14k');
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it('$~s replaces SI "G" with "B" for billions', () => {
|
|
414
|
-
const fmt = buildD3Formatter('$~s');
|
|
415
|
-
expect(fmt).not.toBeNull();
|
|
416
|
-
expect(fmt!(1_500_000_000)).toBe('$1.5B');
|
|
417
|
-
expect(fmt!(2_000_000_000)).toBe('$2B');
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it("'ordinal' returns the rank-axis ordinal formatter", () => {
|
|
421
|
-
const fmt = buildD3Formatter('ordinal');
|
|
422
|
-
expect(fmt).not.toBeNull();
|
|
423
|
-
expect(fmt!(1)).toBe('1st');
|
|
424
|
-
expect(fmt!(6)).toBe('6th');
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
describe('formatOrdinal', () => {
|
|
429
|
-
it('formats 1, 2, 3 with st/nd/rd suffixes', () => {
|
|
430
|
-
expect(formatOrdinal(1)).toBe('1st');
|
|
431
|
-
expect(formatOrdinal(2)).toBe('2nd');
|
|
432
|
-
expect(formatOrdinal(3)).toBe('3rd');
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it('formats 4 through 10 with th', () => {
|
|
436
|
-
expect(formatOrdinal(4)).toBe('4th');
|
|
437
|
-
expect(formatOrdinal(10)).toBe('10th');
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
it('uses th for the 11-13 teens, including 111-113', () => {
|
|
441
|
-
expect(formatOrdinal(11)).toBe('11th');
|
|
442
|
-
expect(formatOrdinal(12)).toBe('12th');
|
|
443
|
-
expect(formatOrdinal(13)).toBe('13th');
|
|
444
|
-
expect(formatOrdinal(111)).toBe('111th');
|
|
445
|
-
expect(formatOrdinal(112)).toBe('112th');
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it('applies st/nd/rd past the teens', () => {
|
|
449
|
-
expect(formatOrdinal(21)).toBe('21st');
|
|
450
|
-
expect(formatOrdinal(22)).toBe('22nd');
|
|
451
|
-
expect(formatOrdinal(23)).toBe('23rd');
|
|
452
|
-
expect(formatOrdinal(101)).toBe('101st');
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
it('rounds non-integers to the nearest rank', () => {
|
|
456
|
-
expect(formatOrdinal(2.4)).toBe('2nd');
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it('falls back to String() for non-finite values', () => {
|
|
460
|
-
expect(formatOrdinal(Number.NaN)).toBe('NaN');
|
|
461
|
-
expect(formatOrdinal(Number.POSITIVE_INFINITY)).toBe('Infinity');
|
|
462
|
-
});
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
describe('formatDate', () => {
|
|
466
|
-
it('formats a Date object', () => {
|
|
467
|
-
const result = formatDate(new Date('2020-06-15'), undefined, 'day');
|
|
468
|
-
expect(result).toContain('2020');
|
|
469
|
-
expect(result).toContain('Jun');
|
|
470
|
-
expect(result).toContain('15');
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it('formats an ISO string', () => {
|
|
474
|
-
const result = formatDate('2020-01-01', undefined, 'year');
|
|
475
|
-
expect(result).toBe('2020');
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
it('formats quarters', () => {
|
|
479
|
-
const result = formatDate(new Date('2020-04-15'), undefined, 'quarter');
|
|
480
|
-
expect(result).toBe('Q2 2020');
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
it('formats months', () => {
|
|
484
|
-
const result = formatDate(new Date('2020-03-01'), undefined, 'month');
|
|
485
|
-
expect(result).toContain('Mar');
|
|
486
|
-
expect(result).toContain('2020');
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it('handles invalid dates gracefully', () => {
|
|
490
|
-
const result = formatDate('not-a-date');
|
|
491
|
-
expect(result).toBe('not-a-date');
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
it('infers year granularity for bare year strings regardless of timezone', () => {
|
|
495
|
-
const result = formatDate('2020');
|
|
496
|
-
expect(result).toBe('2020');
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
it('infers year granularity for Jan 1 ISO dates', () => {
|
|
500
|
-
const result = formatDate('2020-01-01');
|
|
501
|
-
expect(result).toBe('2020');
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
it('infers month granularity for first-of-month dates', () => {
|
|
505
|
-
const result = formatDate('2020-06-01');
|
|
506
|
-
expect(result).toContain('Jun');
|
|
507
|
-
expect(result).toContain('2020');
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
it('infers day granularity for mid-month dates', () => {
|
|
511
|
-
const result = formatDate('2020-06-15');
|
|
512
|
-
expect(result).toContain('15');
|
|
513
|
-
expect(result).toContain('Jun');
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
describe('compact formats', () => {
|
|
517
|
-
it('formats months as bare month name', () => {
|
|
518
|
-
expect(formatDate('2025-02-01', undefined, undefined, true, true)).toBe('Feb');
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
it('infers year granularity at year boundaries (compact stays %Y)', () => {
|
|
522
|
-
expect(formatDate('2025-01-01', undefined, undefined, true, true)).toBe('2025');
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
it('formats days without the year', () => {
|
|
526
|
-
expect(formatDate('2025-03-05', undefined, undefined, true, true)).toBe('Mar 05');
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
it('formats quarters with apostrophe year', () => {
|
|
530
|
-
expect(formatDate('2025-04-15', undefined, 'quarter', true, true)).toBe("Q2 '25");
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
it('formats quarters with local-time year when useUtc is false', () => {
|
|
534
|
-
expect(formatDate(new Date(2025, 4, 15), undefined, 'quarter', false, true)).toBe("Q2 '25");
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
it('formats weeks as month and day', () => {
|
|
538
|
-
expect(formatDate('2025-03-10', undefined, 'week', true, true)).toBe('Mar 10');
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
it('formats hours as time only', () => {
|
|
542
|
-
expect(formatDate('2025-03-05T14:00:00Z', undefined, undefined, true, true)).toBe('14:00');
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
it('formats minutes as time only', () => {
|
|
546
|
-
expect(formatDate('2025-03-05T14:30:00Z', undefined, undefined, true, true)).toBe('14:30');
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
it('default (compact: false) output is unchanged', () => {
|
|
550
|
-
expect(formatDate('2025-02-01')).toBe('Feb 2025');
|
|
551
|
-
expect(formatDate('2025-03-05')).toBe('Mar 05, 2025');
|
|
552
|
-
expect(formatDate('2025-04-15', undefined, 'quarter')).toBe('Q2 2025');
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
it('full quarter year follows useUtc at year boundaries', () => {
|
|
556
|
-
expect(formatDate(new Date('2026-01-01T00:00:00Z'), undefined, 'quarter')).toBe('Q1 2026');
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
describe('buildTemporalFormatter', () => {
|
|
562
|
-
it('returns null for undefined format', () => {
|
|
563
|
-
expect(buildTemporalFormatter(undefined)).toBeNull();
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
it('returns null for empty string', () => {
|
|
567
|
-
expect(buildTemporalFormatter('')).toBeNull();
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
it('formats dates with %Y to just the year', () => {
|
|
571
|
-
const fmt = buildTemporalFormatter('%Y');
|
|
572
|
-
expect(fmt).not.toBeNull();
|
|
573
|
-
expect(fmt!('2020-01-01')).toBe('2020');
|
|
574
|
-
expect(fmt!(new Date('2020-06-15'))).toBe('2020');
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
it('formats dates with %b %Y to month and year', () => {
|
|
578
|
-
const fmt = buildTemporalFormatter('%b %Y');
|
|
579
|
-
expect(fmt).not.toBeNull();
|
|
580
|
-
expect(fmt!('2020-06-01')).toBe('Jun 2020');
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
it('formats dates with full date format', () => {
|
|
584
|
-
const fmt = buildTemporalFormatter('%Y-%m-%d');
|
|
585
|
-
expect(fmt).not.toBeNull();
|
|
586
|
-
expect(fmt!('2020-06-15')).toBe('2020-06-15');
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
it('handles invalid date input gracefully', () => {
|
|
590
|
-
const fmt = buildTemporalFormatter('%Y');
|
|
591
|
-
expect(fmt).not.toBeNull();
|
|
592
|
-
expect(fmt!('not-a-date')).toBe('not-a-date');
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
it('handles Date objects', () => {
|
|
596
|
-
const fmt = buildTemporalFormatter('%Y');
|
|
597
|
-
expect(fmt).not.toBeNull();
|
|
598
|
-
expect(fmt!(new Date('2025-01-01T00:00:00Z'))).toBe('2025');
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
it('handles numeric timestamps', () => {
|
|
602
|
-
const fmt = buildTemporalFormatter('%Y');
|
|
603
|
-
expect(fmt).not.toBeNull();
|
|
604
|
-
expect(fmt!(1577836800000)).toBe('2020');
|
|
605
|
-
});
|
|
606
|
-
});
|