polly-graph 0.1.3 → 0.1.5
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/README.md +82 -159
- package/dist/index.cjs +314 -299
- package/dist/index.css +221 -32
- package/dist/index.d.cts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +304 -299
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -3608,104 +3608,45 @@ function manyBody_default() {
|
|
|
3608
3608
|
return force;
|
|
3609
3609
|
}
|
|
3610
3610
|
|
|
3611
|
-
// node_modules/d3-force/src/x.js
|
|
3612
|
-
function x_default2(x3) {
|
|
3613
|
-
var strength = constant_default5(0.1), nodes, strengths, xz;
|
|
3614
|
-
if (typeof x3 !== "function") x3 = constant_default5(x3 == null ? 0 : +x3);
|
|
3615
|
-
function force(alpha) {
|
|
3616
|
-
for (var i = 0, n = nodes.length, node; i < n; ++i) {
|
|
3617
|
-
node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha;
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
function initialize() {
|
|
3621
|
-
if (!nodes) return;
|
|
3622
|
-
var i, n = nodes.length;
|
|
3623
|
-
strengths = new Array(n);
|
|
3624
|
-
xz = new Array(n);
|
|
3625
|
-
for (i = 0; i < n; ++i) {
|
|
3626
|
-
strengths[i] = isNaN(xz[i] = +x3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
|
|
3627
|
-
}
|
|
3628
|
-
}
|
|
3629
|
-
force.initialize = function(_) {
|
|
3630
|
-
nodes = _;
|
|
3631
|
-
initialize();
|
|
3632
|
-
};
|
|
3633
|
-
force.strength = function(_) {
|
|
3634
|
-
return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
|
|
3635
|
-
};
|
|
3636
|
-
force.x = function(_) {
|
|
3637
|
-
return arguments.length ? (x3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : x3;
|
|
3638
|
-
};
|
|
3639
|
-
return force;
|
|
3640
|
-
}
|
|
3641
|
-
|
|
3642
|
-
// node_modules/d3-force/src/y.js
|
|
3643
|
-
function y_default2(y3) {
|
|
3644
|
-
var strength = constant_default5(0.1), nodes, strengths, yz;
|
|
3645
|
-
if (typeof y3 !== "function") y3 = constant_default5(y3 == null ? 0 : +y3);
|
|
3646
|
-
function force(alpha) {
|
|
3647
|
-
for (var i = 0, n = nodes.length, node; i < n; ++i) {
|
|
3648
|
-
node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha;
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
function initialize() {
|
|
3652
|
-
if (!nodes) return;
|
|
3653
|
-
var i, n = nodes.length;
|
|
3654
|
-
strengths = new Array(n);
|
|
3655
|
-
yz = new Array(n);
|
|
3656
|
-
for (i = 0; i < n; ++i) {
|
|
3657
|
-
strengths[i] = isNaN(yz[i] = +y3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
|
|
3658
|
-
}
|
|
3659
|
-
}
|
|
3660
|
-
force.initialize = function(_) {
|
|
3661
|
-
nodes = _;
|
|
3662
|
-
initialize();
|
|
3663
|
-
};
|
|
3664
|
-
force.strength = function(_) {
|
|
3665
|
-
return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
|
|
3666
|
-
};
|
|
3667
|
-
force.y = function(_) {
|
|
3668
|
-
return arguments.length ? (y3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : y3;
|
|
3669
|
-
};
|
|
3670
|
-
return force;
|
|
3671
|
-
}
|
|
3672
|
-
|
|
3673
3611
|
// src/core/create-graph-layers.ts
|
|
3674
|
-
function createGraphLayers(
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
const
|
|
3612
|
+
function createGraphLayers(host) {
|
|
3613
|
+
host.innerHTML = "";
|
|
3614
|
+
const rootContainer = document.createElement("div");
|
|
3615
|
+
rootContainer.className = "pg-root";
|
|
3616
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
3617
|
+
svg.setAttribute("class", "pg-canvas");
|
|
3618
|
+
const overlay = document.createElement("div");
|
|
3619
|
+
overlay.className = "pg-overlay";
|
|
3620
|
+
rootContainer.appendChild(svg);
|
|
3621
|
+
rootContainer.appendChild(overlay);
|
|
3622
|
+
host.appendChild(rootContainer);
|
|
3623
|
+
const createGroup = (layerName) => {
|
|
3679
3624
|
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
3680
|
-
group.setAttribute("class",
|
|
3681
|
-
group.setAttribute("data-layer",
|
|
3625
|
+
group.setAttribute("class", `pg-layer-${layerName}`);
|
|
3626
|
+
group.setAttribute("data-layer", layerName);
|
|
3682
3627
|
return group;
|
|
3683
3628
|
};
|
|
3684
3629
|
const interactionLayer = createGroup("interaction-layer");
|
|
3685
3630
|
const interactionRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
height: "100%",
|
|
3690
|
-
fill: "transparent",
|
|
3691
|
-
"pointer-events": "all"
|
|
3692
|
-
};
|
|
3693
|
-
Object.entries(interactionAttributes).forEach(([key, value]) => {
|
|
3694
|
-
interactionRect.setAttribute(key, value);
|
|
3695
|
-
});
|
|
3631
|
+
interactionRect.setAttribute("class", "pg-interaction-surface");
|
|
3632
|
+
interactionRect.setAttribute("fill", "transparent");
|
|
3633
|
+
interactionRect.setAttribute("pointer-events", "all");
|
|
3696
3634
|
interactionLayer.appendChild(interactionRect);
|
|
3697
|
-
const
|
|
3635
|
+
const graphRoot = createGroup("viewport");
|
|
3698
3636
|
const layers = {
|
|
3637
|
+
svg,
|
|
3638
|
+
overlay,
|
|
3699
3639
|
interactionLayer,
|
|
3700
3640
|
interactionRect,
|
|
3701
|
-
root:
|
|
3641
|
+
root: graphRoot,
|
|
3642
|
+
// These keys now match your ctx.root.select('[data-layer="..."]') calls
|
|
3702
3643
|
links: createGroup("links"),
|
|
3703
3644
|
linkLabels: createGroup("link-labels"),
|
|
3704
3645
|
nodeRings: createGroup("node-rings"),
|
|
3705
3646
|
nodes: createGroup("nodes"),
|
|
3706
3647
|
nodeLabels: createGroup("node-labels")
|
|
3707
3648
|
};
|
|
3708
|
-
|
|
3649
|
+
graphRoot.append(
|
|
3709
3650
|
layers.links,
|
|
3710
3651
|
layers.linkLabels,
|
|
3711
3652
|
layers.nodeRings,
|
|
@@ -3713,7 +3654,7 @@ function createGraphLayers(svg) {
|
|
|
3713
3654
|
layers.nodeLabels
|
|
3714
3655
|
);
|
|
3715
3656
|
svg.appendChild(interactionLayer);
|
|
3716
|
-
svg.appendChild(
|
|
3657
|
+
svg.appendChild(graphRoot);
|
|
3717
3658
|
return layers;
|
|
3718
3659
|
}
|
|
3719
3660
|
|
|
@@ -3775,10 +3716,17 @@ function createGraphSimulation(config) {
|
|
|
3775
3716
|
});
|
|
3776
3717
|
const simulation = simulation_default(config.nodes).alpha(0.9).alphaDecay(0.12).alphaMin(0.03).velocityDecay(0.5).force(
|
|
3777
3718
|
"link",
|
|
3778
|
-
link_default(config.links).id((d) => d.id).distance(
|
|
3719
|
+
link_default(config.links).id((d) => d.id).distance((d) => {
|
|
3720
|
+
const source = d.source;
|
|
3721
|
+
const target = d.target;
|
|
3722
|
+
const sourceR = source.style?.radius || 20;
|
|
3723
|
+
const targetR = target.style?.radius || 20;
|
|
3724
|
+
const labelBuffer = d.style?.label?.height || 40;
|
|
3725
|
+
return (sourceR + targetR + labelBuffer) * 2;
|
|
3726
|
+
}).strength(0.8)
|
|
3779
3727
|
).force("charge", manyBody_default().strength(-220)).force(
|
|
3780
3728
|
"collide",
|
|
3781
|
-
collide_default().radius((node) => (node.style?.radius ?? 12) + 10).
|
|
3729
|
+
collide_default().radius((node) => (node.style?.radius ?? 12) + 10).iterations(2)
|
|
3782
3730
|
).force("center", center_default(centerX, centerY).strength(0.08));
|
|
3783
3731
|
return { simulation };
|
|
3784
3732
|
}
|
|
@@ -3830,49 +3778,51 @@ function getControlIcon(icon) {
|
|
|
3830
3778
|
}
|
|
3831
3779
|
|
|
3832
3780
|
// src/controls/create-graph-controls.ts
|
|
3833
|
-
function createGraphControls(
|
|
3781
|
+
function createGraphControls(overlay, graph, config) {
|
|
3834
3782
|
let root2 = null;
|
|
3835
3783
|
function mount() {
|
|
3836
3784
|
if (!config.enabled) {
|
|
3837
3785
|
return;
|
|
3838
3786
|
}
|
|
3839
|
-
const parent = container.parentElement;
|
|
3840
|
-
if (!parent) {
|
|
3841
|
-
return;
|
|
3842
|
-
}
|
|
3843
3787
|
root2 = document.createElement("div");
|
|
3844
3788
|
root2.className = "pg-controls";
|
|
3845
|
-
|
|
3846
|
-
|
|
3789
|
+
const position = resolveControlsPosition(config.position);
|
|
3790
|
+
root2.classList.add(`pg-pos-${position}`);
|
|
3791
|
+
const orientation = resolveControlsOrientation(config.orientation);
|
|
3792
|
+
root2.classList.add(`pg-orient-${orientation}`);
|
|
3793
|
+
if (config.offset) {
|
|
3794
|
+
root2.style.setProperty("--pg-controls-offset-x", `${config.offset.x}px`);
|
|
3795
|
+
root2.style.setProperty("--pg-controls-offset-y", `${config.offset.y}px`);
|
|
3796
|
+
}
|
|
3847
3797
|
appendControls(root2, config, graph);
|
|
3848
|
-
|
|
3798
|
+
overlay.appendChild(root2);
|
|
3849
3799
|
}
|
|
3850
3800
|
function appendControls(root3, config2, graph2) {
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
}
|
|
3801
|
+
const actions = [
|
|
3802
|
+
{ key: "zoomIn", icon: "zoom-in", label: "Zoom in", fn: graph2.zoomIn.bind(graph2) },
|
|
3803
|
+
{ key: "zoomOut", icon: "zoom-out", label: "Zoom out", fn: graph2.zoomOut.bind(graph2) },
|
|
3804
|
+
{ key: "fit", icon: "fit", label: "Fit view", fn: graph2.fitView.bind(graph2) },
|
|
3805
|
+
{ key: "reset", icon: "reset", label: "Reset view", fn: graph2.resetView.bind(graph2) }
|
|
3806
|
+
];
|
|
3807
|
+
actions.forEach((action) => {
|
|
3808
|
+
if (shouldRenderControl(config2, action.key)) {
|
|
3809
|
+
root3.appendChild(createButton(action.icon, action.label, action.fn));
|
|
3810
|
+
}
|
|
3811
|
+
});
|
|
3863
3812
|
}
|
|
3864
3813
|
function createButton(type, label, onClick) {
|
|
3865
3814
|
const button = document.createElement("button");
|
|
3815
|
+
button.className = "pg-control-btn";
|
|
3866
3816
|
button.type = "button";
|
|
3867
3817
|
button.setAttribute("aria-label", label);
|
|
3868
3818
|
const wrapper = document.createElement("div");
|
|
3819
|
+
wrapper.className = "pg-icon-wrapper";
|
|
3869
3820
|
wrapper.innerHTML = getControlIcon(type);
|
|
3870
3821
|
const svg = wrapper.querySelector("svg");
|
|
3871
|
-
if (
|
|
3872
|
-
|
|
3822
|
+
if (svg) {
|
|
3823
|
+
svg.classList.add("pg-icon");
|
|
3824
|
+
button.appendChild(svg);
|
|
3873
3825
|
}
|
|
3874
|
-
svg.classList.add("pg-icon");
|
|
3875
|
-
button.appendChild(svg);
|
|
3876
3826
|
button.addEventListener("click", onClick);
|
|
3877
3827
|
return button;
|
|
3878
3828
|
}
|
|
@@ -3880,39 +3830,70 @@ function createGraphControls(container, graph, config) {
|
|
|
3880
3830
|
if (!root2) {
|
|
3881
3831
|
return;
|
|
3882
3832
|
}
|
|
3883
|
-
|
|
3884
|
-
|
|
3833
|
+
if (root2.parentNode === overlay) {
|
|
3834
|
+
overlay.removeChild(root2);
|
|
3835
|
+
}
|
|
3885
3836
|
root2 = null;
|
|
3886
3837
|
}
|
|
3887
3838
|
return { mount, destroy };
|
|
3888
3839
|
}
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
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;
|
|
3840
|
+
|
|
3841
|
+
// src/assets/caret.svg?raw
|
|
3842
|
+
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>';
|
|
3843
|
+
|
|
3844
|
+
// src/legends/graph-legend-icon.ts
|
|
3845
|
+
var LEGEND_ICON_MAP = { caret: caret_default };
|
|
3846
|
+
function getLegendIcon(icon) {
|
|
3847
|
+
const raw = LEGEND_ICON_MAP[icon];
|
|
3848
|
+
if (!raw) {
|
|
3849
|
+
throw new Error(`Legend icon not found: ${icon}`);
|
|
3910
3850
|
}
|
|
3851
|
+
return raw.replace("<svg", '<svg class="pg-icon"');
|
|
3911
3852
|
}
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3853
|
+
|
|
3854
|
+
// src/legends/create-graph-legends.ts
|
|
3855
|
+
function createGraphLegend(overlay, config) {
|
|
3856
|
+
const legendWrapper = document.createElement("div");
|
|
3857
|
+
legendWrapper.className = "pg-legend";
|
|
3858
|
+
const position = config.position || "bottom-right";
|
|
3859
|
+
legendWrapper.classList.add(`pg-pos-${position}`);
|
|
3860
|
+
if (config.defaultExpanded === false) {
|
|
3861
|
+
legendWrapper.classList.add("pg-is-collapsed");
|
|
3862
|
+
}
|
|
3863
|
+
if (config.collapsible) {
|
|
3864
|
+
const toggleBtn = document.createElement("button");
|
|
3865
|
+
toggleBtn.className = "pg-legend-toggle";
|
|
3866
|
+
toggleBtn.type = "button";
|
|
3867
|
+
toggleBtn.innerHTML = getLegendIcon("caret");
|
|
3868
|
+
toggleBtn.onclick = (e) => {
|
|
3869
|
+
e.stopPropagation();
|
|
3870
|
+
legendWrapper.classList.toggle("pg-is-collapsed");
|
|
3871
|
+
};
|
|
3872
|
+
legendWrapper.appendChild(toggleBtn);
|
|
3873
|
+
}
|
|
3874
|
+
const body = document.createElement("div");
|
|
3875
|
+
body.className = "pg-legend-body";
|
|
3876
|
+
const list = document.createElement("ul");
|
|
3877
|
+
list.className = "pg-legend-list";
|
|
3878
|
+
config.items.forEach((item) => {
|
|
3879
|
+
const listItem = document.createElement("li");
|
|
3880
|
+
listItem.className = "pg-legend-item";
|
|
3881
|
+
const swatch = document.createElement("span");
|
|
3882
|
+
swatch.className = `pg-legend-swatch is-${item.shape || "circle"}`;
|
|
3883
|
+
swatch.style.backgroundColor = item.color;
|
|
3884
|
+
const label = document.createElement("span");
|
|
3885
|
+
label.className = "pg-legend-label";
|
|
3886
|
+
label.innerText = item.label;
|
|
3887
|
+
listItem.appendChild(swatch);
|
|
3888
|
+
listItem.appendChild(label);
|
|
3889
|
+
list.appendChild(listItem);
|
|
3890
|
+
});
|
|
3891
|
+
body.appendChild(list);
|
|
3892
|
+
legendWrapper.appendChild(body);
|
|
3893
|
+
overlay.appendChild(legendWrapper);
|
|
3894
|
+
return () => {
|
|
3895
|
+
if (legendWrapper.parentNode === overlay) overlay.removeChild(legendWrapper);
|
|
3896
|
+
};
|
|
3916
3897
|
}
|
|
3917
3898
|
|
|
3918
3899
|
// src/utils/resolve-link-style.ts
|
|
@@ -3928,6 +3909,7 @@ var DEFAULT_LINK_STYLE = {
|
|
|
3928
3909
|
},
|
|
3929
3910
|
label: {
|
|
3930
3911
|
enabled: true,
|
|
3912
|
+
visibility: "always",
|
|
3931
3913
|
backgroundFill: "color-mix(in srgb, #8E42EE, #FFFFFF 90%)",
|
|
3932
3914
|
borderColor: "color-mix(in srgb, #8E42EE, #FFFFFF 10%)",
|
|
3933
3915
|
borderWidth: 1.5,
|
|
@@ -3963,6 +3945,7 @@ function mergeLinkStyle(base, override) {
|
|
|
3963
3945
|
},
|
|
3964
3946
|
label: {
|
|
3965
3947
|
enabled: override?.label?.enabled ?? base.label.enabled,
|
|
3948
|
+
visibility: override?.label?.visibility ?? base.label.visibility,
|
|
3966
3949
|
backgroundFill: override?.label?.backgroundFill ?? base.label.backgroundFill,
|
|
3967
3950
|
borderColor: override?.label?.borderColor ?? base.label.borderColor,
|
|
3968
3951
|
borderWidth: override?.label?.borderWidth ?? base.label.borderWidth,
|
|
@@ -4063,7 +4046,14 @@ function getLinkKey(link) {
|
|
|
4063
4046
|
}
|
|
4064
4047
|
function renderLinks(ctx, links) {
|
|
4065
4048
|
const renderableLinks = createRenderableLinks(ctx, links);
|
|
4066
|
-
|
|
4049
|
+
const linkSelection = ctx.root.select('[data-layer="links"]').selectAll("line").data(renderableLinks, (item) => getLinkKey(item.link)).join("line").attr("class", "graph-link").attr("stroke", (item) => item.style.stroke).attr("stroke-width", (item) => item.style.strokeWidth).attr("opacity", (item) => item.style.opacity).attr("marker-end", (item) => item.markerEnd).style("pointer-events", "stroke");
|
|
4050
|
+
const labelSelection = ctx.root.selectAll(".link-label");
|
|
4051
|
+
linkSelection.on("mouseenter.label-hover", (_event, d) => {
|
|
4052
|
+
labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 1);
|
|
4053
|
+
}).on("mouseleave.label-hover", (_event, d) => {
|
|
4054
|
+
labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 0);
|
|
4055
|
+
});
|
|
4056
|
+
return linkSelection;
|
|
4067
4057
|
}
|
|
4068
4058
|
|
|
4069
4059
|
// src/renderer/nodes.ts
|
|
@@ -4133,7 +4123,10 @@ function renderNodeLabels(ctx, nodes) {
|
|
|
4133
4123
|
// src/renderer/link-labels.ts
|
|
4134
4124
|
function createRenderableLinks2(params, links) {
|
|
4135
4125
|
return links.map(
|
|
4136
|
-
(link) => ({
|
|
4126
|
+
(link) => ({
|
|
4127
|
+
link,
|
|
4128
|
+
style: resolveLinkStyle({ link, interaction: params.interaction })
|
|
4129
|
+
})
|
|
4137
4130
|
).filter(
|
|
4138
4131
|
(item) => item.style.label.enabled && Boolean(item.link.label)
|
|
4139
4132
|
);
|
|
@@ -4145,7 +4138,13 @@ function getLinkKey2(link) {
|
|
|
4145
4138
|
}
|
|
4146
4139
|
function renderLinkLabels(params, links) {
|
|
4147
4140
|
const renderableLinks = createRenderableLinks2(params, links);
|
|
4148
|
-
const labelSelection = params.root.select("
|
|
4141
|
+
const labelSelection = params.root.select('[data-layer="link-labels"]').selectAll(".link-label").data(renderableLinks, (item) => getLinkKey2(item.link)).join("g").attr("class", "link-label").style("opacity", (item) => {
|
|
4142
|
+
const visibility = item.style.label.visibility ?? "always";
|
|
4143
|
+
return visibility === "always" ? 1 : 0;
|
|
4144
|
+
}).style("pointer-events", (item) => {
|
|
4145
|
+
const visibility = item.style.label.visibility ?? "always";
|
|
4146
|
+
return visibility === "always" ? "auto" : "none";
|
|
4147
|
+
}).style("cursor", "pointer");
|
|
4149
4148
|
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
4149
|
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
4150
|
return labelSelection;
|
|
@@ -4174,58 +4173,35 @@ function createDragBehavior(simulation) {
|
|
|
4174
4173
|
|
|
4175
4174
|
// src/interactions/create-node-hover.ts
|
|
4176
4175
|
function createNodeHover(nodeSelection, hoverStyle) {
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
"mouseenter.hover",
|
|
4182
|
-
function(_event, node) {
|
|
4176
|
+
const firstNode = nodeSelection.node();
|
|
4177
|
+
if (!firstNode) return;
|
|
4178
|
+
if (hoverStyle) {
|
|
4179
|
+
nodeSelection.on("mouseenter.hover", function(_event, node) {
|
|
4183
4180
|
const circle = this;
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
"stroke",
|
|
4189
|
-
hoverStroke
|
|
4190
|
-
);
|
|
4191
|
-
circle.setAttribute(
|
|
4192
|
-
"stroke-width",
|
|
4193
|
-
String(
|
|
4194
|
-
hoverStrokeWidth
|
|
4195
|
-
)
|
|
4196
|
-
);
|
|
4197
|
-
circle.setAttribute(
|
|
4198
|
-
"opacity",
|
|
4199
|
-
String(
|
|
4200
|
-
hoverOpacity
|
|
4201
|
-
)
|
|
4202
|
-
);
|
|
4203
|
-
}
|
|
4204
|
-
).on(
|
|
4205
|
-
"mouseleave.hover",
|
|
4206
|
-
function(_event, node) {
|
|
4181
|
+
circle.setAttribute("stroke", hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff");
|
|
4182
|
+
circle.setAttribute("stroke-width", String(hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5));
|
|
4183
|
+
circle.setAttribute("opacity", String(hoverStyle.opacity ?? node.style?.opacity ?? 1));
|
|
4184
|
+
}).on("mouseleave.hover", function(_event, node) {
|
|
4207
4185
|
const circle = this;
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
}
|
|
4228
|
-
);
|
|
4186
|
+
circle.setAttribute("stroke", node.style?.stroke ?? "#ffffff");
|
|
4187
|
+
circle.setAttribute("stroke-width", String(node.style?.strokeWidth ?? 1.5));
|
|
4188
|
+
circle.setAttribute("opacity", String(node.style?.opacity ?? 1));
|
|
4189
|
+
});
|
|
4190
|
+
}
|
|
4191
|
+
const svgElement = firstNode.ownerSVGElement;
|
|
4192
|
+
if (!svgElement) return;
|
|
4193
|
+
const root2 = select_default2(svgElement);
|
|
4194
|
+
const labelSelection = root2.selectAll(".link-label");
|
|
4195
|
+
nodeSelection.on("mouseenter.labels", (_event, d) => {
|
|
4196
|
+
labelSelection.filter((item) => {
|
|
4197
|
+
if (item.style.label.visibility !== "hover") return false;
|
|
4198
|
+
const s = item.link.source;
|
|
4199
|
+
const t = item.link.target;
|
|
4200
|
+
return s.id === d.id || t.id === d.id;
|
|
4201
|
+
}).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
|
|
4202
|
+
}).on("mouseleave.labels", (_event) => {
|
|
4203
|
+
labelSelection.filter((item) => item.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
|
|
4204
|
+
});
|
|
4229
4205
|
}
|
|
4230
4206
|
|
|
4231
4207
|
// src/utils/resolve-tooltip-position.ts
|
|
@@ -4430,10 +4406,11 @@ function observeResize(element, onResize) {
|
|
|
4430
4406
|
if (!entry) {
|
|
4431
4407
|
return;
|
|
4432
4408
|
}
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4409
|
+
const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
|
|
4410
|
+
const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
|
|
4411
|
+
if (width > 0 && height > 0) {
|
|
4412
|
+
onResize(width, height);
|
|
4413
|
+
}
|
|
4437
4414
|
}
|
|
4438
4415
|
);
|
|
4439
4416
|
observer.observe(element);
|
|
@@ -4466,54 +4443,94 @@ function getLinkTargetPoint(link) {
|
|
|
4466
4443
|
};
|
|
4467
4444
|
}
|
|
4468
4445
|
|
|
4446
|
+
// src/utils/export-graph.ts
|
|
4447
|
+
import html2canvas from "html2canvas";
|
|
4448
|
+
async function captureAndDownloadGraph(container, options = {}) {
|
|
4449
|
+
const {
|
|
4450
|
+
fileName = `graph-export-${Date.now()}.png`,
|
|
4451
|
+
backgroundColor = "#ffffff",
|
|
4452
|
+
pixelRatio = 2
|
|
4453
|
+
} = options;
|
|
4454
|
+
const root2 = container.querySelector(".pg-root");
|
|
4455
|
+
if (!root2) return;
|
|
4456
|
+
const controls = root2.querySelector(".pg-controls");
|
|
4457
|
+
const legendToggle = root2.querySelector(".pg-legend-toggle");
|
|
4458
|
+
const interactionLayer = root2.querySelector(".pg-interaction-layer");
|
|
4459
|
+
const legend = root2.querySelector(".pg-legend");
|
|
4460
|
+
const wasCollapsed = legend?.classList.contains("pg-is-collapsed");
|
|
4461
|
+
if (controls) controls.style.display = "none";
|
|
4462
|
+
if (legendToggle) legendToggle.style.display = "none";
|
|
4463
|
+
if (interactionLayer) interactionLayer.style.display = "none";
|
|
4464
|
+
if (legend && wasCollapsed) {
|
|
4465
|
+
legend.classList.remove("pg-is-collapsed");
|
|
4466
|
+
}
|
|
4467
|
+
try {
|
|
4468
|
+
const canvas = await html2canvas(root2, {
|
|
4469
|
+
scale: pixelRatio,
|
|
4470
|
+
backgroundColor,
|
|
4471
|
+
useCORS: true,
|
|
4472
|
+
logging: false
|
|
4473
|
+
});
|
|
4474
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
4475
|
+
const link = document.createElement("a");
|
|
4476
|
+
link.download = fileName;
|
|
4477
|
+
link.href = dataUrl;
|
|
4478
|
+
link.click();
|
|
4479
|
+
} finally {
|
|
4480
|
+
if (controls) controls.style.display = "flex";
|
|
4481
|
+
if (legendToggle) legendToggle.style.display = "flex";
|
|
4482
|
+
if (interactionLayer) interactionLayer.style.display = "block";
|
|
4483
|
+
if (legend && wasCollapsed) {
|
|
4484
|
+
legend.classList.add("pg-is-collapsed");
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4469
4489
|
// src/create-graph.ts
|
|
4470
4490
|
function createGraph(config) {
|
|
4471
4491
|
let cleanupResize = null;
|
|
4472
4492
|
let cleanupZoom = null;
|
|
4473
4493
|
let tooltipBinding = null;
|
|
4474
4494
|
let controls = null;
|
|
4495
|
+
let legendCleanup = null;
|
|
4496
|
+
let fitViewTimer = null;
|
|
4475
4497
|
let dimensions = { width: 0, height: 0 };
|
|
4476
4498
|
let rootGroup = null;
|
|
4499
|
+
let svgElement = null;
|
|
4477
4500
|
let zoomBehavior = null;
|
|
4478
4501
|
let simulation = null;
|
|
4479
4502
|
function render() {
|
|
4480
4503
|
destroy();
|
|
4481
4504
|
const layers = createGraphLayers(config.container);
|
|
4505
|
+
svgElement = layers.svg;
|
|
4482
4506
|
rootGroup = layers.root;
|
|
4483
|
-
cleanupResize = observeResize(
|
|
4484
|
-
|
|
4485
|
-
(width,
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
simulation
|
|
4491
|
-
simulation
|
|
4507
|
+
cleanupResize = observeResize(config.container, (width, height) => {
|
|
4508
|
+
dimensions = { width, height };
|
|
4509
|
+
layers.svg.setAttribute("width", String(width));
|
|
4510
|
+
layers.svg.setAttribute("height", String(height));
|
|
4511
|
+
layers.interactionRect.setAttribute("width", String(width));
|
|
4512
|
+
layers.interactionRect.setAttribute("height", String(height));
|
|
4513
|
+
if (simulation) {
|
|
4514
|
+
simulation.force("center", center_default(width / 2, height / 2));
|
|
4515
|
+
simulation.alpha(0.3).restart();
|
|
4492
4516
|
}
|
|
4493
|
-
|
|
4517
|
+
if (fitViewTimer) {
|
|
4518
|
+
clearTimeout(fitViewTimer);
|
|
4519
|
+
}
|
|
4520
|
+
fitViewTimer = setTimeout(() => {
|
|
4521
|
+
fitView();
|
|
4522
|
+
fitViewTimer = null;
|
|
4523
|
+
}, 150);
|
|
4524
|
+
});
|
|
4494
4525
|
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
|
-
*/
|
|
4526
|
+
svg: layers.svg,
|
|
4507
4527
|
interactionLayer: layers.interactionLayer,
|
|
4508
|
-
/**
|
|
4509
|
-
* Actual graph transform target
|
|
4510
|
-
*/
|
|
4511
4528
|
root: layers.root
|
|
4512
4529
|
});
|
|
4513
4530
|
zoomBehavior = zoomResult.behavior;
|
|
4514
4531
|
cleanupZoom = zoomResult.cleanup;
|
|
4515
4532
|
const root2 = select_default2(layers.root);
|
|
4516
|
-
const renderContext = { svg:
|
|
4533
|
+
const renderContext = { svg: layers.svg, root: root2, interaction: config.interaction };
|
|
4517
4534
|
const linkSelection = renderLinks(renderContext, config.links);
|
|
4518
4535
|
const linkLabelSelection = renderLinkLabels(renderContext, config.links);
|
|
4519
4536
|
const nodeSelection = renderNodes(renderContext, config.nodes);
|
|
@@ -4521,45 +4538,41 @@ function createGraph(config) {
|
|
|
4521
4538
|
const simulationConfig = {
|
|
4522
4539
|
nodes: config.nodes,
|
|
4523
4540
|
links: config.links,
|
|
4524
|
-
|
|
4525
|
-
|
|
4541
|
+
// Uses the observed dimensions to ensure physics are calculated on actual container size
|
|
4542
|
+
width: dimensions.width || config.container.clientWidth,
|
|
4543
|
+
height: dimensions.height || config.container.clientHeight
|
|
4526
4544
|
};
|
|
4527
4545
|
const simulationResult = createGraphSimulation(simulationConfig);
|
|
4528
4546
|
simulation = simulationResult.simulation;
|
|
4529
|
-
simulation.on(
|
|
4530
|
-
"
|
|
4531
|
-
() => {
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
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
|
-
);
|
|
4547
|
+
simulation.on("tick", () => {
|
|
4548
|
+
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);
|
|
4549
|
+
linkLabelSelection.attr("transform", (item) => {
|
|
4550
|
+
const link = item.link;
|
|
4551
|
+
const source = link.source;
|
|
4552
|
+
const targetPoint = getLinkTargetPoint(link);
|
|
4553
|
+
const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
|
|
4554
|
+
const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
|
|
4555
|
+
return `translate(${x3}, ${y3})`;
|
|
4556
|
+
}).each(function() {
|
|
4557
|
+
const group = this;
|
|
4558
|
+
const text = group.querySelector("text");
|
|
4559
|
+
const rect = group.querySelector("rect");
|
|
4560
|
+
if (!text || !rect) return;
|
|
4561
|
+
const bBox = text.getBBox();
|
|
4562
|
+
const padding = 6;
|
|
4563
|
+
rect.setAttribute("x", String(bBox.x - padding));
|
|
4564
|
+
rect.setAttribute("y", String(bBox.y - padding));
|
|
4565
|
+
rect.setAttribute("width", String(bBox.width + padding * 2));
|
|
4566
|
+
rect.setAttribute("height", String(bBox.height + padding * 2));
|
|
4567
|
+
});
|
|
4568
|
+
nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
|
|
4569
|
+
labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
|
|
4570
|
+
tooltipBinding?.reposition();
|
|
4571
|
+
});
|
|
4559
4572
|
if (config.interaction?.hover?.enabled) {
|
|
4560
4573
|
if (config.interaction?.hover?.tooltip?.enabled) {
|
|
4561
4574
|
tooltipBinding = bindNodeTooltip({
|
|
4562
|
-
container: config.container
|
|
4575
|
+
container: config.container,
|
|
4563
4576
|
selection: nodeSelection,
|
|
4564
4577
|
tooltipConfig: config.interaction.hover.tooltip
|
|
4565
4578
|
});
|
|
@@ -4569,66 +4582,53 @@ function createGraph(config) {
|
|
|
4569
4582
|
if (config.interaction?.drag?.enabled !== false) {
|
|
4570
4583
|
nodeSelection.call(createDragBehavior(simulation));
|
|
4571
4584
|
}
|
|
4572
|
-
if (config.interaction?.selection?.enabled) {
|
|
4573
|
-
}
|
|
4574
4585
|
if (config.controls?.enabled) {
|
|
4575
|
-
controls = createGraphControls(
|
|
4586
|
+
controls = createGraphControls(
|
|
4587
|
+
layers.overlay,
|
|
4588
|
+
{ zoomIn, zoomOut, resetView, fitView, destroy, render, exportGraph },
|
|
4589
|
+
config.controls
|
|
4590
|
+
);
|
|
4576
4591
|
controls.mount();
|
|
4577
4592
|
}
|
|
4593
|
+
if (config.legend?.enabled) {
|
|
4594
|
+
legendCleanup = createGraphLegend(layers.overlay, config.legend);
|
|
4595
|
+
}
|
|
4578
4596
|
}
|
|
4579
4597
|
function resetView() {
|
|
4580
|
-
if (!zoomBehavior)
|
|
4581
|
-
|
|
4582
|
-
}
|
|
4583
|
-
select_default2(config.container).transition().call(
|
|
4584
|
-
zoomBehavior.transform,
|
|
4585
|
-
identity2
|
|
4586
|
-
);
|
|
4598
|
+
if (!zoomBehavior || !svgElement) return;
|
|
4599
|
+
select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, identity2);
|
|
4587
4600
|
}
|
|
4588
4601
|
function fitView() {
|
|
4589
|
-
if (!zoomBehavior || !rootGroup || dimensions.width === 0 || dimensions.height === 0)
|
|
4590
|
-
return;
|
|
4591
|
-
}
|
|
4602
|
+
if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
|
|
4592
4603
|
const bounds = rootGroup.getBBox();
|
|
4593
|
-
if (bounds.width === 0 || bounds.height === 0)
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
const
|
|
4597
|
-
const
|
|
4598
|
-
|
|
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
|
-
);
|
|
4604
|
+
if (bounds.width === 0 || bounds.height === 0) return;
|
|
4605
|
+
const scale = Math.min(dimensions.width / bounds.width, dimensions.height / bounds.height) * 0.9;
|
|
4606
|
+
const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
|
|
4607
|
+
const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
|
|
4608
|
+
const transform2 = identity2.translate(translateX, translateY).scale(scale);
|
|
4609
|
+
select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, transform2);
|
|
4612
4610
|
}
|
|
4613
4611
|
function zoomIn() {
|
|
4614
|
-
if (!zoomBehavior)
|
|
4615
|
-
|
|
4616
|
-
}
|
|
4617
|
-
select_default2(config.container).transition().call(
|
|
4618
|
-
zoomBehavior.scaleBy,
|
|
4619
|
-
1.2
|
|
4620
|
-
);
|
|
4612
|
+
if (!zoomBehavior || !svgElement) return;
|
|
4613
|
+
select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
|
|
4621
4614
|
}
|
|
4622
4615
|
function zoomOut() {
|
|
4623
|
-
if (!zoomBehavior)
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4616
|
+
if (!zoomBehavior || !svgElement) return;
|
|
4617
|
+
select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 0.8);
|
|
4618
|
+
}
|
|
4619
|
+
async function exportGraph(fileName) {
|
|
4620
|
+
fitView();
|
|
4621
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4622
|
+
await captureAndDownloadGraph(config.container, {
|
|
4623
|
+
fileName,
|
|
4624
|
+
pixelRatio: 2
|
|
4625
|
+
});
|
|
4630
4626
|
}
|
|
4631
4627
|
function destroy() {
|
|
4628
|
+
if (fitViewTimer) {
|
|
4629
|
+
clearTimeout(fitViewTimer);
|
|
4630
|
+
fitViewTimer = null;
|
|
4631
|
+
}
|
|
4632
4632
|
if (cleanupResize) {
|
|
4633
4633
|
cleanupResize();
|
|
4634
4634
|
cleanupResize = null;
|
|
@@ -4649,13 +4649,18 @@ function createGraph(config) {
|
|
|
4649
4649
|
controls.destroy();
|
|
4650
4650
|
controls = null;
|
|
4651
4651
|
}
|
|
4652
|
+
if (legendCleanup) {
|
|
4653
|
+
legendCleanup();
|
|
4654
|
+
legendCleanup = null;
|
|
4655
|
+
}
|
|
4652
4656
|
rootGroup = null;
|
|
4657
|
+
svgElement = null;
|
|
4653
4658
|
zoomBehavior = null;
|
|
4654
4659
|
while (config.container.firstChild) {
|
|
4655
4660
|
config.container.removeChild(config.container.firstChild);
|
|
4656
4661
|
}
|
|
4657
4662
|
}
|
|
4658
|
-
return { render, zoomIn, zoomOut, resetView, fitView, destroy };
|
|
4663
|
+
return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph };
|
|
4659
4664
|
}
|
|
4660
4665
|
export {
|
|
4661
4666
|
createGraph
|