polly-graph 0.1.2 → 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
 
@@ -3809,6 +3822,171 @@ function createGraphSimulation(config) {
3809
3822
  return { simulation };
3810
3823
  }
3811
3824
 
3825
+ // src/controls/graph-controls.utils.ts
3826
+ function resolveControlsPosition(position) {
3827
+ return position ?? "bottom-left";
3828
+ }
3829
+ function resolveControlsOrientation(orientation) {
3830
+ return orientation ?? "vertical";
3831
+ }
3832
+ function shouldRenderControl(config, key) {
3833
+ const show = config.show;
3834
+ if (!show) {
3835
+ return true;
3836
+ }
3837
+ const value = show[key];
3838
+ if (value === void 0) {
3839
+ return true;
3840
+ }
3841
+ return value;
3842
+ }
3843
+
3844
+ // src/assets/plus.svg?raw
3845
+ var plus_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="M5 12h14m-7-7v14" />\n</svg>';
3846
+
3847
+ // src/assets/minus.svg?raw
3848
+ var minus_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="M19 12H5" />\n</svg>';
3849
+
3850
+ // src/assets/fit.svg?raw
3851
+ var fit_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="M5 9V5H9" />\n <path d="M19 9V5H15" />\n <path d="M5 15V19H9" />\n <path d="M19 15V19H15" />\n</svg>';
3852
+
3853
+ // src/assets/reset.svg?raw
3854
+ var reset_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="M20 12a8 8 0 1 1-2.3-5.7" />\n <path d="M20 4.5v4h-4" />\n</svg>';
3855
+
3856
+ // src/controls/graph-controls.icons.ts
3857
+ var ICON_MAP = {
3858
+ "zoom-in": plus_default,
3859
+ "zoom-out": minus_default,
3860
+ fit: fit_default,
3861
+ reset: reset_default
3862
+ };
3863
+ function getControlIcon(icon) {
3864
+ const raw = ICON_MAP[icon];
3865
+ if (!raw) {
3866
+ throw new Error(`Icon not found: ${icon}`);
3867
+ }
3868
+ return raw.replace("<svg", '<svg class="pg-icon"');
3869
+ }
3870
+
3871
+ // src/controls/create-graph-controls.ts
3872
+ function createGraphControls(overlay, graph, config) {
3873
+ let root2 = null;
3874
+ function mount() {
3875
+ if (!config.enabled) {
3876
+ return;
3877
+ }
3878
+ root2 = document.createElement("div");
3879
+ root2.className = "pg-controls";
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
+ }
3888
+ appendControls(root2, config, graph);
3889
+ overlay.appendChild(root2);
3890
+ }
3891
+ function appendControls(root3, config2, graph2) {
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
+ });
3903
+ }
3904
+ function createButton(type, label, onClick) {
3905
+ const button = document.createElement("button");
3906
+ button.className = "pg-control-btn";
3907
+ button.type = "button";
3908
+ button.setAttribute("aria-label", label);
3909
+ const wrapper = document.createElement("div");
3910
+ wrapper.className = "pg-icon-wrapper";
3911
+ wrapper.innerHTML = getControlIcon(type);
3912
+ const svg = wrapper.querySelector("svg");
3913
+ if (svg) {
3914
+ svg.classList.add("pg-icon");
3915
+ button.appendChild(svg);
3916
+ }
3917
+ button.addEventListener("click", onClick);
3918
+ return button;
3919
+ }
3920
+ function destroy() {
3921
+ if (!root2) {
3922
+ return;
3923
+ }
3924
+ if (root2.parentNode === overlay) {
3925
+ overlay.removeChild(root2);
3926
+ }
3927
+ root2 = null;
3928
+ }
3929
+ return { mount, destroy };
3930
+ }
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}`);
3941
+ }
3942
+ return raw.replace("<svg", '<svg class="pg-icon"');
3943
+ }
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
+ };
3988
+ }
3989
+
3812
3990
  // src/utils/resolve-link-style.ts
3813
3991
  var DEFAULT_LINK_STYLE = {
3814
3992
  stroke: "#94a3b8",
@@ -4039,7 +4217,7 @@ function getLinkKey2(link) {
4039
4217
  }
4040
4218
  function renderLinkLabels(params, links) {
4041
4219
  const renderableLinks = createRenderableLinks2(params, links);
4042
- 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");
4043
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);
4044
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 ?? "");
4045
4223
  return labelSelection;
@@ -4324,10 +4502,11 @@ function observeResize(element, onResize) {
4324
4502
  if (!entry) {
4325
4503
  return;
4326
4504
  }
4327
- onResize(
4328
- entry.contentRect.width,
4329
- entry.contentRect.height
4330
- );
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
+ }
4331
4510
  }
4332
4511
  );
4333
4512
  observer.observe(element);
@@ -4360,53 +4539,85 @@ function getLinkTargetPoint(link) {
4360
4539
  };
4361
4540
  }
4362
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
+
4363
4585
  // src/create-graph.ts
4364
4586
  function createGraph(config) {
4365
4587
  let cleanupResize = null;
4366
4588
  let cleanupZoom = null;
4367
4589
  let tooltipBinding = null;
4590
+ let controls = null;
4591
+ let legendCleanup = null;
4368
4592
  let dimensions = { width: 0, height: 0 };
4369
4593
  let rootGroup = null;
4594
+ let svgElement = null;
4370
4595
  let zoomBehavior = null;
4371
4596
  let simulation = null;
4372
4597
  function render() {
4373
4598
  destroy();
4374
4599
  const layers = createGraphLayers(config.container);
4600
+ svgElement = layers.svg;
4375
4601
  rootGroup = layers.root;
4376
- cleanupResize = observeResize(
4377
- config.container,
4378
- (width, height) => {
4379
- dimensions = { width, height };
4380
- layers.interactionRect.setAttribute("width", String(width));
4381
- layers.interactionRect.setAttribute("height", String(height));
4382
- simulation?.force("x", x_default2(width / 2).strength(0.03));
4383
- simulation?.force("y", y_default2(height / 2).strength(0.03));
4384
- simulation?.alpha(0.25).restart();
4385
- }
4386
- );
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
+ });
4387
4612
  const zoomResult = createZoom({
4388
- /**
4389
- * D3 zoom must be attached to SVG
4390
- * because it requires:
4391
- *
4392
- * width.baseVal
4393
- * height.baseVal
4394
- */
4395
- svg: config.container,
4396
- /**
4397
- * Used for pointer semantics /
4398
- * pan filtering only
4399
- */
4613
+ svg: layers.svg,
4400
4614
  interactionLayer: layers.interactionLayer,
4401
- /**
4402
- * Actual graph transform target
4403
- */
4404
4615
  root: layers.root
4405
4616
  });
4406
4617
  zoomBehavior = zoomResult.behavior;
4407
4618
  cleanupZoom = zoomResult.cleanup;
4408
4619
  const root2 = select_default2(layers.root);
4409
- const renderContext = { svg: config.container, root: root2, interaction: config.interaction };
4620
+ const renderContext = { svg: layers.svg, root: root2, interaction: config.interaction };
4410
4621
  const linkSelection = renderLinks(renderContext, config.links);
4411
4622
  const linkLabelSelection = renderLinkLabels(renderContext, config.links);
4412
4623
  const nodeSelection = renderNodes(renderContext, config.nodes);
@@ -4419,40 +4630,35 @@ function createGraph(config) {
4419
4630
  };
4420
4631
  const simulationResult = createGraphSimulation(simulationConfig);
4421
4632
  simulation = simulationResult.simulation;
4422
- simulation.on(
4423
- "tick",
4424
- () => {
4425
- 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);
4426
- linkLabelSelection.attr("transform", (item) => {
4427
- const link = item.link;
4428
- const source = link.source;
4429
- const targetPoint = getLinkTargetPoint(link);
4430
- const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
4431
- const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
4432
- return `translate(${x3}, ${y3})`;
4433
- }).each(function() {
4434
- const group = this;
4435
- const text = group.querySelector("text");
4436
- const rect = group.querySelector("rect");
4437
- if (!text || !rect) {
4438
- return;
4439
- }
4440
- const bBox = text.getBBox();
4441
- const padding = 6;
4442
- rect.setAttribute("x", String(bBox.x - padding));
4443
- rect.setAttribute("y", String(bBox.y - padding));
4444
- rect.setAttribute("width", String(bBox.width + padding * 2));
4445
- rect.setAttribute("height", String(bBox.height + padding * 2));
4446
- });
4447
- nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
4448
- labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
4449
- tooltipBinding?.reposition();
4450
- }
4451
- );
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
+ });
4452
4658
  if (config.interaction?.hover?.enabled) {
4453
4659
  if (config.interaction?.hover?.tooltip?.enabled) {
4454
4660
  tooltipBinding = bindNodeTooltip({
4455
- container: config.container.parentElement,
4661
+ container: config.container,
4456
4662
  selection: nodeSelection,
4457
4663
  tooltipConfig: config.interaction.hover.tooltip
4458
4664
  });
@@ -4462,60 +4668,47 @@ function createGraph(config) {
4462
4668
  if (config.interaction?.drag?.enabled !== false) {
4463
4669
  nodeSelection.call(createDragBehavior(simulation));
4464
4670
  }
4465
- if (config.interaction?.selection?.enabled) {
4671
+ if (config.controls?.enabled) {
4672
+ controls = createGraphControls(
4673
+ layers.overlay,
4674
+ { zoomIn, zoomOut, resetView, fitView, destroy, render, exportGraph },
4675
+ config.controls
4676
+ );
4677
+ controls.mount();
4678
+ }
4679
+ if (config.legend?.enabled) {
4680
+ legendCleanup = createGraphLegend(layers.overlay, config.legend);
4466
4681
  }
4467
4682
  }
4468
4683
  function resetView() {
4469
- if (!zoomBehavior) {
4470
- return;
4471
- }
4472
- select_default2(config.container).transition().call(
4473
- zoomBehavior.transform,
4474
- identity2
4475
- );
4684
+ if (!zoomBehavior || !svgElement) return;
4685
+ select_default2(svgElement).transition().call(zoomBehavior.transform, identity2);
4476
4686
  }
4477
4687
  function fitView() {
4478
- if (!zoomBehavior || !rootGroup || dimensions.width === 0 || dimensions.height === 0) {
4479
- return;
4480
- }
4688
+ if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
4481
4689
  const bounds = rootGroup.getBBox();
4482
- if (bounds.width === 0 || bounds.height === 0) {
4483
- return;
4484
- }
4485
- const width = dimensions.width;
4486
- const height = dimensions.height;
4487
- const scale = Math.min(
4488
- width / bounds.width,
4489
- height / bounds.height
4490
- ) * 0.9;
4491
- const translateX = (width - bounds.width * scale) / 2 - bounds.x * scale;
4492
- const translateY = (height - bounds.height * scale) / 2 - bounds.y * scale;
4493
- const transform2 = identity2.translate(
4494
- translateX,
4495
- translateY
4496
- ).scale(scale);
4497
- select_default2(config.container).transition().call(
4498
- zoomBehavior.transform,
4499
- transform2
4500
- );
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);
4501
4696
  }
4502
4697
  function zoomIn() {
4503
- if (!zoomBehavior) {
4504
- return;
4505
- }
4506
- select_default2(config.container).transition().call(
4507
- zoomBehavior.scaleBy,
4508
- 1.2
4509
- );
4698
+ if (!zoomBehavior || !svgElement) return;
4699
+ select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4510
4700
  }
4511
4701
  function zoomOut() {
4512
- if (!zoomBehavior) {
4513
- return;
4514
- }
4515
- select_default2(config.container).transition().call(
4516
- zoomBehavior.scaleBy,
4517
- 0.8
4518
- );
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
+ });
4519
4712
  }
4520
4713
  function destroy() {
4521
4714
  if (cleanupResize) {
@@ -4534,13 +4727,22 @@ function createGraph(config) {
4534
4727
  simulation.stop();
4535
4728
  simulation = null;
4536
4729
  }
4730
+ if (controls) {
4731
+ controls.destroy();
4732
+ controls = null;
4733
+ }
4734
+ if (legendCleanup) {
4735
+ legendCleanup();
4736
+ legendCleanup = null;
4737
+ }
4537
4738
  rootGroup = null;
4739
+ svgElement = null;
4538
4740
  zoomBehavior = null;
4539
4741
  while (config.container.firstChild) {
4540
4742
  config.container.removeChild(config.container.firstChild);
4541
4743
  }
4542
4744
  }
4543
- return { render, zoomIn, zoomOut, resetView, fitView, destroy };
4745
+ return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph };
4544
4746
  }
4545
4747
  // Annotate the CommonJS export names for ESM import in node:
4546
4748
  0 && (module.exports = {