@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.
- package/dist/index.d.ts +490 -25
- package/dist/index.js +57 -19
- 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 +485 -35
- package/schema/table.schema.json +35 -1
- package/schema/vizspec.schema.json +1305 -120
- 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 -172
- 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 -199
- 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 -135
- 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 -154
- package/src/types/index.ts +0 -287
- package/src/types/layout.ts +0 -1966
- package/src/types/schema-roots.ts +0 -32
- package/src/types/spec.ts +0 -2867
- package/src/types/table.ts +0 -108
- 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
|
-
});
|