mirror of
https://github.com/jcreek/OpenNetworkDiagram.git
synced 2026-07-13 19:13:43 +00:00
fix(#2): stabilize diagram layout and tooltip behavior
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user