polly-graph 0.1.3 → 0.1.5

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.js CHANGED
@@ -3608,104 +3608,45 @@ function manyBody_default() {
3608
3608
  return force;
3609
3609
  }
3610
3610
 
3611
- // node_modules/d3-force/src/x.js
3612
- function x_default2(x3) {
3613
- var strength = constant_default5(0.1), nodes, strengths, xz;
3614
- if (typeof x3 !== "function") x3 = constant_default5(x3 == null ? 0 : +x3);
3615
- function force(alpha) {
3616
- for (var i = 0, n = nodes.length, node; i < n; ++i) {
3617
- node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha;
3618
- }
3619
- }
3620
- function initialize() {
3621
- if (!nodes) return;
3622
- var i, n = nodes.length;
3623
- strengths = new Array(n);
3624
- xz = new Array(n);
3625
- for (i = 0; i < n; ++i) {
3626
- strengths[i] = isNaN(xz[i] = +x3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
3627
- }
3628
- }
3629
- force.initialize = function(_) {
3630
- nodes = _;
3631
- initialize();
3632
- };
3633
- force.strength = function(_) {
3634
- return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
3635
- };
3636
- force.x = function(_) {
3637
- return arguments.length ? (x3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : x3;
3638
- };
3639
- return force;
3640
- }
3641
-
3642
- // node_modules/d3-force/src/y.js
3643
- function y_default2(y3) {
3644
- var strength = constant_default5(0.1), nodes, strengths, yz;
3645
- if (typeof y3 !== "function") y3 = constant_default5(y3 == null ? 0 : +y3);
3646
- function force(alpha) {
3647
- for (var i = 0, n = nodes.length, node; i < n; ++i) {
3648
- node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha;
3649
- }
3650
- }
3651
- function initialize() {
3652
- if (!nodes) return;
3653
- var i, n = nodes.length;
3654
- strengths = new Array(n);
3655
- yz = new Array(n);
3656
- for (i = 0; i < n; ++i) {
3657
- strengths[i] = isNaN(yz[i] = +y3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
3658
- }
3659
- }
3660
- force.initialize = function(_) {
3661
- nodes = _;
3662
- initialize();
3663
- };
3664
- force.strength = function(_) {
3665
- return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
3666
- };
3667
- force.y = function(_) {
3668
- return arguments.length ? (y3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : y3;
3669
- };
3670
- return force;
3671
- }
3672
-
3673
3611
  // src/core/create-graph-layers.ts
3674
- function createGraphLayers(svg) {
3675
- while (svg.firstChild) {
3676
- svg.removeChild(svg.firstChild);
3677
- }
3678
- const createGroup = (className) => {
3612
+ function createGraphLayers(host) {
3613
+ host.innerHTML = "";
3614
+ const rootContainer = document.createElement("div");
3615
+ rootContainer.className = "pg-root";
3616
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3617
+ svg.setAttribute("class", "pg-canvas");
3618
+ const overlay = document.createElement("div");
3619
+ overlay.className = "pg-overlay";
3620
+ rootContainer.appendChild(svg);
3621
+ rootContainer.appendChild(overlay);
3622
+ host.appendChild(rootContainer);
3623
+ const createGroup = (layerName) => {
3679
3624
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
3680
- group.setAttribute("class", className);
3681
- group.setAttribute("data-layer", className);
3625
+ group.setAttribute("class", `pg-layer-${layerName}`);
3626
+ group.setAttribute("data-layer", layerName);
3682
3627
  return group;
3683
3628
  };
3684
3629
  const interactionLayer = createGroup("interaction-layer");
3685
3630
  const interactionRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
3686
- const interactionAttributes = {
3687
- class: "interaction-surface",
3688
- width: "100%",
3689
- height: "100%",
3690
- fill: "transparent",
3691
- "pointer-events": "all"
3692
- };
3693
- Object.entries(interactionAttributes).forEach(([key, value]) => {
3694
- interactionRect.setAttribute(key, value);
3695
- });
3631
+ interactionRect.setAttribute("class", "pg-interaction-surface");
3632
+ interactionRect.setAttribute("fill", "transparent");
3633
+ interactionRect.setAttribute("pointer-events", "all");
3696
3634
  interactionLayer.appendChild(interactionRect);
3697
- const root2 = createGroup("knowledge-graph-root");
3635
+ const graphRoot = createGroup("viewport");
3698
3636
  const layers = {
3637
+ svg,
3638
+ overlay,
3699
3639
  interactionLayer,
3700
3640
  interactionRect,
3701
- root: root2,
3641
+ root: graphRoot,
3642
+ // These keys now match your ctx.root.select('[data-layer="..."]') calls
3702
3643
  links: createGroup("links"),
3703
3644
  linkLabels: createGroup("link-labels"),
3704
3645
  nodeRings: createGroup("node-rings"),
3705
3646
  nodes: createGroup("nodes"),
3706
3647
  nodeLabels: createGroup("node-labels")
3707
3648
  };
3708
- root2.append(
3649
+ graphRoot.append(
3709
3650
  layers.links,
3710
3651
  layers.linkLabels,
3711
3652
  layers.nodeRings,
@@ -3713,7 +3654,7 @@ function createGraphLayers(svg) {
3713
3654
  layers.nodeLabels
3714
3655
  );
3715
3656
  svg.appendChild(interactionLayer);
3716
- svg.appendChild(root2);
3657
+ svg.appendChild(graphRoot);
3717
3658
  return layers;
3718
3659
  }
3719
3660
 
@@ -3775,10 +3716,17 @@ function createGraphSimulation(config) {
3775
3716
  });
3776
3717
  const simulation = simulation_default(config.nodes).alpha(0.9).alphaDecay(0.12).alphaMin(0.03).velocityDecay(0.5).force(
3777
3718
  "link",
3778
- link_default(config.links).id((d) => d.id).distance(150).strength(0.4)
3719
+ link_default(config.links).id((d) => d.id).distance((d) => {
3720
+ const source = d.source;
3721
+ const target = d.target;
3722
+ const sourceR = source.style?.radius || 20;
3723
+ const targetR = target.style?.radius || 20;
3724
+ const labelBuffer = d.style?.label?.height || 40;
3725
+ return (sourceR + targetR + labelBuffer) * 2;
3726
+ }).strength(0.8)
3779
3727
  ).force("charge", manyBody_default().strength(-220)).force(
3780
3728
  "collide",
3781
- collide_default().radius((node) => (node.style?.radius ?? 12) + 10).strength(0.9)
3729
+ collide_default().radius((node) => (node.style?.radius ?? 12) + 10).iterations(2)
3782
3730
  ).force("center", center_default(centerX, centerY).strength(0.08));
3783
3731
  return { simulation };
3784
3732
  }
@@ -3830,49 +3778,51 @@ function getControlIcon(icon) {
3830
3778
  }
3831
3779
 
3832
3780
  // src/controls/create-graph-controls.ts
3833
- function createGraphControls(container, graph, config) {
3781
+ function createGraphControls(overlay, graph, config) {
3834
3782
  let root2 = null;
3835
3783
  function mount() {
3836
3784
  if (!config.enabled) {
3837
3785
  return;
3838
3786
  }
3839
- const parent = container.parentElement;
3840
- if (!parent) {
3841
- return;
3842
- }
3843
3787
  root2 = document.createElement("div");
3844
3788
  root2.className = "pg-controls";
3845
- applyPosition(root2, config);
3846
- applyOrientation(root2, config);
3789
+ const position = resolveControlsPosition(config.position);
3790
+ root2.classList.add(`pg-pos-${position}`);
3791
+ const orientation = resolveControlsOrientation(config.orientation);
3792
+ root2.classList.add(`pg-orient-${orientation}`);
3793
+ if (config.offset) {
3794
+ root2.style.setProperty("--pg-controls-offset-x", `${config.offset.x}px`);
3795
+ root2.style.setProperty("--pg-controls-offset-y", `${config.offset.y}px`);
3796
+ }
3847
3797
  appendControls(root2, config, graph);
3848
- parent.appendChild(root2);
3798
+ overlay.appendChild(root2);
3849
3799
  }
3850
3800
  function appendControls(root3, config2, graph2) {
3851
- if (shouldRenderControl(config2, "zoomIn")) {
3852
- root3.appendChild(createButton("zoom-in", "Zoom in", graph2.zoomIn.bind(graph2)));
3853
- }
3854
- if (shouldRenderControl(config2, "zoomOut")) {
3855
- root3.appendChild(createButton("zoom-out", "Zoom out", graph2.zoomOut.bind(graph2)));
3856
- }
3857
- if (shouldRenderControl(config2, "fit")) {
3858
- root3.appendChild(createButton("fit", "Fit view", graph2.fitView.bind(graph2)));
3859
- }
3860
- if (shouldRenderControl(config2, "reset")) {
3861
- root3.appendChild(createButton("reset", "Reset view", graph2.resetView.bind(graph2)));
3862
- }
3801
+ const actions = [
3802
+ { key: "zoomIn", icon: "zoom-in", label: "Zoom in", fn: graph2.zoomIn.bind(graph2) },
3803
+ { key: "zoomOut", icon: "zoom-out", label: "Zoom out", fn: graph2.zoomOut.bind(graph2) },
3804
+ { key: "fit", icon: "fit", label: "Fit view", fn: graph2.fitView.bind(graph2) },
3805
+ { key: "reset", icon: "reset", label: "Reset view", fn: graph2.resetView.bind(graph2) }
3806
+ ];
3807
+ actions.forEach((action) => {
3808
+ if (shouldRenderControl(config2, action.key)) {
3809
+ root3.appendChild(createButton(action.icon, action.label, action.fn));
3810
+ }
3811
+ });
3863
3812
  }
3864
3813
  function createButton(type, label, onClick) {
3865
3814
  const button = document.createElement("button");
3815
+ button.className = "pg-control-btn";
3866
3816
  button.type = "button";
3867
3817
  button.setAttribute("aria-label", label);
3868
3818
  const wrapper = document.createElement("div");
3819
+ wrapper.className = "pg-icon-wrapper";
3869
3820
  wrapper.innerHTML = getControlIcon(type);
3870
3821
  const svg = wrapper.querySelector("svg");
3871
- if (!svg) {
3872
- throw new Error(`Invalid SVG for icon: ${type}`);
3822
+ if (svg) {
3823
+ svg.classList.add("pg-icon");
3824
+ button.appendChild(svg);
3873
3825
  }
3874
- svg.classList.add("pg-icon");
3875
- button.appendChild(svg);
3876
3826
  button.addEventListener("click", onClick);
3877
3827
  return button;
3878
3828
  }
@@ -3880,39 +3830,70 @@ function createGraphControls(container, graph, config) {
3880
3830
  if (!root2) {
3881
3831
  return;
3882
3832
  }
3883
- const clone = root2.cloneNode(true);
3884
- root2.replaceWith(clone);
3833
+ if (root2.parentNode === overlay) {
3834
+ overlay.removeChild(root2);
3835
+ }
3885
3836
  root2 = null;
3886
3837
  }
3887
3838
  return { mount, destroy };
3888
3839
  }
3889
- function applyPosition(el, config) {
3890
- const position = resolveControlsPosition(config.position);
3891
- const offset = config.offset ?? { x: 16, y: 16 };
3892
- el.style.position = "absolute";
3893
- switch (position) {
3894
- case "bottom-left":
3895
- el.style.left = `${offset.x}px`;
3896
- el.style.bottom = `${offset.y}px`;
3897
- break;
3898
- case "bottom-right":
3899
- el.style.right = `${offset.x}px`;
3900
- el.style.bottom = `${offset.y}px`;
3901
- break;
3902
- case "top-left":
3903
- el.style.left = `${offset.x}px`;
3904
- el.style.top = `${offset.y}px`;
3905
- break;
3906
- case "top-right":
3907
- el.style.right = `${offset.x}px`;
3908
- el.style.top = `${offset.y}px`;
3909
- break;
3840
+
3841
+ // src/assets/caret.svg?raw
3842
+ var caret_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M9 20L16.5 12L9 4" />\n</svg>';
3843
+
3844
+ // src/legends/graph-legend-icon.ts
3845
+ var LEGEND_ICON_MAP = { caret: caret_default };
3846
+ function getLegendIcon(icon) {
3847
+ const raw = LEGEND_ICON_MAP[icon];
3848
+ if (!raw) {
3849
+ throw new Error(`Legend icon not found: ${icon}`);
3910
3850
  }
3851
+ return raw.replace("<svg", '<svg class="pg-icon"');
3911
3852
  }
3912
- function applyOrientation(el, config) {
3913
- const orientation = resolveControlsOrientation(config.orientation);
3914
- el.style.display = "flex";
3915
- el.style.flexDirection = orientation === "vertical" ? "column" : "row";
3853
+
3854
+ // src/legends/create-graph-legends.ts
3855
+ function createGraphLegend(overlay, config) {
3856
+ const legendWrapper = document.createElement("div");
3857
+ legendWrapper.className = "pg-legend";
3858
+ const position = config.position || "bottom-right";
3859
+ legendWrapper.classList.add(`pg-pos-${position}`);
3860
+ if (config.defaultExpanded === false) {
3861
+ legendWrapper.classList.add("pg-is-collapsed");
3862
+ }
3863
+ if (config.collapsible) {
3864
+ const toggleBtn = document.createElement("button");
3865
+ toggleBtn.className = "pg-legend-toggle";
3866
+ toggleBtn.type = "button";
3867
+ toggleBtn.innerHTML = getLegendIcon("caret");
3868
+ toggleBtn.onclick = (e) => {
3869
+ e.stopPropagation();
3870
+ legendWrapper.classList.toggle("pg-is-collapsed");
3871
+ };
3872
+ legendWrapper.appendChild(toggleBtn);
3873
+ }
3874
+ const body = document.createElement("div");
3875
+ body.className = "pg-legend-body";
3876
+ const list = document.createElement("ul");
3877
+ list.className = "pg-legend-list";
3878
+ config.items.forEach((item) => {
3879
+ const listItem = document.createElement("li");
3880
+ listItem.className = "pg-legend-item";
3881
+ const swatch = document.createElement("span");
3882
+ swatch.className = `pg-legend-swatch is-${item.shape || "circle"}`;
3883
+ swatch.style.backgroundColor = item.color;
3884
+ const label = document.createElement("span");
3885
+ label.className = "pg-legend-label";
3886
+ label.innerText = item.label;
3887
+ listItem.appendChild(swatch);
3888
+ listItem.appendChild(label);
3889
+ list.appendChild(listItem);
3890
+ });
3891
+ body.appendChild(list);
3892
+ legendWrapper.appendChild(body);
3893
+ overlay.appendChild(legendWrapper);
3894
+ return () => {
3895
+ if (legendWrapper.parentNode === overlay) overlay.removeChild(legendWrapper);
3896
+ };
3916
3897
  }
3917
3898
 
3918
3899
  // src/utils/resolve-link-style.ts
@@ -3928,6 +3909,7 @@ var DEFAULT_LINK_STYLE = {
3928
3909
  },
3929
3910
  label: {
3930
3911
  enabled: true,
3912
+ visibility: "always",
3931
3913
  backgroundFill: "color-mix(in srgb, #8E42EE, #FFFFFF 90%)",
3932
3914
  borderColor: "color-mix(in srgb, #8E42EE, #FFFFFF 10%)",
3933
3915
  borderWidth: 1.5,
@@ -3963,6 +3945,7 @@ function mergeLinkStyle(base, override) {
3963
3945
  },
3964
3946
  label: {
3965
3947
  enabled: override?.label?.enabled ?? base.label.enabled,
3948
+ visibility: override?.label?.visibility ?? base.label.visibility,
3966
3949
  backgroundFill: override?.label?.backgroundFill ?? base.label.backgroundFill,
3967
3950
  borderColor: override?.label?.borderColor ?? base.label.borderColor,
3968
3951
  borderWidth: override?.label?.borderWidth ?? base.label.borderWidth,
@@ -4063,7 +4046,14 @@ function getLinkKey(link) {
4063
4046
  }
4064
4047
  function renderLinks(ctx, links) {
4065
4048
  const renderableLinks = createRenderableLinks(ctx, links);
4066
- return ctx.root.select('[data-layer="links"]').selectAll("line").data(renderableLinks, (item) => getLinkKey(item.link)).join("line").attr("stroke", (item) => item.style.stroke).attr("stroke-width", (item) => item.style.strokeWidth).attr("opacity", (item) => item.style.opacity).attr("marker-end", (item) => item.markerEnd);
4049
+ const linkSelection = ctx.root.select('[data-layer="links"]').selectAll("line").data(renderableLinks, (item) => getLinkKey(item.link)).join("line").attr("class", "graph-link").attr("stroke", (item) => item.style.stroke).attr("stroke-width", (item) => item.style.strokeWidth).attr("opacity", (item) => item.style.opacity).attr("marker-end", (item) => item.markerEnd).style("pointer-events", "stroke");
4050
+ const labelSelection = ctx.root.selectAll(".link-label");
4051
+ linkSelection.on("mouseenter.label-hover", (_event, d) => {
4052
+ labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 1);
4053
+ }).on("mouseleave.label-hover", (_event, d) => {
4054
+ labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 0);
4055
+ });
4056
+ return linkSelection;
4067
4057
  }
4068
4058
 
4069
4059
  // src/renderer/nodes.ts
@@ -4133,7 +4123,10 @@ function renderNodeLabels(ctx, nodes) {
4133
4123
  // src/renderer/link-labels.ts
4134
4124
  function createRenderableLinks2(params, links) {
4135
4125
  return links.map(
4136
- (link) => ({ link, style: resolveLinkStyle({ link, interaction: params.interaction }) })
4126
+ (link) => ({
4127
+ link,
4128
+ style: resolveLinkStyle({ link, interaction: params.interaction })
4129
+ })
4137
4130
  ).filter(
4138
4131
  (item) => item.style.label.enabled && Boolean(item.link.label)
4139
4132
  );
@@ -4145,7 +4138,13 @@ function getLinkKey2(link) {
4145
4138
  }
4146
4139
  function renderLinkLabels(params, links) {
4147
4140
  const renderableLinks = createRenderableLinks2(params, links);
4148
- const labelSelection = params.root.select(".link-labels").selectAll(".link-label").data(renderableLinks, (item) => getLinkKey2(item.link)).join("g").attr("class", "link-label").attr("pointer-events", "auto").attr("cursor", "pointer");
4141
+ const labelSelection = params.root.select('[data-layer="link-labels"]').selectAll(".link-label").data(renderableLinks, (item) => getLinkKey2(item.link)).join("g").attr("class", "link-label").style("opacity", (item) => {
4142
+ const visibility = item.style.label.visibility ?? "always";
4143
+ return visibility === "always" ? 1 : 0;
4144
+ }).style("pointer-events", (item) => {
4145
+ const visibility = item.style.label.visibility ?? "always";
4146
+ return visibility === "always" ? "auto" : "none";
4147
+ }).style("cursor", "pointer");
4149
4148
  labelSelection.selectAll("rect").data((item) => [item]).join("rect").attr("rx", (item) => item.style.label.borderRadius).attr("ry", (item) => item.style.label.borderRadius).attr("height", (item) => item.style.label.height).attr("fill", (item) => item.style.label.backgroundFill).attr("stroke", (item) => item.style.label.borderColor).attr("stroke-width", (item) => item.style.label.borderWidth);
4150
4149
  labelSelection.selectAll("text").data((item) => [item]).join("text").attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("font-size", (item) => item.style.label.fontSize).attr("fill", (item) => item.style.label.textColor).text((item) => item.link.label ?? "");
4151
4150
  return labelSelection;
@@ -4174,58 +4173,35 @@ function createDragBehavior(simulation) {
4174
4173
 
4175
4174
  // src/interactions/create-node-hover.ts
4176
4175
  function createNodeHover(nodeSelection, hoverStyle) {
4177
- if (!hoverStyle) {
4178
- return;
4179
- }
4180
- nodeSelection.on(
4181
- "mouseenter.hover",
4182
- function(_event, node) {
4176
+ const firstNode = nodeSelection.node();
4177
+ if (!firstNode) return;
4178
+ if (hoverStyle) {
4179
+ nodeSelection.on("mouseenter.hover", function(_event, node) {
4183
4180
  const circle = this;
4184
- const hoverStroke = hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff";
4185
- const hoverStrokeWidth = hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5;
4186
- const hoverOpacity = hoverStyle.opacity ?? node.style?.opacity ?? 1;
4187
- circle.setAttribute(
4188
- "stroke",
4189
- hoverStroke
4190
- );
4191
- circle.setAttribute(
4192
- "stroke-width",
4193
- String(
4194
- hoverStrokeWidth
4195
- )
4196
- );
4197
- circle.setAttribute(
4198
- "opacity",
4199
- String(
4200
- hoverOpacity
4201
- )
4202
- );
4203
- }
4204
- ).on(
4205
- "mouseleave.hover",
4206
- function(_event, node) {
4181
+ circle.setAttribute("stroke", hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff");
4182
+ circle.setAttribute("stroke-width", String(hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5));
4183
+ circle.setAttribute("opacity", String(hoverStyle.opacity ?? node.style?.opacity ?? 1));
4184
+ }).on("mouseleave.hover", function(_event, node) {
4207
4185
  const circle = this;
4208
- const defaultStroke = node.style?.stroke ?? "#ffffff";
4209
- const defaultStrokeWidth = node.style?.strokeWidth ?? 1.5;
4210
- const defaultOpacity = node.style?.opacity ?? 1;
4211
- circle.setAttribute(
4212
- "stroke",
4213
- defaultStroke
4214
- );
4215
- circle.setAttribute(
4216
- "stroke-width",
4217
- String(
4218
- defaultStrokeWidth
4219
- )
4220
- );
4221
- circle.setAttribute(
4222
- "opacity",
4223
- String(
4224
- defaultOpacity
4225
- )
4226
- );
4227
- }
4228
- );
4186
+ circle.setAttribute("stroke", node.style?.stroke ?? "#ffffff");
4187
+ circle.setAttribute("stroke-width", String(node.style?.strokeWidth ?? 1.5));
4188
+ circle.setAttribute("opacity", String(node.style?.opacity ?? 1));
4189
+ });
4190
+ }
4191
+ const svgElement = firstNode.ownerSVGElement;
4192
+ if (!svgElement) return;
4193
+ const root2 = select_default2(svgElement);
4194
+ const labelSelection = root2.selectAll(".link-label");
4195
+ nodeSelection.on("mouseenter.labels", (_event, d) => {
4196
+ labelSelection.filter((item) => {
4197
+ if (item.style.label.visibility !== "hover") return false;
4198
+ const s = item.link.source;
4199
+ const t = item.link.target;
4200
+ return s.id === d.id || t.id === d.id;
4201
+ }).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4202
+ }).on("mouseleave.labels", (_event) => {
4203
+ labelSelection.filter((item) => item.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4204
+ });
4229
4205
  }
4230
4206
 
4231
4207
  // src/utils/resolve-tooltip-position.ts
@@ -4430,10 +4406,11 @@ function observeResize(element, onResize) {
4430
4406
  if (!entry) {
4431
4407
  return;
4432
4408
  }
4433
- onResize(
4434
- entry.contentRect.width,
4435
- entry.contentRect.height
4436
- );
4409
+ const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
4410
+ const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
4411
+ if (width > 0 && height > 0) {
4412
+ onResize(width, height);
4413
+ }
4437
4414
  }
4438
4415
  );
4439
4416
  observer.observe(element);
@@ -4466,54 +4443,94 @@ function getLinkTargetPoint(link) {
4466
4443
  };
4467
4444
  }
4468
4445
 
4446
+ // src/utils/export-graph.ts
4447
+ import html2canvas from "html2canvas";
4448
+ async function captureAndDownloadGraph(container, options = {}) {
4449
+ const {
4450
+ fileName = `graph-export-${Date.now()}.png`,
4451
+ backgroundColor = "#ffffff",
4452
+ pixelRatio = 2
4453
+ } = options;
4454
+ const root2 = container.querySelector(".pg-root");
4455
+ if (!root2) return;
4456
+ const controls = root2.querySelector(".pg-controls");
4457
+ const legendToggle = root2.querySelector(".pg-legend-toggle");
4458
+ const interactionLayer = root2.querySelector(".pg-interaction-layer");
4459
+ const legend = root2.querySelector(".pg-legend");
4460
+ const wasCollapsed = legend?.classList.contains("pg-is-collapsed");
4461
+ if (controls) controls.style.display = "none";
4462
+ if (legendToggle) legendToggle.style.display = "none";
4463
+ if (interactionLayer) interactionLayer.style.display = "none";
4464
+ if (legend && wasCollapsed) {
4465
+ legend.classList.remove("pg-is-collapsed");
4466
+ }
4467
+ try {
4468
+ const canvas = await html2canvas(root2, {
4469
+ scale: pixelRatio,
4470
+ backgroundColor,
4471
+ useCORS: true,
4472
+ logging: false
4473
+ });
4474
+ const dataUrl = canvas.toDataURL("image/png");
4475
+ const link = document.createElement("a");
4476
+ link.download = fileName;
4477
+ link.href = dataUrl;
4478
+ link.click();
4479
+ } finally {
4480
+ if (controls) controls.style.display = "flex";
4481
+ if (legendToggle) legendToggle.style.display = "flex";
4482
+ if (interactionLayer) interactionLayer.style.display = "block";
4483
+ if (legend && wasCollapsed) {
4484
+ legend.classList.add("pg-is-collapsed");
4485
+ }
4486
+ }
4487
+ }
4488
+
4469
4489
  // src/create-graph.ts
4470
4490
  function createGraph(config) {
4471
4491
  let cleanupResize = null;
4472
4492
  let cleanupZoom = null;
4473
4493
  let tooltipBinding = null;
4474
4494
  let controls = null;
4495
+ let legendCleanup = null;
4496
+ let fitViewTimer = null;
4475
4497
  let dimensions = { width: 0, height: 0 };
4476
4498
  let rootGroup = null;
4499
+ let svgElement = null;
4477
4500
  let zoomBehavior = null;
4478
4501
  let simulation = null;
4479
4502
  function render() {
4480
4503
  destroy();
4481
4504
  const layers = createGraphLayers(config.container);
4505
+ svgElement = layers.svg;
4482
4506
  rootGroup = layers.root;
4483
- cleanupResize = observeResize(
4484
- config.container,
4485
- (width, height) => {
4486
- dimensions = { width, height };
4487
- layers.interactionRect.setAttribute("width", String(width));
4488
- layers.interactionRect.setAttribute("height", String(height));
4489
- simulation?.force("x", x_default2(width / 2).strength(0.03));
4490
- simulation?.force("y", y_default2(height / 2).strength(0.03));
4491
- simulation?.alpha(0.25).restart();
4507
+ cleanupResize = observeResize(config.container, (width, height) => {
4508
+ dimensions = { width, height };
4509
+ layers.svg.setAttribute("width", String(width));
4510
+ layers.svg.setAttribute("height", String(height));
4511
+ layers.interactionRect.setAttribute("width", String(width));
4512
+ layers.interactionRect.setAttribute("height", String(height));
4513
+ if (simulation) {
4514
+ simulation.force("center", center_default(width / 2, height / 2));
4515
+ simulation.alpha(0.3).restart();
4492
4516
  }
4493
- );
4517
+ if (fitViewTimer) {
4518
+ clearTimeout(fitViewTimer);
4519
+ }
4520
+ fitViewTimer = setTimeout(() => {
4521
+ fitView();
4522
+ fitViewTimer = null;
4523
+ }, 150);
4524
+ });
4494
4525
  const zoomResult = createZoom({
4495
- /**
4496
- * D3 zoom must be attached to SVG
4497
- * because it requires:
4498
- *
4499
- * width.baseVal
4500
- * height.baseVal
4501
- */
4502
- svg: config.container,
4503
- /**
4504
- * Used for pointer semantics /
4505
- * pan filtering only
4506
- */
4526
+ svg: layers.svg,
4507
4527
  interactionLayer: layers.interactionLayer,
4508
- /**
4509
- * Actual graph transform target
4510
- */
4511
4528
  root: layers.root
4512
4529
  });
4513
4530
  zoomBehavior = zoomResult.behavior;
4514
4531
  cleanupZoom = zoomResult.cleanup;
4515
4532
  const root2 = select_default2(layers.root);
4516
- const renderContext = { svg: config.container, root: root2, interaction: config.interaction };
4533
+ const renderContext = { svg: layers.svg, root: root2, interaction: config.interaction };
4517
4534
  const linkSelection = renderLinks(renderContext, config.links);
4518
4535
  const linkLabelSelection = renderLinkLabels(renderContext, config.links);
4519
4536
  const nodeSelection = renderNodes(renderContext, config.nodes);
@@ -4521,45 +4538,41 @@ function createGraph(config) {
4521
4538
  const simulationConfig = {
4522
4539
  nodes: config.nodes,
4523
4540
  links: config.links,
4524
- width: config.container.clientWidth,
4525
- height: config.container.clientHeight
4541
+ // Uses the observed dimensions to ensure physics are calculated on actual container size
4542
+ width: dimensions.width || config.container.clientWidth,
4543
+ height: dimensions.height || config.container.clientHeight
4526
4544
  };
4527
4545
  const simulationResult = createGraphSimulation(simulationConfig);
4528
4546
  simulation = simulationResult.simulation;
4529
- simulation.on(
4530
- "tick",
4531
- () => {
4532
- linkSelection.attr("x1", (item) => item.link.source.x ?? 0).attr("y1", (item) => item.link.source.y ?? 0).attr("x2", (item) => getShortenedTargetPoint(item.link, item.style).x).attr("y2", (item) => getShortenedTargetPoint(item.link, item.style).y);
4533
- linkLabelSelection.attr("transform", (item) => {
4534
- const link = item.link;
4535
- const source = link.source;
4536
- const targetPoint = getLinkTargetPoint(link);
4537
- const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
4538
- const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
4539
- return `translate(${x3}, ${y3})`;
4540
- }).each(function() {
4541
- const group = this;
4542
- const text = group.querySelector("text");
4543
- const rect = group.querySelector("rect");
4544
- if (!text || !rect) {
4545
- return;
4546
- }
4547
- const bBox = text.getBBox();
4548
- const padding = 6;
4549
- rect.setAttribute("x", String(bBox.x - padding));
4550
- rect.setAttribute("y", String(bBox.y - padding));
4551
- rect.setAttribute("width", String(bBox.width + padding * 2));
4552
- rect.setAttribute("height", String(bBox.height + padding * 2));
4553
- });
4554
- nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
4555
- labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
4556
- tooltipBinding?.reposition();
4557
- }
4558
- );
4547
+ simulation.on("tick", () => {
4548
+ linkSelection.attr("x1", (item) => item.link.source.x ?? 0).attr("y1", (item) => item.link.source.y ?? 0).attr("x2", (item) => getShortenedTargetPoint(item.link, item.style).x).attr("y2", (item) => getShortenedTargetPoint(item.link, item.style).y);
4549
+ linkLabelSelection.attr("transform", (item) => {
4550
+ const link = item.link;
4551
+ const source = link.source;
4552
+ const targetPoint = getLinkTargetPoint(link);
4553
+ const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
4554
+ const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
4555
+ return `translate(${x3}, ${y3})`;
4556
+ }).each(function() {
4557
+ const group = this;
4558
+ const text = group.querySelector("text");
4559
+ const rect = group.querySelector("rect");
4560
+ if (!text || !rect) return;
4561
+ const bBox = text.getBBox();
4562
+ const padding = 6;
4563
+ rect.setAttribute("x", String(bBox.x - padding));
4564
+ rect.setAttribute("y", String(bBox.y - padding));
4565
+ rect.setAttribute("width", String(bBox.width + padding * 2));
4566
+ rect.setAttribute("height", String(bBox.height + padding * 2));
4567
+ });
4568
+ nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
4569
+ labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
4570
+ tooltipBinding?.reposition();
4571
+ });
4559
4572
  if (config.interaction?.hover?.enabled) {
4560
4573
  if (config.interaction?.hover?.tooltip?.enabled) {
4561
4574
  tooltipBinding = bindNodeTooltip({
4562
- container: config.container.parentElement,
4575
+ container: config.container,
4563
4576
  selection: nodeSelection,
4564
4577
  tooltipConfig: config.interaction.hover.tooltip
4565
4578
  });
@@ -4569,66 +4582,53 @@ function createGraph(config) {
4569
4582
  if (config.interaction?.drag?.enabled !== false) {
4570
4583
  nodeSelection.call(createDragBehavior(simulation));
4571
4584
  }
4572
- if (config.interaction?.selection?.enabled) {
4573
- }
4574
4585
  if (config.controls?.enabled) {
4575
- controls = createGraphControls(config.container, { zoomIn, zoomOut, resetView, fitView, destroy, render }, config.controls);
4586
+ controls = createGraphControls(
4587
+ layers.overlay,
4588
+ { zoomIn, zoomOut, resetView, fitView, destroy, render, exportGraph },
4589
+ config.controls
4590
+ );
4576
4591
  controls.mount();
4577
4592
  }
4593
+ if (config.legend?.enabled) {
4594
+ legendCleanup = createGraphLegend(layers.overlay, config.legend);
4595
+ }
4578
4596
  }
4579
4597
  function resetView() {
4580
- if (!zoomBehavior) {
4581
- return;
4582
- }
4583
- select_default2(config.container).transition().call(
4584
- zoomBehavior.transform,
4585
- identity2
4586
- );
4598
+ if (!zoomBehavior || !svgElement) return;
4599
+ select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, identity2);
4587
4600
  }
4588
4601
  function fitView() {
4589
- if (!zoomBehavior || !rootGroup || dimensions.width === 0 || dimensions.height === 0) {
4590
- return;
4591
- }
4602
+ if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
4592
4603
  const bounds = rootGroup.getBBox();
4593
- if (bounds.width === 0 || bounds.height === 0) {
4594
- return;
4595
- }
4596
- const width = dimensions.width;
4597
- const height = dimensions.height;
4598
- const scale = Math.min(
4599
- width / bounds.width,
4600
- height / bounds.height
4601
- ) * 0.9;
4602
- const translateX = (width - bounds.width * scale) / 2 - bounds.x * scale;
4603
- const translateY = (height - bounds.height * scale) / 2 - bounds.y * scale;
4604
- const transform2 = identity2.translate(
4605
- translateX,
4606
- translateY
4607
- ).scale(scale);
4608
- select_default2(config.container).transition().call(
4609
- zoomBehavior.transform,
4610
- transform2
4611
- );
4604
+ if (bounds.width === 0 || bounds.height === 0) return;
4605
+ const scale = Math.min(dimensions.width / bounds.width, dimensions.height / bounds.height) * 0.9;
4606
+ const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4607
+ const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
4608
+ const transform2 = identity2.translate(translateX, translateY).scale(scale);
4609
+ select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, transform2);
4612
4610
  }
4613
4611
  function zoomIn() {
4614
- if (!zoomBehavior) {
4615
- return;
4616
- }
4617
- select_default2(config.container).transition().call(
4618
- zoomBehavior.scaleBy,
4619
- 1.2
4620
- );
4612
+ if (!zoomBehavior || !svgElement) return;
4613
+ select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4621
4614
  }
4622
4615
  function zoomOut() {
4623
- if (!zoomBehavior) {
4624
- return;
4625
- }
4626
- select_default2(config.container).transition().call(
4627
- zoomBehavior.scaleBy,
4628
- 0.8
4629
- );
4616
+ if (!zoomBehavior || !svgElement) return;
4617
+ select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 0.8);
4618
+ }
4619
+ async function exportGraph(fileName) {
4620
+ fitView();
4621
+ await new Promise((resolve) => setTimeout(resolve, 500));
4622
+ await captureAndDownloadGraph(config.container, {
4623
+ fileName,
4624
+ pixelRatio: 2
4625
+ });
4630
4626
  }
4631
4627
  function destroy() {
4628
+ if (fitViewTimer) {
4629
+ clearTimeout(fitViewTimer);
4630
+ fitViewTimer = null;
4631
+ }
4632
4632
  if (cleanupResize) {
4633
4633
  cleanupResize();
4634
4634
  cleanupResize = null;
@@ -4649,13 +4649,18 @@ function createGraph(config) {
4649
4649
  controls.destroy();
4650
4650
  controls = null;
4651
4651
  }
4652
+ if (legendCleanup) {
4653
+ legendCleanup();
4654
+ legendCleanup = null;
4655
+ }
4652
4656
  rootGroup = null;
4657
+ svgElement = null;
4653
4658
  zoomBehavior = null;
4654
4659
  while (config.container.firstChild) {
4655
4660
  config.container.removeChild(config.container.firstChild);
4656
4661
  }
4657
4662
  }
4658
- return { render, zoomIn, zoomOut, resetView, fitView, destroy };
4663
+ return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph };
4659
4664
  }
4660
4665
  export {
4661
4666
  createGraph