// Copyright (c) 2025-2026, RTE (https://www.rte-france.com) // This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. // If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, // you can obtain one at http://mozilla.org/MPL/2.0/. // SPDX-License-Identifier: MPL-2.0 import type { AssetDelta, MetadataIndex } from '../../types'; import { getIdMap } from './idMap'; /** * Apply delta flow visualizations (coloring + text replacement) on a container. * - Branch coloring is based on active power (P) delta only. * - edgeInfo1 (active power / black arrow) shows P delta text. * - edgeInfo2 (reactive power / blue arrow) shows Q delta text. * - Load/generator assets are colored based on P delta. * Saves original text in data-original-text attribute for restoration. */ export const applyDeltaVisuals = ( container: HTMLElement | null, diagram: { flow_deltas?: Record; reactive_flow_deltas?: Record; asset_deltas?: Record; } | null, metaIndex: MetadataIndex | null, isDeltaMode: boolean, ) => { if (!container || !diagram || !metaIndex) return; // Clear delta classes on edges and nodes container.querySelectorAll('.nad-delta-positive').forEach(el => el.classList.remove('nad-delta-positive')); container.querySelectorAll('.nad-delta-negative').forEach(el => el.classList.remove('nad-delta-negative')); container.querySelectorAll('.nad-delta-grey').forEach(el => el.classList.remove('nad-delta-grey')); // Restore original text labels container.querySelectorAll('[data-original-text]').forEach(el => { el.textContent = el.getAttribute('data-original-text'); el.removeAttribute('data-original-text'); }); if (!isDeltaMode || !diagram.flow_deltas) return; const { edgesByEquipmentId } = metaIndex; const flowDeltas = diagram.flow_deltas; const assetDeltas = diagram.asset_deltas || {}; const idMap = getIdMap(container); const classMap: Record = { positive: 'nad-delta-positive', negative: 'nad-delta-negative', grey: 'nad-delta-grey', }; // --- Branch (edge) deltas --- for (const [lineId, deltaInfo] of Object.entries(flowDeltas)) { const edge = edgesByEquipmentId.get(lineId); if (!edge || !edge.svgId) continue; // Color the edge based on active power delta const el = idMap.get(edge.svgId); if (el) { const cls = classMap[deltaInfo.category]; if (cls) el.classList.add(cls); } // Apply delta text labels to both Terminal 1 and Terminal 2 (edgeInfo1 and edgeInfo2 represent P flow at terminals in NADs) const pDeltaStr = deltaInfo.delta >= 0 ? `+${deltaInfo.delta.toFixed(1)}` : deltaInfo.delta.toFixed(1); // edgeInfo1 = Terminal 1 active power (P) if (edge.edgeInfo1?.svgId) { const infoEl = idMap.get(edge.edgeInfo1.svgId); if (infoEl) { infoEl.querySelectorAll('foreignObject, text').forEach(t => { if (!t.hasAttribute('data-original-text')) { t.setAttribute('data-original-text', t.textContent || ''); } t.textContent = `\u0394 ${pDeltaStr}`; }); } } // edgeInfo2 = Terminal 2 active power (P) (Note: NOT reactive power in NADs) if (edge.edgeInfo2?.svgId) { const infoEl = idMap.get(edge.edgeInfo2.svgId); if (infoEl) { infoEl.querySelectorAll('foreignObject, text').forEach(t => { if (!t.hasAttribute('data-original-text')) { t.setAttribute('data-original-text', t.textContent || ''); } t.textContent = `\u0394 ${pDeltaStr}`; }); } } } // --- Asset (load/generator) deltas --- for (const [assetId, assetInfo] of Object.entries(assetDeltas)) { // Assets appear as legend edges connected to VL nodes. // Try to find them as edges first (pypowsybl maps loads/gens as short edges). const edge = edgesByEquipmentId.get(assetId); if (edge && edge.svgId) { const el = idMap.get(edge.svgId); if (el) { const cls = classMap[assetInfo.category]; if (cls) el.classList.add(cls); } // Update P arrow text on edgeInfo1 const pStr = assetInfo.delta_p >= 0 ? `+${assetInfo.delta_p.toFixed(1)}` : assetInfo.delta_p.toFixed(1); if (edge.edgeInfo1?.svgId) { const infoEl = idMap.get(edge.edgeInfo1.svgId); if (infoEl) { infoEl.querySelectorAll('foreignObject, text').forEach(t => { if (!t.hasAttribute('data-original-text')) { t.setAttribute('data-original-text', t.textContent || ''); } t.textContent = `\u0394 ${pStr}`; }); } } // Update Q arrow text on edgeInfo2 const qStr = assetInfo.delta_q >= 0 ? `+${assetInfo.delta_q.toFixed(1)}` : assetInfo.delta_q.toFixed(1); if (edge.edgeInfo2?.svgId) { const infoEl = idMap.get(edge.edgeInfo2.svgId); if (infoEl) { infoEl.querySelectorAll('foreignObject, text').forEach(t => { if (!t.hasAttribute('data-original-text')) { t.setAttribute('data-original-text', t.textContent || ''); } t.textContent = `\u0394 ${qStr}`; }); } } } } };