fix(d3/ui): implement Zoom-to-Fit and Bound-Constrained Simulation [issue #22]
This commit is contained in:
@@ -7,7 +7,7 @@ let simulation;
|
|||||||
let zoomBehavior;
|
let zoomBehavior;
|
||||||
let svgElement;
|
let svgElement;
|
||||||
|
|
||||||
let node, link, rootGroup, badge, width, height, currentDotNetHelper, resizeHandler;
|
let node, link, rootGroup, badge, width, height, currentDotNetHelper, resizeObserver;
|
||||||
|
|
||||||
export function mount(containerId, data, dotNetHelper) {
|
export function mount(containerId, data, dotNetHelper) {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
@@ -69,8 +69,13 @@ export function mount(containerId, data, dotNetHelper) {
|
|||||||
|
|
||||||
svgElement.call(zoomBehavior).on("wheel.zoom", null);
|
svgElement.call(zoomBehavior).on("wheel.zoom", null);
|
||||||
|
|
||||||
resizeHandler = () => handleResize(containerId);
|
// Use ResizeObserver for more reliable container size tracking
|
||||||
window.addEventListener('resize', resizeHandler);
|
resizeObserver = new ResizeObserver(entries => {
|
||||||
|
for (let entry of entries) {
|
||||||
|
handleResize(containerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resizeObserver.observe(container);
|
||||||
|
|
||||||
simulation = d3.forceSimulation()
|
simulation = d3.forceSimulation()
|
||||||
.force("link", d3.forceLink().id(d => d.id).distance(120))
|
.force("link", d3.forceLink().id(d => d.id).distance(120))
|
||||||
@@ -89,7 +94,14 @@ export function mount(containerId, data, dotNetHelper) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
node.attr("transform", d => {
|
||||||
|
// Keep within bounds with padding
|
||||||
|
const pillWidth = getPillWidth(d);
|
||||||
|
const halfWidth = pillWidth / 2;
|
||||||
|
d.x = Math.max(halfWidth + 20, Math.min(width - halfWidth - 20, d.x));
|
||||||
|
d.y = Math.max(35, Math.min(height - 35, d.y));
|
||||||
|
return `translate(${d.x},${d.y})`;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (badge && badge.style("display") !== "none") {
|
if (badge && badge.style("display") !== "none") {
|
||||||
@@ -205,6 +217,9 @@ export function updateData(data) {
|
|||||||
simulation.nodes(data.nodes);
|
simulation.nodes(data.nodes);
|
||||||
simulation.force("link").links(data.links);
|
simulation.force("link").links(data.links);
|
||||||
simulation.alpha(0.5).restart();
|
simulation.alpha(0.5).restart();
|
||||||
|
|
||||||
|
// Trigger zoom to fit after a short delay to allow simulation to settle
|
||||||
|
setTimeout(zoomToFit, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drag(simulation) {
|
function drag(simulation) {
|
||||||
@@ -280,8 +295,8 @@ export function unmount(containerId) {
|
|||||||
if (simulation) {
|
if (simulation) {
|
||||||
simulation.stop();
|
simulation.stop();
|
||||||
}
|
}
|
||||||
if (resizeHandler) {
|
if (resizeObserver) {
|
||||||
window.removeEventListener('resize', resizeHandler);
|
resizeObserver.disconnect();
|
||||||
}
|
}
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
if (container) {
|
if (container) {
|
||||||
@@ -327,9 +342,43 @@ export function zoomOut() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function zoomReset() {
|
export function zoomReset() {
|
||||||
if (svgElement && zoomBehavior) {
|
zoomToFit();
|
||||||
svgElement.transition().duration(500).call(zoomBehavior.transform, d3.zoomIdentity);
|
}
|
||||||
}
|
|
||||||
|
export function zoomToFit() {
|
||||||
|
if (!node || node.empty() || !svgElement || !zoomBehavior) return;
|
||||||
|
|
||||||
|
// Get the actual bounding box of the nodes
|
||||||
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||||
|
node.each(d => {
|
||||||
|
const pw = getPillWidth(d) / 2;
|
||||||
|
minX = Math.min(minX, d.x - pw);
|
||||||
|
maxX = Math.max(maxX, d.x + pw);
|
||||||
|
minY = Math.min(minY, d.y - 15);
|
||||||
|
maxY = Math.max(maxY, d.y + 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (minX === Infinity) return;
|
||||||
|
|
||||||
|
const graphWidth = maxX - minX;
|
||||||
|
const graphHeight = maxY - minY;
|
||||||
|
const midX = (minX + maxX) / 2;
|
||||||
|
const midY = (minY + maxY) / 2;
|
||||||
|
|
||||||
|
const padding = 60;
|
||||||
|
const scale = Math.min(
|
||||||
|
(width - padding) / graphWidth,
|
||||||
|
(height - padding) / graphHeight,
|
||||||
|
1.2 // Max scale
|
||||||
|
);
|
||||||
|
|
||||||
|
svgElement.transition().duration(750).call(
|
||||||
|
zoomBehavior.transform,
|
||||||
|
d3.zoomIdentity
|
||||||
|
.translate(width / 2, height / 2)
|
||||||
|
.scale(scale)
|
||||||
|
.translate(-midX, -midY)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clear() {
|
export function clear() {
|
||||||
|
|||||||
Reference in New Issue
Block a user