polly-graph 0.1.5 → 0.1.7

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
@@ -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
 
@@ -4522,6 +4524,60 @@ async function captureAndDownloadGraph(container, options = {}) {
4522
4524
  }
4523
4525
  }
4524
4526
 
4527
+ // src/utils/node-link-selection.utils.ts
4528
+ function deselectNode(nodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers) {
4529
+ nodeElement.style.fill = "";
4530
+ nodeElement.style.stroke = "";
4531
+ nodeElement.style.strokeWidth = "";
4532
+ nodeElement.style.opacity = "";
4533
+ nodeElement.style.removeProperty("r");
4534
+ root2.selectAll(".link-label.label-selection-pinned").classed("label-selection-pinned", false).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4535
+ const nodeData = select_default2(nodeElement).datum();
4536
+ nodeSelectHandlers.clear();
4537
+ nodeDeselectHandlers.forEach((handler) => handler(nodeData, nodeElement));
4538
+ }
4539
+ function deselectLink(linkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers) {
4540
+ linkElement.style.stroke = "";
4541
+ linkElement.style.strokeWidth = "";
4542
+ linkElement.style.opacity = "";
4543
+ const originalMarkerEnd = linkMarkerSnapshots.get(linkElement);
4544
+ if (originalMarkerEnd) {
4545
+ linkElement.setAttribute("marker-end", originalMarkerEnd);
4546
+ } else {
4547
+ linkElement.removeAttribute("marker-end");
4548
+ }
4549
+ const linkData = select_default2(linkElement).datum().link;
4550
+ linkSelectHandlers.clear();
4551
+ linkDeselectHandlers.forEach((handler) => handler(linkData, linkElement));
4552
+ }
4553
+ function selectLink(event, renderableLink, linkElement, selectionConfig, layers, linkSelectHandlers) {
4554
+ event.stopPropagation();
4555
+ const linkStyle = selectionConfig.linkStyle;
4556
+ if (linkStyle) {
4557
+ if (linkStyle.stroke !== void 0) {
4558
+ linkElement.style.stroke = linkStyle.stroke;
4559
+ }
4560
+ if (linkStyle.strokeWidth !== void 0) {
4561
+ linkElement.style.strokeWidth = String(linkStyle.strokeWidth);
4562
+ }
4563
+ if (linkStyle.opacity !== void 0) {
4564
+ linkElement.style.opacity = String(linkStyle.opacity);
4565
+ }
4566
+ if (linkStyle.stroke !== void 0 && renderableLink.style.arrow.enabled) {
4567
+ const selectionMarkerStyle = {
4568
+ stroke: linkStyle.stroke,
4569
+ arrow: { fill: linkStyle.stroke, size: renderableLink.style.arrow.size }
4570
+ };
4571
+ const selectionMarkerId = createArrowMarker({ svg: layers.svg, style: selectionMarkerStyle });
4572
+ select_default2(linkElement).attr("marker-end", `url(#${selectionMarkerId})`);
4573
+ }
4574
+ }
4575
+ linkSelectHandlers.forEach((handler) => handler(renderableLink.link, linkElement));
4576
+ }
4577
+ function createLinkHitArea(root2, linkSelection) {
4578
+ return 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);
4579
+ }
4580
+
4525
4581
  // src/create-graph.ts
4526
4582
  function createGraph(config) {
4527
4583
  let cleanupResize = null;
@@ -4535,6 +4591,45 @@ function createGraph(config) {
4535
4591
  let svgElement = null;
4536
4592
  let zoomBehavior = null;
4537
4593
  let simulation = null;
4594
+ const nodeSelectHandlers = /* @__PURE__ */ new Set();
4595
+ const nodeDeselectHandlers = /* @__PURE__ */ new Set();
4596
+ const linkSelectHandlers = /* @__PURE__ */ new Set();
4597
+ const linkDeselectHandlers = /* @__PURE__ */ new Set();
4598
+ function on(event, handler) {
4599
+ if (event === "nodeSelect") {
4600
+ nodeSelectHandlers.add(handler);
4601
+ return () => {
4602
+ nodeSelectHandlers.delete(handler);
4603
+ };
4604
+ }
4605
+ if (event === "nodeDeselect") {
4606
+ nodeDeselectHandlers.add(handler);
4607
+ return () => {
4608
+ nodeDeselectHandlers.delete(handler);
4609
+ };
4610
+ }
4611
+ if (event === "linkSelect") {
4612
+ linkSelectHandlers.add(handler);
4613
+ return () => {
4614
+ linkSelectHandlers.delete(handler);
4615
+ };
4616
+ }
4617
+ linkDeselectHandlers.add(handler);
4618
+ return () => {
4619
+ linkDeselectHandlers.delete(handler);
4620
+ };
4621
+ }
4622
+ function off(event, handler) {
4623
+ if (event === "nodeSelect") {
4624
+ nodeSelectHandlers.delete(handler);
4625
+ } else if (event === "nodeDeselect") {
4626
+ nodeDeselectHandlers.delete(handler);
4627
+ } else if (event === "linkSelect") {
4628
+ linkSelectHandlers.delete(handler);
4629
+ } else {
4630
+ linkDeselectHandlers.delete(handler);
4631
+ }
4632
+ }
4538
4633
  function render() {
4539
4634
  destroy();
4540
4635
  const layers = createGraphLayers(config.container);
@@ -4618,10 +4713,117 @@ function createGraph(config) {
4618
4713
  if (config.interaction?.drag?.enabled !== false) {
4619
4714
  nodeSelection.call(createDragBehavior(simulation));
4620
4715
  }
4716
+ const selectionConfig = config.interaction?.selection;
4717
+ if (selectionConfig?.enabled) {
4718
+ let selectedNodeElement = null;
4719
+ let selectedLinkElement = null;
4720
+ const linkMarkerSnapshots = /* @__PURE__ */ new Map();
4721
+ linkSelection.each(function() {
4722
+ const linkElement = this;
4723
+ linkMarkerSnapshots.set(linkElement, linkElement.getAttribute("marker-end"));
4724
+ });
4725
+ nodeSelection.on("click.select", function(event, node) {
4726
+ event.stopPropagation();
4727
+ const nodeElement = this;
4728
+ if (selectedNodeElement === nodeElement) {
4729
+ deselectNode(nodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4730
+ selectedNodeElement = null;
4731
+ return;
4732
+ }
4733
+ if (selectedNodeElement) {
4734
+ deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4735
+ selectedNodeElement = null;
4736
+ }
4737
+ if (selectedLinkElement) {
4738
+ deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4739
+ selectedLinkElement = null;
4740
+ }
4741
+ selectedNodeElement = nodeElement;
4742
+ const nodeStyle = selectionConfig.nodeStyle;
4743
+ if (nodeStyle) {
4744
+ if (nodeStyle.fill !== void 0) {
4745
+ nodeElement.style.fill = nodeStyle.fill;
4746
+ }
4747
+ if (nodeStyle.stroke !== void 0) {
4748
+ nodeElement.style.stroke = nodeStyle.stroke;
4749
+ }
4750
+ if (nodeStyle.strokeWidth !== void 0) {
4751
+ nodeElement.style.strokeWidth = String(nodeStyle.strokeWidth);
4752
+ }
4753
+ if (nodeStyle.opacity !== void 0) {
4754
+ nodeElement.style.opacity = String(nodeStyle.opacity);
4755
+ }
4756
+ if (nodeStyle.radius !== void 0) {
4757
+ nodeElement.style.setProperty("r", String(nodeStyle.radius));
4758
+ }
4759
+ }
4760
+ root2.selectAll(".link-label").filter((item) => {
4761
+ if (item.style.label.visibility !== "hover") {
4762
+ return false;
4763
+ }
4764
+ const source = item.link.source;
4765
+ const target = item.link.target;
4766
+ return source.id === node.id || target.id === node.id;
4767
+ }).classed("label-selection-pinned", true).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4768
+ nodeSelectHandlers.forEach((handler) => handler(node, nodeElement));
4769
+ });
4770
+ linkSelection.on("click.select", function(event, renderableLink) {
4771
+ const linkElement = this;
4772
+ if (selectedLinkElement === linkElement) {
4773
+ deselectLink(linkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4774
+ selectedLinkElement = null;
4775
+ return;
4776
+ }
4777
+ if (selectedLinkElement) {
4778
+ deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4779
+ selectedLinkElement = null;
4780
+ }
4781
+ if (selectedNodeElement) {
4782
+ deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4783
+ selectedNodeElement = null;
4784
+ }
4785
+ selectedLinkElement = linkElement;
4786
+ selectLink(event, renderableLink, linkElement, selectionConfig, layers, linkSelectHandlers);
4787
+ });
4788
+ const linkHitAreaSelection = createLinkHitArea(root2, linkSelection);
4789
+ simulation.on("tick.hitarea", () => {
4790
+ 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);
4791
+ });
4792
+ linkHitAreaSelection.on("click.select", function(event, renderableLink) {
4793
+ const visibleLinkNode = linkSelection.filter((d) => d === renderableLink).node();
4794
+ if (visibleLinkNode) {
4795
+ if (selectedLinkElement === visibleLinkNode) {
4796
+ deselectLink(visibleLinkNode, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4797
+ selectedLinkElement = null;
4798
+ return;
4799
+ }
4800
+ if (selectedLinkElement) {
4801
+ deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4802
+ selectedLinkElement = null;
4803
+ }
4804
+ if (selectedNodeElement) {
4805
+ deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4806
+ selectedNodeElement = null;
4807
+ }
4808
+ selectedLinkElement = visibleLinkNode;
4809
+ selectLink(event, renderableLink, visibleLinkNode, selectionConfig, layers, linkSelectHandlers);
4810
+ }
4811
+ });
4812
+ select_default2(layers.svg).on("click.deselect", () => {
4813
+ if (selectedNodeElement) {
4814
+ deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4815
+ selectedNodeElement = null;
4816
+ }
4817
+ if (selectedLinkElement) {
4818
+ deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4819
+ selectedLinkElement = null;
4820
+ }
4821
+ });
4822
+ }
4621
4823
  if (config.controls?.enabled) {
4622
4824
  controls = createGraphControls(
4623
4825
  layers.overlay,
4624
- { zoomIn, zoomOut, resetView, fitView, destroy, render, exportGraph },
4826
+ { zoomIn, zoomOut, resetView, fitView },
4625
4827
  config.controls
4626
4828
  );
4627
4829
  controls.mount();
@@ -4631,13 +4833,19 @@ function createGraph(config) {
4631
4833
  }
4632
4834
  }
4633
4835
  function resetView() {
4634
- if (!zoomBehavior || !svgElement) return;
4836
+ if (!zoomBehavior || !svgElement) {
4837
+ return;
4838
+ }
4635
4839
  select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, identity2);
4636
4840
  }
4637
4841
  function fitView() {
4638
- if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
4842
+ if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) {
4843
+ return;
4844
+ }
4639
4845
  const bounds = rootGroup.getBBox();
4640
- if (bounds.width === 0 || bounds.height === 0) return;
4846
+ if (bounds.width === 0 || bounds.height === 0) {
4847
+ return;
4848
+ }
4641
4849
  const scale = Math.min(dimensions.width / bounds.width, dimensions.height / bounds.height) * 0.9;
4642
4850
  const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4643
4851
  const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
@@ -4645,11 +4853,15 @@ function createGraph(config) {
4645
4853
  select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, transform2);
4646
4854
  }
4647
4855
  function zoomIn() {
4648
- if (!zoomBehavior || !svgElement) return;
4856
+ if (!zoomBehavior || !svgElement) {
4857
+ return;
4858
+ }
4649
4859
  select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4650
4860
  }
4651
4861
  function zoomOut() {
4652
- if (!zoomBehavior || !svgElement) return;
4862
+ if (!zoomBehavior || !svgElement) {
4863
+ return;
4864
+ }
4653
4865
  select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 0.8);
4654
4866
  }
4655
4867
  async function exportGraph(fileName) {
@@ -4696,7 +4908,7 @@ function createGraph(config) {
4696
4908
  config.container.removeChild(config.container.firstChild);
4697
4909
  }
4698
4910
  }
4699
- return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph };
4911
+ return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph, on, off };
4700
4912
  }
4701
4913
  // Annotate the CommonJS export names for ESM import in node:
4702
4914
  0 && (module.exports = {
package/dist/index.d.cts CHANGED
@@ -138,6 +138,14 @@ 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: 'nodeDeselect', handler: (node: GraphNode, element: SVGCircleElement) => void): () => void;
143
+ on(event: 'linkSelect', handler: (link: GraphLink, element: SVGLineElement) => void): () => void;
144
+ on(event: 'linkDeselect', handler: (link: GraphLink, element: SVGLineElement) => void): () => void;
145
+ off(event: 'nodeSelect', handler: (node: GraphNode, element: SVGCircleElement) => void): void;
146
+ off(event: 'nodeDeselect', handler: (node: GraphNode, element: SVGCircleElement) => void): void;
147
+ off(event: 'linkSelect', handler: (link: GraphLink, element: SVGLineElement) => void): void;
148
+ off(event: 'linkDeselect', handler: (link: GraphLink, element: SVGLineElement) => void): void;
141
149
  }
142
150
 
143
151
  declare function createGraph(config: GraphConfig): GraphInstance;
package/dist/index.d.ts CHANGED
@@ -138,6 +138,14 @@ 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: 'nodeDeselect', handler: (node: GraphNode, element: SVGCircleElement) => void): () => void;
143
+ on(event: 'linkSelect', handler: (link: GraphLink, element: SVGLineElement) => void): () => void;
144
+ on(event: 'linkDeselect', handler: (link: GraphLink, element: SVGLineElement) => void): () => void;
145
+ off(event: 'nodeSelect', handler: (node: GraphNode, element: SVGCircleElement) => void): void;
146
+ off(event: 'nodeDeselect', handler: (node: GraphNode, element: SVGCircleElement) => void): void;
147
+ off(event: 'linkSelect', handler: (link: GraphLink, element: SVGLineElement) => void): void;
148
+ off(event: 'linkDeselect', handler: (link: GraphLink, element: SVGLineElement) => void): void;
141
149
  }
142
150
 
143
151
  declare function createGraph(config: GraphConfig): GraphInstance;
package/dist/index.js CHANGED
@@ -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
 
@@ -4486,6 +4488,60 @@ async function captureAndDownloadGraph(container, options = {}) {
4486
4488
  }
4487
4489
  }
4488
4490
 
4491
+ // src/utils/node-link-selection.utils.ts
4492
+ function deselectNode(nodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers) {
4493
+ nodeElement.style.fill = "";
4494
+ nodeElement.style.stroke = "";
4495
+ nodeElement.style.strokeWidth = "";
4496
+ nodeElement.style.opacity = "";
4497
+ nodeElement.style.removeProperty("r");
4498
+ root2.selectAll(".link-label.label-selection-pinned").classed("label-selection-pinned", false).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4499
+ const nodeData = select_default2(nodeElement).datum();
4500
+ nodeSelectHandlers.clear();
4501
+ nodeDeselectHandlers.forEach((handler) => handler(nodeData, nodeElement));
4502
+ }
4503
+ function deselectLink(linkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers) {
4504
+ linkElement.style.stroke = "";
4505
+ linkElement.style.strokeWidth = "";
4506
+ linkElement.style.opacity = "";
4507
+ const originalMarkerEnd = linkMarkerSnapshots.get(linkElement);
4508
+ if (originalMarkerEnd) {
4509
+ linkElement.setAttribute("marker-end", originalMarkerEnd);
4510
+ } else {
4511
+ linkElement.removeAttribute("marker-end");
4512
+ }
4513
+ const linkData = select_default2(linkElement).datum().link;
4514
+ linkSelectHandlers.clear();
4515
+ linkDeselectHandlers.forEach((handler) => handler(linkData, linkElement));
4516
+ }
4517
+ function selectLink(event, renderableLink, linkElement, selectionConfig, layers, linkSelectHandlers) {
4518
+ event.stopPropagation();
4519
+ const linkStyle = selectionConfig.linkStyle;
4520
+ if (linkStyle) {
4521
+ if (linkStyle.stroke !== void 0) {
4522
+ linkElement.style.stroke = linkStyle.stroke;
4523
+ }
4524
+ if (linkStyle.strokeWidth !== void 0) {
4525
+ linkElement.style.strokeWidth = String(linkStyle.strokeWidth);
4526
+ }
4527
+ if (linkStyle.opacity !== void 0) {
4528
+ linkElement.style.opacity = String(linkStyle.opacity);
4529
+ }
4530
+ if (linkStyle.stroke !== void 0 && renderableLink.style.arrow.enabled) {
4531
+ const selectionMarkerStyle = {
4532
+ stroke: linkStyle.stroke,
4533
+ arrow: { fill: linkStyle.stroke, size: renderableLink.style.arrow.size }
4534
+ };
4535
+ const selectionMarkerId = createArrowMarker({ svg: layers.svg, style: selectionMarkerStyle });
4536
+ select_default2(linkElement).attr("marker-end", `url(#${selectionMarkerId})`);
4537
+ }
4538
+ }
4539
+ linkSelectHandlers.forEach((handler) => handler(renderableLink.link, linkElement));
4540
+ }
4541
+ function createLinkHitArea(root2, linkSelection) {
4542
+ return 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);
4543
+ }
4544
+
4489
4545
  // src/create-graph.ts
4490
4546
  function createGraph(config) {
4491
4547
  let cleanupResize = null;
@@ -4499,6 +4555,45 @@ function createGraph(config) {
4499
4555
  let svgElement = null;
4500
4556
  let zoomBehavior = null;
4501
4557
  let simulation = null;
4558
+ const nodeSelectHandlers = /* @__PURE__ */ new Set();
4559
+ const nodeDeselectHandlers = /* @__PURE__ */ new Set();
4560
+ const linkSelectHandlers = /* @__PURE__ */ new Set();
4561
+ const linkDeselectHandlers = /* @__PURE__ */ new Set();
4562
+ function on(event, handler) {
4563
+ if (event === "nodeSelect") {
4564
+ nodeSelectHandlers.add(handler);
4565
+ return () => {
4566
+ nodeSelectHandlers.delete(handler);
4567
+ };
4568
+ }
4569
+ if (event === "nodeDeselect") {
4570
+ nodeDeselectHandlers.add(handler);
4571
+ return () => {
4572
+ nodeDeselectHandlers.delete(handler);
4573
+ };
4574
+ }
4575
+ if (event === "linkSelect") {
4576
+ linkSelectHandlers.add(handler);
4577
+ return () => {
4578
+ linkSelectHandlers.delete(handler);
4579
+ };
4580
+ }
4581
+ linkDeselectHandlers.add(handler);
4582
+ return () => {
4583
+ linkDeselectHandlers.delete(handler);
4584
+ };
4585
+ }
4586
+ function off(event, handler) {
4587
+ if (event === "nodeSelect") {
4588
+ nodeSelectHandlers.delete(handler);
4589
+ } else if (event === "nodeDeselect") {
4590
+ nodeDeselectHandlers.delete(handler);
4591
+ } else if (event === "linkSelect") {
4592
+ linkSelectHandlers.delete(handler);
4593
+ } else {
4594
+ linkDeselectHandlers.delete(handler);
4595
+ }
4596
+ }
4502
4597
  function render() {
4503
4598
  destroy();
4504
4599
  const layers = createGraphLayers(config.container);
@@ -4582,10 +4677,117 @@ function createGraph(config) {
4582
4677
  if (config.interaction?.drag?.enabled !== false) {
4583
4678
  nodeSelection.call(createDragBehavior(simulation));
4584
4679
  }
4680
+ const selectionConfig = config.interaction?.selection;
4681
+ if (selectionConfig?.enabled) {
4682
+ let selectedNodeElement = null;
4683
+ let selectedLinkElement = null;
4684
+ const linkMarkerSnapshots = /* @__PURE__ */ new Map();
4685
+ linkSelection.each(function() {
4686
+ const linkElement = this;
4687
+ linkMarkerSnapshots.set(linkElement, linkElement.getAttribute("marker-end"));
4688
+ });
4689
+ nodeSelection.on("click.select", function(event, node) {
4690
+ event.stopPropagation();
4691
+ const nodeElement = this;
4692
+ if (selectedNodeElement === nodeElement) {
4693
+ deselectNode(nodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4694
+ selectedNodeElement = null;
4695
+ return;
4696
+ }
4697
+ if (selectedNodeElement) {
4698
+ deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4699
+ selectedNodeElement = null;
4700
+ }
4701
+ if (selectedLinkElement) {
4702
+ deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4703
+ selectedLinkElement = null;
4704
+ }
4705
+ selectedNodeElement = nodeElement;
4706
+ const nodeStyle = selectionConfig.nodeStyle;
4707
+ if (nodeStyle) {
4708
+ if (nodeStyle.fill !== void 0) {
4709
+ nodeElement.style.fill = nodeStyle.fill;
4710
+ }
4711
+ if (nodeStyle.stroke !== void 0) {
4712
+ nodeElement.style.stroke = nodeStyle.stroke;
4713
+ }
4714
+ if (nodeStyle.strokeWidth !== void 0) {
4715
+ nodeElement.style.strokeWidth = String(nodeStyle.strokeWidth);
4716
+ }
4717
+ if (nodeStyle.opacity !== void 0) {
4718
+ nodeElement.style.opacity = String(nodeStyle.opacity);
4719
+ }
4720
+ if (nodeStyle.radius !== void 0) {
4721
+ nodeElement.style.setProperty("r", String(nodeStyle.radius));
4722
+ }
4723
+ }
4724
+ root2.selectAll(".link-label").filter((item) => {
4725
+ if (item.style.label.visibility !== "hover") {
4726
+ return false;
4727
+ }
4728
+ const source = item.link.source;
4729
+ const target = item.link.target;
4730
+ return source.id === node.id || target.id === node.id;
4731
+ }).classed("label-selection-pinned", true).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4732
+ nodeSelectHandlers.forEach((handler) => handler(node, nodeElement));
4733
+ });
4734
+ linkSelection.on("click.select", function(event, renderableLink) {
4735
+ const linkElement = this;
4736
+ if (selectedLinkElement === linkElement) {
4737
+ deselectLink(linkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4738
+ selectedLinkElement = null;
4739
+ return;
4740
+ }
4741
+ if (selectedLinkElement) {
4742
+ deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4743
+ selectedLinkElement = null;
4744
+ }
4745
+ if (selectedNodeElement) {
4746
+ deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4747
+ selectedNodeElement = null;
4748
+ }
4749
+ selectedLinkElement = linkElement;
4750
+ selectLink(event, renderableLink, linkElement, selectionConfig, layers, linkSelectHandlers);
4751
+ });
4752
+ const linkHitAreaSelection = createLinkHitArea(root2, linkSelection);
4753
+ simulation.on("tick.hitarea", () => {
4754
+ 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);
4755
+ });
4756
+ linkHitAreaSelection.on("click.select", function(event, renderableLink) {
4757
+ const visibleLinkNode = linkSelection.filter((d) => d === renderableLink).node();
4758
+ if (visibleLinkNode) {
4759
+ if (selectedLinkElement === visibleLinkNode) {
4760
+ deselectLink(visibleLinkNode, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4761
+ selectedLinkElement = null;
4762
+ return;
4763
+ }
4764
+ if (selectedLinkElement) {
4765
+ deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4766
+ selectedLinkElement = null;
4767
+ }
4768
+ if (selectedNodeElement) {
4769
+ deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4770
+ selectedNodeElement = null;
4771
+ }
4772
+ selectedLinkElement = visibleLinkNode;
4773
+ selectLink(event, renderableLink, visibleLinkNode, selectionConfig, layers, linkSelectHandlers);
4774
+ }
4775
+ });
4776
+ select_default2(layers.svg).on("click.deselect", () => {
4777
+ if (selectedNodeElement) {
4778
+ deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4779
+ selectedNodeElement = null;
4780
+ }
4781
+ if (selectedLinkElement) {
4782
+ deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4783
+ selectedLinkElement = null;
4784
+ }
4785
+ });
4786
+ }
4585
4787
  if (config.controls?.enabled) {
4586
4788
  controls = createGraphControls(
4587
4789
  layers.overlay,
4588
- { zoomIn, zoomOut, resetView, fitView, destroy, render, exportGraph },
4790
+ { zoomIn, zoomOut, resetView, fitView },
4589
4791
  config.controls
4590
4792
  );
4591
4793
  controls.mount();
@@ -4595,13 +4797,19 @@ function createGraph(config) {
4595
4797
  }
4596
4798
  }
4597
4799
  function resetView() {
4598
- if (!zoomBehavior || !svgElement) return;
4800
+ if (!zoomBehavior || !svgElement) {
4801
+ return;
4802
+ }
4599
4803
  select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, identity2);
4600
4804
  }
4601
4805
  function fitView() {
4602
- if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
4806
+ if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) {
4807
+ return;
4808
+ }
4603
4809
  const bounds = rootGroup.getBBox();
4604
- if (bounds.width === 0 || bounds.height === 0) return;
4810
+ if (bounds.width === 0 || bounds.height === 0) {
4811
+ return;
4812
+ }
4605
4813
  const scale = Math.min(dimensions.width / bounds.width, dimensions.height / bounds.height) * 0.9;
4606
4814
  const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4607
4815
  const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
@@ -4609,11 +4817,15 @@ function createGraph(config) {
4609
4817
  select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, transform2);
4610
4818
  }
4611
4819
  function zoomIn() {
4612
- if (!zoomBehavior || !svgElement) return;
4820
+ if (!zoomBehavior || !svgElement) {
4821
+ return;
4822
+ }
4613
4823
  select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4614
4824
  }
4615
4825
  function zoomOut() {
4616
- if (!zoomBehavior || !svgElement) return;
4826
+ if (!zoomBehavior || !svgElement) {
4827
+ return;
4828
+ }
4617
4829
  select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 0.8);
4618
4830
  }
4619
4831
  async function exportGraph(fileName) {
@@ -4660,7 +4872,7 @@ function createGraph(config) {
4660
4872
  config.container.removeChild(config.container.firstChild);
4661
4873
  }
4662
4874
  }
4663
- return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph };
4875
+ return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph, on, off };
4664
4876
  }
4665
4877
  export {
4666
4878
  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.7",
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",