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..24286683 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, + structuredClone(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..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 @@ -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,16 @@ 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; + const threshold = adaptiveTextZoom?.threshold ?? NadViewerParameters.THRESHOLD_ADAPTIVE_ZOOM_DEFAULT; + return { + enabled: adaptiveTextZoom?.enabled ?? NadViewerParameters.ENABLE_ADAPTIVE_ZOOM_DEFAULT, + threshold, + edgeSideLabelThreshold: adaptiveTextZoom?.edgeSideLabelThreshold ?? threshold, + edgeMiddleLabelThreshold: adaptiveTextZoom?.edgeMiddleLabelThreshold ?? threshold, + edgeMiddleArrowThreshold: adaptiveTextZoom?.edgeMiddleArrowThreshold ?? threshold, + }; } 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..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 @@ -21,6 +21,7 @@ import { } from './diagram-metadata'; import debounce from 'lodash.debounce'; import { + AdaptiveTextZoomOptions, NadViewerParameters, NadViewerParametersOptions, OnBendLineCallbackType, @@ -419,7 +420,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 +437,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 @@ -1414,7 +1418,7 @@ 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) { @@ -1425,15 +1429,7 @@ export class NetworkAreaDiagramViewer { true, this.svgParameters.getArrowLabelShift() ); - 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'); - } + this.redrawLabel(labelBElement, infoAngle, middleLabelBData[0] * factor, middleLabelBData[1]); const middleLabelAData = HalfEdgeUtils.getMiddleLabelData( halfEdge1, @@ -1441,18 +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 + ')') 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'); - } + this.redrawLabel(labelAElement, infoAngle, shift * factor, style); } private redrawTransformer( @@ -1753,7 +1743,7 @@ export class NetworkAreaDiagramViewer { } this.setPreviousMaxDisplayedSize(maxDisplayedSize); - if (this.nadViewerParameters.getEnableAdaptiveTextZoom()) { + if (this.nadViewerParameters.getAdaptiveTextZoom().enabled) { this.adaptiveZoomViewboxUpdate(maxDisplayedSize); } @@ -1958,10 +1948,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 +1964,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 +1976,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); } } } @@ -2026,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)); @@ -2047,52 +2044,98 @@ export class NetworkAreaDiagramViewer { polyline.remove(); } }); - - // filter edge info items that fall outside the viewbox - this.removeEdgeInfoItems(viewBox); } - private adaptiveZoomViewboxUpdate(maxDisplayedSize: number) { - if (maxDisplayedSize > this.nadViewerParameters.getThresholdAdaptiveTextZoom()) { - 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); + // filter edge info items that fall outside the viewbox + private filterEdgeInfos( + edges: EdgeMetadata[], + viewBox: ViewBox | undefined, + maxDisplayedSize: number, + adaptiveTextZoom: Required + ): void { + this.removeEdgeInfoItems(viewBox); - const containedElementList = this.getElementsInViewbox(viewBox, 50); - const containedNodeList = containedElementList.nodes; - const containedEdgeList = containedElementList.edges; + const shouldRemoveSideInfos = maxDisplayedSize > adaptiveTextZoom.edgeSideLabelThreshold; + const shouldRemoveMiddleInfo = + maxDisplayedSize > + Math.min(adaptiveTextZoom.edgeMiddleLabelThreshold, adaptiveTextZoom.edgeMiddleArrowThreshold); - 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`); + for (const edge of edges) { + if (shouldRemoveSideInfos) { + if (edge.edgeInfo1) { + this.getEdgeInfo(edge.edgeInfo1.svgId)?.remove(); + } + if (edge.edgeInfo2) { + this.getEdgeInfo(edge.edgeInfo2.svgId)?.remove(); + } + } + if (shouldRemoveMiddleInfo && edge.edgeInfoMiddle) { + this.getEdgeInfo(edge.edgeInfoMiddle.svgId)?.remove(); + } + } + } - start = performance.now(); + private updateAdaptiveEdgeInfos( + edges: EdgeMetadata[], + viewBox: ViewBox | undefined, + maxDisplayedSize: number, + adaptiveTextZoom: Required + ): void { + this.filterEdgeInfos(edges, viewBox, maxDisplayedSize, adaptiveTextZoom); + this.createEdgesInfos(edges, maxDisplayedSize); + } - this.filterElements(containedNodeList, viewBox); + private updateAdaptiveLegends( + nodeList: NodeMetadata[], + maxDisplayedSize: number, + adaptiveTextZoom: Required + ): void { + if (maxDisplayedSize > adaptiveTextZoom.threshold) { + this.textNodesSection?.replaceChildren(); + this.textEdgesSection?.replaceChildren(); + return; + } - console.log(`time to remove elements not in the current viewbox: ${performance.now() - start} ms`); + this.filterLegends(nodeList); - 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) ?? []; + 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.createLegendBox(textNode, busNodes, node); - this.createLegendEdge(textNode, busNodes, node); - } + this.createLegendBox(textNode, busNodes, node); + this.createLegendEdge(textNode, busNodes, node); } - console.log(`adaptive zoom mode adding legends elements time: ${performance.now() - start} ms`); + } + } - start = performance.now(); - this.createEdgesInfos(containedEdgeList); - console.log(`adaptive zoom mode adding edges info elements time: ${performance.now() - start} ms`); + private adaptiveZoomViewboxUpdate(maxDisplayedSize: number) { + const adaptiveTextZoom = this.nadViewerParameters.getAdaptiveTextZoom(); + + // 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; } + + 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.updateAdaptiveLegends(containedNodeList, maxDisplayedSize, adaptiveTextZoom); + this.updateAdaptiveEdgeInfos(containedEdgeList, viewBox, maxDisplayedSize, adaptiveTextZoom); } public setJsonBranchStates(branchStates: string) { @@ -2342,7 +2385,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; @@ -2358,23 +2403,46 @@ export class NetworkAreaDiagramViewer { const edgeInfo = this.getOrCreateEdgeInfo(edgeInfoMetadata); + // componentType replaces the arrow, so it follows the same showArrow threshold + if (showArrow) { + this.addBranchMiddleArrowOrComponent(edgeInfo, edgeInfoMetadata); + } + + if (showLabel) { + this.addBranchMiddleLabels(edgeInfo, edgeInfoMetadata); + } + + this.redrawMiddleEdgeArrowAndLabels( + halfEdge1, + halfEdge2, + edgeInfo, + 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); - } else { - if (edgeInfoMetadata.direction || edgeInfoMetadata.directionB) { - this.addBranchArrowElement( - edgeInfo, - edgeInfoMetadata.direction ?? edgeInfoMetadata.directionB, - edgeInfoMetadata.infoTypeB, - 1 - ); - } + return; + } - if (edgeInfoMetadata.directionA) { - this.addBranchArrowElement(edgeInfo, edgeInfoMetadata.directionA, edgeInfoMetadata.infoTypeA, 2); - } + 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); @@ -2386,15 +2454,6 @@ export class NetworkAreaDiagramViewer { edgeInfoMetadata.infoTypeA ?? edgeInfoMetadata.infoTypeB, edgeInfoMetadata.labelA ?? edgeInfoMetadata.labelB ); - - this.redrawMiddleEdgeArrowAndLabels( - halfEdge1, - halfEdge2, - edgeInfo, - edgeInfoMetadata.direction ?? edgeInfoMetadata.directionB, - edgeInfoMetadata.directionA, - edgeInfoMetadata.labelA !== undefined && edgeInfoMetadata.labelB !== undefined - ); } private addBranchLabelElement(