@opendata-ai/openchart-vanilla 8.0.0-rc.9 → 8.0.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 (150) hide show
  1. package/dist/{chunk-SPILUE5S.js → chunk-2NAQRP66.js} +2 -2
  2. package/dist/{chunk-IZQBT6QM.js → chunk-6BUCYCLD.js} +7283 -5881
  3. package/dist/chunk-6BUCYCLD.js.map +1 -0
  4. package/dist/chunk-DCODCLWI.js +234 -0
  5. package/dist/chunk-DCODCLWI.js.map +1 -0
  6. package/dist/{chunk-QW7ZB6NE.js → chunk-EKAUQAAX.js} +49 -45
  7. package/dist/chunk-EKAUQAAX.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 +309 -27
  16. package/dist/index.js +2020 -836
  17. package/dist/index.js.map +1 -1
  18. package/dist/{mount-CUKQLeqD.d.ts → mount-CL-nA0f-.d.ts} +85 -2
  19. package/dist/simulation-worker.js +83 -2
  20. package/dist/static.d.ts +2 -0
  21. package/dist/static.js +8 -69
  22. package/dist/static.js.map +1 -1
  23. package/dist/story/index.d.ts +25 -9
  24. package/dist/story/index.js +9 -7
  25. package/dist/story/index.js.map +1 -1
  26. package/dist/styles.css +1 -1
  27. package/dist/styles.css.map +1 -1
  28. package/package.json +14 -6
  29. package/dist/chunk-IZQBT6QM.js.map +0 -1
  30. package/dist/chunk-QW7ZB6NE.js.map +0 -1
  31. package/src/__test-fixtures__/dom.ts +0 -70
  32. package/src/__test-fixtures__/specs.ts +0 -214
  33. package/src/__tests__/animation.test.ts +0 -358
  34. package/src/__tests__/annotations-render.test.ts +0 -201
  35. package/src/__tests__/compound-labels.test.ts +0 -122
  36. package/src/__tests__/continuous-legend.test.ts +0 -91
  37. package/src/__tests__/crosshair.test.ts +0 -130
  38. package/src/__tests__/edit-events.test.ts +0 -1477
  39. package/src/__tests__/editable-prop.test.ts +0 -174
  40. package/src/__tests__/events.test.ts +0 -387
  41. package/src/__tests__/export.test.ts +0 -241
  42. package/src/__tests__/font-race.test.ts +0 -199
  43. package/src/__tests__/gradient-ids.test.ts +0 -159
  44. package/src/__tests__/gradient-utils.test.ts +0 -125
  45. package/src/__tests__/invert.test.ts +0 -187
  46. package/src/__tests__/known-bugs-render.test.ts +0 -183
  47. package/src/__tests__/map.test.ts +0 -636
  48. package/src/__tests__/mount.test.ts +0 -357
  49. package/src/__tests__/pattern-fills.test.ts +0 -105
  50. package/src/__tests__/resize-timing.test.ts +0 -218
  51. package/src/__tests__/sankey.test.ts +0 -133
  52. package/src/__tests__/selection-events.test.ts +0 -1062
  53. package/src/__tests__/series-search.test.ts +0 -311
  54. package/src/__tests__/static.test.ts +0 -141
  55. package/src/__tests__/svg-renderer.test.ts +0 -895
  56. package/src/__tests__/table-keyboard.test.ts +0 -361
  57. package/src/__tests__/table-mount.test.ts +0 -639
  58. package/src/__tests__/tilemap.test.ts +0 -158
  59. package/src/__tests__/tooltip.test.ts +0 -337
  60. package/src/__tests__/transition.test.ts +0 -1792
  61. package/src/__tests__/update-scrolly-matrix.test.ts +0 -563
  62. package/src/__tests__/you-draw-it.test.ts +0 -273
  63. package/src/animation-vars.ts +0 -42
  64. package/src/animation.ts +0 -76
  65. package/src/barlist-mount.ts +0 -383
  66. package/src/barlist-renderer.ts +0 -267
  67. package/src/dom-helpers.ts +0 -17
  68. package/src/export.ts +0 -482
  69. package/src/gradient-utils.ts +0 -138
  70. package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
  71. package/src/graph/__tests__/graph-mount.test.ts +0 -213
  72. package/src/graph/__tests__/interaction.test.ts +0 -205
  73. package/src/graph/__tests__/keyboard.test.ts +0 -653
  74. package/src/graph/__tests__/search.test.ts +0 -88
  75. package/src/graph/__tests__/simulation.test.ts +0 -233
  76. package/src/graph/__tests__/spatial-index.test.ts +0 -142
  77. package/src/graph/__tests__/zoom.test.ts +0 -203
  78. package/src/graph/canvas-renderer.ts +0 -747
  79. package/src/graph/interaction.ts +0 -367
  80. package/src/graph/keyboard.ts +0 -208
  81. package/src/graph/search.ts +0 -50
  82. package/src/graph/simulation-worker-url.ts +0 -12
  83. package/src/graph/simulation-worker.ts +0 -278
  84. package/src/graph/simulation.ts +0 -422
  85. package/src/graph/spatial-index.ts +0 -121
  86. package/src/graph/types.ts +0 -47
  87. package/src/graph/worker-protocol.ts +0 -70
  88. package/src/graph/zoom.ts +0 -134
  89. package/src/graph-mount.ts +0 -888
  90. package/src/index.ts +0 -76
  91. package/src/interactions/chart-events.ts +0 -139
  92. package/src/interactions/crosshair.ts +0 -233
  93. package/src/interactions/drag-handler.ts +0 -175
  94. package/src/interactions/editing-drags.ts +0 -649
  95. package/src/interactions/index.ts +0 -27
  96. package/src/interactions/invert.ts +0 -54
  97. package/src/interactions/keyboard-nav.ts +0 -111
  98. package/src/interactions/legend-interaction.ts +0 -46
  99. package/src/interactions/selection.ts +0 -286
  100. package/src/interactions/tooltip-events.ts +0 -72
  101. package/src/map-camera.ts +0 -111
  102. package/src/map-mount.ts +0 -797
  103. package/src/map-renderer.ts +0 -476
  104. package/src/map-transition.ts +0 -99
  105. package/src/measure-text.ts +0 -134
  106. package/src/mount.ts +0 -1434
  107. package/src/pattern-utils.ts +0 -110
  108. package/src/renderers/annotations.ts +0 -388
  109. package/src/renderers/axes.ts +0 -351
  110. package/src/renderers/brand.ts +0 -87
  111. package/src/renderers/chrome.ts +0 -196
  112. package/src/renderers/endpoint-labels.ts +0 -150
  113. package/src/renderers/legend.ts +0 -312
  114. package/src/renderers/marks.ts +0 -561
  115. package/src/renderers/metrics.ts +0 -63
  116. package/src/renderers/svg-dom.ts +0 -40
  117. package/src/renderers/table-cells.ts +0 -446
  118. package/src/resize-observer.ts +0 -46
  119. package/src/sankey-mount.ts +0 -622
  120. package/src/sankey-renderer.ts +0 -614
  121. package/src/series-search.ts +0 -392
  122. package/src/static.ts +0 -251
  123. package/src/story/__tests__/camera-math.test.ts +0 -145
  124. package/src/story/__tests__/create-chart-story.test.ts +0 -317
  125. package/src/story/__tests__/progress-math.test.ts +0 -95
  126. package/src/story/__tests__/scroll-driver.test.ts +0 -130
  127. package/src/story/__tests__/tween.test.ts +0 -159
  128. package/src/story/camera-math.ts +0 -125
  129. package/src/story/create-chart-story.ts +0 -353
  130. package/src/story/crossfade.ts +0 -70
  131. package/src/story/index.ts +0 -39
  132. package/src/story/progress-math.ts +0 -85
  133. package/src/story/resolve-camera-target.ts +0 -119
  134. package/src/story/scroll-driver.ts +0 -206
  135. package/src/story/story-camera.ts +0 -49
  136. package/src/story/tween.ts +0 -112
  137. package/src/story/types.ts +0 -76
  138. package/src/svg-ids.ts +0 -23
  139. package/src/svg-renderer.ts +0 -383
  140. package/src/table-keyboard.ts +0 -266
  141. package/src/table-mount.ts +0 -567
  142. package/src/table-renderer.ts +0 -414
  143. package/src/text-edit-overlay.ts +0 -389
  144. package/src/theme-tokens.ts +0 -51
  145. package/src/tilemap-mount.ts +0 -511
  146. package/src/tilemap-renderer.ts +0 -396
  147. package/src/tooltip.ts +0 -177
  148. package/src/transition.ts +0 -2816
  149. package/src/you-draw-it.ts +0 -480
  150. /package/dist/{chunk-SPILUE5S.js.map → chunk-2NAQRP66.js.map} +0 -0
@@ -1,383 +0,0 @@
1
- /**
2
- * BarList mount API: the main entry point for vanilla JS barlist usage.
3
- *
4
- * createBarList() takes a container, BarListSpec, and options, compiles the
5
- * barlist, renders it as SVG, sets up responsive resizing, tooltip interaction,
6
- * and returns a BarListInstance with update/resize/export/destroy.
7
- */
8
-
9
- import type {
10
- BarListLayout,
11
- BarListSpec,
12
- CompileOptions,
13
- DarkMode,
14
- ThemeConfig,
15
- } from '@opendata-ai/openchart-core';
16
- import { compileBarList } from '@opendata-ai/openchart-engine';
17
- import { cancelAnimations, setupAnimationCleanup } from './animation';
18
- import { renderBarListSVG } from './barlist-renderer';
19
- import {
20
- exportJPG,
21
- exportPNG,
22
- exportSVG,
23
- exportSVGWithFonts,
24
- type JPGExportOptions,
25
- type SVGExportOptions,
26
- } from './export';
27
- import { createMeasureText, resolveFontFamily, scheduleFontReload } from './measure-text';
28
- import { observeResize } from './resize-observer';
29
- import { createTooltipManager, type TooltipManager } from './tooltip';
30
-
31
- // ---------------------------------------------------------------------------
32
- // Types
33
- // ---------------------------------------------------------------------------
34
-
35
- export interface BarListMountOptions {
36
- theme?: ThemeConfig;
37
- darkMode?: DarkMode;
38
- responsive?: boolean;
39
- watermark?: boolean;
40
- tooltip?: boolean;
41
- onRowClick?: (row: { label: string; value: number; data: Record<string, unknown> }) => void;
42
- onRowHover?: (
43
- row: { label: string; value: number; data: Record<string, unknown> } | null,
44
- ) => void;
45
- }
46
-
47
- export interface BarListInstance {
48
- update(spec: BarListSpec): void;
49
- resize(): void;
50
- export(
51
- format: 'svg' | 'svg-with-fonts' | 'png' | 'jpg',
52
- options?: JPGExportOptions | SVGExportOptions,
53
- ): string | Promise<Blob> | Promise<string>;
54
- destroy(): void;
55
- readonly layout: BarListLayout;
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Dark mode resolution
60
- // ---------------------------------------------------------------------------
61
-
62
- function resolveDarkMode(mode?: DarkMode): boolean {
63
- if (mode === 'force') return true;
64
- if (mode === 'off' || mode === undefined) return false;
65
- if (typeof window !== 'undefined' && window.matchMedia) {
66
- return window.matchMedia('(prefers-color-scheme: dark)').matches;
67
- }
68
- return false;
69
- }
70
-
71
- // ---------------------------------------------------------------------------
72
- // Main API
73
- // ---------------------------------------------------------------------------
74
-
75
- export function createBarList(
76
- container: HTMLElement,
77
- spec: BarListSpec,
78
- options?: BarListMountOptions,
79
- ): BarListInstance {
80
- let currentSpec = spec;
81
- let currentLayout: BarListLayout;
82
- let destroyed = false;
83
-
84
- let svgElement: SVGSVGElement | null = null;
85
- let tooltipManager: TooltipManager | null = null;
86
- let cleanupTooltipEvents: (() => void) | null = null;
87
- let disconnectResize: (() => void) | null = null;
88
- let animationCleanup: (() => void) | null = null;
89
- let pendingResize = false;
90
-
91
- // Set when webfonts have loaded and a recompile is owed. The next render()
92
- // that recompiles flips data-oc-fonts-state to 'ready' and clears this, so
93
- // the attribute stays honest when resize() defers to pendingResize during
94
- // the entrance animation.
95
- let fontsReloadPending = false;
96
-
97
- // Apply the root class up front so getComputedStyle sees --oc-font-family
98
- // before the text measurer is built.
99
- container.classList.add('oc-barlist-root');
100
-
101
- // Resolve the effective font the way compile() will: compile merges
102
- // { ...spec.theme, ...options.theme }, so options.theme wins over the
103
- // spec-level theme; fall back to the container's computed font. Measuring a
104
- // different font than gets rendered desyncs layout metrics and the reload watcher.
105
- function resolveEffectiveFont(): string {
106
- return (
107
- options?.theme?.fonts?.family ??
108
- currentSpec.theme?.fonts?.family ??
109
- resolveFontFamily(container)
110
- );
111
- }
112
- let fontFamily = resolveEffectiveFont();
113
- let measureText = createMeasureText(fontFamily);
114
- let renderGen = 0;
115
-
116
- function getContainerDimensions(): { width: number; height: number } {
117
- const rect = container.getBoundingClientRect();
118
- return {
119
- width: Math.max(rect.width || 600, 100),
120
- height: Math.max(rect.height || 400, 100),
121
- };
122
- }
123
-
124
- function compile(): BarListLayout {
125
- const { width, height } = getContainerDimensions();
126
- const darkMode = resolveDarkMode(options?.darkMode);
127
-
128
- const compileOpts: CompileOptions = {
129
- width,
130
- height,
131
- theme: options?.theme,
132
- darkMode,
133
- watermark: options?.watermark,
134
- measureText,
135
- };
136
-
137
- const layout = compileBarList(currentSpec, compileOpts);
138
-
139
- // Auto-size height to content so few-row lists don't leave empty space.
140
- if (layout.rows.length > 0) {
141
- const lastRow = layout.rows[layout.rows.length - 1];
142
- const contentBottom =
143
- lastRow.y + lastRow.height + layout.chrome.bottomHeight + layout.theme.spacing.padding;
144
- if (contentBottom < layout.height) {
145
- layout.height = contentBottom;
146
- layout.area.height =
147
- contentBottom - layout.area.y - layout.chrome.bottomHeight - layout.theme.spacing.padding;
148
- }
149
- }
150
-
151
- return layout;
152
- }
153
-
154
- function wireTooltipAndInteraction(svg: SVGSVGElement, layout: BarListLayout): () => void {
155
- const cleanups: Array<() => void> = [];
156
-
157
- const rowElements = svg.querySelectorAll('.oc-barlist-row');
158
- for (const el of rowElements) {
159
- const indexStr = el.getAttribute('data-row-index');
160
- if (indexStr === null) continue;
161
-
162
- const content = layout.tooltipDescriptors.get(indexStr);
163
- const row = layout.rows[Number(indexStr)];
164
-
165
- const handleMouseEnter = (e: Event) => {
166
- const mouseEvent = e as MouseEvent;
167
- if (content && tooltipManager && options?.tooltip !== false) {
168
- const svgRect = svg.getBoundingClientRect();
169
- const x = mouseEvent.clientX - svgRect.left;
170
- const y = mouseEvent.clientY - svgRect.top;
171
- tooltipManager.show(content, x, y);
172
- }
173
- if (row) {
174
- options?.onRowHover?.({
175
- label: row.label.text,
176
- value: row.value,
177
- data: row.data,
178
- });
179
- }
180
- };
181
-
182
- const handleMouseMove = (e: Event) => {
183
- if (content && tooltipManager && options?.tooltip !== false) {
184
- const mouseEvent = e as MouseEvent;
185
- const svgRect = svg.getBoundingClientRect();
186
- const x = mouseEvent.clientX - svgRect.left;
187
- const y = mouseEvent.clientY - svgRect.top;
188
- tooltipManager.show(content, x, y);
189
- }
190
- };
191
-
192
- const handleMouseLeave = () => {
193
- tooltipManager?.hide();
194
- options?.onRowHover?.(null);
195
- };
196
-
197
- const handleClick = () => {
198
- if (row) {
199
- options?.onRowClick?.({
200
- label: row.label.text,
201
- value: row.value,
202
- data: row.data,
203
- });
204
- }
205
- };
206
-
207
- el.addEventListener('mouseenter', handleMouseEnter);
208
- el.addEventListener('mousemove', handleMouseMove);
209
- el.addEventListener('mouseleave', handleMouseLeave);
210
- el.addEventListener('click', handleClick);
211
-
212
- cleanups.push(() => {
213
- el.removeEventListener('mouseenter', handleMouseEnter);
214
- el.removeEventListener('mousemove', handleMouseMove);
215
- el.removeEventListener('mouseleave', handleMouseLeave);
216
- el.removeEventListener('click', handleClick);
217
- });
218
- }
219
-
220
- return () => {
221
- for (const cleanup of cleanups) cleanup();
222
- };
223
- }
224
-
225
- function render(animate = false): void {
226
- if (animationCleanup) {
227
- animationCleanup();
228
- animationCleanup = null;
229
- }
230
-
231
- if (svgElement) {
232
- if (cleanupTooltipEvents) {
233
- cleanupTooltipEvents();
234
- cleanupTooltipEvents = null;
235
- }
236
- svgElement.remove();
237
- }
238
-
239
- const newSvg = renderBarListSVG(currentLayout, { animate });
240
- container.appendChild(newSvg);
241
- svgElement = newSvg;
242
-
243
- cleanupTooltipEvents = wireTooltipAndInteraction(newSvg, currentLayout);
244
-
245
- if (options?.tooltip !== false) {
246
- if (!tooltipManager) {
247
- tooltipManager = createTooltipManager(container);
248
- }
249
- }
250
-
251
- if (currentLayout.animation?.enter) {
252
- animationCleanup = setupAnimationCleanup(newSvg, () => {
253
- if (pendingResize && !destroyed) {
254
- pendingResize = false;
255
- resize();
256
- }
257
- });
258
- }
259
-
260
- renderGen += 1;
261
- container.dataset.ocRenderGen = String(renderGen);
262
-
263
- // This render recompiled with the loaded webfonts; publish 'ready' now
264
- // rather than right after the fonts-ready resize() (which may have deferred
265
- // to pendingResize during the entrance animation).
266
- if (fontsReloadPending) {
267
- fontsReloadPending = false;
268
- container.dataset.ocFontsState = 'ready';
269
- }
270
- }
271
-
272
- function update(newSpec: BarListSpec): void {
273
- currentSpec = newSpec;
274
- // A new spec can change theme.fonts.family; rebuild the measurer so layout
275
- // measures the font compile will actually render with.
276
- const nextFont = resolveEffectiveFont();
277
- if (nextFont !== fontFamily) {
278
- fontFamily = nextFont;
279
- measureText = createMeasureText(fontFamily);
280
- }
281
- currentLayout = compile();
282
- render();
283
- }
284
-
285
- function resize(): void {
286
- if (destroyed) return;
287
- if (animationCleanup) {
288
- pendingResize = true;
289
- return;
290
- }
291
- currentLayout = compile();
292
- render();
293
- }
294
-
295
- function exportChart(
296
- format: 'svg' | 'svg-with-fonts' | 'png' | 'jpg',
297
- options_?: JPGExportOptions | SVGExportOptions,
298
- ): string | Promise<Blob> | Promise<string> {
299
- if (!svgElement) return '';
300
- switch (format) {
301
- case 'svg':
302
- return exportSVG(svgElement);
303
- case 'svg-with-fonts':
304
- return exportSVGWithFonts(svgElement);
305
- case 'png':
306
- return exportPNG(svgElement, options_ as JPGExportOptions);
307
- case 'jpg':
308
- return exportJPG(svgElement, options_ as JPGExportOptions);
309
- default:
310
- return '';
311
- }
312
- }
313
-
314
- function destroy(): void {
315
- if (destroyed) return;
316
- destroyed = true;
317
-
318
- if (animationCleanup) {
319
- cancelAnimations(svgElement);
320
- animationCleanup();
321
- animationCleanup = null;
322
- }
323
-
324
- if (cleanupTooltipEvents) {
325
- cleanupTooltipEvents();
326
- cleanupTooltipEvents = null;
327
- }
328
-
329
- if (svgElement) {
330
- svgElement.remove();
331
- svgElement = null;
332
- }
333
-
334
- if (tooltipManager) {
335
- tooltipManager.destroy();
336
- tooltipManager = null;
337
- }
338
-
339
- if (disconnectResize) {
340
- disconnectResize();
341
- disconnectResize = null;
342
- }
343
-
344
- container.classList.remove('oc-barlist-root', 'oc-dark');
345
- }
346
-
347
- // Initialize
348
- if (resolveDarkMode(options?.darkMode)) {
349
- container.classList.add('oc-dark');
350
- }
351
-
352
- currentLayout = compile();
353
- render(true);
354
-
355
- // Recompile once after webfonts load so row labels aren't stuck measured
356
- // against fallback metrics on real devices.
357
- const fontsPending = scheduleFontReload(
358
- fontFamily,
359
- () => !destroyed,
360
- () => {
361
- // Mark the reload owed, then recompile. render() flips the attribute to
362
- // 'ready' once it actually recompiles, including the pendingResize replay
363
- // after the entrance animation finishes.
364
- fontsReloadPending = true;
365
- resize();
366
- },
367
- );
368
- container.dataset.ocFontsState = fontsPending ? 'pending' : 'ready';
369
-
370
- if (options?.responsive !== false) {
371
- disconnectResize = observeResize(container, () => resize());
372
- }
373
-
374
- return {
375
- update,
376
- resize,
377
- export: exportChart,
378
- destroy,
379
- get layout(): BarListLayout {
380
- return currentLayout;
381
- },
382
- };
383
- }
@@ -1,267 +0,0 @@
1
- /**
2
- * BarList SVG renderer: converts a BarListLayout into SVG DOM elements.
3
- *
4
- * Creates an <svg> with rows of label + track + bar + value. Animation is
5
- * pure CSS, driven by data attributes and CSS custom properties.
6
- */
7
-
8
- import type { BarListLayout, BarListRowMark, ResolvedAnimation } from '@opendata-ai/openchart-core';
9
- import { textAscent } from '@opendata-ai/openchart-core';
10
- import { stampAnimationVars } from './animation-vars';
11
-
12
- const SVG_NS = 'http://www.w3.org/2000/svg';
13
- const XLINK_NS = 'http://www.w3.org/1999/xlink';
14
- const BRAND_URL = 'https://tryopendata.ai';
15
-
16
- // ---------------------------------------------------------------------------
17
- // Helpers
18
- // ---------------------------------------------------------------------------
19
-
20
- function createSVGElement(tag: string): SVGElement {
21
- return document.createElementNS(SVG_NS, tag);
22
- }
23
-
24
- function setAttrs(el: SVGElement, attrs: Record<string, string | number>): void {
25
- for (const [key, value] of Object.entries(attrs)) {
26
- el.setAttribute(key, String(value));
27
- }
28
- }
29
-
30
- // ---------------------------------------------------------------------------
31
- // Chrome rendering
32
- // ---------------------------------------------------------------------------
33
-
34
- function renderChrome(parent: SVGElement, layout: BarListLayout): void {
35
- const g = createSVGElement('g');
36
- g.setAttribute('class', 'oc-chrome');
37
-
38
- const { chrome } = layout;
39
- const bottomOffset = layout.area.y + layout.area.height;
40
-
41
- for (const key of ['title', 'subtitle', 'source', 'byline', 'footer'] as const) {
42
- const el = chrome[key];
43
- if (!el) continue;
44
-
45
- const isBottom = key === 'source' || key === 'byline' || key === 'footer';
46
- const text = createSVGElement('text');
47
- // el.y is the TOP of the text box; convert to the alphabetic baseline
48
- // (WebKit mishandles dominant-baseline:hanging, see renderers/chrome.ts).
49
- setAttrs(text, {
50
- x: el.x,
51
- y: (isBottom ? bottomOffset + el.y : el.y) + textAscent(el.style.fontSize),
52
- });
53
- text.setAttribute('class', `oc-${key}`);
54
- text.setAttribute('font-family', el.style.fontFamily);
55
- text.setAttribute('font-size', String(el.style.fontSize));
56
- text.setAttribute('font-weight', String(el.style.fontWeight));
57
- (text as SVGElement & ElementCSSInlineStyle).style.setProperty('fill', el.style.fill);
58
- text.textContent = el.text;
59
- g.appendChild(text);
60
- }
61
-
62
- parent.appendChild(g);
63
- }
64
-
65
- // ---------------------------------------------------------------------------
66
- // Watermark rendering
67
- // ---------------------------------------------------------------------------
68
-
69
- function renderWatermark(parent: SVGElement, layout: BarListLayout): void {
70
- if (layout.width < 480) return;
71
-
72
- const { width, height, theme } = layout;
73
- const padding = theme.spacing.padding;
74
- const rightEdge = width - padding;
75
- const bottomEdge = height - padding;
76
- const fill = theme.colors.axis;
77
-
78
- const a = createSVGElement('a');
79
- a.setAttribute('href', BRAND_URL);
80
- a.setAttributeNS(XLINK_NS, 'xlink:href', BRAND_URL);
81
- a.setAttribute('target', '_blank');
82
- a.setAttribute('rel', 'noopener');
83
- a.setAttribute('class', 'oc-chrome-ref');
84
-
85
- const text = createSVGElement('text');
86
- setAttrs(text, {
87
- x: rightEdge,
88
- y: bottomEdge,
89
- 'dominant-baseline': 'alphabetic',
90
- 'text-anchor': 'end',
91
- 'font-family': theme.fonts.family,
92
- 'font-size': 12,
93
- 'fill-opacity': 0.55,
94
- });
95
- (text as SVGElement & ElementCSSInlineStyle).style.setProperty('fill', fill);
96
-
97
- const trySpan = createSVGElement('tspan');
98
- setAttrs(trySpan, { 'font-weight': 500 });
99
- trySpan.textContent = 'try';
100
-
101
- const openDataSpan = createSVGElement('tspan');
102
- setAttrs(openDataSpan, { 'font-weight': 600, 'font-size': 16 });
103
- openDataSpan.textContent = 'OpenData';
104
-
105
- const aiSpan = createSVGElement('tspan');
106
- setAttrs(aiSpan, { 'font-weight': 500 });
107
- aiSpan.textContent = '.ai';
108
-
109
- text.appendChild(trySpan);
110
- text.appendChild(openDataSpan);
111
- text.appendChild(aiSpan);
112
- a.appendChild(text);
113
- parent.appendChild(a);
114
- }
115
-
116
- // ---------------------------------------------------------------------------
117
- // Row rendering
118
- // ---------------------------------------------------------------------------
119
-
120
- function renderRows(
121
- parent: SVGElement,
122
- rows: BarListRowMark[],
123
- animation?: ResolvedAnimation,
124
- ): void {
125
- const g = createSVGElement('g');
126
- g.setAttribute('class', 'oc-barlist-rows');
127
- g.setAttribute('role', 'list');
128
-
129
- for (const row of rows) {
130
- const rowGroup = createSVGElement('g');
131
- rowGroup.setAttribute('class', 'oc-barlist-row');
132
- rowGroup.setAttribute('data-row-index', String(row.index));
133
- rowGroup.setAttribute('role', 'listitem');
134
- if (row.aria?.label) {
135
- rowGroup.setAttribute('aria-label', row.aria.label);
136
- }
137
-
138
- if (animation?.enter) {
139
- rowGroup.setAttribute('data-animation-index', String(row.animationIndex));
140
- const style = (rowGroup as SVGElement & ElementCSSInlineStyle).style;
141
- style.setProperty('--oc-mark-index', String(row.animationIndex));
142
- style.setProperty('--oc-row-delay', `${row.animationIndex * 40}ms`);
143
- }
144
-
145
- // Label text
146
- const labelEl = createSVGElement('text');
147
- setAttrs(labelEl, {
148
- x: row.label.x,
149
- y: row.label.y,
150
- 'dominant-baseline': 'central',
151
- 'text-anchor': 'start',
152
- 'font-family': row.label.style.fontFamily,
153
- 'font-size': row.label.style.fontSize,
154
- 'font-weight': row.label.style.fontWeight,
155
- });
156
- (labelEl as SVGElement & ElementCSSInlineStyle).style.setProperty('fill', row.label.style.fill);
157
- labelEl.textContent = row.label.text;
158
- rowGroup.appendChild(labelEl);
159
-
160
- // Subtitle text (if present)
161
- if (row.subtitle?.visible) {
162
- const subEl = createSVGElement('text');
163
- setAttrs(subEl, {
164
- x: row.subtitle.x,
165
- y: row.subtitle.y,
166
- 'dominant-baseline': 'central',
167
- 'text-anchor': 'start',
168
- 'font-family': row.subtitle.style.fontFamily,
169
- 'font-size': row.subtitle.style.fontSize,
170
- 'font-weight': row.subtitle.style.fontWeight,
171
- });
172
- (subEl as SVGElement & ElementCSSInlineStyle).style.setProperty(
173
- 'fill',
174
- row.subtitle.style.fill,
175
- );
176
- subEl.textContent = row.subtitle.text;
177
- rowGroup.appendChild(subEl);
178
- }
179
-
180
- // Track (muted background bar)
181
- const trackRect = createSVGElement('rect');
182
- setAttrs(trackRect, {
183
- x: row.track.x,
184
- y: row.track.y,
185
- width: row.track.width,
186
- height: row.track.height,
187
- rx: row.track.cornerRadius,
188
- fill: 'currentColor',
189
- 'fill-opacity': 0.06,
190
- });
191
- trackRect.setAttribute('class', 'oc-barlist-track');
192
- rowGroup.appendChild(trackRect);
193
-
194
- // Fill bar
195
- const barRect = createSVGElement('rect');
196
- setAttrs(barRect, {
197
- x: row.bar.x,
198
- y: row.bar.y,
199
- width: row.bar.width,
200
- height: row.bar.height,
201
- rx: row.bar.cornerRadius,
202
- fill: row.bar.fill,
203
- });
204
- barRect.setAttribute('class', 'oc-barlist-bar');
205
- rowGroup.appendChild(barRect);
206
-
207
- // Value label (right-aligned)
208
- const valueEl = createSVGElement('text');
209
- setAttrs(valueEl, {
210
- x: row.valueLabel.x,
211
- y: row.valueLabel.y,
212
- 'dominant-baseline': 'central',
213
- 'text-anchor': 'end',
214
- 'font-family': row.valueLabel.style.fontFamily,
215
- 'font-size': row.valueLabel.style.fontSize,
216
- 'font-weight': row.valueLabel.style.fontWeight,
217
- });
218
- (valueEl as SVGElement & ElementCSSInlineStyle).style.setProperty(
219
- 'fill',
220
- row.valueLabel.style.fill,
221
- );
222
- valueEl.textContent = row.valueLabel.text;
223
- rowGroup.appendChild(valueEl);
224
-
225
- g.appendChild(rowGroup);
226
- }
227
-
228
- parent.appendChild(g);
229
- }
230
-
231
- // ---------------------------------------------------------------------------
232
- // Public API
233
- // ---------------------------------------------------------------------------
234
-
235
- export function renderBarListSVG(
236
- layout: BarListLayout,
237
- opts?: { animate?: boolean },
238
- ): SVGSVGElement {
239
- const { width, height, rows, a11y, watermark, animation } = layout;
240
- const animate = opts?.animate && !!animation?.enter;
241
-
242
- const svg = createSVGElement('svg') as SVGSVGElement;
243
- svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
244
- svg.setAttribute('role', 'list');
245
- // No explicit width attribute — CSS width:100% on .oc-barlist-root handles it.
246
- // Explicit pixel height via inline style avoids iOS Safari's height:100% quirk.
247
- svg.style.height = `${height}px`;
248
- if (a11y.altText) {
249
- svg.setAttribute('aria-label', a11y.altText);
250
- }
251
-
252
- const classes = animate ? 'oc-barlist oc-animate' : 'oc-barlist';
253
- svg.setAttribute('class', classes);
254
-
255
- if (animate && animation?.enter) {
256
- stampAnimationVars(svg, { duration: animation.enter.duration, stagger: 40 });
257
- }
258
-
259
- renderChrome(svg, layout);
260
- renderRows(svg, rows, animate ? animation : undefined);
261
-
262
- if (watermark) {
263
- renderWatermark(svg, layout);
264
- }
265
-
266
- return svg;
267
- }
@@ -1,17 +0,0 @@
1
- /** Shared small DOM helpers used across vanilla renderers. */
2
-
3
- /**
4
- * Inline sr-only styles — mirrors .oc-sr-only in base.css but works without
5
- * the stylesheet (CDN / esm.sh usage).
6
- */
7
- export function applySrOnlyStyles(el: HTMLElement): void {
8
- el.style.position = 'absolute';
9
- el.style.width = '1px';
10
- el.style.height = '1px';
11
- el.style.padding = '0';
12
- el.style.margin = '-1px';
13
- el.style.overflow = 'hidden';
14
- el.style.clipPath = 'inset(50%)';
15
- el.style.whiteSpace = 'nowrap';
16
- el.style.borderWidth = '0';
17
- }