@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,208 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { resolveTheme } from '../../theme/resolve';
3
- import type { Chrome } from '../../types/spec';
4
- import { computeChrome } from '../chrome';
5
- import { BRAND_FONT_SIZE, estimateTextHeight } from '../text-measure';
6
-
7
- const theme = resolveTheme();
8
-
9
- describe('computeChrome', () => {
10
- it('returns zero heights when chrome is undefined', () => {
11
- const result = computeChrome(undefined, theme, 600);
12
- expect(result.topHeight).toBe(0);
13
- expect(result.bottomHeight).toBe(0);
14
- expect(result.title).toBeUndefined();
15
- expect(result.subtitle).toBeUndefined();
16
- });
17
-
18
- it('reserves brand height when chrome is empty but chart is wide enough', () => {
19
- const result = computeChrome({}, theme, 600);
20
- const pad = theme.spacing.padding;
21
- const expectedBottom =
22
- theme.spacing.chartToFooter + estimateTextHeight(BRAND_FONT_SIZE, 1) + pad;
23
- expect(result.topHeight).toBe(0);
24
- expect(result.bottomHeight).toBe(expectedBottom);
25
- });
26
-
27
- it('returns zero bottom height when chrome is empty and chart is too narrow for brand', () => {
28
- const result = computeChrome({}, theme, 100);
29
- expect(result.topHeight).toBe(0);
30
- expect(result.bottomHeight).toBe(0);
31
- });
32
-
33
- it('reserves brand height in compact mode for wide charts', () => {
34
- const chrome: Chrome = { title: 'Title', source: 'Source' };
35
- const result = computeChrome(chrome, theme, 600, undefined, 'compact');
36
- const pad = theme.spacing.padding;
37
- const expectedBottom =
38
- theme.spacing.chartToFooter + estimateTextHeight(BRAND_FONT_SIZE, 1) + pad;
39
- expect(result.topHeight).toBeGreaterThan(0);
40
- expect(result.source).toBeUndefined(); // compact hides bottom chrome text
41
- expect(result.bottomHeight).toBe(expectedBottom);
42
- });
43
-
44
- it('returns zero bottom height in compact mode for narrow charts', () => {
45
- const chrome: Chrome = { title: 'Title', source: 'Source' };
46
- const result = computeChrome(chrome, theme, 100, undefined, 'compact');
47
- expect(result.bottomHeight).toBe(0);
48
- });
49
-
50
- it('positions title correctly', () => {
51
- const chrome: Chrome = { title: 'GDP Growth Rate' };
52
- const result = computeChrome(chrome, theme, 600);
53
-
54
- expect(result.title).toBeDefined();
55
- expect(result.title!.text).toBe('GDP Growth Rate');
56
- expect(result.title!.x).toBe(theme.spacing.padding);
57
- expect(result.title!.y).toBe(theme.spacing.padding);
58
- expect(result.title!.style.fontSize).toBe(theme.chrome.title.fontSize);
59
- expect(result.title!.style.fontWeight).toBe(theme.chrome.title.fontWeight);
60
- expect(result.topHeight).toBeGreaterThan(0);
61
- });
62
-
63
- it('positions subtitle below title', () => {
64
- const chrome: Chrome = { title: 'Title', subtitle: 'Subtitle text' };
65
- const result = computeChrome(chrome, theme, 600);
66
-
67
- expect(result.title).toBeDefined();
68
- expect(result.subtitle).toBeDefined();
69
- expect(result.subtitle!.y).toBeGreaterThan(result.title!.y);
70
- });
71
-
72
- it('computes top height from title + subtitle + gaps', () => {
73
- const chrome: Chrome = { title: 'Title', subtitle: 'Subtitle' };
74
- const result = computeChrome(chrome, theme, 600);
75
-
76
- // Top height should account for title, gap, subtitle, and chromeToChart
77
- expect(result.topHeight).toBeGreaterThan(30);
78
- });
79
-
80
- it('positions source in bottom chrome', () => {
81
- const chrome: Chrome = { source: 'Source: World Bank' };
82
- const result = computeChrome(chrome, theme, 600);
83
-
84
- expect(result.source).toBeDefined();
85
- expect(result.source!.text).toBe('Source: World Bank');
86
- expect(result.bottomHeight).toBeGreaterThan(0);
87
- });
88
-
89
- it('handles ChromeText objects with style overrides', () => {
90
- const chrome: Chrome = {
91
- title: {
92
- text: 'Custom Title',
93
- style: { fontSize: 24, fontWeight: 700, color: '#ff0000' },
94
- },
95
- };
96
- const result = computeChrome(chrome, theme, 600);
97
-
98
- expect(result.title!.style.fontSize).toBe(24);
99
- expect(result.title!.style.fontWeight).toBe(700);
100
- expect(result.title!.style.fill).toBe('#ff0000');
101
- });
102
-
103
- it('sets maxWidth based on width minus padding', () => {
104
- const chrome: Chrome = { title: 'Title' };
105
- const result = computeChrome(chrome, theme, 600);
106
-
107
- const expectedMaxWidth = 600 - theme.spacing.padding * 2 - 5;
108
- expect(result.title!.maxWidth).toBe(expectedMaxWidth);
109
- });
110
-
111
- it('handles full chrome with all elements', () => {
112
- const chrome: Chrome = {
113
- title: 'Title',
114
- subtitle: 'Subtitle',
115
- source: 'Source',
116
- byline: 'By Author',
117
- footer: 'Footer note',
118
- };
119
- const result = computeChrome(chrome, theme, 600);
120
-
121
- expect(result.title).toBeDefined();
122
- expect(result.subtitle).toBeDefined();
123
- expect(result.source).toBeDefined();
124
- expect(result.byline).toBeDefined();
125
- expect(result.footer).toBeDefined();
126
- expect(result.topHeight).toBeGreaterThan(0);
127
- expect(result.bottomHeight).toBeGreaterThan(0);
128
- });
129
-
130
- it('reserves extra height when title wraps to multiple lines at narrow widths', () => {
131
- const longTitle =
132
- 'Global Economic Recovery Trends Show Surprising Resilience Across Major Markets';
133
- const chrome: Chrome = { title: longTitle, subtitle: 'Subtitle text' };
134
-
135
- // At wide width, title fits on one line
136
- const wide = computeChrome(chrome, theme, 800);
137
- // At narrow width, title wraps to multiple lines
138
- const narrow = computeChrome(chrome, theme, 300);
139
-
140
- // Narrow should reserve more top height due to title wrapping
141
- expect(narrow.topHeight).toBeGreaterThan(wide.topHeight);
142
-
143
- // Subtitle should be pushed further down to avoid collision
144
- expect(narrow.subtitle!.y).toBeGreaterThan(wide.subtitle!.y);
145
- });
146
-
147
- it('reserves extra height when subtitle contains newline characters', () => {
148
- const chrome: Chrome = { title: 'Title', subtitle: 'Line one\nLine two' };
149
- const withNewline = computeChrome(chrome, theme, 600);
150
-
151
- const chromeNoNewline: Chrome = { title: 'Title', subtitle: 'Line one Line two' };
152
- const withoutNewline = computeChrome(chromeNoNewline, theme, 600);
153
-
154
- // The newline version should reserve more top height since it forces two lines
155
- expect(withNewline.topHeight).toBeGreaterThan(withoutNewline.topHeight);
156
- });
157
-
158
- it('handles consecutive newlines in chrome text', () => {
159
- const chrome: Chrome = { title: 'Title', subtitle: 'Above\n\nBelow' };
160
- const result = computeChrome(chrome, theme, 600);
161
-
162
- // Three segments: "Above", "", "Below" -> 3 lines total
163
- // This should be taller than a simple two-line subtitle
164
- const twoLine: Chrome = { title: 'Title', subtitle: 'Above\nBelow' };
165
- const twoLineResult = computeChrome(twoLine, theme, 600);
166
-
167
- expect(result.topHeight).toBeGreaterThan(twoLineResult.topHeight);
168
- });
169
-
170
- it('handles newlines combined with long text that also word-wraps', () => {
171
- const longSegment = 'This is a very long segment that should wrap at narrow widths on its own';
172
- const chrome: Chrome = { title: 'Title', subtitle: `${longSegment}\nShort` };
173
- const result = computeChrome(chrome, theme, 300);
174
-
175
- // At narrow width, the long segment wraps AND the \n adds another line
176
- const noNewline: Chrome = { title: 'Title', subtitle: longSegment };
177
- const noNewlineResult = computeChrome(noNewline, theme, 300);
178
-
179
- expect(result.topHeight).toBeGreaterThan(noNewlineResult.topHeight);
180
- });
181
-
182
- it('uses measureText function when provided', () => {
183
- const measureText = (text: string, fontSize: number) => ({
184
- width: text.length * fontSize * 0.6,
185
- height: fontSize * 1.2,
186
- });
187
-
188
- const chrome: Chrome = { title: 'Title' };
189
- const result = computeChrome(chrome, theme, 600, measureText);
190
- expect(result.title).toBeDefined();
191
- // Just verify it runs without error; exact values depend on measure fn
192
- });
193
-
194
- it('returns zero bottom height when watermark is false and no bottom chrome', () => {
195
- const chrome: Chrome = { title: 'Title' };
196
- const result = computeChrome(chrome, theme, 600, undefined, 'full', undefined, false);
197
- expect(result.bottomHeight).toBe(0);
198
- });
199
-
200
- it('does not reserve brand width for bottom text when watermark is false', () => {
201
- const chrome: Chrome = { source: 'Source: World Bank' };
202
- const withWatermark = computeChrome(chrome, theme, 600, undefined, 'full', undefined, true);
203
- const withoutWatermark = computeChrome(chrome, theme, 600, undefined, 'full', undefined, false);
204
-
205
- // Without watermark, source text gets full width (not reduced by BRAND_RESERVE_WIDTH)
206
- expect(withoutWatermark.source!.maxWidth).toBeGreaterThan(withWatermark.source!.maxWidth);
207
- });
208
- });
@@ -1,57 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { estimateTextHeight, estimateTextWidth } from '../text-measure';
3
-
4
- describe('estimateTextWidth', () => {
5
- it('returns 0 for empty string', () => {
6
- expect(estimateTextWidth('', 14)).toBe(0);
7
- });
8
-
9
- it('scales linearly with text length', () => {
10
- const w5 = estimateTextWidth('abcde', 14);
11
- const w10 = estimateTextWidth('abcdeabcde', 14);
12
- expect(w10).toBeCloseTo(w5 * 2, 1);
13
- });
14
-
15
- it('scales with font size', () => {
16
- const w14 = estimateTextWidth('test', 14);
17
- const w28 = estimateTextWidth('test', 28);
18
- expect(w28).toBeCloseTo(w14 * 2, 1);
19
- });
20
-
21
- it('heavier weights produce wider estimates', () => {
22
- const normal = estimateTextWidth('test', 14, 400);
23
- const bold = estimateTextWidth('test', 14, 700);
24
- expect(bold).toBeGreaterThan(normal);
25
- });
26
-
27
- it('produces reasonable values for common cases', () => {
28
- // "GDP Growth Rate" at 18px should be roughly 120-180px wide
29
- const width = estimateTextWidth('GDP Growth Rate', 18);
30
- expect(width).toBeGreaterThan(100);
31
- expect(width).toBeLessThan(250);
32
- });
33
-
34
- // Pins the per-character width table output. Any change to the tables
35
- // shifts layout-wide text wrapping decisions, so we freeze the exact value.
36
- it('estimateTextWidth("sample", 14, 400) returns the locked numeric value', () => {
37
- // Per-char lookup: s(0.5) + a(0.556) + m(0.833) + p(0.556) + l(0.222) + e(0.556) = 3.223
38
- // 3.223 * 14 * 1.0 = 45.122
39
- expect(estimateTextWidth('sample', 14, 400)).toBeCloseTo(45.122, 2);
40
- });
41
- });
42
-
43
- describe('estimateTextHeight', () => {
44
- it('single line at 14px with 1.3 lineHeight is 18.2', () => {
45
- expect(estimateTextHeight(14, 1, 1.3)).toBeCloseTo(18.2, 1);
46
- });
47
-
48
- it('two lines are double the height', () => {
49
- const h1 = estimateTextHeight(14, 1);
50
- const h2 = estimateTextHeight(14, 2);
51
- expect(h2).toBeCloseTo(h1 * 2, 1);
52
- });
53
-
54
- it('defaults to 1 line and 1.3 lineHeight', () => {
55
- expect(estimateTextHeight(10)).toBeCloseTo(13, 1);
56
- });
57
- });