@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,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
- }