recon comparison views
3 changed files+172−22
web-ui/src/components/dataset/TimeseriesView.tsxmodified+63−1View file
@@ -116,6 +116,65 @@ const TimeseriesView: React.FC<TimeseriesViewProps> = ({
116116 loadRangeData();
117117 }, [client, xRange, selectedChannel, numChannels]);
118118
119+ // Initialize reconstructed data client when reconstructedInfo changes
120+ useEffect(() => {
121+ if (!reconstructedInfo) {
122+ setReconstructedClient(null);
123+ setDataYReconstructed(null);
124+ setDataYResiduals(null);
125+ return;
126+ }
127+
128+ const initClient = async () => {
129+ try {
130+ const client = await TimeseriesDataClient.create(
131+ reconstructedInfo.datasetJsonUrl,
132+ reconstructedInfo.reconstructedUrl,
133+ 1000
134+ );
135+ setReconstructedClient(client);
136+ } catch (err) {
137+ console.error("Failed to initialize reconstructed data client:", err);
138+ setError("Failed to load reconstructed data");
139+ }
140+ };
141+
142+ initClient();
143+ }, [reconstructedInfo]);
144+
145+ // Load reconstructed data for current range
146+ useEffect(() => {
147+ if (!reconstructedClient || !xRange || selectedChannel === "all" || comparisonMode === "original") {
148+ setDataYReconstructed(null);
149+ setDataYResiduals(null);
150+ return;
151+ }
152+
153+ const loadReconstructedData = async () => {
154+ try {
155+ const start = Math.floor(xRange.min);
156+ const end = Math.ceil(xRange.max) + 1;
157+ const channel = typeof selectedChannel === "number" ? selectedChannel : 0;
158+
159+ const reconstructedData = await reconstructedClient.fetchRange(start, end, channel);
160+ setDataYReconstructed(reconstructedData);
161+
162+ // Compute residuals if we have both original and reconstructed
163+ if (dataY && reconstructedData.length === dataY.length) {
164+ const residuals = new Float32Array(dataY.length);
165+ for (let i = 0; i < dataY.length; i++) {
166+ residuals[i] = dataY[i] - reconstructedData[i];
167+ }
168+ setDataYResiduals(residuals);
169+ }
170+ } catch (err) {
171+ console.error("Failed to load reconstructed data:", err);
172+ }
173+ };
174+
175+ loadReconstructedData();
176+ }, [reconstructedClient, xRange, selectedChannel, dataY, comparisonMode]);
177+
119178 // Update xRange when client is initialized
120179 useEffect(() => {
121180 if (client) {
@@ -285,6 +344,9 @@ const TimeseriesView: React.FC<TimeseriesViewProps> = ({
285344 timeseriesT: dataT,
286345 timeseriesY: dataY ? Array.from(dataY) : [],
287346 timeseriesYAll: dataYAll ? dataYAll.map(ch => Array.from(ch)) : undefined,
347+ timeseriesYReconstructed: dataYReconstructed ? Array.from(dataYReconstructed) : undefined,
348+ timeseriesYResiduals: dataYResiduals ? Array.from(dataYResiduals) : undefined,
349+ comparisonMode: comparisonMode,
288350 width,
289351 height,
290352 margins,
@@ -292,7 +354,7 @@ const TimeseriesView: React.FC<TimeseriesViewProps> = ({
292354 yRange,
293355 };
294356 worker.postMessage(msg);
295- }, [width, height, dataT, dataY, dataYAll, worker, margins, xRange, yRange]);
357+ }, [width, height, dataT, dataY, dataYAll, dataYReconstructed, dataYResiduals, comparisonMode, worker, margins, xRange, yRange]);
296358
297359 // Render cursor on overlay canvas
298360 useEffect(() => {
web-ui/src/components/dataset/TimeseriesViewWorker.tsmodified+104−21View file
@@ -66,6 +66,9 @@ function renderTimeseries(
6666 timeseriesT: number[],
6767 timeseriesY: number[],
6868 timeseriesYAll: number[][] | undefined,
69+ timeseriesYReconstructed: number[] | undefined,
70+ timeseriesYResiduals: number[] | undefined,
71+ comparisonMode: string | undefined,
6972 width: number,
7073 height: number,
7174 margins: Margins,
@@ -108,8 +111,95 @@ function renderTimeseries(
108111 const xScale = drawingWidth / (xRange.max - xRange.min);
109112 const yScale = drawingHeight / (yRange.max - yRange.min);
110113
111- // Draw timeseries - either all channels or single channel
112- if (timeseriesYAll && timeseriesYAll.length > 0) {
114+ // Draw timeseries based on comparison mode
115+ const mode = comparisonMode || "original";
116+
117+ if (mode === "side-by-side" && timeseriesYReconstructed) {
118+ // Split canvas vertically
119+ const halfWidth = drawingWidth / 2;
120+
121+ // Draw original on left
122+ context.strokeStyle = "#2196f3";
123+ context.lineWidth = 2;
124+ context.beginPath();
125+ for (let i = 0; i < timeseriesT.length; i++) {
126+ const x = margins.left + ((timeseriesT[i] - xRange.min) * halfWidth) / (xRange.max - xRange.min);
127+ const y = margins.top + drawingHeight - (timeseriesY[i] - yRange.min) * yScale;
128+ if (i === 0) context.moveTo(x, y);
129+ else context.lineTo(x, y);
130+ }
131+ context.stroke();
132+
133+ // Draw reconstructed on right
134+ context.strokeStyle = "#ff9800"; // orange
135+ context.lineWidth = 2;
136+ context.beginPath();
137+ for (let i = 0; i < timeseriesT.length; i++) {
138+ const x = margins.left + halfWidth + ((timeseriesT[i] - xRange.min) * halfWidth) / (xRange.max - xRange.min);
139+ const y = margins.top + drawingHeight - (timeseriesYReconstructed[i] - yRange.min) * yScale;
140+ if (i === 0) context.moveTo(x, y);
141+ else context.lineTo(x, y);
142+ }
143+ context.stroke();
144+
145+ // Draw divider line
146+ context.strokeStyle = "#999";
147+ context.lineWidth = 1;
148+ context.beginPath();
149+ context.moveTo(margins.left + halfWidth, margins.top);
150+ context.lineTo(margins.left + halfWidth, height - margins.bottom);
151+ context.stroke();
152+ } else if (mode === "overlay" && timeseriesYReconstructed) {
153+ // Draw original in blue
154+ context.strokeStyle = "#2196f3";
155+ context.lineWidth = 2;
156+ context.beginPath();
157+ for (let i = 0; i < timeseriesT.length; i++) {
158+ const x = margins.left + (timeseriesT[i] - xRange.min) * xScale;
159+ const y = margins.top + drawingHeight - (timeseriesY[i] - yRange.min) * yScale;
160+ if (i === 0) context.moveTo(x, y);
161+ else context.lineTo(x, y);
162+ }
163+ context.stroke();
164+
165+ // Draw reconstructed in orange
166+ context.strokeStyle = "#ff9800";
167+ context.lineWidth = 2;
168+ context.beginPath();
169+ for (let i = 0; i < timeseriesT.length; i++) {
170+ const x = margins.left + (timeseriesT[i] - xRange.min) * xScale;
171+ const y = margins.top + drawingHeight - (timeseriesYReconstructed[i] - yRange.min) * yScale;
172+ if (i === 0) context.moveTo(x, y);
173+ else context.lineTo(x, y);
174+ }
175+ context.stroke();
176+ } else if (mode === "residuals" && timeseriesYResiduals) {
177+ // Draw residuals with diverging colors
178+ context.lineWidth = 2;
179+ context.beginPath();
180+
181+ // Draw zero line
182+ const zeroY = margins.top + drawingHeight - (0 - yRange.min) * yScale;
183+ context.strokeStyle = "#999";
184+ context.lineWidth = 1;
185+ context.setLineDash([4, 4]);
186+ context.moveTo(margins.left, zeroY);
187+ context.lineTo(width - margins.right, zeroY);
188+ context.stroke();
189+ context.setLineDash([]);
190+
191+ // Draw residuals
192+ context.strokeStyle = "#9c27b0"; // purple for residuals
193+ context.lineWidth = 2;
194+ context.beginPath();
195+ for (let i = 0; i < timeseriesT.length; i++) {
196+ const x = margins.left + (timeseriesT[i] - xRange.min) * xScale;
197+ const y = margins.top + drawingHeight - (timeseriesYResiduals[i] - yRange.min) * yScale;
198+ if (i === 0) context.moveTo(x, y);
199+ else context.lineTo(x, y);
200+ }
201+ context.stroke();
202+ } else if (timeseriesYAll && timeseriesYAll.length > 0) {
113203 // Draw all channels with different colors
114204 const colors = [
115205 "#2196f3", // blue
@@ -127,39 +217,26 @@ function renderTimeseries(
127217 context.lineWidth = 1.5;
128218 context.beginPath();
129219
130- let isFirst = true;
131220 for (let i = 0; i < timeseriesT.length; i++) {
132221 const x = margins.left + (timeseriesT[i] - xRange.min) * xScale;
133222 const y = margins.top + drawingHeight - (channelY[i] - yRange.min) * yScale;
134- if (isFirst) {
135- context.moveTo(x, y);
136- isFirst = false;
137- } else {
138- context.lineTo(x, y);
139- }
223+ if (i === 0) context.moveTo(x, y);
224+ else context.lineTo(x, y);
140225 }
141226 context.stroke();
142227 });
143228 } else {
144- // Draw single channel
229+ // Draw single channel - original only
145230 context.strokeStyle = "#2196f3";
146231 context.lineWidth = 2;
147232 context.beginPath();
148233
149- // Draw the path
150- let isFirst = true;
151234 for (let i = 0; i < timeseriesT.length; i++) {
152235 const x = margins.left + (timeseriesT[i] - xRange.min) * xScale;
153- const y =
154- margins.top + drawingHeight - (timeseriesY[i] - yRange.min) * yScale;
155- if (isFirst) {
156- context.moveTo(x, y);
157- isFirst = false;
158- } else {
159- context.lineTo(x, y);
160- }
236+ const y = margins.top + drawingHeight - (timeseriesY[i] - yRange.min) * yScale;
237+ if (i === 0) context.moveTo(x, y);
238+ else context.lineTo(x, y);
161239 }
162-
163240 context.stroke();
164241 }
165242
@@ -232,6 +309,9 @@ self.onmessage = (evt: MessageEvent) => {
232309 timeseriesT,
233310 timeseriesY,
234311 timeseriesYAll,
312+ timeseriesYReconstructed,
313+ timeseriesYResiduals,
314+ comparisonMode,
235315 width,
236316 height,
237317 margins,
@@ -242,6 +322,9 @@ self.onmessage = (evt: MessageEvent) => {
242322 timeseriesT,
243323 timeseriesY,
244324 timeseriesYAll,
325+ timeseriesYReconstructed,
326+ timeseriesYResiduals,
327+ comparisonMode,
245328 width,
246329 height,
247330 margins,
web-ui/src/components/dataset/WorkerTypes.tsmodified+5−0View file
@@ -1,3 +1,5 @@
1+import { ComparisonMode } from "../../types/comparison";
2+
13 export interface Range {
24 min: number;
35 max: number;
@@ -17,6 +19,9 @@ export type WorkerMessage =
1719 timeseriesT: number[];
1820 timeseriesY: number[];
1921 timeseriesYAll?: number[][]; // For multi-channel overlay
22+ timeseriesYReconstructed?: number[]; // For comparison with lossy reconstruction
23+ timeseriesYResiduals?: number[]; // For showing residuals
24+ comparisonMode?: ComparisonMode;
2025 width: number;
2126 height: number;
2227 margins: Margins;