Skip to content

Commit 45cb0e2

Browse files
authored
Merge pull request #990 from arayabrain/feature/hdf5-mat-visualise
H5 and mat files visualisable, read path from workflow.yaml
2 parents da9b1cf + 4592898 commit 45cb0e2

10 files changed

Lines changed: 831 additions & 1 deletion

File tree

frontend/src/api/outputs/Outputs.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,34 @@ export async function getHistogramDataApi(
254254
return response.data
255255
}
256256

257+
export type StructuredData = {
258+
data: number[] | number[][] | number[][][]
259+
data_type: "timeseries" | "images" | "bar"
260+
columns?: string[]
261+
index?: (string | number)[]
262+
total_frames?: number
263+
dataset_path?: string
264+
}
265+
266+
export async function getStructuredDataApi(
267+
workspaceId: string,
268+
uniqueId: string,
269+
nodeId: string,
270+
startIndex?: number,
271+
endIndex?: number,
272+
): Promise<StructuredData> {
273+
const response = await axios.get(
274+
`${BASE_URL}/outputs/structured/${workspaceId}/${uniqueId}/${nodeId}`,
275+
{
276+
params: {
277+
start_index: startIndex,
278+
end_index: endIndex,
279+
},
280+
},
281+
)
282+
return response.data
283+
}
284+
257285
export type PieData = number[][]
258286

259287
export async function getPieDataApi(

frontend/src/components/Workspace/Visualize/DisplayDataItem.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { PiePlot } from "components/Workspace/Visualize/Plot/PiePlot"
1313
import { PolarPlot } from "components/Workspace/Visualize/Plot/PolarPlot"
1414
import { RoiPlot } from "components/Workspace/Visualize/Plot/RoiPlot"
1515
import { ScatterPlot } from "components/Workspace/Visualize/Plot/ScatterPlot"
16+
import { StructuredFilePlot } from "components/Workspace/Visualize/Plot/StructuredFilePlot"
1617
import { TimeSeriesPlot } from "components/Workspace/Visualize/Plot/TimeSeriesPlot"
1718
import {
1819
DATA_TYPE,
@@ -77,6 +78,9 @@ const DisplayPlot = memo(function DisplayPlot({ dataType }: DataTypeProps) {
7778
return <PiePlot />
7879
case DATA_TYPE_SET.POLAR:
7980
return <PolarPlot />
81+
case DATA_TYPE_SET.HDF5:
82+
case DATA_TYPE_SET.MATLAB:
83+
return <StructuredFilePlot />
8084
default:
8185
return null
8286
}
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
import {
2+
ChangeEvent,
3+
memo,
4+
useCallback,
5+
useContext,
6+
useEffect,
7+
useRef,
8+
useState,
9+
} from "react"
10+
import PlotlyChart from "react-plotlyjs-ts"
11+
import { useDispatch, useSelector } from "react-redux"
12+
13+
import {
14+
Box,
15+
Button,
16+
LinearProgress,
17+
Slider,
18+
TextField,
19+
Typography,
20+
} from "@mui/material"
21+
22+
import { DisplayDataContext } from "components/Workspace/Visualize/DataContext"
23+
import { getStructuredData } from "store/slice/DisplayData/DisplayDataActions"
24+
import { selectPipelineLatestUid } from "store/slice/Pipeline/PipelineSelectors"
25+
import { selectCurrentWorkspaceId } from "store/slice/Workspace/WorkspaceSelector"
26+
import { AppDispatch, RootState } from "store/store"
27+
28+
const DEFAULT_START_INDEX = 0
29+
const DEFAULT_END_INDEX = 10
30+
31+
export const StructuredFilePlot = memo(function StructuredFilePlot() {
32+
const { nodeId, itemId } = useContext(DisplayDataContext)
33+
const dispatch = useDispatch<AppDispatch>()
34+
35+
const workspaceId = useSelector(selectCurrentWorkspaceId)
36+
const uid = useSelector(selectPipelineLatestUid)
37+
38+
const structuredState = useSelector(
39+
(state: RootState) => state.displayData.structured[itemId],
40+
)
41+
42+
useEffect(() => {
43+
if (workspaceId && uid && nodeId) {
44+
dispatch(
45+
getStructuredData({
46+
workspaceId: String(workspaceId),
47+
uniqueId: uid,
48+
nodeId,
49+
itemId,
50+
startIndex: DEFAULT_START_INDEX,
51+
endIndex: DEFAULT_END_INDEX,
52+
}),
53+
)
54+
}
55+
}, [dispatch, workspaceId, uid, nodeId, itemId])
56+
57+
if (!uid || !nodeId) {
58+
return (
59+
<Box p={2}>
60+
<Typography color="text.secondary">
61+
No workflow run available. Please run the workflow first.
62+
</Typography>
63+
</Box>
64+
)
65+
}
66+
67+
if (!structuredState || structuredState.pending) {
68+
return <LinearProgress />
69+
}
70+
71+
if (structuredState.error) {
72+
return (
73+
<Box p={2}>
74+
<Typography color="error">
75+
Error loading data: {structuredState.error}
76+
</Typography>
77+
</Box>
78+
)
79+
}
80+
81+
const result = structuredState.data
82+
if (!result) return null
83+
84+
return (
85+
<Box>
86+
{result.dataset_path && (
87+
<Typography
88+
variant="caption"
89+
color="text.secondary"
90+
title={result.dataset_path}
91+
noWrap
92+
sx={{ px: 1, display: "block", maxWidth: "100%" }}
93+
>
94+
{result.dataset_path}
95+
</Typography>
96+
)}
97+
<DataView result={result} />
98+
</Box>
99+
)
100+
})
101+
102+
const DataView = memo(function DataView({
103+
result,
104+
}: {
105+
result: {
106+
data_type: string
107+
data: number[] | number[][] | number[][][]
108+
total_frames?: number
109+
}
110+
}) {
111+
switch (result.data_type) {
112+
case "timeseries":
113+
return <TimeSeriesView data={result.data as number[][]} />
114+
case "images":
115+
return (
116+
<ImageView
117+
data={result.data as number[][][]}
118+
totalFrames={
119+
result.total_frames ?? (result.data as number[][][]).length
120+
}
121+
/>
122+
)
123+
case "bar":
124+
return <BarView data={result.data as number[]} />
125+
default:
126+
return <Typography>Unsupported data type: {result.data_type}</Typography>
127+
}
128+
})
129+
130+
const TimeSeriesView = memo(function TimeSeriesView({
131+
data,
132+
}: {
133+
data: number[][]
134+
}) {
135+
const traces = []
136+
if (data.length > 0) {
137+
const numCols = data[0].length
138+
for (let col = 0; col < numCols; col++) {
139+
traces.push({
140+
y: data.map((row) => row[col]),
141+
type: "scatter",
142+
mode: "lines",
143+
name: `col ${col}`,
144+
})
145+
}
146+
}
147+
148+
const layout = {
149+
title: "Time Series",
150+
xaxis: { title: "Frame" },
151+
yaxis: { title: "Value" },
152+
autosize: true,
153+
}
154+
155+
return <PlotlyChart data={traces} layout={layout} />
156+
})
157+
158+
const ImageView = memo(function ImageView({
159+
data,
160+
totalFrames,
161+
}: {
162+
data: number[][][]
163+
totalFrames: number
164+
}) {
165+
const [activeIndex, setActiveIndex] = useState(0)
166+
const [duration, setDuration] = useState(500)
167+
const intervalRef = useRef<null | NodeJS.Timeout>(null)
168+
const maxIndex = data.length - 1
169+
170+
const frame = data[activeIndex] || []
171+
172+
useEffect(() => {
173+
if (intervalRef.current !== null && activeIndex >= maxIndex) {
174+
clearInterval(intervalRef.current)
175+
intervalRef.current = null
176+
}
177+
}, [activeIndex, maxIndex])
178+
179+
const onPlayClick = useCallback(() => {
180+
if (activeIndex >= maxIndex) {
181+
setActiveIndex(0)
182+
}
183+
if (maxIndex > 0 && intervalRef.current === null) {
184+
intervalRef.current = setInterval(() => {
185+
setActiveIndex((prev) => Math.min(prev + 1, maxIndex))
186+
}, duration)
187+
}
188+
}, [activeIndex, maxIndex, duration])
189+
190+
const onPauseClick = () => {
191+
if (intervalRef.current !== null) {
192+
clearInterval(intervalRef.current)
193+
intervalRef.current = null
194+
}
195+
}
196+
197+
const onDurationChange = useCallback(
198+
(event: ChangeEvent<HTMLInputElement>) => {
199+
const newValue =
200+
event.target?.value === "" ? "" : Number(event.target?.value)
201+
if (typeof newValue === "number") {
202+
setDuration(newValue)
203+
}
204+
},
205+
[],
206+
)
207+
208+
const onSliderChange = (_: Event, value: number | number[]) => {
209+
if (typeof value === "number") {
210+
setActiveIndex(value)
211+
}
212+
}
213+
214+
const plotData = [
215+
{
216+
z: frame,
217+
type: "heatmap",
218+
colorscale: "Greys",
219+
reversescale: true,
220+
},
221+
]
222+
223+
const layout = {
224+
title: `Frame ${activeIndex + 1} / ${data.length} (of ${totalFrames} total)`,
225+
yaxis: { autorange: "reversed" as const },
226+
autosize: true,
227+
}
228+
229+
return (
230+
<Box>
231+
<PlotlyChart data={plotData} layout={layout} />
232+
{data.length > 1 && (
233+
<>
234+
<Button sx={{ mt: 1.5 }} variant="outlined" onClick={onPlayClick}>
235+
Play
236+
</Button>
237+
<Button
238+
sx={{ mt: 1.5, ml: 1 }}
239+
variant="outlined"
240+
onClick={onPauseClick}
241+
>
242+
Pause
243+
</Button>
244+
<TextField
245+
sx={{ width: 100, ml: 2 }}
246+
label="msec/frame"
247+
type="number"
248+
inputProps={{
249+
step: 100,
250+
min: 0,
251+
max: 1000,
252+
}}
253+
InputLabelProps={{
254+
shrink: true,
255+
}}
256+
onChange={onDurationChange}
257+
value={duration}
258+
/>
259+
<Slider
260+
aria-label="Custom marks"
261+
value={activeIndex}
262+
valueLabelDisplay="auto"
263+
step={1}
264+
marks
265+
min={0}
266+
max={maxIndex}
267+
onChange={onSliderChange}
268+
/>
269+
</>
270+
)}
271+
</Box>
272+
)
273+
})
274+
275+
const BarView = memo(function BarView({ data }: { data: number[] }) {
276+
const plotData = [
277+
{
278+
x: data.map((_, i) => i),
279+
y: data,
280+
type: "bar",
281+
},
282+
]
283+
284+
const layout = {
285+
title: "Values",
286+
xaxis: { title: "Index" },
287+
yaxis: { title: "Value" },
288+
autosize: true,
289+
}
290+
291+
return <PlotlyChart data={plotData} layout={layout} />
292+
})

frontend/src/components/Workspace/Visualize/VisualizeItemEditor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ const DisplayEditor: FC<{
6060
case DATA_TYPE_SET.LINE:
6161
case DATA_TYPE_SET.PIE:
6262
case DATA_TYPE_SET.POLAR:
63+
case DATA_TYPE_SET.HDF5:
64+
case DATA_TYPE_SET.MATLAB:
6365
return <SaveFig />
6466
case DATA_TYPE_SET.HTML:
6567
return <div>html editor</div>

frontend/src/components/Workspace/__tests__/Experiment/ExperimentTable.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ describe("ExperimentTable", () => {
132132
line: {},
133133
pie: {},
134134
polar: {},
135+
structured: {},
135136
loading: false,
136137
statusRoi: {
137138
temp_add_roi: [],

0 commit comments

Comments
 (0)