polly-graph 0.1.8 → 0.1.9

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;
@@ -6099,6 +6081,27 @@ var SelectionManager = class {
6099
6081
  }
6100
6082
  };
6101
6083
 
6084
+ // src/utils/resolve-node-style.ts
6085
+ var DEFAULT_NODE_HOVER_STYLE = {
6086
+ stroke: `color-mix(in srgb, ${"#8E42EE" /* PURPLE */}, ${"#000000" /* BLACK */} 20%)`,
6087
+ strokeWidth: 3
6088
+ };
6089
+ function resolveNodeStyle(params) {
6090
+ if (params.isSelected) {
6091
+ return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6092
+ }
6093
+ if (params.isHovered) {
6094
+ return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6095
+ }
6096
+ return void 0;
6097
+ }
6098
+ function mergeNodeStyle(base, override) {
6099
+ return {
6100
+ ...base,
6101
+ ...override
6102
+ };
6103
+ }
6104
+
6102
6105
  // src/core/interaction-manager.ts
6103
6106
  var InteractionManager = class {
6104
6107
  constructor(manager) {
@@ -6150,7 +6153,13 @@ var InteractionManager = class {
6150
6153
  tooltipConfig: this.manager.config.interaction.hover.tooltip
6151
6154
  });
6152
6155
  }
6153
- createNodeHover(selections.nodeSelection, this.manager.config.interaction.hover.nodeStyle);
6156
+ const defaultNodeHoverStyle = resolveNodeStyle({
6157
+ node: {},
6158
+ // We don't need the actual node for defaults
6159
+ interaction: this.manager.config.interaction,
6160
+ isHovered: true
6161
+ });
6162
+ createNodeHover(selections.nodeSelection, defaultNodeHoverStyle);
6154
6163
  createLinkHover(selections.linkSelection, this.manager.config.interaction.hover.linkStyle);
6155
6164
  }
6156
6165
  }
@@ -6470,6 +6479,7 @@ async function captureAndDownloadGraph(container, options = {}) {
6470
6479
  }
6471
6480
  svgClone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
6472
6481
  svgClone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
6482
+ fixTextStyling(svgClone);
6473
6483
  const svgString = new XMLSerializer().serializeToString(svgClone);
6474
6484
  const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
6475
6485
  const svgUrl = URL.createObjectURL(svgBlob);
@@ -6616,6 +6626,64 @@ function createLegendSVGElement(legendEntries, dimensions) {
6616
6626
  });
6617
6627
  return legendGroup;
6618
6628
  }
6629
+ function fixTextStyling(svgElement) {
6630
+ const nodeLabelsLayer = svgElement.querySelector('[data-layer="node-labels"]');
6631
+ if (nodeLabelsLayer) {
6632
+ const nodeTexts = nodeLabelsLayer.querySelectorAll("text");
6633
+ nodeTexts.forEach((text) => {
6634
+ const textElement = text;
6635
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6636
+ if (!textElement.getAttribute("font-size")) {
6637
+ textElement.setAttribute("font-size", "9");
6638
+ }
6639
+ textElement.setAttribute("font-weight", "500");
6640
+ textElement.setAttribute("text-anchor", "middle");
6641
+ textElement.setAttribute("dominant-baseline", "central");
6642
+ if (!textElement.getAttribute("fill")) {
6643
+ textElement.setAttribute("fill", "#374151");
6644
+ }
6645
+ });
6646
+ }
6647
+ const linkLabels = svgElement.querySelectorAll("g.link-label");
6648
+ linkLabels.forEach((labelGroup) => {
6649
+ const textElement = labelGroup.querySelector("text");
6650
+ if (textElement) {
6651
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6652
+ if (!textElement.getAttribute("font-size")) {
6653
+ textElement.setAttribute("font-size", "10");
6654
+ }
6655
+ textElement.setAttribute("font-weight", "400");
6656
+ textElement.setAttribute("text-anchor", "middle");
6657
+ textElement.setAttribute("dominant-baseline", "central");
6658
+ if (!textElement.getAttribute("fill")) {
6659
+ textElement.setAttribute("fill", "#6b7280");
6660
+ }
6661
+ }
6662
+ const rectElement = labelGroup.querySelector("rect");
6663
+ if (rectElement) {
6664
+ if (!rectElement.getAttribute("fill")) {
6665
+ rectElement.setAttribute("fill", "#ffffff");
6666
+ }
6667
+ if (!rectElement.getAttribute("stroke")) {
6668
+ rectElement.setAttribute("stroke", "#e5e7eb");
6669
+ }
6670
+ if (!rectElement.getAttribute("stroke-width")) {
6671
+ rectElement.setAttribute("stroke-width", "1");
6672
+ }
6673
+ rectElement.setAttribute("rx", "4");
6674
+ }
6675
+ });
6676
+ const allTexts = svgElement.querySelectorAll("text");
6677
+ allTexts.forEach((text) => {
6678
+ const textElement = text;
6679
+ if (!textElement.getAttribute("font-family")) {
6680
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6681
+ }
6682
+ if (!textElement.getAttribute("font-size")) {
6683
+ textElement.setAttribute("font-size", "10");
6684
+ }
6685
+ });
6686
+ }
6619
6687
  function normalizeColor(color2) {
6620
6688
  const rgbMatch = color2.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
6621
6689
  if (rgbMatch) {
@@ -6948,10 +7016,6 @@ function createGraph(config) {
6948
7016
  renderPipeline.execute().then((selections) => {
6949
7017
  interactionManager.setupInteractions(selections);
6950
7018
  setupAdditionalComponents();
6951
- if (graphManager.needsImmediateFitView) {
6952
- graphManager.needsImmediateFitView = false;
6953
- fitViewWithInitialPositions();
6954
- }
6955
7019
  }).catch((error) => {
6956
7020
  console.error("[Polly Graph] Render failed:", error);
6957
7021
  });
@@ -7022,56 +7086,34 @@ function createGraph(config) {
7022
7086
  const svg = graphManager.svgElement;
7023
7087
  const nodes = config.nodes;
7024
7088
  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) {
7089
+ const graphRoot = select_default2(svg).select('[data-layer="viewport"]');
7090
+ const graphRootNode = graphRoot.node();
7091
+ if (!graphRootNode) {
7092
+ console.warn("[Polly Graph] Cannot fit view: graph root not found");
7037
7093
  return;
7038
7094
  }
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");
7095
+ let bounds;
7096
+ try {
7097
+ bounds = graphRootNode.getBBox();
7098
+ } catch {
7099
+ console.warn("[Polly Graph] Cannot get bounds, falling back to default view");
7050
7100
  return;
7051
7101
  }
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) {
7102
+ if (bounds.width === 0 || bounds.height === 0) {
7067
7103
  return;
7068
7104
  }
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);
7105
+ const svgRect = svg.getBoundingClientRect();
7106
+ const padding = 40;
7107
+ const availableWidth = svgRect.width - padding * 2;
7108
+ const availableHeight = svgRect.height - padding * 2;
7109
+ const scaleX = availableWidth / bounds.width;
7110
+ const scaleY = availableHeight / bounds.height;
7111
+ const scale = Math.min(scaleX, scaleY, 4);
7112
+ const centerX = bounds.x + bounds.width / 2;
7113
+ const centerY = bounds.y + bounds.height / 2;
7114
+ const transform2 = identity2.translate(svgRect.width / 2, svgRect.height / 2).scale(scale).translate(-centerX, -centerY);
7073
7115
  if (graphManager.zoomBehavior) {
7074
- select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.transform, transform2);
7116
+ select_default2(svg).transition().duration(750).call(graphManager.zoomBehavior.transform, transform2);
7075
7117
  }
7076
7118
  }
7077
7119
  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.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;
@@ -6067,6 +6049,27 @@ var SelectionManager = class {
6067
6049
  }
6068
6050
  };
6069
6051
 
6052
+ // src/utils/resolve-node-style.ts
6053
+ var DEFAULT_NODE_HOVER_STYLE = {
6054
+ stroke: `color-mix(in srgb, ${"#8E42EE" /* PURPLE */}, ${"#000000" /* BLACK */} 20%)`,
6055
+ strokeWidth: 3
6056
+ };
6057
+ function resolveNodeStyle(params) {
6058
+ if (params.isSelected) {
6059
+ return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6060
+ }
6061
+ if (params.isHovered) {
6062
+ return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6063
+ }
6064
+ return void 0;
6065
+ }
6066
+ function mergeNodeStyle(base, override) {
6067
+ return {
6068
+ ...base,
6069
+ ...override
6070
+ };
6071
+ }
6072
+
6070
6073
  // src/core/interaction-manager.ts
6071
6074
  var InteractionManager = class {
6072
6075
  constructor(manager) {
@@ -6118,7 +6121,13 @@ var InteractionManager = class {
6118
6121
  tooltipConfig: this.manager.config.interaction.hover.tooltip
6119
6122
  });
6120
6123
  }
6121
- createNodeHover(selections.nodeSelection, this.manager.config.interaction.hover.nodeStyle);
6124
+ const defaultNodeHoverStyle = resolveNodeStyle({
6125
+ node: {},
6126
+ // We don't need the actual node for defaults
6127
+ interaction: this.manager.config.interaction,
6128
+ isHovered: true
6129
+ });
6130
+ createNodeHover(selections.nodeSelection, defaultNodeHoverStyle);
6122
6131
  createLinkHover(selections.linkSelection, this.manager.config.interaction.hover.linkStyle);
6123
6132
  }
6124
6133
  }
@@ -6438,6 +6447,7 @@ async function captureAndDownloadGraph(container, options = {}) {
6438
6447
  }
6439
6448
  svgClone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
6440
6449
  svgClone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
6450
+ fixTextStyling(svgClone);
6441
6451
  const svgString = new XMLSerializer().serializeToString(svgClone);
6442
6452
  const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
6443
6453
  const svgUrl = URL.createObjectURL(svgBlob);
@@ -6584,6 +6594,64 @@ function createLegendSVGElement(legendEntries, dimensions) {
6584
6594
  });
6585
6595
  return legendGroup;
6586
6596
  }
6597
+ function fixTextStyling(svgElement) {
6598
+ const nodeLabelsLayer = svgElement.querySelector('[data-layer="node-labels"]');
6599
+ if (nodeLabelsLayer) {
6600
+ const nodeTexts = nodeLabelsLayer.querySelectorAll("text");
6601
+ nodeTexts.forEach((text) => {
6602
+ const textElement = text;
6603
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6604
+ if (!textElement.getAttribute("font-size")) {
6605
+ textElement.setAttribute("font-size", "9");
6606
+ }
6607
+ textElement.setAttribute("font-weight", "500");
6608
+ textElement.setAttribute("text-anchor", "middle");
6609
+ textElement.setAttribute("dominant-baseline", "central");
6610
+ if (!textElement.getAttribute("fill")) {
6611
+ textElement.setAttribute("fill", "#374151");
6612
+ }
6613
+ });
6614
+ }
6615
+ const linkLabels = svgElement.querySelectorAll("g.link-label");
6616
+ linkLabels.forEach((labelGroup) => {
6617
+ const textElement = labelGroup.querySelector("text");
6618
+ if (textElement) {
6619
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6620
+ if (!textElement.getAttribute("font-size")) {
6621
+ textElement.setAttribute("font-size", "10");
6622
+ }
6623
+ textElement.setAttribute("font-weight", "400");
6624
+ textElement.setAttribute("text-anchor", "middle");
6625
+ textElement.setAttribute("dominant-baseline", "central");
6626
+ if (!textElement.getAttribute("fill")) {
6627
+ textElement.setAttribute("fill", "#6b7280");
6628
+ }
6629
+ }
6630
+ const rectElement = labelGroup.querySelector("rect");
6631
+ if (rectElement) {
6632
+ if (!rectElement.getAttribute("fill")) {
6633
+ rectElement.setAttribute("fill", "#ffffff");
6634
+ }
6635
+ if (!rectElement.getAttribute("stroke")) {
6636
+ rectElement.setAttribute("stroke", "#e5e7eb");
6637
+ }
6638
+ if (!rectElement.getAttribute("stroke-width")) {
6639
+ rectElement.setAttribute("stroke-width", "1");
6640
+ }
6641
+ rectElement.setAttribute("rx", "4");
6642
+ }
6643
+ });
6644
+ const allTexts = svgElement.querySelectorAll("text");
6645
+ allTexts.forEach((text) => {
6646
+ const textElement = text;
6647
+ if (!textElement.getAttribute("font-family")) {
6648
+ textElement.setAttribute("font-family", 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif');
6649
+ }
6650
+ if (!textElement.getAttribute("font-size")) {
6651
+ textElement.setAttribute("font-size", "10");
6652
+ }
6653
+ });
6654
+ }
6587
6655
  function normalizeColor(color2) {
6588
6656
  const rgbMatch = color2.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
6589
6657
  if (rgbMatch) {
@@ -6916,10 +6984,6 @@ function createGraph(config) {
6916
6984
  renderPipeline.execute().then((selections) => {
6917
6985
  interactionManager.setupInteractions(selections);
6918
6986
  setupAdditionalComponents();
6919
- if (graphManager.needsImmediateFitView) {
6920
- graphManager.needsImmediateFitView = false;
6921
- fitViewWithInitialPositions();
6922
- }
6923
6987
  }).catch((error) => {
6924
6988
  console.error("[Polly Graph] Render failed:", error);
6925
6989
  });
@@ -6990,56 +7054,34 @@ function createGraph(config) {
6990
7054
  const svg = graphManager.svgElement;
6991
7055
  const nodes = config.nodes;
6992
7056
  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) {
7057
+ const graphRoot = select_default2(svg).select('[data-layer="viewport"]');
7058
+ const graphRootNode = graphRoot.node();
7059
+ if (!graphRootNode) {
7060
+ console.warn("[Polly Graph] Cannot fit view: graph root not found");
7005
7061
  return;
7006
7062
  }
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");
7063
+ let bounds;
7064
+ try {
7065
+ bounds = graphRootNode.getBBox();
7066
+ } catch {
7067
+ console.warn("[Polly Graph] Cannot get bounds, falling back to default view");
7018
7068
  return;
7019
7069
  }
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) {
7070
+ if (bounds.width === 0 || bounds.height === 0) {
7035
7071
  return;
7036
7072
  }
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);
7073
+ const svgRect = svg.getBoundingClientRect();
7074
+ const padding = 40;
7075
+ const availableWidth = svgRect.width - padding * 2;
7076
+ const availableHeight = svgRect.height - padding * 2;
7077
+ const scaleX = availableWidth / bounds.width;
7078
+ const scaleY = availableHeight / bounds.height;
7079
+ const scale = Math.min(scaleX, scaleY, 4);
7080
+ const centerX = bounds.x + bounds.width / 2;
7081
+ const centerY = bounds.y + bounds.height / 2;
7082
+ const transform2 = identity2.translate(svgRect.width / 2, svgRect.height / 2).scale(scale).translate(-centerX, -centerY);
7041
7083
  if (graphManager.zoomBehavior) {
7042
- select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.transform, transform2);
7084
+ select_default2(svg).transition().duration(750).call(graphManager.zoomBehavior.transform, transform2);
7043
7085
  }
7044
7086
  }
7045
7087
  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.9",
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",
@@ -55,8 +55,7 @@
55
55
  "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build"
56
56
  },
57
57
  "dependencies": {
58
- "d3": "7.9.0",
59
- "html2canvas": "^1.4.1"
58
+ "d3": "7.9.0"
60
59
  },
61
60
  "devDependencies": {
62
61
  "@eslint/js": "^9.39.4",