@opendata-ai/openchart-vanilla 2.9.1 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +126 -55
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2 -0
- package/package.json +4 -3
- package/src/__tests__/table-keyboard.test.ts +361 -0
- package/src/__tests__/tooltip.test.ts +119 -9
- package/src/graph/__tests__/zoom.test.ts +12 -4
- package/src/graph/zoom.ts +16 -4
- package/src/graph-mount.ts +31 -8
- package/src/mount.ts +14 -2
- package/src/svg-renderer.ts +28 -13
- package/src/tooltip.ts +70 -44
package/dist/index.d.ts
CHANGED
|
@@ -326,7 +326,8 @@ declare function renderTable(layout: TableLayout, container: HTMLElement): HTMLE
|
|
|
326
326
|
* Tooltip manager: creates and positions a floating tooltip element.
|
|
327
327
|
*
|
|
328
328
|
* Shows tooltip content near the mouse/touch position with viewport
|
|
329
|
-
* edge avoidance. Touch support via tap-to-show,
|
|
329
|
+
* edge avoidance via @floating-ui/dom. Touch support via tap-to-show,
|
|
330
|
+
* tap-outside-to-hide.
|
|
330
331
|
*/
|
|
331
332
|
|
|
332
333
|
interface TooltipManager {
|
package/dist/index.js
CHANGED
|
@@ -640,10 +640,13 @@ var ZoomTransform = class _ZoomTransform {
|
|
|
640
640
|
/**
|
|
641
641
|
* Compute a transform that fits all nodes within the given canvas
|
|
642
642
|
* dimensions with the specified padding.
|
|
643
|
+
*
|
|
644
|
+
* Returns the transform and the ideal content height (in screen pixels)
|
|
645
|
+
* so callers can shrink the canvas to eliminate dead space.
|
|
643
646
|
*/
|
|
644
647
|
static fitBounds(nodes, canvasW, canvasH, padding = 40) {
|
|
645
648
|
if (nodes.length === 0) {
|
|
646
|
-
return _ZoomTransform.identity();
|
|
649
|
+
return { transform: _ZoomTransform.identity(), contentHeight: canvasH };
|
|
647
650
|
}
|
|
648
651
|
let minX = Infinity;
|
|
649
652
|
let minY = Infinity;
|
|
@@ -659,7 +662,10 @@ var ZoomTransform = class _ZoomTransform {
|
|
|
659
662
|
const graphW = maxX - minX;
|
|
660
663
|
const graphH = maxY - minY;
|
|
661
664
|
if (graphW === 0 && graphH === 0) {
|
|
662
|
-
return
|
|
665
|
+
return {
|
|
666
|
+
transform: new _ZoomTransform(canvasW / 2 - minX, canvasH / 2 - minY, 1),
|
|
667
|
+
contentHeight: padding * 2
|
|
668
|
+
};
|
|
663
669
|
}
|
|
664
670
|
const availW = canvasW - padding * 2;
|
|
665
671
|
const availH = canvasH - padding * 2;
|
|
@@ -667,7 +673,11 @@ var ZoomTransform = class _ZoomTransform {
|
|
|
667
673
|
const cx = (minX + maxX) / 2;
|
|
668
674
|
const tx = canvasW / 2 - cx * k;
|
|
669
675
|
const ty = padding - minY * k;
|
|
670
|
-
|
|
676
|
+
const contentHeight = graphH * k + padding * 2;
|
|
677
|
+
return {
|
|
678
|
+
transform: new _ZoomTransform(tx, ty, k),
|
|
679
|
+
contentHeight
|
|
680
|
+
};
|
|
671
681
|
}
|
|
672
682
|
/** Identity transform (no pan, no zoom). */
|
|
673
683
|
static identity() {
|
|
@@ -2373,6 +2383,7 @@ function observeResize(container, callback) {
|
|
|
2373
2383
|
}
|
|
2374
2384
|
|
|
2375
2385
|
// src/tooltip.ts
|
|
2386
|
+
import { computePosition, flip, offset, shift } from "@floating-ui/dom";
|
|
2376
2387
|
var TOOLTIP_OFFSET = 12;
|
|
2377
2388
|
function createTooltipManager(container) {
|
|
2378
2389
|
const tooltip = document.createElement("div");
|
|
@@ -2380,6 +2391,8 @@ function createTooltipManager(container) {
|
|
|
2380
2391
|
tooltip.setAttribute("role", "tooltip");
|
|
2381
2392
|
container.style.position = container.style.position || "relative";
|
|
2382
2393
|
container.appendChild(tooltip);
|
|
2394
|
+
let lastContentKey = "";
|
|
2395
|
+
let currentPositionId = 0;
|
|
2383
2396
|
const handleDocumentTouch = (e) => {
|
|
2384
2397
|
if (!container.contains(e.target)) {
|
|
2385
2398
|
hide();
|
|
@@ -2387,45 +2400,60 @@ function createTooltipManager(container) {
|
|
|
2387
2400
|
};
|
|
2388
2401
|
document.addEventListener("touchstart", handleDocumentTouch);
|
|
2389
2402
|
function show(content, x3, y3) {
|
|
2390
|
-
|
|
2391
|
-
if (
|
|
2392
|
-
|
|
2393
|
-
html
|
|
2394
|
-
if (
|
|
2395
|
-
|
|
2403
|
+
const contentKey = `${content.title}|${content.fields.length}|${content.fields[0]?.value}|${content.fields[content.fields.length - 1]?.value}`;
|
|
2404
|
+
if (contentKey !== lastContentKey) {
|
|
2405
|
+
lastContentKey = contentKey;
|
|
2406
|
+
let html = "";
|
|
2407
|
+
if (content.title) {
|
|
2408
|
+
const titleColor = content.fields.find((f) => f.color)?.color;
|
|
2409
|
+
html += '<div class="viz-tooltip-header">';
|
|
2410
|
+
if (titleColor) {
|
|
2411
|
+
html += `<span class="viz-tooltip-dot" style="background:${esc(titleColor)}"></span>`;
|
|
2412
|
+
}
|
|
2413
|
+
html += `<span class="viz-tooltip-title">${esc(content.title)}</span>`;
|
|
2414
|
+
html += "</div>";
|
|
2396
2415
|
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
html += `<span class="viz-tooltip-value">${esc(field.value)}</span>`;
|
|
2416
|
+
if (content.fields.length > 0) {
|
|
2417
|
+
html += '<div class="viz-tooltip-body">';
|
|
2418
|
+
for (const field of content.fields) {
|
|
2419
|
+
html += '<div class="viz-tooltip-row">';
|
|
2420
|
+
html += `<span class="viz-tooltip-label">${esc(field.label)}</span>`;
|
|
2421
|
+
html += `<span class="viz-tooltip-value">${esc(field.value)}</span>`;
|
|
2422
|
+
html += "</div>";
|
|
2423
|
+
}
|
|
2406
2424
|
html += "</div>";
|
|
2407
2425
|
}
|
|
2408
|
-
|
|
2426
|
+
tooltip.innerHTML = html;
|
|
2409
2427
|
}
|
|
2410
|
-
tooltip.innerHTML = html;
|
|
2411
2428
|
tooltip.style.display = "block";
|
|
2412
|
-
const
|
|
2413
|
-
const
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2429
|
+
const positionId = ++currentPositionId;
|
|
2430
|
+
const virtualRef = {
|
|
2431
|
+
getBoundingClientRect() {
|
|
2432
|
+
const rect = container.getBoundingClientRect();
|
|
2433
|
+
return {
|
|
2434
|
+
x: rect.left + x3,
|
|
2435
|
+
y: rect.top + y3,
|
|
2436
|
+
width: 0,
|
|
2437
|
+
height: 0,
|
|
2438
|
+
top: rect.top + y3,
|
|
2439
|
+
left: rect.left + x3,
|
|
2440
|
+
right: rect.left + x3,
|
|
2441
|
+
bottom: rect.top + y3
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
};
|
|
2445
|
+
computePosition(virtualRef, tooltip, {
|
|
2446
|
+
placement: "bottom-start",
|
|
2447
|
+
middleware: [offset(TOOLTIP_OFFSET), flip(), shift({ padding: 5 })]
|
|
2448
|
+
}).then(({ x: fx, y: fy }) => {
|
|
2449
|
+
if (positionId !== currentPositionId) return;
|
|
2450
|
+
tooltip.style.left = `${fx}px`;
|
|
2451
|
+
tooltip.style.top = `${fy}px`;
|
|
2452
|
+
});
|
|
2426
2453
|
}
|
|
2427
2454
|
function hide() {
|
|
2428
2455
|
tooltip.style.display = "none";
|
|
2456
|
+
lastContentKey = "";
|
|
2429
2457
|
}
|
|
2430
2458
|
function destroy() {
|
|
2431
2459
|
document.removeEventListener("touchstart", handleDocumentTouch);
|
|
@@ -2436,7 +2464,7 @@ function createTooltipManager(container) {
|
|
|
2436
2464
|
return { show, hide, destroy };
|
|
2437
2465
|
}
|
|
2438
2466
|
function esc(str) {
|
|
2439
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2467
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2440
2468
|
}
|
|
2441
2469
|
|
|
2442
2470
|
// src/graph-mount.ts
|
|
@@ -2467,6 +2495,8 @@ function createGraph(container, spec, options) {
|
|
|
2467
2495
|
let positionedNodes = [];
|
|
2468
2496
|
let positionedEdges = [];
|
|
2469
2497
|
let adjacencyMap = /* @__PURE__ */ new Map();
|
|
2498
|
+
let nodeDataMap = /* @__PURE__ */ new Map();
|
|
2499
|
+
let edgeDataMap = /* @__PURE__ */ new Map();
|
|
2470
2500
|
let hoveredNodeId = null;
|
|
2471
2501
|
let hoveredEdgeId = null;
|
|
2472
2502
|
let selectedNodeIds = /* @__PURE__ */ new Set();
|
|
@@ -2474,6 +2504,7 @@ function createGraph(container, spec, options) {
|
|
|
2474
2504
|
let needsRender = false;
|
|
2475
2505
|
let isGesturing = false;
|
|
2476
2506
|
let gestureTimeout = null;
|
|
2507
|
+
let lastEdgeHitTime = 0;
|
|
2477
2508
|
function markGesture() {
|
|
2478
2509
|
isGesturing = true;
|
|
2479
2510
|
if (gestureTimeout !== null) clearTimeout(gestureTimeout);
|
|
@@ -2502,6 +2533,10 @@ function createGraph(container, spec, options) {
|
|
|
2502
2533
|
};
|
|
2503
2534
|
return compileGraph(currentSpec, compileOpts);
|
|
2504
2535
|
}
|
|
2536
|
+
function buildDataMaps() {
|
|
2537
|
+
nodeDataMap = new Map(compilation.nodes.map((n) => [n.id, n.data ?? {}]));
|
|
2538
|
+
edgeDataMap = new Map(compilation.edges.map((e) => [`${e.source}->${e.target}`, e.data ?? {}]));
|
|
2539
|
+
}
|
|
2505
2540
|
function buildAdjacencyMap(edges) {
|
|
2506
2541
|
const map = /* @__PURE__ */ new Map();
|
|
2507
2542
|
for (const edge of edges) {
|
|
@@ -2526,8 +2561,7 @@ function createGraph(container, spec, options) {
|
|
|
2526
2561
|
}));
|
|
2527
2562
|
}
|
|
2528
2563
|
function nodeDataById(nodeId) {
|
|
2529
|
-
|
|
2530
|
-
return node?.data ?? {};
|
|
2564
|
+
return nodeDataMap.get(nodeId) ?? {};
|
|
2531
2565
|
}
|
|
2532
2566
|
function pointToSegmentDist(px, py, ax, ay, bx, by) {
|
|
2533
2567
|
const dx = bx - ax;
|
|
@@ -2557,9 +2591,7 @@ function createGraph(container, spec, options) {
|
|
|
2557
2591
|
return bestEdgeId;
|
|
2558
2592
|
}
|
|
2559
2593
|
function edgeDataById(edgeId) {
|
|
2560
|
-
|
|
2561
|
-
const edge = compilation.edges.find((e) => e.source === source && e.target === target);
|
|
2562
|
-
return edge?.data ?? null;
|
|
2594
|
+
return edgeDataMap.get(edgeId) ?? null;
|
|
2563
2595
|
}
|
|
2564
2596
|
function createDOM() {
|
|
2565
2597
|
const { width, height } = getContainerDimensions();
|
|
@@ -2677,9 +2709,9 @@ function createGraph(container, spec, options) {
|
|
|
2677
2709
|
scheduleRender();
|
|
2678
2710
|
});
|
|
2679
2711
|
simulation.onSettled(() => {
|
|
2680
|
-
if (canvas && positionedNodes.length > 0 && interactionManager) {
|
|
2712
|
+
if (canvas && positionedNodes.length > 0 && interactionManager && renderer) {
|
|
2681
2713
|
const { width: cw, height: ch } = getCanvasDimensions();
|
|
2682
|
-
const fitTransform = ZoomTransform.fitBounds(positionedNodes, cw, ch);
|
|
2714
|
+
const { transform: fitTransform } = ZoomTransform.fitBounds(positionedNodes, cw, ch);
|
|
2683
2715
|
interactionManager.setTransform(fitTransform);
|
|
2684
2716
|
needsRender = true;
|
|
2685
2717
|
scheduleRender();
|
|
@@ -2757,6 +2789,18 @@ function createGraph(container, spec, options) {
|
|
|
2757
2789
|
}
|
|
2758
2790
|
},
|
|
2759
2791
|
onBackgroundHover(graphX, graphY, screenX, screenY) {
|
|
2792
|
+
const now2 = performance.now();
|
|
2793
|
+
if (now2 - lastEdgeHitTime < 32) {
|
|
2794
|
+
if (hoveredEdgeId) {
|
|
2795
|
+
hoveredEdgeId = null;
|
|
2796
|
+
needsRender = true;
|
|
2797
|
+
scheduleRender();
|
|
2798
|
+
options?.onEdgeHover?.(null);
|
|
2799
|
+
tooltipManager?.hide();
|
|
2800
|
+
}
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
lastEdgeHitTime = now2;
|
|
2760
2804
|
const transform = interactionManager?.getTransform();
|
|
2761
2805
|
const threshold = 5 / (transform?.k ?? 1);
|
|
2762
2806
|
const edgeId = hitTestEdge(graphX, graphY, threshold);
|
|
@@ -2858,7 +2902,7 @@ function createGraph(container, spec, options) {
|
|
|
2858
2902
|
function zoomToFit() {
|
|
2859
2903
|
if (destroyed || !interactionManager || positionedNodes.length === 0) return;
|
|
2860
2904
|
const { width: cw, height: ch } = getCanvasDimensions();
|
|
2861
|
-
const fitTransform = ZoomTransform.fitBounds(positionedNodes, cw, ch);
|
|
2905
|
+
const { transform: fitTransform } = ZoomTransform.fitBounds(positionedNodes, cw, ch);
|
|
2862
2906
|
interactionManager.setTransform(fitTransform);
|
|
2863
2907
|
needsRender = true;
|
|
2864
2908
|
scheduleRender();
|
|
@@ -2901,6 +2945,7 @@ function createGraph(container, spec, options) {
|
|
|
2901
2945
|
teardownSubsystems();
|
|
2902
2946
|
compilation = compile();
|
|
2903
2947
|
adjacencyMap = buildAdjacencyMap(compilation.edges);
|
|
2948
|
+
buildDataMaps();
|
|
2904
2949
|
renderChrome2();
|
|
2905
2950
|
renderLegend2();
|
|
2906
2951
|
initSimulation();
|
|
@@ -2919,6 +2964,7 @@ function createGraph(container, spec, options) {
|
|
|
2919
2964
|
}
|
|
2920
2965
|
compilation = compile();
|
|
2921
2966
|
adjacencyMap = buildAdjacencyMap(compilation.edges);
|
|
2967
|
+
buildDataMaps();
|
|
2922
2968
|
positionedNodes = compilation.nodes.map((node) => {
|
|
2923
2969
|
const pos = posMap.get(node.id) ?? { x: 0, y: 0 };
|
|
2924
2970
|
return { ...node, x: pos.x, y: pos.y };
|
|
@@ -2981,6 +3027,7 @@ function createGraph(container, spec, options) {
|
|
|
2981
3027
|
try {
|
|
2982
3028
|
compilation = compile();
|
|
2983
3029
|
adjacencyMap = buildAdjacencyMap(compilation.edges);
|
|
3030
|
+
buildDataMaps();
|
|
2984
3031
|
createDOM();
|
|
2985
3032
|
initSimulation();
|
|
2986
3033
|
initInteraction();
|
|
@@ -3675,18 +3722,36 @@ function renderLegend(parent, legend) {
|
|
|
3675
3722
|
let offsetY = legend.bounds.y;
|
|
3676
3723
|
for (let i = 0; i < legend.entries.length; i++) {
|
|
3677
3724
|
const entry = legend.entries[i];
|
|
3725
|
+
if (isHorizontal && i > 0) {
|
|
3726
|
+
const labelWidth = estimateTextWidth(
|
|
3727
|
+
entry.label,
|
|
3728
|
+
legend.labelStyle.fontSize,
|
|
3729
|
+
legend.labelStyle.fontWeight
|
|
3730
|
+
);
|
|
3731
|
+
const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
|
|
3732
|
+
if (offsetX + entryWidth > legend.bounds.x + legend.bounds.width) {
|
|
3733
|
+
offsetX = legend.bounds.x;
|
|
3734
|
+
offsetY += legend.swatchSize + 6;
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3678
3737
|
const entryG = createSVGElement("g");
|
|
3679
3738
|
entryG.setAttribute("class", "viz-legend-entry");
|
|
3680
3739
|
entryG.setAttribute("role", "listitem");
|
|
3681
3740
|
entryG.setAttribute("data-legend-index", String(i));
|
|
3682
3741
|
entryG.setAttribute("data-legend-label", entry.label);
|
|
3683
|
-
|
|
3684
|
-
"
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3742
|
+
if (entry.overflow) {
|
|
3743
|
+
entryG.setAttribute("data-legend-overflow", "true");
|
|
3744
|
+
entryG.setAttribute("aria-label", entry.label);
|
|
3745
|
+
entryG.setAttribute("opacity", "0.5");
|
|
3746
|
+
} else {
|
|
3747
|
+
entryG.setAttribute(
|
|
3748
|
+
"aria-label",
|
|
3749
|
+
`${entry.label}: ${entry.active !== false ? "visible" : "hidden"}`
|
|
3750
|
+
);
|
|
3751
|
+
entryG.setAttribute("style", "cursor: pointer");
|
|
3752
|
+
if (entry.active === false) {
|
|
3753
|
+
entryG.setAttribute("opacity", "0.3");
|
|
3754
|
+
}
|
|
3690
3755
|
}
|
|
3691
3756
|
if (entry.shape === "circle") {
|
|
3692
3757
|
const circle = createSVGElement("circle");
|
|
@@ -3746,10 +3811,6 @@ function renderLegend(parent, legend) {
|
|
|
3746
3811
|
);
|
|
3747
3812
|
const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
|
|
3748
3813
|
offsetX += entryWidth;
|
|
3749
|
-
if (offsetX > legend.bounds.x + legend.bounds.width && i < legend.entries.length - 1) {
|
|
3750
|
-
offsetX = legend.bounds.x;
|
|
3751
|
-
offsetY += legend.swatchSize + 6;
|
|
3752
|
-
}
|
|
3753
3814
|
} else {
|
|
3754
3815
|
offsetY += legend.swatchSize + legend.entryGap;
|
|
3755
3816
|
}
|
|
@@ -4538,6 +4599,7 @@ function wireLegendInteraction(svg, _layout, onLegendToggle, onEdit) {
|
|
|
4538
4599
|
const cleanups = [];
|
|
4539
4600
|
const hiddenSeries = /* @__PURE__ */ new Set();
|
|
4540
4601
|
for (const entry of legendEntries) {
|
|
4602
|
+
if (entry.getAttribute("data-legend-overflow") === "true") continue;
|
|
4541
4603
|
const handleClick = () => {
|
|
4542
4604
|
const label = entry.getAttribute("data-legend-label");
|
|
4543
4605
|
if (!label) return;
|
|
@@ -4711,6 +4773,7 @@ function createChart(container, spec, options) {
|
|
|
4711
4773
|
let destroyed = false;
|
|
4712
4774
|
let isDragging = false;
|
|
4713
4775
|
let pendingRender = false;
|
|
4776
|
+
let resizeTimer = null;
|
|
4714
4777
|
const measureText = createMeasureText();
|
|
4715
4778
|
function compile() {
|
|
4716
4779
|
const { width, height } = getContainerDimensions();
|
|
@@ -4869,6 +4932,10 @@ function createChart(container, spec, options) {
|
|
|
4869
4932
|
function destroy() {
|
|
4870
4933
|
if (destroyed) return;
|
|
4871
4934
|
destroyed = true;
|
|
4935
|
+
if (resizeTimer !== null) {
|
|
4936
|
+
clearTimeout(resizeTimer);
|
|
4937
|
+
resizeTimer = null;
|
|
4938
|
+
}
|
|
4872
4939
|
if (cleanupTooltipEvents) {
|
|
4873
4940
|
cleanupTooltipEvents();
|
|
4874
4941
|
cleanupTooltipEvents = null;
|
|
@@ -4915,7 +4982,11 @@ function createChart(container, spec, options) {
|
|
|
4915
4982
|
render();
|
|
4916
4983
|
if (options?.responsive !== false) {
|
|
4917
4984
|
disconnectResize = observeResize(container, () => {
|
|
4918
|
-
|
|
4985
|
+
if (resizeTimer !== null) clearTimeout(resizeTimer);
|
|
4986
|
+
resizeTimer = setTimeout(() => {
|
|
4987
|
+
resizeTimer = null;
|
|
4988
|
+
resize();
|
|
4989
|
+
}, 100);
|
|
4919
4990
|
});
|
|
4920
4991
|
}
|
|
4921
4992
|
return {
|