@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.
Files changed (145) hide show
  1. package/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
  2. package/dist/chunk-DCODCLWI.js +234 -0
  3. package/dist/chunk-DCODCLWI.js.map +1 -0
  4. package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
  5. package/dist/chunk-EKAUQAAX.js.map +1 -0
  6. package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
  7. package/dist/chunk-KNT5AZ2O.js.map +1 -0
  8. package/dist/chunk-RM5XS6NR.js +113 -0
  9. package/dist/chunk-RM5XS6NR.js.map +1 -0
  10. package/dist/chunk-T32UHMZA.js +282 -0
  11. package/dist/chunk-T32UHMZA.js.map +1 -0
  12. package/dist/export-gif.d.ts +113 -0
  13. package/dist/export-gif.js +21 -0
  14. package/dist/export-gif.js.map +1 -0
  15. package/dist/index.d.ts +283 -24
  16. package/dist/index.js +2012 -763
  17. package/dist/index.js.map +1 -1
  18. package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
  19. package/dist/simulation-worker.js +83 -2
  20. package/dist/static.js +7 -69
  21. package/dist/static.js.map +1 -1
  22. package/dist/story/index.d.ts +3 -2
  23. package/dist/story/index.js +5 -3
  24. package/dist/story/index.js.map +1 -1
  25. package/dist/styles.css +1 -1
  26. package/dist/styles.css.map +1 -1
  27. package/package.json +14 -6
  28. package/dist/chunk-5EOV5JQZ.js.map +0 -1
  29. package/dist/chunk-ENXSUXC5.js.map +0 -1
  30. package/src/__test-fixtures__/dom.ts +0 -70
  31. package/src/__test-fixtures__/specs.ts +0 -214
  32. package/src/__tests__/animation.test.ts +0 -358
  33. package/src/__tests__/annotations-render.test.ts +0 -201
  34. package/src/__tests__/compound-labels.test.ts +0 -122
  35. package/src/__tests__/continuous-legend.test.ts +0 -91
  36. package/src/__tests__/crosshair.test.ts +0 -130
  37. package/src/__tests__/edit-events.test.ts +0 -902
  38. package/src/__tests__/events.test.ts +0 -387
  39. package/src/__tests__/export.test.ts +0 -241
  40. package/src/__tests__/font-race.test.ts +0 -199
  41. package/src/__tests__/gradient-ids.test.ts +0 -159
  42. package/src/__tests__/gradient-utils.test.ts +0 -125
  43. package/src/__tests__/known-bugs-render.test.ts +0 -183
  44. package/src/__tests__/map.test.ts +0 -293
  45. package/src/__tests__/mount.test.ts +0 -357
  46. package/src/__tests__/pattern-fills.test.ts +0 -105
  47. package/src/__tests__/resize-timing.test.ts +0 -218
  48. package/src/__tests__/sankey.test.ts +0 -133
  49. package/src/__tests__/selection-events.test.ts +0 -869
  50. package/src/__tests__/series-search.test.ts +0 -311
  51. package/src/__tests__/static.test.ts +0 -141
  52. package/src/__tests__/svg-renderer.test.ts +0 -895
  53. package/src/__tests__/table-keyboard.test.ts +0 -361
  54. package/src/__tests__/table-mount.test.ts +0 -639
  55. package/src/__tests__/tilemap.test.ts +0 -158
  56. package/src/__tests__/tooltip.test.ts +0 -337
  57. package/src/__tests__/transition.test.ts +0 -1777
  58. package/src/__tests__/you-draw-it.test.ts +0 -273
  59. package/src/animation-vars.ts +0 -42
  60. package/src/animation.ts +0 -76
  61. package/src/barlist-mount.ts +0 -383
  62. package/src/barlist-renderer.ts +0 -267
  63. package/src/dom-helpers.ts +0 -17
  64. package/src/export.ts +0 -482
  65. package/src/gradient-utils.ts +0 -138
  66. package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
  67. package/src/graph/__tests__/graph-mount.test.ts +0 -213
  68. package/src/graph/__tests__/interaction.test.ts +0 -205
  69. package/src/graph/__tests__/keyboard.test.ts +0 -653
  70. package/src/graph/__tests__/search.test.ts +0 -88
  71. package/src/graph/__tests__/simulation.test.ts +0 -233
  72. package/src/graph/__tests__/spatial-index.test.ts +0 -142
  73. package/src/graph/__tests__/zoom.test.ts +0 -203
  74. package/src/graph/canvas-renderer.ts +0 -747
  75. package/src/graph/interaction.ts +0 -367
  76. package/src/graph/keyboard.ts +0 -208
  77. package/src/graph/search.ts +0 -50
  78. package/src/graph/simulation-worker-url.ts +0 -12
  79. package/src/graph/simulation-worker.ts +0 -278
  80. package/src/graph/simulation.ts +0 -422
  81. package/src/graph/spatial-index.ts +0 -121
  82. package/src/graph/types.ts +0 -47
  83. package/src/graph/worker-protocol.ts +0 -70
  84. package/src/graph/zoom.ts +0 -134
  85. package/src/graph-mount.ts +0 -888
  86. package/src/index.ts +0 -76
  87. package/src/interactions/chart-events.ts +0 -139
  88. package/src/interactions/crosshair.ts +0 -233
  89. package/src/interactions/drag-handler.ts +0 -175
  90. package/src/interactions/editing-drags.ts +0 -515
  91. package/src/interactions/index.ts +0 -25
  92. package/src/interactions/keyboard-nav.ts +0 -111
  93. package/src/interactions/legend-interaction.ts +0 -46
  94. package/src/interactions/selection.ts +0 -280
  95. package/src/interactions/tooltip-events.ts +0 -72
  96. package/src/map-camera.ts +0 -88
  97. package/src/map-mount.ts +0 -701
  98. package/src/map-renderer.ts +0 -338
  99. package/src/map-transition.ts +0 -83
  100. package/src/measure-text.ts +0 -134
  101. package/src/mount.ts +0 -1382
  102. package/src/pattern-utils.ts +0 -110
  103. package/src/renderers/annotations.ts +0 -388
  104. package/src/renderers/axes.ts +0 -351
  105. package/src/renderers/brand.ts +0 -87
  106. package/src/renderers/chrome.ts +0 -196
  107. package/src/renderers/endpoint-labels.ts +0 -150
  108. package/src/renderers/legend.ts +0 -311
  109. package/src/renderers/marks.ts +0 -561
  110. package/src/renderers/metrics.ts +0 -63
  111. package/src/renderers/svg-dom.ts +0 -40
  112. package/src/renderers/table-cells.ts +0 -446
  113. package/src/resize-observer.ts +0 -46
  114. package/src/sankey-mount.ts +0 -622
  115. package/src/sankey-renderer.ts +0 -614
  116. package/src/series-search.ts +0 -392
  117. package/src/static.ts +0 -251
  118. package/src/story/__tests__/camera-math.test.ts +0 -145
  119. package/src/story/__tests__/create-chart-story.test.ts +0 -317
  120. package/src/story/__tests__/progress-math.test.ts +0 -95
  121. package/src/story/__tests__/scroll-driver.test.ts +0 -130
  122. package/src/story/__tests__/tween.test.ts +0 -159
  123. package/src/story/camera-math.ts +0 -125
  124. package/src/story/create-chart-story.ts +0 -353
  125. package/src/story/crossfade.ts +0 -70
  126. package/src/story/index.ts +0 -39
  127. package/src/story/progress-math.ts +0 -85
  128. package/src/story/resolve-camera-target.ts +0 -119
  129. package/src/story/scroll-driver.ts +0 -206
  130. package/src/story/story-camera.ts +0 -49
  131. package/src/story/tween.ts +0 -112
  132. package/src/story/types.ts +0 -76
  133. package/src/svg-ids.ts +0 -23
  134. package/src/svg-renderer.ts +0 -383
  135. package/src/table-keyboard.ts +0 -266
  136. package/src/table-mount.ts +0 -567
  137. package/src/table-renderer.ts +0 -414
  138. package/src/text-edit-overlay.ts +0 -255
  139. package/src/theme-tokens.ts +0 -51
  140. package/src/tilemap-mount.ts +0 -511
  141. package/src/tilemap-renderer.ts +0 -396
  142. package/src/tooltip.ts +0 -177
  143. package/src/transition.ts +0 -2816
  144. package/src/you-draw-it.ts +0 -480
  145. /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
- });