From 7ac38e1e580d31ec7aa79396d4561160384b43af Mon Sep 17 00:00:00 2001 From: Ayoub LABIDI Date: Thu, 11 Jun 2026 09:27:01 +0200 Subject: [PATCH 1/4] Add per-element thresholds to adaptive zoom Signed-off-by: Ayoub LABIDI --- demo/index.html | 4 + demo/src/nad.ts | 53 +++++--- .../nad-viewer-parameters.ts | 51 +++++--- .../network-area-diagram-viewer.ts | 113 ++++++++++++------ 4 files changed, 150 insertions(+), 71 deletions(-) diff --git a/demo/index.html b/demo/index.html index 39f8f796..36b07fb9 100644 --- a/demo/index.html +++ b/demo/index.html @@ -86,6 +86,10 @@

Network Area Diagram Viewers

Edge info with middle arrows
+
+
Edge info with adaptive zoom thresholds
+
+
Edge info with limit percentage and blinking display
diff --git a/demo/src/nad.ts b/demo/src/nad.ts index 33b65b0c..a645f7e9 100644 --- a/demo/src/nad.ts +++ b/demo/src/nad.ts @@ -262,8 +262,7 @@ const addNadToDemo = () => { onToggleHoverCallback: handleToggleNadHover, onRightClickCallback: handleRightClick, onBendLineCallback: handleLineBending, - enableAdaptiveTextZoom: true, - adaptiveTextZoomThreshold: 1500, + adaptiveTextZoom: { enabled: true, threshold: 1500 }, }; new NetworkAreaDiagramViewer( document.getElementById('svg-container-nad-pst-hvdc-multiple-labels')!, @@ -379,8 +378,10 @@ const addNadToDemo = () => { onRightClickCallback: handleRightClick, onBendLineCallback: handleLineBending, - enableAdaptiveTextZoom: true, - adaptiveTextZoomThreshold: 3000, + adaptiveTextZoom: { + enabled: true, + threshold: 3000, + }, }; const svgContainerNadPegase = document.getElementById('svg-container-nad-pegase-network-adaptive-zoom'); new NetworkAreaDiagramViewer( @@ -404,8 +405,7 @@ const addNadToDemo = () => { onRightClickCallback: handleRightClick, onBendLineCallback: handleLineBending, - enableAdaptiveTextZoom: true, - adaptiveTextZoomThreshold: 850, + adaptiveTextZoom: { enabled: true, threshold: 850 }, }; new NetworkAreaDiagramViewer( document.getElementById('svg-container-nad-partial-network-adaptive-zoom')!, @@ -428,8 +428,7 @@ const addNadToDemo = () => { onRightClickCallback: handleRightClick, onBendLineCallback: handleLineBending, - enableAdaptiveTextZoom: true, - adaptiveTextZoomThreshold: 850, + adaptiveTextZoom: { enabled: true, threshold: 850 }, }; new NetworkAreaDiagramViewer( document.getElementById('svg-container-nad-multibus-vlnodes-middle-arrow')!, @@ -439,6 +438,35 @@ const addNadToDemo = () => { ); }); + fetch(NadSvgMultibusVLNodesMiddleArrowExample) + .then((response) => response.text()) + .then((svgContent) => { + const nadViewerParametersOptions: NadViewerParametersOptions = { + enableDragInteraction: true, + addButtons: true, + onMoveNodeCallback: handleNodeMove, + onMoveTextNodeCallback: handleTextNodeMove, + onSelectNodeCallback: handleNodeSelect, + onToggleHoverCallback: handleToggleNadHover, + onRightClickCallback: handleRightClick, + onBendLineCallback: handleLineBending, + + adaptiveTextZoom: { + enabled: true, + edgeSideLabelThreshold: 1000, + edgeMiddleArrowThreshold: 2000, + edgeMiddleLabelThreshold: 1500, + threshold: 2500, + }, + }; + new NetworkAreaDiagramViewer( + document.getElementById('svg-container-nad-multibus-vlnodes-adaptive-thresholds')!, + svgContent, + NadSvgMultibusVLNodesMiddleArrowExampleMeta, + nadViewerParametersOptions + ); + }); + fetch(NadSvgMultibusVLNodesLimitPercentageExample) .then((response) => response.text()) .then((svgContent) => { @@ -452,8 +480,7 @@ const addNadToDemo = () => { onRightClickCallback: handleRightClick, onBendLineCallback: handleLineBending, - enableAdaptiveTextZoom: true, - adaptiveTextZoomThreshold: 3000, + adaptiveTextZoom: { enabled: true, threshold: 3000 }, }; const nadViewer = new NetworkAreaDiagramViewer( document.getElementById('svg-container-nad-multibus-vlnodes-limit-percentage')!, @@ -609,8 +636,7 @@ const addNadToDemo = () => { onToggleHoverCallback: handleToggleNadHover, onRightClickCallback: handleRightClick, onBendLineCallback: handleLineBending, - enableAdaptiveTextZoom: true, - adaptiveTextZoomThreshold: 1100, + adaptiveTextZoom: { enabled: true, threshold: 1100 }, }; new NetworkAreaDiagramViewer( document.getElementById('svg-container-nad-double-arrows')!, @@ -632,8 +658,7 @@ const addNadToDemo = () => { onToggleHoverCallback: handleToggleNadHover, onRightClickCallback: handleRightClick, onBendLineCallback: handleLineBending, - enableAdaptiveTextZoom: true, - adaptiveTextZoomThreshold: 1100, + adaptiveTextZoom: { enabled: true, threshold: 1100 }, }; new NetworkAreaDiagramViewer( document.getElementById('svg-container-nad-components')!, diff --git a/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts b/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts index 10f94403..55e913c9 100644 --- a/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts +++ b/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts @@ -54,6 +54,23 @@ export type OnBendLineCallbackType = ( lineOperation: string ) => void; +export interface AdaptiveTextZoomOptions { + // Whether adaptive zoom is enabled. + enabled?: boolean; + + // Threshold for the adaptive zoom (legends, edge infos and text nodes wholesale removal). + threshold?: number; + + // Threshold for edge side labels (edgeInfo1 / edgeInfo2). + edgeSideLabelThreshold?: number; + + // Threshold for edge middle labels (edgeInfoMiddle). + edgeMiddleLabelThreshold?: number; + + // Threshold for the edge middle arrow (edgeInfoMiddle arrow only). + edgeMiddleArrowThreshold?: number; +} + export interface NadViewerParametersOptions { // The minimum width of the viewer. minWidth?: number; @@ -104,15 +121,12 @@ export interface NadViewerParametersOptions { // Size in pixel of the margin that is added to hoverable objects to help the user stay over them. hoverPositionPrecision?: number | null; - // Whether enabling adaptive zoom, to improve the performnces of the viewer with large networks. - // If enabled, and the viewbox's zoom level is above a threshold, edge infos and legends are removed + // Adaptive zoom options, to improve the performances of the viewer with large networks. + // If enabled, and the viewbox's zoom level is above the threshold, edge infos and legends are removed // from the SVG, to speed-up panning and zooming. - // When the zoom level is below a threshold, edge infos and legends for the NAD elements that are + // When the zoom level is below the threshold, edge infos and legends for the NAD elements that are // inside the viewbox are created in the SVG, on the fly, from the NAD metadata. - enableAdaptiveTextZoom?: boolean; - - // Threshold for the adaptiveZoom. - adaptiveTextZoomThreshold?: number; + adaptiveTextZoom?: AdaptiveTextZoomOptions; // Custom component library to use instead of the default one. // If not provided, the default library is used. @@ -199,17 +213,18 @@ export class NadViewerParameters { ); } - public getEnableAdaptiveTextZoom(): boolean { - return ( - this.nadViewerParametersOptions?.enableAdaptiveTextZoom ?? NadViewerParameters.ENABLE_ADAPTIVE_ZOOM_DEFAULT - ); - } - - public getThresholdAdaptiveTextZoom(): number { - return ( - this.nadViewerParametersOptions?.adaptiveTextZoomThreshold ?? - NadViewerParameters.THRESHOLD_ADAPTIVE_ZOOM_DEFAULT - ); + public getAdaptiveTextZoom(): Required { + const adaptiveTextZoom = this.nadViewerParametersOptions?.adaptiveTextZoom; + return { + enabled: adaptiveTextZoom?.enabled ?? NadViewerParameters.ENABLE_ADAPTIVE_ZOOM_DEFAULT, + threshold: adaptiveTextZoom?.threshold ?? NadViewerParameters.THRESHOLD_ADAPTIVE_ZOOM_DEFAULT, + edgeSideLabelThreshold: + adaptiveTextZoom?.edgeSideLabelThreshold ?? NadViewerParameters.THRESHOLD_ADAPTIVE_ZOOM_DEFAULT, + edgeMiddleLabelThreshold: + adaptiveTextZoom?.edgeMiddleLabelThreshold ?? NadViewerParameters.THRESHOLD_ADAPTIVE_ZOOM_DEFAULT, + edgeMiddleArrowThreshold: + adaptiveTextZoom?.edgeMiddleArrowThreshold ?? NadViewerParameters.THRESHOLD_ADAPTIVE_ZOOM_DEFAULT, + }; } public getComponentLibrary(): LibraryComponent[] | undefined { diff --git a/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts b/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts index 7fd61769..9451a813 100644 --- a/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts +++ b/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts @@ -419,7 +419,7 @@ export class NetworkAreaDiagramViewer { this.svgDraw.on('panEnd', () => { this.detachCursorOverlay(); //if the adaptive zoom feature is enabled, updates the diagram - if (this.nadViewerParameters.getEnableAdaptiveTextZoom()) { + if (this.nadViewerParameters.getAdaptiveTextZoom().enabled) { this.adaptiveZoomViewboxUpdate(this.getCurrentlyMaxDisplayedSize()); } }); @@ -436,7 +436,10 @@ export class NetworkAreaDiagramViewer { firstChild.removeAttribute('width'); firstChild.removeAttribute('height'); - if (this.nadViewerParameters.getEnableLevelOfDetail() || this.nadViewerParameters.getEnableAdaptiveTextZoom()) { + if ( + this.nadViewerParameters.getEnableLevelOfDetail() || + this.nadViewerParameters.getAdaptiveTextZoom().enabled + ) { this.svgDraw.fire('zoom'); // Forces a new dynamic zoom check to correctly update the dynamic CSS // We add an observer to track when the SVG's viewBox is updated by panzoom @@ -1418,7 +1421,7 @@ export class NetworkAreaDiagramViewer { let style: string | undefined = 'text-anchor:middle'; let i = 1; if (bothLabels) { - const labelBElement = edgeInfo.querySelector('text:nth-of-type(' + i++ + ')') as SVGGraphicsElement; + const labelBElement = edgeInfo.querySelector('text:nth-of-type(' + i++ + ')'); const middleLabelBData = HalfEdgeUtils.getMiddleLabelData( halfEdge1, halfEdge2, @@ -1427,12 +1430,14 @@ export class NetworkAreaDiagramViewer { ); x = DiagramUtils.getFormattedValue(middleLabelBData[0] * factor); style = middleLabelBData[1]; - labelBElement.setAttribute('transform', 'rotate(' + DiagramUtils.getFormattedValue(infoAngle) + ')'); - labelBElement.setAttribute('x', x); - if (style) { - labelBElement.setAttribute('style', style); - } else if (labelBElement.hasAttribute('style')) { - labelBElement.removeAttribute('style'); + if (labelBElement) { + labelBElement.setAttribute('transform', 'rotate(' + DiagramUtils.getFormattedValue(infoAngle) + ')'); + labelBElement.setAttribute('x', x); + if (style) { + labelBElement.setAttribute('style', style); + } else if (labelBElement.hasAttribute('style')) { + labelBElement.removeAttribute('style'); + } } const middleLabelAData = HalfEdgeUtils.getMiddleLabelData( @@ -1445,13 +1450,15 @@ export class NetworkAreaDiagramViewer { style = middleLabelAData[1]; } - const labelAElement = edgeInfo.querySelector('text:nth-of-type(' + i + ')') as SVGGraphicsElement; - labelAElement.setAttribute('transform', 'rotate(' + DiagramUtils.getFormattedValue(infoAngle) + ')'); - labelAElement.setAttribute('x', x); - if (style) { - labelAElement.setAttribute('style', style); - } else if (labelAElement.hasAttribute('style')) { - labelAElement.removeAttribute('style'); + const labelAElement = edgeInfo.querySelector('text:nth-of-type(' + i + ')'); + if (labelAElement) { + labelAElement.setAttribute('transform', 'rotate(' + DiagramUtils.getFormattedValue(infoAngle) + ')'); + labelAElement.setAttribute('x', x); + if (style) { + labelAElement.setAttribute('style', style); + } else if (labelAElement.hasAttribute('style')) { + labelAElement.removeAttribute('style'); + } } } @@ -1753,7 +1760,7 @@ export class NetworkAreaDiagramViewer { } this.setPreviousMaxDisplayedSize(maxDisplayedSize); - if (this.nadViewerParameters.getEnableAdaptiveTextZoom()) { + if (this.nadViewerParameters.getAdaptiveTextZoom().enabled) { this.adaptiveZoomViewboxUpdate(maxDisplayedSize); } @@ -1958,10 +1965,11 @@ export class NetworkAreaDiagramViewer { return halfEdges; } - private createEdgeInfos(edge: EdgeMetadata): void { + private createEdgeInfos(edge: EdgeMetadata, maxDisplayedSize: number): void { const halfEdges = this.getHalfEdgesForEdgeInfos(edge); + const adaptiveTextZoom = this.nadViewerParameters.getAdaptiveTextZoom(); - if (edge.edgeInfo1 && halfEdges[0]) { + if (edge.edgeInfo1 && halfEdges[0] && maxDisplayedSize <= adaptiveTextZoom.edgeSideLabelThreshold) { const edgeValue1 = Number(edge.edgeInfo1?.labelB); this.setBranchSideLabel( edge, @@ -1973,7 +1981,7 @@ export class NetworkAreaDiagramViewer { ); } - if (edge.edgeInfo2 && halfEdges[1]) { + if (edge.edgeInfo2 && halfEdges[1] && maxDisplayedSize <= adaptiveTextZoom.edgeSideLabelThreshold) { const edgeValue2 = Number(edge.edgeInfo2?.labelB); this.setBranchSideLabel( edge, @@ -1985,19 +1993,25 @@ export class NetworkAreaDiagramViewer { ); } - if (edge.edgeInfoMiddle) { - this.setBranchMiddleLabel(edge, halfEdges[0], halfEdges[1], edge.edgeInfoMiddle); + if ( + edge.edgeInfoMiddle && + maxDisplayedSize <= + Math.max(adaptiveTextZoom.edgeMiddleLabelThreshold, adaptiveTextZoom.edgeMiddleArrowThreshold) + ) { + const showArrow = maxDisplayedSize <= adaptiveTextZoom.edgeMiddleArrowThreshold; + const showLabel = maxDisplayedSize <= adaptiveTextZoom.edgeMiddleLabelThreshold; + this.setBranchMiddleLabel(edge, halfEdges[0], halfEdges[1], edge.edgeInfoMiddle, showArrow, showLabel); } } - private createEdgesInfos(edges: EdgeMetadata[]): void { + private createEdgesInfos(edges: EdgeMetadata[], maxDisplayedSize: number): void { for (const edge of edges) { if ( (edge.edgeInfo1 && !this.hasEdgeInfo(edge.edgeInfo1)) || (edge.edgeInfo2 && !this.hasEdgeInfo(edge.edgeInfo2)) || (edge.edgeInfoMiddle && !this.hasEdgeInfo(edge.edgeInfoMiddle)) ) { - this.createEdgeInfos(edge); + this.createEdgeInfos(edge, maxDisplayedSize); } } } @@ -2053,7 +2067,8 @@ export class NetworkAreaDiagramViewer { } private adaptiveZoomViewboxUpdate(maxDisplayedSize: number) { - if (maxDisplayedSize > this.nadViewerParameters.getThresholdAdaptiveTextZoom()) { + const adaptiveTextZoom = this.nadViewerParameters.getAdaptiveTextZoom(); + if (maxDisplayedSize > adaptiveTextZoom.threshold) { this.edgeInfosSection?.replaceChildren(); this.textEdgesSection?.replaceChildren(); this.textNodesSection?.replaceChildren(); @@ -2076,6 +2091,22 @@ export class NetworkAreaDiagramViewer { console.log(`time to remove elements not in the current viewbox: ${performance.now() - start} ms`); + if (maxDisplayedSize > adaptiveTextZoom.edgeSideLabelThreshold) { + for (const edge of containedEdgeList) { + if (edge.edgeInfo1) { + this.getEdgeInfo(edge.edgeInfo1.svgId)?.remove(); + } + if (edge.edgeInfo2) { + this.getEdgeInfo(edge.edgeInfo2.svgId)?.remove(); + } + } + } + for (const edge of containedEdgeList) { + if (edge.edgeInfoMiddle) { + this.getEdgeInfo(edge.edgeInfoMiddle.svgId)?.remove(); + } + } + start = performance.now(); for (const node of containedNodeList) { const textNode = this.diagramMetadata?.textNodes.find((tNode) => tNode.svgId === node.legendSvgId); @@ -2090,7 +2121,7 @@ export class NetworkAreaDiagramViewer { console.log(`adaptive zoom mode adding legends elements time: ${performance.now() - start} ms`); start = performance.now(); - this.createEdgesInfos(containedEdgeList); + this.createEdgesInfos(containedEdgeList, maxDisplayedSize); console.log(`adaptive zoom mode adding edges info elements time: ${performance.now() - start} ms`); } } @@ -2342,7 +2373,9 @@ export class NetworkAreaDiagramViewer { edge: EdgeMetadata, halfEdge1: HalfEdge | null, halfEdge2: HalfEdge | null, - edgeInfoMetadata: EdgeInfoMetadata | undefined + edgeInfoMetadata: EdgeInfoMetadata | undefined, + showArrow: boolean = true, + showLabel: boolean = true ) { if (!halfEdge1 && !halfEdge2) { return; @@ -2360,7 +2393,7 @@ export class NetworkAreaDiagramViewer { if (edgeInfoMetadata.componentType) { this.addBranchComponentElement(edgeInfo, edgeInfoMetadata.componentType); - } else { + } else if (showArrow) { if (edgeInfoMetadata.direction || edgeInfoMetadata.directionB) { this.addBranchArrowElement( edgeInfo, @@ -2375,17 +2408,19 @@ export class NetworkAreaDiagramViewer { } } - let i = 1; - if (edgeInfoMetadata.labelA && edgeInfoMetadata.labelB) { - this.addBranchLabelElement(edgeInfo, i++, edgeInfoMetadata.infoTypeB, edgeInfoMetadata.labelB); - } + if (showLabel) { + let i = 1; + if (edgeInfoMetadata.labelA && edgeInfoMetadata.labelB) { + this.addBranchLabelElement(edgeInfo, i++, edgeInfoMetadata.infoTypeB, edgeInfoMetadata.labelB); + } - this.addBranchLabelElement( - edgeInfo, - i, - edgeInfoMetadata.infoTypeA ?? edgeInfoMetadata.infoTypeB, - edgeInfoMetadata.labelA ?? edgeInfoMetadata.labelB - ); + this.addBranchLabelElement( + edgeInfo, + i, + edgeInfoMetadata.infoTypeA ?? edgeInfoMetadata.infoTypeB, + edgeInfoMetadata.labelA ?? edgeInfoMetadata.labelB + ); + } this.redrawMiddleEdgeArrowAndLabels( halfEdge1, @@ -2393,7 +2428,7 @@ export class NetworkAreaDiagramViewer { edgeInfo, edgeInfoMetadata.direction ?? edgeInfoMetadata.directionB, edgeInfoMetadata.directionA, - edgeInfoMetadata.labelA !== undefined && edgeInfoMetadata.labelB !== undefined + showLabel && edgeInfoMetadata.labelA !== undefined && edgeInfoMetadata.labelB !== undefined ); } From bacbd0bf6ee75abecbd4da05b691b6863bab84aa Mon Sep 17 00:00:00 2001 From: Ayoub LABIDI Date: Thu, 11 Jun 2026 11:44:30 +0200 Subject: [PATCH 2/4] fix sonar issues and refactor code Signed-off-by: Ayoub LABIDI --- .../network-area-diagram-viewer.ts | 157 +++++++++--------- 1 file changed, 83 insertions(+), 74 deletions(-) diff --git a/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts b/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts index 9451a813..90c3a0fc 100644 --- a/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts +++ b/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts @@ -21,6 +21,7 @@ import { } from './diagram-metadata'; import debounce from 'lodash.debounce'; import { + AdaptiveTextZoomOptions, NadViewerParameters, NadViewerParametersOptions, OnBendLineCallbackType, @@ -1417,28 +1418,18 @@ export class NetworkAreaDiagramViewer { factor = arrowsNum == 2 ? this.svgParameters.getDoubleArrowShiftFactorText() : 1; } - let x = '0.0'; + let shift = 0; let style: string | undefined = 'text-anchor:middle'; let i = 1; if (bothLabels) { - const labelBElement = edgeInfo.querySelector('text:nth-of-type(' + i++ + ')'); + const labelBElement = edgeInfo.querySelector('text:nth-of-type(' + i++ + ')') as SVGGraphicsElement; const middleLabelBData = HalfEdgeUtils.getMiddleLabelData( halfEdge1, halfEdge2, true, this.svgParameters.getArrowLabelShift() ); - x = DiagramUtils.getFormattedValue(middleLabelBData[0] * factor); - style = middleLabelBData[1]; - if (labelBElement) { - labelBElement.setAttribute('transform', 'rotate(' + DiagramUtils.getFormattedValue(infoAngle) + ')'); - labelBElement.setAttribute('x', x); - if (style) { - labelBElement.setAttribute('style', style); - } else if (labelBElement.hasAttribute('style')) { - labelBElement.removeAttribute('style'); - } - } + this.redrawLabel(labelBElement, infoAngle, middleLabelBData[0] * factor, middleLabelBData[1]); const middleLabelAData = HalfEdgeUtils.getMiddleLabelData( halfEdge1, @@ -1446,20 +1437,12 @@ export class NetworkAreaDiagramViewer { false, this.svgParameters.getArrowLabelShift() ); - x = DiagramUtils.getFormattedValue(middleLabelAData[0] * factor); + shift = middleLabelAData[0]; style = middleLabelAData[1]; } - const labelAElement = edgeInfo.querySelector('text:nth-of-type(' + i + ')'); - if (labelAElement) { - labelAElement.setAttribute('transform', 'rotate(' + DiagramUtils.getFormattedValue(infoAngle) + ')'); - labelAElement.setAttribute('x', x); - if (style) { - labelAElement.setAttribute('style', style); - } else if (labelAElement.hasAttribute('style')) { - labelAElement.removeAttribute('style'); - } - } + const labelAElement = edgeInfo.querySelector('text:nth-of-type(' + i + ')') as SVGGraphicsElement; + this.redrawLabel(labelAElement, infoAngle, shift * factor, style); } private redrawTransformer( @@ -2040,7 +2023,7 @@ export class NetworkAreaDiagramViewer { } } - private filterElements(nodeList: NodeMetadata[], viewBox: ViewBox | undefined): void { + private filterLegends(nodeList: NodeMetadata[]): void { const validLegendIds = new Set(nodeList.map((n) => n.legendSvgId)); const validLegendEdgeIds = new Set(nodeList.map((n) => n.legendEdgeSvgId)); @@ -2061,69 +2044,95 @@ export class NetworkAreaDiagramViewer { polyline.remove(); } }); + } - // filter edge info items that fall outside the viewbox + // filter edge info items that fall outside the viewbox + private filterEdgeInfos( + edges: EdgeMetadata[], + viewBox: ViewBox | undefined, + maxDisplayedSize: number, + adaptiveTextZoom: Required + ): void { this.removeEdgeInfoItems(viewBox); + + if (maxDisplayedSize > adaptiveTextZoom.edgeSideLabelThreshold) { + for (const edge of edges) { + if (edge.edgeInfo1) { + this.getEdgeInfo(edge.edgeInfo1.svgId)?.remove(); + } + if (edge.edgeInfo2) { + this.getEdgeInfo(edge.edgeInfo2.svgId)?.remove(); + } + } + } + for (const edge of edges) { + if (edge.edgeInfoMiddle) { + this.getEdgeInfo(edge.edgeInfoMiddle.svgId)?.remove(); + } + } } - private adaptiveZoomViewboxUpdate(maxDisplayedSize: number) { - const adaptiveTextZoom = this.nadViewerParameters.getAdaptiveTextZoom(); + private updateAdaptiveEdgeInfos( + edges: EdgeMetadata[], + viewBox: ViewBox | undefined, + maxDisplayedSize: number, + adaptiveTextZoom: Required + ): void { + this.filterEdgeInfos(edges, viewBox, maxDisplayedSize, adaptiveTextZoom); + this.createEdgesInfos(edges, maxDisplayedSize); + } + + private updateAdaptiveLegends( + nodeList: NodeMetadata[], + maxDisplayedSize: number, + adaptiveTextZoom: Required + ): void { if (maxDisplayedSize > adaptiveTextZoom.threshold) { - this.edgeInfosSection?.replaceChildren(); - this.textEdgesSection?.replaceChildren(); this.textNodesSection?.replaceChildren(); - } else { - let start = performance.now(); - const containerRect = this.container.getBoundingClientRect(); - const viewBox = SvgUtils.computeVisibleArea(this.getViewBox(), containerRect.width, containerRect.height); - - const containedElementList = this.getElementsInViewbox(viewBox, 50); - const containedNodeList = containedElementList.nodes; - const containedEdgeList = containedElementList.edges; + this.textEdgesSection?.replaceChildren(); + return; + } - console.log('number of nodes in the current viewbox: ' + containedNodeList.length); - console.log('number of edges in the current viewbox: ' + containedEdgeList.length); - console.log(`number of elements in the current viewbox computing time: ${performance.now() - start} ms`); + this.filterLegends(nodeList); - start = performance.now(); + for (const node of nodeList) { + const textNode = this.diagramMetadata?.textNodes.find((tNode) => tNode.svgId === node.legendSvgId); + if (textNode) { + const busNodes: BusNodeMetadata[] = + this.diagramMetadata?.busNodes.filter((busNode) => busNode.vlNode == node.svgId) ?? []; - this.filterElements(containedNodeList, viewBox); + this.createLegendBox(textNode, busNodes, node); + this.createLegendEdge(textNode, busNodes, node); + } + } + } - console.log(`time to remove elements not in the current viewbox: ${performance.now() - start} ms`); + private adaptiveZoomViewboxUpdate(maxDisplayedSize: number) { + const adaptiveTextZoom = this.nadViewerParameters.getAdaptiveTextZoom(); - if (maxDisplayedSize > adaptiveTextZoom.edgeSideLabelThreshold) { - for (const edge of containedEdgeList) { - if (edge.edgeInfo1) { - this.getEdgeInfo(edge.edgeInfo1.svgId)?.remove(); - } - if (edge.edgeInfo2) { - this.getEdgeInfo(edge.edgeInfo2.svgId)?.remove(); - } - } - } - for (const edge of containedEdgeList) { - if (edge.edgeInfoMiddle) { - this.getEdgeInfo(edge.edgeInfoMiddle.svgId)?.remove(); - } - } + // above the largest configured threshold, nothing needs to be displayed: clear everything + const maxThreshold = Math.max( + adaptiveTextZoom.threshold, + adaptiveTextZoom.edgeSideLabelThreshold, + adaptiveTextZoom.edgeMiddleLabelThreshold, + adaptiveTextZoom.edgeMiddleArrowThreshold + ); + if (maxDisplayedSize > maxThreshold) { + this.edgeInfosSection?.replaceChildren(); + this.textEdgesSection?.replaceChildren(); + this.textNodesSection?.replaceChildren(); + return; + } - start = performance.now(); - for (const node of containedNodeList) { - const textNode = this.diagramMetadata?.textNodes.find((tNode) => tNode.svgId === node.legendSvgId); - if (textNode) { - const busNodes: BusNodeMetadata[] = - this.diagramMetadata?.busNodes.filter((busNode) => busNode.vlNode == node.svgId) ?? []; + const containerRect = this.container.getBoundingClientRect(); + const viewBox = SvgUtils.computeVisibleArea(this.getViewBox(), containerRect.width, containerRect.height); - this.createLegendBox(textNode, busNodes, node); - this.createLegendEdge(textNode, busNodes, node); - } - } - console.log(`adaptive zoom mode adding legends elements time: ${performance.now() - start} ms`); + const containedElementList = this.getElementsInViewbox(viewBox, 50); + const containedNodeList = containedElementList.nodes; + const containedEdgeList = containedElementList.edges; - start = performance.now(); - this.createEdgesInfos(containedEdgeList, maxDisplayedSize); - console.log(`adaptive zoom mode adding edges info elements time: ${performance.now() - start} ms`); - } + this.updateAdaptiveLegends(containedNodeList, maxDisplayedSize, adaptiveTextZoom); + this.updateAdaptiveEdgeInfos(containedEdgeList, viewBox, maxDisplayedSize, adaptiveTextZoom); } public setJsonBranchStates(branchStates: string) { From f16a96abf8aebe5e4d3cacf3337aeb6938cd7a05 Mon Sep 17 00:00:00 2001 From: Ayoub LABIDI Date: Fri, 12 Jun 2026 13:24:14 +0200 Subject: [PATCH 3/4] review Signed-off-by: Ayoub LABIDI --- .../nad-viewer-parameters.ts | 12 ++- .../network-area-diagram-viewer.ts | 79 +++++++++++-------- 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts b/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts index 55e913c9..81753b8d 100644 --- a/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts +++ b/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts @@ -215,15 +215,13 @@ export class NadViewerParameters { public getAdaptiveTextZoom(): Required { const adaptiveTextZoom = this.nadViewerParametersOptions?.adaptiveTextZoom; + const threshold = adaptiveTextZoom?.threshold ?? NadViewerParameters.THRESHOLD_ADAPTIVE_ZOOM_DEFAULT; return { enabled: adaptiveTextZoom?.enabled ?? NadViewerParameters.ENABLE_ADAPTIVE_ZOOM_DEFAULT, - threshold: adaptiveTextZoom?.threshold ?? NadViewerParameters.THRESHOLD_ADAPTIVE_ZOOM_DEFAULT, - edgeSideLabelThreshold: - adaptiveTextZoom?.edgeSideLabelThreshold ?? NadViewerParameters.THRESHOLD_ADAPTIVE_ZOOM_DEFAULT, - edgeMiddleLabelThreshold: - adaptiveTextZoom?.edgeMiddleLabelThreshold ?? NadViewerParameters.THRESHOLD_ADAPTIVE_ZOOM_DEFAULT, - edgeMiddleArrowThreshold: - adaptiveTextZoom?.edgeMiddleArrowThreshold ?? NadViewerParameters.THRESHOLD_ADAPTIVE_ZOOM_DEFAULT, + threshold, + edgeSideLabelThreshold: adaptiveTextZoom?.edgeSideLabelThreshold ?? threshold, + edgeMiddleLabelThreshold: adaptiveTextZoom?.edgeMiddleLabelThreshold ?? threshold, + edgeMiddleArrowThreshold: adaptiveTextZoom?.edgeMiddleArrowThreshold ?? threshold, }; } diff --git a/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts b/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts index 90c3a0fc..d19c67f4 100644 --- a/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts +++ b/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts @@ -2055,8 +2055,13 @@ export class NetworkAreaDiagramViewer { ): void { this.removeEdgeInfoItems(viewBox); - if (maxDisplayedSize > adaptiveTextZoom.edgeSideLabelThreshold) { - for (const edge of edges) { + const shouldRemoveSideInfos = maxDisplayedSize > adaptiveTextZoom.edgeSideLabelThreshold; + const shouldRemoveMiddleInfo = + maxDisplayedSize > + Math.min(adaptiveTextZoom.edgeMiddleLabelThreshold, adaptiveTextZoom.edgeMiddleArrowThreshold); + + for (const edge of edges) { + if (shouldRemoveSideInfos) { if (edge.edgeInfo1) { this.getEdgeInfo(edge.edgeInfo1.svgId)?.remove(); } @@ -2064,9 +2069,7 @@ export class NetworkAreaDiagramViewer { this.getEdgeInfo(edge.edgeInfo2.svgId)?.remove(); } } - } - for (const edge of edges) { - if (edge.edgeInfoMiddle) { + if (shouldRemoveMiddleInfo && edge.edgeInfoMiddle) { this.getEdgeInfo(edge.edgeInfoMiddle.svgId)?.remove(); } } @@ -2400,47 +2403,59 @@ export class NetworkAreaDiagramViewer { const edgeInfo = this.getOrCreateEdgeInfo(edgeInfoMetadata); - if (edgeInfoMetadata.componentType) { - this.addBranchComponentElement(edgeInfo, edgeInfoMetadata.componentType); - } else if (showArrow) { - if (edgeInfoMetadata.direction || edgeInfoMetadata.directionB) { - this.addBranchArrowElement( - edgeInfo, - edgeInfoMetadata.direction ?? edgeInfoMetadata.directionB, - edgeInfoMetadata.infoTypeB, - 1 - ); - } - - if (edgeInfoMetadata.directionA) { - this.addBranchArrowElement(edgeInfo, edgeInfoMetadata.directionA, edgeInfoMetadata.infoTypeA, 2); - } + // componentType replaces the arrow, so it follows the same showArrow threshold + if (showArrow) { + this.addBranchMiddleArrowOrComponent(edgeInfo, edgeInfoMetadata); } if (showLabel) { - let i = 1; - if (edgeInfoMetadata.labelA && edgeInfoMetadata.labelB) { - this.addBranchLabelElement(edgeInfo, i++, edgeInfoMetadata.infoTypeB, edgeInfoMetadata.labelB); - } - - this.addBranchLabelElement( - edgeInfo, - i, - edgeInfoMetadata.infoTypeA ?? edgeInfoMetadata.infoTypeB, - edgeInfoMetadata.labelA ?? edgeInfoMetadata.labelB - ); + this.addBranchMiddleLabels(edgeInfo, edgeInfoMetadata); } this.redrawMiddleEdgeArrowAndLabels( halfEdge1, halfEdge2, edgeInfo, - edgeInfoMetadata.direction ?? edgeInfoMetadata.directionB, + showArrow ? (edgeInfoMetadata.direction ?? edgeInfoMetadata.directionB) : undefined, edgeInfoMetadata.directionA, showLabel && edgeInfoMetadata.labelA !== undefined && edgeInfoMetadata.labelB !== undefined ); } + private addBranchMiddleArrowOrComponent(edgeInfo: SVGElement, edgeInfoMetadata: EdgeInfoMetadata) { + if (edgeInfoMetadata.componentType) { + this.addBranchComponentElement(edgeInfo, edgeInfoMetadata.componentType); + return; + } + + if (edgeInfoMetadata.direction || edgeInfoMetadata.directionB) { + this.addBranchArrowElement( + edgeInfo, + edgeInfoMetadata.direction ?? edgeInfoMetadata.directionB, + edgeInfoMetadata.infoTypeB, + 1 + ); + } + + if (edgeInfoMetadata.directionA) { + this.addBranchArrowElement(edgeInfo, edgeInfoMetadata.directionA, edgeInfoMetadata.infoTypeA, 2); + } + } + + private addBranchMiddleLabels(edgeInfo: SVGElement, edgeInfoMetadata: EdgeInfoMetadata) { + let i = 1; + if (edgeInfoMetadata.labelA && edgeInfoMetadata.labelB) { + this.addBranchLabelElement(edgeInfo, i++, edgeInfoMetadata.infoTypeB, edgeInfoMetadata.labelB); + } + + this.addBranchLabelElement( + edgeInfo, + i, + edgeInfoMetadata.infoTypeA ?? edgeInfoMetadata.infoTypeB, + edgeInfoMetadata.labelA ?? edgeInfoMetadata.labelB + ); + } + private addBranchLabelElement( edgeInfo: SVGElement, i: number, From 680a6a0c943becfee4600c4fe1f4316b10a9e171 Mon Sep 17 00:00:00 2001 From: Ayoub LABIDI Date: Fri, 12 Jun 2026 15:48:33 +0200 Subject: [PATCH 4/4] fix demo Signed-off-by: Ayoub LABIDI --- demo/src/nad.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/src/nad.ts b/demo/src/nad.ts index a645f7e9..24286683 100644 --- a/demo/src/nad.ts +++ b/demo/src/nad.ts @@ -462,7 +462,7 @@ const addNadToDemo = () => { new NetworkAreaDiagramViewer( document.getElementById('svg-container-nad-multibus-vlnodes-adaptive-thresholds')!, svgContent, - NadSvgMultibusVLNodesMiddleArrowExampleMeta, + structuredClone(NadSvgMultibusVLNodesMiddleArrowExampleMeta), nadViewerParametersOptions ); });