@opendata-ai/openchart-vanilla 2.10.0 → 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 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, tap-outside-to-hide.
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
- let html = "";
2401
- if (content.title) {
2402
- const titleColor = content.fields.find((f) => f.color)?.color;
2403
- html += '<div class="viz-tooltip-header">';
2404
- if (titleColor) {
2405
- html += `<span class="viz-tooltip-dot" style="background:${esc(titleColor)}"></span>`;
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
- html += `<span class="viz-tooltip-title">${esc(content.title)}</span>`;
2408
- html += "</div>";
2409
- }
2410
- if (content.fields.length > 0) {
2411
- html += '<div class="viz-tooltip-body">';
2412
- for (const field of content.fields) {
2413
- html += '<div class="viz-tooltip-row">';
2414
- html += `<span class="viz-tooltip-label">${esc(field.label)}</span>`;
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
- html += "</div>";
2426
+ tooltip.innerHTML = html;
2419
2427
  }
2420
- tooltip.innerHTML = html;
2421
2428
  tooltip.style.display = "block";
2422
- const containerRect = container.getBoundingClientRect();
2423
- const tooltipRect = tooltip.getBoundingClientRect();
2424
- let left = x3 + TOOLTIP_OFFSET;
2425
- let top = y3 + TOOLTIP_OFFSET;
2426
- if (left + tooltipRect.width > containerRect.width) {
2427
- left = x3 - tooltipRect.width - TOOLTIP_OFFSET;
2428
- }
2429
- if (top + tooltipRect.height > containerRect.height) {
2430
- top = y3 - tooltipRect.height - TOOLTIP_OFFSET;
2431
- }
2432
- left = Math.max(0, Math.min(left, containerRect.width - tooltipRect.width));
2433
- top = Math.max(0, Math.min(top, containerRect.height - tooltipRect.height));
2434
- tooltip.style.left = `${left}px`;
2435
- tooltip.style.top = `${top}px`;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2467
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
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 selfResizing = false;
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
- const node = compilation.nodes.find((n) => n.id === nodeId);
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
- const [source, target] = edgeId.split("->");
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, contentHeight } = ZoomTransform.fitBounds(
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 || selfResizing) return;
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
- resize();
4985
+ if (resizeTimer !== null) clearTimeout(resizeTimer);
4986
+ resizeTimer = setTimeout(() => {
4987
+ resizeTimer = null;
4988
+ resize();
4989
+ }, 100);
4956
4990
  });
4957
4991
  }
4958
4992
  return {