@opendata-ai/openchart-vanilla 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/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
- package/dist/chunk-DCODCLWI.js +234 -0
- package/dist/chunk-DCODCLWI.js.map +1 -0
- package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
- package/dist/chunk-EKAUQAAX.js.map +1 -0
- package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
- package/dist/chunk-KNT5AZ2O.js.map +1 -0
- package/dist/chunk-RM5XS6NR.js +113 -0
- package/dist/chunk-RM5XS6NR.js.map +1 -0
- package/dist/chunk-T32UHMZA.js +282 -0
- package/dist/chunk-T32UHMZA.js.map +1 -0
- package/dist/export-gif.d.ts +113 -0
- package/dist/export-gif.js +21 -0
- package/dist/export-gif.js.map +1 -0
- package/dist/index.d.ts +283 -24
- package/dist/index.js +2012 -763
- package/dist/index.js.map +1 -1
- package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
- package/dist/simulation-worker.js +83 -2
- package/dist/static.js +7 -69
- package/dist/static.js.map +1 -1
- package/dist/story/index.d.ts +3 -2
- package/dist/story/index.js +5 -3
- package/dist/story/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +14 -6
- package/dist/chunk-5EOV5JQZ.js.map +0 -1
- package/dist/chunk-ENXSUXC5.js.map +0 -1
- package/src/__test-fixtures__/dom.ts +0 -70
- package/src/__test-fixtures__/specs.ts +0 -214
- package/src/__tests__/animation.test.ts +0 -358
- package/src/__tests__/annotations-render.test.ts +0 -201
- package/src/__tests__/compound-labels.test.ts +0 -122
- package/src/__tests__/continuous-legend.test.ts +0 -91
- package/src/__tests__/crosshair.test.ts +0 -130
- package/src/__tests__/edit-events.test.ts +0 -902
- package/src/__tests__/events.test.ts +0 -387
- package/src/__tests__/export.test.ts +0 -241
- package/src/__tests__/font-race.test.ts +0 -199
- package/src/__tests__/gradient-ids.test.ts +0 -159
- package/src/__tests__/gradient-utils.test.ts +0 -125
- package/src/__tests__/known-bugs-render.test.ts +0 -183
- package/src/__tests__/map.test.ts +0 -293
- package/src/__tests__/mount.test.ts +0 -357
- package/src/__tests__/pattern-fills.test.ts +0 -105
- package/src/__tests__/resize-timing.test.ts +0 -218
- package/src/__tests__/sankey.test.ts +0 -133
- package/src/__tests__/selection-events.test.ts +0 -869
- package/src/__tests__/series-search.test.ts +0 -311
- package/src/__tests__/static.test.ts +0 -141
- package/src/__tests__/svg-renderer.test.ts +0 -895
- package/src/__tests__/table-keyboard.test.ts +0 -361
- package/src/__tests__/table-mount.test.ts +0 -639
- package/src/__tests__/tilemap.test.ts +0 -158
- package/src/__tests__/tooltip.test.ts +0 -337
- package/src/__tests__/transition.test.ts +0 -1777
- package/src/__tests__/you-draw-it.test.ts +0 -273
- package/src/animation-vars.ts +0 -42
- package/src/animation.ts +0 -76
- package/src/barlist-mount.ts +0 -383
- package/src/barlist-renderer.ts +0 -267
- package/src/dom-helpers.ts +0 -17
- package/src/export.ts +0 -482
- package/src/gradient-utils.ts +0 -138
- package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
- package/src/graph/__tests__/graph-mount.test.ts +0 -213
- package/src/graph/__tests__/interaction.test.ts +0 -205
- package/src/graph/__tests__/keyboard.test.ts +0 -653
- package/src/graph/__tests__/search.test.ts +0 -88
- package/src/graph/__tests__/simulation.test.ts +0 -233
- package/src/graph/__tests__/spatial-index.test.ts +0 -142
- package/src/graph/__tests__/zoom.test.ts +0 -203
- package/src/graph/canvas-renderer.ts +0 -747
- package/src/graph/interaction.ts +0 -367
- package/src/graph/keyboard.ts +0 -208
- package/src/graph/search.ts +0 -50
- package/src/graph/simulation-worker-url.ts +0 -12
- package/src/graph/simulation-worker.ts +0 -278
- package/src/graph/simulation.ts +0 -422
- package/src/graph/spatial-index.ts +0 -121
- package/src/graph/types.ts +0 -47
- package/src/graph/worker-protocol.ts +0 -70
- package/src/graph/zoom.ts +0 -134
- package/src/graph-mount.ts +0 -888
- package/src/index.ts +0 -76
- package/src/interactions/chart-events.ts +0 -139
- package/src/interactions/crosshair.ts +0 -233
- package/src/interactions/drag-handler.ts +0 -175
- package/src/interactions/editing-drags.ts +0 -515
- package/src/interactions/index.ts +0 -25
- package/src/interactions/keyboard-nav.ts +0 -111
- package/src/interactions/legend-interaction.ts +0 -46
- package/src/interactions/selection.ts +0 -280
- package/src/interactions/tooltip-events.ts +0 -72
- package/src/map-camera.ts +0 -88
- package/src/map-mount.ts +0 -701
- package/src/map-renderer.ts +0 -338
- package/src/map-transition.ts +0 -83
- package/src/measure-text.ts +0 -134
- package/src/mount.ts +0 -1382
- package/src/pattern-utils.ts +0 -110
- package/src/renderers/annotations.ts +0 -388
- package/src/renderers/axes.ts +0 -351
- package/src/renderers/brand.ts +0 -87
- package/src/renderers/chrome.ts +0 -196
- package/src/renderers/endpoint-labels.ts +0 -150
- package/src/renderers/legend.ts +0 -311
- package/src/renderers/marks.ts +0 -561
- package/src/renderers/metrics.ts +0 -63
- package/src/renderers/svg-dom.ts +0 -40
- package/src/renderers/table-cells.ts +0 -446
- package/src/resize-observer.ts +0 -46
- package/src/sankey-mount.ts +0 -622
- package/src/sankey-renderer.ts +0 -614
- package/src/series-search.ts +0 -392
- package/src/static.ts +0 -251
- package/src/story/__tests__/camera-math.test.ts +0 -145
- package/src/story/__tests__/create-chart-story.test.ts +0 -317
- package/src/story/__tests__/progress-math.test.ts +0 -95
- package/src/story/__tests__/scroll-driver.test.ts +0 -130
- package/src/story/__tests__/tween.test.ts +0 -159
- package/src/story/camera-math.ts +0 -125
- package/src/story/create-chart-story.ts +0 -353
- package/src/story/crossfade.ts +0 -70
- package/src/story/index.ts +0 -39
- package/src/story/progress-math.ts +0 -85
- package/src/story/resolve-camera-target.ts +0 -119
- package/src/story/scroll-driver.ts +0 -206
- package/src/story/story-camera.ts +0 -49
- package/src/story/tween.ts +0 -112
- package/src/story/types.ts +0 -76
- package/src/svg-ids.ts +0 -23
- package/src/svg-renderer.ts +0 -383
- package/src/table-keyboard.ts +0 -266
- package/src/table-mount.ts +0 -567
- package/src/table-renderer.ts +0 -414
- package/src/text-edit-overlay.ts +0 -255
- package/src/theme-tokens.ts +0 -51
- package/src/tilemap-mount.ts +0 -511
- package/src/tilemap-renderer.ts +0 -396
- package/src/tooltip.ts +0 -177
- package/src/transition.ts +0 -2816
- package/src/you-draw-it.ts +0 -480
- /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { createContainer } from '../__test-fixtures__/dom';
|
|
3
|
-
import { createMap } from '../map-mount';
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Shared fixtures
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
// Minimal valid TopoJSON with 3 US states (delta-encoded arcs).
|
|
10
|
-
const MINI_TOPO = {
|
|
11
|
-
type: 'Topology',
|
|
12
|
-
objects: {
|
|
13
|
-
states: {
|
|
14
|
-
type: 'GeometryCollection',
|
|
15
|
-
geometries: [
|
|
16
|
-
{ type: 'Polygon', id: '06', properties: { name: 'California' }, arcs: [[0]] },
|
|
17
|
-
{ type: 'Polygon', id: '48', properties: { name: 'Texas' }, arcs: [[1]] },
|
|
18
|
-
{ type: 'Polygon', id: '36', properties: { name: 'New York' }, arcs: [[2]] },
|
|
19
|
-
],
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
arcs: [
|
|
23
|
-
[
|
|
24
|
-
[-122, 37],
|
|
25
|
-
[4, -3],
|
|
26
|
-
[-3, 2],
|
|
27
|
-
[-1, 1],
|
|
28
|
-
],
|
|
29
|
-
[
|
|
30
|
-
[-100, 31],
|
|
31
|
-
[6, -1],
|
|
32
|
-
[-3, -4],
|
|
33
|
-
[-3, 5],
|
|
34
|
-
],
|
|
35
|
-
[
|
|
36
|
-
[-74, 41],
|
|
37
|
-
[1, -1],
|
|
38
|
-
[-3, 2],
|
|
39
|
-
[2, -1],
|
|
40
|
-
],
|
|
41
|
-
],
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const basicMapSpec = {
|
|
45
|
-
type: 'map' as const,
|
|
46
|
-
geo: { features: MINI_TOPO, projection: 'mercator' as const },
|
|
47
|
-
data: [
|
|
48
|
-
{ fips: '06', value: 10 },
|
|
49
|
-
{ fips: '48', value: 20 },
|
|
50
|
-
],
|
|
51
|
-
encoding: {
|
|
52
|
-
key: { field: 'fips', type: 'nominal' as const },
|
|
53
|
-
color: { field: 'value', type: 'quantitative' as const },
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
// Tests
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
|
|
61
|
-
describe('createMap', () => {
|
|
62
|
-
let container: HTMLDivElement;
|
|
63
|
-
|
|
64
|
-
beforeEach(() => {
|
|
65
|
-
container = createContainer();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
afterEach(() => {
|
|
69
|
-
document.body.innerHTML = '';
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('mounts an SVG with path elements', () => {
|
|
73
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
74
|
-
|
|
75
|
-
const svg = container.querySelector('svg');
|
|
76
|
-
expect(svg).not.toBeNull();
|
|
77
|
-
expect(svg!.classList.contains('oc-map')).toBe(true);
|
|
78
|
-
|
|
79
|
-
const features = container.querySelectorAll('.oc-map-feature');
|
|
80
|
-
expect(features.length).toBe(3);
|
|
81
|
-
|
|
82
|
-
instance.destroy();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('update() re-renders with new data', () => {
|
|
86
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
87
|
-
|
|
88
|
-
instance.update({
|
|
89
|
-
...basicMapSpec,
|
|
90
|
-
data: [
|
|
91
|
-
{ fips: '06', value: 100 },
|
|
92
|
-
{ fips: '48', value: 200 },
|
|
93
|
-
{ fips: '36', value: 300 },
|
|
94
|
-
],
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const features = container.querySelectorAll('.oc-map-feature');
|
|
98
|
-
expect(features.length).toBe(3);
|
|
99
|
-
|
|
100
|
-
instance.destroy();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('destroy() removes SVG from DOM', () => {
|
|
104
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
105
|
-
|
|
106
|
-
expect(container.querySelector('svg')).not.toBeNull();
|
|
107
|
-
instance.destroy();
|
|
108
|
-
expect(container.querySelector('svg')).toBeNull();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('SVG does not have oc-chart-root class', () => {
|
|
112
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
113
|
-
|
|
114
|
-
const svg = container.querySelector('svg');
|
|
115
|
-
expect(svg).not.toBeNull();
|
|
116
|
-
expect(svg!.classList.contains('oc-chart-root')).toBe(false);
|
|
117
|
-
expect(svg!.classList.contains('oc-map')).toBe(true);
|
|
118
|
-
|
|
119
|
-
instance.destroy();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('viewBox matches layout dimensions', () => {
|
|
123
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
124
|
-
|
|
125
|
-
const svg = container.querySelector('svg');
|
|
126
|
-
expect(svg).not.toBeNull();
|
|
127
|
-
const viewBox = svg!.getAttribute('viewBox');
|
|
128
|
-
expect(viewBox).toBe(`0 0 ${instance.layout.width} ${instance.layout.height}`);
|
|
129
|
-
|
|
130
|
-
instance.destroy();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('no gradient legend group present for quantitative map', () => {
|
|
134
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
135
|
-
|
|
136
|
-
// Old hand-rolled gradient legend used .oc-map-legend; it should be gone
|
|
137
|
-
const oldLegend = container.querySelector('.oc-map-legend');
|
|
138
|
-
expect(oldLegend).toBeNull();
|
|
139
|
-
|
|
140
|
-
// Continuous legend uses the shared .oc-legend class
|
|
141
|
-
const legend = container.querySelector('.oc-legend');
|
|
142
|
-
expect(legend).not.toBeNull();
|
|
143
|
-
|
|
144
|
-
instance.destroy();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('exposes layout property', () => {
|
|
148
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
149
|
-
|
|
150
|
-
expect(instance.layout).toBeDefined();
|
|
151
|
-
expect(instance.layout.features.length).toBeGreaterThan(0);
|
|
152
|
-
|
|
153
|
-
instance.destroy();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('export("svg") returns a string containing SVG content', () => {
|
|
157
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
158
|
-
|
|
159
|
-
const svgString = instance.export('svg');
|
|
160
|
-
expect(typeof svgString).toBe('string');
|
|
161
|
-
expect(svgString).toContain('<svg');
|
|
162
|
-
expect(svgString).toContain('oc-map');
|
|
163
|
-
|
|
164
|
-
instance.destroy();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('stamps data-key on feature paths equal to feature id', () => {
|
|
168
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
169
|
-
|
|
170
|
-
const features = container.querySelectorAll('.oc-map-feature');
|
|
171
|
-
for (const feature of features) {
|
|
172
|
-
const featureId = feature.getAttribute('data-feature-id');
|
|
173
|
-
const dataKey = feature.getAttribute('data-key');
|
|
174
|
-
expect(dataKey).not.toBeNull();
|
|
175
|
-
expect(dataKey).toBe(featureId);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
instance.destroy();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('stagger custom property is at most 80ms for any feature count', () => {
|
|
182
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
183
|
-
|
|
184
|
-
const svg = container.querySelector('svg');
|
|
185
|
-
expect(svg).not.toBeNull();
|
|
186
|
-
|
|
187
|
-
const staggerValue = svg!.style.getPropertyValue('--oc-animation-stagger');
|
|
188
|
-
if (staggerValue) {
|
|
189
|
-
const numericMs = parseFloat(staggerValue);
|
|
190
|
-
expect(numericMs).toBeLessThanOrEqual(80);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
instance.destroy();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('camera group [data-oc-map-camera] wraps features and borders', () => {
|
|
197
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
198
|
-
|
|
199
|
-
const cameraGroup = container.querySelector('[data-oc-map-camera]');
|
|
200
|
-
expect(cameraGroup).not.toBeNull();
|
|
201
|
-
|
|
202
|
-
// Features and borders should be inside the camera group
|
|
203
|
-
const features = cameraGroup!.querySelector('.oc-map-features');
|
|
204
|
-
const borders = cameraGroup!.querySelector('.oc-map-borders');
|
|
205
|
-
expect(features).not.toBeNull();
|
|
206
|
-
expect(borders).not.toBeNull();
|
|
207
|
-
|
|
208
|
-
// Chrome and legend should NOT be inside the camera group
|
|
209
|
-
const chrome = cameraGroup!.querySelector('.oc-chrome');
|
|
210
|
-
expect(chrome).toBeNull();
|
|
211
|
-
|
|
212
|
-
instance.destroy();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('zoomTo applies a transform with scale > 1 and sets vector-effect', () => {
|
|
216
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
217
|
-
|
|
218
|
-
// Use New York ('36') which is small enough relative to the full map to produce a zoom > 1
|
|
219
|
-
instance.zoomTo('36', { duration: 0 });
|
|
220
|
-
|
|
221
|
-
const cameraGroup = container.querySelector('[data-oc-map-camera]');
|
|
222
|
-
expect(cameraGroup).not.toBeNull();
|
|
223
|
-
|
|
224
|
-
const transform = cameraGroup!.getAttribute('transform');
|
|
225
|
-
expect(transform).not.toBeNull();
|
|
226
|
-
// Transform should contain a scale factor > 1
|
|
227
|
-
const scaleMatch = transform!.match(/scale\(([\d.]+)\)/);
|
|
228
|
-
expect(scaleMatch).not.toBeNull();
|
|
229
|
-
expect(Number(scaleMatch![1])).toBeGreaterThan(1);
|
|
230
|
-
|
|
231
|
-
// vector-effect should be set on feature paths
|
|
232
|
-
const featurePaths = container.querySelectorAll('.oc-map-feature');
|
|
233
|
-
for (const p of featurePaths) {
|
|
234
|
-
expect(p.getAttribute('vector-effect')).toBe('non-scaling-stroke');
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
instance.destroy();
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('resetView removes the transform and vector-effect', () => {
|
|
241
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
242
|
-
|
|
243
|
-
// First zoom in to New York (small enough to produce zoom > 1)
|
|
244
|
-
instance.zoomTo('36', { duration: 0 });
|
|
245
|
-
const cameraGroup = container.querySelector('[data-oc-map-camera]');
|
|
246
|
-
expect(cameraGroup!.getAttribute('transform')).not.toBeNull();
|
|
247
|
-
|
|
248
|
-
// Then reset
|
|
249
|
-
instance.resetView({ duration: 0 });
|
|
250
|
-
expect(cameraGroup!.getAttribute('transform')).toBeNull();
|
|
251
|
-
|
|
252
|
-
// vector-effect should be removed
|
|
253
|
-
const featurePaths = container.querySelectorAll('.oc-map-feature');
|
|
254
|
-
for (const p of featurePaths) {
|
|
255
|
-
expect(p.getAttribute('vector-effect')).toBeNull();
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
instance.destroy();
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('zoomTo with unknown feature id warns via console.warn', () => {
|
|
262
|
-
const instance = createMap(container, basicMapSpec, { responsive: false });
|
|
263
|
-
|
|
264
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
265
|
-
|
|
266
|
-
instance.zoomTo('NONEXISTENT', { duration: 0 });
|
|
267
|
-
expect(warnSpy).toHaveBeenCalledOnce();
|
|
268
|
-
expect(warnSpy.mock.calls[0][0]).toContain('NONEXISTENT');
|
|
269
|
-
|
|
270
|
-
warnSpy.mockRestore();
|
|
271
|
-
instance.destroy();
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('spec with geo.focus snaps camera on first render', () => {
|
|
275
|
-
const focusSpec = {
|
|
276
|
-
...basicMapSpec,
|
|
277
|
-
geo: { ...basicMapSpec.geo, focus: '36' },
|
|
278
|
-
};
|
|
279
|
-
const instance = createMap(container, focusSpec, { responsive: false });
|
|
280
|
-
|
|
281
|
-
const cameraGroup = container.querySelector('[data-oc-map-camera]');
|
|
282
|
-
expect(cameraGroup).not.toBeNull();
|
|
283
|
-
|
|
284
|
-
// Camera should be applied (transform present because '36' is small enough to zoom)
|
|
285
|
-
const transform = cameraGroup!.getAttribute('transform');
|
|
286
|
-
expect(transform).not.toBeNull();
|
|
287
|
-
const scaleMatch = transform!.match(/scale\(([\d.]+)\)/);
|
|
288
|
-
expect(scaleMatch).not.toBeNull();
|
|
289
|
-
expect(Number(scaleMatch![1])).toBeGreaterThan(1);
|
|
290
|
-
|
|
291
|
-
instance.destroy();
|
|
292
|
-
});
|
|
293
|
-
});
|
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
import type { ChartLayout } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { legendGap } from '@opendata-ai/openchart-engine';
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
-
import { createAutoHeightContainer, createContainer } from '../__test-fixtures__/dom';
|
|
5
|
-
import {
|
|
6
|
-
barSpec,
|
|
7
|
-
lineSpec,
|
|
8
|
-
longChromeLineSpec,
|
|
9
|
-
singleSeriesLineSpec,
|
|
10
|
-
} from '../__test-fixtures__/specs';
|
|
11
|
-
import { createChart } from '../mount';
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Tests
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
describe('createChart', () => {
|
|
18
|
-
let container: HTMLDivElement;
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
container = createContainer();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
document.body.innerHTML = '';
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('creates an SVG element in the container', () => {
|
|
29
|
-
const chart = createChart(container, lineSpec);
|
|
30
|
-
|
|
31
|
-
const svg = container.querySelector('svg');
|
|
32
|
-
expect(svg).not.toBeNull();
|
|
33
|
-
expect(svg?.getAttribute('class')).toBe('oc-chart');
|
|
34
|
-
|
|
35
|
-
chart.destroy();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('SVG has correct viewBox dimensions', () => {
|
|
39
|
-
const chart = createChart(container, lineSpec);
|
|
40
|
-
|
|
41
|
-
const svg = container.querySelector('svg');
|
|
42
|
-
const viewBox = svg?.getAttribute('viewBox');
|
|
43
|
-
expect(viewBox).toBe('0 0 600 400');
|
|
44
|
-
|
|
45
|
-
chart.destroy();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('chrome text elements are present with correct content', () => {
|
|
49
|
-
const chart = createChart(container, lineSpec);
|
|
50
|
-
|
|
51
|
-
const title = container.querySelector('.oc-title');
|
|
52
|
-
expect(title).not.toBeNull();
|
|
53
|
-
expect(title?.textContent).toBe('GDP Growth');
|
|
54
|
-
|
|
55
|
-
const subtitle = container.querySelector('.oc-subtitle');
|
|
56
|
-
expect(subtitle).not.toBeNull();
|
|
57
|
-
expect(subtitle?.textContent).toBe('US vs UK over time');
|
|
58
|
-
|
|
59
|
-
const source = container.querySelector('.oc-source');
|
|
60
|
-
expect(source).not.toBeNull();
|
|
61
|
-
expect(source?.textContent).toBe('World Bank');
|
|
62
|
-
|
|
63
|
-
chart.destroy();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('update() re-renders with new data', () => {
|
|
67
|
-
const chart = createChart(container, lineSpec);
|
|
68
|
-
|
|
69
|
-
const titleBefore = container.querySelector('.oc-title');
|
|
70
|
-
expect(titleBefore?.textContent).toBe('GDP Growth');
|
|
71
|
-
|
|
72
|
-
chart.update(barSpec);
|
|
73
|
-
|
|
74
|
-
const titleAfter = container.querySelector('.oc-title');
|
|
75
|
-
expect(titleAfter?.textContent).toBe('Updated Chart');
|
|
76
|
-
|
|
77
|
-
chart.destroy();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('destroy() removes all DOM elements and disconnects observer', () => {
|
|
81
|
-
const chart = createChart(container, lineSpec);
|
|
82
|
-
|
|
83
|
-
const svgBefore = container.querySelector('svg');
|
|
84
|
-
expect(svgBefore).not.toBeNull();
|
|
85
|
-
|
|
86
|
-
chart.destroy();
|
|
87
|
-
|
|
88
|
-
const svgAfter = container.querySelector('svg');
|
|
89
|
-
expect(svgAfter).toBeNull();
|
|
90
|
-
|
|
91
|
-
// Tooltip div should also be removed
|
|
92
|
-
const tooltip = container.querySelector('.oc-tooltip');
|
|
93
|
-
expect(tooltip).toBeNull();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('has accessible ARIA attributes on the SVG', () => {
|
|
97
|
-
const chart = createChart(container, lineSpec);
|
|
98
|
-
|
|
99
|
-
const svg = container.querySelector('svg');
|
|
100
|
-
expect(svg?.getAttribute('role')).toBe('img');
|
|
101
|
-
expect(svg?.getAttribute('aria-label')).toContain('Line chart');
|
|
102
|
-
|
|
103
|
-
chart.destroy();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('a11y.description overrides the SVG aria-label', () => {
|
|
107
|
-
const chart = createChart(container, {
|
|
108
|
-
...lineSpec,
|
|
109
|
-
a11y: { description: 'GDP growth for US and UK.' },
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const svg = container.querySelector('svg');
|
|
113
|
-
expect(svg?.getAttribute('aria-label')).toBe('GDP growth for US and UK.');
|
|
114
|
-
|
|
115
|
-
chart.destroy();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('a11y.hidden removes the chart from the accessibility tree', () => {
|
|
119
|
-
const chart = createChart(container, { ...lineSpec, a11y: { hidden: true } });
|
|
120
|
-
|
|
121
|
-
const svg = container.querySelector('svg');
|
|
122
|
-
expect(svg?.getAttribute('aria-hidden')).toBe('true');
|
|
123
|
-
expect(svg?.getAttribute('role')).toBeNull();
|
|
124
|
-
expect(svg?.getAttribute('aria-label')).toBeNull();
|
|
125
|
-
// No screen-reader table, no keyboard tab stop
|
|
126
|
-
expect(container.querySelector('.oc-sr-only')).toBeNull();
|
|
127
|
-
expect(container.getAttribute('tabindex')).toBeNull();
|
|
128
|
-
|
|
129
|
-
chart.destroy();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('layout property returns the compiled layout', () => {
|
|
133
|
-
const chart = createChart(container, lineSpec);
|
|
134
|
-
|
|
135
|
-
expect(chart.layout).toBeDefined();
|
|
136
|
-
expect(chart.layout.dimensions.width).toBe(600);
|
|
137
|
-
expect(chart.layout.dimensions.height).toBe(400);
|
|
138
|
-
expect(chart.layout.chrome.title?.text).toBe('GDP Growth');
|
|
139
|
-
|
|
140
|
-
chart.destroy();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('export("svg") returns a valid SVG string', () => {
|
|
144
|
-
const chart = createChart(container, lineSpec);
|
|
145
|
-
|
|
146
|
-
const svgString = chart.export('svg');
|
|
147
|
-
expect(svgString).toContain('<svg');
|
|
148
|
-
expect(svgString).toContain('viewBox');
|
|
149
|
-
expect(svgString).toContain('GDP Growth');
|
|
150
|
-
|
|
151
|
-
chart.destroy();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('export("csv") returns CSV data', () => {
|
|
155
|
-
const chart = createChart(container, lineSpec);
|
|
156
|
-
|
|
157
|
-
const csv = chart.export('csv');
|
|
158
|
-
expect(csv).toContain('date');
|
|
159
|
-
expect(csv).toContain('value');
|
|
160
|
-
expect(csv).toContain('country');
|
|
161
|
-
|
|
162
|
-
chart.destroy();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('renders axis elements', () => {
|
|
166
|
-
const chart = createChart(container, lineSpec);
|
|
167
|
-
|
|
168
|
-
const xAxis = container.querySelector('.oc-axis-x');
|
|
169
|
-
const yAxis = container.querySelector('.oc-axis-y');
|
|
170
|
-
expect(xAxis).not.toBeNull();
|
|
171
|
-
expect(yAxis).not.toBeNull();
|
|
172
|
-
|
|
173
|
-
chart.destroy();
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('renders chrome wrapper group', () => {
|
|
177
|
-
const chart = createChart(container, lineSpec);
|
|
178
|
-
|
|
179
|
-
const chromeGroup = container.querySelector('.oc-chrome');
|
|
180
|
-
expect(chromeGroup).not.toBeNull();
|
|
181
|
-
|
|
182
|
-
chart.destroy();
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('passes responsive option to skip resize observer', () => {
|
|
186
|
-
// Should not throw with responsive: false
|
|
187
|
-
const chart = createChart(container, lineSpec, { responsive: false });
|
|
188
|
-
|
|
189
|
-
const svg = container.querySelector('svg');
|
|
190
|
-
expect(svg).not.toBeNull();
|
|
191
|
-
|
|
192
|
-
chart.destroy();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('renders marks with data-mark-id attributes for data points', () => {
|
|
196
|
-
const chart = createChart(container, barSpec);
|
|
197
|
-
|
|
198
|
-
const marks = container.querySelectorAll('[data-mark-id]');
|
|
199
|
-
// barSpec has 3 data points, each should produce a mark
|
|
200
|
-
expect(marks.length).toBe(3);
|
|
201
|
-
|
|
202
|
-
for (const mark of marks) {
|
|
203
|
-
const id = mark.getAttribute('data-mark-id');
|
|
204
|
-
expect(id).toMatch(/^rect-\d+$/);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
chart.destroy();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('multi-series marks have data-series attributes', () => {
|
|
211
|
-
const chart = createChart(container, lineSpec);
|
|
212
|
-
|
|
213
|
-
const marks = container.querySelectorAll('[data-series]');
|
|
214
|
-
expect(marks.length).toBeGreaterThan(0);
|
|
215
|
-
|
|
216
|
-
const seriesNames = new Set<string>();
|
|
217
|
-
for (const mark of marks) {
|
|
218
|
-
const series = mark.getAttribute('data-series');
|
|
219
|
-
expect(series).not.toBeNull();
|
|
220
|
-
seriesNames.add(series!);
|
|
221
|
-
}
|
|
222
|
-
// lineSpec has US and UK series
|
|
223
|
-
expect(seriesNames.has('US')).toBe(true);
|
|
224
|
-
expect(seriesNames.has('UK')).toBe(true);
|
|
225
|
-
|
|
226
|
-
chart.destroy();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('does NOT stamp data-display on default (full) charts', () => {
|
|
230
|
-
const chart = createChart(container, lineSpec);
|
|
231
|
-
const svg = container.querySelector('svg');
|
|
232
|
-
expect(svg?.hasAttribute('data-display')).toBe(false);
|
|
233
|
-
chart.destroy();
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('stamps data-display="sparkline" when display is sparkline', () => {
|
|
237
|
-
const chart = createChart(container, {
|
|
238
|
-
mark: 'line',
|
|
239
|
-
data: lineSpec.data,
|
|
240
|
-
encoding: lineSpec.encoding,
|
|
241
|
-
display: 'sparkline',
|
|
242
|
-
});
|
|
243
|
-
const svg = container.querySelector('svg');
|
|
244
|
-
expect(svg?.getAttribute('data-display')).toBe('sparkline');
|
|
245
|
-
chart.destroy();
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
describe('resize observer integration', () => {
|
|
250
|
-
let container: HTMLDivElement;
|
|
251
|
-
|
|
252
|
-
beforeEach(() => {
|
|
253
|
-
container = createContainer();
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
afterEach(() => {
|
|
257
|
-
document.body.innerHTML = '';
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('resize() re-renders the chart', () => {
|
|
261
|
-
const chart = createChart(container, lineSpec);
|
|
262
|
-
|
|
263
|
-
// Should not throw
|
|
264
|
-
chart.resize();
|
|
265
|
-
|
|
266
|
-
const svg = container.querySelector('svg');
|
|
267
|
-
expect(svg).not.toBeNull();
|
|
268
|
-
|
|
269
|
-
chart.destroy();
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
describe('auto-height container growth', () => {
|
|
274
|
-
afterEach(() => {
|
|
275
|
-
document.body.innerHTML = '';
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
/** The chrome/legend/metrics blocks the mount grows the figure by. */
|
|
279
|
-
function overheadsOf(layout: ChartLayout, width: number): number {
|
|
280
|
-
const topLegendBlock =
|
|
281
|
-
layout.legend.position === 'top' &&
|
|
282
|
-
'entries' in layout.legend &&
|
|
283
|
-
layout.legend.entries.length > 0
|
|
284
|
-
? layout.legend.bounds.height + legendGap(width)
|
|
285
|
-
: 0;
|
|
286
|
-
return (
|
|
287
|
-
layout.chrome.topHeight +
|
|
288
|
-
layout.chrome.bottomHeight +
|
|
289
|
-
topLegendBlock +
|
|
290
|
-
(layout.metrics?.height ?? 0)
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
it('grows the figure beyond the 400px budget by the measured overheads', () => {
|
|
295
|
-
const container = createAutoHeightContainer(390);
|
|
296
|
-
const chart = createChart(container, longChromeLineSpec);
|
|
297
|
-
|
|
298
|
-
const height = chart.layout.dimensions.height;
|
|
299
|
-
expect(height).toBeGreaterThan(400);
|
|
300
|
-
expect(height).toBeCloseTo(400 + overheadsOf(chart.layout, 390), 5);
|
|
301
|
-
|
|
302
|
-
// The rendered SVG is sized to the grown figure, not the 400 fallback.
|
|
303
|
-
const svg = container.querySelector('svg');
|
|
304
|
-
expect(svg?.getAttribute('viewBox')).toBe(`0 0 390 ${height}`);
|
|
305
|
-
expect((svg as SVGElement).style.height).toBe(`${height}px`);
|
|
306
|
-
|
|
307
|
-
chart.destroy();
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('keeps explicit-height containers at the host height', () => {
|
|
311
|
-
const container = createContainer(390, 500);
|
|
312
|
-
const chart = createChart(container, longChromeLineSpec);
|
|
313
|
-
|
|
314
|
-
// Host constrains the box; chrome must fit inside it, not grow it.
|
|
315
|
-
expect(chart.layout.dimensions.height).toBe(500);
|
|
316
|
-
|
|
317
|
-
chart.destroy();
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('re-renders against the 400 budget, not the grown height (no re-pin)', () => {
|
|
321
|
-
const container = createAutoHeightContainer(390);
|
|
322
|
-
const chart = createChart(container, longChromeLineSpec);
|
|
323
|
-
|
|
324
|
-
const grownHeight = chart.layout.dimensions.height;
|
|
325
|
-
expect(grownHeight).toBeGreaterThan(400);
|
|
326
|
-
|
|
327
|
-
// Same spec again: the container now measures at the grown SVG height,
|
|
328
|
-
// but the latched-auto mount keeps compiling against the 400 budget, so
|
|
329
|
-
// the figure height is stable.
|
|
330
|
-
chart.update(longChromeLineSpec);
|
|
331
|
-
expect(chart.layout.dimensions.height).toBe(grownHeight);
|
|
332
|
-
|
|
333
|
-
// Shorter chrome: the figure must shrink toward the budget instead of
|
|
334
|
-
// staying pinned at the previously grown container height.
|
|
335
|
-
chart.update(singleSeriesLineSpec);
|
|
336
|
-
const shrunkHeight = chart.layout.dimensions.height;
|
|
337
|
-
expect(shrunkHeight).toBeLessThan(grownHeight);
|
|
338
|
-
expect(shrunkHeight).toBeCloseTo(400 + overheadsOf(chart.layout, 390), 5);
|
|
339
|
-
|
|
340
|
-
chart.destroy();
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it('does not grow sparkline mounts', () => {
|
|
344
|
-
const container = createAutoHeightContainer(200);
|
|
345
|
-
const chart = createChart(container, {
|
|
346
|
-
mark: 'line',
|
|
347
|
-
data: lineSpec.data,
|
|
348
|
-
encoding: lineSpec.encoding,
|
|
349
|
-
display: 'sparkline',
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
// Sparkline auto-height fallback stays at the tiny 40px default.
|
|
353
|
-
expect(chart.layout.dimensions.height).toBe(40);
|
|
354
|
-
|
|
355
|
-
chart.destroy();
|
|
356
|
-
});
|
|
357
|
-
});
|