From 175fc3a61ad1704b2f9ac962991f1ea566c3db2a Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Mon, 27 Jul 2026 15:14:15 +0200 Subject: [PATCH 1/3] New version. --- package.json | 2 +- src/renderer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0366c9de..c72e5715 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "url": "git+https://github.com/opencor/webapp.git" }, "type": "module", - "version": "0.20260727.0", + "version": "0.20260727.1", "engines": { "bun": ">=1.2.0" }, diff --git a/src/renderer/package.json b/src/renderer/package.json index 6cb3b661..2df8f02a 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -42,7 +42,7 @@ }, "./style.css": "./dist/opencor.css" }, - "version": "0.20260727.0", + "version": "0.20260727.1", "libopencorVersion": "1.20260723.2", "scripts": { "build": "vite build && bun scripts/generate.version.ts", From 29d99d9bb7b1f643ea181191c2a45fd0e3d4a63c Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Mon, 27 Jul 2026 15:27:04 +0200 Subject: [PATCH 2/3] Interactive mode: preserve graph trace IDs in plot data. Include each trace's `traceId` when rebuilding Plotly traces in `GraphPanelWidget`. This keeps trace identity available during plot updates so visibility and related per-trace state can stay aligned across rerenders. --- src/renderer/src/components/widgets/GraphPanelWidget.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/src/components/widgets/GraphPanelWidget.vue b/src/renderer/src/components/widgets/GraphPanelWidget.vue index e1f2aea3..c5e39970 100644 --- a/src/renderer/src/components/widgets/GraphPanelWidget.vue +++ b/src/renderer/src/components/widgets/GraphPanelWidget.vue @@ -759,6 +759,7 @@ const updatePlot = (): void => { x: dataTrace.x, y: dataTrace.y, name: dataTrace.name, + traceId: dataTrace.traceId, visible: dataTraceKey ? previousTraceVisibilityByKey[dataTraceKey] : undefined, line: { color: dataTrace.color }, legendrank: dataTrace.zorder From ebf11aef91abbee13809186c5e441a8a46ba2698 Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Mon, 27 Jul 2026 16:10:21 +0200 Subject: [PATCH 3/3] Graph Panel widget: show/hide a trace immediately (when toggling its legend). --- .../components/widgets/GraphPanelWidget.vue | 104 +++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/widgets/GraphPanelWidget.vue b/src/renderer/src/components/widgets/GraphPanelWidget.vue index c5e39970..6e01c4fc 100644 --- a/src/renderer/src/components/widgets/GraphPanelWidget.vue +++ b/src/renderer/src/components/widgets/GraphPanelWidget.vue @@ -134,6 +134,8 @@ defineExpose({ resize }); +const DOUBLE_CLICK_DELAY = 300; + const instanceId = Symbol('GraphPanelWidget'); const rootRef = vue.ref(null); const mainDivRef = vue.ref(null); @@ -150,6 +152,8 @@ let trackedMargins: IGraphPanelMargins | undefined; let stopTrackingContainerSize: (() => void) | undefined; let marginsRafId: number | undefined; let resizeRafId: number | undefined; +let lastLegendClickIndex: number | undefined; +let lastLegendClickTime = 0; // Context menu functionality. @@ -710,6 +714,10 @@ const updateMarginsAsync = (): void => { }); }; +const plotlyTraceData = (): IPlotlyTraceState[] | undefined => { + return (mainDivRef.value as unknown as { data?: IPlotlyTraceState[] })?.data; +}; + const updatePlot = (): void => { plotIsReady = false; @@ -735,7 +743,7 @@ const updatePlot = (): void => { }; const previousTraceVisibilityByKey: Record = {}; - const previousPlotlyData = (mainDivRef.value as unknown as { data?: IPlotlyTraceState[] })?.data; + const previousPlotlyData = plotlyTraceData(); for (const plotlyTrace of previousPlotlyData ?? []) { const plotlyTraceKey = traceVisibilityKey(plotlyTrace); @@ -799,7 +807,7 @@ const updatePlot = (): void => { responsive: true, displayModeBar: false, - doubleClickDelay: 1000, + doubleClickDelay: DOUBLE_CLICK_DELAY, scrollZoom: true, showTips: false }) @@ -875,6 +883,98 @@ vue.onMounted(() => { emit('resetMargins'); } }); + + // Intercept legend single-click to toggle trace visibility immediately, bypassing Plotly's setTimeout()-based + // handler. + + plotlyElement.on('plotly_legendclick', (...args: unknown[]) => { + const eventData = args[0] as { curveNumber?: number } | undefined; + const curveNumber = eventData?.curveNumber; + + if (curveNumber === undefined) { + return; + } + + const now = Date.now(); + + // If this is a rapid second click on the same trace, a double-click is in progress, so skip toggling and let + // plotly_legenddoubleclick() handle isolation. + + if (curveNumber === lastLegendClickIndex && now - lastLegendClickTime < DOUBLE_CLICK_DELAY) { + lastLegendClickIndex = undefined; + + return false; + } + + // Toggle the trace visibility immediately. + + const plotlyData = plotlyTraceData(); + + if (!plotlyData || curveNumber >= plotlyData.length) { + return false; + } + + const currentVisibility = plotlyData[curveNumber]?.visible; + const newVisibility = currentVisibility === 'legendonly' ? true : 'legendonly'; + + dependencies._plotlyJs.restyle(mainDivRef.value, 'visible', newVisibility, [curveNumber]); + + lastLegendClickIndex = curveNumber; + lastLegendClickTime = now; + + // Return false to cancel Plotly's own setTimeout()-based handler. + + return false; + }); + + // Intercept legend double-click to implement trace isolation (show only the clicked trace) immediately, since we + // already cancelled the single-click path. + + plotlyElement.on('plotly_legenddoubleclick', (...args: unknown[]) => { + const eventData = args[0] as { curveNumber?: number } | undefined; + const curveNumber = eventData?.curveNumber; + + if (curveNumber === undefined) { + return false; + } + + lastLegendClickIndex = undefined; + + const plotlyData = plotlyTraceData(); + + if (!plotlyData || curveNumber >= plotlyData.length) { + return false; + } + + // Collect the indices of visible (not 'legendonly') traces. + + const visibleIndices: number[] = []; + + for (let i = 0; i < plotlyData.length; ++i) { + if (plotlyData[i] && plotlyData[i].visible !== 'legendonly' && plotlyData[i].visible !== false) { + visibleIndices.push(i); + } + } + + // If the clicked trace is the only one visible, then show all traces again. Otherwise, isolate the clicked trace + // by hiding all the other traces (by setting them to 'legendonly'). + + const isSoleVisible = visibleIndices.length === 1 && visibleIndices[0] === curveNumber; + + if (isSoleVisible) { + dependencies._plotlyJs.restyle(mainDivRef.value, 'visible', true); + } else { + const visibilityUpdates: PlotlyTraceVisible[] = new Array(plotlyData.length); + + for (let i = 0; i < plotlyData.length; ++i) { + visibilityUpdates[i] = i === curveNumber ? true : 'legendonly'; + } + + dependencies._plotlyJs.restyle(mainDivRef.value, 'visible', visibilityUpdates); + } + + return false; + }); }); });