polly-graph 0.1.3 → 0.1.4

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
@@ -3697,41 +3707,44 @@ function y_default2(y3) {
3697
3707
  }
3698
3708
 
3699
3709
  // src/core/create-graph-layers.ts
3700
- function createGraphLayers(svg) {
3701
- while (svg.firstChild) {
3702
- svg.removeChild(svg.firstChild);
3703
- }
3704
- const createGroup = (className) => {
3710
+ function createGraphLayers(host) {
3711
+ host.innerHTML = "";
3712
+ const rootContainer = document.createElement("div");
3713
+ rootContainer.className = "pg-root";
3714
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3715
+ svg.setAttribute("class", "pg-canvas");
3716
+ const overlay = document.createElement("div");
3717
+ overlay.className = "pg-overlay";
3718
+ rootContainer.appendChild(svg);
3719
+ rootContainer.appendChild(overlay);
3720
+ host.appendChild(rootContainer);
3721
+ const createGroup = (layerName) => {
3705
3722
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
3706
- group.setAttribute("class", className);
3707
- group.setAttribute("data-layer", className);
3723
+ group.setAttribute("class", `pg-layer-${layerName}`);
3724
+ group.setAttribute("data-layer", layerName);
3708
3725
  return group;
3709
3726
  };
3710
3727
  const interactionLayer = createGroup("interaction-layer");
3711
3728
  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
- });
3729
+ interactionRect.setAttribute("class", "pg-interaction-surface");
3730
+ interactionRect.setAttribute("fill", "transparent");
3731
+ interactionRect.setAttribute("pointer-events", "all");
3722
3732
  interactionLayer.appendChild(interactionRect);
3723
- const root2 = createGroup("knowledge-graph-root");
3733
+ const graphRoot = createGroup("viewport");
3724
3734
  const layers = {
3735
+ svg,
3736
+ overlay,
3725
3737
  interactionLayer,
3726
3738
  interactionRect,
3727
- root: root2,
3739
+ root: graphRoot,
3740
+ // These keys now match your ctx.root.select('[data-layer="..."]') calls
3728
3741
  links: createGroup("links"),
3729
3742
  linkLabels: createGroup("link-labels"),
3730
3743
  nodeRings: createGroup("node-rings"),
3731
3744
  nodes: createGroup("nodes"),
3732
3745
  nodeLabels: createGroup("node-labels")
3733
3746
  };
3734
- root2.append(
3747
+ graphRoot.append(
3735
3748
  layers.links,
3736
3749
  layers.linkLabels,
3737
3750
  layers.nodeRings,
@@ -3739,7 +3752,7 @@ function createGraphLayers(svg) {
3739
3752
  layers.nodeLabels
3740
3753
  );
3741
3754
  svg.appendChild(interactionLayer);
3742
- svg.appendChild(root2);
3755
+ svg.appendChild(graphRoot);
3743
3756
  return layers;
3744
3757
  }
3745
3758
 
@@ -3856,49 +3869,51 @@ function getControlIcon(icon) {
3856
3869
  }
3857
3870
 
3858
3871
  // src/controls/create-graph-controls.ts
3859
- function createGraphControls(container, graph, config) {
3872
+ function createGraphControls(overlay, graph, config) {
3860
3873
  let root2 = null;
3861
3874
  function mount() {
3862
3875
  if (!config.enabled) {
3863
3876
  return;
3864
3877
  }
3865
- const parent = container.parentElement;
3866
- if (!parent) {
3867
- return;
3868
- }
3869
3878
  root2 = document.createElement("div");
3870
3879
  root2.className = "pg-controls";
3871
- applyPosition(root2, config);
3872
- applyOrientation(root2, config);
3880
+ const position = resolveControlsPosition(config.position);
3881
+ root2.classList.add(`pg-pos-${position}`);
3882
+ const orientation = resolveControlsOrientation(config.orientation);
3883
+ root2.classList.add(`pg-orient-${orientation}`);
3884
+ if (config.offset) {
3885
+ root2.style.setProperty("--pg-controls-offset-x", `${config.offset.x}px`);
3886
+ root2.style.setProperty("--pg-controls-offset-y", `${config.offset.y}px`);
3887
+ }
3873
3888
  appendControls(root2, config, graph);
3874
- parent.appendChild(root2);
3889
+ overlay.appendChild(root2);
3875
3890
  }
3876
3891
  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
- }
3892
+ const actions = [
3893
+ { key: "zoomIn", icon: "zoom-in", label: "Zoom in", fn: graph2.zoomIn.bind(graph2) },
3894
+ { key: "zoomOut", icon: "zoom-out", label: "Zoom out", fn: graph2.zoomOut.bind(graph2) },
3895
+ { key: "fit", icon: "fit", label: "Fit view", fn: graph2.fitView.bind(graph2) },
3896
+ { key: "reset", icon: "reset", label: "Reset view", fn: graph2.resetView.bind(graph2) }
3897
+ ];
3898
+ actions.forEach((action) => {
3899
+ if (shouldRenderControl(config2, action.key)) {
3900
+ root3.appendChild(createButton(action.icon, action.label, action.fn));
3901
+ }
3902
+ });
3889
3903
  }
3890
3904
  function createButton(type, label, onClick) {
3891
3905
  const button = document.createElement("button");
3906
+ button.className = "pg-control-btn";
3892
3907
  button.type = "button";
3893
3908
  button.setAttribute("aria-label", label);
3894
3909
  const wrapper = document.createElement("div");
3910
+ wrapper.className = "pg-icon-wrapper";
3895
3911
  wrapper.innerHTML = getControlIcon(type);
3896
3912
  const svg = wrapper.querySelector("svg");
3897
- if (!svg) {
3898
- throw new Error(`Invalid SVG for icon: ${type}`);
3913
+ if (svg) {
3914
+ svg.classList.add("pg-icon");
3915
+ button.appendChild(svg);
3899
3916
  }
3900
- svg.classList.add("pg-icon");
3901
- button.appendChild(svg);
3902
3917
  button.addEventListener("click", onClick);
3903
3918
  return button;
3904
3919
  }
@@ -3906,39 +3921,70 @@ function createGraphControls(container, graph, config) {
3906
3921
  if (!root2) {
3907
3922
  return;
3908
3923
  }
3909
- const clone = root2.cloneNode(true);
3910
- root2.replaceWith(clone);
3924
+ if (root2.parentNode === overlay) {
3925
+ overlay.removeChild(root2);
3926
+ }
3911
3927
  root2 = null;
3912
3928
  }
3913
3929
  return { mount, destroy };
3914
3930
  }
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;
3931
+
3932
+ // src/assets/caret.svg?raw
3933
+ 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>';
3934
+
3935
+ // src/legends/graph-legend-icon.ts
3936
+ var LEGEND_ICON_MAP = { caret: caret_default };
3937
+ function getLegendIcon(icon) {
3938
+ const raw = LEGEND_ICON_MAP[icon];
3939
+ if (!raw) {
3940
+ throw new Error(`Legend icon not found: ${icon}`);
3936
3941
  }
3942
+ return raw.replace("<svg", '<svg class="pg-icon"');
3937
3943
  }
3938
- function applyOrientation(el, config) {
3939
- const orientation = resolveControlsOrientation(config.orientation);
3940
- el.style.display = "flex";
3941
- el.style.flexDirection = orientation === "vertical" ? "column" : "row";
3944
+
3945
+ // src/legends/create-graph-legends.ts
3946
+ function createGraphLegend(overlay, config) {
3947
+ const legendWrapper = document.createElement("div");
3948
+ legendWrapper.className = "pg-legend";
3949
+ const position = config.position || "bottom-right";
3950
+ legendWrapper.classList.add(`pg-pos-${position}`);
3951
+ if (config.defaultExpanded === false) {
3952
+ legendWrapper.classList.add("pg-is-collapsed");
3953
+ }
3954
+ if (config.collapsible) {
3955
+ const toggleBtn = document.createElement("button");
3956
+ toggleBtn.className = "pg-legend-toggle";
3957
+ toggleBtn.type = "button";
3958
+ toggleBtn.innerHTML = getLegendIcon("caret");
3959
+ toggleBtn.onclick = (e) => {
3960
+ e.stopPropagation();
3961
+ legendWrapper.classList.toggle("pg-is-collapsed");
3962
+ };
3963
+ legendWrapper.appendChild(toggleBtn);
3964
+ }
3965
+ const body = document.createElement("div");
3966
+ body.className = "pg-legend-body";
3967
+ const list = document.createElement("ul");
3968
+ list.className = "pg-legend-list";
3969
+ config.items.forEach((item) => {
3970
+ const listItem = document.createElement("li");
3971
+ listItem.className = "pg-legend-item";
3972
+ const swatch = document.createElement("span");
3973
+ swatch.className = `pg-legend-swatch is-${item.shape || "circle"}`;
3974
+ swatch.style.backgroundColor = item.color;
3975
+ const label = document.createElement("span");
3976
+ label.className = "pg-legend-label";
3977
+ label.innerText = item.label;
3978
+ listItem.appendChild(swatch);
3979
+ listItem.appendChild(label);
3980
+ list.appendChild(listItem);
3981
+ });
3982
+ body.appendChild(list);
3983
+ legendWrapper.appendChild(body);
3984
+ overlay.appendChild(legendWrapper);
3985
+ return () => {
3986
+ if (legendWrapper.parentNode === overlay) overlay.removeChild(legendWrapper);
3987
+ };
3942
3988
  }
3943
3989
 
3944
3990
  // src/utils/resolve-link-style.ts
@@ -4171,7 +4217,7 @@ function getLinkKey2(link) {
4171
4217
  }
4172
4218
  function renderLinkLabels(params, links) {
4173
4219
  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");
4220
+ const labelSelection = params.root.select('[data-layer="link-labels"]').selectAll(".link-label").data(renderableLinks, (item) => getLinkKey2(item.link)).join("g").attr("class", "link-label").attr("pointer-events", "auto").attr("cursor", "pointer");
4175
4221
  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
4222
  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
4223
  return labelSelection;
@@ -4456,10 +4502,11 @@ function observeResize(element, onResize) {
4456
4502
  if (!entry) {
4457
4503
  return;
4458
4504
  }
4459
- onResize(
4460
- entry.contentRect.width,
4461
- entry.contentRect.height
4462
- );
4505
+ const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
4506
+ const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
4507
+ if (width > 0 && height > 0) {
4508
+ onResize(width, height);
4509
+ }
4463
4510
  }
4464
4511
  );
4465
4512
  observer.observe(element);
@@ -4492,54 +4539,85 @@ function getLinkTargetPoint(link) {
4492
4539
  };
4493
4540
  }
4494
4541
 
4542
+ // src/utils/export-graph.ts
4543
+ var import_html2canvas = __toESM(require("html2canvas"), 1);
4544
+ async function captureAndDownloadGraph(container, options = {}) {
4545
+ const {
4546
+ fileName = `graph-export-${Date.now()}.png`,
4547
+ backgroundColor = "#ffffff",
4548
+ pixelRatio = 2
4549
+ } = options;
4550
+ const root2 = container.querySelector(".pg-root");
4551
+ if (!root2) return;
4552
+ const controls = root2.querySelector(".pg-controls");
4553
+ const legendToggle = root2.querySelector(".pg-legend-toggle");
4554
+ const interactionLayer = root2.querySelector(".pg-interaction-layer");
4555
+ const legend = root2.querySelector(".pg-legend");
4556
+ const wasCollapsed = legend?.classList.contains("pg-is-collapsed");
4557
+ if (controls) controls.style.display = "none";
4558
+ if (legendToggle) legendToggle.style.display = "none";
4559
+ if (interactionLayer) interactionLayer.style.display = "none";
4560
+ if (legend && wasCollapsed) {
4561
+ legend.classList.remove("pg-is-collapsed");
4562
+ }
4563
+ try {
4564
+ const canvas = await (0, import_html2canvas.default)(root2, {
4565
+ scale: pixelRatio,
4566
+ backgroundColor,
4567
+ useCORS: true,
4568
+ logging: false
4569
+ });
4570
+ const dataUrl = canvas.toDataURL("image/png");
4571
+ const link = document.createElement("a");
4572
+ link.download = fileName;
4573
+ link.href = dataUrl;
4574
+ link.click();
4575
+ } finally {
4576
+ if (controls) controls.style.display = "flex";
4577
+ if (legendToggle) legendToggle.style.display = "flex";
4578
+ if (interactionLayer) interactionLayer.style.display = "block";
4579
+ if (legend && wasCollapsed) {
4580
+ legend.classList.add("pg-is-collapsed");
4581
+ }
4582
+ }
4583
+ }
4584
+
4495
4585
  // src/create-graph.ts
4496
4586
  function createGraph(config) {
4497
4587
  let cleanupResize = null;
4498
4588
  let cleanupZoom = null;
4499
4589
  let tooltipBinding = null;
4500
4590
  let controls = null;
4591
+ let legendCleanup = null;
4501
4592
  let dimensions = { width: 0, height: 0 };
4502
4593
  let rootGroup = null;
4594
+ let svgElement = null;
4503
4595
  let zoomBehavior = null;
4504
4596
  let simulation = null;
4505
4597
  function render() {
4506
4598
  destroy();
4507
4599
  const layers = createGraphLayers(config.container);
4600
+ svgElement = layers.svg;
4508
4601
  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();
4518
- }
4519
- );
4602
+ cleanupResize = observeResize(config.container, (width, height) => {
4603
+ dimensions = { width, height };
4604
+ layers.svg.setAttribute("width", String(width));
4605
+ layers.svg.setAttribute("height", String(height));
4606
+ layers.interactionRect.setAttribute("width", String(width));
4607
+ layers.interactionRect.setAttribute("height", String(height));
4608
+ simulation?.force("x", x_default2(width / 2).strength(0.03));
4609
+ simulation?.force("y", y_default2(height / 2).strength(0.03));
4610
+ simulation?.alpha(0.25).restart();
4611
+ });
4520
4612
  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
- */
4613
+ svg: layers.svg,
4533
4614
  interactionLayer: layers.interactionLayer,
4534
- /**
4535
- * Actual graph transform target
4536
- */
4537
4615
  root: layers.root
4538
4616
  });
4539
4617
  zoomBehavior = zoomResult.behavior;
4540
4618
  cleanupZoom = zoomResult.cleanup;
4541
4619
  const root2 = select_default2(layers.root);
4542
- const renderContext = { svg: config.container, root: root2, interaction: config.interaction };
4620
+ const renderContext = { svg: layers.svg, root: root2, interaction: config.interaction };
4543
4621
  const linkSelection = renderLinks(renderContext, config.links);
4544
4622
  const linkLabelSelection = renderLinkLabels(renderContext, config.links);
4545
4623
  const nodeSelection = renderNodes(renderContext, config.nodes);
@@ -4552,40 +4630,35 @@ function createGraph(config) {
4552
4630
  };
4553
4631
  const simulationResult = createGraphSimulation(simulationConfig);
4554
4632
  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
- );
4633
+ simulation.on("tick", () => {
4634
+ 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);
4635
+ linkLabelSelection.attr("transform", (item) => {
4636
+ const link = item.link;
4637
+ const source = link.source;
4638
+ const targetPoint = getLinkTargetPoint(link);
4639
+ const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
4640
+ const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
4641
+ return `translate(${x3}, ${y3})`;
4642
+ }).each(function() {
4643
+ const group = this;
4644
+ const text = group.querySelector("text");
4645
+ const rect = group.querySelector("rect");
4646
+ if (!text || !rect) return;
4647
+ const bBox = text.getBBox();
4648
+ const padding = 6;
4649
+ rect.setAttribute("x", String(bBox.x - padding));
4650
+ rect.setAttribute("y", String(bBox.y - padding));
4651
+ rect.setAttribute("width", String(bBox.width + padding * 2));
4652
+ rect.setAttribute("height", String(bBox.height + padding * 2));
4653
+ });
4654
+ nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
4655
+ labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
4656
+ tooltipBinding?.reposition();
4657
+ });
4585
4658
  if (config.interaction?.hover?.enabled) {
4586
4659
  if (config.interaction?.hover?.tooltip?.enabled) {
4587
4660
  tooltipBinding = bindNodeTooltip({
4588
- container: config.container.parentElement,
4661
+ container: config.container,
4589
4662
  selection: nodeSelection,
4590
4663
  tooltipConfig: config.interaction.hover.tooltip
4591
4664
  });
@@ -4595,64 +4668,47 @@ function createGraph(config) {
4595
4668
  if (config.interaction?.drag?.enabled !== false) {
4596
4669
  nodeSelection.call(createDragBehavior(simulation));
4597
4670
  }
4598
- if (config.interaction?.selection?.enabled) {
4599
- }
4600
4671
  if (config.controls?.enabled) {
4601
- controls = createGraphControls(config.container, { zoomIn, zoomOut, resetView, fitView, destroy, render }, config.controls);
4672
+ controls = createGraphControls(
4673
+ layers.overlay,
4674
+ { zoomIn, zoomOut, resetView, fitView, destroy, render, exportGraph },
4675
+ config.controls
4676
+ );
4602
4677
  controls.mount();
4603
4678
  }
4679
+ if (config.legend?.enabled) {
4680
+ legendCleanup = createGraphLegend(layers.overlay, config.legend);
4681
+ }
4604
4682
  }
4605
4683
  function resetView() {
4606
- if (!zoomBehavior) {
4607
- return;
4608
- }
4609
- select_default2(config.container).transition().call(
4610
- zoomBehavior.transform,
4611
- identity2
4612
- );
4684
+ if (!zoomBehavior || !svgElement) return;
4685
+ select_default2(svgElement).transition().call(zoomBehavior.transform, identity2);
4613
4686
  }
4614
4687
  function fitView() {
4615
- if (!zoomBehavior || !rootGroup || dimensions.width === 0 || dimensions.height === 0) {
4616
- return;
4617
- }
4688
+ if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
4618
4689
  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
- );
4690
+ if (bounds.width === 0 || bounds.height === 0) return;
4691
+ const scale = Math.min(dimensions.width / bounds.width, dimensions.height / bounds.height) * 0.9;
4692
+ const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4693
+ const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
4694
+ const transform2 = identity2.translate(translateX, translateY).scale(scale);
4695
+ select_default2(svgElement).transition().call(zoomBehavior.transform, transform2);
4638
4696
  }
4639
4697
  function zoomIn() {
4640
- if (!zoomBehavior) {
4641
- return;
4642
- }
4643
- select_default2(config.container).transition().call(
4644
- zoomBehavior.scaleBy,
4645
- 1.2
4646
- );
4698
+ if (!zoomBehavior || !svgElement) return;
4699
+ select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4647
4700
  }
4648
4701
  function zoomOut() {
4649
- if (!zoomBehavior) {
4650
- return;
4651
- }
4652
- select_default2(config.container).transition().call(
4653
- zoomBehavior.scaleBy,
4654
- 0.8
4655
- );
4702
+ if (!zoomBehavior || !svgElement) return;
4703
+ select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 0.8);
4704
+ }
4705
+ async function exportGraph(fileName) {
4706
+ fitView();
4707
+ await new Promise((resolve) => setTimeout(resolve, 500));
4708
+ await captureAndDownloadGraph(config.container, {
4709
+ fileName,
4710
+ pixelRatio: 2
4711
+ });
4656
4712
  }
4657
4713
  function destroy() {
4658
4714
  if (cleanupResize) {
@@ -4675,13 +4731,18 @@ function createGraph(config) {
4675
4731
  controls.destroy();
4676
4732
  controls = null;
4677
4733
  }
4734
+ if (legendCleanup) {
4735
+ legendCleanup();
4736
+ legendCleanup = null;
4737
+ }
4678
4738
  rootGroup = null;
4739
+ svgElement = null;
4679
4740
  zoomBehavior = null;
4680
4741
  while (config.container.firstChild) {
4681
4742
  config.container.removeChild(config.container.firstChild);
4682
4743
  }
4683
4744
  }
4684
- return { render, zoomIn, zoomOut, resetView, fitView, destroy };
4745
+ return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph };
4685
4746
  }
4686
4747
  // Annotate the CommonJS export names for ESM import in node:
4687
4748
  0 && (module.exports = {