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.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
 
@@ -3783,6 +3786,171 @@ function createGraphSimulation(config) {
3783
3786
  return { simulation };
3784
3787
  }
3785
3788
 
3789
+ // src/controls/graph-controls.utils.ts
3790
+ function resolveControlsPosition(position) {
3791
+ return position ?? "bottom-left";
3792
+ }
3793
+ function resolveControlsOrientation(orientation) {
3794
+ return orientation ?? "vertical";
3795
+ }
3796
+ function shouldRenderControl(config, key) {
3797
+ const show = config.show;
3798
+ if (!show) {
3799
+ return true;
3800
+ }
3801
+ const value = show[key];
3802
+ if (value === void 0) {
3803
+ return true;
3804
+ }
3805
+ return value;
3806
+ }
3807
+
3808
+ // src/assets/plus.svg?raw
3809
+ 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>';
3810
+
3811
+ // src/assets/minus.svg?raw
3812
+ 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>';
3813
+
3814
+ // src/assets/fit.svg?raw
3815
+ 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>';
3816
+
3817
+ // src/assets/reset.svg?raw
3818
+ 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>';
3819
+
3820
+ // src/controls/graph-controls.icons.ts
3821
+ var ICON_MAP = {
3822
+ "zoom-in": plus_default,
3823
+ "zoom-out": minus_default,
3824
+ fit: fit_default,
3825
+ reset: reset_default
3826
+ };
3827
+ function getControlIcon(icon) {
3828
+ const raw = ICON_MAP[icon];
3829
+ if (!raw) {
3830
+ throw new Error(`Icon not found: ${icon}`);
3831
+ }
3832
+ return raw.replace("<svg", '<svg class="pg-icon"');
3833
+ }
3834
+
3835
+ // src/controls/create-graph-controls.ts
3836
+ function createGraphControls(overlay, graph, config) {
3837
+ let root2 = null;
3838
+ function mount() {
3839
+ if (!config.enabled) {
3840
+ return;
3841
+ }
3842
+ root2 = document.createElement("div");
3843
+ root2.className = "pg-controls";
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
+ }
3852
+ appendControls(root2, config, graph);
3853
+ overlay.appendChild(root2);
3854
+ }
3855
+ function appendControls(root3, config2, graph2) {
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
+ });
3867
+ }
3868
+ function createButton(type, label, onClick) {
3869
+ const button = document.createElement("button");
3870
+ button.className = "pg-control-btn";
3871
+ button.type = "button";
3872
+ button.setAttribute("aria-label", label);
3873
+ const wrapper = document.createElement("div");
3874
+ wrapper.className = "pg-icon-wrapper";
3875
+ wrapper.innerHTML = getControlIcon(type);
3876
+ const svg = wrapper.querySelector("svg");
3877
+ if (svg) {
3878
+ svg.classList.add("pg-icon");
3879
+ button.appendChild(svg);
3880
+ }
3881
+ button.addEventListener("click", onClick);
3882
+ return button;
3883
+ }
3884
+ function destroy() {
3885
+ if (!root2) {
3886
+ return;
3887
+ }
3888
+ if (root2.parentNode === overlay) {
3889
+ overlay.removeChild(root2);
3890
+ }
3891
+ root2 = null;
3892
+ }
3893
+ return { mount, destroy };
3894
+ }
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}`);
3905
+ }
3906
+ return raw.replace("<svg", '<svg class="pg-icon"');
3907
+ }
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
+ };
3952
+ }
3953
+
3786
3954
  // src/utils/resolve-link-style.ts
3787
3955
  var DEFAULT_LINK_STYLE = {
3788
3956
  stroke: "#94a3b8",
@@ -4013,7 +4181,7 @@ function getLinkKey2(link) {
4013
4181
  }
4014
4182
  function renderLinkLabels(params, links) {
4015
4183
  const renderableLinks = createRenderableLinks2(params, links);
4016
- 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");
4017
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);
4018
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 ?? "");
4019
4187
  return labelSelection;
@@ -4298,10 +4466,11 @@ function observeResize(element, onResize) {
4298
4466
  if (!entry) {
4299
4467
  return;
4300
4468
  }
4301
- onResize(
4302
- entry.contentRect.width,
4303
- entry.contentRect.height
4304
- );
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
+ }
4305
4474
  }
4306
4475
  );
4307
4476
  observer.observe(element);
@@ -4334,53 +4503,85 @@ function getLinkTargetPoint(link) {
4334
4503
  };
4335
4504
  }
4336
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
+
4337
4549
  // src/create-graph.ts
4338
4550
  function createGraph(config) {
4339
4551
  let cleanupResize = null;
4340
4552
  let cleanupZoom = null;
4341
4553
  let tooltipBinding = null;
4554
+ let controls = null;
4555
+ let legendCleanup = null;
4342
4556
  let dimensions = { width: 0, height: 0 };
4343
4557
  let rootGroup = null;
4558
+ let svgElement = null;
4344
4559
  let zoomBehavior = null;
4345
4560
  let simulation = null;
4346
4561
  function render() {
4347
4562
  destroy();
4348
4563
  const layers = createGraphLayers(config.container);
4564
+ svgElement = layers.svg;
4349
4565
  rootGroup = layers.root;
4350
- cleanupResize = observeResize(
4351
- config.container,
4352
- (width, height) => {
4353
- dimensions = { width, height };
4354
- layers.interactionRect.setAttribute("width", String(width));
4355
- layers.interactionRect.setAttribute("height", String(height));
4356
- simulation?.force("x", x_default2(width / 2).strength(0.03));
4357
- simulation?.force("y", y_default2(height / 2).strength(0.03));
4358
- simulation?.alpha(0.25).restart();
4359
- }
4360
- );
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
+ });
4361
4576
  const zoomResult = createZoom({
4362
- /**
4363
- * D3 zoom must be attached to SVG
4364
- * because it requires:
4365
- *
4366
- * width.baseVal
4367
- * height.baseVal
4368
- */
4369
- svg: config.container,
4370
- /**
4371
- * Used for pointer semantics /
4372
- * pan filtering only
4373
- */
4577
+ svg: layers.svg,
4374
4578
  interactionLayer: layers.interactionLayer,
4375
- /**
4376
- * Actual graph transform target
4377
- */
4378
4579
  root: layers.root
4379
4580
  });
4380
4581
  zoomBehavior = zoomResult.behavior;
4381
4582
  cleanupZoom = zoomResult.cleanup;
4382
4583
  const root2 = select_default2(layers.root);
4383
- const renderContext = { svg: config.container, root: root2, interaction: config.interaction };
4584
+ const renderContext = { svg: layers.svg, root: root2, interaction: config.interaction };
4384
4585
  const linkSelection = renderLinks(renderContext, config.links);
4385
4586
  const linkLabelSelection = renderLinkLabels(renderContext, config.links);
4386
4587
  const nodeSelection = renderNodes(renderContext, config.nodes);
@@ -4393,40 +4594,35 @@ function createGraph(config) {
4393
4594
  };
4394
4595
  const simulationResult = createGraphSimulation(simulationConfig);
4395
4596
  simulation = simulationResult.simulation;
4396
- simulation.on(
4397
- "tick",
4398
- () => {
4399
- 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);
4400
- linkLabelSelection.attr("transform", (item) => {
4401
- const link = item.link;
4402
- const source = link.source;
4403
- const targetPoint = getLinkTargetPoint(link);
4404
- const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
4405
- const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
4406
- return `translate(${x3}, ${y3})`;
4407
- }).each(function() {
4408
- const group = this;
4409
- const text = group.querySelector("text");
4410
- const rect = group.querySelector("rect");
4411
- if (!text || !rect) {
4412
- return;
4413
- }
4414
- const bBox = text.getBBox();
4415
- const padding = 6;
4416
- rect.setAttribute("x", String(bBox.x - padding));
4417
- rect.setAttribute("y", String(bBox.y - padding));
4418
- rect.setAttribute("width", String(bBox.width + padding * 2));
4419
- rect.setAttribute("height", String(bBox.height + padding * 2));
4420
- });
4421
- nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
4422
- labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
4423
- tooltipBinding?.reposition();
4424
- }
4425
- );
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
+ });
4426
4622
  if (config.interaction?.hover?.enabled) {
4427
4623
  if (config.interaction?.hover?.tooltip?.enabled) {
4428
4624
  tooltipBinding = bindNodeTooltip({
4429
- container: config.container.parentElement,
4625
+ container: config.container,
4430
4626
  selection: nodeSelection,
4431
4627
  tooltipConfig: config.interaction.hover.tooltip
4432
4628
  });
@@ -4436,60 +4632,47 @@ function createGraph(config) {
4436
4632
  if (config.interaction?.drag?.enabled !== false) {
4437
4633
  nodeSelection.call(createDragBehavior(simulation));
4438
4634
  }
4439
- if (config.interaction?.selection?.enabled) {
4635
+ if (config.controls?.enabled) {
4636
+ controls = createGraphControls(
4637
+ layers.overlay,
4638
+ { zoomIn, zoomOut, resetView, fitView, destroy, render, exportGraph },
4639
+ config.controls
4640
+ );
4641
+ controls.mount();
4642
+ }
4643
+ if (config.legend?.enabled) {
4644
+ legendCleanup = createGraphLegend(layers.overlay, config.legend);
4440
4645
  }
4441
4646
  }
4442
4647
  function resetView() {
4443
- if (!zoomBehavior) {
4444
- return;
4445
- }
4446
- select_default2(config.container).transition().call(
4447
- zoomBehavior.transform,
4448
- identity2
4449
- );
4648
+ if (!zoomBehavior || !svgElement) return;
4649
+ select_default2(svgElement).transition().call(zoomBehavior.transform, identity2);
4450
4650
  }
4451
4651
  function fitView() {
4452
- if (!zoomBehavior || !rootGroup || dimensions.width === 0 || dimensions.height === 0) {
4453
- return;
4454
- }
4652
+ if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
4455
4653
  const bounds = rootGroup.getBBox();
4456
- if (bounds.width === 0 || bounds.height === 0) {
4457
- return;
4458
- }
4459
- const width = dimensions.width;
4460
- const height = dimensions.height;
4461
- const scale = Math.min(
4462
- width / bounds.width,
4463
- height / bounds.height
4464
- ) * 0.9;
4465
- const translateX = (width - bounds.width * scale) / 2 - bounds.x * scale;
4466
- const translateY = (height - bounds.height * scale) / 2 - bounds.y * scale;
4467
- const transform2 = identity2.translate(
4468
- translateX,
4469
- translateY
4470
- ).scale(scale);
4471
- select_default2(config.container).transition().call(
4472
- zoomBehavior.transform,
4473
- transform2
4474
- );
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);
4475
4660
  }
4476
4661
  function zoomIn() {
4477
- if (!zoomBehavior) {
4478
- return;
4479
- }
4480
- select_default2(config.container).transition().call(
4481
- zoomBehavior.scaleBy,
4482
- 1.2
4483
- );
4662
+ if (!zoomBehavior || !svgElement) return;
4663
+ select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4484
4664
  }
4485
4665
  function zoomOut() {
4486
- if (!zoomBehavior) {
4487
- return;
4488
- }
4489
- select_default2(config.container).transition().call(
4490
- zoomBehavior.scaleBy,
4491
- 0.8
4492
- );
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
+ });
4493
4676
  }
4494
4677
  function destroy() {
4495
4678
  if (cleanupResize) {
@@ -4508,13 +4691,22 @@ function createGraph(config) {
4508
4691
  simulation.stop();
4509
4692
  simulation = null;
4510
4693
  }
4694
+ if (controls) {
4695
+ controls.destroy();
4696
+ controls = null;
4697
+ }
4698
+ if (legendCleanup) {
4699
+ legendCleanup();
4700
+ legendCleanup = null;
4701
+ }
4511
4702
  rootGroup = null;
4703
+ svgElement = null;
4512
4704
  zoomBehavior = null;
4513
4705
  while (config.container.firstChild) {
4514
4706
  config.container.removeChild(config.container.firstChild);
4515
4707
  }
4516
4708
  }
4517
- return { render, zoomIn, zoomOut, resetView, fitView, destroy };
4709
+ return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph };
4518
4710
  }
4519
4711
  export {
4520
4712
  createGraph
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-graph",
3
- "version": "0.1.2",
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",
@@ -47,15 +47,16 @@
47
47
  },
48
48
  "scripts": {
49
49
  "clean": "rm -rf dist",
50
- "build": "tsup src/index.ts --format esm,cjs --dts --clean",
51
- "dev": "tsup src/index.ts --watch --format esm,cjs --dts",
50
+ "build": "tsup",
51
+ "dev": "tsup",
52
52
  "lint": "eslint \"src/**/*.ts\"",
53
53
  "typecheck": "tsc --noEmit",
54
54
  "test": "vitest run --passWithNoTests",
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",