@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
package/src/barlist-mount.ts
DELETED
|
@@ -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
|
-
}
|
package/src/barlist-renderer.ts
DELETED
|
@@ -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
|
-
}
|
package/src/dom-helpers.ts
DELETED
|
@@ -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
|
-
}
|