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.js CHANGED
@@ -3671,41 +3671,44 @@ function y_default2(y3) {
3671
3671
  }
3672
3672
 
3673
3673
  // src/core/create-graph-layers.ts
3674
- function createGraphLayers(svg) {
3675
- while (svg.firstChild) {
3676
- svg.removeChild(svg.firstChild);
3677
- }
3678
- const createGroup = (className) => {
3674
+ function createGraphLayers(host) {
3675
+ host.innerHTML = "";
3676
+ const rootContainer = document.createElement("div");
3677
+ rootContainer.className = "pg-root";
3678
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3679
+ svg.setAttribute("class", "pg-canvas");
3680
+ const overlay = document.createElement("div");
3681
+ overlay.className = "pg-overlay";
3682
+ rootContainer.appendChild(svg);
3683
+ rootContainer.appendChild(overlay);
3684
+ host.appendChild(rootContainer);
3685
+ const createGroup = (layerName) => {
3679
3686
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
3680
- group.setAttribute("class", className);
3681
- group.setAttribute("data-layer", className);
3687
+ group.setAttribute("class", `pg-layer-${layerName}`);
3688
+ group.setAttribute("data-layer", layerName);
3682
3689
  return group;
3683
3690
  };
3684
3691
  const interactionLayer = createGroup("interaction-layer");
3685
3692
  const interactionRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
3686
- const interactionAttributes = {
3687
- class: "interaction-surface",
3688
- width: "100%",
3689
- height: "100%",
3690
- fill: "transparent",
3691
- "pointer-events": "all"
3692
- };
3693
- Object.entries(interactionAttributes).forEach(([key, value]) => {
3694
- interactionRect.setAttribute(key, value);
3695
- });
3693
+ interactionRect.setAttribute("class", "pg-interaction-surface");
3694
+ interactionRect.setAttribute("fill", "transparent");
3695
+ interactionRect.setAttribute("pointer-events", "all");
3696
3696
  interactionLayer.appendChild(interactionRect);
3697
- const root2 = createGroup("knowledge-graph-root");
3697
+ const graphRoot = createGroup("viewport");
3698
3698
  const layers = {
3699
+ svg,
3700
+ overlay,
3699
3701
  interactionLayer,
3700
3702
  interactionRect,
3701
- root: root2,
3703
+ root: graphRoot,
3704
+ // These keys now match your ctx.root.select('[data-layer="..."]') calls
3702
3705
  links: createGroup("links"),
3703
3706
  linkLabels: createGroup("link-labels"),
3704
3707
  nodeRings: createGroup("node-rings"),
3705
3708
  nodes: createGroup("nodes"),
3706
3709
  nodeLabels: createGroup("node-labels")
3707
3710
  };
3708
- root2.append(
3711
+ graphRoot.append(
3709
3712
  layers.links,
3710
3713
  layers.linkLabels,
3711
3714
  layers.nodeRings,
@@ -3713,7 +3716,7 @@ function createGraphLayers(svg) {
3713
3716
  layers.nodeLabels
3714
3717
  );
3715
3718
  svg.appendChild(interactionLayer);
3716
- svg.appendChild(root2);
3719
+ svg.appendChild(graphRoot);
3717
3720
  return layers;
3718
3721
  }
3719
3722
 
@@ -3830,49 +3833,51 @@ function getControlIcon(icon) {
3830
3833
  }
3831
3834
 
3832
3835
  // src/controls/create-graph-controls.ts
3833
- function createGraphControls(container, graph, config) {
3836
+ function createGraphControls(overlay, graph, config) {
3834
3837
  let root2 = null;
3835
3838
  function mount() {
3836
3839
  if (!config.enabled) {
3837
3840
  return;
3838
3841
  }
3839
- const parent = container.parentElement;
3840
- if (!parent) {
3841
- return;
3842
- }
3843
3842
  root2 = document.createElement("div");
3844
3843
  root2.className = "pg-controls";
3845
- applyPosition(root2, config);
3846
- applyOrientation(root2, config);
3844
+ const position = resolveControlsPosition(config.position);
3845
+ root2.classList.add(`pg-pos-${position}`);
3846
+ const orientation = resolveControlsOrientation(config.orientation);
3847
+ root2.classList.add(`pg-orient-${orientation}`);
3848
+ if (config.offset) {
3849
+ root2.style.setProperty("--pg-controls-offset-x", `${config.offset.x}px`);
3850
+ root2.style.setProperty("--pg-controls-offset-y", `${config.offset.y}px`);
3851
+ }
3847
3852
  appendControls(root2, config, graph);
3848
- parent.appendChild(root2);
3853
+ overlay.appendChild(root2);
3849
3854
  }
3850
3855
  function appendControls(root3, config2, graph2) {
3851
- if (shouldRenderControl(config2, "zoomIn")) {
3852
- root3.appendChild(createButton("zoom-in", "Zoom in", graph2.zoomIn.bind(graph2)));
3853
- }
3854
- if (shouldRenderControl(config2, "zoomOut")) {
3855
- root3.appendChild(createButton("zoom-out", "Zoom out", graph2.zoomOut.bind(graph2)));
3856
- }
3857
- if (shouldRenderControl(config2, "fit")) {
3858
- root3.appendChild(createButton("fit", "Fit view", graph2.fitView.bind(graph2)));
3859
- }
3860
- if (shouldRenderControl(config2, "reset")) {
3861
- root3.appendChild(createButton("reset", "Reset view", graph2.resetView.bind(graph2)));
3862
- }
3856
+ const actions = [
3857
+ { key: "zoomIn", icon: "zoom-in", label: "Zoom in", fn: graph2.zoomIn.bind(graph2) },
3858
+ { key: "zoomOut", icon: "zoom-out", label: "Zoom out", fn: graph2.zoomOut.bind(graph2) },
3859
+ { key: "fit", icon: "fit", label: "Fit view", fn: graph2.fitView.bind(graph2) },
3860
+ { key: "reset", icon: "reset", label: "Reset view", fn: graph2.resetView.bind(graph2) }
3861
+ ];
3862
+ actions.forEach((action) => {
3863
+ if (shouldRenderControl(config2, action.key)) {
3864
+ root3.appendChild(createButton(action.icon, action.label, action.fn));
3865
+ }
3866
+ });
3863
3867
  }
3864
3868
  function createButton(type, label, onClick) {
3865
3869
  const button = document.createElement("button");
3870
+ button.className = "pg-control-btn";
3866
3871
  button.type = "button";
3867
3872
  button.setAttribute("aria-label", label);
3868
3873
  const wrapper = document.createElement("div");
3874
+ wrapper.className = "pg-icon-wrapper";
3869
3875
  wrapper.innerHTML = getControlIcon(type);
3870
3876
  const svg = wrapper.querySelector("svg");
3871
- if (!svg) {
3872
- throw new Error(`Invalid SVG for icon: ${type}`);
3877
+ if (svg) {
3878
+ svg.classList.add("pg-icon");
3879
+ button.appendChild(svg);
3873
3880
  }
3874
- svg.classList.add("pg-icon");
3875
- button.appendChild(svg);
3876
3881
  button.addEventListener("click", onClick);
3877
3882
  return button;
3878
3883
  }
@@ -3880,39 +3885,70 @@ function createGraphControls(container, graph, config) {
3880
3885
  if (!root2) {
3881
3886
  return;
3882
3887
  }
3883
- const clone = root2.cloneNode(true);
3884
- root2.replaceWith(clone);
3888
+ if (root2.parentNode === overlay) {
3889
+ overlay.removeChild(root2);
3890
+ }
3885
3891
  root2 = null;
3886
3892
  }
3887
3893
  return { mount, destroy };
3888
3894
  }
3889
- function applyPosition(el, config) {
3890
- const position = resolveControlsPosition(config.position);
3891
- const offset = config.offset ?? { x: 16, y: 16 };
3892
- el.style.position = "absolute";
3893
- switch (position) {
3894
- case "bottom-left":
3895
- el.style.left = `${offset.x}px`;
3896
- el.style.bottom = `${offset.y}px`;
3897
- break;
3898
- case "bottom-right":
3899
- el.style.right = `${offset.x}px`;
3900
- el.style.bottom = `${offset.y}px`;
3901
- break;
3902
- case "top-left":
3903
- el.style.left = `${offset.x}px`;
3904
- el.style.top = `${offset.y}px`;
3905
- break;
3906
- case "top-right":
3907
- el.style.right = `${offset.x}px`;
3908
- el.style.top = `${offset.y}px`;
3909
- break;
3895
+
3896
+ // src/assets/caret.svg?raw
3897
+ 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>';
3898
+
3899
+ // src/legends/graph-legend-icon.ts
3900
+ var LEGEND_ICON_MAP = { caret: caret_default };
3901
+ function getLegendIcon(icon) {
3902
+ const raw = LEGEND_ICON_MAP[icon];
3903
+ if (!raw) {
3904
+ throw new Error(`Legend icon not found: ${icon}`);
3910
3905
  }
3906
+ return raw.replace("<svg", '<svg class="pg-icon"');
3911
3907
  }
3912
- function applyOrientation(el, config) {
3913
- const orientation = resolveControlsOrientation(config.orientation);
3914
- el.style.display = "flex";
3915
- el.style.flexDirection = orientation === "vertical" ? "column" : "row";
3908
+
3909
+ // src/legends/create-graph-legends.ts
3910
+ function createGraphLegend(overlay, config) {
3911
+ const legendWrapper = document.createElement("div");
3912
+ legendWrapper.className = "pg-legend";
3913
+ const position = config.position || "bottom-right";
3914
+ legendWrapper.classList.add(`pg-pos-${position}`);
3915
+ if (config.defaultExpanded === false) {
3916
+ legendWrapper.classList.add("pg-is-collapsed");
3917
+ }
3918
+ if (config.collapsible) {
3919
+ const toggleBtn = document.createElement("button");
3920
+ toggleBtn.className = "pg-legend-toggle";
3921
+ toggleBtn.type = "button";
3922
+ toggleBtn.innerHTML = getLegendIcon("caret");
3923
+ toggleBtn.onclick = (e) => {
3924
+ e.stopPropagation();
3925
+ legendWrapper.classList.toggle("pg-is-collapsed");
3926
+ };
3927
+ legendWrapper.appendChild(toggleBtn);
3928
+ }
3929
+ const body = document.createElement("div");
3930
+ body.className = "pg-legend-body";
3931
+ const list = document.createElement("ul");
3932
+ list.className = "pg-legend-list";
3933
+ config.items.forEach((item) => {
3934
+ const listItem = document.createElement("li");
3935
+ listItem.className = "pg-legend-item";
3936
+ const swatch = document.createElement("span");
3937
+ swatch.className = `pg-legend-swatch is-${item.shape || "circle"}`;
3938
+ swatch.style.backgroundColor = item.color;
3939
+ const label = document.createElement("span");
3940
+ label.className = "pg-legend-label";
3941
+ label.innerText = item.label;
3942
+ listItem.appendChild(swatch);
3943
+ listItem.appendChild(label);
3944
+ list.appendChild(listItem);
3945
+ });
3946
+ body.appendChild(list);
3947
+ legendWrapper.appendChild(body);
3948
+ overlay.appendChild(legendWrapper);
3949
+ return () => {
3950
+ if (legendWrapper.parentNode === overlay) overlay.removeChild(legendWrapper);
3951
+ };
3916
3952
  }
3917
3953
 
3918
3954
  // src/utils/resolve-link-style.ts
@@ -4145,7 +4181,7 @@ function getLinkKey2(link) {
4145
4181
  }
4146
4182
  function renderLinkLabels(params, links) {
4147
4183
  const renderableLinks = createRenderableLinks2(params, links);
4148
- 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");
4184
+ 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");
4149
4185
  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);
4150
4186
  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 ?? "");
4151
4187
  return labelSelection;
@@ -4430,10 +4466,11 @@ function observeResize(element, onResize) {
4430
4466
  if (!entry) {
4431
4467
  return;
4432
4468
  }
4433
- onResize(
4434
- entry.contentRect.width,
4435
- entry.contentRect.height
4436
- );
4469
+ const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
4470
+ const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
4471
+ if (width > 0 && height > 0) {
4472
+ onResize(width, height);
4473
+ }
4437
4474
  }
4438
4475
  );
4439
4476
  observer.observe(element);
@@ -4466,54 +4503,85 @@ function getLinkTargetPoint(link) {
4466
4503
  };
4467
4504
  }
4468
4505
 
4506
+ // src/utils/export-graph.ts
4507
+ import html2canvas from "html2canvas";
4508
+ async function captureAndDownloadGraph(container, options = {}) {
4509
+ const {
4510
+ fileName = `graph-export-${Date.now()}.png`,
4511
+ backgroundColor = "#ffffff",
4512
+ pixelRatio = 2
4513
+ } = options;
4514
+ const root2 = container.querySelector(".pg-root");
4515
+ if (!root2) return;
4516
+ const controls = root2.querySelector(".pg-controls");
4517
+ const legendToggle = root2.querySelector(".pg-legend-toggle");
4518
+ const interactionLayer = root2.querySelector(".pg-interaction-layer");
4519
+ const legend = root2.querySelector(".pg-legend");
4520
+ const wasCollapsed = legend?.classList.contains("pg-is-collapsed");
4521
+ if (controls) controls.style.display = "none";
4522
+ if (legendToggle) legendToggle.style.display = "none";
4523
+ if (interactionLayer) interactionLayer.style.display = "none";
4524
+ if (legend && wasCollapsed) {
4525
+ legend.classList.remove("pg-is-collapsed");
4526
+ }
4527
+ try {
4528
+ const canvas = await html2canvas(root2, {
4529
+ scale: pixelRatio,
4530
+ backgroundColor,
4531
+ useCORS: true,
4532
+ logging: false
4533
+ });
4534
+ const dataUrl = canvas.toDataURL("image/png");
4535
+ const link = document.createElement("a");
4536
+ link.download = fileName;
4537
+ link.href = dataUrl;
4538
+ link.click();
4539
+ } finally {
4540
+ if (controls) controls.style.display = "flex";
4541
+ if (legendToggle) legendToggle.style.display = "flex";
4542
+ if (interactionLayer) interactionLayer.style.display = "block";
4543
+ if (legend && wasCollapsed) {
4544
+ legend.classList.add("pg-is-collapsed");
4545
+ }
4546
+ }
4547
+ }
4548
+
4469
4549
  // src/create-graph.ts
4470
4550
  function createGraph(config) {
4471
4551
  let cleanupResize = null;
4472
4552
  let cleanupZoom = null;
4473
4553
  let tooltipBinding = null;
4474
4554
  let controls = null;
4555
+ let legendCleanup = null;
4475
4556
  let dimensions = { width: 0, height: 0 };
4476
4557
  let rootGroup = null;
4558
+ let svgElement = null;
4477
4559
  let zoomBehavior = null;
4478
4560
  let simulation = null;
4479
4561
  function render() {
4480
4562
  destroy();
4481
4563
  const layers = createGraphLayers(config.container);
4564
+ svgElement = layers.svg;
4482
4565
  rootGroup = layers.root;
4483
- cleanupResize = observeResize(
4484
- config.container,
4485
- (width, height) => {
4486
- dimensions = { width, height };
4487
- layers.interactionRect.setAttribute("width", String(width));
4488
- layers.interactionRect.setAttribute("height", String(height));
4489
- simulation?.force("x", x_default2(width / 2).strength(0.03));
4490
- simulation?.force("y", y_default2(height / 2).strength(0.03));
4491
- simulation?.alpha(0.25).restart();
4492
- }
4493
- );
4566
+ cleanupResize = observeResize(config.container, (width, height) => {
4567
+ dimensions = { width, height };
4568
+ layers.svg.setAttribute("width", String(width));
4569
+ layers.svg.setAttribute("height", String(height));
4570
+ layers.interactionRect.setAttribute("width", String(width));
4571
+ layers.interactionRect.setAttribute("height", String(height));
4572
+ simulation?.force("x", x_default2(width / 2).strength(0.03));
4573
+ simulation?.force("y", y_default2(height / 2).strength(0.03));
4574
+ simulation?.alpha(0.25).restart();
4575
+ });
4494
4576
  const zoomResult = createZoom({
4495
- /**
4496
- * D3 zoom must be attached to SVG
4497
- * because it requires:
4498
- *
4499
- * width.baseVal
4500
- * height.baseVal
4501
- */
4502
- svg: config.container,
4503
- /**
4504
- * Used for pointer semantics /
4505
- * pan filtering only
4506
- */
4577
+ svg: layers.svg,
4507
4578
  interactionLayer: layers.interactionLayer,
4508
- /**
4509
- * Actual graph transform target
4510
- */
4511
4579
  root: layers.root
4512
4580
  });
4513
4581
  zoomBehavior = zoomResult.behavior;
4514
4582
  cleanupZoom = zoomResult.cleanup;
4515
4583
  const root2 = select_default2(layers.root);
4516
- const renderContext = { svg: config.container, root: root2, interaction: config.interaction };
4584
+ const renderContext = { svg: layers.svg, root: root2, interaction: config.interaction };
4517
4585
  const linkSelection = renderLinks(renderContext, config.links);
4518
4586
  const linkLabelSelection = renderLinkLabels(renderContext, config.links);
4519
4587
  const nodeSelection = renderNodes(renderContext, config.nodes);
@@ -4526,40 +4594,35 @@ function createGraph(config) {
4526
4594
  };
4527
4595
  const simulationResult = createGraphSimulation(simulationConfig);
4528
4596
  simulation = simulationResult.simulation;
4529
- simulation.on(
4530
- "tick",
4531
- () => {
4532
- 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);
4533
- linkLabelSelection.attr("transform", (item) => {
4534
- const link = item.link;
4535
- const source = link.source;
4536
- const targetPoint = getLinkTargetPoint(link);
4537
- const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
4538
- const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
4539
- return `translate(${x3}, ${y3})`;
4540
- }).each(function() {
4541
- const group = this;
4542
- const text = group.querySelector("text");
4543
- const rect = group.querySelector("rect");
4544
- if (!text || !rect) {
4545
- return;
4546
- }
4547
- const bBox = text.getBBox();
4548
- const padding = 6;
4549
- rect.setAttribute("x", String(bBox.x - padding));
4550
- rect.setAttribute("y", String(bBox.y - padding));
4551
- rect.setAttribute("width", String(bBox.width + padding * 2));
4552
- rect.setAttribute("height", String(bBox.height + padding * 2));
4553
- });
4554
- nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
4555
- labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
4556
- tooltipBinding?.reposition();
4557
- }
4558
- );
4597
+ simulation.on("tick", () => {
4598
+ 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);
4599
+ linkLabelSelection.attr("transform", (item) => {
4600
+ const link = item.link;
4601
+ const source = link.source;
4602
+ const targetPoint = getLinkTargetPoint(link);
4603
+ const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
4604
+ const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
4605
+ return `translate(${x3}, ${y3})`;
4606
+ }).each(function() {
4607
+ const group = this;
4608
+ const text = group.querySelector("text");
4609
+ const rect = group.querySelector("rect");
4610
+ if (!text || !rect) return;
4611
+ const bBox = text.getBBox();
4612
+ const padding = 6;
4613
+ rect.setAttribute("x", String(bBox.x - padding));
4614
+ rect.setAttribute("y", String(bBox.y - padding));
4615
+ rect.setAttribute("width", String(bBox.width + padding * 2));
4616
+ rect.setAttribute("height", String(bBox.height + padding * 2));
4617
+ });
4618
+ nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
4619
+ labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
4620
+ tooltipBinding?.reposition();
4621
+ });
4559
4622
  if (config.interaction?.hover?.enabled) {
4560
4623
  if (config.interaction?.hover?.tooltip?.enabled) {
4561
4624
  tooltipBinding = bindNodeTooltip({
4562
- container: config.container.parentElement,
4625
+ container: config.container,
4563
4626
  selection: nodeSelection,
4564
4627
  tooltipConfig: config.interaction.hover.tooltip
4565
4628
  });
@@ -4569,64 +4632,47 @@ function createGraph(config) {
4569
4632
  if (config.interaction?.drag?.enabled !== false) {
4570
4633
  nodeSelection.call(createDragBehavior(simulation));
4571
4634
  }
4572
- if (config.interaction?.selection?.enabled) {
4573
- }
4574
4635
  if (config.controls?.enabled) {
4575
- controls = createGraphControls(config.container, { zoomIn, zoomOut, resetView, fitView, destroy, render }, config.controls);
4636
+ controls = createGraphControls(
4637
+ layers.overlay,
4638
+ { zoomIn, zoomOut, resetView, fitView, destroy, render, exportGraph },
4639
+ config.controls
4640
+ );
4576
4641
  controls.mount();
4577
4642
  }
4643
+ if (config.legend?.enabled) {
4644
+ legendCleanup = createGraphLegend(layers.overlay, config.legend);
4645
+ }
4578
4646
  }
4579
4647
  function resetView() {
4580
- if (!zoomBehavior) {
4581
- return;
4582
- }
4583
- select_default2(config.container).transition().call(
4584
- zoomBehavior.transform,
4585
- identity2
4586
- );
4648
+ if (!zoomBehavior || !svgElement) return;
4649
+ select_default2(svgElement).transition().call(zoomBehavior.transform, identity2);
4587
4650
  }
4588
4651
  function fitView() {
4589
- if (!zoomBehavior || !rootGroup || dimensions.width === 0 || dimensions.height === 0) {
4590
- return;
4591
- }
4652
+ if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
4592
4653
  const bounds = rootGroup.getBBox();
4593
- if (bounds.width === 0 || bounds.height === 0) {
4594
- return;
4595
- }
4596
- const width = dimensions.width;
4597
- const height = dimensions.height;
4598
- const scale = Math.min(
4599
- width / bounds.width,
4600
- height / bounds.height
4601
- ) * 0.9;
4602
- const translateX = (width - bounds.width * scale) / 2 - bounds.x * scale;
4603
- const translateY = (height - bounds.height * scale) / 2 - bounds.y * scale;
4604
- const transform2 = identity2.translate(
4605
- translateX,
4606
- translateY
4607
- ).scale(scale);
4608
- select_default2(config.container).transition().call(
4609
- zoomBehavior.transform,
4610
- transform2
4611
- );
4654
+ if (bounds.width === 0 || bounds.height === 0) return;
4655
+ const scale = Math.min(dimensions.width / bounds.width, dimensions.height / bounds.height) * 0.9;
4656
+ const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4657
+ const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
4658
+ const transform2 = identity2.translate(translateX, translateY).scale(scale);
4659
+ select_default2(svgElement).transition().call(zoomBehavior.transform, transform2);
4612
4660
  }
4613
4661
  function zoomIn() {
4614
- if (!zoomBehavior) {
4615
- return;
4616
- }
4617
- select_default2(config.container).transition().call(
4618
- zoomBehavior.scaleBy,
4619
- 1.2
4620
- );
4662
+ if (!zoomBehavior || !svgElement) return;
4663
+ select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4621
4664
  }
4622
4665
  function zoomOut() {
4623
- if (!zoomBehavior) {
4624
- return;
4625
- }
4626
- select_default2(config.container).transition().call(
4627
- zoomBehavior.scaleBy,
4628
- 0.8
4629
- );
4666
+ if (!zoomBehavior || !svgElement) return;
4667
+ select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 0.8);
4668
+ }
4669
+ async function exportGraph(fileName) {
4670
+ fitView();
4671
+ await new Promise((resolve) => setTimeout(resolve, 500));
4672
+ await captureAndDownloadGraph(config.container, {
4673
+ fileName,
4674
+ pixelRatio: 2
4675
+ });
4630
4676
  }
4631
4677
  function destroy() {
4632
4678
  if (cleanupResize) {
@@ -4649,13 +4695,18 @@ function createGraph(config) {
4649
4695
  controls.destroy();
4650
4696
  controls = null;
4651
4697
  }
4698
+ if (legendCleanup) {
4699
+ legendCleanup();
4700
+ legendCleanup = null;
4701
+ }
4652
4702
  rootGroup = null;
4703
+ svgElement = null;
4653
4704
  zoomBehavior = null;
4654
4705
  while (config.container.firstChild) {
4655
4706
  config.container.removeChild(config.container.firstChild);
4656
4707
  }
4657
4708
  }
4658
- return { render, zoomIn, zoomOut, resetView, fitView, destroy };
4709
+ return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph };
4659
4710
  }
4660
4711
  export {
4661
4712
  createGraph
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-graph",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",
@@ -55,7 +55,8 @@
55
55
  "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build"
56
56
  },
57
57
  "dependencies": {
58
- "d3": "7.9.0"
58
+ "d3": "7.9.0",
59
+ "html2canvas": "^1.4.1"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@eslint/js": "^9.39.4",