@opendata-ai/openchart-core 8.0.0-rc.2 → 8.0.0-rc.21

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 (82) hide show
  1. package/dist/index.d.ts +490 -25
  2. package/dist/index.js +57 -19
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/dist/styles.css.map +1 -1
  6. package/package.json +1 -2
  7. package/schema/chart.schema.json +485 -35
  8. package/schema/table.schema.json +35 -1
  9. package/schema/vizspec.schema.json +1305 -120
  10. package/src/accessibility/__tests__/alt-text.test.ts +0 -110
  11. package/src/accessibility/__tests__/aria.test.ts +0 -125
  12. package/src/accessibility/alt-text.ts +0 -155
  13. package/src/accessibility/aria.ts +0 -75
  14. package/src/accessibility/index.ts +0 -6
  15. package/src/colors/__tests__/colorblind.test.ts +0 -75
  16. package/src/colors/__tests__/contrast.test.ts +0 -119
  17. package/src/colors/__tests__/palettes.test.ts +0 -74
  18. package/src/colors/colorblind.ts +0 -131
  19. package/src/colors/contrast.ts +0 -143
  20. package/src/colors/index.ts +0 -36
  21. package/src/colors/palettes.ts +0 -206
  22. package/src/helpers/__tests__/deep-merge.test.ts +0 -54
  23. package/src/helpers/__tests__/spec-builders.test.ts +0 -338
  24. package/src/helpers/deep-merge.ts +0 -29
  25. package/src/helpers/spec-builders.ts +0 -479
  26. package/src/index.ts +0 -215
  27. package/src/labels/__tests__/collision.test.ts +0 -310
  28. package/src/labels/collision.ts +0 -249
  29. package/src/labels/index.ts +0 -13
  30. package/src/layout/__tests__/chrome.test.ts +0 -208
  31. package/src/layout/__tests__/text-measure.test.ts +0 -57
  32. package/src/layout/chrome.ts +0 -470
  33. package/src/layout/index.ts +0 -18
  34. package/src/layout/text-measure.ts +0 -404
  35. package/src/layout/text-wrap.ts +0 -93
  36. package/src/locale/__tests__/format.test.ts +0 -606
  37. package/src/locale/format.ts +0 -459
  38. package/src/locale/index.ts +0 -21
  39. package/src/responsive/__tests__/breakpoints.test.ts +0 -146
  40. package/src/responsive/__tests__/metrics.test.ts +0 -52
  41. package/src/responsive/breakpoints.ts +0 -172
  42. package/src/responsive/index.ts +0 -53
  43. package/src/responsive/metrics.ts +0 -299
  44. package/src/schema/__tests__/generated-artifacts.test.ts +0 -42
  45. package/src/schema/__tests__/schema.test.ts +0 -179
  46. package/src/styles/__tests__/reduced-motion-selectors.test.ts +0 -28
  47. package/src/styles/animation.css +0 -199
  48. package/src/styles/base.css +0 -102
  49. package/src/styles/chrome.css +0 -174
  50. package/src/styles/dark.css +0 -44
  51. package/src/styles/graph.css +0 -135
  52. package/src/styles/index.css +0 -28
  53. package/src/styles/keyframes.css +0 -135
  54. package/src/styles/legend.css +0 -18
  55. package/src/styles/reduced-motion.css +0 -67
  56. package/src/styles/series-search.css +0 -153
  57. package/src/styles/sparkline.css +0 -16
  58. package/src/styles/table-animation.css +0 -93
  59. package/src/styles/table.css +0 -398
  60. package/src/styles/token-definitions.ts +0 -199
  61. package/src/styles/tokens.css +0 -156
  62. package/src/styles/tooltip.css +0 -126
  63. package/src/styles/you-draw-it.css +0 -126
  64. package/src/theme/__tests__/dark-mode.test.ts +0 -232
  65. package/src/theme/__tests__/defaults.test.ts +0 -73
  66. package/src/theme/__tests__/resolve.test.ts +0 -277
  67. package/src/theme/dark-mode.ts +0 -258
  68. package/src/theme/defaults.ts +0 -110
  69. package/src/theme/index.ts +0 -8
  70. package/src/theme/presets.ts +0 -64
  71. package/src/theme/resolve.ts +0 -312
  72. package/src/types/__tests__/encoding.test.ts +0 -368
  73. package/src/types/__tests__/gradient.test.ts +0 -95
  74. package/src/types/__tests__/spec.test.ts +0 -499
  75. package/src/types/encoding.ts +0 -280
  76. package/src/types/events.ts +0 -154
  77. package/src/types/index.ts +0 -287
  78. package/src/types/layout.ts +0 -1966
  79. package/src/types/schema-roots.ts +0 -32
  80. package/src/types/spec.ts +0 -2867
  81. package/src/types/table.ts +0 -108
  82. 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
- }