Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions frontend/src/api/outputs/Outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,34 @@ export async function getHistogramDataApi(
return response.data
}

export type StructuredData = {
data: number[] | number[][] | number[][][]
data_type: "timeseries" | "images" | "bar"
columns?: string[]
index?: (string | number)[]
total_frames?: number
dataset_path?: string
}

export async function getStructuredDataApi(
workspaceId: string,
uniqueId: string,
nodeId: string,
startIndex?: number,
endIndex?: number,
): Promise<StructuredData> {
const response = await axios.get(
`${BASE_URL}/outputs/structured/${workspaceId}/${uniqueId}/${nodeId}`,
{
params: {
start_index: startIndex,
end_index: endIndex,
},
},
)
return response.data
}

export type PieData = number[][]

export async function getPieDataApi(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PiePlot } from "components/Workspace/Visualize/Plot/PiePlot"
import { PolarPlot } from "components/Workspace/Visualize/Plot/PolarPlot"
import { RoiPlot } from "components/Workspace/Visualize/Plot/RoiPlot"
import { ScatterPlot } from "components/Workspace/Visualize/Plot/ScatterPlot"
import { StructuredFilePlot } from "components/Workspace/Visualize/Plot/StructuredFilePlot"
import { TimeSeriesPlot } from "components/Workspace/Visualize/Plot/TimeSeriesPlot"
import {
DATA_TYPE,
Expand Down Expand Up @@ -77,6 +78,9 @@ const DisplayPlot = memo(function DisplayPlot({ dataType }: DataTypeProps) {
return <PiePlot />
case DATA_TYPE_SET.POLAR:
return <PolarPlot />
case DATA_TYPE_SET.HDF5:
case DATA_TYPE_SET.MATLAB:
return <StructuredFilePlot />
default:
return null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
import {
ChangeEvent,
memo,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react"
import PlotlyChart from "react-plotlyjs-ts"
import { useDispatch, useSelector } from "react-redux"

import {
Box,
Button,
LinearProgress,
Slider,
TextField,
Typography,
} from "@mui/material"

import { DisplayDataContext } from "components/Workspace/Visualize/DataContext"
import { getStructuredData } from "store/slice/DisplayData/DisplayDataActions"
import { selectPipelineLatestUid } from "store/slice/Pipeline/PipelineSelectors"
import { selectCurrentWorkspaceId } from "store/slice/Workspace/WorkspaceSelector"
import { AppDispatch, RootState } from "store/store"

const DEFAULT_START_INDEX = 0
const DEFAULT_END_INDEX = 10

export const StructuredFilePlot = memo(function StructuredFilePlot() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plotting of various images is good.

Also, it might be useful to display the internal paths of the structured data along with the plot.
*Please skip this suggestion if it's not particularly useful.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backend now returns dataset_path (e.g. data/image, data/behavior) in every response. The component shows it as a caption above the plot.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The content seems fine.
I only made a slight adjustment to the label's display style.

image

const { nodeId, itemId } = useContext(DisplayDataContext)
const dispatch = useDispatch<AppDispatch>()

const workspaceId = useSelector(selectCurrentWorkspaceId)
const uid = useSelector(selectPipelineLatestUid)

const structuredState = useSelector(
(state: RootState) => state.displayData.structured[itemId],
)

useEffect(() => {
if (workspaceId && uid && nodeId) {
dispatch(
getStructuredData({
workspaceId: String(workspaceId),
uniqueId: uid,
nodeId,
itemId,
startIndex: DEFAULT_START_INDEX,
endIndex: DEFAULT_END_INDEX,
}),
)
}
}, [dispatch, workspaceId, uid, nodeId, itemId])

if (!uid || !nodeId) {
return (
<Box p={2}>
<Typography color="text.secondary">
No workflow run available. Please run the workflow first.
</Typography>
</Box>
)
}

if (!structuredState || structuredState.pending) {
return <LinearProgress />
}

if (structuredState.error) {
return (
<Box p={2}>
<Typography color="error">
Error loading data: {structuredState.error}
</Typography>
</Box>
)
}

const result = structuredState.data
if (!result) return null

return (
<Box>
{result.dataset_path && (
<Typography
variant="caption"
color="text.secondary"
title={result.dataset_path}
noWrap
sx={{ px: 1, display: "block", maxWidth: "100%" }}
>
{result.dataset_path}
</Typography>
)}
<DataView result={result} />
</Box>
)
})

const DataView = memo(function DataView({
result,
}: {
result: {
data_type: string
data: number[] | number[][] | number[][][]
total_frames?: number
}
}) {
switch (result.data_type) {
case "timeseries":
return <TimeSeriesView data={result.data as number[][]} />
case "images":
return (
<ImageView
data={result.data as number[][][]}
totalFrames={
result.total_frames ?? (result.data as number[][][]).length
}
/>
)
case "bar":
return <BarView data={result.data as number[]} />
default:
return <Typography>Unsupported data type: {result.data_type}</Typography>
}
})

const TimeSeriesView = memo(function TimeSeriesView({
data,
}: {
data: number[][]
}) {
const traces = []
if (data.length > 0) {
const numCols = data[0].length
for (let col = 0; col < numCols; col++) {
traces.push({
y: data.map((row) => row[col]),
type: "scatter",
mode: "lines",
name: `col ${col}`,
})
}
}

const layout = {
title: "Time Series",
xaxis: { title: "Frame" },
yaxis: { title: "Value" },
autosize: true,
}

return <PlotlyChart data={traces} layout={layout} />
})

const ImageView = memo(function ImageView({
data,
totalFrames,
}: {
data: number[][][]
totalFrames: number
}) {
const [activeIndex, setActiveIndex] = useState(0)
const [duration, setDuration] = useState(500)
const intervalRef = useRef<null | NodeJS.Timeout>(null)
const maxIndex = data.length - 1

const frame = data[activeIndex] || []

useEffect(() => {
if (intervalRef.current !== null && activeIndex >= maxIndex) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [activeIndex, maxIndex])

const onPlayClick = useCallback(() => {
if (activeIndex >= maxIndex) {
setActiveIndex(0)
}
if (maxIndex > 0 && intervalRef.current === null) {
intervalRef.current = setInterval(() => {
setActiveIndex((prev) => Math.min(prev + 1, maxIndex))
}, duration)
}
}, [activeIndex, maxIndex, duration])

const onPauseClick = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}

const onDurationChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const newValue =
event.target?.value === "" ? "" : Number(event.target?.value)
if (typeof newValue === "number") {
setDuration(newValue)
}
},
[],
)

const onSliderChange = (_: Event, value: number | number[]) => {
if (typeof value === "number") {
setActiveIndex(value)
}
}

const plotData = [
{
z: frame,
type: "heatmap",
colorscale: "Greys",
reversescale: true,
},
]

const layout = {
title: `Frame ${activeIndex + 1} / ${data.length} (of ${totalFrames} total)`,
yaxis: { autorange: "reversed" as const },
autosize: true,
}

return (
<Box>
<PlotlyChart data={plotData} layout={layout} />
{data.length > 1 && (
<>
<Button sx={{ mt: 1.5 }} variant="outlined" onClick={onPlayClick}>
Play
</Button>
<Button
sx={{ mt: 1.5, ml: 1 }}
variant="outlined"
onClick={onPauseClick}
>
Pause
</Button>
<TextField
sx={{ width: 100, ml: 2 }}
label="msec/frame"
type="number"
inputProps={{
step: 100,
min: 0,
max: 1000,
}}
InputLabelProps={{
shrink: true,
}}
onChange={onDurationChange}
value={duration}
/>
<Slider
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Play slider's style is slightly different from existing image plots, etc. Is that okay?

*While it may not be extremely important, I think keeping the UI somewhat similar will make it easier for users to understand.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the simple with the full PlayBack pattern from ImagePlot — Play/Pause buttons, msec/frame TextField, and the same with marks, valueLabelDisplay="auto", and step={1}.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The content seems fine.

I made some minor code adjustments here.

  • Componentizing the player control
  • Style adjustments (similar to ImagePlot)
image

aria-label="Custom marks"
value={activeIndex}
valueLabelDisplay="auto"
step={1}
marks
min={0}
max={maxIndex}
onChange={onSliderChange}
/>
</>
)}
</Box>
)
})

const BarView = memo(function BarView({ data }: { data: number[] }) {
const plotData = [
{
x: data.map((_, i) => i),
y: data,
type: "bar",
},
]

const layout = {
title: "Values",
xaxis: { title: "Index" },
yaxis: { title: "Value" },
autosize: true,
}

return <PlotlyChart data={plotData} layout={layout} />
})
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const DisplayEditor: FC<{
case DATA_TYPE_SET.LINE:
case DATA_TYPE_SET.PIE:
case DATA_TYPE_SET.POLAR:
case DATA_TYPE_SET.HDF5:
case DATA_TYPE_SET.MATLAB:
return <SaveFig />
case DATA_TYPE_SET.HTML:
return <div>html editor</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ describe("ExperimentTable", () => {
line: {},
pie: {},
polar: {},
structured: {},
loading: false,
statusRoi: {
temp_add_roi: [],
Expand Down
Loading
Loading