@opendata-ai/openchart-core 8.0.0-rc.9 → 8.1.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.
Files changed (83) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +403 -45
  3. package/dist/index.js +50 -18
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/dist/styles.css.map +1 -1
  7. package/package.json +1 -2
  8. package/schema/chart.schema.json +375 -69
  9. package/schema/table.schema.json +35 -1
  10. package/schema/vizspec.schema.json +992 -171
  11. package/src/accessibility/__tests__/alt-text.test.ts +0 -110
  12. package/src/accessibility/__tests__/aria.test.ts +0 -125
  13. package/src/accessibility/alt-text.ts +0 -155
  14. package/src/accessibility/aria.ts +0 -75
  15. package/src/accessibility/index.ts +0 -6
  16. package/src/colors/__tests__/colorblind.test.ts +0 -75
  17. package/src/colors/__tests__/contrast.test.ts +0 -119
  18. package/src/colors/__tests__/palettes.test.ts +0 -74
  19. package/src/colors/colorblind.ts +0 -131
  20. package/src/colors/contrast.ts +0 -143
  21. package/src/colors/index.ts +0 -36
  22. package/src/colors/palettes.ts +0 -206
  23. package/src/helpers/__tests__/deep-merge.test.ts +0 -54
  24. package/src/helpers/__tests__/spec-builders.test.ts +0 -338
  25. package/src/helpers/deep-merge.ts +0 -29
  26. package/src/helpers/spec-builders.ts +0 -479
  27. package/src/index.ts +0 -215
  28. package/src/labels/__tests__/collision.test.ts +0 -310
  29. package/src/labels/collision.ts +0 -249
  30. package/src/labels/index.ts +0 -13
  31. package/src/layout/__tests__/chrome.test.ts +0 -208
  32. package/src/layout/__tests__/text-measure.test.ts +0 -57
  33. package/src/layout/chrome.ts +0 -470
  34. package/src/layout/index.ts +0 -18
  35. package/src/layout/text-measure.ts +0 -404
  36. package/src/layout/text-wrap.ts +0 -93
  37. package/src/locale/__tests__/format.test.ts +0 -606
  38. package/src/locale/format.ts +0 -459
  39. package/src/locale/index.ts +0 -21
  40. package/src/responsive/__tests__/breakpoints.test.ts +0 -146
  41. package/src/responsive/__tests__/metrics.test.ts +0 -52
  42. package/src/responsive/breakpoints.ts +0 -177
  43. package/src/responsive/index.ts +0 -53
  44. package/src/responsive/metrics.ts +0 -299
  45. package/src/schema/__tests__/generated-artifacts.test.ts +0 -42
  46. package/src/schema/__tests__/schema.test.ts +0 -179
  47. package/src/styles/__tests__/reduced-motion-selectors.test.ts +0 -28
  48. package/src/styles/animation.css +0 -213
  49. package/src/styles/base.css +0 -102
  50. package/src/styles/chrome.css +0 -174
  51. package/src/styles/dark.css +0 -44
  52. package/src/styles/graph.css +0 -135
  53. package/src/styles/index.css +0 -28
  54. package/src/styles/keyframes.css +0 -151
  55. package/src/styles/legend.css +0 -18
  56. package/src/styles/reduced-motion.css +0 -67
  57. package/src/styles/series-search.css +0 -153
  58. package/src/styles/sparkline.css +0 -16
  59. package/src/styles/table-animation.css +0 -93
  60. package/src/styles/table.css +0 -398
  61. package/src/styles/token-definitions.ts +0 -199
  62. package/src/styles/tokens.css +0 -156
  63. package/src/styles/tooltip.css +0 -126
  64. package/src/styles/you-draw-it.css +0 -126
  65. package/src/theme/__tests__/dark-mode.test.ts +0 -232
  66. package/src/theme/__tests__/defaults.test.ts +0 -73
  67. package/src/theme/__tests__/resolve.test.ts +0 -277
  68. package/src/theme/dark-mode.ts +0 -258
  69. package/src/theme/defaults.ts +0 -110
  70. package/src/theme/index.ts +0 -8
  71. package/src/theme/presets.ts +0 -64
  72. package/src/theme/resolve.ts +0 -312
  73. package/src/types/__tests__/encoding.test.ts +0 -368
  74. package/src/types/__tests__/gradient.test.ts +0 -95
  75. package/src/types/__tests__/spec.test.ts +0 -499
  76. package/src/types/encoding.ts +0 -280
  77. package/src/types/events.ts +0 -236
  78. package/src/types/index.ts +0 -290
  79. package/src/types/layout.ts +0 -2024
  80. package/src/types/schema-roots.ts +0 -32
  81. package/src/types/spec.ts +0 -2925
  82. package/src/types/table.ts +0 -108
  83. package/src/types/theme.ts +0 -209
@@ -1,404 +0,0 @@
1
- /**
2
- * Text measurement for environments without a DOM.
3
- *
4
- * Uses per-character advance-width tables measured from Inter (the default
5
- * chart font) for ~2-3% accuracy on Latin text at weights 400/700.
6
- * Other fonts set via theme.fonts.family still get a better-than-flat-ratio
7
- * approximation but no accuracy guarantee.
8
- *
9
- * Tables generated by scripts/generate-char-widths.mjs — regenerate, don't
10
- * hand-edit.
11
- */
12
-
13
- import type { MeasureTextFn } from '../types/layout';
14
-
15
- // ---------------------------------------------------------------------------
16
- // Per-character advance widths for Inter, normalized to fontSize = 1.
17
- // Generated by scripts/generate-char-widths.mjs from Chromium canvas metrics.
18
- // ---------------------------------------------------------------------------
19
-
20
- const CHAR_WIDTHS_400: Record<string, number> = {
21
- ' ': 0.278,
22
- '!': 0.278,
23
- '"': 0.355,
24
- '#': 0.556,
25
- $: 0.556,
26
- '%': 0.889,
27
- '&': 0.667,
28
- "'": 0.191,
29
- '(': 0.333,
30
- ')': 0.333,
31
- '*': 0.389,
32
- '+': 0.584,
33
- ',': 0.278,
34
- '-': 0.333,
35
- '.': 0.278,
36
- '/': 0.278,
37
- '0': 0.556,
38
- '1': 0.556,
39
- '2': 0.556,
40
- '3': 0.556,
41
- '4': 0.556,
42
- '5': 0.556,
43
- '6': 0.556,
44
- '7': 0.556,
45
- '8': 0.556,
46
- '9': 0.556,
47
- ':': 0.278,
48
- ';': 0.278,
49
- '<': 0.584,
50
- '=': 0.584,
51
- '>': 0.584,
52
- '?': 0.556,
53
- '@': 1.015,
54
- A: 0.667,
55
- B: 0.667,
56
- C: 0.722,
57
- D: 0.722,
58
- E: 0.667,
59
- F: 0.611,
60
- G: 0.778,
61
- H: 0.722,
62
- I: 0.278,
63
- J: 0.5,
64
- K: 0.667,
65
- L: 0.556,
66
- M: 0.833,
67
- N: 0.722,
68
- O: 0.778,
69
- P: 0.667,
70
- Q: 0.778,
71
- R: 0.722,
72
- S: 0.667,
73
- T: 0.611,
74
- U: 0.722,
75
- V: 0.667,
76
- W: 0.944,
77
- X: 0.667,
78
- Y: 0.667,
79
- Z: 0.611,
80
- '[': 0.278,
81
- '\\': 0.278,
82
- ']': 0.278,
83
- '^': 0.469,
84
- _: 0.556,
85
- '`': 0.333,
86
- a: 0.556,
87
- b: 0.556,
88
- c: 0.5,
89
- d: 0.556,
90
- e: 0.556,
91
- f: 0.278,
92
- g: 0.556,
93
- h: 0.556,
94
- i: 0.222,
95
- j: 0.222,
96
- k: 0.5,
97
- l: 0.222,
98
- m: 0.833,
99
- n: 0.556,
100
- o: 0.556,
101
- p: 0.556,
102
- q: 0.556,
103
- r: 0.333,
104
- s: 0.5,
105
- t: 0.278,
106
- u: 0.556,
107
- v: 0.5,
108
- w: 0.722,
109
- x: 0.5,
110
- y: 0.5,
111
- z: 0.5,
112
- '{': 0.334,
113
- '|': 0.26,
114
- '}': 0.334,
115
- '~': 0.584,
116
- '₹': 0.52,
117
- '₩': 1.022,
118
- '¥': 0.556,
119
- '¥': 1,
120
- '€': 0.744,
121
- '£': 0.556,
122
- '¢': 0.556,
123
- '§': 0.556,
124
- '©': 0.737,
125
- '®': 0.737,
126
- '™': 1,
127
- '°': 0.4,
128
- '–': 0.556,
129
- '—': 1,
130
- '‘': 0.222,
131
- '’': 0.222,
132
- '“': 0.333,
133
- '”': 0.333,
134
- '…': 1,
135
- '±': 0.549,
136
- '×': 0.584,
137
- '÷': 0.549,
138
- };
139
-
140
- const AVG_CHAR_WIDTH_400 = 0.53;
141
-
142
- const CHAR_WIDTHS_700: Record<string, number> = {
143
- ' ': 0.278,
144
- '!': 0.333,
145
- '"': 0.474,
146
- '#': 0.556,
147
- $: 0.556,
148
- '%': 0.889,
149
- '&': 0.722,
150
- "'": 0.238,
151
- '(': 0.333,
152
- ')': 0.333,
153
- '*': 0.389,
154
- '+': 0.584,
155
- ',': 0.278,
156
- '-': 0.333,
157
- '.': 0.278,
158
- '/': 0.278,
159
- '0': 0.556,
160
- '1': 0.556,
161
- '2': 0.556,
162
- '3': 0.556,
163
- '4': 0.556,
164
- '5': 0.556,
165
- '6': 0.556,
166
- '7': 0.556,
167
- '8': 0.556,
168
- '9': 0.556,
169
- ':': 0.333,
170
- ';': 0.333,
171
- '<': 0.584,
172
- '=': 0.584,
173
- '>': 0.584,
174
- '?': 0.611,
175
- '@': 0.975,
176
- A: 0.722,
177
- B: 0.722,
178
- C: 0.722,
179
- D: 0.722,
180
- E: 0.667,
181
- F: 0.611,
182
- G: 0.778,
183
- H: 0.722,
184
- I: 0.278,
185
- J: 0.556,
186
- K: 0.722,
187
- L: 0.611,
188
- M: 0.833,
189
- N: 0.722,
190
- O: 0.778,
191
- P: 0.667,
192
- Q: 0.778,
193
- R: 0.722,
194
- S: 0.667,
195
- T: 0.611,
196
- U: 0.722,
197
- V: 0.667,
198
- W: 0.944,
199
- X: 0.667,
200
- Y: 0.667,
201
- Z: 0.611,
202
- '[': 0.333,
203
- '\\': 0.278,
204
- ']': 0.333,
205
- '^': 0.584,
206
- _: 0.556,
207
- '`': 0.333,
208
- a: 0.556,
209
- b: 0.611,
210
- c: 0.556,
211
- d: 0.611,
212
- e: 0.556,
213
- f: 0.333,
214
- g: 0.611,
215
- h: 0.611,
216
- i: 0.278,
217
- j: 0.278,
218
- k: 0.556,
219
- l: 0.278,
220
- m: 0.889,
221
- n: 0.611,
222
- o: 0.611,
223
- p: 0.611,
224
- q: 0.611,
225
- r: 0.389,
226
- s: 0.556,
227
- t: 0.333,
228
- u: 0.611,
229
- v: 0.556,
230
- w: 0.778,
231
- x: 0.556,
232
- y: 0.556,
233
- z: 0.5,
234
- '{': 0.389,
235
- '|': 0.28,
236
- '}': 0.389,
237
- '~': 0.584,
238
- '₹': 0.553,
239
- '₩': 1.024,
240
- '¥': 0.556,
241
- '¥': 1,
242
- '€': 0.744,
243
- '£': 0.556,
244
- '¢': 0.556,
245
- '§': 0.556,
246
- '©': 0.737,
247
- '®': 0.737,
248
- '™': 1,
249
- '°': 0.4,
250
- '–': 0.556,
251
- '—': 1,
252
- '‘': 0.278,
253
- '’': 0.278,
254
- '“': 0.5,
255
- '”': 0.5,
256
- '…': 1,
257
- '±': 0.549,
258
- '×': 0.584,
259
- '÷': 0.549,
260
- };
261
-
262
- const AVG_CHAR_WIDTH_700 = 0.554;
263
-
264
- // Weights < 650 scale the CHAR_WIDTHS_400 table; weights >= 650 scale
265
- // CHAR_WIDTHS_700. The factor is 1.0 at each table's native weight.
266
- const WEIGHT_ADJUSTMENT: Record<number, number> = {
267
- 100: 0.9,
268
- 200: 0.92,
269
- 300: 0.95,
270
- 400: 1.0,
271
- 500: 1.02,
272
- 600: 1.05,
273
- 700: 1.0, // uses CHAR_WIDTHS_700 directly
274
- 800: 1.1,
275
- 900: 1.12,
276
- };
277
-
278
- function resolveTable(fontWeight: number): {
279
- table: Record<string, number>;
280
- avg: number;
281
- scale: number;
282
- } {
283
- if (fontWeight >= 650) {
284
- return {
285
- table: CHAR_WIDTHS_700,
286
- avg: AVG_CHAR_WIDTH_700,
287
- scale: WEIGHT_ADJUSTMENT[fontWeight] ?? 1.0,
288
- };
289
- }
290
- return {
291
- table: CHAR_WIDTHS_400,
292
- avg: AVG_CHAR_WIDTH_400,
293
- scale: WEIGHT_ADJUSTMENT[fontWeight] ?? 1.0,
294
- };
295
- }
296
-
297
- function charWidthRatio(ch: string, table: Record<string, number>, avg: number): number {
298
- const direct = table[ch];
299
- if (direct !== undefined) return direct;
300
- const code = ch.codePointAt(0) ?? 0;
301
- // Combining marks (diacritics) have zero advance width
302
- if ((code >= 0x0300 && code <= 0x036f) || (code >= 0x1ab0 && code <= 0x1aff)) return 0;
303
- // CJK ideographs, kana, hangul, and fullwidth forms are full-em
304
- if (
305
- (code >= 0x1100 && code <= 0x11ff) ||
306
- (code >= 0x2e80 && code <= 0x9fff) ||
307
- (code >= 0xac00 && code <= 0xd7af) ||
308
- (code >= 0xf900 && code <= 0xfaff) ||
309
- (code >= 0xff00 && code <= 0xff60)
310
- )
311
- return 1.0;
312
- return avg;
313
- }
314
-
315
- /**
316
- * Estimate the average character width for a given font size and weight.
317
- * Used by both text width estimation and word-wrap line counting.
318
- */
319
- export function estimateCharWidth(fontSize: number, fontWeight = 400): number {
320
- const { avg, scale } = resolveTable(fontWeight);
321
- return fontSize * avg * scale;
322
- }
323
-
324
- /**
325
- * Estimate the rendered width of a text string.
326
- *
327
- * Uses per-character advance widths measured from Inter (the default chart
328
- * font). Accurate to ~2-3% for Latin text at weights 400/700; other weights
329
- * are interpolated from the nearest table.
330
- */
331
- export function estimateTextWidth(text: string, fontSize: number, fontWeight = 400): number {
332
- const { table, avg, scale } = resolveTable(fontWeight);
333
-
334
- const measureLine = (line: string): number => {
335
- let sum = 0;
336
- for (const ch of line) {
337
- sum += charWidthRatio(ch, table, avg);
338
- }
339
- return sum * fontSize * scale;
340
- };
341
-
342
- if (text.includes('\n')) {
343
- let maxWidth = 0;
344
- for (const line of text.split('\n')) {
345
- const w = measureLine(line);
346
- if (w > maxWidth) maxWidth = w;
347
- }
348
- return maxWidth;
349
- }
350
- return measureLine(text);
351
- }
352
-
353
- /**
354
- * Width reserved for the "tryOpenData.ai" brand watermark in the bottom-right corner.
355
- */
356
- export const BRAND_RESERVE_WIDTH = 130;
357
-
358
- /** Font size of the brand watermark (px). Shared between layout and renderer. */
359
- export const BRAND_FONT_SIZE = 12;
360
-
361
- /** Minimum chart width to render the brand watermark (px). */
362
- export const BRAND_MIN_WIDTH = 120;
363
-
364
- /**
365
- * Width threshold below which layout tightens spacing (legend gaps, chrome
366
- * padding, label gaps) to maximize data area on small screens.
367
- */
368
- export const COMPACT_WIDTH = 420;
369
-
370
- /**
371
- * Estimate the rendered height of a text block.
372
- */
373
- export function estimateTextHeight(fontSize: number, lineCount = 1, lineHeight = 1.3): number {
374
- return fontSize * lineHeight * lineCount;
375
- }
376
-
377
- /**
378
- * Distance from the top of a text box to its alphabetic baseline.
379
- *
380
- * Layout stores text positions as top-edge coordinates; renderers convert to
381
- * baseline coordinates with this ratio instead of `dominant-baseline: hanging`.
382
- * WebKit computes the hanging baseline from different font metrics than Blink
383
- * and never inherits dominant-baseline into tspans (WebKit bugs 139258 /
384
- * 297455), which shifted whole titles above the container on iOS Safari.
385
- * The 0.8 ratio matches Blink's rendered hanging position for typical UI
386
- * fonts (hanging sits at ~0.8 x ascent, Inter ascent ~= 0.97em) and the
387
- * Highcharts fontMetrics convention.
388
- */
389
- export function textAscent(fontSize: number): number {
390
- return fontSize * 0.8;
391
- }
392
-
393
- // ---------------------------------------------------------------------------
394
- // Measurer resolver — bridges heuristic ↔ injected canvas measurer
395
- // ---------------------------------------------------------------------------
396
-
397
- export const heuristicMeasure: MeasureTextFn = (text, fontSize, fontWeight) => ({
398
- width: estimateTextWidth(text, fontSize, fontWeight),
399
- height: estimateTextHeight(fontSize),
400
- });
401
-
402
- export function resolveMeasurer(measureText?: MeasureTextFn): MeasureTextFn {
403
- return measureText ?? heuristicMeasure;
404
- }
@@ -1,93 +0,0 @@
1
- /**
2
- * Word-wrap a text string into lines that fit within a max width.
3
- *
4
- * Shared by the SVG chart renderer and the sankey renderer. When a
5
- * `measureText` callback is provided, uses real DOM measurement for
6
- * accurate wrapping. Otherwise falls back to a character-width
7
- * heuristic driven by `estimateCharWidth` from text-measure.ts.
8
- *
9
- * Non-browser callers (SSR, tests) omit the measureText argument to
10
- * use the heuristic fallback. Do not change the signature without
11
- * re-verifying visual baselines for every caller.
12
- */
13
-
14
- import type { MeasureTextFn } from '../types/layout';
15
- import { estimateCharWidth } from './text-measure';
16
-
17
- /**
18
- * Break text into lines that fit within maxWidth using word wrapping.
19
- *
20
- * Splits on explicit newlines first, then word-wraps each segment.
21
- * Preserves empty segments so consecutive newlines produce blank lines.
22
- *
23
- * @param text - The text to wrap. May contain `\n` for forced breaks.
24
- * @param fontSize - Font size in pixels.
25
- * @param fontWeight - Font weight (100-900).
26
- * @param maxWidth - Maximum line width in pixels. Non-positive values return `[text]` unchanged.
27
- * @param measureText - Optional real text measurer. When omitted, uses a character-width heuristic.
28
- */
29
- export function wrapText(
30
- text: string,
31
- fontSize: number,
32
- fontWeight: number,
33
- maxWidth: number,
34
- measureText?: MeasureTextFn,
35
- ): string[] {
36
- if (maxWidth <= 0) return [text];
37
-
38
- // Split on explicit newlines first
39
- const segments = text.split('\n');
40
- if (segments.length > 1) {
41
- return segments.flatMap((segment) =>
42
- segment.length === 0 ? [''] : wrapText(segment, fontSize, fontWeight, maxWidth, measureText),
43
- );
44
- }
45
-
46
- // Use real text measurement when available
47
- if (measureText) {
48
- const textWidth = measureText(text, fontSize, fontWeight).width;
49
- if (textWidth <= maxWidth) return [text];
50
-
51
- const words = text.split(' ');
52
- const lines: string[] = [];
53
- let current = '';
54
-
55
- for (const word of words) {
56
- const candidate = current ? `${current} ${word}` : word;
57
- const candidateWidth = measureText(candidate, fontSize, fontWeight).width;
58
- if (candidateWidth > maxWidth && current) {
59
- lines.push(current);
60
- current = word;
61
- } else {
62
- current = candidate;
63
- }
64
- }
65
- if (current) lines.push(current);
66
-
67
- return lines;
68
- }
69
-
70
- // Heuristic: estimate character width from font size and weight.
71
- // Reuses the same ratio and weight adjustment constants as text-measure.
72
- const charWidth = estimateCharWidth(fontSize, fontWeight);
73
- const maxChars = Math.floor(maxWidth / charWidth);
74
-
75
- if (text.length <= maxChars) return [text];
76
-
77
- const words = text.split(' ');
78
- const lines: string[] = [];
79
- let current = '';
80
-
81
- for (const word of words) {
82
- const candidate = current ? `${current} ${word}` : word;
83
- if (candidate.length > maxChars && current) {
84
- lines.push(current);
85
- current = word;
86
- } else {
87
- current = candidate;
88
- }
89
- }
90
- if (current) lines.push(current);
91
-
92
- return lines;
93
- }