@opendata-ai/openchart-vanilla 8.8.0 → 8.9.1
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-SXTHPXG6.js → chunk-APMUBW72.js} +174 -65
- package/dist/chunk-APMUBW72.js.map +1 -0
- package/dist/{chunk-QBTRAQAX.js → chunk-CCRFYGHU.js} +2299 -1766
- package/dist/chunk-CCRFYGHU.js.map +1 -0
- package/dist/{chunk-5IMVUHVC.js → chunk-GX6BWMJD.js} +23 -21
- package/dist/chunk-GX6BWMJD.js.map +1 -0
- package/dist/{chunk-DFKLAB67.js → chunk-MGFXAROR.js} +20 -8
- package/dist/chunk-MGFXAROR.js.map +1 -0
- package/dist/{chunk-EZQDCLZN.js → chunk-Q7YEEF2X.js} +3 -2
- package/dist/{chunk-EZQDCLZN.js.map → chunk-Q7YEEF2X.js.map} +1 -1
- package/dist/{chunk-I73B3DJI.js → chunk-R5TZHNTG.js} +23 -5
- package/dist/chunk-R5TZHNTG.js.map +1 -0
- package/dist/{chunk-2OJZBMXM.js → chunk-REH5UQCL.js} +46 -46
- package/dist/{chunk-2OJZBMXM.js.map → chunk-REH5UQCL.js.map} +1 -1
- package/dist/{chunk-NG7CVCI2.js → chunk-UC4IHEUO.js} +16 -1
- package/dist/chunk-UC4IHEUO.js.map +1 -0
- package/dist/export-gif.d.ts +2 -2
- package/dist/export-gif.js +2 -2
- package/dist/graph-3d/index.d.ts +0 -27
- package/dist/graph-3d/index.js +9 -5
- package/dist/graph-3d/index.js.map +1 -1
- package/dist/index.d.ts +27 -5
- package/dist/index.js +635 -214
- package/dist/index.js.map +1 -1
- package/dist/{mount-WiY9BIr9.d.ts → mount-TEjIdF0m.d.ts} +16 -1
- package/dist/static.js +3 -3
- package/dist/story/index.d.ts +2 -2
- package/dist/story/index.js +5 -5
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-5IMVUHVC.js.map +0 -1
- package/dist/chunk-DFKLAB67.js.map +0 -1
- package/dist/chunk-I73B3DJI.js.map +0 -1
- package/dist/chunk-NG7CVCI2.js.map +0 -1
- package/dist/chunk-QBTRAQAX.js.map +0 -1
- package/dist/chunk-SXTHPXG6.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -3,21 +3,22 @@ import {
|
|
|
3
3
|
paletteFromCanvas,
|
|
4
4
|
readCanvasSRGB,
|
|
5
5
|
resolveRepeat
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-MGFXAROR.js";
|
|
7
7
|
import {
|
|
8
8
|
renderTileMapSVG
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-Q7YEEF2X.js";
|
|
10
10
|
import {
|
|
11
11
|
GRAPH_3D_NOT_REGISTERED_ERROR,
|
|
12
12
|
createGraph,
|
|
13
13
|
registerGraphRenderer
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-GX6BWMJD.js";
|
|
15
15
|
import {
|
|
16
16
|
CANVAS_DEFAULT_UPDATE_MAX_MARKS,
|
|
17
17
|
DEFAULT_UPDATE_MAX_MARKS,
|
|
18
18
|
VECTOR_EXPORT_MAX_POINTS,
|
|
19
19
|
applySrOnlyStyles,
|
|
20
20
|
cancelAnimations,
|
|
21
|
+
captureMarkFocus,
|
|
21
22
|
createChart,
|
|
22
23
|
createGeoMap,
|
|
23
24
|
createMeasureText,
|
|
@@ -25,10 +26,12 @@ import {
|
|
|
25
26
|
createYouDrawIt,
|
|
26
27
|
materializeCanvasModeSVG,
|
|
27
28
|
resolveFontFamily,
|
|
29
|
+
restoreMarkFocus,
|
|
28
30
|
scheduleFontReload,
|
|
29
31
|
setupAnimationCleanup,
|
|
30
|
-
setupTableAnimationCleanup
|
|
31
|
-
|
|
32
|
+
setupTableAnimationCleanup,
|
|
33
|
+
wireMarkNavigation
|
|
34
|
+
} from "./chunk-CCRFYGHU.js";
|
|
32
35
|
import {
|
|
33
36
|
embedFonts,
|
|
34
37
|
ensureSVGDimensions,
|
|
@@ -40,30 +43,31 @@ import {
|
|
|
40
43
|
getSVGBackgroundColor,
|
|
41
44
|
getSVGDimensions,
|
|
42
45
|
rasterizeSVGToCanvas
|
|
43
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-R5TZHNTG.js";
|
|
44
47
|
import {
|
|
45
48
|
rectPathWithCorners,
|
|
46
49
|
registerMarkRenderer,
|
|
47
50
|
renderChartSVG,
|
|
51
|
+
renderChromeElement,
|
|
48
52
|
stampAnimationVars
|
|
49
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-APMUBW72.js";
|
|
50
54
|
import {
|
|
55
|
+
applyTextStyle,
|
|
51
56
|
injectThemeStyleBlock
|
|
52
57
|
} from "./chunk-SDJHUELU.js";
|
|
53
58
|
import {
|
|
54
59
|
createTooltipManager,
|
|
55
60
|
observeResize,
|
|
56
61
|
resolveDarkMode
|
|
57
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-REH5UQCL.js";
|
|
58
63
|
import {
|
|
59
64
|
resolvedSurface
|
|
60
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-UC4IHEUO.js";
|
|
61
66
|
|
|
62
67
|
// src/barlist-mount.ts
|
|
63
68
|
import { compileBarList } from "@opendata-ai/openchart-engine";
|
|
64
69
|
|
|
65
70
|
// src/barlist-renderer.ts
|
|
66
|
-
import { textAscent } from "@opendata-ai/openchart-core";
|
|
67
71
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
68
72
|
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
69
73
|
var BRAND_URL = "https://tryopendata.ai";
|
|
@@ -84,18 +88,13 @@ function renderChrome(parent, layout) {
|
|
|
84
88
|
const el = chrome[key];
|
|
85
89
|
if (!el) continue;
|
|
86
90
|
const isBottom = key === "source" || key === "byline" || key === "footer";
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
text.setAttribute("font-size", String(el.style.fontSize));
|
|
95
|
-
text.setAttribute("font-weight", String(el.style.fontWeight));
|
|
96
|
-
text.style.setProperty("fill", el.style.fill);
|
|
97
|
-
text.textContent = el.text;
|
|
98
|
-
g.appendChild(text);
|
|
91
|
+
renderChromeElement(
|
|
92
|
+
g,
|
|
93
|
+
isBottom ? { ...el, y: bottomOffset + el.y } : el,
|
|
94
|
+
`oc-${key}`,
|
|
95
|
+
key,
|
|
96
|
+
layout.measureText
|
|
97
|
+
);
|
|
99
98
|
}
|
|
100
99
|
parent.appendChild(g);
|
|
101
100
|
}
|
|
@@ -233,7 +232,11 @@ function renderBarListSVG(layout, opts) {
|
|
|
233
232
|
const classes = animate ? "oc-barlist oc-animate" : "oc-barlist";
|
|
234
233
|
svg.setAttribute("class", classes);
|
|
235
234
|
if (animate && animation?.enter) {
|
|
236
|
-
stampAnimationVars(svg, {
|
|
235
|
+
stampAnimationVars(svg, {
|
|
236
|
+
duration: animation.enter.duration,
|
|
237
|
+
spring: animation.enter.spring,
|
|
238
|
+
stagger: 40
|
|
239
|
+
});
|
|
237
240
|
}
|
|
238
241
|
renderChrome(svg, layout);
|
|
239
242
|
renderRows(svg, rows, animate ? animation : void 0);
|
|
@@ -256,8 +259,8 @@ function createBarList(container, spec, options) {
|
|
|
256
259
|
let pendingResize = false;
|
|
257
260
|
let fontsReloadPending = false;
|
|
258
261
|
container.classList.add("oc-barlist-root");
|
|
259
|
-
function resolveEffectiveFont() {
|
|
260
|
-
return options?.theme?.fonts?.family ??
|
|
262
|
+
function resolveEffectiveFont(candidate = currentSpec) {
|
|
263
|
+
return options?.theme?.fonts?.family ?? candidate.theme?.fonts?.family ?? resolveFontFamily(container);
|
|
261
264
|
}
|
|
262
265
|
let fontFamily = resolveEffectiveFont();
|
|
263
266
|
let measureText = createMeasureText(fontFamily);
|
|
@@ -269,7 +272,7 @@ function createBarList(container, spec, options) {
|
|
|
269
272
|
height: Math.max(rect.height || 400, 100)
|
|
270
273
|
};
|
|
271
274
|
}
|
|
272
|
-
function compile() {
|
|
275
|
+
function compile(candidate = currentSpec, measure = measureText) {
|
|
273
276
|
const { width, height } = getContainerDimensions();
|
|
274
277
|
const darkMode = resolveDarkMode(options?.darkMode);
|
|
275
278
|
const compileOpts = {
|
|
@@ -278,9 +281,9 @@ function createBarList(container, spec, options) {
|
|
|
278
281
|
theme: options?.theme,
|
|
279
282
|
darkMode,
|
|
280
283
|
watermark: options?.watermark,
|
|
281
|
-
measureText
|
|
284
|
+
measureText: measure
|
|
282
285
|
};
|
|
283
|
-
const layout = compileBarList(
|
|
286
|
+
const layout = compileBarList(candidate, compileOpts);
|
|
284
287
|
if (layout.rows.length > 0) {
|
|
285
288
|
const lastRow = layout.rows[layout.rows.length - 1];
|
|
286
289
|
const contentBottom = lastRow.y + lastRow.height + layout.chrome.bottomHeight + layout.theme.spacing.padding;
|
|
@@ -293,6 +296,8 @@ function createBarList(container, spec, options) {
|
|
|
293
296
|
}
|
|
294
297
|
function wireTooltipAndInteraction(svg, layout) {
|
|
295
298
|
const cleanups = [];
|
|
299
|
+
const navigation = [];
|
|
300
|
+
let activeLeave = null;
|
|
296
301
|
const rowElements = svg.querySelectorAll(".oc-barlist-row");
|
|
297
302
|
for (const el of rowElements) {
|
|
298
303
|
const indexStr = el.getAttribute("data-row-index");
|
|
@@ -300,6 +305,8 @@ function createBarList(container, spec, options) {
|
|
|
300
305
|
const content = layout.tooltipDescriptors.get(indexStr);
|
|
301
306
|
const row = layout.rows[Number(indexStr)];
|
|
302
307
|
const handleMouseEnter = (e) => {
|
|
308
|
+
activeLeave?.();
|
|
309
|
+
activeLeave = handleMouseLeave;
|
|
303
310
|
const mouseEvent = e;
|
|
304
311
|
if (content && tooltipManager && options?.tooltip !== false) {
|
|
305
312
|
const svgRect = svg.getBoundingClientRect();
|
|
@@ -325,6 +332,8 @@ function createBarList(container, spec, options) {
|
|
|
325
332
|
}
|
|
326
333
|
};
|
|
327
334
|
const handleMouseLeave = () => {
|
|
335
|
+
if (activeLeave !== handleMouseLeave) return;
|
|
336
|
+
activeLeave = null;
|
|
328
337
|
tooltipManager?.hide();
|
|
329
338
|
options?.onRowHover?.(null);
|
|
330
339
|
};
|
|
@@ -337,6 +346,13 @@ function createBarList(container, spec, options) {
|
|
|
337
346
|
});
|
|
338
347
|
}
|
|
339
348
|
};
|
|
349
|
+
navigation.push({
|
|
350
|
+
element: el,
|
|
351
|
+
key: row?.label.text ?? indexStr,
|
|
352
|
+
enter: handleMouseEnter,
|
|
353
|
+
leave: handleMouseLeave,
|
|
354
|
+
activate: handleClick
|
|
355
|
+
});
|
|
340
356
|
el.addEventListener("mouseenter", handleMouseEnter);
|
|
341
357
|
el.addEventListener("mousemove", handleMouseMove);
|
|
342
358
|
el.addEventListener("mouseleave", handleMouseLeave);
|
|
@@ -348,11 +364,15 @@ function createBarList(container, spec, options) {
|
|
|
348
364
|
el.removeEventListener("click", handleClick);
|
|
349
365
|
});
|
|
350
366
|
}
|
|
367
|
+
cleanups.push(wireMarkNavigation(svg, navigation));
|
|
351
368
|
return () => {
|
|
369
|
+
activeLeave?.();
|
|
352
370
|
for (const cleanup of cleanups) cleanup();
|
|
353
371
|
};
|
|
354
372
|
}
|
|
355
373
|
function render(animate = false) {
|
|
374
|
+
const focusedMark = captureMarkFocus(svgElement);
|
|
375
|
+
tooltipManager?.hide();
|
|
356
376
|
if (animationCleanup) {
|
|
357
377
|
animationCleanup();
|
|
358
378
|
animationCleanup = null;
|
|
@@ -373,7 +393,7 @@ function createBarList(container, spec, options) {
|
|
|
373
393
|
tooltipManager = createTooltipManager(container);
|
|
374
394
|
}
|
|
375
395
|
}
|
|
376
|
-
if (currentLayout.animation?.enter) {
|
|
396
|
+
if (animate && currentLayout.animation?.enter) {
|
|
377
397
|
animationCleanup = setupAnimationCleanup(newSvg, () => {
|
|
378
398
|
animationCleanup = null;
|
|
379
399
|
if (pendingResize && !destroyed) {
|
|
@@ -388,15 +408,17 @@ function createBarList(container, spec, options) {
|
|
|
388
408
|
fontsReloadPending = false;
|
|
389
409
|
container.dataset.ocFontsState = "ready";
|
|
390
410
|
}
|
|
411
|
+
if (svgElement) restoreMarkFocus(svgElement, focusedMark);
|
|
391
412
|
}
|
|
392
413
|
function update(newSpec) {
|
|
414
|
+
if (destroyed) return;
|
|
415
|
+
const nextFont = resolveEffectiveFont(newSpec);
|
|
416
|
+
const nextMeasure = nextFont === fontFamily ? measureText : createMeasureText(nextFont);
|
|
417
|
+
const nextLayout = compile(newSpec, nextMeasure);
|
|
393
418
|
currentSpec = newSpec;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
measureText = createMeasureText(fontFamily);
|
|
398
|
-
}
|
|
399
|
-
currentLayout = compile();
|
|
419
|
+
fontFamily = nextFont;
|
|
420
|
+
measureText = nextMeasure;
|
|
421
|
+
currentLayout = nextLayout;
|
|
400
422
|
render();
|
|
401
423
|
}
|
|
402
424
|
function resize() {
|
|
@@ -410,15 +432,17 @@ function createBarList(container, spec, options) {
|
|
|
410
432
|
}
|
|
411
433
|
function exportChart(format, options_) {
|
|
412
434
|
if (!svgElement) return "";
|
|
435
|
+
const terminalSvg = renderBarListSVG(currentLayout, { animate: false });
|
|
436
|
+
const resolvedOptions = { theme: currentLayout.theme, ...options_ };
|
|
413
437
|
switch (format) {
|
|
414
438
|
case "svg":
|
|
415
|
-
return exportSVG(
|
|
439
|
+
return exportSVG(terminalSvg);
|
|
416
440
|
case "svg-with-fonts":
|
|
417
|
-
return exportSVGWithFonts(
|
|
441
|
+
return exportSVGWithFonts(terminalSvg, resolvedOptions);
|
|
418
442
|
case "png":
|
|
419
|
-
return exportPNG(
|
|
443
|
+
return exportPNG(terminalSvg, resolvedOptions);
|
|
420
444
|
case "jpg":
|
|
421
|
-
return exportJPG(
|
|
445
|
+
return exportJPG(terminalSvg, resolvedOptions);
|
|
422
446
|
default:
|
|
423
447
|
return "";
|
|
424
448
|
}
|
|
@@ -611,6 +635,285 @@ function createSimulationWorker() {
|
|
|
611
635
|
return new Worker(new URL("./simulation-worker.js", import.meta.url), { type: "module" });
|
|
612
636
|
}
|
|
613
637
|
|
|
638
|
+
// src/reader.ts
|
|
639
|
+
import {
|
|
640
|
+
observationKey,
|
|
641
|
+
reconcileReaderSelection,
|
|
642
|
+
resolveObservationIdentities
|
|
643
|
+
} from "@opendata-ai/openchart-engine";
|
|
644
|
+
var LAYER_UNSUPPORTED = "Reader inspection is unavailable for layered charts: observations cannot be bound to a single dataset.";
|
|
645
|
+
var seriesField = (spec) => spec.encoding.color && "field" in spec.encoding.color ? spec.encoding.color.field : void 0;
|
|
646
|
+
function createReader(container, instance, initial, onChange) {
|
|
647
|
+
let spec = "layer" in initial ? null : initial;
|
|
648
|
+
let state = {
|
|
649
|
+
preview: null,
|
|
650
|
+
pinned: [],
|
|
651
|
+
announcement: spec ? "" : LAYER_UNSUPPORTED
|
|
652
|
+
};
|
|
653
|
+
let keyboardIndex = -1;
|
|
654
|
+
let settleFrame = null;
|
|
655
|
+
let visibilityOverride;
|
|
656
|
+
let identities = spec ? resolveObservationIdentities(spec.data, spec.encoding.key?.field, seriesField(spec)) : [];
|
|
657
|
+
const emit = () => onChange({ ...state, pinned: [...state.pinned] });
|
|
658
|
+
const visibleData = (layout = instance.sampleLayout()) => {
|
|
659
|
+
const rows = layout.marks.flatMap((mark) => {
|
|
660
|
+
if ("opacity" in mark && mark.opacity === 0) return [];
|
|
661
|
+
if (mark.type === "line" || mark.type === "area")
|
|
662
|
+
return (mark.dataPoints ?? []).map((point, index) => ({
|
|
663
|
+
datum: point.datum,
|
|
664
|
+
key: mark.pointKeys?.[index]
|
|
665
|
+
}));
|
|
666
|
+
if ("data" in mark) return [{ datum: mark.data, key: mark.key }];
|
|
667
|
+
return [];
|
|
668
|
+
});
|
|
669
|
+
return [...new Map(rows.map((row) => [row.datum, row])).values()];
|
|
670
|
+
};
|
|
671
|
+
const compiledVisibleKeys = (observations) => {
|
|
672
|
+
const keys = /* @__PURE__ */ new Set();
|
|
673
|
+
if (!spec || !observations.length) return keys;
|
|
674
|
+
const rows = visibleData(instance.layout);
|
|
675
|
+
const references = new Set(rows.map(({ datum }) => datum));
|
|
676
|
+
const signatures = new Set(rows.map(({ datum }) => JSON.stringify(datum)));
|
|
677
|
+
const keyField = spec.encoding.key?.field;
|
|
678
|
+
const visibleSourceKeys = new Set(
|
|
679
|
+
keyField ? rows.map(({ datum }) => observationKey(datum[keyField])) : []
|
|
680
|
+
);
|
|
681
|
+
for (const observation of observations) {
|
|
682
|
+
if (observation.kind === "aggregate") {
|
|
683
|
+
if (signatures.has(JSON.stringify(observation.datum))) keys.add(observation.observation);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const index = identities.findIndex((id) => id.observation === observation.observation);
|
|
687
|
+
if (index < 0) continue;
|
|
688
|
+
const row = spec.data[index];
|
|
689
|
+
const signature = JSON.stringify(row);
|
|
690
|
+
const visible = observation.persistent ? visibleSourceKeys.has(observation.observation) : references.has(row) || signatures.has(signature) && spec.data.filter((source) => JSON.stringify(source) === signature).length === 1;
|
|
691
|
+
if (visible) keys.add(observation.observation);
|
|
692
|
+
}
|
|
693
|
+
return keys;
|
|
694
|
+
};
|
|
695
|
+
const resolve = (datum) => {
|
|
696
|
+
if (!spec) return null;
|
|
697
|
+
const keyField = spec.encoding.key?.field;
|
|
698
|
+
let index = spec.data.indexOf(datum);
|
|
699
|
+
if (index >= 0 && spec.data.lastIndexOf(datum) !== index) return null;
|
|
700
|
+
if (index < 0 && keyField)
|
|
701
|
+
index = spec.data.findIndex(
|
|
702
|
+
(_row, i) => identities[i].persistent && identities[i].observation === observationKey(datum[keyField])
|
|
703
|
+
);
|
|
704
|
+
if (index < 0) {
|
|
705
|
+
const matches = spec.data.flatMap(
|
|
706
|
+
(row, i) => JSON.stringify(row) === JSON.stringify(datum) ? [i] : []
|
|
707
|
+
);
|
|
708
|
+
if (matches.length > 1) return null;
|
|
709
|
+
if (matches.length === 1) index = matches[0];
|
|
710
|
+
}
|
|
711
|
+
if (index >= 0) return { ...identities[index], datum: spec.data[index], state: "visible" };
|
|
712
|
+
const group = visibleData(instance.layout).find(
|
|
713
|
+
(row) => row.datum === datum || JSON.stringify(row.datum) === JSON.stringify(datum)
|
|
714
|
+
);
|
|
715
|
+
return group ? {
|
|
716
|
+
observation: `aggregate:${JSON.stringify([spec.mark, spec.encoding, spec.transform])}:${group.key ?? JSON.stringify(datum)}`,
|
|
717
|
+
persistent: false,
|
|
718
|
+
datum: group.datum,
|
|
719
|
+
kind: "aggregate",
|
|
720
|
+
state: "visible"
|
|
721
|
+
} : null;
|
|
722
|
+
};
|
|
723
|
+
const pin = (datum) => {
|
|
724
|
+
const observation = resolve(datum);
|
|
725
|
+
if (!observation) return;
|
|
726
|
+
observation.state = (visibilityOverride ?? compiledVisibleKeys([observation])).has(
|
|
727
|
+
observation.observation
|
|
728
|
+
) ? "visible" : "filtered";
|
|
729
|
+
state = {
|
|
730
|
+
...state,
|
|
731
|
+
preview: observation,
|
|
732
|
+
announcement: "Observation pinned.",
|
|
733
|
+
pinned: [
|
|
734
|
+
...state.pinned.filter((item) => item.observation !== observation.observation),
|
|
735
|
+
observation
|
|
736
|
+
].slice(-2)
|
|
737
|
+
};
|
|
738
|
+
emit();
|
|
739
|
+
};
|
|
740
|
+
const clear = () => {
|
|
741
|
+
state = {
|
|
742
|
+
preview: null,
|
|
743
|
+
pinned: [],
|
|
744
|
+
announcement: spec ? "Selection dismissed." : LAYER_UNSUPPORTED
|
|
745
|
+
};
|
|
746
|
+
emit();
|
|
747
|
+
};
|
|
748
|
+
function hit(event) {
|
|
749
|
+
if (!spec) return null;
|
|
750
|
+
const svg = container.querySelector("svg");
|
|
751
|
+
if (!svg) return null;
|
|
752
|
+
const bounds = svg.getBoundingClientRect();
|
|
753
|
+
if (!bounds.width || !bounds.height) return null;
|
|
754
|
+
const layout = instance.sampleLayout();
|
|
755
|
+
const x = (event.clientX - bounds.left) * layout.dimensions.width / bounds.width;
|
|
756
|
+
const y = (event.clientY - bounds.top) * layout.dimensions.height / bounds.height;
|
|
757
|
+
if (x < layout.area.x || x > layout.area.x + layout.area.width || y < layout.area.y || y > layout.area.y + layout.area.height)
|
|
758
|
+
return null;
|
|
759
|
+
let nearest = null;
|
|
760
|
+
let distance = 32;
|
|
761
|
+
const consider = (datum, dx, dy) => {
|
|
762
|
+
const d = Math.hypot(dx, dy);
|
|
763
|
+
if (d <= distance) {
|
|
764
|
+
distance = d;
|
|
765
|
+
nearest = datum;
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
for (const mark of layout.marks) {
|
|
769
|
+
if ("opacity" in mark && mark.opacity === 0) continue;
|
|
770
|
+
if (mark.type === "point") consider(mark.data, x - mark.cx, y - mark.cy);
|
|
771
|
+
else if (mark.type === "rect")
|
|
772
|
+
consider(
|
|
773
|
+
mark.data,
|
|
774
|
+
Math.max(mark.x - x, 0, x - mark.x - mark.width),
|
|
775
|
+
Math.max(mark.y - y, 0, y - mark.y - mark.height)
|
|
776
|
+
);
|
|
777
|
+
else if (mark.type === "arc") {
|
|
778
|
+
const dx = x - mark.center.x;
|
|
779
|
+
const dy = y - mark.center.y;
|
|
780
|
+
const radius = Math.hypot(dx, dy);
|
|
781
|
+
const angle = (Math.atan2(dy, dx) + Math.PI / 2 + Math.PI * 2) % (Math.PI * 2);
|
|
782
|
+
const start = (mark.startAngle + Math.PI * 2) % (Math.PI * 2);
|
|
783
|
+
const sweep = mark.endAngle - mark.startAngle;
|
|
784
|
+
if (radius >= mark.innerRadius && radius <= mark.outerRadius && (angle - start + Math.PI * 2) % (Math.PI * 2) <= sweep)
|
|
785
|
+
consider(mark.data, 0, 0);
|
|
786
|
+
} else if (mark.type === "line" || mark.type === "area") {
|
|
787
|
+
for (const point of mark.dataPoints ?? []) consider(point.datum, x - point.x, y - point.y);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return nearest;
|
|
791
|
+
}
|
|
792
|
+
const preview = (event) => {
|
|
793
|
+
if (event.pointerType === "touch") return;
|
|
794
|
+
const datum = hit(event);
|
|
795
|
+
state = { ...state, preview: datum ? resolve(datum) : null };
|
|
796
|
+
emit();
|
|
797
|
+
};
|
|
798
|
+
const click = (event) => {
|
|
799
|
+
const datum = hit(event);
|
|
800
|
+
if (datum) pin(datum);
|
|
801
|
+
};
|
|
802
|
+
const leave = () => {
|
|
803
|
+
state = { ...state, preview: null };
|
|
804
|
+
emit();
|
|
805
|
+
};
|
|
806
|
+
const keydown = (event) => {
|
|
807
|
+
if (!spec) return;
|
|
808
|
+
if (event.target.closest(
|
|
809
|
+
"input, textarea, select, button, [contenteditable=true]"
|
|
810
|
+
))
|
|
811
|
+
return;
|
|
812
|
+
if (event.key === "Escape") {
|
|
813
|
+
clear();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (event.key === "Enter" && state.preview) {
|
|
817
|
+
event.preventDefault();
|
|
818
|
+
pin(state.preview.datum);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const rows = visibleData();
|
|
822
|
+
if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key) || !rows.length)
|
|
823
|
+
return;
|
|
824
|
+
event.preventDefault();
|
|
825
|
+
event.stopPropagation();
|
|
826
|
+
const backwards = event.key === "ArrowLeft" || event.key === "ArrowUp";
|
|
827
|
+
keyboardIndex = keyboardIndex < 0 ? backwards ? rows.length - 1 : 0 : (keyboardIndex + (backwards ? -1 : 1) + rows.length) % rows.length;
|
|
828
|
+
state = { ...state, preview: resolve(rows[keyboardIndex].datum) };
|
|
829
|
+
emit();
|
|
830
|
+
};
|
|
831
|
+
container.addEventListener("pointermove", preview);
|
|
832
|
+
container.addEventListener("pointerleave", leave);
|
|
833
|
+
container.addEventListener("click", click);
|
|
834
|
+
container.addEventListener("keydown", keydown, true);
|
|
835
|
+
if (!spec) emit();
|
|
836
|
+
return {
|
|
837
|
+
get selection() {
|
|
838
|
+
return state;
|
|
839
|
+
},
|
|
840
|
+
pin,
|
|
841
|
+
clear,
|
|
842
|
+
update(next, visibleKeys) {
|
|
843
|
+
if (settleFrame !== null) {
|
|
844
|
+
cancelAnimationFrame(settleFrame);
|
|
845
|
+
settleFrame = null;
|
|
846
|
+
}
|
|
847
|
+
visibilityOverride = visibleKeys;
|
|
848
|
+
if ("layer" in next) {
|
|
849
|
+
spec = null;
|
|
850
|
+
identities = [];
|
|
851
|
+
keyboardIndex = -1;
|
|
852
|
+
state = { preview: null, pinned: [], announcement: LAYER_UNSUPPORTED };
|
|
853
|
+
emit();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const meaningChanged = !spec || JSON.stringify([next.mark, next.encoding, next.transform]) !== JSON.stringify([spec.mark, spec.encoding, spec.transform]);
|
|
857
|
+
if (meaningChanged && (state.pinned.some((item) => item.kind === "aggregate") || state.preview?.kind === "aggregate")) {
|
|
858
|
+
state = {
|
|
859
|
+
...state,
|
|
860
|
+
preview: null,
|
|
861
|
+
pinned: state.pinned.filter((item) => item.kind !== "aggregate"),
|
|
862
|
+
announcement: "Aggregate selection cleared because its grouping or transformation changed."
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
const replaced = !spec || next.data !== spec.data || next.encoding.key?.field !== spec.encoding.key?.field;
|
|
866
|
+
const resuming = !spec;
|
|
867
|
+
spec = next;
|
|
868
|
+
identities = resolveObservationIdentities(
|
|
869
|
+
spec.data,
|
|
870
|
+
spec.encoding.key?.field,
|
|
871
|
+
seriesField(spec)
|
|
872
|
+
);
|
|
873
|
+
const effectiveVisibility = visibleKeys ?? compiledVisibleKeys(state.pinned);
|
|
874
|
+
if (replaced)
|
|
875
|
+
state = reconcileReaderSelection(
|
|
876
|
+
state,
|
|
877
|
+
spec.data,
|
|
878
|
+
spec.encoding.key?.field,
|
|
879
|
+
seriesField(spec),
|
|
880
|
+
effectiveVisibility
|
|
881
|
+
);
|
|
882
|
+
else
|
|
883
|
+
state = {
|
|
884
|
+
...state,
|
|
885
|
+
preview: null,
|
|
886
|
+
pinned: state.pinned.map((item) => ({
|
|
887
|
+
...item,
|
|
888
|
+
...item.kind !== "aggregate" ? identities.find((id) => id.observation === item.observation) : {},
|
|
889
|
+
state: effectiveVisibility.has(item.observation) ? "visible" : "filtered"
|
|
890
|
+
}))
|
|
891
|
+
};
|
|
892
|
+
if (resuming) state = { ...state, announcement: "" };
|
|
893
|
+
if (replaced || meaningChanged) keyboardIndex = -1;
|
|
894
|
+
emit();
|
|
895
|
+
const settle = () => {
|
|
896
|
+
settleFrame = null;
|
|
897
|
+
if (!state.pinned.some((item) => item.previous)) return;
|
|
898
|
+
if (instance.sampleLayout() !== instance.layout) {
|
|
899
|
+
settleFrame = requestAnimationFrame(settle);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
state = { ...state, pinned: state.pinned.map(({ previous: _previous, ...item }) => item) };
|
|
903
|
+
emit();
|
|
904
|
+
};
|
|
905
|
+
settleFrame = requestAnimationFrame(settle);
|
|
906
|
+
},
|
|
907
|
+
destroy() {
|
|
908
|
+
if (settleFrame !== null) cancelAnimationFrame(settleFrame);
|
|
909
|
+
container.removeEventListener("pointermove", preview);
|
|
910
|
+
container.removeEventListener("pointerleave", leave);
|
|
911
|
+
container.removeEventListener("click", click);
|
|
912
|
+
container.removeEventListener("keydown", keydown, true);
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
614
917
|
// src/renderers/table-cells.ts
|
|
615
918
|
function applyCellStyle(td, cell) {
|
|
616
919
|
if (cell.style.backgroundColor) {
|
|
@@ -972,7 +1275,6 @@ import {
|
|
|
972
1275
|
BRAND_FONT_SIZE,
|
|
973
1276
|
BRAND_MIN_WIDTH,
|
|
974
1277
|
estimateTextWidth,
|
|
975
|
-
textAscent as textAscent2,
|
|
976
1278
|
wrapText
|
|
977
1279
|
} from "@opendata-ai/openchart-core";
|
|
978
1280
|
import { clampStaggerDelay } from "@opendata-ai/openchart-engine";
|
|
@@ -987,55 +1289,12 @@ function setAttrs2(el, attrs) {
|
|
|
987
1289
|
el.setAttribute(key, String(value));
|
|
988
1290
|
}
|
|
989
1291
|
}
|
|
990
|
-
function applyTextStyle(el, style) {
|
|
991
|
-
setAttrs2(el, {
|
|
992
|
-
"font-family": style.fontFamily,
|
|
993
|
-
"font-size": style.fontSize,
|
|
994
|
-
"font-weight": style.fontWeight
|
|
995
|
-
});
|
|
996
|
-
el.style.setProperty("fill", style.fill);
|
|
997
|
-
if (style.textAnchor) {
|
|
998
|
-
el.setAttribute("text-anchor", style.textAnchor);
|
|
999
|
-
}
|
|
1000
|
-
if (style.dominantBaseline) {
|
|
1001
|
-
el.setAttribute("dominant-baseline", style.dominantBaseline);
|
|
1002
|
-
}
|
|
1003
|
-
if (style.fontVariant) {
|
|
1004
|
-
el.setAttribute("font-variant", style.fontVariant);
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
1292
|
function stampAnimationAttrs(el, mark, fallbackIndex, animation) {
|
|
1008
1293
|
if (!animation?.enter) return;
|
|
1009
1294
|
const idx = mark.animationIndex ?? fallbackIndex;
|
|
1010
1295
|
el.setAttribute("data-animation-index", String(idx));
|
|
1011
1296
|
el.style.setProperty("--oc-mark-index", String(idx));
|
|
1012
1297
|
}
|
|
1013
|
-
function renderChromeElement(parent, element, className, chromeKey, measureText) {
|
|
1014
|
-
const text = createSVGElement2("text");
|
|
1015
|
-
setAttrs2(text, { x: element.x, y: element.y + textAscent2(element.style.fontSize) });
|
|
1016
|
-
applyTextStyle(text, element.style);
|
|
1017
|
-
text.setAttribute("class", className);
|
|
1018
|
-
text.setAttribute("data-chrome-key", chromeKey);
|
|
1019
|
-
const lines = wrapText(
|
|
1020
|
-
element.text,
|
|
1021
|
-
element.style.fontSize,
|
|
1022
|
-
element.style.fontWeight,
|
|
1023
|
-
element.maxWidth,
|
|
1024
|
-
measureText
|
|
1025
|
-
);
|
|
1026
|
-
if (lines.length === 1) {
|
|
1027
|
-
text.textContent = element.text;
|
|
1028
|
-
} else {
|
|
1029
|
-
const lineHeight = element.style.fontSize * (element.style.lineHeight ?? 1.3);
|
|
1030
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1031
|
-
const tspan = createSVGElement2("tspan");
|
|
1032
|
-
setAttrs2(tspan, { x: element.x, dy: i === 0 ? 0 : lineHeight });
|
|
1033
|
-
tspan.textContent = lines[i];
|
|
1034
|
-
text.appendChild(tspan);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
parent.appendChild(text);
|
|
1038
|
-
}
|
|
1039
1298
|
function renderChrome2(parent, layout) {
|
|
1040
1299
|
const g = createSVGElement2("g");
|
|
1041
1300
|
g.setAttribute("class", "oc-chrome");
|
|
@@ -1361,6 +1620,7 @@ function renderSankeySVG(layout, animation) {
|
|
|
1361
1620
|
const stagger = clampStaggerDelay(enterPhase.staggerDelay, totalMarks);
|
|
1362
1621
|
stampAnimationVars(svg, {
|
|
1363
1622
|
duration: enterPhase.duration,
|
|
1623
|
+
spring: enterPhase.spring,
|
|
1364
1624
|
stagger,
|
|
1365
1625
|
annotationDelay: animation.annotationDelay,
|
|
1366
1626
|
ease: enterPhase.ease
|
|
@@ -1408,8 +1668,8 @@ function createSankey(container, spec, options) {
|
|
|
1408
1668
|
let pendingResize = false;
|
|
1409
1669
|
let fontsReloadPending = false;
|
|
1410
1670
|
container.classList.add("oc-sankey-root");
|
|
1411
|
-
function resolveEffectiveFont() {
|
|
1412
|
-
return options?.theme?.fonts?.family ??
|
|
1671
|
+
function resolveEffectiveFont(candidate = currentSpec) {
|
|
1672
|
+
return options?.theme?.fonts?.family ?? candidate.theme?.fonts?.family ?? resolveFontFamily(container);
|
|
1413
1673
|
}
|
|
1414
1674
|
let fontFamily = resolveEffectiveFont();
|
|
1415
1675
|
let measureText = createMeasureText(fontFamily);
|
|
@@ -1421,7 +1681,7 @@ function createSankey(container, spec, options) {
|
|
|
1421
1681
|
height: Math.max(rect.height || 400, 100)
|
|
1422
1682
|
};
|
|
1423
1683
|
}
|
|
1424
|
-
function compile() {
|
|
1684
|
+
function compile(candidate = currentSpec, measure = measureText) {
|
|
1425
1685
|
const { width, height } = getContainerDimensions();
|
|
1426
1686
|
const darkMode = resolveDarkMode(options?.darkMode);
|
|
1427
1687
|
const compileOpts = {
|
|
@@ -1430,12 +1690,14 @@ function createSankey(container, spec, options) {
|
|
|
1430
1690
|
theme: options?.theme,
|
|
1431
1691
|
darkMode,
|
|
1432
1692
|
watermark: options?.watermark,
|
|
1433
|
-
measureText
|
|
1693
|
+
measureText: measure
|
|
1434
1694
|
};
|
|
1435
|
-
return compileSankey(
|
|
1695
|
+
return compileSankey(candidate, compileOpts);
|
|
1436
1696
|
}
|
|
1437
1697
|
function wireTooltipAndInteraction(svg, layout) {
|
|
1438
1698
|
const cleanups = [];
|
|
1699
|
+
const navigation = [];
|
|
1700
|
+
let activeLeave = null;
|
|
1439
1701
|
const nodesById = /* @__PURE__ */ new Map();
|
|
1440
1702
|
for (const n of layout.nodes) {
|
|
1441
1703
|
if (!nodesById.has(n.nodeId)) nodesById.set(n.nodeId, n);
|
|
@@ -1448,6 +1710,8 @@ function createSankey(container, spec, options) {
|
|
|
1448
1710
|
const nodeId = el.getAttribute("data-node-id");
|
|
1449
1711
|
const nodeData = nodeId ? nodesById.get(nodeId)?.data ?? {} : {};
|
|
1450
1712
|
const handleMouseEnter = (e) => {
|
|
1713
|
+
activeLeave?.();
|
|
1714
|
+
activeLeave = handleMouseLeave;
|
|
1451
1715
|
const mouseEvent = e;
|
|
1452
1716
|
if (content && tooltipManager) {
|
|
1453
1717
|
const svgRect = svg.getBoundingClientRect();
|
|
@@ -1468,6 +1732,8 @@ function createSankey(container, spec, options) {
|
|
|
1468
1732
|
}
|
|
1469
1733
|
};
|
|
1470
1734
|
const handleMouseLeave = () => {
|
|
1735
|
+
if (activeLeave !== handleMouseLeave) return;
|
|
1736
|
+
activeLeave = null;
|
|
1471
1737
|
tooltipManager?.hide();
|
|
1472
1738
|
options?.onNodeHover?.(null);
|
|
1473
1739
|
resetLinkOpacity(svg, layout);
|
|
@@ -1475,6 +1741,13 @@ function createSankey(container, spec, options) {
|
|
|
1475
1741
|
const handleClick = () => {
|
|
1476
1742
|
options?.onNodeClick?.(nodeData);
|
|
1477
1743
|
};
|
|
1744
|
+
navigation.push({
|
|
1745
|
+
element: el,
|
|
1746
|
+
key: nodeId ? `node:${nodeId}` : markId,
|
|
1747
|
+
enter: handleMouseEnter,
|
|
1748
|
+
leave: handleMouseLeave,
|
|
1749
|
+
activate: handleClick
|
|
1750
|
+
});
|
|
1478
1751
|
el.addEventListener("mouseenter", handleMouseEnter);
|
|
1479
1752
|
el.addEventListener("mousemove", handleMouseMove);
|
|
1480
1753
|
el.addEventListener("mouseleave", handleMouseLeave);
|
|
@@ -1487,14 +1760,16 @@ function createSankey(container, spec, options) {
|
|
|
1487
1760
|
});
|
|
1488
1761
|
}
|
|
1489
1762
|
const linkElements = svg.querySelectorAll(".oc-sankey-link");
|
|
1490
|
-
for (const el of linkElements) {
|
|
1763
|
+
for (const [index, el] of linkElements.entries()) {
|
|
1491
1764
|
const markId = el.getAttribute("data-mark-id");
|
|
1492
1765
|
if (!markId) continue;
|
|
1493
1766
|
const content = layout.tooltipDescriptors.get(markId);
|
|
1494
1767
|
const sourceId = el.getAttribute("data-source");
|
|
1495
1768
|
const targetId = el.getAttribute("data-target");
|
|
1496
|
-
const linkData =
|
|
1769
|
+
const linkData = layout.links[index]?.data ?? {};
|
|
1497
1770
|
const handleMouseEnter = (e) => {
|
|
1771
|
+
activeLeave?.();
|
|
1772
|
+
activeLeave = handleMouseLeave;
|
|
1498
1773
|
const mouseEvent = e;
|
|
1499
1774
|
if (content && tooltipManager) {
|
|
1500
1775
|
const svgRect = svg.getBoundingClientRect();
|
|
@@ -1521,6 +1796,8 @@ function createSankey(container, spec, options) {
|
|
|
1521
1796
|
}
|
|
1522
1797
|
};
|
|
1523
1798
|
const handleMouseLeave = () => {
|
|
1799
|
+
if (activeLeave !== handleMouseLeave) return;
|
|
1800
|
+
activeLeave = null;
|
|
1524
1801
|
tooltipManager?.hide();
|
|
1525
1802
|
options?.onLinkHover?.(null);
|
|
1526
1803
|
resetLinkOpacity(svg, layout);
|
|
@@ -1528,6 +1805,13 @@ function createSankey(container, spec, options) {
|
|
|
1528
1805
|
const handleClick = () => {
|
|
1529
1806
|
options?.onLinkClick?.(linkData);
|
|
1530
1807
|
};
|
|
1808
|
+
navigation.push({
|
|
1809
|
+
element: el,
|
|
1810
|
+
key: JSON.stringify(["link", sourceId, targetId]),
|
|
1811
|
+
enter: handleMouseEnter,
|
|
1812
|
+
leave: handleMouseLeave,
|
|
1813
|
+
activate: handleClick
|
|
1814
|
+
});
|
|
1531
1815
|
el.addEventListener("mouseenter", handleMouseEnter);
|
|
1532
1816
|
el.addEventListener("mousemove", handleMouseMove);
|
|
1533
1817
|
el.addEventListener("mouseleave", handleMouseLeave);
|
|
@@ -1539,17 +1823,14 @@ function createSankey(container, spec, options) {
|
|
|
1539
1823
|
el.removeEventListener("click", handleClick);
|
|
1540
1824
|
});
|
|
1541
1825
|
}
|
|
1826
|
+
cleanups.push(wireMarkNavigation(svg, navigation));
|
|
1542
1827
|
return () => {
|
|
1828
|
+
activeLeave?.();
|
|
1543
1829
|
for (const cleanup of cleanups) {
|
|
1544
1830
|
cleanup();
|
|
1545
1831
|
}
|
|
1546
1832
|
};
|
|
1547
1833
|
}
|
|
1548
|
-
function findLinkData(layout, sourceId, targetId) {
|
|
1549
|
-
if (!sourceId || !targetId) return {};
|
|
1550
|
-
const link = layout.links.find((l) => l.sourceId === sourceId && l.targetId === targetId);
|
|
1551
|
-
return link?.data ?? {};
|
|
1552
|
-
}
|
|
1553
1834
|
function tracePath(layout, seeds) {
|
|
1554
1835
|
const out = /* @__PURE__ */ new Map();
|
|
1555
1836
|
const inn = /* @__PURE__ */ new Map();
|
|
@@ -1601,20 +1882,21 @@ function createSankey(container, spec, options) {
|
|
|
1601
1882
|
}
|
|
1602
1883
|
function resetLinkOpacity(svg, layout) {
|
|
1603
1884
|
const linkElements = svg.querySelectorAll(".oc-sankey-link");
|
|
1604
|
-
for (const el of linkElements) {
|
|
1885
|
+
for (const [index, el] of linkElements.entries()) {
|
|
1605
1886
|
const path = el.querySelector("path");
|
|
1606
1887
|
if (!path) continue;
|
|
1607
|
-
const
|
|
1608
|
-
const target = el.getAttribute("data-target");
|
|
1609
|
-
const link = layout.links.find((l) => l.sourceId === source && l.targetId === target);
|
|
1888
|
+
const link = layout.links[index];
|
|
1610
1889
|
path.setAttribute("fill-opacity", String(link?.fillOpacity ?? 0.5));
|
|
1611
1890
|
}
|
|
1612
1891
|
for (const el of svg.querySelectorAll(".oc-sankey-node, .oc-sankey-label")) {
|
|
1613
1892
|
el.style.opacity = "1";
|
|
1614
1893
|
}
|
|
1615
1894
|
}
|
|
1616
|
-
function render() {
|
|
1895
|
+
function render(prepared) {
|
|
1617
1896
|
if (destroyed) return;
|
|
1897
|
+
const nextLayout = prepared ?? compile();
|
|
1898
|
+
const focusedMark = captureMarkFocus(svgElement);
|
|
1899
|
+
tooltipManager?.hide();
|
|
1618
1900
|
if (animationCleanup) {
|
|
1619
1901
|
animationCleanup();
|
|
1620
1902
|
animationCleanup = null;
|
|
@@ -1629,7 +1911,7 @@ function createSankey(container, spec, options) {
|
|
|
1629
1911
|
if (svgElement?.parentNode) {
|
|
1630
1912
|
svgElement.parentNode.removeChild(svgElement);
|
|
1631
1913
|
}
|
|
1632
|
-
currentLayout =
|
|
1914
|
+
currentLayout = nextLayout;
|
|
1633
1915
|
const shouldAnimate = isFirstRender && !!currentLayout.animation?.enter;
|
|
1634
1916
|
isFirstRender = false;
|
|
1635
1917
|
const animation = shouldAnimate ? currentLayout.animation : void 0;
|
|
@@ -1645,8 +1927,8 @@ function createSankey(container, spec, options) {
|
|
|
1645
1927
|
if (!tooltipManager) {
|
|
1646
1928
|
tooltipManager = createTooltipManager(container);
|
|
1647
1929
|
}
|
|
1648
|
-
cleanupTooltipEvents = wireTooltipAndInteraction(svgElement, currentLayout);
|
|
1649
1930
|
}
|
|
1931
|
+
cleanupTooltipEvents = wireTooltipAndInteraction(svgElement, currentLayout);
|
|
1650
1932
|
if (shouldAnimate && svgElement) {
|
|
1651
1933
|
animationCleanup = setupAnimationCleanup(svgElement, () => {
|
|
1652
1934
|
animationCleanup = null;
|
|
@@ -1662,17 +1944,17 @@ function createSankey(container, spec, options) {
|
|
|
1662
1944
|
fontsReloadPending = false;
|
|
1663
1945
|
container.dataset.ocFontsState = "ready";
|
|
1664
1946
|
}
|
|
1947
|
+
if (svgElement) restoreMarkFocus(svgElement, focusedMark);
|
|
1665
1948
|
}
|
|
1666
1949
|
function update(newSpec) {
|
|
1667
1950
|
if (destroyed) return;
|
|
1951
|
+
const nextFont = resolveEffectiveFont(newSpec);
|
|
1952
|
+
const nextMeasure = nextFont === fontFamily ? measureText : createMeasureText(nextFont);
|
|
1953
|
+
const nextLayout = compile(newSpec, nextMeasure);
|
|
1668
1954
|
currentSpec = newSpec;
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
measureText = createMeasureText(fontFamily);
|
|
1673
|
-
}
|
|
1674
|
-
isFirstRender = true;
|
|
1675
|
-
render();
|
|
1955
|
+
fontFamily = nextFont;
|
|
1956
|
+
measureText = nextMeasure;
|
|
1957
|
+
render(nextLayout);
|
|
1676
1958
|
}
|
|
1677
1959
|
function resize() {
|
|
1678
1960
|
if (destroyed) return;
|
|
@@ -1686,15 +1968,17 @@ function createSankey(container, spec, options) {
|
|
|
1686
1968
|
if (!svgElement) {
|
|
1687
1969
|
throw new Error("Sankey is not rendered yet");
|
|
1688
1970
|
}
|
|
1971
|
+
const terminalSvg = renderSankeySVG(currentLayout);
|
|
1972
|
+
const resolvedOptions = { theme: currentLayout.theme, ...exportOptions };
|
|
1689
1973
|
switch (format) {
|
|
1690
1974
|
case "svg":
|
|
1691
|
-
return exportSVG(
|
|
1975
|
+
return exportSVG(terminalSvg);
|
|
1692
1976
|
case "svg-with-fonts":
|
|
1693
|
-
return exportSVGWithFonts(
|
|
1977
|
+
return exportSVGWithFonts(terminalSvg, resolvedOptions);
|
|
1694
1978
|
case "png":
|
|
1695
|
-
return exportPNG(
|
|
1979
|
+
return exportPNG(terminalSvg, resolvedOptions);
|
|
1696
1980
|
case "jpg":
|
|
1697
|
-
return exportJPG(
|
|
1981
|
+
return exportJPG(terminalSvg, resolvedOptions);
|
|
1698
1982
|
default:
|
|
1699
1983
|
throw new Error(`Unsupported export format: ${format}`);
|
|
1700
1984
|
}
|
|
@@ -1744,8 +2028,8 @@ function createSankey(container, spec, options) {
|
|
|
1744
2028
|
}
|
|
1745
2029
|
if (options?.tooltip !== false && svgElement) {
|
|
1746
2030
|
tooltipManager = createTooltipManager(container);
|
|
1747
|
-
cleanupTooltipEvents = wireTooltipAndInteraction(svgElement, currentLayout);
|
|
1748
2031
|
}
|
|
2032
|
+
cleanupTooltipEvents = wireTooltipAndInteraction(svgElement, currentLayout);
|
|
1749
2033
|
if (shouldAnimate && svgElement) {
|
|
1750
2034
|
animationCleanup = setupAnimationCleanup(svgElement, () => {
|
|
1751
2035
|
animationCleanup = null;
|
|
@@ -1787,9 +2071,11 @@ function createSankey(container, spec, options) {
|
|
|
1787
2071
|
}
|
|
1788
2072
|
|
|
1789
2073
|
// src/table-keyboard.ts
|
|
2074
|
+
var navigationId = 0;
|
|
1790
2075
|
function attachKeyboardNav(options) {
|
|
1791
2076
|
const { wrapper, onSort, onClearSearch, onAnnounce } = options;
|
|
1792
2077
|
let focusedCell = { row: -1, col: 0 };
|
|
2078
|
+
const idPrefix = `oc-table-${++navigationId}`;
|
|
1793
2079
|
const table = wrapper.querySelector("table");
|
|
1794
2080
|
if (!table) return () => {
|
|
1795
2081
|
};
|
|
@@ -1806,10 +2092,10 @@ function attachKeyboardNav(options) {
|
|
|
1806
2092
|
if (!thead) return [];
|
|
1807
2093
|
const headerRow = thead.querySelector("tr");
|
|
1808
2094
|
if (!headerRow) return [];
|
|
1809
|
-
return Array.from(headerRow.querySelectorAll("th"));
|
|
2095
|
+
return Array.from(headerRow.querySelectorAll("th")).filter((cell) => !cell.hidden);
|
|
1810
2096
|
}
|
|
1811
2097
|
function getCellsInRow(tr) {
|
|
1812
|
-
return Array.from(tr.querySelectorAll("td"));
|
|
2098
|
+
return Array.from(tr.querySelectorAll("td")).filter((cell) => !cell.hidden);
|
|
1813
2099
|
}
|
|
1814
2100
|
function getColCount() {
|
|
1815
2101
|
const rows = getRows();
|
|
@@ -1836,7 +2122,7 @@ function attachKeyboardNav(options) {
|
|
|
1836
2122
|
const cells = getCellsInRow(tr);
|
|
1837
2123
|
const cell = cells[col];
|
|
1838
2124
|
if (!cell) return;
|
|
1839
|
-
const cellId =
|
|
2125
|
+
const cellId = `${idPrefix}-cell-${row}-${col}`;
|
|
1840
2126
|
cell.id = cellId;
|
|
1841
2127
|
cell.classList.add("oc-table-cell-focus");
|
|
1842
2128
|
cell.setAttribute("data-row", String(row));
|
|
@@ -1848,17 +2134,33 @@ function attachKeyboardNav(options) {
|
|
|
1848
2134
|
}
|
|
1849
2135
|
function handleTbodyFocus() {
|
|
1850
2136
|
if (focusedCell.row < 0) {
|
|
1851
|
-
|
|
2137
|
+
const row = getRows().findIndex(
|
|
2138
|
+
(item) => item.getAttribute("data-row-id") === options.initialCell?.rowId
|
|
2139
|
+
);
|
|
2140
|
+
const col = getHeaderCells().findIndex(
|
|
2141
|
+
(item) => item.getAttribute("data-column") === options.initialCell?.columnKey
|
|
2142
|
+
);
|
|
2143
|
+
setFocusedCell(Math.max(0, row), Math.max(0, col));
|
|
1852
2144
|
} else {
|
|
1853
2145
|
setFocusedCell(focusedCell.row, focusedCell.col);
|
|
1854
2146
|
}
|
|
1855
2147
|
}
|
|
1856
2148
|
function handleTbodyKeydown(e) {
|
|
2149
|
+
if (e.target !== tbody) return;
|
|
1857
2150
|
const rows = getRows();
|
|
1858
2151
|
if (rows.length === 0) return;
|
|
1859
2152
|
const colCount = getColCount();
|
|
1860
2153
|
const { row, col } = focusedCell;
|
|
1861
2154
|
switch (e.key) {
|
|
2155
|
+
case "Enter":
|
|
2156
|
+
case " ": {
|
|
2157
|
+
const rowId = rows[Math.max(row, 0)]?.getAttribute("data-row-id");
|
|
2158
|
+
if (rowId && options.onRowActivate) {
|
|
2159
|
+
e.preventDefault();
|
|
2160
|
+
options.onRowActivate(rowId);
|
|
2161
|
+
}
|
|
2162
|
+
break;
|
|
2163
|
+
}
|
|
1862
2164
|
case "ArrowDown":
|
|
1863
2165
|
e.preventDefault();
|
|
1864
2166
|
if (row < rows.length - 1) {
|
|
@@ -1944,7 +2246,7 @@ function attachKeyboardNav(options) {
|
|
|
1944
2246
|
if (e.key === "Escape") {
|
|
1945
2247
|
e.preventDefault();
|
|
1946
2248
|
onClearSearch();
|
|
1947
|
-
if (tbody) {
|
|
2249
|
+
if (tbody?.isConnected) {
|
|
1948
2250
|
tbody.focus();
|
|
1949
2251
|
onAnnounce("Search cleared");
|
|
1950
2252
|
}
|
|
@@ -1973,6 +2275,46 @@ function attachKeyboardNav(options) {
|
|
|
1973
2275
|
};
|
|
1974
2276
|
}
|
|
1975
2277
|
|
|
2278
|
+
// src/table-responsive.ts
|
|
2279
|
+
function applyPriorityTable(wrapper, layout, enabled) {
|
|
2280
|
+
wrapper.classList.toggle("oc-table--priority", enabled);
|
|
2281
|
+
for (const old of wrapper.querySelectorAll(".oc-table-details")) old.remove();
|
|
2282
|
+
const secondary = layout.columns.flatMap((column, index) => column.priority > 1 ? [index] : []);
|
|
2283
|
+
const hidden = new Set(secondary);
|
|
2284
|
+
for (const row of wrapper.querySelectorAll("thead tr, tbody tr, tfoot tr")) {
|
|
2285
|
+
const cells = Array.from(row.children);
|
|
2286
|
+
cells.forEach((cell2, index) => {
|
|
2287
|
+
cell2.hidden = enabled && hidden.has(index);
|
|
2288
|
+
});
|
|
2289
|
+
if (!enabled || !secondary.length) continue;
|
|
2290
|
+
const cell = document.createElement(row.parentElement?.tagName === "THEAD" ? "th" : "td");
|
|
2291
|
+
cell.className = "oc-table-details";
|
|
2292
|
+
if (cell.tagName === "TH") {
|
|
2293
|
+
cell.textContent = "Details";
|
|
2294
|
+
cell.setAttribute("scope", "col");
|
|
2295
|
+
} else if (row.parentElement?.tagName === "TBODY") {
|
|
2296
|
+
const details = document.createElement("details");
|
|
2297
|
+
const summary = document.createElement("summary");
|
|
2298
|
+
summary.textContent = "More";
|
|
2299
|
+
summary.setAttribute("aria-label", `More details for ${cells[0]?.textContent ?? "row"}`);
|
|
2300
|
+
details.appendChild(summary);
|
|
2301
|
+
const list = document.createElement("dl");
|
|
2302
|
+
for (const index of secondary) {
|
|
2303
|
+
const term = document.createElement("dt");
|
|
2304
|
+
term.textContent = layout.columns[index].label;
|
|
2305
|
+
const description = document.createElement("dd");
|
|
2306
|
+
for (const child of Array.from(cells[index]?.childNodes ?? []))
|
|
2307
|
+
description.appendChild(child.cloneNode(true));
|
|
2308
|
+
list.append(term, description);
|
|
2309
|
+
}
|
|
2310
|
+
details.appendChild(list);
|
|
2311
|
+
details.addEventListener("click", (event) => event.stopPropagation());
|
|
2312
|
+
cell.appendChild(details);
|
|
2313
|
+
}
|
|
2314
|
+
row.appendChild(cell);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
1976
2318
|
// src/table-mount.ts
|
|
1977
2319
|
import { BREAKPOINT_COMPACT_MAX } from "@opendata-ai/openchart-core";
|
|
1978
2320
|
import { compileTable } from "@opendata-ai/openchart-engine";
|
|
@@ -2064,6 +2406,7 @@ function renderTbody(rows, columns) {
|
|
|
2064
2406
|
if (!cell) continue;
|
|
2065
2407
|
const td = renderCell(cell);
|
|
2066
2408
|
td.setAttribute("role", "gridcell");
|
|
2409
|
+
td.setAttribute("data-column", columns[c].key);
|
|
2067
2410
|
td.style.textAlign = columns[c].align;
|
|
2068
2411
|
td.setAttribute("data-label", columns[c].label);
|
|
2069
2412
|
td.setAttribute("data-priority", String(columns[c].priority));
|
|
@@ -2083,6 +2426,7 @@ function renderTfoot(totalRow, columns) {
|
|
|
2083
2426
|
const cell = totalRow.cells[c];
|
|
2084
2427
|
const td = cell ? renderCell(cell) : document.createElement("td");
|
|
2085
2428
|
td.setAttribute("role", "gridcell");
|
|
2429
|
+
td.setAttribute("data-column", columns[c].key);
|
|
2086
2430
|
td.style.textAlign = columns[c].align;
|
|
2087
2431
|
td.setAttribute("data-label", columns[c].label);
|
|
2088
2432
|
td.setAttribute("data-priority", String(columns[c].priority));
|
|
@@ -2271,6 +2615,7 @@ function renderTable(layout, container, opts) {
|
|
|
2271
2615
|
const stagger = clampStaggerDelay2(enterPhase.staggerDelay, rowCount);
|
|
2272
2616
|
stampAnimationVars(wrapper, {
|
|
2273
2617
|
duration: enterPhase.duration,
|
|
2618
|
+
spring: enterPhase.spring,
|
|
2274
2619
|
stagger,
|
|
2275
2620
|
ease: enterPhase.ease
|
|
2276
2621
|
});
|
|
@@ -2305,6 +2650,7 @@ function createTable(container, spec, options) {
|
|
|
2305
2650
|
let disconnectResize = null;
|
|
2306
2651
|
let cleanupKeyboard = null;
|
|
2307
2652
|
let cleanupAnimations = null;
|
|
2653
|
+
let pendingResize = false;
|
|
2308
2654
|
let isFirstRender = true;
|
|
2309
2655
|
let destroyed = false;
|
|
2310
2656
|
let sortTouched = false;
|
|
@@ -2346,7 +2692,7 @@ function createTable(container, spec, options) {
|
|
|
2346
2692
|
options?.onStateChange?.({ ...internalState });
|
|
2347
2693
|
}
|
|
2348
2694
|
}
|
|
2349
|
-
function compile() {
|
|
2695
|
+
function compile(candidate = currentSpec) {
|
|
2350
2696
|
const state = getState();
|
|
2351
2697
|
const darkMode = resolveDarkMode(options?.darkMode);
|
|
2352
2698
|
const { width } = getContainerDimensions();
|
|
@@ -2364,7 +2710,7 @@ function createTable(container, spec, options) {
|
|
|
2364
2710
|
search: state.search || void 0,
|
|
2365
2711
|
page: state.page
|
|
2366
2712
|
};
|
|
2367
|
-
return compileTable(
|
|
2713
|
+
return compileTable(candidate, compileOpts);
|
|
2368
2714
|
}
|
|
2369
2715
|
function sameSort(a, b) {
|
|
2370
2716
|
if (a === null || b === null) return a === b;
|
|
@@ -2390,7 +2736,17 @@ function createTable(container, spec, options) {
|
|
|
2390
2736
|
function applyBreakpointClass() {
|
|
2391
2737
|
if (!wrapperElement) return;
|
|
2392
2738
|
const { width } = getContainerDimensions();
|
|
2393
|
-
|
|
2739
|
+
const compact = width < BREAKPOINT_COMPACT_MAX;
|
|
2740
|
+
wrapperElement.classList.toggle(
|
|
2741
|
+
"oc-table--cards",
|
|
2742
|
+
compact && (currentSpec.mobileLayout ?? "cards") === "cards"
|
|
2743
|
+
);
|
|
2744
|
+
if (currentLayout)
|
|
2745
|
+
applyPriorityTable(
|
|
2746
|
+
wrapperElement,
|
|
2747
|
+
currentLayout,
|
|
2748
|
+
compact && currentSpec.mobileLayout === "priority"
|
|
2749
|
+
);
|
|
2394
2750
|
if (currentSpec.density !== void 0 || currentSpec.compact === true) return;
|
|
2395
2751
|
const columns = currentLayout?.columns.length ?? 0;
|
|
2396
2752
|
const condensed = columns * MIN_READABLE_COLUMN_WIDTH > width || width < AUTO_CONDENSE_MAX_WIDTH;
|
|
@@ -2398,69 +2754,100 @@ function createTable(container, spec, options) {
|
|
|
2398
2754
|
wrapperElement.classList.toggle("oc-table--compact", condensed);
|
|
2399
2755
|
wrapperElement.classList.toggle("oc-table--regular", !condensed);
|
|
2400
2756
|
}
|
|
2401
|
-
function render() {
|
|
2757
|
+
function render(candidate = currentSpec) {
|
|
2402
2758
|
if (destroyed) return;
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2759
|
+
const nextLayout = compile(candidate);
|
|
2760
|
+
const active = document.activeElement;
|
|
2761
|
+
const bodyFocused = active === wrapperElement?.querySelector("tbody");
|
|
2762
|
+
const focusedCell = wrapperElement?.querySelector(".oc-table-cell-focus");
|
|
2763
|
+
const initialCell = bodyFocused && focusedCell ? {
|
|
2764
|
+
rowId: focusedCell.closest("tr")?.getAttribute("data-row-id") ?? "",
|
|
2765
|
+
columnKey: focusedCell.getAttribute("data-column") ?? ""
|
|
2766
|
+
} : void 0;
|
|
2767
|
+
const headerKey = wrapperElement?.contains(active) ? active?.closest("th")?.getAttribute("data-column") : null;
|
|
2768
|
+
const pageAction = wrapperElement?.contains(active) ? active?.getAttribute("data-page-action") : null;
|
|
2769
|
+
currentSpec = candidate;
|
|
2770
|
+
if (cleanupAnimations) {
|
|
2771
|
+
cleanupAnimations();
|
|
2772
|
+
cleanupAnimations = null;
|
|
2773
|
+
}
|
|
2774
|
+
if (cleanupKeyboard) {
|
|
2775
|
+
cleanupKeyboard();
|
|
2776
|
+
cleanupKeyboard = null;
|
|
2777
|
+
}
|
|
2778
|
+
if (wrapperElement?.parentNode) {
|
|
2779
|
+
wrapperElement.parentNode.removeChild(wrapperElement);
|
|
2780
|
+
wrapperElement = null;
|
|
2781
|
+
}
|
|
2782
|
+
currentLayout = nextLayout;
|
|
2783
|
+
const shouldAnimate = isFirstRender && !!currentLayout.animation?.enter;
|
|
2784
|
+
wrapperElement = renderTable(currentLayout, container, { animate: shouldAnimate });
|
|
2785
|
+
if (shouldAnimate && wrapperElement) {
|
|
2786
|
+
cleanupAnimations = setupTableAnimationCleanup(wrapperElement, () => {
|
|
2406
2787
|
cleanupAnimations = null;
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
const
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2788
|
+
if (pendingResize && !destroyed) {
|
|
2789
|
+
pendingResize = false;
|
|
2790
|
+
resize();
|
|
2791
|
+
}
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
if (isFirstRender) {
|
|
2795
|
+
isFirstRender = false;
|
|
2796
|
+
}
|
|
2797
|
+
const isDark = resolveDarkMode(options?.darkMode);
|
|
2798
|
+
if (isDark) {
|
|
2799
|
+
container.classList.add("oc-dark");
|
|
2800
|
+
} else {
|
|
2801
|
+
container.classList.remove("oc-dark");
|
|
2802
|
+
}
|
|
2803
|
+
applyBreakpointClass();
|
|
2804
|
+
if (options?.onRowClick) {
|
|
2805
|
+
wrapperElement.classList.add("oc-table--clickable");
|
|
2806
|
+
}
|
|
2807
|
+
wireEvents();
|
|
2808
|
+
if (wrapperElement) {
|
|
2809
|
+
cleanupKeyboard = attachKeyboardNav({
|
|
2810
|
+
wrapper: wrapperElement,
|
|
2811
|
+
initialCell,
|
|
2812
|
+
onRowActivate: options?.onRowClick ? (rowId) => {
|
|
2813
|
+
const row = currentLayout.rows.find((item) => item.id === rowId);
|
|
2814
|
+
if (row) options.onRowClick?.(row.data);
|
|
2815
|
+
} : void 0,
|
|
2816
|
+
onSort: (columnKey) => {
|
|
2817
|
+
const state = getState();
|
|
2818
|
+
const newSort = cycleSort(state.sort, columnKey);
|
|
2819
|
+
updateState({ sort: newSort, page: 0 });
|
|
2820
|
+
if (newSort) {
|
|
2821
|
+
const dir = newSort.direction === "asc" ? "ascending" : "descending";
|
|
2822
|
+
announce(`Sorted by ${columnKey} ${dir}`);
|
|
2823
|
+
} else {
|
|
2824
|
+
announce("Sort cleared");
|
|
2825
|
+
}
|
|
2826
|
+
if (!isControlled) {
|
|
2827
|
+
rerender();
|
|
2828
|
+
}
|
|
2829
|
+
},
|
|
2830
|
+
onClearSearch: () => {
|
|
2831
|
+
if (searchDebounceTimer !== null) clearTimeout(searchDebounceTimer);
|
|
2832
|
+
searchDebounceTimer = null;
|
|
2833
|
+
updateState({ search: "", page: 0 });
|
|
2834
|
+
if (!isControlled) {
|
|
2835
|
+
rerender();
|
|
2836
|
+
}
|
|
2837
|
+
wrapperElement?.querySelector("tbody")?.focus();
|
|
2838
|
+
announce("Search cleared");
|
|
2839
|
+
},
|
|
2840
|
+
onAnnounce: announce
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
if (bodyFocused) wrapperElement?.querySelector("tbody")?.focus();
|
|
2844
|
+
else if (headerKey) {
|
|
2845
|
+
[...wrapperElement?.querySelectorAll("th[data-column]") ?? []].find((element) => element.dataset.column === headerKey)?.focus();
|
|
2846
|
+
} else if (pageAction) {
|
|
2847
|
+
const buttons = [
|
|
2848
|
+
...wrapperElement?.querySelectorAll("[data-page-action]") ?? []
|
|
2849
|
+
];
|
|
2850
|
+
(buttons.find((button) => button.dataset.pageAction === pageAction && !button.disabled) ?? buttons.find((button) => !button.disabled) ?? wrapperElement?.querySelector("tbody"))?.focus();
|
|
2464
2851
|
}
|
|
2465
2852
|
}
|
|
2466
2853
|
function wireEvents() {
|
|
@@ -2543,20 +2930,22 @@ function createTable(container, spec, options) {
|
|
|
2543
2930
|
options?.onRowClick?.(row.data);
|
|
2544
2931
|
}
|
|
2545
2932
|
}
|
|
2546
|
-
function rerender() {
|
|
2933
|
+
function rerender(candidate = currentSpec) {
|
|
2547
2934
|
if (destroyed) return;
|
|
2548
2935
|
const searchInput = wrapperElement?.querySelector(
|
|
2549
2936
|
".oc-table-search input"
|
|
2550
2937
|
);
|
|
2551
2938
|
const hadFocus = searchInput && document.activeElement === searchInput;
|
|
2939
|
+
const pendingQuery = searchDebounceTimer !== null ? searchInput?.value : void 0;
|
|
2552
2940
|
const selectionStart = searchInput?.selectionStart ?? 0;
|
|
2553
2941
|
const selectionEnd = searchInput?.selectionEnd ?? 0;
|
|
2554
|
-
render();
|
|
2942
|
+
render(candidate);
|
|
2555
2943
|
if (hadFocus) {
|
|
2556
2944
|
const newInput = wrapperElement?.querySelector(
|
|
2557
2945
|
".oc-table-search input"
|
|
2558
2946
|
);
|
|
2559
2947
|
if (newInput) {
|
|
2948
|
+
if (pendingQuery !== void 0) newInput.value = pendingQuery;
|
|
2560
2949
|
newInput.focus();
|
|
2561
2950
|
newInput.setSelectionRange(selectionStart, selectionEnd);
|
|
2562
2951
|
}
|
|
@@ -2564,13 +2953,15 @@ function createTable(container, spec, options) {
|
|
|
2564
2953
|
}
|
|
2565
2954
|
function update(newSpec) {
|
|
2566
2955
|
if (destroyed) return;
|
|
2567
|
-
|
|
2568
|
-
render();
|
|
2956
|
+
rerender(newSpec);
|
|
2569
2957
|
}
|
|
2570
2958
|
function resize() {
|
|
2571
2959
|
if (destroyed) return;
|
|
2572
|
-
if (cleanupAnimations)
|
|
2573
|
-
|
|
2960
|
+
if (cleanupAnimations) {
|
|
2961
|
+
pendingResize = true;
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
rerender();
|
|
2574
2965
|
}
|
|
2575
2966
|
function doExport(format) {
|
|
2576
2967
|
if (format !== "csv") {
|
|
@@ -2579,15 +2970,18 @@ function createTable(container, spec, options) {
|
|
|
2579
2970
|
const state = getState();
|
|
2580
2971
|
const darkMode = resolveDarkMode(options?.darkMode);
|
|
2581
2972
|
const { width } = getContainerDimensions();
|
|
2582
|
-
const fullLayout = compileTable(
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2973
|
+
const fullLayout = compileTable(
|
|
2974
|
+
{ ...currentSpec, pagination: false },
|
|
2975
|
+
{
|
|
2976
|
+
width,
|
|
2977
|
+
height: 600,
|
|
2978
|
+
theme: options?.theme,
|
|
2979
|
+
darkMode,
|
|
2980
|
+
sort: resolveEffectiveSort(state),
|
|
2981
|
+
search: state.search || void 0
|
|
2982
|
+
// No page/pageSize: get all rows
|
|
2983
|
+
}
|
|
2984
|
+
);
|
|
2591
2985
|
const headers = fullLayout.columns.map((c) => c.label);
|
|
2592
2986
|
const csvRows = [headers.map(csvEscape).join(",")];
|
|
2593
2987
|
for (const row of fullLayout.rows) {
|
|
@@ -2598,6 +2992,10 @@ function createTable(container, spec, options) {
|
|
|
2598
2992
|
}
|
|
2599
2993
|
function setState(partial) {
|
|
2600
2994
|
if (destroyed) return;
|
|
2995
|
+
if (partial.search !== void 0 && partial.search !== getState().search && searchDebounceTimer !== null) {
|
|
2996
|
+
clearTimeout(searchDebounceTimer);
|
|
2997
|
+
searchDebounceTimer = null;
|
|
2998
|
+
}
|
|
2601
2999
|
if (partial.sort !== void 0 && !sameSort(partial.sort, getState().sort)) {
|
|
2602
3000
|
sortTouched = true;
|
|
2603
3001
|
}
|
|
@@ -2610,7 +3008,7 @@ function createTable(container, spec, options) {
|
|
|
2610
3008
|
if (partial.search !== void 0) internalState.search = partial.search;
|
|
2611
3009
|
if (partial.page !== void 0) internalState.page = partial.page;
|
|
2612
3010
|
}
|
|
2613
|
-
|
|
3011
|
+
rerender();
|
|
2614
3012
|
}
|
|
2615
3013
|
function destroy() {
|
|
2616
3014
|
if (destroyed) return;
|
|
@@ -2687,8 +3085,8 @@ function createTileMap(container, spec, options) {
|
|
|
2687
3085
|
let isAutoHeight = null;
|
|
2688
3086
|
let fontsReloadPending = false;
|
|
2689
3087
|
container.classList.add("oc-tilemap-root");
|
|
2690
|
-
function resolveEffectiveFont() {
|
|
2691
|
-
return options?.theme?.fonts?.family ??
|
|
3088
|
+
function resolveEffectiveFont(candidate = currentSpec) {
|
|
3089
|
+
return options?.theme?.fonts?.family ?? candidate.theme?.fonts?.family ?? resolveFontFamily(container);
|
|
2692
3090
|
}
|
|
2693
3091
|
let fontFamily = resolveEffectiveFont();
|
|
2694
3092
|
let measureText = createMeasureText(fontFamily);
|
|
@@ -2709,7 +3107,7 @@ function createTileMap(container, spec, options) {
|
|
|
2709
3107
|
const height = targetTile * TILEMAP_GRID_ROWS + TILEMAP_TILE_GAP * (TILEMAP_GRID_ROWS - 1) + TILEMAP_CHROME_RESERVE;
|
|
2710
3108
|
return { width, height };
|
|
2711
3109
|
}
|
|
2712
|
-
function compile() {
|
|
3110
|
+
function compile(candidate = currentSpec, measure = measureText) {
|
|
2713
3111
|
const { width, height } = getContainerDimensions();
|
|
2714
3112
|
const darkMode = resolveDarkMode(options?.darkMode);
|
|
2715
3113
|
const compileOpts = {
|
|
@@ -2718,12 +3116,14 @@ function createTileMap(container, spec, options) {
|
|
|
2718
3116
|
theme: options?.theme,
|
|
2719
3117
|
darkMode,
|
|
2720
3118
|
watermark: options?.watermark,
|
|
2721
|
-
measureText
|
|
3119
|
+
measureText: measure
|
|
2722
3120
|
};
|
|
2723
|
-
return compileTileMap(
|
|
3121
|
+
return compileTileMap(candidate, compileOpts);
|
|
2724
3122
|
}
|
|
2725
3123
|
function wireTooltipAndInteraction(svg, layout) {
|
|
2726
3124
|
const cleanups = [];
|
|
3125
|
+
const navigation = [];
|
|
3126
|
+
let activeLeave = null;
|
|
2727
3127
|
const tilesByStateCode = /* @__PURE__ */ new Map();
|
|
2728
3128
|
for (const t of layout.tiles) {
|
|
2729
3129
|
if (!tilesByStateCode.has(t.stateCode)) tilesByStateCode.set(t.stateCode, t);
|
|
@@ -2736,6 +3136,8 @@ function createTileMap(container, spec, options) {
|
|
|
2736
3136
|
const content = layout.tooltipDescriptors.get(stateCode);
|
|
2737
3137
|
const tile = tilesByStateCode.get(stateCode);
|
|
2738
3138
|
const handleMouseEnter = (e) => {
|
|
3139
|
+
activeLeave?.();
|
|
3140
|
+
activeLeave = handleMouseLeave;
|
|
2739
3141
|
const mouseEvent = e;
|
|
2740
3142
|
tilesGroup?.classList.add("oc-hover-active");
|
|
2741
3143
|
el.classList.add("oc-tilemap-tile--hover");
|
|
@@ -2764,6 +3166,8 @@ function createTileMap(container, spec, options) {
|
|
|
2764
3166
|
}
|
|
2765
3167
|
};
|
|
2766
3168
|
const handleMouseLeave = () => {
|
|
3169
|
+
if (activeLeave !== handleMouseLeave) return;
|
|
3170
|
+
activeLeave = null;
|
|
2767
3171
|
tilesGroup?.classList.remove("oc-hover-active");
|
|
2768
3172
|
el.classList.remove("oc-tilemap-tile--hover");
|
|
2769
3173
|
tooltipManager?.hide();
|
|
@@ -2779,6 +3183,13 @@ function createTileMap(container, spec, options) {
|
|
|
2779
3183
|
});
|
|
2780
3184
|
}
|
|
2781
3185
|
};
|
|
3186
|
+
navigation.push({
|
|
3187
|
+
element: el,
|
|
3188
|
+
key: stateCode,
|
|
3189
|
+
enter: handleMouseEnter,
|
|
3190
|
+
leave: handleMouseLeave,
|
|
3191
|
+
activate: handleClick
|
|
3192
|
+
});
|
|
2782
3193
|
el.addEventListener("mouseenter", handleMouseEnter);
|
|
2783
3194
|
el.addEventListener("mousemove", handleMouseMove);
|
|
2784
3195
|
el.addEventListener("mouseleave", handleMouseLeave);
|
|
@@ -2790,13 +3201,17 @@ function createTileMap(container, spec, options) {
|
|
|
2790
3201
|
el.removeEventListener("click", handleClick);
|
|
2791
3202
|
});
|
|
2792
3203
|
}
|
|
3204
|
+
cleanups.push(wireMarkNavigation(svg, navigation));
|
|
2793
3205
|
return () => {
|
|
3206
|
+
activeLeave?.();
|
|
2794
3207
|
for (const cleanup of cleanups) {
|
|
2795
3208
|
cleanup();
|
|
2796
3209
|
}
|
|
2797
3210
|
};
|
|
2798
3211
|
}
|
|
2799
3212
|
function render(animate = false) {
|
|
3213
|
+
const focusedMark = captureMarkFocus(svgElement);
|
|
3214
|
+
tooltipManager?.hide();
|
|
2800
3215
|
if (animationCleanup) {
|
|
2801
3216
|
animationCleanup();
|
|
2802
3217
|
animationCleanup = null;
|
|
@@ -2819,6 +3234,7 @@ function createTileMap(container, spec, options) {
|
|
|
2819
3234
|
}
|
|
2820
3235
|
if (animate && currentLayout.animation?.enter) {
|
|
2821
3236
|
animationCleanup = setupAnimationCleanup(newSvg, () => {
|
|
3237
|
+
animationCleanup = null;
|
|
2822
3238
|
if (pendingResize && !destroyed) {
|
|
2823
3239
|
pendingResize = false;
|
|
2824
3240
|
resize();
|
|
@@ -2831,15 +3247,17 @@ function createTileMap(container, spec, options) {
|
|
|
2831
3247
|
fontsReloadPending = false;
|
|
2832
3248
|
container.dataset.ocFontsState = "ready";
|
|
2833
3249
|
}
|
|
3250
|
+
if (svgElement) restoreMarkFocus(svgElement, focusedMark);
|
|
2834
3251
|
}
|
|
2835
3252
|
function update(newSpec) {
|
|
3253
|
+
if (destroyed) return;
|
|
3254
|
+
const nextFont = resolveEffectiveFont(newSpec);
|
|
3255
|
+
const nextMeasure = nextFont === fontFamily ? measureText : createMeasureText(nextFont);
|
|
3256
|
+
const nextLayout = compile(newSpec, nextMeasure);
|
|
2836
3257
|
currentSpec = newSpec;
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
measureText = createMeasureText(fontFamily);
|
|
2841
|
-
}
|
|
2842
|
-
currentLayout = compile();
|
|
3258
|
+
fontFamily = nextFont;
|
|
3259
|
+
measureText = nextMeasure;
|
|
3260
|
+
currentLayout = nextLayout;
|
|
2843
3261
|
render();
|
|
2844
3262
|
}
|
|
2845
3263
|
function resize() {
|
|
@@ -2853,15 +3271,17 @@ function createTileMap(container, spec, options) {
|
|
|
2853
3271
|
}
|
|
2854
3272
|
function exportChart(format, options_) {
|
|
2855
3273
|
if (!svgElement) return "";
|
|
3274
|
+
const terminalSvg = renderTileMapSVG(currentLayout, { animate: false });
|
|
3275
|
+
const resolvedOptions = { theme: currentLayout.theme, ...options_ };
|
|
2856
3276
|
switch (format) {
|
|
2857
3277
|
case "svg":
|
|
2858
|
-
return exportSVG(
|
|
3278
|
+
return exportSVG(terminalSvg);
|
|
2859
3279
|
case "svg-with-fonts":
|
|
2860
|
-
return exportSVGWithFonts(
|
|
3280
|
+
return exportSVGWithFonts(terminalSvg, resolvedOptions);
|
|
2861
3281
|
case "png":
|
|
2862
|
-
return exportPNG(
|
|
3282
|
+
return exportPNG(terminalSvg, resolvedOptions);
|
|
2863
3283
|
case "jpg":
|
|
2864
|
-
return exportJPG(
|
|
3284
|
+
return exportJPG(terminalSvg, resolvedOptions);
|
|
2865
3285
|
default:
|
|
2866
3286
|
return "";
|
|
2867
3287
|
}
|
|
@@ -2931,6 +3351,7 @@ export {
|
|
|
2931
3351
|
createChart,
|
|
2932
3352
|
createGeoMap,
|
|
2933
3353
|
createGraph,
|
|
3354
|
+
createReader,
|
|
2934
3355
|
createSankey,
|
|
2935
3356
|
createSimulationWorker,
|
|
2936
3357
|
createTable,
|