@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/series-search.ts
DELETED
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Series search combobox: the DOM overlay for the `seriesSearch` spec option.
|
|
3
|
-
*
|
|
4
|
-
* Renders an ARIA 1.2 editable combobox (input + popup listbox) with
|
|
5
|
-
* multi-select chips, absolutely positioned inside `.oc-root` over the band
|
|
6
|
-
* the chrome layout reserves (`layout.seriesSearch`). Because the overlay is
|
|
7
|
-
* position: absolute, mounting or updating it never changes the observed
|
|
8
|
-
* container's size, so it cannot re-trigger the container ResizeObserver.
|
|
9
|
-
*
|
|
10
|
-
* Matching is a diacritic-insensitive normalized prefix/substring match
|
|
11
|
-
* (prefix matches rank first). No fuzzy-ranking dependency.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { ResolvedSeriesSearch } from '@opendata-ai/openchart-core';
|
|
15
|
-
|
|
16
|
-
export interface SeriesSearchOptions {
|
|
17
|
-
/** The chart container (`.oc-root`). Must be position: relative. */
|
|
18
|
-
container: HTMLElement;
|
|
19
|
-
/** Fired with the full selection (insertion order) after every add/remove. */
|
|
20
|
-
onChange: (selected: string[]) => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface SeriesSearchController {
|
|
24
|
-
/** Reposition and refresh the control against a freshly rendered layout. */
|
|
25
|
-
update(band: ResolvedSeriesSearch, svg: SVGElement): void;
|
|
26
|
-
/** Hide the control (layout stripped the band but the spec still wants it). */
|
|
27
|
-
hide(): void;
|
|
28
|
-
/** Currently selected values, in insertion order. */
|
|
29
|
-
getSelected(): string[];
|
|
30
|
-
/** Replace the selection without firing onChange (e.g. reset on spec update). */
|
|
31
|
-
setSelected(values: string[]): void;
|
|
32
|
-
/** Remove the control from the DOM. */
|
|
33
|
-
destroy(): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let seriesSearchIdCounter = 0;
|
|
37
|
-
|
|
38
|
-
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Fold a string for matching: lowercase with combining diacritical marks
|
|
42
|
-
* stripped. `map[i]` is the index in the original string that produced
|
|
43
|
-
* folded character `i`, so match ranges can be mapped back for emphasis.
|
|
44
|
-
*/
|
|
45
|
-
function foldWithMap(s: string): { folded: string; map: number[] } {
|
|
46
|
-
let folded = '';
|
|
47
|
-
const map: number[] = [];
|
|
48
|
-
for (let i = 0; i < s.length; i++) {
|
|
49
|
-
const f = s[i]
|
|
50
|
-
.normalize('NFD')
|
|
51
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
52
|
-
.toLowerCase();
|
|
53
|
-
for (const ch of f) {
|
|
54
|
-
folded += ch;
|
|
55
|
-
map.push(i);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return { folded, map };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function fold(s: string): string {
|
|
62
|
-
return foldWithMap(s).folded;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Small stroke icon (magnifier or cross) built from SVG path data. */
|
|
66
|
-
function createIcon(size: number, paths: string[]): SVGSVGElement {
|
|
67
|
-
const svg = document.createElementNS(SVG_NS, 'svg');
|
|
68
|
-
svg.setAttribute('width', String(size));
|
|
69
|
-
svg.setAttribute('height', String(size));
|
|
70
|
-
svg.setAttribute('viewBox', '0 0 16 16');
|
|
71
|
-
svg.setAttribute('fill', 'none');
|
|
72
|
-
svg.setAttribute('aria-hidden', 'true');
|
|
73
|
-
for (const d of paths) {
|
|
74
|
-
const path = document.createElementNS(SVG_NS, 'path');
|
|
75
|
-
path.setAttribute('d', d);
|
|
76
|
-
path.setAttribute('stroke', 'currentColor');
|
|
77
|
-
path.setAttribute('stroke-width', '1.5');
|
|
78
|
-
path.setAttribute('stroke-linecap', 'round');
|
|
79
|
-
svg.appendChild(path);
|
|
80
|
-
}
|
|
81
|
-
return svg;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Create the series search control. Mounted once per chart instance and kept
|
|
86
|
-
* across re-renders (so typing focus and chips survive highlight re-renders);
|
|
87
|
-
* `update()` re-syncs position, values, and placeholder after each render.
|
|
88
|
-
*/
|
|
89
|
-
export function createSeriesSearch(options: SeriesSearchOptions): SeriesSearchController {
|
|
90
|
-
const { container, onChange } = options;
|
|
91
|
-
|
|
92
|
-
const id = `oc-series-search-${++seriesSearchIdCounter}`;
|
|
93
|
-
const listboxId = `${id}-listbox`;
|
|
94
|
-
|
|
95
|
-
let values: string[] = [];
|
|
96
|
-
let selected: string[] = [];
|
|
97
|
-
let matches: string[] = [];
|
|
98
|
-
let activeIndex = -1;
|
|
99
|
-
let open = false;
|
|
100
|
-
let destroyed = false;
|
|
101
|
-
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
// DOM
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
const root = document.createElement('div');
|
|
107
|
-
root.className = 'oc-series-search';
|
|
108
|
-
root.style.display = 'none';
|
|
109
|
-
|
|
110
|
-
const chips = document.createElement('div');
|
|
111
|
-
chips.className = 'oc-series-search-chips';
|
|
112
|
-
|
|
113
|
-
const box = document.createElement('div');
|
|
114
|
-
box.className = 'oc-series-search-box';
|
|
115
|
-
|
|
116
|
-
const icon = createIcon(14, [
|
|
117
|
-
'M7 11.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9Z',
|
|
118
|
-
'M10.5 10.5 13.5 13.5',
|
|
119
|
-
]);
|
|
120
|
-
icon.classList.add('oc-series-search-icon');
|
|
121
|
-
|
|
122
|
-
const input = document.createElement('input');
|
|
123
|
-
input.className = 'oc-series-search-input';
|
|
124
|
-
input.type = 'text';
|
|
125
|
-
input.setAttribute('role', 'combobox');
|
|
126
|
-
input.setAttribute('aria-expanded', 'false');
|
|
127
|
-
input.setAttribute('aria-controls', listboxId);
|
|
128
|
-
input.setAttribute('aria-autocomplete', 'list');
|
|
129
|
-
input.setAttribute('aria-haspopup', 'listbox');
|
|
130
|
-
input.autocomplete = 'off';
|
|
131
|
-
input.spellcheck = false;
|
|
132
|
-
|
|
133
|
-
const listbox = document.createElement('ul');
|
|
134
|
-
listbox.className = 'oc-series-search-listbox';
|
|
135
|
-
listbox.id = listboxId;
|
|
136
|
-
listbox.setAttribute('role', 'listbox');
|
|
137
|
-
listbox.hidden = true;
|
|
138
|
-
|
|
139
|
-
box.append(icon, input, listbox);
|
|
140
|
-
root.append(chips, box);
|
|
141
|
-
// Ensure absolute children position against the chart container, matching
|
|
142
|
-
// the tooltip manager's approach.
|
|
143
|
-
container.style.position = container.style.position || 'relative';
|
|
144
|
-
// First child so the tab order matches the visual order (search above chart);
|
|
145
|
-
// render() re-appends the SVG after this node on every re-render.
|
|
146
|
-
container.insertBefore(root, container.firstChild);
|
|
147
|
-
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
// Matching
|
|
150
|
-
// ---------------------------------------------------------------------------
|
|
151
|
-
|
|
152
|
-
function computeMatches(): string[] {
|
|
153
|
-
const q = fold(input.value.trim());
|
|
154
|
-
const available = values.filter((v) => !selected.includes(v));
|
|
155
|
-
if (!q) return available;
|
|
156
|
-
const prefix: string[] = [];
|
|
157
|
-
const substring: string[] = [];
|
|
158
|
-
for (const v of available) {
|
|
159
|
-
const idx = fold(v).indexOf(q);
|
|
160
|
-
if (idx === 0) prefix.push(v);
|
|
161
|
-
else if (idx > 0) substring.push(v);
|
|
162
|
-
}
|
|
163
|
-
return [...prefix, ...substring];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Append the option label with the matched substring emphasized. */
|
|
167
|
-
function appendOptionText(li: HTMLLIElement, value: string): void {
|
|
168
|
-
const q = fold(input.value.trim());
|
|
169
|
-
if (!q) {
|
|
170
|
-
li.textContent = value;
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const { folded, map } = foldWithMap(value);
|
|
174
|
-
const idx = folded.indexOf(q);
|
|
175
|
-
if (idx < 0) {
|
|
176
|
-
li.textContent = value;
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const start = map[idx];
|
|
180
|
-
const end = idx + q.length < folded.length ? map[idx + q.length] : value.length;
|
|
181
|
-
li.append(document.createTextNode(value.slice(0, start)));
|
|
182
|
-
const match = document.createElement('span');
|
|
183
|
-
match.className = 'oc-series-search-match';
|
|
184
|
-
match.textContent = value.slice(start, end);
|
|
185
|
-
li.append(match, document.createTextNode(value.slice(end)));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
|
-
// Rendering
|
|
190
|
-
// ---------------------------------------------------------------------------
|
|
191
|
-
|
|
192
|
-
function renderChips(): void {
|
|
193
|
-
chips.textContent = '';
|
|
194
|
-
for (const value of selected) {
|
|
195
|
-
const chip = document.createElement('span');
|
|
196
|
-
chip.className = 'oc-series-search-chip';
|
|
197
|
-
chip.append(document.createTextNode(value));
|
|
198
|
-
|
|
199
|
-
const remove = document.createElement('button');
|
|
200
|
-
remove.type = 'button';
|
|
201
|
-
remove.className = 'oc-series-search-chip-remove';
|
|
202
|
-
remove.setAttribute('aria-label', `Remove ${value}`);
|
|
203
|
-
remove.appendChild(createIcon(8, ['M3 3 13 13', 'M13 3 3 13']));
|
|
204
|
-
remove.addEventListener('click', () => {
|
|
205
|
-
removeValue(value);
|
|
206
|
-
input.focus();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
chip.appendChild(remove);
|
|
210
|
-
chips.appendChild(chip);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function renderList(): void {
|
|
215
|
-
matches = computeMatches();
|
|
216
|
-
if (activeIndex >= matches.length) activeIndex = matches.length - 1;
|
|
217
|
-
listbox.textContent = '';
|
|
218
|
-
|
|
219
|
-
if (matches.length === 0) {
|
|
220
|
-
const empty = document.createElement('li');
|
|
221
|
-
empty.className = 'oc-series-search-empty';
|
|
222
|
-
empty.setAttribute('role', 'presentation');
|
|
223
|
-
empty.textContent = 'No matches';
|
|
224
|
-
listbox.appendChild(empty);
|
|
225
|
-
input.removeAttribute('aria-activedescendant');
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
matches.forEach((value, i) => {
|
|
230
|
-
const li = document.createElement('li');
|
|
231
|
-
li.className = 'oc-series-search-option';
|
|
232
|
-
li.id = `${id}-opt-${i}`;
|
|
233
|
-
li.setAttribute('role', 'option');
|
|
234
|
-
li.setAttribute('aria-selected', String(i === activeIndex));
|
|
235
|
-
appendOptionText(li, value);
|
|
236
|
-
li.addEventListener('click', () => selectValue(value));
|
|
237
|
-
listbox.appendChild(li);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
if (activeIndex >= 0) {
|
|
241
|
-
input.setAttribute('aria-activedescendant', `${id}-opt-${activeIndex}`);
|
|
242
|
-
const activeEl = listbox.children[activeIndex] as HTMLElement | undefined;
|
|
243
|
-
activeEl?.scrollIntoView?.({ block: 'nearest' });
|
|
244
|
-
} else {
|
|
245
|
-
input.removeAttribute('aria-activedescendant');
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function openList(): void {
|
|
250
|
-
if (open) return;
|
|
251
|
-
open = true;
|
|
252
|
-
listbox.hidden = false;
|
|
253
|
-
input.setAttribute('aria-expanded', 'true');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function closeList(): void {
|
|
257
|
-
if (!open) return;
|
|
258
|
-
open = false;
|
|
259
|
-
activeIndex = -1;
|
|
260
|
-
listbox.hidden = true;
|
|
261
|
-
input.setAttribute('aria-expanded', 'false');
|
|
262
|
-
input.removeAttribute('aria-activedescendant');
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// Selection
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
|
|
269
|
-
function selectValue(value: string): void {
|
|
270
|
-
if (!selected.includes(value)) selected.push(value);
|
|
271
|
-
input.value = '';
|
|
272
|
-
closeList();
|
|
273
|
-
renderChips();
|
|
274
|
-
onChange([...selected]);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function removeValue(value: string): void {
|
|
278
|
-
const idx = selected.indexOf(value);
|
|
279
|
-
if (idx < 0) return;
|
|
280
|
-
selected.splice(idx, 1);
|
|
281
|
-
renderChips();
|
|
282
|
-
onChange([...selected]);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// ---------------------------------------------------------------------------
|
|
286
|
-
// Events
|
|
287
|
-
// ---------------------------------------------------------------------------
|
|
288
|
-
|
|
289
|
-
input.addEventListener('input', () => {
|
|
290
|
-
openList();
|
|
291
|
-
activeIndex = computeMatches().length > 0 ? 0 : -1;
|
|
292
|
-
renderList();
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
input.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
296
|
-
switch (e.key) {
|
|
297
|
-
case 'ArrowDown':
|
|
298
|
-
case 'ArrowUp': {
|
|
299
|
-
e.preventDefault();
|
|
300
|
-
const wasOpen = open;
|
|
301
|
-
openList();
|
|
302
|
-
if (!wasOpen) {
|
|
303
|
-
matches = computeMatches();
|
|
304
|
-
activeIndex = matches.length > 0 ? 0 : -1;
|
|
305
|
-
} else if (matches.length > 0) {
|
|
306
|
-
const delta = e.key === 'ArrowDown' ? 1 : -1;
|
|
307
|
-
activeIndex = (activeIndex + delta + matches.length) % matches.length;
|
|
308
|
-
}
|
|
309
|
-
renderList();
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
case 'Enter': {
|
|
313
|
-
if (open && activeIndex >= 0 && activeIndex < matches.length) {
|
|
314
|
-
e.preventDefault();
|
|
315
|
-
selectValue(matches[activeIndex]);
|
|
316
|
-
}
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
case 'Escape': {
|
|
320
|
-
if (open) {
|
|
321
|
-
e.preventDefault();
|
|
322
|
-
e.stopPropagation();
|
|
323
|
-
closeList();
|
|
324
|
-
}
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
case 'Backspace': {
|
|
328
|
-
if (input.value === '' && selected.length > 0) {
|
|
329
|
-
removeValue(selected[selected.length - 1]);
|
|
330
|
-
}
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
case 'Tab': {
|
|
334
|
-
closeList();
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Keep focus in the input while clicking options/chips inside the popup.
|
|
341
|
-
listbox.addEventListener('mousedown', (e) => e.preventDefault());
|
|
342
|
-
|
|
343
|
-
// Close when focus leaves the whole control.
|
|
344
|
-
root.addEventListener('focusout', (e: FocusEvent) => {
|
|
345
|
-
const next = e.relatedTarget as Node | null;
|
|
346
|
-
if (!next || !root.contains(next)) closeList();
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
// ---------------------------------------------------------------------------
|
|
350
|
-
// Controller
|
|
351
|
-
// ---------------------------------------------------------------------------
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
update(band: ResolvedSeriesSearch, svg: SVGElement): void {
|
|
355
|
-
if (destroyed) return;
|
|
356
|
-
values = band.values;
|
|
357
|
-
input.placeholder = band.placeholder;
|
|
358
|
-
input.setAttribute('aria-label', band.placeholder);
|
|
359
|
-
|
|
360
|
-
// Layout coords -> pixel coords relative to the container, same
|
|
361
|
-
// viewBox-scale approach as the text edit overlay.
|
|
362
|
-
const viewBox = (svg as SVGSVGElement).viewBox?.baseVal;
|
|
363
|
-
const svgRect = svg.getBoundingClientRect();
|
|
364
|
-
const containerRect = container.getBoundingClientRect();
|
|
365
|
-
const scaleX = viewBox?.width && svgRect.width ? svgRect.width / viewBox.width : 1;
|
|
366
|
-
const scaleY = viewBox?.height && svgRect.height ? svgRect.height / viewBox.height : 1;
|
|
367
|
-
root.style.left = `${band.x * scaleX + (svgRect.left - containerRect.left)}px`;
|
|
368
|
-
root.style.top = `${band.y * scaleY + (svgRect.top - containerRect.top)}px`;
|
|
369
|
-
root.style.width = `${band.width * scaleX}px`;
|
|
370
|
-
root.style.height = `${band.height * scaleY}px`;
|
|
371
|
-
root.style.display = 'flex';
|
|
372
|
-
|
|
373
|
-
if (open) renderList();
|
|
374
|
-
},
|
|
375
|
-
hide(): void {
|
|
376
|
-
root.style.display = 'none';
|
|
377
|
-
closeList();
|
|
378
|
-
},
|
|
379
|
-
getSelected(): string[] {
|
|
380
|
-
return [...selected];
|
|
381
|
-
},
|
|
382
|
-
setSelected(next: string[]): void {
|
|
383
|
-
selected = [...next];
|
|
384
|
-
renderChips();
|
|
385
|
-
},
|
|
386
|
-
destroy(): void {
|
|
387
|
-
if (destroyed) return;
|
|
388
|
-
destroyed = true;
|
|
389
|
-
root.remove();
|
|
390
|
-
},
|
|
391
|
-
};
|
|
392
|
-
}
|
package/src/static.ts
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
// Node.js only — do not import from browser code.
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
|
-
import type {
|
|
4
|
-
ChartLayout,
|
|
5
|
-
ChartSpec,
|
|
6
|
-
CompileOptions,
|
|
7
|
-
DarkMode,
|
|
8
|
-
LayerSpec,
|
|
9
|
-
ResolvedTheme,
|
|
10
|
-
ThemeConfig,
|
|
11
|
-
TileMapSpec,
|
|
12
|
-
} from '@opendata-ai/openchart-core';
|
|
13
|
-
import {
|
|
14
|
-
adaptForLightLineStroke,
|
|
15
|
-
cssTokenDefault,
|
|
16
|
-
isLayerSpec,
|
|
17
|
-
isTileMapSpec,
|
|
18
|
-
} from '@opendata-ai/openchart-core';
|
|
19
|
-
import { compileChart, compileLayer, compileTileMap } from '@opendata-ai/openchart-engine';
|
|
20
|
-
import { SVG_NS } from './renderers/svg-dom';
|
|
21
|
-
import { resetSvgIdCounter } from './svg-ids';
|
|
22
|
-
import { renderChartSVG } from './svg-renderer';
|
|
23
|
-
import { renderTileMapSVG } from './tilemap-renderer';
|
|
24
|
-
|
|
25
|
-
const esmRequire = createRequire(import.meta.url);
|
|
26
|
-
|
|
27
|
-
let cachedWindow: typeof import('happy-dom').Window | undefined;
|
|
28
|
-
function getHappyDomWindow(): typeof import('happy-dom').Window {
|
|
29
|
-
if (cachedWindow) return cachedWindow;
|
|
30
|
-
try {
|
|
31
|
-
({ Window: cachedWindow } = esmRequire('happy-dom') as typeof import('happy-dom'));
|
|
32
|
-
} catch {
|
|
33
|
-
throw new Error(
|
|
34
|
-
"renderStaticSVG requires 'happy-dom' as a peer dependency. Install it with: npm add happy-dom",
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return cachedWindow;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
let rendering = false;
|
|
41
|
-
|
|
42
|
-
export interface StaticRenderOptions {
|
|
43
|
-
width?: number;
|
|
44
|
-
height?: number;
|
|
45
|
-
theme?: ThemeConfig;
|
|
46
|
-
/**
|
|
47
|
-
* Dark mode setting. In static rendering `'auto'` resolves to light mode
|
|
48
|
-
* since there is no `matchMedia` to query. Use `'force'` for dark output.
|
|
49
|
-
*/
|
|
50
|
-
darkMode?: DarkMode;
|
|
51
|
-
watermark?: boolean;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function resolveStaticDarkMode(mode?: DarkMode): boolean {
|
|
55
|
-
if (mode === 'force') return true;
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function buildThemeStyleBlock(theme: ResolvedTheme): string {
|
|
60
|
-
const accent = theme.colors.categorical[0] ?? cssTokenDefault('--oc-accent', 'light');
|
|
61
|
-
const bg =
|
|
62
|
-
theme.colors.background === 'transparent'
|
|
63
|
-
? theme.isDark
|
|
64
|
-
? cssTokenDefault('--oc-bg', 'dark')
|
|
65
|
-
: cssTokenDefault('--oc-bg', 'light')
|
|
66
|
-
: theme.colors.background;
|
|
67
|
-
|
|
68
|
-
const props = [
|
|
69
|
-
`--oc-font-family: ${theme.fonts.family}`,
|
|
70
|
-
`--oc-font-mono: ${theme.fonts.mono}`,
|
|
71
|
-
`--oc-title-size: ${theme.chrome.title.fontSize}px`,
|
|
72
|
-
`--oc-title-weight: ${theme.chrome.title.fontWeight}`,
|
|
73
|
-
`--oc-title-tracking: ${cssTokenDefault('--oc-title-tracking', 'light')}`,
|
|
74
|
-
`--oc-subtitle-size: ${theme.chrome.subtitle.fontSize}px`,
|
|
75
|
-
`--oc-subtitle-weight: ${theme.chrome.subtitle.fontWeight}`,
|
|
76
|
-
`--oc-source-size: ${theme.chrome.source.fontSize}px`,
|
|
77
|
-
`--oc-source-weight: ${theme.chrome.source.fontWeight}`,
|
|
78
|
-
`--oc-body-size: ${theme.fonts.sizes.body}px`,
|
|
79
|
-
`--oc-eyebrow-size: ${theme.chrome.eyebrow.fontSize}px`,
|
|
80
|
-
`--oc-eyebrow-weight: ${theme.chrome.eyebrow.fontWeight}`,
|
|
81
|
-
`--oc-eyebrow-tracking: ${cssTokenDefault('--oc-eyebrow-tracking', 'light')}`,
|
|
82
|
-
`--oc-bg: ${bg}`,
|
|
83
|
-
`--oc-text: ${theme.colors.text}`,
|
|
84
|
-
`--oc-text-muted: ${theme.colors.axis}`,
|
|
85
|
-
`--oc-text-faint: ${theme.isDark ? cssTokenDefault('--oc-text-faint', 'dark') : cssTokenDefault('--oc-text-faint', 'light')}`,
|
|
86
|
-
`--oc-gridline: ${theme.colors.gridline}`,
|
|
87
|
-
`--oc-axis: ${theme.colors.axis}`,
|
|
88
|
-
`--oc-border-radius: ${theme.borderRadius}px`,
|
|
89
|
-
`--oc-accent: ${accent}`,
|
|
90
|
-
`--oc-accent-strong: ${adaptForLightLineStroke(accent)}`,
|
|
91
|
-
`--oc-positive: ${theme.colors.positive}`,
|
|
92
|
-
`--oc-negative: ${theme.colors.negative}`,
|
|
93
|
-
`--oc-legend-text: ${theme.isDark ? cssTokenDefault('--oc-legend-text', 'dark') : cssTokenDefault('--oc-legend-text', 'light')}`,
|
|
94
|
-
`--oc-space-2: ${theme.spacing.chromeGap * 2}px`,
|
|
95
|
-
`--oc-space-4: ${theme.spacing.padding}px`,
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
const rules = [
|
|
99
|
-
`svg.oc-chart { ${props.join('; ')}; }`,
|
|
100
|
-
`.oc-chrome { font-family: var(--oc-font-family); }`,
|
|
101
|
-
`.oc-eyebrow { font-size: var(--oc-eyebrow-size); font-weight: var(--oc-eyebrow-weight); letter-spacing: var(--oc-eyebrow-tracking); text-transform: uppercase; fill: var(--oc-accent); }`,
|
|
102
|
-
`.oc-title { font-size: var(--oc-title-size); font-weight: var(--oc-title-weight); letter-spacing: var(--oc-title-tracking); fill: var(--oc-text); }`,
|
|
103
|
-
`.oc-subtitle { font-size: var(--oc-subtitle-size); font-weight: var(--oc-subtitle-weight); fill: var(--oc-text-muted); }`,
|
|
104
|
-
`.oc-source, .oc-byline, .oc-footer { font-size: var(--oc-source-size); font-weight: var(--oc-source-weight); fill: var(--oc-text-muted); }`,
|
|
105
|
-
`.oc-brand { font-size: 11px; font-weight: 510; letter-spacing: 0.02em; fill: var(--oc-text-faint); }`,
|
|
106
|
-
`.oc-brand-dot { fill: var(--oc-accent); }`,
|
|
107
|
-
`.oc-eyebrow-dot { fill: var(--oc-accent); }`,
|
|
108
|
-
`.oc-metrics { font-family: var(--oc-font-family); }`,
|
|
109
|
-
`.oc-metric-label { font-size: 10px; font-weight: 510; letter-spacing: 0.08em; text-transform: uppercase; fill: var(--oc-text-muted); }`,
|
|
110
|
-
`.oc-metric-value { font-size: 22px; font-weight: 510; letter-spacing: -0.01em; fill: var(--oc-text); font-variant-numeric: tabular-nums; }`,
|
|
111
|
-
`.oc-metric-delta-up { fill: var(--oc-positive); font-size: 12px; font-weight: 510; }`,
|
|
112
|
-
`.oc-metric-delta-down { fill: var(--oc-negative); font-size: 12px; font-weight: 510; }`,
|
|
113
|
-
`.oc-axis-tick-inline { font-size: 11px; font-weight: 400; fill: var(--oc-text-muted); }`,
|
|
114
|
-
`.oc-endpoint-labels { font-family: var(--oc-font-family); }`,
|
|
115
|
-
`.oc-endpoint-label { fill: var(--oc-endpoint-label-color, var(--oc-text)); }`,
|
|
116
|
-
`.oc-endpoint-value { fill: var(--oc-endpoint-value-color, var(--oc-text-muted)); }`,
|
|
117
|
-
`.oc-endpoint-leader { stroke: var(--oc-endpoint-leader-color, currentColor); }`,
|
|
118
|
-
`.oc-annotation-subtitle { fill: var(--oc-annotation-subtitle-color, var(--oc-text-muted)); }`,
|
|
119
|
-
`.oc-metric-secondary { fill: var(--oc-positive); font-size: 12px; font-weight: 400; }`,
|
|
120
|
-
`.oc-legend { font-family: var(--oc-font-family); font-size: var(--oc-body-size); }`,
|
|
121
|
-
`.oc-legend-entry { cursor: default; }`,
|
|
122
|
-
`.oc-legend text { fill: var(--oc-legend-text); }`,
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
return rules.join('\n');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function stripInteractiveElements(svg: Element): void {
|
|
129
|
-
const selectors = ['[data-voronoi-overlay]', '[data-crosshair]', '[data-snap-dots]'];
|
|
130
|
-
for (const selector of selectors) {
|
|
131
|
-
const els = svg.querySelectorAll(selector);
|
|
132
|
-
for (const el of els) {
|
|
133
|
-
el.parentNode?.removeChild(el);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Render a chart spec to a standalone SVG string without a browser DOM.
|
|
140
|
-
*
|
|
141
|
-
* Requires `happy-dom` as a peer dependency (`bun add happy-dom`).
|
|
142
|
-
*
|
|
143
|
-
* Not safe for concurrent invocation: the render pipeline relies on a global
|
|
144
|
-
* SVG ID counter and a temporary global `document` swap, both of which are
|
|
145
|
-
* single-threaded. In a server handling parallel requests, serialize calls
|
|
146
|
-
* through a queue or mutex.
|
|
147
|
-
*
|
|
148
|
-
* The entire render pipeline is synchronous; the global swap is safe as long
|
|
149
|
-
* as no code schedules microtasks that outlive the call.
|
|
150
|
-
*/
|
|
151
|
-
export function renderStaticSVG(
|
|
152
|
-
spec: ChartSpec | LayerSpec | TileMapSpec,
|
|
153
|
-
options?: StaticRenderOptions,
|
|
154
|
-
): string {
|
|
155
|
-
if (rendering) {
|
|
156
|
-
throw new Error('renderStaticSVG is not reentrant — serialize calls through a queue or mutex');
|
|
157
|
-
}
|
|
158
|
-
rendering = true;
|
|
159
|
-
|
|
160
|
-
const Window = getHappyDomWindow();
|
|
161
|
-
const win = new Window({ url: 'about:blank' });
|
|
162
|
-
|
|
163
|
-
const prevDocument = globalThis.document;
|
|
164
|
-
const prevWindow = (globalThis as Record<string, unknown>).window;
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
(globalThis as Record<string, unknown>).document = win.document;
|
|
168
|
-
(globalThis as Record<string, unknown>).window = win;
|
|
169
|
-
|
|
170
|
-
resetSvgIdCounter();
|
|
171
|
-
|
|
172
|
-
const width = options?.width ?? 640;
|
|
173
|
-
const height = options?.height ?? 420;
|
|
174
|
-
const darkMode = resolveStaticDarkMode(options?.darkMode);
|
|
175
|
-
|
|
176
|
-
const compileOpts: CompileOptions = {
|
|
177
|
-
width,
|
|
178
|
-
height,
|
|
179
|
-
theme: options?.theme,
|
|
180
|
-
darkMode,
|
|
181
|
-
watermark: options?.watermark,
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
let svg: SVGElement;
|
|
185
|
-
let themeForStyle: ResolvedTheme;
|
|
186
|
-
|
|
187
|
-
if (isTileMapSpec(spec)) {
|
|
188
|
-
const tileMapLayout = compileTileMap(spec, compileOpts);
|
|
189
|
-
svg = renderTileMapSVG(tileMapLayout, { animate: false });
|
|
190
|
-
themeForStyle = tileMapLayout.theme;
|
|
191
|
-
} else {
|
|
192
|
-
let layout: ChartLayout;
|
|
193
|
-
if (isLayerSpec(spec)) {
|
|
194
|
-
layout = compileLayer(spec, compileOpts);
|
|
195
|
-
} else {
|
|
196
|
-
layout = compileChart(spec, compileOpts);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const container = win.document.createElement('div');
|
|
200
|
-
Object.defineProperty(container, 'getBoundingClientRect', {
|
|
201
|
-
value: () => ({
|
|
202
|
-
width,
|
|
203
|
-
height,
|
|
204
|
-
top: 0,
|
|
205
|
-
left: 0,
|
|
206
|
-
right: width,
|
|
207
|
-
bottom: height,
|
|
208
|
-
x: 0,
|
|
209
|
-
y: 0,
|
|
210
|
-
toJSON: () => ({}),
|
|
211
|
-
}),
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
svg = renderChartSVG(layout, container as unknown as HTMLElement, {
|
|
215
|
-
animate: false,
|
|
216
|
-
crosshair: false,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
stripInteractiveElements(svg);
|
|
220
|
-
themeForStyle = layout.theme;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const doc = win.document as unknown as Document;
|
|
224
|
-
let defs = svg.querySelector('defs');
|
|
225
|
-
if (!defs) {
|
|
226
|
-
defs = doc.createElementNS(SVG_NS, 'defs');
|
|
227
|
-
svg.insertBefore(defs as unknown as Node, svg.firstChild);
|
|
228
|
-
}
|
|
229
|
-
const styleEl = doc.createElementNS(SVG_NS, 'style');
|
|
230
|
-
styleEl.textContent = buildThemeStyleBlock(themeForStyle);
|
|
231
|
-
defs.insertBefore(styleEl as unknown as Node, defs.firstChild);
|
|
232
|
-
|
|
233
|
-
const serializer = new (
|
|
234
|
-
win as unknown as { XMLSerializer: typeof XMLSerializer }
|
|
235
|
-
).XMLSerializer();
|
|
236
|
-
return serializer.serializeToString(svg as unknown as Node);
|
|
237
|
-
} finally {
|
|
238
|
-
rendering = false;
|
|
239
|
-
if (prevDocument !== undefined) {
|
|
240
|
-
(globalThis as Record<string, unknown>).document = prevDocument;
|
|
241
|
-
} else {
|
|
242
|
-
delete (globalThis as Record<string, unknown>).document;
|
|
243
|
-
}
|
|
244
|
-
if (prevWindow !== undefined) {
|
|
245
|
-
(globalThis as Record<string, unknown>).window = prevWindow;
|
|
246
|
-
} else {
|
|
247
|
-
delete (globalThis as Record<string, unknown>).window;
|
|
248
|
-
}
|
|
249
|
-
win.close();
|
|
250
|
-
}
|
|
251
|
-
}
|