fix(#2): stabilize diagram layout and tooltip behavior

This commit is contained in:
Josh Creek
2026-02-22 13:53:55 +00:00
parent defa4d4c20
commit c9e7882e27
2 changed files with 73 additions and 13 deletions
+1 -1
View File
@@ -57,7 +57,7 @@
} }
.modal-container { .modal-container {
width: min(100%, 680px); width: 100%;
max-height: 90vh; max-height: 90vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+67 -7
View File
@@ -14,14 +14,14 @@
GraphTransformResult GraphTransformResult
} from '../graph/types'; } from '../graph/types';
cytoscape.use(dagre);
export let jsonPath = '/data/network.json'; export let jsonPath = '/data/network.json';
type CameraMode = 'readable' | 'overview'; type CameraMode = 'readable' | 'overview';
type Position = { x: number; y: number }; type Position = { x: number; y: number };
let container: HTMLDivElement; let container: HTMLDivElement;
let diagramStage: HTMLDivElement;
let tooltipElement: HTMLDivElement | null = null;
let cy: cytoscape.Core | null = null; let cy: cytoscape.Core | null = null;
let warnings: string[] = []; let warnings: string[] = [];
let selectedDetails: GraphNodeDetails | null = null; let selectedDetails: GraphNodeDetails | null = null;
@@ -46,6 +46,16 @@
y: 0 y: 0
}; };
const tooltipOffset = {
x: 44,
y: -8
};
const fallbackTooltipSize = {
width: 260,
height: 32
};
const tooltipViewportPadding = 8;
function closeDetails() { function closeDetails() {
selectedDetails = null; selectedDetails = null;
selectedNodeId = null; selectedNodeId = null;
@@ -58,6 +68,39 @@
return `${connection.device}:${connection.port}`; return `${connection.device}:${connection.port}`;
} }
function getTooltipSize(): { width: number; height: number } {
if (tooltipElement) {
const rect = tooltipElement.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
return { width: rect.width, height: rect.height };
}
}
return fallbackTooltipSize;
}
function clampTooltipPosition(rawX: number, rawY: number): Position {
const stageRect = diagramStage.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const { width: tooltipWidth, height: tooltipHeight } = getTooltipSize();
const rawViewportX = containerRect.left + rawX;
const rawViewportY = containerRect.top + rawY;
const minViewportX = stageRect.left + tooltipViewportPadding;
const maxViewportX = stageRect.right - tooltipWidth - tooltipViewportPadding;
const minViewportY = stageRect.top + tooltipViewportPadding;
const maxViewportY = stageRect.bottom - tooltipHeight - tooltipViewportPadding;
const clampedViewportX = Math.max(minViewportX, Math.min(rawViewportX, maxViewportX));
const clampedViewportY = Math.max(minViewportY, Math.min(rawViewportY, maxViewportY));
return {
x: clampedViewportX - stageRect.left + diagramStage.scrollLeft,
y: clampedViewportY - stageRect.top + diagramStage.scrollTop
};
}
function isHostCollapsed(hostId: string): boolean { function isHostCollapsed(hostId: string): boolean {
return collapsedHosts[hostId] === true; return collapsedHosts[hostId] === true;
} }
@@ -892,8 +935,10 @@
onMount(() => { onMount(() => {
let resizeHandler: (() => void) | null = null; let resizeHandler: (() => void) | null = null;
let resizeTimeoutId: ReturnType<typeof setTimeout> | null = null;
const initialize = async () => { const initialize = async () => {
cytoscape.use(dagre);
cy = cytoscape({ cy = cytoscape({
container, container,
elements: [], elements: [],
@@ -1022,11 +1067,15 @@
const node = event.target; const node = event.target;
const renderedPosition = node.renderedPosition(); const renderedPosition = node.renderedPosition();
const data = node.data() as GraphNodeData; const data = node.data() as GraphNodeData;
const nextText = data.rawName ?? data.label;
const rawX = renderedPosition.x + tooltipOffset.x;
const rawY = renderedPosition.y + tooltipOffset.y;
const clampedPosition = clampTooltipPosition(rawX, rawY);
tooltip = { tooltip = {
visible: true, visible: true,
text: data.rawName ?? data.label, text: nextText,
x: renderedPosition.x + 44, x: clampedPosition.x,
y: renderedPosition.y - 8 y: clampedPosition.y
}; };
}); });
@@ -1039,12 +1088,19 @@
}); });
resizeHandler = () => { resizeHandler = () => {
if (resizeTimeoutId !== null) {
clearTimeout(resizeTimeoutId);
}
resizeTimeoutId = setTimeout(() => {
if (!cy) { if (!cy) {
return; return;
} }
runAdaptiveLayout(cy, cameraMode); runAdaptiveLayout(cy, cameraMode);
applyPhysicalEdgeDeconfliction(cy); applyPhysicalEdgeDeconfliction(cy);
applyEthernetLabelVisibility(cy, showEthernetLabels); applyEthernetLabelVisibility(cy, showEthernetLabels);
resizeTimeoutId = null;
}, 100);
}; };
window.addEventListener('resize', resizeHandler); window.addEventListener('resize', resizeHandler);
}; };
@@ -1055,6 +1111,10 @@
if (resizeHandler) { if (resizeHandler) {
window.removeEventListener('resize', resizeHandler); window.removeEventListener('resize', resizeHandler);
} }
if (resizeTimeoutId !== null) {
clearTimeout(resizeTimeoutId);
resizeTimeoutId = null;
}
if (cy) { if (cy) {
cy.destroy(); cy.destroy();
cy = null; cy = null;
@@ -1064,7 +1124,7 @@
</script> </script>
<div class="diagram-shell"> <div class="diagram-shell">
<div class="diagram-stage"> <div class="diagram-stage" bind:this={diagramStage}>
<div class="diagram-canvas" bind:this={container}></div> <div class="diagram-canvas" bind:this={container}></div>
<div class="map-controls"> <div class="map-controls">
@@ -1112,7 +1172,7 @@
{/if} {/if}
{#if tooltip.visible} {#if tooltip.visible}
<div class="tooltip" style={`left: ${tooltip.x}px; top: ${tooltip.y}px;`}> <div class="tooltip" bind:this={tooltipElement} style={`left: ${tooltip.x}px; top: ${tooltip.y}px;`}>
{tooltip.text} {tooltip.text}
</div> </div>
{/if} {/if}