polly-graph 0.1.5 → 0.1.6

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
@@ -3767,6 +3767,56 @@ function createGraphSimulation(config) {
3767
3767
  return { simulation };
3768
3768
  }
3769
3769
 
3770
+ // src/utils/get-link-marker-id.ts
3771
+ function getLinkMarkerId(style) {
3772
+ const markerStyle = {
3773
+ stroke: style.stroke ?? "#94a3b8",
3774
+ strokeWidth: style.strokeWidth ?? 2,
3775
+ arrowFill: style.arrow?.fill ?? style.stroke ?? "#94a3b8",
3776
+ arrowSize: style.arrow?.size ?? 6
3777
+ };
3778
+ const serializedStyle = JSON.stringify(markerStyle);
3779
+ const hash = createHash(serializedStyle);
3780
+ return `graph-arrow-${hash}`;
3781
+ }
3782
+ function createHash(value) {
3783
+ let hash = 0;
3784
+ for (let index2 = 0; index2 < value.length; index2 += 1) {
3785
+ const charCode = value.charCodeAt(index2);
3786
+ hash = (hash << 5) - hash + charCode;
3787
+ hash |= 0;
3788
+ }
3789
+ return Math.abs(hash).toString(36);
3790
+ }
3791
+
3792
+ // src/core/create-arrow-marker.ts
3793
+ function createArrowMarker(params) {
3794
+ const markerId = getLinkMarkerId(params.style);
3795
+ const existingMarker = params.svg.querySelector(`#${markerId}`);
3796
+ if (existingMarker) {
3797
+ return markerId;
3798
+ }
3799
+ const arrowSize = params.style.arrow?.size ?? 6;
3800
+ const fill = params.style.arrow?.fill ?? params.style.stroke ?? "#94a3b8";
3801
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
3802
+ const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
3803
+ marker.setAttribute("id", markerId);
3804
+ marker.setAttribute("viewBox", "0 0 20 20");
3805
+ marker.setAttribute("refX", "0");
3806
+ marker.setAttribute("refY", "10");
3807
+ marker.setAttribute("markerWidth", String(arrowSize * 2));
3808
+ marker.setAttribute("markerHeight", String(arrowSize * 2));
3809
+ marker.setAttribute("orient", "auto");
3810
+ marker.setAttribute("markerUnits", "userSpaceOnUse");
3811
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
3812
+ path.setAttribute("d", "M 0 0 L 20 10 L 0 20 z");
3813
+ path.setAttribute("fill", fill);
3814
+ marker.appendChild(path);
3815
+ defs.appendChild(marker);
3816
+ params.svg.insertBefore(defs, params.svg.firstChild);
3817
+ return markerId;
3818
+ }
3819
+
3770
3820
  // src/controls/graph-controls.utils.ts
3771
3821
  function resolveControlsPosition(position) {
3772
3822
  return position ?? "bottom-left";
@@ -3995,56 +4045,6 @@ function mergeLinkStyle(base, override) {
3995
4045
  };
3996
4046
  }
3997
4047
 
3998
- // src/utils/get-link-marker-id.ts
3999
- function getLinkMarkerId(style) {
4000
- const markerStyle = {
4001
- stroke: style.stroke ?? "#94a3b8",
4002
- strokeWidth: style.strokeWidth ?? 2,
4003
- arrowFill: style.arrow?.fill ?? style.stroke ?? "#94a3b8",
4004
- arrowSize: style.arrow?.size ?? 6
4005
- };
4006
- const serializedStyle = JSON.stringify(markerStyle);
4007
- const hash = createHash(serializedStyle);
4008
- return `graph-arrow-${hash}`;
4009
- }
4010
- function createHash(value) {
4011
- let hash = 0;
4012
- for (let index2 = 0; index2 < value.length; index2 += 1) {
4013
- const charCode = value.charCodeAt(index2);
4014
- hash = (hash << 5) - hash + charCode;
4015
- hash |= 0;
4016
- }
4017
- return Math.abs(hash).toString(36);
4018
- }
4019
-
4020
- // src/core/create-arrow-marker.ts
4021
- function createArrowMarker(params) {
4022
- const markerId = getLinkMarkerId(params.style);
4023
- const existingMarker = params.svg.querySelector(`#${markerId}`);
4024
- if (existingMarker) {
4025
- return markerId;
4026
- }
4027
- const arrowSize = params.style.arrow?.size ?? 6;
4028
- const fill = params.style.arrow?.fill ?? params.style.stroke ?? "#94a3b8";
4029
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
4030
- const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
4031
- marker.setAttribute("id", markerId);
4032
- marker.setAttribute("viewBox", "0 0 20 20");
4033
- marker.setAttribute("refX", "0");
4034
- marker.setAttribute("refY", "10");
4035
- marker.setAttribute("markerWidth", String(arrowSize * 2));
4036
- marker.setAttribute("markerHeight", String(arrowSize * 2));
4037
- marker.setAttribute("orient", "auto");
4038
- marker.setAttribute("markerUnits", "userSpaceOnUse");
4039
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
4040
- path.setAttribute("d", "M 0 0 L 20 10 L 0 20 z");
4041
- path.setAttribute("fill", fill);
4042
- marker.appendChild(path);
4043
- defs.appendChild(marker);
4044
- params.svg.insertBefore(defs, params.svg.firstChild);
4045
- return markerId;
4046
- }
4047
-
4048
4048
  // src/renderer/links.ts
4049
4049
  function getShortenedTargetPoint(link, style) {
4050
4050
  const source = link.source;
@@ -4082,7 +4082,7 @@ function getLinkKey(link) {
4082
4082
  }
4083
4083
  function renderLinks(ctx, links) {
4084
4084
  const renderableLinks = createRenderableLinks(ctx, links);
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");
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").style("cursor", "pointer");
4086
4086
  const labelSelection = ctx.root.selectAll(".link-label");
4087
4087
  linkSelection.on("mouseenter.label-hover", (_event, d) => {
4088
4088
  labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 1);
@@ -4236,7 +4236,9 @@ function createNodeHover(nodeSelection, hoverStyle) {
4236
4236
  return s.id === d.id || t.id === d.id;
4237
4237
  }).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4238
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");
4239
+ labelSelection.filter(function(item) {
4240
+ return item.style.label.visibility === "hover" && !this.classList.contains("label-selection-pinned");
4241
+ }).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4240
4242
  });
4241
4243
  }
4242
4244
 
@@ -4535,6 +4537,27 @@ function createGraph(config) {
4535
4537
  let svgElement = null;
4536
4538
  let zoomBehavior = null;
4537
4539
  let simulation = null;
4540
+ const nodeSelectHandlers = /* @__PURE__ */ new Set();
4541
+ const linkSelectHandlers = /* @__PURE__ */ new Set();
4542
+ function on(event, handler) {
4543
+ if (event === "nodeSelect") {
4544
+ nodeSelectHandlers.add(handler);
4545
+ return () => {
4546
+ nodeSelectHandlers.delete(handler);
4547
+ };
4548
+ }
4549
+ linkSelectHandlers.add(handler);
4550
+ return () => {
4551
+ linkSelectHandlers.delete(handler);
4552
+ };
4553
+ }
4554
+ function off(event, handler) {
4555
+ if (event === "nodeSelect") {
4556
+ nodeSelectHandlers.delete(handler);
4557
+ } else {
4558
+ linkSelectHandlers.delete(handler);
4559
+ }
4560
+ }
4538
4561
  function render() {
4539
4562
  destroy();
4540
4563
  const layers = createGraphLayers(config.container);
@@ -4618,10 +4641,135 @@ function createGraph(config) {
4618
4641
  if (config.interaction?.drag?.enabled !== false) {
4619
4642
  nodeSelection.call(createDragBehavior(simulation));
4620
4643
  }
4644
+ const selectionConfig = config.interaction?.selection;
4645
+ if (selectionConfig?.enabled) {
4646
+ let selectedNodeElement = null;
4647
+ let selectedLinkElement = null;
4648
+ const linkMarkerSnapshots = /* @__PURE__ */ new Map();
4649
+ linkSelection.each(function() {
4650
+ const linkElement = this;
4651
+ linkMarkerSnapshots.set(linkElement, linkElement.getAttribute("marker-end"));
4652
+ });
4653
+ const deselectNode = () => {
4654
+ if (!selectedNodeElement) {
4655
+ return;
4656
+ }
4657
+ const nodeElement = selectedNodeElement;
4658
+ nodeElement.style.fill = "";
4659
+ nodeElement.style.stroke = "";
4660
+ nodeElement.style.strokeWidth = "";
4661
+ nodeElement.style.opacity = "";
4662
+ nodeElement.style.removeProperty("r");
4663
+ root2.selectAll(".link-label.label-selection-pinned").classed("label-selection-pinned", false).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4664
+ selectedNodeElement = null;
4665
+ };
4666
+ const deselectLink = () => {
4667
+ if (!selectedLinkElement) {
4668
+ return;
4669
+ }
4670
+ const linkElement = selectedLinkElement;
4671
+ linkElement.style.stroke = "";
4672
+ linkElement.style.strokeWidth = "";
4673
+ linkElement.style.opacity = "";
4674
+ const originalMarkerEnd = linkMarkerSnapshots.get(linkElement);
4675
+ if (originalMarkerEnd) {
4676
+ linkElement.setAttribute("marker-end", originalMarkerEnd);
4677
+ } else {
4678
+ linkElement.removeAttribute("marker-end");
4679
+ }
4680
+ selectedLinkElement = null;
4681
+ };
4682
+ nodeSelection.on("click.select", function(event, node) {
4683
+ event.stopPropagation();
4684
+ const nodeElement = this;
4685
+ if (selectedNodeElement === nodeElement) {
4686
+ deselectNode();
4687
+ return;
4688
+ }
4689
+ deselectNode();
4690
+ deselectLink();
4691
+ selectedNodeElement = nodeElement;
4692
+ const nodeStyle = selectionConfig.nodeStyle;
4693
+ if (nodeStyle) {
4694
+ if (nodeStyle.fill !== void 0) {
4695
+ nodeElement.style.fill = nodeStyle.fill;
4696
+ }
4697
+ if (nodeStyle.stroke !== void 0) {
4698
+ nodeElement.style.stroke = nodeStyle.stroke;
4699
+ }
4700
+ if (nodeStyle.strokeWidth !== void 0) {
4701
+ nodeElement.style.strokeWidth = String(nodeStyle.strokeWidth);
4702
+ }
4703
+ if (nodeStyle.opacity !== void 0) {
4704
+ nodeElement.style.opacity = String(nodeStyle.opacity);
4705
+ }
4706
+ if (nodeStyle.radius !== void 0) {
4707
+ nodeElement.style.setProperty("r", String(nodeStyle.radius));
4708
+ }
4709
+ }
4710
+ root2.selectAll(".link-label").filter((item) => {
4711
+ if (item.style.label.visibility !== "hover") {
4712
+ return false;
4713
+ }
4714
+ const source = item.link.source;
4715
+ const target = item.link.target;
4716
+ return source.id === node.id || target.id === node.id;
4717
+ }).classed("label-selection-pinned", true).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4718
+ nodeSelectHandlers.forEach((handler) => handler(node, nodeElement));
4719
+ });
4720
+ const selectLink = (event, renderableLink, linkElement) => {
4721
+ event.stopPropagation();
4722
+ if (selectedLinkElement === linkElement) {
4723
+ deselectLink();
4724
+ return;
4725
+ }
4726
+ deselectLink();
4727
+ deselectNode();
4728
+ selectedLinkElement = linkElement;
4729
+ const linkStyle = selectionConfig.linkStyle;
4730
+ if (linkStyle) {
4731
+ if (linkStyle.stroke !== void 0) {
4732
+ linkElement.style.stroke = linkStyle.stroke;
4733
+ }
4734
+ if (linkStyle.strokeWidth !== void 0) {
4735
+ linkElement.style.strokeWidth = String(linkStyle.strokeWidth);
4736
+ }
4737
+ if (linkStyle.opacity !== void 0) {
4738
+ linkElement.style.opacity = String(linkStyle.opacity);
4739
+ }
4740
+ if (linkStyle.stroke !== void 0 && renderableLink.style.arrow.enabled) {
4741
+ const selectionMarkerStyle = {
4742
+ stroke: linkStyle.stroke,
4743
+ arrow: { fill: linkStyle.stroke, size: renderableLink.style.arrow.size }
4744
+ };
4745
+ const selectionMarkerId = createArrowMarker({ svg: layers.svg, style: selectionMarkerStyle });
4746
+ select_default2(linkElement).attr("marker-end", `url(#${selectionMarkerId})`);
4747
+ }
4748
+ }
4749
+ linkSelectHandlers.forEach((handler) => handler(renderableLink.link, linkElement));
4750
+ };
4751
+ linkSelection.on("click.select", function(event, renderableLink) {
4752
+ selectLink(event, renderableLink, this);
4753
+ });
4754
+ const linkHitAreaSelection = root2.select('[data-layer="links"]').selectAll("line.link-hit-area").data(linkSelection.data()).join("line").attr("class", "link-hit-area").attr("stroke", "rgba(0,0,0,0)").attr("stroke-width", (item) => item.style.arrow.size * 4).style("pointer-events", "stroke").style("cursor", "pointer").attr("opacity", 0);
4755
+ simulation.on("tick.hitarea", () => {
4756
+ linkHitAreaSelection.attr("x1", (item) => item.link.source.x ?? 0).attr("y1", (item) => item.link.source.y ?? 0).attr("x2", (item) => item.link.target.x ?? 0).attr("y2", (item) => item.link.target.y ?? 0);
4757
+ });
4758
+ linkHitAreaSelection.on("click.select", function(event, renderableLink) {
4759
+ const visibleLinkNode = linkSelection.filter((d) => d === renderableLink).node();
4760
+ if (visibleLinkNode) {
4761
+ selectLink(event, renderableLink, visibleLinkNode);
4762
+ }
4763
+ });
4764
+ select_default2(layers.svg).on("click.deselect", () => {
4765
+ deselectNode();
4766
+ deselectLink();
4767
+ });
4768
+ }
4621
4769
  if (config.controls?.enabled) {
4622
4770
  controls = createGraphControls(
4623
4771
  layers.overlay,
4624
- { zoomIn, zoomOut, resetView, fitView, destroy, render, exportGraph },
4772
+ { zoomIn, zoomOut, resetView, fitView },
4625
4773
  config.controls
4626
4774
  );
4627
4775
  controls.mount();
@@ -4631,13 +4779,19 @@ function createGraph(config) {
4631
4779
  }
4632
4780
  }
4633
4781
  function resetView() {
4634
- if (!zoomBehavior || !svgElement) return;
4782
+ if (!zoomBehavior || !svgElement) {
4783
+ return;
4784
+ }
4635
4785
  select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, identity2);
4636
4786
  }
4637
4787
  function fitView() {
4638
- if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
4788
+ if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) {
4789
+ return;
4790
+ }
4639
4791
  const bounds = rootGroup.getBBox();
4640
- if (bounds.width === 0 || bounds.height === 0) return;
4792
+ if (bounds.width === 0 || bounds.height === 0) {
4793
+ return;
4794
+ }
4641
4795
  const scale = Math.min(dimensions.width / bounds.width, dimensions.height / bounds.height) * 0.9;
4642
4796
  const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4643
4797
  const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
@@ -4645,11 +4799,15 @@ function createGraph(config) {
4645
4799
  select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, transform2);
4646
4800
  }
4647
4801
  function zoomIn() {
4648
- if (!zoomBehavior || !svgElement) return;
4802
+ if (!zoomBehavior || !svgElement) {
4803
+ return;
4804
+ }
4649
4805
  select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4650
4806
  }
4651
4807
  function zoomOut() {
4652
- if (!zoomBehavior || !svgElement) return;
4808
+ if (!zoomBehavior || !svgElement) {
4809
+ return;
4810
+ }
4653
4811
  select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 0.8);
4654
4812
  }
4655
4813
  async function exportGraph(fileName) {
@@ -4696,7 +4854,7 @@ function createGraph(config) {
4696
4854
  config.container.removeChild(config.container.firstChild);
4697
4855
  }
4698
4856
  }
4699
- return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph };
4857
+ return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph, on, off };
4700
4858
  }
4701
4859
  // Annotate the CommonJS export names for ESM import in node:
4702
4860
  0 && (module.exports = {
package/dist/index.d.cts CHANGED
@@ -138,6 +138,10 @@ interface GraphInstance {
138
138
  fitView(): void;
139
139
  destroy(): void;
140
140
  exportGraph(fileName?: string): void;
141
+ on(event: 'nodeSelect', handler: (node: GraphNode, element: SVGCircleElement) => void): () => void;
142
+ on(event: 'linkSelect', handler: (link: GraphLink, element: SVGLineElement) => void): () => void;
143
+ off(event: 'nodeSelect', handler: (node: GraphNode, element: SVGCircleElement) => void): void;
144
+ off(event: 'linkSelect', handler: (link: GraphLink, element: SVGLineElement) => void): void;
141
145
  }
142
146
 
143
147
  declare function createGraph(config: GraphConfig): GraphInstance;
package/dist/index.d.ts CHANGED
@@ -138,6 +138,10 @@ interface GraphInstance {
138
138
  fitView(): void;
139
139
  destroy(): void;
140
140
  exportGraph(fileName?: string): void;
141
+ on(event: 'nodeSelect', handler: (node: GraphNode, element: SVGCircleElement) => void): () => void;
142
+ on(event: 'linkSelect', handler: (link: GraphLink, element: SVGLineElement) => void): () => void;
143
+ off(event: 'nodeSelect', handler: (node: GraphNode, element: SVGCircleElement) => void): void;
144
+ off(event: 'linkSelect', handler: (link: GraphLink, element: SVGLineElement) => void): void;
141
145
  }
142
146
 
143
147
  declare function createGraph(config: GraphConfig): GraphInstance;
package/dist/index.js CHANGED
@@ -3731,6 +3731,56 @@ function createGraphSimulation(config) {
3731
3731
  return { simulation };
3732
3732
  }
3733
3733
 
3734
+ // src/utils/get-link-marker-id.ts
3735
+ function getLinkMarkerId(style) {
3736
+ const markerStyle = {
3737
+ stroke: style.stroke ?? "#94a3b8",
3738
+ strokeWidth: style.strokeWidth ?? 2,
3739
+ arrowFill: style.arrow?.fill ?? style.stroke ?? "#94a3b8",
3740
+ arrowSize: style.arrow?.size ?? 6
3741
+ };
3742
+ const serializedStyle = JSON.stringify(markerStyle);
3743
+ const hash = createHash(serializedStyle);
3744
+ return `graph-arrow-${hash}`;
3745
+ }
3746
+ function createHash(value) {
3747
+ let hash = 0;
3748
+ for (let index2 = 0; index2 < value.length; index2 += 1) {
3749
+ const charCode = value.charCodeAt(index2);
3750
+ hash = (hash << 5) - hash + charCode;
3751
+ hash |= 0;
3752
+ }
3753
+ return Math.abs(hash).toString(36);
3754
+ }
3755
+
3756
+ // src/core/create-arrow-marker.ts
3757
+ function createArrowMarker(params) {
3758
+ const markerId = getLinkMarkerId(params.style);
3759
+ const existingMarker = params.svg.querySelector(`#${markerId}`);
3760
+ if (existingMarker) {
3761
+ return markerId;
3762
+ }
3763
+ const arrowSize = params.style.arrow?.size ?? 6;
3764
+ const fill = params.style.arrow?.fill ?? params.style.stroke ?? "#94a3b8";
3765
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
3766
+ const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
3767
+ marker.setAttribute("id", markerId);
3768
+ marker.setAttribute("viewBox", "0 0 20 20");
3769
+ marker.setAttribute("refX", "0");
3770
+ marker.setAttribute("refY", "10");
3771
+ marker.setAttribute("markerWidth", String(arrowSize * 2));
3772
+ marker.setAttribute("markerHeight", String(arrowSize * 2));
3773
+ marker.setAttribute("orient", "auto");
3774
+ marker.setAttribute("markerUnits", "userSpaceOnUse");
3775
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
3776
+ path.setAttribute("d", "M 0 0 L 20 10 L 0 20 z");
3777
+ path.setAttribute("fill", fill);
3778
+ marker.appendChild(path);
3779
+ defs.appendChild(marker);
3780
+ params.svg.insertBefore(defs, params.svg.firstChild);
3781
+ return markerId;
3782
+ }
3783
+
3734
3784
  // src/controls/graph-controls.utils.ts
3735
3785
  function resolveControlsPosition(position) {
3736
3786
  return position ?? "bottom-left";
@@ -3959,56 +4009,6 @@ function mergeLinkStyle(base, override) {
3959
4009
  };
3960
4010
  }
3961
4011
 
3962
- // src/utils/get-link-marker-id.ts
3963
- function getLinkMarkerId(style) {
3964
- const markerStyle = {
3965
- stroke: style.stroke ?? "#94a3b8",
3966
- strokeWidth: style.strokeWidth ?? 2,
3967
- arrowFill: style.arrow?.fill ?? style.stroke ?? "#94a3b8",
3968
- arrowSize: style.arrow?.size ?? 6
3969
- };
3970
- const serializedStyle = JSON.stringify(markerStyle);
3971
- const hash = createHash(serializedStyle);
3972
- return `graph-arrow-${hash}`;
3973
- }
3974
- function createHash(value) {
3975
- let hash = 0;
3976
- for (let index2 = 0; index2 < value.length; index2 += 1) {
3977
- const charCode = value.charCodeAt(index2);
3978
- hash = (hash << 5) - hash + charCode;
3979
- hash |= 0;
3980
- }
3981
- return Math.abs(hash).toString(36);
3982
- }
3983
-
3984
- // src/core/create-arrow-marker.ts
3985
- function createArrowMarker(params) {
3986
- const markerId = getLinkMarkerId(params.style);
3987
- const existingMarker = params.svg.querySelector(`#${markerId}`);
3988
- if (existingMarker) {
3989
- return markerId;
3990
- }
3991
- const arrowSize = params.style.arrow?.size ?? 6;
3992
- const fill = params.style.arrow?.fill ?? params.style.stroke ?? "#94a3b8";
3993
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
3994
- const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
3995
- marker.setAttribute("id", markerId);
3996
- marker.setAttribute("viewBox", "0 0 20 20");
3997
- marker.setAttribute("refX", "0");
3998
- marker.setAttribute("refY", "10");
3999
- marker.setAttribute("markerWidth", String(arrowSize * 2));
4000
- marker.setAttribute("markerHeight", String(arrowSize * 2));
4001
- marker.setAttribute("orient", "auto");
4002
- marker.setAttribute("markerUnits", "userSpaceOnUse");
4003
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
4004
- path.setAttribute("d", "M 0 0 L 20 10 L 0 20 z");
4005
- path.setAttribute("fill", fill);
4006
- marker.appendChild(path);
4007
- defs.appendChild(marker);
4008
- params.svg.insertBefore(defs, params.svg.firstChild);
4009
- return markerId;
4010
- }
4011
-
4012
4012
  // src/renderer/links.ts
4013
4013
  function getShortenedTargetPoint(link, style) {
4014
4014
  const source = link.source;
@@ -4046,7 +4046,7 @@ function getLinkKey(link) {
4046
4046
  }
4047
4047
  function renderLinks(ctx, links) {
4048
4048
  const renderableLinks = createRenderableLinks(ctx, links);
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");
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").style("cursor", "pointer");
4050
4050
  const labelSelection = ctx.root.selectAll(".link-label");
4051
4051
  linkSelection.on("mouseenter.label-hover", (_event, d) => {
4052
4052
  labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 1);
@@ -4200,7 +4200,9 @@ function createNodeHover(nodeSelection, hoverStyle) {
4200
4200
  return s.id === d.id || t.id === d.id;
4201
4201
  }).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4202
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");
4203
+ labelSelection.filter(function(item) {
4204
+ return item.style.label.visibility === "hover" && !this.classList.contains("label-selection-pinned");
4205
+ }).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4204
4206
  });
4205
4207
  }
4206
4208
 
@@ -4499,6 +4501,27 @@ function createGraph(config) {
4499
4501
  let svgElement = null;
4500
4502
  let zoomBehavior = null;
4501
4503
  let simulation = null;
4504
+ const nodeSelectHandlers = /* @__PURE__ */ new Set();
4505
+ const linkSelectHandlers = /* @__PURE__ */ new Set();
4506
+ function on(event, handler) {
4507
+ if (event === "nodeSelect") {
4508
+ nodeSelectHandlers.add(handler);
4509
+ return () => {
4510
+ nodeSelectHandlers.delete(handler);
4511
+ };
4512
+ }
4513
+ linkSelectHandlers.add(handler);
4514
+ return () => {
4515
+ linkSelectHandlers.delete(handler);
4516
+ };
4517
+ }
4518
+ function off(event, handler) {
4519
+ if (event === "nodeSelect") {
4520
+ nodeSelectHandlers.delete(handler);
4521
+ } else {
4522
+ linkSelectHandlers.delete(handler);
4523
+ }
4524
+ }
4502
4525
  function render() {
4503
4526
  destroy();
4504
4527
  const layers = createGraphLayers(config.container);
@@ -4582,10 +4605,135 @@ function createGraph(config) {
4582
4605
  if (config.interaction?.drag?.enabled !== false) {
4583
4606
  nodeSelection.call(createDragBehavior(simulation));
4584
4607
  }
4608
+ const selectionConfig = config.interaction?.selection;
4609
+ if (selectionConfig?.enabled) {
4610
+ let selectedNodeElement = null;
4611
+ let selectedLinkElement = null;
4612
+ const linkMarkerSnapshots = /* @__PURE__ */ new Map();
4613
+ linkSelection.each(function() {
4614
+ const linkElement = this;
4615
+ linkMarkerSnapshots.set(linkElement, linkElement.getAttribute("marker-end"));
4616
+ });
4617
+ const deselectNode = () => {
4618
+ if (!selectedNodeElement) {
4619
+ return;
4620
+ }
4621
+ const nodeElement = selectedNodeElement;
4622
+ nodeElement.style.fill = "";
4623
+ nodeElement.style.stroke = "";
4624
+ nodeElement.style.strokeWidth = "";
4625
+ nodeElement.style.opacity = "";
4626
+ nodeElement.style.removeProperty("r");
4627
+ root2.selectAll(".link-label.label-selection-pinned").classed("label-selection-pinned", false).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4628
+ selectedNodeElement = null;
4629
+ };
4630
+ const deselectLink = () => {
4631
+ if (!selectedLinkElement) {
4632
+ return;
4633
+ }
4634
+ const linkElement = selectedLinkElement;
4635
+ linkElement.style.stroke = "";
4636
+ linkElement.style.strokeWidth = "";
4637
+ linkElement.style.opacity = "";
4638
+ const originalMarkerEnd = linkMarkerSnapshots.get(linkElement);
4639
+ if (originalMarkerEnd) {
4640
+ linkElement.setAttribute("marker-end", originalMarkerEnd);
4641
+ } else {
4642
+ linkElement.removeAttribute("marker-end");
4643
+ }
4644
+ selectedLinkElement = null;
4645
+ };
4646
+ nodeSelection.on("click.select", function(event, node) {
4647
+ event.stopPropagation();
4648
+ const nodeElement = this;
4649
+ if (selectedNodeElement === nodeElement) {
4650
+ deselectNode();
4651
+ return;
4652
+ }
4653
+ deselectNode();
4654
+ deselectLink();
4655
+ selectedNodeElement = nodeElement;
4656
+ const nodeStyle = selectionConfig.nodeStyle;
4657
+ if (nodeStyle) {
4658
+ if (nodeStyle.fill !== void 0) {
4659
+ nodeElement.style.fill = nodeStyle.fill;
4660
+ }
4661
+ if (nodeStyle.stroke !== void 0) {
4662
+ nodeElement.style.stroke = nodeStyle.stroke;
4663
+ }
4664
+ if (nodeStyle.strokeWidth !== void 0) {
4665
+ nodeElement.style.strokeWidth = String(nodeStyle.strokeWidth);
4666
+ }
4667
+ if (nodeStyle.opacity !== void 0) {
4668
+ nodeElement.style.opacity = String(nodeStyle.opacity);
4669
+ }
4670
+ if (nodeStyle.radius !== void 0) {
4671
+ nodeElement.style.setProperty("r", String(nodeStyle.radius));
4672
+ }
4673
+ }
4674
+ root2.selectAll(".link-label").filter((item) => {
4675
+ if (item.style.label.visibility !== "hover") {
4676
+ return false;
4677
+ }
4678
+ const source = item.link.source;
4679
+ const target = item.link.target;
4680
+ return source.id === node.id || target.id === node.id;
4681
+ }).classed("label-selection-pinned", true).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4682
+ nodeSelectHandlers.forEach((handler) => handler(node, nodeElement));
4683
+ });
4684
+ const selectLink = (event, renderableLink, linkElement) => {
4685
+ event.stopPropagation();
4686
+ if (selectedLinkElement === linkElement) {
4687
+ deselectLink();
4688
+ return;
4689
+ }
4690
+ deselectLink();
4691
+ deselectNode();
4692
+ selectedLinkElement = linkElement;
4693
+ const linkStyle = selectionConfig.linkStyle;
4694
+ if (linkStyle) {
4695
+ if (linkStyle.stroke !== void 0) {
4696
+ linkElement.style.stroke = linkStyle.stroke;
4697
+ }
4698
+ if (linkStyle.strokeWidth !== void 0) {
4699
+ linkElement.style.strokeWidth = String(linkStyle.strokeWidth);
4700
+ }
4701
+ if (linkStyle.opacity !== void 0) {
4702
+ linkElement.style.opacity = String(linkStyle.opacity);
4703
+ }
4704
+ if (linkStyle.stroke !== void 0 && renderableLink.style.arrow.enabled) {
4705
+ const selectionMarkerStyle = {
4706
+ stroke: linkStyle.stroke,
4707
+ arrow: { fill: linkStyle.stroke, size: renderableLink.style.arrow.size }
4708
+ };
4709
+ const selectionMarkerId = createArrowMarker({ svg: layers.svg, style: selectionMarkerStyle });
4710
+ select_default2(linkElement).attr("marker-end", `url(#${selectionMarkerId})`);
4711
+ }
4712
+ }
4713
+ linkSelectHandlers.forEach((handler) => handler(renderableLink.link, linkElement));
4714
+ };
4715
+ linkSelection.on("click.select", function(event, renderableLink) {
4716
+ selectLink(event, renderableLink, this);
4717
+ });
4718
+ const linkHitAreaSelection = root2.select('[data-layer="links"]').selectAll("line.link-hit-area").data(linkSelection.data()).join("line").attr("class", "link-hit-area").attr("stroke", "rgba(0,0,0,0)").attr("stroke-width", (item) => item.style.arrow.size * 4).style("pointer-events", "stroke").style("cursor", "pointer").attr("opacity", 0);
4719
+ simulation.on("tick.hitarea", () => {
4720
+ linkHitAreaSelection.attr("x1", (item) => item.link.source.x ?? 0).attr("y1", (item) => item.link.source.y ?? 0).attr("x2", (item) => item.link.target.x ?? 0).attr("y2", (item) => item.link.target.y ?? 0);
4721
+ });
4722
+ linkHitAreaSelection.on("click.select", function(event, renderableLink) {
4723
+ const visibleLinkNode = linkSelection.filter((d) => d === renderableLink).node();
4724
+ if (visibleLinkNode) {
4725
+ selectLink(event, renderableLink, visibleLinkNode);
4726
+ }
4727
+ });
4728
+ select_default2(layers.svg).on("click.deselect", () => {
4729
+ deselectNode();
4730
+ deselectLink();
4731
+ });
4732
+ }
4585
4733
  if (config.controls?.enabled) {
4586
4734
  controls = createGraphControls(
4587
4735
  layers.overlay,
4588
- { zoomIn, zoomOut, resetView, fitView, destroy, render, exportGraph },
4736
+ { zoomIn, zoomOut, resetView, fitView },
4589
4737
  config.controls
4590
4738
  );
4591
4739
  controls.mount();
@@ -4595,13 +4743,19 @@ function createGraph(config) {
4595
4743
  }
4596
4744
  }
4597
4745
  function resetView() {
4598
- if (!zoomBehavior || !svgElement) return;
4746
+ if (!zoomBehavior || !svgElement) {
4747
+ return;
4748
+ }
4599
4749
  select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, identity2);
4600
4750
  }
4601
4751
  function fitView() {
4602
- if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
4752
+ if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) {
4753
+ return;
4754
+ }
4603
4755
  const bounds = rootGroup.getBBox();
4604
- if (bounds.width === 0 || bounds.height === 0) return;
4756
+ if (bounds.width === 0 || bounds.height === 0) {
4757
+ return;
4758
+ }
4605
4759
  const scale = Math.min(dimensions.width / bounds.width, dimensions.height / bounds.height) * 0.9;
4606
4760
  const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4607
4761
  const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
@@ -4609,11 +4763,15 @@ function createGraph(config) {
4609
4763
  select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, transform2);
4610
4764
  }
4611
4765
  function zoomIn() {
4612
- if (!zoomBehavior || !svgElement) return;
4766
+ if (!zoomBehavior || !svgElement) {
4767
+ return;
4768
+ }
4613
4769
  select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4614
4770
  }
4615
4771
  function zoomOut() {
4616
- if (!zoomBehavior || !svgElement) return;
4772
+ if (!zoomBehavior || !svgElement) {
4773
+ return;
4774
+ }
4617
4775
  select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 0.8);
4618
4776
  }
4619
4777
  async function exportGraph(fileName) {
@@ -4660,7 +4818,7 @@ function createGraph(config) {
4660
4818
  config.container.removeChild(config.container.firstChild);
4661
4819
  }
4662
4820
  }
4663
- return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph };
4821
+ return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph, on, off };
4664
4822
  }
4665
4823
  export {
4666
4824
  createGraph
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-graph",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",