@opendata-ai/openchart-vanilla 2.10.0 → 2.12.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 +98 -64
- 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-mount.ts +30 -38
- package/src/mount.ts +11 -2
- package/src/svg-renderer.ts +14 -5
- 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
|
@@ -2383,6 +2383,7 @@ function observeResize(container, callback) {
|
|
|
2383
2383
|
}
|
|
2384
2384
|
|
|
2385
2385
|
// src/tooltip.ts
|
|
2386
|
+
import { computePosition, flip, offset, shift } from "@floating-ui/dom";
|
|
2386
2387
|
var TOOLTIP_OFFSET = 12;
|
|
2387
2388
|
function createTooltipManager(container) {
|
|
2388
2389
|
const tooltip = document.createElement("div");
|
|
@@ -2390,6 +2391,8 @@ function createTooltipManager(container) {
|
|
|
2390
2391
|
tooltip.setAttribute("role", "tooltip");
|
|
2391
2392
|
container.style.position = container.style.position || "relative";
|
|
2392
2393
|
container.appendChild(tooltip);
|
|
2394
|
+
let lastContentKey = "";
|
|
2395
|
+
let currentPositionId = 0;
|
|
2393
2396
|
const handleDocumentTouch = (e) => {
|
|
2394
2397
|
if (!container.contains(e.target)) {
|
|
2395
2398
|
hide();
|
|
@@ -2397,45 +2400,60 @@ function createTooltipManager(container) {
|
|
|
2397
2400
|
};
|
|
2398
2401
|
document.addEventListener("touchstart", handleDocumentTouch);
|
|
2399
2402
|
function show(content, x3, y3) {
|
|
2400
|
-
|
|
2401
|
-
if (
|
|
2402
|
-
|
|
2403
|
-
html
|
|
2404
|
-
if (
|
|
2405
|
-
|
|
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>";
|
|
2406
2415
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
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
|
+
}
|
|
2416
2424
|
html += "</div>";
|
|
2417
2425
|
}
|
|
2418
|
-
|
|
2426
|
+
tooltip.innerHTML = html;
|
|
2419
2427
|
}
|
|
2420
|
-
tooltip.innerHTML = html;
|
|
2421
2428
|
tooltip.style.display = "block";
|
|
2422
|
-
const
|
|
2423
|
-
const
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
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
|
+
});
|
|
2436
2453
|
}
|
|
2437
2454
|
function hide() {
|
|
2438
2455
|
tooltip.style.display = "none";
|
|
2456
|
+
lastContentKey = "";
|
|
2439
2457
|
}
|
|
2440
2458
|
function destroy() {
|
|
2441
2459
|
document.removeEventListener("touchstart", handleDocumentTouch);
|
|
@@ -2446,7 +2464,7 @@ function createTooltipManager(container) {
|
|
|
2446
2464
|
return { show, hide, destroy };
|
|
2447
2465
|
}
|
|
2448
2466
|
function esc(str) {
|
|
2449
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2467
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2450
2468
|
}
|
|
2451
2469
|
|
|
2452
2470
|
// src/graph-mount.ts
|
|
@@ -2477,6 +2495,8 @@ function createGraph(container, spec, options) {
|
|
|
2477
2495
|
let positionedNodes = [];
|
|
2478
2496
|
let positionedEdges = [];
|
|
2479
2497
|
let adjacencyMap = /* @__PURE__ */ new Map();
|
|
2498
|
+
let nodeDataMap = /* @__PURE__ */ new Map();
|
|
2499
|
+
let edgeDataMap = /* @__PURE__ */ new Map();
|
|
2480
2500
|
let hoveredNodeId = null;
|
|
2481
2501
|
let hoveredEdgeId = null;
|
|
2482
2502
|
let selectedNodeIds = /* @__PURE__ */ new Set();
|
|
@@ -2484,7 +2504,7 @@ function createGraph(container, spec, options) {
|
|
|
2484
2504
|
let needsRender = false;
|
|
2485
2505
|
let isGesturing = false;
|
|
2486
2506
|
let gestureTimeout = null;
|
|
2487
|
-
let
|
|
2507
|
+
let lastEdgeHitTime = 0;
|
|
2488
2508
|
function markGesture() {
|
|
2489
2509
|
isGesturing = true;
|
|
2490
2510
|
if (gestureTimeout !== null) clearTimeout(gestureTimeout);
|
|
@@ -2513,6 +2533,10 @@ function createGraph(container, spec, options) {
|
|
|
2513
2533
|
};
|
|
2514
2534
|
return compileGraph(currentSpec, compileOpts);
|
|
2515
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
|
+
}
|
|
2516
2540
|
function buildAdjacencyMap(edges) {
|
|
2517
2541
|
const map = /* @__PURE__ */ new Map();
|
|
2518
2542
|
for (const edge of edges) {
|
|
@@ -2537,8 +2561,7 @@ function createGraph(container, spec, options) {
|
|
|
2537
2561
|
}));
|
|
2538
2562
|
}
|
|
2539
2563
|
function nodeDataById(nodeId) {
|
|
2540
|
-
|
|
2541
|
-
return node?.data ?? {};
|
|
2564
|
+
return nodeDataMap.get(nodeId) ?? {};
|
|
2542
2565
|
}
|
|
2543
2566
|
function pointToSegmentDist(px, py, ax, ay, bx, by) {
|
|
2544
2567
|
const dx = bx - ax;
|
|
@@ -2568,9 +2591,7 @@ function createGraph(container, spec, options) {
|
|
|
2568
2591
|
return bestEdgeId;
|
|
2569
2592
|
}
|
|
2570
2593
|
function edgeDataById(edgeId) {
|
|
2571
|
-
|
|
2572
|
-
const edge = compilation.edges.find((e) => e.source === source && e.target === target);
|
|
2573
|
-
return edge?.data ?? null;
|
|
2594
|
+
return edgeDataMap.get(edgeId) ?? null;
|
|
2574
2595
|
}
|
|
2575
2596
|
function createDOM() {
|
|
2576
2597
|
const { width, height } = getContainerDimensions();
|
|
@@ -2690,26 +2711,8 @@ function createGraph(container, spec, options) {
|
|
|
2690
2711
|
simulation.onSettled(() => {
|
|
2691
2712
|
if (canvas && positionedNodes.length > 0 && interactionManager && renderer) {
|
|
2692
2713
|
const { width: cw, height: ch } = getCanvasDimensions();
|
|
2693
|
-
const { transform: fitTransform
|
|
2694
|
-
positionedNodes,
|
|
2695
|
-
cw,
|
|
2696
|
-
ch
|
|
2697
|
-
);
|
|
2714
|
+
const { transform: fitTransform } = ZoomTransform.fitBounds(positionedNodes, cw, ch);
|
|
2698
2715
|
interactionManager.setTransform(fitTransform);
|
|
2699
|
-
const chromeH = chromeEl?.getBoundingClientRect().height || 0;
|
|
2700
|
-
const totalContentHeight = Math.ceil(contentHeight) + chromeH;
|
|
2701
|
-
const containerH = container.getBoundingClientRect().height;
|
|
2702
|
-
if (totalContentHeight < containerH) {
|
|
2703
|
-
selfResizing = true;
|
|
2704
|
-
const targetCanvasH = Math.ceil(contentHeight);
|
|
2705
|
-
renderer.resize(cw, targetCanvasH);
|
|
2706
|
-
const refit = ZoomTransform.fitBounds(positionedNodes, cw, targetCanvasH);
|
|
2707
|
-
interactionManager.setTransform(refit.transform);
|
|
2708
|
-
container.style.height = "fit-content";
|
|
2709
|
-
setTimeout(() => {
|
|
2710
|
-
selfResizing = false;
|
|
2711
|
-
}, 100);
|
|
2712
|
-
}
|
|
2713
2716
|
needsRender = true;
|
|
2714
2717
|
scheduleRender();
|
|
2715
2718
|
}
|
|
@@ -2786,6 +2789,18 @@ function createGraph(container, spec, options) {
|
|
|
2786
2789
|
}
|
|
2787
2790
|
},
|
|
2788
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;
|
|
2789
2804
|
const transform = interactionManager?.getTransform();
|
|
2790
2805
|
const threshold = 5 / (transform?.k ?? 1);
|
|
2791
2806
|
const edgeId = hitTestEdge(graphX, graphY, threshold);
|
|
@@ -2916,8 +2931,7 @@ function createGraph(container, spec, options) {
|
|
|
2916
2931
|
return [...selectedNodeIds];
|
|
2917
2932
|
}
|
|
2918
2933
|
function doResize() {
|
|
2919
|
-
if (destroyed || !canvas || !renderer || !wrapper
|
|
2920
|
-
container.style.height = "";
|
|
2934
|
+
if (destroyed || !canvas || !renderer || !wrapper) return;
|
|
2921
2935
|
const { width, height } = getContainerDimensions();
|
|
2922
2936
|
const chromeHeight = chromeEl?.getBoundingClientRect().height || 0;
|
|
2923
2937
|
const canvasHeight = Math.max(height - chromeHeight, 200);
|
|
@@ -2931,6 +2945,7 @@ function createGraph(container, spec, options) {
|
|
|
2931
2945
|
teardownSubsystems();
|
|
2932
2946
|
compilation = compile();
|
|
2933
2947
|
adjacencyMap = buildAdjacencyMap(compilation.edges);
|
|
2948
|
+
buildDataMaps();
|
|
2934
2949
|
renderChrome2();
|
|
2935
2950
|
renderLegend2();
|
|
2936
2951
|
initSimulation();
|
|
@@ -2949,6 +2964,7 @@ function createGraph(container, spec, options) {
|
|
|
2949
2964
|
}
|
|
2950
2965
|
compilation = compile();
|
|
2951
2966
|
adjacencyMap = buildAdjacencyMap(compilation.edges);
|
|
2967
|
+
buildDataMaps();
|
|
2952
2968
|
positionedNodes = compilation.nodes.map((node) => {
|
|
2953
2969
|
const pos = posMap.get(node.id) ?? { x: 0, y: 0 };
|
|
2954
2970
|
return { ...node, x: pos.x, y: pos.y };
|
|
@@ -3011,6 +3027,7 @@ function createGraph(container, spec, options) {
|
|
|
3011
3027
|
try {
|
|
3012
3028
|
compilation = compile();
|
|
3013
3029
|
adjacencyMap = buildAdjacencyMap(compilation.edges);
|
|
3030
|
+
buildDataMaps();
|
|
3014
3031
|
createDOM();
|
|
3015
3032
|
initSimulation();
|
|
3016
3033
|
initInteraction();
|
|
@@ -3705,6 +3722,18 @@ function renderLegend(parent, legend) {
|
|
|
3705
3722
|
let offsetY = legend.bounds.y;
|
|
3706
3723
|
for (let i = 0; i < legend.entries.length; i++) {
|
|
3707
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
|
+
}
|
|
3708
3737
|
const entryG = createSVGElement("g");
|
|
3709
3738
|
entryG.setAttribute("class", "viz-legend-entry");
|
|
3710
3739
|
entryG.setAttribute("role", "listitem");
|
|
@@ -3782,10 +3811,6 @@ function renderLegend(parent, legend) {
|
|
|
3782
3811
|
);
|
|
3783
3812
|
const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
|
|
3784
3813
|
offsetX += entryWidth;
|
|
3785
|
-
if (offsetX > legend.bounds.x + legend.bounds.width && i < legend.entries.length - 1) {
|
|
3786
|
-
offsetX = legend.bounds.x;
|
|
3787
|
-
offsetY += legend.swatchSize + 6;
|
|
3788
|
-
}
|
|
3789
3814
|
} else {
|
|
3790
3815
|
offsetY += legend.swatchSize + legend.entryGap;
|
|
3791
3816
|
}
|
|
@@ -4748,6 +4773,7 @@ function createChart(container, spec, options) {
|
|
|
4748
4773
|
let destroyed = false;
|
|
4749
4774
|
let isDragging = false;
|
|
4750
4775
|
let pendingRender = false;
|
|
4776
|
+
let resizeTimer = null;
|
|
4751
4777
|
const measureText = createMeasureText();
|
|
4752
4778
|
function compile() {
|
|
4753
4779
|
const { width, height } = getContainerDimensions();
|
|
@@ -4906,6 +4932,10 @@ function createChart(container, spec, options) {
|
|
|
4906
4932
|
function destroy() {
|
|
4907
4933
|
if (destroyed) return;
|
|
4908
4934
|
destroyed = true;
|
|
4935
|
+
if (resizeTimer !== null) {
|
|
4936
|
+
clearTimeout(resizeTimer);
|
|
4937
|
+
resizeTimer = null;
|
|
4938
|
+
}
|
|
4909
4939
|
if (cleanupTooltipEvents) {
|
|
4910
4940
|
cleanupTooltipEvents();
|
|
4911
4941
|
cleanupTooltipEvents = null;
|
|
@@ -4952,7 +4982,11 @@ function createChart(container, spec, options) {
|
|
|
4952
4982
|
render();
|
|
4953
4983
|
if (options?.responsive !== false) {
|
|
4954
4984
|
disconnectResize = observeResize(container, () => {
|
|
4955
|
-
|
|
4985
|
+
if (resizeTimer !== null) clearTimeout(resizeTimer);
|
|
4986
|
+
resizeTimer = setTimeout(() => {
|
|
4987
|
+
resizeTimer = null;
|
|
4988
|
+
resize();
|
|
4989
|
+
}, 100);
|
|
4956
4990
|
});
|
|
4957
4991
|
}
|
|
4958
4992
|
return {
|