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.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -3634,104 +3644,45 @@ function manyBody_default() {
3634
3644
  return force;
3635
3645
  }
3636
3646
 
3637
- // node_modules/d3-force/src/x.js
3638
- function x_default2(x3) {
3639
- var strength = constant_default5(0.1), nodes, strengths, xz;
3640
- if (typeof x3 !== "function") x3 = constant_default5(x3 == null ? 0 : +x3);
3641
- function force(alpha) {
3642
- for (var i = 0, n = nodes.length, node; i < n; ++i) {
3643
- node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha;
3644
- }
3645
- }
3646
- function initialize() {
3647
- if (!nodes) return;
3648
- var i, n = nodes.length;
3649
- strengths = new Array(n);
3650
- xz = new Array(n);
3651
- for (i = 0; i < n; ++i) {
3652
- strengths[i] = isNaN(xz[i] = +x3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
3653
- }
3654
- }
3655
- force.initialize = function(_) {
3656
- nodes = _;
3657
- initialize();
3658
- };
3659
- force.strength = function(_) {
3660
- return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
3661
- };
3662
- force.x = function(_) {
3663
- return arguments.length ? (x3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : x3;
3664
- };
3665
- return force;
3666
- }
3667
-
3668
- // node_modules/d3-force/src/y.js
3669
- function y_default2(y3) {
3670
- var strength = constant_default5(0.1), nodes, strengths, yz;
3671
- if (typeof y3 !== "function") y3 = constant_default5(y3 == null ? 0 : +y3);
3672
- function force(alpha) {
3673
- for (var i = 0, n = nodes.length, node; i < n; ++i) {
3674
- node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha;
3675
- }
3676
- }
3677
- function initialize() {
3678
- if (!nodes) return;
3679
- var i, n = nodes.length;
3680
- strengths = new Array(n);
3681
- yz = new Array(n);
3682
- for (i = 0; i < n; ++i) {
3683
- strengths[i] = isNaN(yz[i] = +y3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
3684
- }
3685
- }
3686
- force.initialize = function(_) {
3687
- nodes = _;
3688
- initialize();
3689
- };
3690
- force.strength = function(_) {
3691
- return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
3692
- };
3693
- force.y = function(_) {
3694
- return arguments.length ? (y3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : y3;
3695
- };
3696
- return force;
3697
- }
3698
-
3699
3647
  // src/core/create-graph-layers.ts
3700
- function createGraphLayers(svg) {
3701
- while (svg.firstChild) {
3702
- svg.removeChild(svg.firstChild);
3703
- }
3704
- const createGroup = (className) => {
3648
+ function createGraphLayers(host) {
3649
+ host.innerHTML = "";
3650
+ const rootContainer = document.createElement("div");
3651
+ rootContainer.className = "pg-root";
3652
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3653
+ svg.setAttribute("class", "pg-canvas");
3654
+ const overlay = document.createElement("div");
3655
+ overlay.className = "pg-overlay";
3656
+ rootContainer.appendChild(svg);
3657
+ rootContainer.appendChild(overlay);
3658
+ host.appendChild(rootContainer);
3659
+ const createGroup = (layerName) => {
3705
3660
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
3706
- group.setAttribute("class", className);
3707
- group.setAttribute("data-layer", className);
3661
+ group.setAttribute("class", `pg-layer-${layerName}`);
3662
+ group.setAttribute("data-layer", layerName);
3708
3663
  return group;
3709
3664
  };
3710
3665
  const interactionLayer = createGroup("interaction-layer");
3711
3666
  const interactionRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
3712
- const interactionAttributes = {
3713
- class: "interaction-surface",
3714
- width: "100%",
3715
- height: "100%",
3716
- fill: "transparent",
3717
- "pointer-events": "all"
3718
- };
3719
- Object.entries(interactionAttributes).forEach(([key, value]) => {
3720
- interactionRect.setAttribute(key, value);
3721
- });
3667
+ interactionRect.setAttribute("class", "pg-interaction-surface");
3668
+ interactionRect.setAttribute("fill", "transparent");
3669
+ interactionRect.setAttribute("pointer-events", "all");
3722
3670
  interactionLayer.appendChild(interactionRect);
3723
- const root2 = createGroup("knowledge-graph-root");
3671
+ const graphRoot = createGroup("viewport");
3724
3672
  const layers = {
3673
+ svg,
3674
+ overlay,
3725
3675
  interactionLayer,
3726
3676
  interactionRect,
3727
- root: root2,
3677
+ root: graphRoot,
3678
+ // These keys now match your ctx.root.select('[data-layer="..."]') calls
3728
3679
  links: createGroup("links"),
3729
3680
  linkLabels: createGroup("link-labels"),
3730
3681
  nodeRings: createGroup("node-rings"),
3731
3682
  nodes: createGroup("nodes"),
3732
3683
  nodeLabels: createGroup("node-labels")
3733
3684
  };
3734
- root2.append(
3685
+ graphRoot.append(
3735
3686
  layers.links,
3736
3687
  layers.linkLabels,
3737
3688
  layers.nodeRings,
@@ -3739,7 +3690,7 @@ function createGraphLayers(svg) {
3739
3690
  layers.nodeLabels
3740
3691
  );
3741
3692
  svg.appendChild(interactionLayer);
3742
- svg.appendChild(root2);
3693
+ svg.appendChild(graphRoot);
3743
3694
  return layers;
3744
3695
  }
3745
3696
 
@@ -3801,10 +3752,17 @@ function createGraphSimulation(config) {
3801
3752
  });
3802
3753
  const simulation = simulation_default(config.nodes).alpha(0.9).alphaDecay(0.12).alphaMin(0.03).velocityDecay(0.5).force(
3803
3754
  "link",
3804
- link_default(config.links).id((d) => d.id).distance(150).strength(0.4)
3755
+ link_default(config.links).id((d) => d.id).distance((d) => {
3756
+ const source = d.source;
3757
+ const target = d.target;
3758
+ const sourceR = source.style?.radius || 20;
3759
+ const targetR = target.style?.radius || 20;
3760
+ const labelBuffer = d.style?.label?.height || 40;
3761
+ return (sourceR + targetR + labelBuffer) * 2;
3762
+ }).strength(0.8)
3805
3763
  ).force("charge", manyBody_default().strength(-220)).force(
3806
3764
  "collide",
3807
- collide_default().radius((node) => (node.style?.radius ?? 12) + 10).strength(0.9)
3765
+ collide_default().radius((node) => (node.style?.radius ?? 12) + 10).iterations(2)
3808
3766
  ).force("center", center_default(centerX, centerY).strength(0.08));
3809
3767
  return { simulation };
3810
3768
  }
@@ -3856,49 +3814,51 @@ function getControlIcon(icon) {
3856
3814
  }
3857
3815
 
3858
3816
  // src/controls/create-graph-controls.ts
3859
- function createGraphControls(container, graph, config) {
3817
+ function createGraphControls(overlay, graph, config) {
3860
3818
  let root2 = null;
3861
3819
  function mount() {
3862
3820
  if (!config.enabled) {
3863
3821
  return;
3864
3822
  }
3865
- const parent = container.parentElement;
3866
- if (!parent) {
3867
- return;
3868
- }
3869
3823
  root2 = document.createElement("div");
3870
3824
  root2.className = "pg-controls";
3871
- applyPosition(root2, config);
3872
- applyOrientation(root2, config);
3825
+ const position = resolveControlsPosition(config.position);
3826
+ root2.classList.add(`pg-pos-${position}`);
3827
+ const orientation = resolveControlsOrientation(config.orientation);
3828
+ root2.classList.add(`pg-orient-${orientation}`);
3829
+ if (config.offset) {
3830
+ root2.style.setProperty("--pg-controls-offset-x", `${config.offset.x}px`);
3831
+ root2.style.setProperty("--pg-controls-offset-y", `${config.offset.y}px`);
3832
+ }
3873
3833
  appendControls(root2, config, graph);
3874
- parent.appendChild(root2);
3834
+ overlay.appendChild(root2);
3875
3835
  }
3876
3836
  function appendControls(root3, config2, graph2) {
3877
- if (shouldRenderControl(config2, "zoomIn")) {
3878
- root3.appendChild(createButton("zoom-in", "Zoom in", graph2.zoomIn.bind(graph2)));
3879
- }
3880
- if (shouldRenderControl(config2, "zoomOut")) {
3881
- root3.appendChild(createButton("zoom-out", "Zoom out", graph2.zoomOut.bind(graph2)));
3882
- }
3883
- if (shouldRenderControl(config2, "fit")) {
3884
- root3.appendChild(createButton("fit", "Fit view", graph2.fitView.bind(graph2)));
3885
- }
3886
- if (shouldRenderControl(config2, "reset")) {
3887
- root3.appendChild(createButton("reset", "Reset view", graph2.resetView.bind(graph2)));
3888
- }
3837
+ const actions = [
3838
+ { key: "zoomIn", icon: "zoom-in", label: "Zoom in", fn: graph2.zoomIn.bind(graph2) },
3839
+ { key: "zoomOut", icon: "zoom-out", label: "Zoom out", fn: graph2.zoomOut.bind(graph2) },
3840
+ { key: "fit", icon: "fit", label: "Fit view", fn: graph2.fitView.bind(graph2) },
3841
+ { key: "reset", icon: "reset", label: "Reset view", fn: graph2.resetView.bind(graph2) }
3842
+ ];
3843
+ actions.forEach((action) => {
3844
+ if (shouldRenderControl(config2, action.key)) {
3845
+ root3.appendChild(createButton(action.icon, action.label, action.fn));
3846
+ }
3847
+ });
3889
3848
  }
3890
3849
  function createButton(type, label, onClick) {
3891
3850
  const button = document.createElement("button");
3851
+ button.className = "pg-control-btn";
3892
3852
  button.type = "button";
3893
3853
  button.setAttribute("aria-label", label);
3894
3854
  const wrapper = document.createElement("div");
3855
+ wrapper.className = "pg-icon-wrapper";
3895
3856
  wrapper.innerHTML = getControlIcon(type);
3896
3857
  const svg = wrapper.querySelector("svg");
3897
- if (!svg) {
3898
- throw new Error(`Invalid SVG for icon: ${type}`);
3858
+ if (svg) {
3859
+ svg.classList.add("pg-icon");
3860
+ button.appendChild(svg);
3899
3861
  }
3900
- svg.classList.add("pg-icon");
3901
- button.appendChild(svg);
3902
3862
  button.addEventListener("click", onClick);
3903
3863
  return button;
3904
3864
  }
@@ -3906,39 +3866,70 @@ function createGraphControls(container, graph, config) {
3906
3866
  if (!root2) {
3907
3867
  return;
3908
3868
  }
3909
- const clone = root2.cloneNode(true);
3910
- root2.replaceWith(clone);
3869
+ if (root2.parentNode === overlay) {
3870
+ overlay.removeChild(root2);
3871
+ }
3911
3872
  root2 = null;
3912
3873
  }
3913
3874
  return { mount, destroy };
3914
3875
  }
3915
- function applyPosition(el, config) {
3916
- const position = resolveControlsPosition(config.position);
3917
- const offset = config.offset ?? { x: 16, y: 16 };
3918
- el.style.position = "absolute";
3919
- switch (position) {
3920
- case "bottom-left":
3921
- el.style.left = `${offset.x}px`;
3922
- el.style.bottom = `${offset.y}px`;
3923
- break;
3924
- case "bottom-right":
3925
- el.style.right = `${offset.x}px`;
3926
- el.style.bottom = `${offset.y}px`;
3927
- break;
3928
- case "top-left":
3929
- el.style.left = `${offset.x}px`;
3930
- el.style.top = `${offset.y}px`;
3931
- break;
3932
- case "top-right":
3933
- el.style.right = `${offset.x}px`;
3934
- el.style.top = `${offset.y}px`;
3935
- break;
3876
+
3877
+ // src/assets/caret.svg?raw
3878
+ 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>';
3879
+
3880
+ // src/legends/graph-legend-icon.ts
3881
+ var LEGEND_ICON_MAP = { caret: caret_default };
3882
+ function getLegendIcon(icon) {
3883
+ const raw = LEGEND_ICON_MAP[icon];
3884
+ if (!raw) {
3885
+ throw new Error(`Legend icon not found: ${icon}`);
3936
3886
  }
3887
+ return raw.replace("<svg", '<svg class="pg-icon"');
3937
3888
  }
3938
- function applyOrientation(el, config) {
3939
- const orientation = resolveControlsOrientation(config.orientation);
3940
- el.style.display = "flex";
3941
- el.style.flexDirection = orientation === "vertical" ? "column" : "row";
3889
+
3890
+ // src/legends/create-graph-legends.ts
3891
+ function createGraphLegend(overlay, config) {
3892
+ const legendWrapper = document.createElement("div");
3893
+ legendWrapper.className = "pg-legend";
3894
+ const position = config.position || "bottom-right";
3895
+ legendWrapper.classList.add(`pg-pos-${position}`);
3896
+ if (config.defaultExpanded === false) {
3897
+ legendWrapper.classList.add("pg-is-collapsed");
3898
+ }
3899
+ if (config.collapsible) {
3900
+ const toggleBtn = document.createElement("button");
3901
+ toggleBtn.className = "pg-legend-toggle";
3902
+ toggleBtn.type = "button";
3903
+ toggleBtn.innerHTML = getLegendIcon("caret");
3904
+ toggleBtn.onclick = (e) => {
3905
+ e.stopPropagation();
3906
+ legendWrapper.classList.toggle("pg-is-collapsed");
3907
+ };
3908
+ legendWrapper.appendChild(toggleBtn);
3909
+ }
3910
+ const body = document.createElement("div");
3911
+ body.className = "pg-legend-body";
3912
+ const list = document.createElement("ul");
3913
+ list.className = "pg-legend-list";
3914
+ config.items.forEach((item) => {
3915
+ const listItem = document.createElement("li");
3916
+ listItem.className = "pg-legend-item";
3917
+ const swatch = document.createElement("span");
3918
+ swatch.className = `pg-legend-swatch is-${item.shape || "circle"}`;
3919
+ swatch.style.backgroundColor = item.color;
3920
+ const label = document.createElement("span");
3921
+ label.className = "pg-legend-label";
3922
+ label.innerText = item.label;
3923
+ listItem.appendChild(swatch);
3924
+ listItem.appendChild(label);
3925
+ list.appendChild(listItem);
3926
+ });
3927
+ body.appendChild(list);
3928
+ legendWrapper.appendChild(body);
3929
+ overlay.appendChild(legendWrapper);
3930
+ return () => {
3931
+ if (legendWrapper.parentNode === overlay) overlay.removeChild(legendWrapper);
3932
+ };
3942
3933
  }
3943
3934
 
3944
3935
  // src/utils/resolve-link-style.ts
@@ -3954,6 +3945,7 @@ var DEFAULT_LINK_STYLE = {
3954
3945
  },
3955
3946
  label: {
3956
3947
  enabled: true,
3948
+ visibility: "always",
3957
3949
  backgroundFill: "color-mix(in srgb, #8E42EE, #FFFFFF 90%)",
3958
3950
  borderColor: "color-mix(in srgb, #8E42EE, #FFFFFF 10%)",
3959
3951
  borderWidth: 1.5,
@@ -3989,6 +3981,7 @@ function mergeLinkStyle(base, override) {
3989
3981
  },
3990
3982
  label: {
3991
3983
  enabled: override?.label?.enabled ?? base.label.enabled,
3984
+ visibility: override?.label?.visibility ?? base.label.visibility,
3992
3985
  backgroundFill: override?.label?.backgroundFill ?? base.label.backgroundFill,
3993
3986
  borderColor: override?.label?.borderColor ?? base.label.borderColor,
3994
3987
  borderWidth: override?.label?.borderWidth ?? base.label.borderWidth,
@@ -4089,7 +4082,14 @@ function getLinkKey(link) {
4089
4082
  }
4090
4083
  function renderLinks(ctx, links) {
4091
4084
  const renderableLinks = createRenderableLinks(ctx, links);
4092
- 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);
4085
+ 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");
4086
+ const labelSelection = ctx.root.selectAll(".link-label");
4087
+ linkSelection.on("mouseenter.label-hover", (_event, d) => {
4088
+ labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 1);
4089
+ }).on("mouseleave.label-hover", (_event, d) => {
4090
+ labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 0);
4091
+ });
4092
+ return linkSelection;
4093
4093
  }
4094
4094
 
4095
4095
  // src/renderer/nodes.ts
@@ -4159,7 +4159,10 @@ function renderNodeLabels(ctx, nodes) {
4159
4159
  // src/renderer/link-labels.ts
4160
4160
  function createRenderableLinks2(params, links) {
4161
4161
  return links.map(
4162
- (link) => ({ link, style: resolveLinkStyle({ link, interaction: params.interaction }) })
4162
+ (link) => ({
4163
+ link,
4164
+ style: resolveLinkStyle({ link, interaction: params.interaction })
4165
+ })
4163
4166
  ).filter(
4164
4167
  (item) => item.style.label.enabled && Boolean(item.link.label)
4165
4168
  );
@@ -4171,7 +4174,13 @@ function getLinkKey2(link) {
4171
4174
  }
4172
4175
  function renderLinkLabels(params, links) {
4173
4176
  const renderableLinks = createRenderableLinks2(params, links);
4174
- 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");
4177
+ 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) => {
4178
+ const visibility = item.style.label.visibility ?? "always";
4179
+ return visibility === "always" ? 1 : 0;
4180
+ }).style("pointer-events", (item) => {
4181
+ const visibility = item.style.label.visibility ?? "always";
4182
+ return visibility === "always" ? "auto" : "none";
4183
+ }).style("cursor", "pointer");
4175
4184
  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);
4176
4185
  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 ?? "");
4177
4186
  return labelSelection;
@@ -4200,58 +4209,35 @@ function createDragBehavior(simulation) {
4200
4209
 
4201
4210
  // src/interactions/create-node-hover.ts
4202
4211
  function createNodeHover(nodeSelection, hoverStyle) {
4203
- if (!hoverStyle) {
4204
- return;
4205
- }
4206
- nodeSelection.on(
4207
- "mouseenter.hover",
4208
- function(_event, node) {
4212
+ const firstNode = nodeSelection.node();
4213
+ if (!firstNode) return;
4214
+ if (hoverStyle) {
4215
+ nodeSelection.on("mouseenter.hover", function(_event, node) {
4209
4216
  const circle = this;
4210
- const hoverStroke = hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff";
4211
- const hoverStrokeWidth = hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5;
4212
- const hoverOpacity = hoverStyle.opacity ?? node.style?.opacity ?? 1;
4213
- circle.setAttribute(
4214
- "stroke",
4215
- hoverStroke
4216
- );
4217
- circle.setAttribute(
4218
- "stroke-width",
4219
- String(
4220
- hoverStrokeWidth
4221
- )
4222
- );
4223
- circle.setAttribute(
4224
- "opacity",
4225
- String(
4226
- hoverOpacity
4227
- )
4228
- );
4229
- }
4230
- ).on(
4231
- "mouseleave.hover",
4232
- function(_event, node) {
4217
+ circle.setAttribute("stroke", hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff");
4218
+ circle.setAttribute("stroke-width", String(hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5));
4219
+ circle.setAttribute("opacity", String(hoverStyle.opacity ?? node.style?.opacity ?? 1));
4220
+ }).on("mouseleave.hover", function(_event, node) {
4233
4221
  const circle = this;
4234
- const defaultStroke = node.style?.stroke ?? "#ffffff";
4235
- const defaultStrokeWidth = node.style?.strokeWidth ?? 1.5;
4236
- const defaultOpacity = node.style?.opacity ?? 1;
4237
- circle.setAttribute(
4238
- "stroke",
4239
- defaultStroke
4240
- );
4241
- circle.setAttribute(
4242
- "stroke-width",
4243
- String(
4244
- defaultStrokeWidth
4245
- )
4246
- );
4247
- circle.setAttribute(
4248
- "opacity",
4249
- String(
4250
- defaultOpacity
4251
- )
4252
- );
4253
- }
4254
- );
4222
+ circle.setAttribute("stroke", node.style?.stroke ?? "#ffffff");
4223
+ circle.setAttribute("stroke-width", String(node.style?.strokeWidth ?? 1.5));
4224
+ circle.setAttribute("opacity", String(node.style?.opacity ?? 1));
4225
+ });
4226
+ }
4227
+ const svgElement = firstNode.ownerSVGElement;
4228
+ if (!svgElement) return;
4229
+ const root2 = select_default2(svgElement);
4230
+ const labelSelection = root2.selectAll(".link-label");
4231
+ nodeSelection.on("mouseenter.labels", (_event, d) => {
4232
+ labelSelection.filter((item) => {
4233
+ if (item.style.label.visibility !== "hover") return false;
4234
+ const s = item.link.source;
4235
+ const t = item.link.target;
4236
+ return s.id === d.id || t.id === d.id;
4237
+ }).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4238
+ }).on("mouseleave.labels", (_event) => {
4239
+ labelSelection.filter((item) => item.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4240
+ });
4255
4241
  }
4256
4242
 
4257
4243
  // src/utils/resolve-tooltip-position.ts
@@ -4456,10 +4442,11 @@ function observeResize(element, onResize) {
4456
4442
  if (!entry) {
4457
4443
  return;
4458
4444
  }
4459
- onResize(
4460
- entry.contentRect.width,
4461
- entry.contentRect.height
4462
- );
4445
+ const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
4446
+ const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
4447
+ if (width > 0 && height > 0) {
4448
+ onResize(width, height);
4449
+ }
4463
4450
  }
4464
4451
  );
4465
4452
  observer.observe(element);
@@ -4492,54 +4479,94 @@ function getLinkTargetPoint(link) {
4492
4479
  };
4493
4480
  }
4494
4481
 
4482
+ // src/utils/export-graph.ts
4483
+ var import_html2canvas = __toESM(require("html2canvas"), 1);
4484
+ async function captureAndDownloadGraph(container, options = {}) {
4485
+ const {
4486
+ fileName = `graph-export-${Date.now()}.png`,
4487
+ backgroundColor = "#ffffff",
4488
+ pixelRatio = 2
4489
+ } = options;
4490
+ const root2 = container.querySelector(".pg-root");
4491
+ if (!root2) return;
4492
+ const controls = root2.querySelector(".pg-controls");
4493
+ const legendToggle = root2.querySelector(".pg-legend-toggle");
4494
+ const interactionLayer = root2.querySelector(".pg-interaction-layer");
4495
+ const legend = root2.querySelector(".pg-legend");
4496
+ const wasCollapsed = legend?.classList.contains("pg-is-collapsed");
4497
+ if (controls) controls.style.display = "none";
4498
+ if (legendToggle) legendToggle.style.display = "none";
4499
+ if (interactionLayer) interactionLayer.style.display = "none";
4500
+ if (legend && wasCollapsed) {
4501
+ legend.classList.remove("pg-is-collapsed");
4502
+ }
4503
+ try {
4504
+ const canvas = await (0, import_html2canvas.default)(root2, {
4505
+ scale: pixelRatio,
4506
+ backgroundColor,
4507
+ useCORS: true,
4508
+ logging: false
4509
+ });
4510
+ const dataUrl = canvas.toDataURL("image/png");
4511
+ const link = document.createElement("a");
4512
+ link.download = fileName;
4513
+ link.href = dataUrl;
4514
+ link.click();
4515
+ } finally {
4516
+ if (controls) controls.style.display = "flex";
4517
+ if (legendToggle) legendToggle.style.display = "flex";
4518
+ if (interactionLayer) interactionLayer.style.display = "block";
4519
+ if (legend && wasCollapsed) {
4520
+ legend.classList.add("pg-is-collapsed");
4521
+ }
4522
+ }
4523
+ }
4524
+
4495
4525
  // src/create-graph.ts
4496
4526
  function createGraph(config) {
4497
4527
  let cleanupResize = null;
4498
4528
  let cleanupZoom = null;
4499
4529
  let tooltipBinding = null;
4500
4530
  let controls = null;
4531
+ let legendCleanup = null;
4532
+ let fitViewTimer = null;
4501
4533
  let dimensions = { width: 0, height: 0 };
4502
4534
  let rootGroup = null;
4535
+ let svgElement = null;
4503
4536
  let zoomBehavior = null;
4504
4537
  let simulation = null;
4505
4538
  function render() {
4506
4539
  destroy();
4507
4540
  const layers = createGraphLayers(config.container);
4541
+ svgElement = layers.svg;
4508
4542
  rootGroup = layers.root;
4509
- cleanupResize = observeResize(
4510
- config.container,
4511
- (width, height) => {
4512
- dimensions = { width, height };
4513
- layers.interactionRect.setAttribute("width", String(width));
4514
- layers.interactionRect.setAttribute("height", String(height));
4515
- simulation?.force("x", x_default2(width / 2).strength(0.03));
4516
- simulation?.force("y", y_default2(height / 2).strength(0.03));
4517
- simulation?.alpha(0.25).restart();
4543
+ cleanupResize = observeResize(config.container, (width, height) => {
4544
+ dimensions = { width, height };
4545
+ layers.svg.setAttribute("width", String(width));
4546
+ layers.svg.setAttribute("height", String(height));
4547
+ layers.interactionRect.setAttribute("width", String(width));
4548
+ layers.interactionRect.setAttribute("height", String(height));
4549
+ if (simulation) {
4550
+ simulation.force("center", center_default(width / 2, height / 2));
4551
+ simulation.alpha(0.3).restart();
4518
4552
  }
4519
- );
4553
+ if (fitViewTimer) {
4554
+ clearTimeout(fitViewTimer);
4555
+ }
4556
+ fitViewTimer = setTimeout(() => {
4557
+ fitView();
4558
+ fitViewTimer = null;
4559
+ }, 150);
4560
+ });
4520
4561
  const zoomResult = createZoom({
4521
- /**
4522
- * D3 zoom must be attached to SVG
4523
- * because it requires:
4524
- *
4525
- * width.baseVal
4526
- * height.baseVal
4527
- */
4528
- svg: config.container,
4529
- /**
4530
- * Used for pointer semantics /
4531
- * pan filtering only
4532
- */
4562
+ svg: layers.svg,
4533
4563
  interactionLayer: layers.interactionLayer,
4534
- /**
4535
- * Actual graph transform target
4536
- */
4537
4564
  root: layers.root
4538
4565
  });
4539
4566
  zoomBehavior = zoomResult.behavior;
4540
4567
  cleanupZoom = zoomResult.cleanup;
4541
4568
  const root2 = select_default2(layers.root);
4542
- const renderContext = { svg: config.container, root: root2, interaction: config.interaction };
4569
+ const renderContext = { svg: layers.svg, root: root2, interaction: config.interaction };
4543
4570
  const linkSelection = renderLinks(renderContext, config.links);
4544
4571
  const linkLabelSelection = renderLinkLabels(renderContext, config.links);
4545
4572
  const nodeSelection = renderNodes(renderContext, config.nodes);
@@ -4547,45 +4574,41 @@ function createGraph(config) {
4547
4574
  const simulationConfig = {
4548
4575
  nodes: config.nodes,
4549
4576
  links: config.links,
4550
- width: config.container.clientWidth,
4551
- height: config.container.clientHeight
4577
+ // Uses the observed dimensions to ensure physics are calculated on actual container size
4578
+ width: dimensions.width || config.container.clientWidth,
4579
+ height: dimensions.height || config.container.clientHeight
4552
4580
  };
4553
4581
  const simulationResult = createGraphSimulation(simulationConfig);
4554
4582
  simulation = simulationResult.simulation;
4555
- simulation.on(
4556
- "tick",
4557
- () => {
4558
- 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);
4559
- linkLabelSelection.attr("transform", (item) => {
4560
- const link = item.link;
4561
- const source = link.source;
4562
- const targetPoint = getLinkTargetPoint(link);
4563
- const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
4564
- const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
4565
- return `translate(${x3}, ${y3})`;
4566
- }).each(function() {
4567
- const group = this;
4568
- const text = group.querySelector("text");
4569
- const rect = group.querySelector("rect");
4570
- if (!text || !rect) {
4571
- return;
4572
- }
4573
- const bBox = text.getBBox();
4574
- const padding = 6;
4575
- rect.setAttribute("x", String(bBox.x - padding));
4576
- rect.setAttribute("y", String(bBox.y - padding));
4577
- rect.setAttribute("width", String(bBox.width + padding * 2));
4578
- rect.setAttribute("height", String(bBox.height + padding * 2));
4579
- });
4580
- nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
4581
- labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
4582
- tooltipBinding?.reposition();
4583
- }
4584
- );
4583
+ simulation.on("tick", () => {
4584
+ 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);
4585
+ linkLabelSelection.attr("transform", (item) => {
4586
+ const link = item.link;
4587
+ const source = link.source;
4588
+ const targetPoint = getLinkTargetPoint(link);
4589
+ const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
4590
+ const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
4591
+ return `translate(${x3}, ${y3})`;
4592
+ }).each(function() {
4593
+ const group = this;
4594
+ const text = group.querySelector("text");
4595
+ const rect = group.querySelector("rect");
4596
+ if (!text || !rect) return;
4597
+ const bBox = text.getBBox();
4598
+ const padding = 6;
4599
+ rect.setAttribute("x", String(bBox.x - padding));
4600
+ rect.setAttribute("y", String(bBox.y - padding));
4601
+ rect.setAttribute("width", String(bBox.width + padding * 2));
4602
+ rect.setAttribute("height", String(bBox.height + padding * 2));
4603
+ });
4604
+ nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
4605
+ labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
4606
+ tooltipBinding?.reposition();
4607
+ });
4585
4608
  if (config.interaction?.hover?.enabled) {
4586
4609
  if (config.interaction?.hover?.tooltip?.enabled) {
4587
4610
  tooltipBinding = bindNodeTooltip({
4588
- container: config.container.parentElement,
4611
+ container: config.container,
4589
4612
  selection: nodeSelection,
4590
4613
  tooltipConfig: config.interaction.hover.tooltip
4591
4614
  });
@@ -4595,66 +4618,53 @@ function createGraph(config) {
4595
4618
  if (config.interaction?.drag?.enabled !== false) {
4596
4619
  nodeSelection.call(createDragBehavior(simulation));
4597
4620
  }
4598
- if (config.interaction?.selection?.enabled) {
4599
- }
4600
4621
  if (config.controls?.enabled) {
4601
- controls = createGraphControls(config.container, { zoomIn, zoomOut, resetView, fitView, destroy, render }, config.controls);
4622
+ controls = createGraphControls(
4623
+ layers.overlay,
4624
+ { zoomIn, zoomOut, resetView, fitView, destroy, render, exportGraph },
4625
+ config.controls
4626
+ );
4602
4627
  controls.mount();
4603
4628
  }
4629
+ if (config.legend?.enabled) {
4630
+ legendCleanup = createGraphLegend(layers.overlay, config.legend);
4631
+ }
4604
4632
  }
4605
4633
  function resetView() {
4606
- if (!zoomBehavior) {
4607
- return;
4608
- }
4609
- select_default2(config.container).transition().call(
4610
- zoomBehavior.transform,
4611
- identity2
4612
- );
4634
+ if (!zoomBehavior || !svgElement) return;
4635
+ select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, identity2);
4613
4636
  }
4614
4637
  function fitView() {
4615
- if (!zoomBehavior || !rootGroup || dimensions.width === 0 || dimensions.height === 0) {
4616
- return;
4617
- }
4638
+ if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
4618
4639
  const bounds = rootGroup.getBBox();
4619
- if (bounds.width === 0 || bounds.height === 0) {
4620
- return;
4621
- }
4622
- const width = dimensions.width;
4623
- const height = dimensions.height;
4624
- const scale = Math.min(
4625
- width / bounds.width,
4626
- height / bounds.height
4627
- ) * 0.9;
4628
- const translateX = (width - bounds.width * scale) / 2 - bounds.x * scale;
4629
- const translateY = (height - bounds.height * scale) / 2 - bounds.y * scale;
4630
- const transform2 = identity2.translate(
4631
- translateX,
4632
- translateY
4633
- ).scale(scale);
4634
- select_default2(config.container).transition().call(
4635
- zoomBehavior.transform,
4636
- transform2
4637
- );
4640
+ if (bounds.width === 0 || bounds.height === 0) return;
4641
+ const scale = Math.min(dimensions.width / bounds.width, dimensions.height / bounds.height) * 0.9;
4642
+ const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4643
+ const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
4644
+ const transform2 = identity2.translate(translateX, translateY).scale(scale);
4645
+ select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, transform2);
4638
4646
  }
4639
4647
  function zoomIn() {
4640
- if (!zoomBehavior) {
4641
- return;
4642
- }
4643
- select_default2(config.container).transition().call(
4644
- zoomBehavior.scaleBy,
4645
- 1.2
4646
- );
4648
+ if (!zoomBehavior || !svgElement) return;
4649
+ select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4647
4650
  }
4648
4651
  function zoomOut() {
4649
- if (!zoomBehavior) {
4650
- return;
4651
- }
4652
- select_default2(config.container).transition().call(
4653
- zoomBehavior.scaleBy,
4654
- 0.8
4655
- );
4652
+ if (!zoomBehavior || !svgElement) return;
4653
+ select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 0.8);
4654
+ }
4655
+ async function exportGraph(fileName) {
4656
+ fitView();
4657
+ await new Promise((resolve) => setTimeout(resolve, 500));
4658
+ await captureAndDownloadGraph(config.container, {
4659
+ fileName,
4660
+ pixelRatio: 2
4661
+ });
4656
4662
  }
4657
4663
  function destroy() {
4664
+ if (fitViewTimer) {
4665
+ clearTimeout(fitViewTimer);
4666
+ fitViewTimer = null;
4667
+ }
4658
4668
  if (cleanupResize) {
4659
4669
  cleanupResize();
4660
4670
  cleanupResize = null;
@@ -4675,13 +4685,18 @@ function createGraph(config) {
4675
4685
  controls.destroy();
4676
4686
  controls = null;
4677
4687
  }
4688
+ if (legendCleanup) {
4689
+ legendCleanup();
4690
+ legendCleanup = null;
4691
+ }
4678
4692
  rootGroup = null;
4693
+ svgElement = null;
4679
4694
  zoomBehavior = null;
4680
4695
  while (config.container.firstChild) {
4681
4696
  config.container.removeChild(config.container.firstChild);
4682
4697
  }
4683
4698
  }
4684
- return { render, zoomIn, zoomOut, resetView, fitView, destroy };
4699
+ return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph };
4685
4700
  }
4686
4701
  // Annotate the CommonJS export names for ESM import in node:
4687
4702
  0 && (module.exports = {