polly-graph 0.1.8 → 0.1.10

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.cjs CHANGED
@@ -2662,15 +2662,15 @@ function defaultWheelDelta(event) {
2662
2662
  function defaultTouchable2() {
2663
2663
  return navigator.maxTouchPoints || "ontouchstart" in this;
2664
2664
  }
2665
- function defaultConstrain(transform2, extent2, translateExtent) {
2666
- var dx0 = transform2.invertX(extent2[0][0]) - translateExtent[0][0], dx1 = transform2.invertX(extent2[1][0]) - translateExtent[1][0], dy0 = transform2.invertY(extent2[0][1]) - translateExtent[0][1], dy1 = transform2.invertY(extent2[1][1]) - translateExtent[1][1];
2665
+ function defaultConstrain(transform2, extent, translateExtent) {
2666
+ var dx0 = transform2.invertX(extent[0][0]) - translateExtent[0][0], dx1 = transform2.invertX(extent[1][0]) - translateExtent[1][0], dy0 = transform2.invertY(extent[0][1]) - translateExtent[0][1], dy1 = transform2.invertY(extent[1][1]) - translateExtent[1][1];
2667
2667
  return transform2.translate(
2668
2668
  dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),
2669
2669
  dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)
2670
2670
  );
2671
2671
  }
2672
2672
  function zoom_default2() {
2673
- var filter2 = defaultFilter2, extent2 = defaultExtent, constrain = defaultConstrain, wheelDelta = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default, listeners = dispatch_default2("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
2673
+ var filter2 = defaultFilter2, extent = defaultExtent, constrain = defaultConstrain, wheelDelta = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default, listeners = dispatch_default2("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
2674
2674
  function zoom(selection2) {
2675
2675
  selection2.property("__zoom", defaultTransform).on("wheel.zoom", wheeled, { passive: false }).on("mousedown.zoom", mousedowned).on("dblclick.zoom", dblclicked).filter(touchable).on("touchstart.zoom", touchstarted).on("touchmove.zoom", touchmoved).on("touchend.zoom touchcancel.zoom", touchended).style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
2676
2676
  }
@@ -2693,7 +2693,7 @@ function zoom_default2() {
2693
2693
  };
2694
2694
  zoom.scaleTo = function(selection2, k, p, event) {
2695
2695
  zoom.transform(selection2, function() {
2696
- var e = extent2.apply(this, arguments), t0 = this.__zoom, p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p, p1 = t0.invert(p0), k1 = typeof k === "function" ? k.apply(this, arguments) : k;
2696
+ var e = extent.apply(this, arguments), t0 = this.__zoom, p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p, p1 = t0.invert(p0), k1 = typeof k === "function" ? k.apply(this, arguments) : k;
2697
2697
  return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);
2698
2698
  }, p, event);
2699
2699
  };
@@ -2702,12 +2702,12 @@ function zoom_default2() {
2702
2702
  return constrain(this.__zoom.translate(
2703
2703
  typeof x3 === "function" ? x3.apply(this, arguments) : x3,
2704
2704
  typeof y3 === "function" ? y3.apply(this, arguments) : y3
2705
- ), extent2.apply(this, arguments), translateExtent);
2705
+ ), extent.apply(this, arguments), translateExtent);
2706
2706
  }, null, event);
2707
2707
  };
2708
2708
  zoom.translateTo = function(selection2, x3, y3, p, event) {
2709
2709
  zoom.transform(selection2, function() {
2710
- var e = extent2.apply(this, arguments), t = this.__zoom, p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p;
2710
+ var e = extent.apply(this, arguments), t = this.__zoom, p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p;
2711
2711
  return constrain(identity2.translate(p0[0], p0[1]).scale(t.k).translate(
2712
2712
  typeof x3 === "function" ? -x3.apply(this, arguments) : -x3,
2713
2713
  typeof y3 === "function" ? -y3.apply(this, arguments) : -y3
@@ -2722,8 +2722,8 @@ function zoom_default2() {
2722
2722
  var x3 = p0[0] - p1[0] * transform2.k, y3 = p0[1] - p1[1] * transform2.k;
2723
2723
  return x3 === transform2.x && y3 === transform2.y ? transform2 : new Transform(transform2.k, x3, y3);
2724
2724
  }
2725
- function centroid(extent3) {
2726
- return [(+extent3[0][0] + +extent3[1][0]) / 2, (+extent3[0][1] + +extent3[1][1]) / 2];
2725
+ function centroid(extent2) {
2726
+ return [(+extent2[0][0] + +extent2[1][0]) / 2, (+extent2[0][1] + +extent2[1][1]) / 2];
2727
2727
  }
2728
2728
  function schedule(transition2, transform2, point, event) {
2729
2729
  transition2.on("start.zoom", function() {
@@ -2731,7 +2731,7 @@ function zoom_default2() {
2731
2731
  }).on("interrupt.zoom end.zoom", function() {
2732
2732
  gesture(this, arguments).event(event).end();
2733
2733
  }).tween("zoom", function() {
2734
- var that = this, args = arguments, g = gesture(that, args).event(event), e = extent2.apply(that, args), p = point == null ? centroid(e) : typeof point === "function" ? point.apply(that, args) : point, w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]), a2 = that.__zoom, b = typeof transform2 === "function" ? transform2.apply(that, args) : transform2, i = interpolate(a2.invert(p).concat(w / a2.k), b.invert(p).concat(w / b.k));
2734
+ var that = this, args = arguments, g = gesture(that, args).event(event), e = extent.apply(that, args), p = point == null ? centroid(e) : typeof point === "function" ? point.apply(that, args) : point, w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]), a2 = that.__zoom, b = typeof transform2 === "function" ? transform2.apply(that, args) : transform2, i = interpolate(a2.invert(p).concat(w / a2.k), b.invert(p).concat(w / b.k));
2735
2735
  return function(t) {
2736
2736
  if (t === 1) t = b;
2737
2737
  else {
@@ -2750,7 +2750,7 @@ function zoom_default2() {
2750
2750
  this.args = args;
2751
2751
  this.active = 0;
2752
2752
  this.sourceEvent = null;
2753
- this.extent = extent2.apply(that, args);
2753
+ this.extent = extent.apply(that, args);
2754
2754
  this.taps = 0;
2755
2755
  }
2756
2756
  Gesture.prototype = {
@@ -2843,7 +2843,7 @@ function zoom_default2() {
2843
2843
  }
2844
2844
  function dblclicked(event, ...args) {
2845
2845
  if (!filter2.apply(this, arguments)) return;
2846
- var t0 = this.__zoom, p0 = pointer_default(event.changedTouches ? event.changedTouches[0] : event, this), p1 = t0.invert(p0), k1 = t0.k * (event.shiftKey ? 0.5 : 2), t1 = constrain(translate(scale(t0, k1), p0, p1), extent2.apply(this, args), translateExtent);
2846
+ var t0 = this.__zoom, p0 = pointer_default(event.changedTouches ? event.changedTouches[0] : event, this), p1 = t0.invert(p0), k1 = t0.k * (event.shiftKey ? 0.5 : 2), t1 = constrain(translate(scale(t0, k1), p0, p1), extent.apply(this, args), translateExtent);
2847
2847
  noevent_default2(event);
2848
2848
  if (duration > 0) select_default2(this).transition().duration(duration).call(schedule, t1, p0, event);
2849
2849
  else select_default2(this).call(zoom.transform, t1, p0, event);
@@ -2922,7 +2922,7 @@ function zoom_default2() {
2922
2922
  return arguments.length ? (touchable = typeof _ === "function" ? _ : constant_default4(!!_), zoom) : touchable;
2923
2923
  };
2924
2924
  zoom.extent = function(_) {
2925
- return arguments.length ? (extent2 = typeof _ === "function" ? _ : constant_default4([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent2;
2925
+ return arguments.length ? (extent = typeof _ === "function" ? _ : constant_default4([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;
2926
2926
  };
2927
2927
  zoom.scaleExtent = function(_) {
2928
2928
  return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];
@@ -2952,9 +2952,6 @@ function zoom_default2() {
2952
2952
  return zoom;
2953
2953
  }
2954
2954
 
2955
- // src/create-graph.ts
2956
- var import_d3 = require("d3");
2957
-
2958
2955
  // src/utils/timer-manager.ts
2959
2956
  var TimerManager = class {
2960
2957
  activeTimers = /* @__PURE__ */ new Map();
@@ -3341,7 +3338,6 @@ var GraphManager = class {
3341
3338
  linkMarkerSnapshots = null;
3342
3339
  rootSelection = null;
3343
3340
  simulationPaused = false;
3344
- needsImmediateFitView = false;
3345
3341
  /**
3346
3342
  * Initialize core managers
3347
3343
  */
@@ -3397,7 +3393,6 @@ var GraphManager = class {
3397
3393
  this.linkMarkerSnapshots = null;
3398
3394
  this.rootSelection = null;
3399
3395
  this.simulationPaused = false;
3400
- this.needsImmediateFitView = false;
3401
3396
  this.cleanupFunctions = [];
3402
3397
  }
3403
3398
  /**
@@ -4417,6 +4412,24 @@ function createGraphSimulation(config) {
4417
4412
  const warmupTicks = enhancedConfig.warmup?.ticks ?? (useAdaptive ? Math.min(100, nodeCount * 2) : 50);
4418
4413
  warmupSimulation(simulation, warmupTicks);
4419
4414
  }
4415
+ if (config.onReady && config.timerManager) {
4416
+ let readyCallbackFired = false;
4417
+ const handleTick = () => {
4418
+ if (!readyCallbackFired && simulation.alpha() < 0.1) {
4419
+ readyCallbackFired = true;
4420
+ simulation.on("tick.ready", null);
4421
+ config.timerManager.setTimeout("simulation-ready", () => {
4422
+ config.onReady?.();
4423
+ }, 100);
4424
+ }
4425
+ };
4426
+ const cleanup = () => {
4427
+ simulation.on("tick.ready", null);
4428
+ config.timerManager.clearTimer("simulation-ready");
4429
+ };
4430
+ simulation.on("tick.ready", handleTick);
4431
+ simulation.on("end.ready", cleanup);
4432
+ }
4420
4433
  return { simulation };
4421
4434
  }
4422
4435
  function seedNodePositions(nodes, containerWidth, containerHeight) {
@@ -4803,50 +4816,12 @@ function renderLinks(ctx, links) {
4803
4816
 
4804
4817
  // src/renderer/nodes.ts
4805
4818
  function renderNodes(ctx, nodes) {
4806
- return ctx.root.select('[data-layer="nodes"]').selectAll("circle").data(
4807
- nodes,
4808
- (d) => d.id
4809
- ).join("circle").attr(
4810
- "r",
4811
- (node) => node.style?.radius ?? 8
4812
- ).attr(
4813
- "fill",
4814
- (node) => node.style?.fill ?? "#6c5ce7"
4815
- ).attr(
4816
- "stroke",
4817
- (node) => node.style?.stroke ?? "#ffffff"
4818
- ).attr(
4819
- "stroke-width",
4820
- (node) => node.style?.strokeWidth ?? 1.5
4821
- ).attr(
4822
- "opacity",
4823
- (node) => node.style?.opacity ?? 1
4824
- );
4819
+ return ctx.root.select('[data-layer="nodes"]').selectAll("circle").data(nodes, (d) => d.id).join("circle").attr("r", (node) => node.style?.radius ?? 8).attr("fill", (node) => node.style?.fill ?? "#6c5ce7").attr("stroke", (node) => node.style?.stroke ?? "#ffffff").attr("stroke-width", (node) => node.style?.strokeWidth ?? 1.5).attr("opacity", (node) => node.style?.opacity ?? 1).style("cursor", "pointer");
4825
4820
  }
4826
4821
 
4827
4822
  // src/renderer/node-labels.ts
4828
4823
  function renderNodeLabels(ctx, nodes) {
4829
- const selection2 = ctx.root.select('[data-layer="node-labels"]').selectAll("text").data(
4830
- nodes,
4831
- (d) => d.id
4832
- ).join("text").attr(
4833
- "text-anchor",
4834
- "middle"
4835
- ).attr(
4836
- "dominant-baseline",
4837
- "middle"
4838
- ).attr(
4839
- "font-size",
4840
- 9
4841
- ).attr(
4842
- "fill",
4843
- (node) => node.style?.textColor ?? "#ffffff"
4844
- ).attr(
4845
- "pointer-events",
4846
- "none"
4847
- ).text(
4848
- (node) => node.label ?? node.id
4849
- );
4824
+ const selection2 = ctx.root.select('[data-layer="node-labels"]').selectAll("text").data(nodes, (d) => d.id).join("text").attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("font-size", 9).attr("fill", (node) => node.style?.textColor ?? "#ffffff").attr("pointer-events", "none").text((node) => node.label ?? node.id);
4850
4825
  selection2.each(function(node) {
4851
4826
  const textElement = this;
4852
4827
  const fullLabel = node.label ?? node.id;
@@ -4855,10 +4830,7 @@ function renderNodeLabels(ctx, nodes) {
4855
4830
  let truncatedLabel = fullLabel;
4856
4831
  textElement.textContent = truncatedLabel;
4857
4832
  while (truncatedLabel.length > 1 && textElement.getComputedTextLength() > maxWidth) {
4858
- truncatedLabel = truncatedLabel.slice(
4859
- 0,
4860
- -1
4861
- );
4833
+ truncatedLabel = truncatedLabel.slice(0, -1);
4862
4834
  textElement.textContent = `${truncatedLabel}\u2026`;
4863
4835
  }
4864
4836
  });
@@ -4992,10 +4964,12 @@ var RenderPipeline = class {
4992
4964
  }
4993
4965
  if (this.manager.timerManager && this.manager.fitViewCallback) {
4994
4966
  this.manager.timerManager.debounce("fit-view-resize", () => {
4995
- if (this.manager.fitViewCallback) {
4996
- this.manager.fitViewCallback();
4997
- }
4998
- }, 150);
4967
+ this.manager.timerManager.setTimeout("fit-view-layout", () => {
4968
+ if (this.manager.fitViewCallback) {
4969
+ this.manager.fitViewCallback();
4970
+ }
4971
+ }, 50);
4972
+ }, 200);
4999
4973
  }
5000
4974
  });
5001
4975
  this.manager.addCleanup(cleanupResize);
@@ -5065,15 +5039,23 @@ var RenderPipeline = class {
5065
5039
  config: this.manager.config.simulation
5066
5040
  };
5067
5041
  try {
5068
- const simulationResult = createGraphSimulation(simulationConfig);
5042
+ const simulationConfigWithCallback = {
5043
+ ...simulationConfig,
5044
+ onReady: () => {
5045
+ if (this.manager.fitViewCallback) {
5046
+ this.manager.fitViewCallback();
5047
+ }
5048
+ },
5049
+ timerManager: this.manager.timerManager ?? void 0
5050
+ };
5051
+ const simulationResult = createGraphSimulation(simulationConfigWithCallback);
5069
5052
  this.manager.simulation = simulationResult.simulation;
5070
5053
  const centerX = simulationConfig.width / 2;
5071
5054
  const centerY = simulationConfig.height / 2;
5072
5055
  this.manager.simulation.force("center", center_default(centerX, centerY));
5073
- if (simulationConfig.width > 0 && simulationConfig.height > 0) {
5056
+ if (simulationConfigWithCallback.width > 0 && simulationConfigWithCallback.height > 0) {
5074
5057
  this.manager.reheatSimulation(0.3);
5075
5058
  this.manager.simulationPaused = false;
5076
- this.manager.needsImmediateFitView = true;
5077
5059
  } else {
5078
5060
  this.manager.simulation.stop();
5079
5061
  this.manager.simulationPaused = true;
@@ -5132,14 +5114,35 @@ function createNodeHover(nodeSelection, hoverStyle) {
5132
5114
  if (hoverStyle) {
5133
5115
  nodeSelection.on("mouseenter.hover", function(_event, node) {
5134
5116
  const circle = this;
5117
+ if (circle.dataset.selected === "true") {
5118
+ return;
5119
+ }
5135
5120
  circle.setAttribute("stroke", hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff");
5136
5121
  circle.setAttribute("stroke-width", String(hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5));
5137
5122
  circle.setAttribute("opacity", String(hoverStyle.opacity ?? node.style?.opacity ?? 1));
5138
5123
  }).on("mouseleave.hover", function(_event, node) {
5139
5124
  const circle = this;
5140
- circle.setAttribute("stroke", node.style?.stroke ?? "#ffffff");
5141
- circle.setAttribute("stroke-width", String(node.style?.strokeWidth ?? 1.5));
5142
- circle.setAttribute("opacity", String(node.style?.opacity ?? 1));
5125
+ if (circle.dataset.selected === "true") {
5126
+ return;
5127
+ }
5128
+ circle.style.stroke = "";
5129
+ circle.style.strokeWidth = "";
5130
+ circle.style.opacity = "";
5131
+ if (!node.style?.stroke) {
5132
+ circle.setAttribute("stroke", "#ffffff");
5133
+ } else {
5134
+ circle.setAttribute("stroke", node.style.stroke);
5135
+ }
5136
+ if (!node.style?.strokeWidth) {
5137
+ circle.setAttribute("stroke-width", "1.5");
5138
+ } else {
5139
+ circle.setAttribute("stroke-width", String(node.style.strokeWidth));
5140
+ }
5141
+ if (!node.style?.opacity) {
5142
+ circle.setAttribute("opacity", "1");
5143
+ } else {
5144
+ circle.setAttribute("opacity", String(node.style.opacity));
5145
+ }
5143
5146
  });
5144
5147
  }
5145
5148
  const svgElement = firstNode.ownerSVGElement;
@@ -5536,6 +5539,8 @@ function bindNodeTooltip(params) {
5536
5539
  destroy: () => {
5537
5540
  },
5538
5541
  reposition: () => {
5542
+ },
5543
+ hide: () => {
5539
5544
  }
5540
5545
  };
5541
5546
  }
@@ -5545,6 +5550,9 @@ function bindNodeTooltip(params) {
5545
5550
  "mouseenter.tooltip",
5546
5551
  function(event, node) {
5547
5552
  const target = this;
5553
+ if (target.dataset.selected === "true") {
5554
+ return;
5555
+ }
5548
5556
  activeTarget = target;
5549
5557
  const customContent = params.tooltipConfig?.renderContent?.(node);
5550
5558
  const content = customContent ?? getDefaultContent(node);
@@ -5554,6 +5562,11 @@ function bindNodeTooltip(params) {
5554
5562
  "mousemove.tooltip",
5555
5563
  function() {
5556
5564
  const target = this;
5565
+ if (target.dataset.selected === "true") {
5566
+ activeTarget = null;
5567
+ tooltip.hide();
5568
+ return;
5569
+ }
5557
5570
  activeTarget = target;
5558
5571
  tooltip.move(target);
5559
5572
  }
@@ -5570,12 +5583,16 @@ function bindNodeTooltip(params) {
5570
5583
  }
5571
5584
  tooltip.move(activeTarget);
5572
5585
  }
5586
+ function hide() {
5587
+ activeTarget = null;
5588
+ tooltip.hide();
5589
+ }
5573
5590
  function destroy() {
5574
5591
  activeTarget = null;
5575
5592
  params.selection.on(".tooltip", null);
5576
5593
  tooltip.destroy();
5577
5594
  }
5578
- return { destroy, reposition };
5595
+ return { destroy, reposition, hide };
5579
5596
  }
5580
5597
  function getDefaultContent(node) {
5581
5598
  return `
@@ -5805,17 +5822,22 @@ var SelectionManager = class {
5805
5822
  layers;
5806
5823
  linkMarkerSnapshots;
5807
5824
  root;
5808
- constructor(eventEmitter, config, layers, linkMarkerSnapshots, root2) {
5825
+ tooltipBinding;
5826
+ constructor(eventEmitter, config, layers, linkMarkerSnapshots, root2, tooltipBinding) {
5809
5827
  this.eventEmitter = eventEmitter;
5810
5828
  this.config = config;
5811
5829
  this.layers = layers;
5812
5830
  this.linkMarkerSnapshots = linkMarkerSnapshots;
5813
5831
  this.root = root2;
5832
+ this.tooltipBinding = tooltipBinding;
5814
5833
  }
5815
5834
  /**
5816
5835
  * Select a node, automatically deselecting any current selection
5817
5836
  */
5818
5837
  selectNode(nodeElement, nodeData) {
5838
+ if (this.tooltipBinding) {
5839
+ this.tooltipBinding.hide();
5840
+ }
5819
5841
  this.clearHoverState();
5820
5842
  this.clearSelection();
5821
5843
  this.bringNodeToFront(nodeElement, nodeData);
@@ -6099,6 +6121,27 @@ var SelectionManager = class {
6099
6121
  }
6100
6122
  };
6101
6123
 
6124
+ // src/utils/resolve-node-style.ts
6125
+ var DEFAULT_NODE_HOVER_STYLE = {
6126
+ stroke: `color-mix(in srgb, ${"#8E42EE" /* PURPLE */}, ${"#000000" /* BLACK */} 20%)`,
6127
+ strokeWidth: 3
6128
+ };
6129
+ function resolveNodeStyle(params) {
6130
+ if (params.isSelected) {
6131
+ return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6132
+ }
6133
+ if (params.isHovered) {
6134
+ return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6135
+ }
6136
+ return void 0;
6137
+ }
6138
+ function mergeNodeStyle(base, override) {
6139
+ return {
6140
+ ...base,
6141
+ ...override
6142
+ };
6143
+ }
6144
+
6102
6145
  // src/core/interaction-manager.ts
6103
6146
  var InteractionManager = class {
6104
6147
  constructor(manager) {
@@ -6150,7 +6193,13 @@ var InteractionManager = class {
6150
6193
  tooltipConfig: this.manager.config.interaction.hover.tooltip
6151
6194
  });
6152
6195
  }
6153
- createNodeHover(selections.nodeSelection, this.manager.config.interaction.hover.nodeStyle);
6196
+ const defaultNodeHoverStyle = resolveNodeStyle({
6197
+ node: {},
6198
+ // We don't need the actual node for defaults
6199
+ interaction: this.manager.config.interaction,
6200
+ isHovered: true
6201
+ });
6202
+ createNodeHover(selections.nodeSelection, defaultNodeHoverStyle);
6154
6203
  createLinkHover(selections.linkSelection, this.manager.config.interaction.hover.linkStyle);
6155
6204
  }
6156
6205
  }
@@ -6190,7 +6239,8 @@ var InteractionManager = class {
6190
6239
  this.manager.config.interaction.selection,
6191
6240
  this.manager.layers,
6192
6241
  this.manager.linkMarkerSnapshots,
6193
- this.manager.rootSelection
6242
+ this.manager.rootSelection,
6243
+ this.manager.tooltipBinding || void 0
6194
6244
  );
6195
6245
  this.setupSelectionHandlers(selections);
6196
6246
  this.setupBackgroundClickHandler();
@@ -6470,6 +6520,7 @@ async function captureAndDownloadGraph(container, options = {}) {
6470
6520
  }
6471
6521
  svgClone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
6472
6522
  svgClone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
6523
+ fixTextStyling(svgClone);
6473
6524
  const svgString = new XMLSerializer().serializeToString(svgClone);
6474
6525
  const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
6475
6526
  const svgUrl = URL.createObjectURL(svgBlob);
@@ -6616,6 +6667,64 @@ function createLegendSVGElement(legendEntries, dimensions) {
6616
6667
  });
6617
6668
  return legendGroup;
6618
6669
  }
6670
+ function fixTextStyling(svgElement) {
6671
+ const nodeLabelsLayer = svgElement.querySelector('[data-layer="node-labels"]');
6672
+ if (nodeLabelsLayer) {
6673
+ const nodeTexts = nodeLabelsLayer.querySelectorAll("text");
6674
+ nodeTexts.forEach((text) => {
6675
+ const textElement = text;
6676
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6677
+ if (!textElement.getAttribute("font-size")) {
6678
+ textElement.setAttribute("font-size", "9");
6679
+ }
6680
+ textElement.setAttribute("font-weight", "500");
6681
+ textElement.setAttribute("text-anchor", "middle");
6682
+ textElement.setAttribute("dominant-baseline", "central");
6683
+ if (!textElement.getAttribute("fill")) {
6684
+ textElement.setAttribute("fill", "#374151");
6685
+ }
6686
+ });
6687
+ }
6688
+ const linkLabels = svgElement.querySelectorAll("g.link-label");
6689
+ linkLabels.forEach((labelGroup) => {
6690
+ const textElement = labelGroup.querySelector("text");
6691
+ if (textElement) {
6692
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6693
+ if (!textElement.getAttribute("font-size")) {
6694
+ textElement.setAttribute("font-size", "10");
6695
+ }
6696
+ textElement.setAttribute("font-weight", "400");
6697
+ textElement.setAttribute("text-anchor", "middle");
6698
+ textElement.setAttribute("dominant-baseline", "central");
6699
+ if (!textElement.getAttribute("fill")) {
6700
+ textElement.setAttribute("fill", "#6b7280");
6701
+ }
6702
+ }
6703
+ const rectElement = labelGroup.querySelector("rect");
6704
+ if (rectElement) {
6705
+ if (!rectElement.getAttribute("fill")) {
6706
+ rectElement.setAttribute("fill", "#ffffff");
6707
+ }
6708
+ if (!rectElement.getAttribute("stroke")) {
6709
+ rectElement.setAttribute("stroke", "#e5e7eb");
6710
+ }
6711
+ if (!rectElement.getAttribute("stroke-width")) {
6712
+ rectElement.setAttribute("stroke-width", "1");
6713
+ }
6714
+ rectElement.setAttribute("rx", "4");
6715
+ }
6716
+ });
6717
+ const allTexts = svgElement.querySelectorAll("text");
6718
+ allTexts.forEach((text) => {
6719
+ const textElement = text;
6720
+ if (!textElement.getAttribute("font-family")) {
6721
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6722
+ }
6723
+ if (!textElement.getAttribute("font-size")) {
6724
+ textElement.setAttribute("font-size", "10");
6725
+ }
6726
+ });
6727
+ }
6619
6728
  function normalizeColor(color2) {
6620
6729
  const rgbMatch = color2.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
6621
6730
  if (rgbMatch) {
@@ -6948,10 +7057,6 @@ function createGraph(config) {
6948
7057
  renderPipeline.execute().then((selections) => {
6949
7058
  interactionManager.setupInteractions(selections);
6950
7059
  setupAdditionalComponents();
6951
- if (graphManager.needsImmediateFitView) {
6952
- graphManager.needsImmediateFitView = false;
6953
- fitViewWithInitialPositions();
6954
- }
6955
7060
  }).catch((error) => {
6956
7061
  console.error("[Polly Graph] Render failed:", error);
6957
7062
  });
@@ -7022,56 +7127,34 @@ function createGraph(config) {
7022
7127
  const svg = graphManager.svgElement;
7023
7128
  const nodes = config.nodes;
7024
7129
  if (nodes.length === 0) return;
7025
- const positions = nodes.map((node) => ({
7026
- x: node.x ?? 0,
7027
- y: node.y ?? 0
7028
- }));
7029
- const xExtent = (0, import_d3.extent)(positions, (d) => d.x);
7030
- const yExtent = (0, import_d3.extent)(positions, (d) => d.y);
7031
- const padding = 50;
7032
- const width = graphManager.dimensions.width - padding * 2;
7033
- const height = graphManager.dimensions.height - padding * 2;
7034
- const nodeWidth = xExtent[1] - xExtent[0];
7035
- const nodeHeight = yExtent[1] - yExtent[0];
7036
- if (nodeWidth === 0 || nodeHeight === 0) {
7130
+ const graphRoot = select_default2(svg).select('[data-layer="viewport"]');
7131
+ const graphRootNode = graphRoot.node();
7132
+ if (!graphRootNode) {
7133
+ console.warn("[Polly Graph] Cannot fit view: graph root not found");
7037
7134
  return;
7038
7135
  }
7039
- const scale = Math.min(width / nodeWidth, height / nodeHeight, 3);
7040
- const centerX = (xExtent[0] + xExtent[1]) / 2;
7041
- const centerY = (yExtent[0] + yExtent[1]) / 2;
7042
- const transform2 = identity2.translate(graphManager.dimensions.width / 2, graphManager.dimensions.height / 2).scale(scale).translate(-centerX, -centerY);
7043
- if (graphManager.zoomBehavior) {
7044
- select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.transform, transform2);
7045
- }
7046
- }
7047
- function fitViewWithInitialPositions() {
7048
- if (!graphManager.simulation || !graphManager.svgElement) {
7049
- console.warn("[Polly Graph] Cannot fit view: simulation or SVG not available");
7136
+ let bounds;
7137
+ try {
7138
+ bounds = graphRootNode.getBBox();
7139
+ } catch {
7140
+ console.warn("[Polly Graph] Cannot get bounds, falling back to default view");
7050
7141
  return;
7051
7142
  }
7052
- const svg = graphManager.svgElement;
7053
- const nodes = config.nodes;
7054
- if (nodes.length === 0) return;
7055
- const positions = nodes.map((node) => ({
7056
- x: node.initialX ?? node.x ?? 0,
7057
- y: node.initialY ?? node.y ?? 0
7058
- }));
7059
- const xExtent = (0, import_d3.extent)(positions, (d) => d.x);
7060
- const yExtent = (0, import_d3.extent)(positions, (d) => d.y);
7061
- const padding = 50;
7062
- const width = graphManager.dimensions.width - padding * 2;
7063
- const height = graphManager.dimensions.height - padding * 2;
7064
- const nodeWidth = xExtent[1] - xExtent[0];
7065
- const nodeHeight = yExtent[1] - yExtent[0];
7066
- if (nodeWidth === 0 || nodeHeight === 0) {
7143
+ if (bounds.width === 0 || bounds.height === 0) {
7067
7144
  return;
7068
7145
  }
7069
- const scale = Math.min(width / nodeWidth, height / nodeHeight, 3);
7070
- const centerX = (xExtent[0] + xExtent[1]) / 2;
7071
- const centerY = (yExtent[0] + yExtent[1]) / 2;
7072
- const transform2 = identity2.translate(graphManager.dimensions.width / 2, graphManager.dimensions.height / 2).scale(scale).translate(-centerX, -centerY);
7146
+ const svgRect = svg.getBoundingClientRect();
7147
+ const padding = 40;
7148
+ const availableWidth = svgRect.width - padding * 2;
7149
+ const availableHeight = svgRect.height - padding * 2;
7150
+ const scaleX = availableWidth / bounds.width;
7151
+ const scaleY = availableHeight / bounds.height;
7152
+ const scale = Math.min(scaleX, scaleY, 4);
7153
+ const centerX = bounds.x + bounds.width / 2;
7154
+ const centerY = bounds.y + bounds.height / 2;
7155
+ const transform2 = identity2.translate(svgRect.width / 2, svgRect.height / 2).scale(scale).translate(-centerX, -centerY);
7073
7156
  if (graphManager.zoomBehavior) {
7074
- select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.transform, transform2);
7157
+ select_default2(svg).transition().duration(750).call(graphManager.zoomBehavior.transform, transform2);
7075
7158
  }
7076
7159
  }
7077
7160
  function exportGraph(fileName) {
package/dist/index.css CHANGED
@@ -200,14 +200,17 @@
200
200
  }
201
201
 
202
202
  /* src/styles/graph-tooltip.css */
203
+ :root {
204
+ --dark-bg: color-mix(in srgb, #808080, #000000 20%);
205
+ }
203
206
  .graph-tooltip {
204
207
  position: absolute;
205
208
  pointer-events: none;
206
209
  z-index: 1000;
207
210
  width: fit-content;
208
211
  max-width: 280px;
209
- border-radius: 10px;
210
- background: #0f172a;
212
+ border-radius: 0.375rem;
213
+ background: var(--dark-bg);
211
214
  color: #f8fafc;
212
215
  border: 1px solid rgba(255, 255, 255, 0.08);
213
216
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22);
@@ -230,15 +233,15 @@
230
233
  .graph-tooltip__content {
231
234
  position: relative;
232
235
  z-index: 2;
233
- padding: 10px 14px;
236
+ padding: 0.375rem 0.5rem;
234
237
  word-wrap: break-word;
235
238
  overflow-wrap: break-word;
236
239
  }
237
240
  .graph-tooltip__arrow {
238
241
  position: absolute;
239
- width: 12px;
240
- height: 12px;
241
- background: #0f172a;
242
+ width: 0.625rem;
243
+ height: 0.625rem;
244
+ background: var(--dark-bg);
242
245
  transform: rotate(45deg);
243
246
  z-index: 1;
244
247
  }
@@ -298,6 +301,10 @@
298
301
  display: block;
299
302
  width: 100%;
300
303
  height: 100%;
304
+ cursor: grab;
305
+ }
306
+ .pg-canvas:active {
307
+ cursor: grabbing;
301
308
  }
302
309
  .pg-overlay {
303
310
  position: absolute;
@@ -310,6 +317,7 @@
310
317
  transition: r 0.2s ease, stroke-width 0.2s ease;
311
318
  }
312
319
  .pg-layer-links line {
320
+ cursor: pointer;
313
321
  transition: stroke-opacity 0.2s ease;
314
322
  }
315
323
 
package/dist/index.d.cts CHANGED
@@ -537,6 +537,12 @@ interface RenderableGraphLink {
537
537
  readonly markerEnd: string;
538
538
  }
539
539
 
540
+ interface NodeTooltipBinding {
541
+ destroy(): void;
542
+ reposition(): void;
543
+ hide(): void;
544
+ }
545
+
540
546
  /**
541
547
  * Centralized selection management for graph nodes and links.
542
548
  * Simplifies selection logic and ensures consistent behavior.
@@ -560,7 +566,8 @@ declare class SelectionManager {
560
566
  private readonly layers;
561
567
  private readonly linkMarkerSnapshots;
562
568
  private readonly root;
563
- constructor(eventEmitter: TypedGraphEventEmitter, config: SelectionInteractionConfig, layers: GraphLayers, linkMarkerSnapshots: Map<SVGLineElement, string | null>, root: Selection<SVGGElement, unknown, null, undefined>);
569
+ private readonly tooltipBinding?;
570
+ constructor(eventEmitter: TypedGraphEventEmitter, config: SelectionInteractionConfig, layers: GraphLayers, linkMarkerSnapshots: Map<SVGLineElement, string | null>, root: Selection<SVGGElement, unknown, null, undefined>, tooltipBinding?: NodeTooltipBinding);
564
571
  /**
565
572
  * Select a node, automatically deselecting any current selection
566
573
  */
package/dist/index.d.ts CHANGED
@@ -537,6 +537,12 @@ interface RenderableGraphLink {
537
537
  readonly markerEnd: string;
538
538
  }
539
539
 
540
+ interface NodeTooltipBinding {
541
+ destroy(): void;
542
+ reposition(): void;
543
+ hide(): void;
544
+ }
545
+
540
546
  /**
541
547
  * Centralized selection management for graph nodes and links.
542
548
  * Simplifies selection logic and ensures consistent behavior.
@@ -560,7 +566,8 @@ declare class SelectionManager {
560
566
  private readonly layers;
561
567
  private readonly linkMarkerSnapshots;
562
568
  private readonly root;
563
- constructor(eventEmitter: TypedGraphEventEmitter, config: SelectionInteractionConfig, layers: GraphLayers, linkMarkerSnapshots: Map<SVGLineElement, string | null>, root: Selection<SVGGElement, unknown, null, undefined>);
569
+ private readonly tooltipBinding?;
570
+ constructor(eventEmitter: TypedGraphEventEmitter, config: SelectionInteractionConfig, layers: GraphLayers, linkMarkerSnapshots: Map<SVGLineElement, string | null>, root: Selection<SVGGElement, unknown, null, undefined>, tooltipBinding?: NodeTooltipBinding);
564
571
  /**
565
572
  * Select a node, automatically deselecting any current selection
566
573
  */
package/dist/index.js CHANGED
@@ -2630,15 +2630,15 @@ function defaultWheelDelta(event) {
2630
2630
  function defaultTouchable2() {
2631
2631
  return navigator.maxTouchPoints || "ontouchstart" in this;
2632
2632
  }
2633
- function defaultConstrain(transform2, extent2, translateExtent) {
2634
- var dx0 = transform2.invertX(extent2[0][0]) - translateExtent[0][0], dx1 = transform2.invertX(extent2[1][0]) - translateExtent[1][0], dy0 = transform2.invertY(extent2[0][1]) - translateExtent[0][1], dy1 = transform2.invertY(extent2[1][1]) - translateExtent[1][1];
2633
+ function defaultConstrain(transform2, extent, translateExtent) {
2634
+ var dx0 = transform2.invertX(extent[0][0]) - translateExtent[0][0], dx1 = transform2.invertX(extent[1][0]) - translateExtent[1][0], dy0 = transform2.invertY(extent[0][1]) - translateExtent[0][1], dy1 = transform2.invertY(extent[1][1]) - translateExtent[1][1];
2635
2635
  return transform2.translate(
2636
2636
  dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),
2637
2637
  dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)
2638
2638
  );
2639
2639
  }
2640
2640
  function zoom_default2() {
2641
- var filter2 = defaultFilter2, extent2 = defaultExtent, constrain = defaultConstrain, wheelDelta = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default, listeners = dispatch_default2("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
2641
+ var filter2 = defaultFilter2, extent = defaultExtent, constrain = defaultConstrain, wheelDelta = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default, listeners = dispatch_default2("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
2642
2642
  function zoom(selection2) {
2643
2643
  selection2.property("__zoom", defaultTransform).on("wheel.zoom", wheeled, { passive: false }).on("mousedown.zoom", mousedowned).on("dblclick.zoom", dblclicked).filter(touchable).on("touchstart.zoom", touchstarted).on("touchmove.zoom", touchmoved).on("touchend.zoom touchcancel.zoom", touchended).style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
2644
2644
  }
@@ -2661,7 +2661,7 @@ function zoom_default2() {
2661
2661
  };
2662
2662
  zoom.scaleTo = function(selection2, k, p, event) {
2663
2663
  zoom.transform(selection2, function() {
2664
- var e = extent2.apply(this, arguments), t0 = this.__zoom, p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p, p1 = t0.invert(p0), k1 = typeof k === "function" ? k.apply(this, arguments) : k;
2664
+ var e = extent.apply(this, arguments), t0 = this.__zoom, p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p, p1 = t0.invert(p0), k1 = typeof k === "function" ? k.apply(this, arguments) : k;
2665
2665
  return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);
2666
2666
  }, p, event);
2667
2667
  };
@@ -2670,12 +2670,12 @@ function zoom_default2() {
2670
2670
  return constrain(this.__zoom.translate(
2671
2671
  typeof x3 === "function" ? x3.apply(this, arguments) : x3,
2672
2672
  typeof y3 === "function" ? y3.apply(this, arguments) : y3
2673
- ), extent2.apply(this, arguments), translateExtent);
2673
+ ), extent.apply(this, arguments), translateExtent);
2674
2674
  }, null, event);
2675
2675
  };
2676
2676
  zoom.translateTo = function(selection2, x3, y3, p, event) {
2677
2677
  zoom.transform(selection2, function() {
2678
- var e = extent2.apply(this, arguments), t = this.__zoom, p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p;
2678
+ var e = extent.apply(this, arguments), t = this.__zoom, p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p;
2679
2679
  return constrain(identity2.translate(p0[0], p0[1]).scale(t.k).translate(
2680
2680
  typeof x3 === "function" ? -x3.apply(this, arguments) : -x3,
2681
2681
  typeof y3 === "function" ? -y3.apply(this, arguments) : -y3
@@ -2690,8 +2690,8 @@ function zoom_default2() {
2690
2690
  var x3 = p0[0] - p1[0] * transform2.k, y3 = p0[1] - p1[1] * transform2.k;
2691
2691
  return x3 === transform2.x && y3 === transform2.y ? transform2 : new Transform(transform2.k, x3, y3);
2692
2692
  }
2693
- function centroid(extent3) {
2694
- return [(+extent3[0][0] + +extent3[1][0]) / 2, (+extent3[0][1] + +extent3[1][1]) / 2];
2693
+ function centroid(extent2) {
2694
+ return [(+extent2[0][0] + +extent2[1][0]) / 2, (+extent2[0][1] + +extent2[1][1]) / 2];
2695
2695
  }
2696
2696
  function schedule(transition2, transform2, point, event) {
2697
2697
  transition2.on("start.zoom", function() {
@@ -2699,7 +2699,7 @@ function zoom_default2() {
2699
2699
  }).on("interrupt.zoom end.zoom", function() {
2700
2700
  gesture(this, arguments).event(event).end();
2701
2701
  }).tween("zoom", function() {
2702
- var that = this, args = arguments, g = gesture(that, args).event(event), e = extent2.apply(that, args), p = point == null ? centroid(e) : typeof point === "function" ? point.apply(that, args) : point, w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]), a2 = that.__zoom, b = typeof transform2 === "function" ? transform2.apply(that, args) : transform2, i = interpolate(a2.invert(p).concat(w / a2.k), b.invert(p).concat(w / b.k));
2702
+ var that = this, args = arguments, g = gesture(that, args).event(event), e = extent.apply(that, args), p = point == null ? centroid(e) : typeof point === "function" ? point.apply(that, args) : point, w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]), a2 = that.__zoom, b = typeof transform2 === "function" ? transform2.apply(that, args) : transform2, i = interpolate(a2.invert(p).concat(w / a2.k), b.invert(p).concat(w / b.k));
2703
2703
  return function(t) {
2704
2704
  if (t === 1) t = b;
2705
2705
  else {
@@ -2718,7 +2718,7 @@ function zoom_default2() {
2718
2718
  this.args = args;
2719
2719
  this.active = 0;
2720
2720
  this.sourceEvent = null;
2721
- this.extent = extent2.apply(that, args);
2721
+ this.extent = extent.apply(that, args);
2722
2722
  this.taps = 0;
2723
2723
  }
2724
2724
  Gesture.prototype = {
@@ -2811,7 +2811,7 @@ function zoom_default2() {
2811
2811
  }
2812
2812
  function dblclicked(event, ...args) {
2813
2813
  if (!filter2.apply(this, arguments)) return;
2814
- var t0 = this.__zoom, p0 = pointer_default(event.changedTouches ? event.changedTouches[0] : event, this), p1 = t0.invert(p0), k1 = t0.k * (event.shiftKey ? 0.5 : 2), t1 = constrain(translate(scale(t0, k1), p0, p1), extent2.apply(this, args), translateExtent);
2814
+ var t0 = this.__zoom, p0 = pointer_default(event.changedTouches ? event.changedTouches[0] : event, this), p1 = t0.invert(p0), k1 = t0.k * (event.shiftKey ? 0.5 : 2), t1 = constrain(translate(scale(t0, k1), p0, p1), extent.apply(this, args), translateExtent);
2815
2815
  noevent_default2(event);
2816
2816
  if (duration > 0) select_default2(this).transition().duration(duration).call(schedule, t1, p0, event);
2817
2817
  else select_default2(this).call(zoom.transform, t1, p0, event);
@@ -2890,7 +2890,7 @@ function zoom_default2() {
2890
2890
  return arguments.length ? (touchable = typeof _ === "function" ? _ : constant_default4(!!_), zoom) : touchable;
2891
2891
  };
2892
2892
  zoom.extent = function(_) {
2893
- return arguments.length ? (extent2 = typeof _ === "function" ? _ : constant_default4([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent2;
2893
+ return arguments.length ? (extent = typeof _ === "function" ? _ : constant_default4([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;
2894
2894
  };
2895
2895
  zoom.scaleExtent = function(_) {
2896
2896
  return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];
@@ -2920,9 +2920,6 @@ function zoom_default2() {
2920
2920
  return zoom;
2921
2921
  }
2922
2922
 
2923
- // src/create-graph.ts
2924
- import { extent } from "d3";
2925
-
2926
2923
  // src/utils/timer-manager.ts
2927
2924
  var TimerManager = class {
2928
2925
  activeTimers = /* @__PURE__ */ new Map();
@@ -3309,7 +3306,6 @@ var GraphManager = class {
3309
3306
  linkMarkerSnapshots = null;
3310
3307
  rootSelection = null;
3311
3308
  simulationPaused = false;
3312
- needsImmediateFitView = false;
3313
3309
  /**
3314
3310
  * Initialize core managers
3315
3311
  */
@@ -3365,7 +3361,6 @@ var GraphManager = class {
3365
3361
  this.linkMarkerSnapshots = null;
3366
3362
  this.rootSelection = null;
3367
3363
  this.simulationPaused = false;
3368
- this.needsImmediateFitView = false;
3369
3364
  this.cleanupFunctions = [];
3370
3365
  }
3371
3366
  /**
@@ -4385,6 +4380,24 @@ function createGraphSimulation(config) {
4385
4380
  const warmupTicks = enhancedConfig.warmup?.ticks ?? (useAdaptive ? Math.min(100, nodeCount * 2) : 50);
4386
4381
  warmupSimulation(simulation, warmupTicks);
4387
4382
  }
4383
+ if (config.onReady && config.timerManager) {
4384
+ let readyCallbackFired = false;
4385
+ const handleTick = () => {
4386
+ if (!readyCallbackFired && simulation.alpha() < 0.1) {
4387
+ readyCallbackFired = true;
4388
+ simulation.on("tick.ready", null);
4389
+ config.timerManager.setTimeout("simulation-ready", () => {
4390
+ config.onReady?.();
4391
+ }, 100);
4392
+ }
4393
+ };
4394
+ const cleanup = () => {
4395
+ simulation.on("tick.ready", null);
4396
+ config.timerManager.clearTimer("simulation-ready");
4397
+ };
4398
+ simulation.on("tick.ready", handleTick);
4399
+ simulation.on("end.ready", cleanup);
4400
+ }
4388
4401
  return { simulation };
4389
4402
  }
4390
4403
  function seedNodePositions(nodes, containerWidth, containerHeight) {
@@ -4771,50 +4784,12 @@ function renderLinks(ctx, links) {
4771
4784
 
4772
4785
  // src/renderer/nodes.ts
4773
4786
  function renderNodes(ctx, nodes) {
4774
- return ctx.root.select('[data-layer="nodes"]').selectAll("circle").data(
4775
- nodes,
4776
- (d) => d.id
4777
- ).join("circle").attr(
4778
- "r",
4779
- (node) => node.style?.radius ?? 8
4780
- ).attr(
4781
- "fill",
4782
- (node) => node.style?.fill ?? "#6c5ce7"
4783
- ).attr(
4784
- "stroke",
4785
- (node) => node.style?.stroke ?? "#ffffff"
4786
- ).attr(
4787
- "stroke-width",
4788
- (node) => node.style?.strokeWidth ?? 1.5
4789
- ).attr(
4790
- "opacity",
4791
- (node) => node.style?.opacity ?? 1
4792
- );
4787
+ return ctx.root.select('[data-layer="nodes"]').selectAll("circle").data(nodes, (d) => d.id).join("circle").attr("r", (node) => node.style?.radius ?? 8).attr("fill", (node) => node.style?.fill ?? "#6c5ce7").attr("stroke", (node) => node.style?.stroke ?? "#ffffff").attr("stroke-width", (node) => node.style?.strokeWidth ?? 1.5).attr("opacity", (node) => node.style?.opacity ?? 1).style("cursor", "pointer");
4793
4788
  }
4794
4789
 
4795
4790
  // src/renderer/node-labels.ts
4796
4791
  function renderNodeLabels(ctx, nodes) {
4797
- const selection2 = ctx.root.select('[data-layer="node-labels"]').selectAll("text").data(
4798
- nodes,
4799
- (d) => d.id
4800
- ).join("text").attr(
4801
- "text-anchor",
4802
- "middle"
4803
- ).attr(
4804
- "dominant-baseline",
4805
- "middle"
4806
- ).attr(
4807
- "font-size",
4808
- 9
4809
- ).attr(
4810
- "fill",
4811
- (node) => node.style?.textColor ?? "#ffffff"
4812
- ).attr(
4813
- "pointer-events",
4814
- "none"
4815
- ).text(
4816
- (node) => node.label ?? node.id
4817
- );
4792
+ const selection2 = ctx.root.select('[data-layer="node-labels"]').selectAll("text").data(nodes, (d) => d.id).join("text").attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("font-size", 9).attr("fill", (node) => node.style?.textColor ?? "#ffffff").attr("pointer-events", "none").text((node) => node.label ?? node.id);
4818
4793
  selection2.each(function(node) {
4819
4794
  const textElement = this;
4820
4795
  const fullLabel = node.label ?? node.id;
@@ -4823,10 +4798,7 @@ function renderNodeLabels(ctx, nodes) {
4823
4798
  let truncatedLabel = fullLabel;
4824
4799
  textElement.textContent = truncatedLabel;
4825
4800
  while (truncatedLabel.length > 1 && textElement.getComputedTextLength() > maxWidth) {
4826
- truncatedLabel = truncatedLabel.slice(
4827
- 0,
4828
- -1
4829
- );
4801
+ truncatedLabel = truncatedLabel.slice(0, -1);
4830
4802
  textElement.textContent = `${truncatedLabel}\u2026`;
4831
4803
  }
4832
4804
  });
@@ -4960,10 +4932,12 @@ var RenderPipeline = class {
4960
4932
  }
4961
4933
  if (this.manager.timerManager && this.manager.fitViewCallback) {
4962
4934
  this.manager.timerManager.debounce("fit-view-resize", () => {
4963
- if (this.manager.fitViewCallback) {
4964
- this.manager.fitViewCallback();
4965
- }
4966
- }, 150);
4935
+ this.manager.timerManager.setTimeout("fit-view-layout", () => {
4936
+ if (this.manager.fitViewCallback) {
4937
+ this.manager.fitViewCallback();
4938
+ }
4939
+ }, 50);
4940
+ }, 200);
4967
4941
  }
4968
4942
  });
4969
4943
  this.manager.addCleanup(cleanupResize);
@@ -5033,15 +5007,23 @@ var RenderPipeline = class {
5033
5007
  config: this.manager.config.simulation
5034
5008
  };
5035
5009
  try {
5036
- const simulationResult = createGraphSimulation(simulationConfig);
5010
+ const simulationConfigWithCallback = {
5011
+ ...simulationConfig,
5012
+ onReady: () => {
5013
+ if (this.manager.fitViewCallback) {
5014
+ this.manager.fitViewCallback();
5015
+ }
5016
+ },
5017
+ timerManager: this.manager.timerManager ?? void 0
5018
+ };
5019
+ const simulationResult = createGraphSimulation(simulationConfigWithCallback);
5037
5020
  this.manager.simulation = simulationResult.simulation;
5038
5021
  const centerX = simulationConfig.width / 2;
5039
5022
  const centerY = simulationConfig.height / 2;
5040
5023
  this.manager.simulation.force("center", center_default(centerX, centerY));
5041
- if (simulationConfig.width > 0 && simulationConfig.height > 0) {
5024
+ if (simulationConfigWithCallback.width > 0 && simulationConfigWithCallback.height > 0) {
5042
5025
  this.manager.reheatSimulation(0.3);
5043
5026
  this.manager.simulationPaused = false;
5044
- this.manager.needsImmediateFitView = true;
5045
5027
  } else {
5046
5028
  this.manager.simulation.stop();
5047
5029
  this.manager.simulationPaused = true;
@@ -5100,14 +5082,35 @@ function createNodeHover(nodeSelection, hoverStyle) {
5100
5082
  if (hoverStyle) {
5101
5083
  nodeSelection.on("mouseenter.hover", function(_event, node) {
5102
5084
  const circle = this;
5085
+ if (circle.dataset.selected === "true") {
5086
+ return;
5087
+ }
5103
5088
  circle.setAttribute("stroke", hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff");
5104
5089
  circle.setAttribute("stroke-width", String(hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5));
5105
5090
  circle.setAttribute("opacity", String(hoverStyle.opacity ?? node.style?.opacity ?? 1));
5106
5091
  }).on("mouseleave.hover", function(_event, node) {
5107
5092
  const circle = this;
5108
- circle.setAttribute("stroke", node.style?.stroke ?? "#ffffff");
5109
- circle.setAttribute("stroke-width", String(node.style?.strokeWidth ?? 1.5));
5110
- circle.setAttribute("opacity", String(node.style?.opacity ?? 1));
5093
+ if (circle.dataset.selected === "true") {
5094
+ return;
5095
+ }
5096
+ circle.style.stroke = "";
5097
+ circle.style.strokeWidth = "";
5098
+ circle.style.opacity = "";
5099
+ if (!node.style?.stroke) {
5100
+ circle.setAttribute("stroke", "#ffffff");
5101
+ } else {
5102
+ circle.setAttribute("stroke", node.style.stroke);
5103
+ }
5104
+ if (!node.style?.strokeWidth) {
5105
+ circle.setAttribute("stroke-width", "1.5");
5106
+ } else {
5107
+ circle.setAttribute("stroke-width", String(node.style.strokeWidth));
5108
+ }
5109
+ if (!node.style?.opacity) {
5110
+ circle.setAttribute("opacity", "1");
5111
+ } else {
5112
+ circle.setAttribute("opacity", String(node.style.opacity));
5113
+ }
5111
5114
  });
5112
5115
  }
5113
5116
  const svgElement = firstNode.ownerSVGElement;
@@ -5504,6 +5507,8 @@ function bindNodeTooltip(params) {
5504
5507
  destroy: () => {
5505
5508
  },
5506
5509
  reposition: () => {
5510
+ },
5511
+ hide: () => {
5507
5512
  }
5508
5513
  };
5509
5514
  }
@@ -5513,6 +5518,9 @@ function bindNodeTooltip(params) {
5513
5518
  "mouseenter.tooltip",
5514
5519
  function(event, node) {
5515
5520
  const target = this;
5521
+ if (target.dataset.selected === "true") {
5522
+ return;
5523
+ }
5516
5524
  activeTarget = target;
5517
5525
  const customContent = params.tooltipConfig?.renderContent?.(node);
5518
5526
  const content = customContent ?? getDefaultContent(node);
@@ -5522,6 +5530,11 @@ function bindNodeTooltip(params) {
5522
5530
  "mousemove.tooltip",
5523
5531
  function() {
5524
5532
  const target = this;
5533
+ if (target.dataset.selected === "true") {
5534
+ activeTarget = null;
5535
+ tooltip.hide();
5536
+ return;
5537
+ }
5525
5538
  activeTarget = target;
5526
5539
  tooltip.move(target);
5527
5540
  }
@@ -5538,12 +5551,16 @@ function bindNodeTooltip(params) {
5538
5551
  }
5539
5552
  tooltip.move(activeTarget);
5540
5553
  }
5554
+ function hide() {
5555
+ activeTarget = null;
5556
+ tooltip.hide();
5557
+ }
5541
5558
  function destroy() {
5542
5559
  activeTarget = null;
5543
5560
  params.selection.on(".tooltip", null);
5544
5561
  tooltip.destroy();
5545
5562
  }
5546
- return { destroy, reposition };
5563
+ return { destroy, reposition, hide };
5547
5564
  }
5548
5565
  function getDefaultContent(node) {
5549
5566
  return `
@@ -5773,17 +5790,22 @@ var SelectionManager = class {
5773
5790
  layers;
5774
5791
  linkMarkerSnapshots;
5775
5792
  root;
5776
- constructor(eventEmitter, config, layers, linkMarkerSnapshots, root2) {
5793
+ tooltipBinding;
5794
+ constructor(eventEmitter, config, layers, linkMarkerSnapshots, root2, tooltipBinding) {
5777
5795
  this.eventEmitter = eventEmitter;
5778
5796
  this.config = config;
5779
5797
  this.layers = layers;
5780
5798
  this.linkMarkerSnapshots = linkMarkerSnapshots;
5781
5799
  this.root = root2;
5800
+ this.tooltipBinding = tooltipBinding;
5782
5801
  }
5783
5802
  /**
5784
5803
  * Select a node, automatically deselecting any current selection
5785
5804
  */
5786
5805
  selectNode(nodeElement, nodeData) {
5806
+ if (this.tooltipBinding) {
5807
+ this.tooltipBinding.hide();
5808
+ }
5787
5809
  this.clearHoverState();
5788
5810
  this.clearSelection();
5789
5811
  this.bringNodeToFront(nodeElement, nodeData);
@@ -6067,6 +6089,27 @@ var SelectionManager = class {
6067
6089
  }
6068
6090
  };
6069
6091
 
6092
+ // src/utils/resolve-node-style.ts
6093
+ var DEFAULT_NODE_HOVER_STYLE = {
6094
+ stroke: `color-mix(in srgb, ${"#8E42EE" /* PURPLE */}, ${"#000000" /* BLACK */} 20%)`,
6095
+ strokeWidth: 3
6096
+ };
6097
+ function resolveNodeStyle(params) {
6098
+ if (params.isSelected) {
6099
+ return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6100
+ }
6101
+ if (params.isHovered) {
6102
+ return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6103
+ }
6104
+ return void 0;
6105
+ }
6106
+ function mergeNodeStyle(base, override) {
6107
+ return {
6108
+ ...base,
6109
+ ...override
6110
+ };
6111
+ }
6112
+
6070
6113
  // src/core/interaction-manager.ts
6071
6114
  var InteractionManager = class {
6072
6115
  constructor(manager) {
@@ -6118,7 +6161,13 @@ var InteractionManager = class {
6118
6161
  tooltipConfig: this.manager.config.interaction.hover.tooltip
6119
6162
  });
6120
6163
  }
6121
- createNodeHover(selections.nodeSelection, this.manager.config.interaction.hover.nodeStyle);
6164
+ const defaultNodeHoverStyle = resolveNodeStyle({
6165
+ node: {},
6166
+ // We don't need the actual node for defaults
6167
+ interaction: this.manager.config.interaction,
6168
+ isHovered: true
6169
+ });
6170
+ createNodeHover(selections.nodeSelection, defaultNodeHoverStyle);
6122
6171
  createLinkHover(selections.linkSelection, this.manager.config.interaction.hover.linkStyle);
6123
6172
  }
6124
6173
  }
@@ -6158,7 +6207,8 @@ var InteractionManager = class {
6158
6207
  this.manager.config.interaction.selection,
6159
6208
  this.manager.layers,
6160
6209
  this.manager.linkMarkerSnapshots,
6161
- this.manager.rootSelection
6210
+ this.manager.rootSelection,
6211
+ this.manager.tooltipBinding || void 0
6162
6212
  );
6163
6213
  this.setupSelectionHandlers(selections);
6164
6214
  this.setupBackgroundClickHandler();
@@ -6438,6 +6488,7 @@ async function captureAndDownloadGraph(container, options = {}) {
6438
6488
  }
6439
6489
  svgClone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
6440
6490
  svgClone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
6491
+ fixTextStyling(svgClone);
6441
6492
  const svgString = new XMLSerializer().serializeToString(svgClone);
6442
6493
  const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
6443
6494
  const svgUrl = URL.createObjectURL(svgBlob);
@@ -6584,6 +6635,64 @@ function createLegendSVGElement(legendEntries, dimensions) {
6584
6635
  });
6585
6636
  return legendGroup;
6586
6637
  }
6638
+ function fixTextStyling(svgElement) {
6639
+ const nodeLabelsLayer = svgElement.querySelector('[data-layer="node-labels"]');
6640
+ if (nodeLabelsLayer) {
6641
+ const nodeTexts = nodeLabelsLayer.querySelectorAll("text");
6642
+ nodeTexts.forEach((text) => {
6643
+ const textElement = text;
6644
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6645
+ if (!textElement.getAttribute("font-size")) {
6646
+ textElement.setAttribute("font-size", "9");
6647
+ }
6648
+ textElement.setAttribute("font-weight", "500");
6649
+ textElement.setAttribute("text-anchor", "middle");
6650
+ textElement.setAttribute("dominant-baseline", "central");
6651
+ if (!textElement.getAttribute("fill")) {
6652
+ textElement.setAttribute("fill", "#374151");
6653
+ }
6654
+ });
6655
+ }
6656
+ const linkLabels = svgElement.querySelectorAll("g.link-label");
6657
+ linkLabels.forEach((labelGroup) => {
6658
+ const textElement = labelGroup.querySelector("text");
6659
+ if (textElement) {
6660
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6661
+ if (!textElement.getAttribute("font-size")) {
6662
+ textElement.setAttribute("font-size", "10");
6663
+ }
6664
+ textElement.setAttribute("font-weight", "400");
6665
+ textElement.setAttribute("text-anchor", "middle");
6666
+ textElement.setAttribute("dominant-baseline", "central");
6667
+ if (!textElement.getAttribute("fill")) {
6668
+ textElement.setAttribute("fill", "#6b7280");
6669
+ }
6670
+ }
6671
+ const rectElement = labelGroup.querySelector("rect");
6672
+ if (rectElement) {
6673
+ if (!rectElement.getAttribute("fill")) {
6674
+ rectElement.setAttribute("fill", "#ffffff");
6675
+ }
6676
+ if (!rectElement.getAttribute("stroke")) {
6677
+ rectElement.setAttribute("stroke", "#e5e7eb");
6678
+ }
6679
+ if (!rectElement.getAttribute("stroke-width")) {
6680
+ rectElement.setAttribute("stroke-width", "1");
6681
+ }
6682
+ rectElement.setAttribute("rx", "4");
6683
+ }
6684
+ });
6685
+ const allTexts = svgElement.querySelectorAll("text");
6686
+ allTexts.forEach((text) => {
6687
+ const textElement = text;
6688
+ if (!textElement.getAttribute("font-family")) {
6689
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6690
+ }
6691
+ if (!textElement.getAttribute("font-size")) {
6692
+ textElement.setAttribute("font-size", "10");
6693
+ }
6694
+ });
6695
+ }
6587
6696
  function normalizeColor(color2) {
6588
6697
  const rgbMatch = color2.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
6589
6698
  if (rgbMatch) {
@@ -6916,10 +7025,6 @@ function createGraph(config) {
6916
7025
  renderPipeline.execute().then((selections) => {
6917
7026
  interactionManager.setupInteractions(selections);
6918
7027
  setupAdditionalComponents();
6919
- if (graphManager.needsImmediateFitView) {
6920
- graphManager.needsImmediateFitView = false;
6921
- fitViewWithInitialPositions();
6922
- }
6923
7028
  }).catch((error) => {
6924
7029
  console.error("[Polly Graph] Render failed:", error);
6925
7030
  });
@@ -6990,56 +7095,34 @@ function createGraph(config) {
6990
7095
  const svg = graphManager.svgElement;
6991
7096
  const nodes = config.nodes;
6992
7097
  if (nodes.length === 0) return;
6993
- const positions = nodes.map((node) => ({
6994
- x: node.x ?? 0,
6995
- y: node.y ?? 0
6996
- }));
6997
- const xExtent = extent(positions, (d) => d.x);
6998
- const yExtent = extent(positions, (d) => d.y);
6999
- const padding = 50;
7000
- const width = graphManager.dimensions.width - padding * 2;
7001
- const height = graphManager.dimensions.height - padding * 2;
7002
- const nodeWidth = xExtent[1] - xExtent[0];
7003
- const nodeHeight = yExtent[1] - yExtent[0];
7004
- if (nodeWidth === 0 || nodeHeight === 0) {
7098
+ const graphRoot = select_default2(svg).select('[data-layer="viewport"]');
7099
+ const graphRootNode = graphRoot.node();
7100
+ if (!graphRootNode) {
7101
+ console.warn("[Polly Graph] Cannot fit view: graph root not found");
7005
7102
  return;
7006
7103
  }
7007
- const scale = Math.min(width / nodeWidth, height / nodeHeight, 3);
7008
- const centerX = (xExtent[0] + xExtent[1]) / 2;
7009
- const centerY = (yExtent[0] + yExtent[1]) / 2;
7010
- const transform2 = identity2.translate(graphManager.dimensions.width / 2, graphManager.dimensions.height / 2).scale(scale).translate(-centerX, -centerY);
7011
- if (graphManager.zoomBehavior) {
7012
- select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.transform, transform2);
7013
- }
7014
- }
7015
- function fitViewWithInitialPositions() {
7016
- if (!graphManager.simulation || !graphManager.svgElement) {
7017
- console.warn("[Polly Graph] Cannot fit view: simulation or SVG not available");
7104
+ let bounds;
7105
+ try {
7106
+ bounds = graphRootNode.getBBox();
7107
+ } catch {
7108
+ console.warn("[Polly Graph] Cannot get bounds, falling back to default view");
7018
7109
  return;
7019
7110
  }
7020
- const svg = graphManager.svgElement;
7021
- const nodes = config.nodes;
7022
- if (nodes.length === 0) return;
7023
- const positions = nodes.map((node) => ({
7024
- x: node.initialX ?? node.x ?? 0,
7025
- y: node.initialY ?? node.y ?? 0
7026
- }));
7027
- const xExtent = extent(positions, (d) => d.x);
7028
- const yExtent = extent(positions, (d) => d.y);
7029
- const padding = 50;
7030
- const width = graphManager.dimensions.width - padding * 2;
7031
- const height = graphManager.dimensions.height - padding * 2;
7032
- const nodeWidth = xExtent[1] - xExtent[0];
7033
- const nodeHeight = yExtent[1] - yExtent[0];
7034
- if (nodeWidth === 0 || nodeHeight === 0) {
7111
+ if (bounds.width === 0 || bounds.height === 0) {
7035
7112
  return;
7036
7113
  }
7037
- const scale = Math.min(width / nodeWidth, height / nodeHeight, 3);
7038
- const centerX = (xExtent[0] + xExtent[1]) / 2;
7039
- const centerY = (yExtent[0] + yExtent[1]) / 2;
7040
- const transform2 = identity2.translate(graphManager.dimensions.width / 2, graphManager.dimensions.height / 2).scale(scale).translate(-centerX, -centerY);
7114
+ const svgRect = svg.getBoundingClientRect();
7115
+ const padding = 40;
7116
+ const availableWidth = svgRect.width - padding * 2;
7117
+ const availableHeight = svgRect.height - padding * 2;
7118
+ const scaleX = availableWidth / bounds.width;
7119
+ const scaleY = availableHeight / bounds.height;
7120
+ const scale = Math.min(scaleX, scaleY, 4);
7121
+ const centerX = bounds.x + bounds.width / 2;
7122
+ const centerY = bounds.y + bounds.height / 2;
7123
+ const transform2 = identity2.translate(svgRect.width / 2, svgRect.height / 2).scale(scale).translate(-centerX, -centerY);
7041
7124
  if (graphManager.zoomBehavior) {
7042
- select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.transform, transform2);
7125
+ select_default2(svg).transition().duration(750).call(graphManager.zoomBehavior.transform, transform2);
7043
7126
  }
7044
7127
  }
7045
7128
  function exportGraph(fileName) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-graph",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Reusable D3-based graph visualization SDK with configurable nodes, links, labels, interactions, and layout behaviors.",
5
5
  "license": "MIT",
6
6
  "author": "Badal",
@@ -52,11 +52,14 @@
52
52
  "lint": "eslint \"src/**/*.ts\"",
53
53
  "typecheck": "tsc --noEmit",
54
54
  "test": "vitest run --passWithNoTests",
55
- "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build"
55
+ "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
56
+ "version:patch": "npm version patch",
57
+ "version:minor": "npm version minor",
58
+ "version:major": "npm version major",
59
+ "version:prerelease": "npm version prerelease"
56
60
  },
57
61
  "dependencies": {
58
- "d3": "7.9.0",
59
- "html2canvas": "^1.4.1"
62
+ "d3": "7.9.0"
60
63
  },
61
64
  "devDependencies": {
62
65
  "@eslint/js": "^9.39.4",