From 479c5cd3f41d13f027eaa6b69f72f9715b126230 Mon Sep 17 00:00:00 2001 From: ReiHashimoto <42664619+ReiHashimoto@users.noreply.github.com> Date: Wed, 23 Aug 2023 19:35:43 +0900 Subject: [PATCH 1/4] add plot meta data --- frontend/src/api/outputs/Outputs.ts | 22 ++++++++++++------- .../Workspace/Visualize/Plot/BarPlot.tsx | 18 ++++++++++++--- .../Workspace/Visualize/Plot/HeatMapPlot.tsx | 18 ++++++++++++--- .../Workspace/Visualize/Plot/ScatterPlot.tsx | 17 +++++++++----- .../slice/DisplayData/DisplayDataActions.ts | 12 +++++++--- .../slice/DisplayData/DisplayDataSelectors.ts | 9 ++++++++ .../slice/DisplayData/DisplayDataSlice.ts | 3 +++ .../slice/DisplayData/DisplayDataType.ts | 12 +++++++++- studio/app/common/core/utils/file_reader.py | 7 +++++- studio/app/common/core/utils/json_writer.py | 17 ++++++++++---- studio/app/common/dataclass/bar.py | 10 +++++++-- studio/app/common/dataclass/heatmap.py | 14 ++++++++++-- studio/app/common/dataclass/scatter.py | 8 +++++-- studio/app/common/schemas/outputs.py | 15 +++++++++++-- 14 files changed, 145 insertions(+), 37 deletions(-) diff --git a/frontend/src/api/outputs/Outputs.ts b/frontend/src/api/outputs/Outputs.ts index 83fe804b9..d44fe8e55 100644 --- a/frontend/src/api/outputs/Outputs.ts +++ b/frontend/src/api/outputs/Outputs.ts @@ -1,5 +1,5 @@ import axios from 'utils/axios' - +import { PlotMetaData } from 'store/slice/DisplayData/DisplayDataType' import { BASE_URL } from 'const/API' export type TimeSeriesData = { @@ -36,9 +36,12 @@ export async function getTimeSeriesAllDataApi( export type HeatMapData = number[][] -export async function getHeatMapDataApi( - path: string, -): Promise<{ data: HeatMapData; columns: string[]; index: string[] }> { +export async function getHeatMapDataApi(path: string): Promise<{ + data: HeatMapData + columns: string[] + index: string[] + meta?: PlotMetaData +}> { const response = await axios.get(`${BASE_URL}/outputs/data/${path}`) return response.data } @@ -96,7 +99,7 @@ export type ScatterData = { export async function getScatterDataApi( path: string, -): Promise<{ data: ScatterData }> { +): Promise<{ data: ScatterData; meta?: PlotMetaData }> { const response = await axios.get(`${BASE_URL}/outputs/data/${path}`, {}) return response.data } @@ -107,9 +110,12 @@ export type BarData = { } } -export async function getBarDataApi( - path: string, -): Promise<{ data: BarData; columns: string[]; index: string[] }> { +export async function getBarDataApi(path: string): Promise<{ + data: BarData + columns: string[] + index: string[] + meta?: PlotMetaData +}> { const response = await axios.get(`${BASE_URL}/outputs/data/${path}`, {}) return response.data } diff --git a/frontend/src/components/Workspace/Visualize/Plot/BarPlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/BarPlot.tsx index 2a99ba1af..f4c9aa122 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/BarPlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/BarPlot.tsx @@ -17,6 +17,7 @@ import { selectBarDataIsInitialized, selectBarDataIsPending, selectBarIndex, + selectBarMeta, } from 'store/slice/DisplayData/DisplayDataSelectors' import { getBarData } from 'store/slice/DisplayData/DisplayDataActions' import { BarData } from 'api/outputs/Outputs' @@ -55,6 +56,7 @@ export const BarPlot = React.memo(() => { const BarPlotImple = React.memo(() => { const { filePath: path, itemId } = React.useContext(DisplayDataContext) const barData = useSelector(selectBarData(path), barDataEqualityFn) + const meta = useSelector(selectBarMeta(path)) const width = useSelector(selectVisualizeItemWidth(itemId)) const height = useSelector(selectVisualizeItemHeight(itemId)) const index = useSelector(selectBarItemIndex(itemId)) @@ -73,17 +75,27 @@ const BarPlotImple = React.memo(() => { const layout = React.useMemo( () => ({ + title: { + text: meta?.title, + x: 0.1, + }, width: width, height: height - 120, margin: { - t: 60, // top + t: 50, // top l: 50, // left - b: 30, // bottom + b: 40, // bottom }, dragmode: 'pan', autosize: true, + xaxis: { + title: meta?.xlabel, + }, + yaxis: { + title: meta?.ylabel, + }, }), - [width, height], + [meta, width, height], ) const saveFileName = useSelector(selectVisualizeSaveFilename(itemId)) diff --git a/frontend/src/components/Workspace/Visualize/Plot/HeatMapPlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/HeatMapPlot.tsx index b4e34cea9..efdd854ba 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/HeatMapPlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/HeatMapPlot.tsx @@ -13,6 +13,7 @@ import { selectHeatMapDataIsInitialized, selectHeatMapDataIsPending, selectHeatMapIndex, + selectHeatMapMeta, } from 'store/slice/DisplayData/DisplayDataSelectors' import { getHeatMapData } from 'store/slice/DisplayData/DisplayDataActions' import { @@ -50,6 +51,7 @@ export const HeatMapPlot = React.memo(() => { const HeatMapImple = React.memo(() => { const { filePath: path, itemId } = React.useContext(DisplayDataContext) const heatMapData = useSelector(selectHeatMapData(path), heatMapDataEqualtyFn) + const meta = useSelector(selectHeatMapMeta(path)) const columns = useSelector(selectHeatMapColumns(path)) const index = useSelector(selectHeatMapIndex(path)) const showscale = useSelector(selectHeatMapItemShowScale(itemId)) @@ -92,17 +94,27 @@ const HeatMapImple = React.memo(() => { const layout = React.useMemo( () => ({ + title: { + text: meta?.title, + x: 0.1, + }, width: width, height: height - 50, dragmode: 'pan', margin: { - t: 60, // top + t: 50, // top l: 50, // left - b: 30, // bottom + b: 40, // bottom }, autosize: true, + xaxis: { + title: meta?.xlabel, + }, + yaxis: { + title: meta?.ylabel, + }, }), - [width, height], + [meta, width, height], ) const saveFileName = useSelector(selectVisualizeSaveFilename(itemId)) diff --git a/frontend/src/components/Workspace/Visualize/Plot/ScatterPlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/ScatterPlot.tsx index e807d3a16..ee86b5ae0 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/ScatterPlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/ScatterPlot.tsx @@ -16,6 +16,7 @@ import { selectScatterDataIsFulfilled, selectScatterDataIsInitialized, selectScatterDataIsPending, + selectScatterMeta, } from 'store/slice/DisplayData/DisplayDataSelectors' import { getScatterData } from 'store/slice/DisplayData/DisplayDataActions' import { ScatterData } from 'api/outputs/Outputs' @@ -62,7 +63,7 @@ const ScatterPlotImple = React.memo(() => { selectScatterData(path), scatterDataEqualityFn, ) - + const meta = useSelector(selectScatterMeta(path)) const xIndex = useSelector(selectScatterItemXIndex(itemId)) const yIndex = useSelector(selectScatterItemYIndex(itemId)) const width = useSelector(selectVisualizeItemWidth(itemId)) @@ -91,18 +92,22 @@ const ScatterPlotImple = React.memo(() => { const layout = React.useMemo( () => ({ + title: { + text: meta?.title, + x: 0.1, + }, width: width, height: height - 120, margin: { - t: 60, // top + t: 50, // top l: 50, // left - b: 30, // bottom + b: 40, // bottom }, dragmode: 'pan', autosize: true, xaxis: { title: { - text: `x: ${xIndex}`, + text: meta?.xlabel ?? `x: ${xIndex}`, font: { family: 'Courier New, monospace', size: 18, @@ -112,7 +117,7 @@ const ScatterPlotImple = React.memo(() => { }, yaxis: { title: { - text: `y: ${yIndex}`, + text: meta?.ylabel ?? `y: ${yIndex}`, font: { family: 'Courier New, monospace', size: 18, @@ -121,7 +126,7 @@ const ScatterPlotImple = React.memo(() => { }, }, }), - [xIndex, yIndex, width, height], + [meta, xIndex, yIndex, width, height], ) const saveFileName = useSelector(selectVisualizeSaveFilename(itemId)) diff --git a/frontend/src/store/slice/DisplayData/DisplayDataActions.ts b/frontend/src/store/slice/DisplayData/DisplayDataActions.ts index b86672a2d..dc6cfb8f0 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataActions.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataActions.ts @@ -27,6 +27,7 @@ import { getPieDataApi, getPolarDataApi, } from 'api/outputs/Outputs' +import { PlotMetaData } from './DisplayDataType' import { DISPLAY_DATA_SLICE_NAME } from './DisplayDataType' export const getTimeSeriesInitData = createAsyncThunk< @@ -75,7 +76,12 @@ export const getTimeSeriesAllData = createAsyncThunk< ) export const getHeatMapData = createAsyncThunk< - { data: HeatMapData; columns: string[]; index: string[] }, + { + data: HeatMapData + columns: string[] + index: string[] + meta?: PlotMetaData + }, { path: string } >(`${DISPLAY_DATA_SLICE_NAME}/getHeatMapData`, async ({ path }, thunkAPI) => { try { @@ -138,7 +144,7 @@ export const getRoiData = createAsyncThunk< ) export const getScatterData = createAsyncThunk< - { data: ScatterData }, + { data: ScatterData; meta?: PlotMetaData }, { path: string } >(`${DISPLAY_DATA_SLICE_NAME}/getScatterData`, async ({ path }, thunkAPI) => { try { @@ -150,7 +156,7 @@ export const getScatterData = createAsyncThunk< }) export const getBarData = createAsyncThunk< - { data: BarData; columns: string[]; index: string[] }, + { data: BarData; columns: string[]; index: string[]; meta?: PlotMetaData }, { path: string } >(`${DISPLAY_DATA_SLICE_NAME}/getBarData`, async ({ path }, thunkAPI) => { try { diff --git a/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts b/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts index 4d2b1b248..cf8a3aceb 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts @@ -35,6 +35,9 @@ export const selectTimeSeriesDataError = export const selectHeatMapData = (filePath: string) => (state: RootState) => selectDisplayData(state).heatMap[filePath].data +export const selectHeatMapMeta = (filePath: string) => (state: RootState) => + selectDisplayData(state).heatMap[filePath].meta + export const selectHeatMapColumns = (filePath: string) => (state: RootState) => selectDisplayData(state).heatMap[filePath].columns @@ -151,6 +154,9 @@ export const selectRoiUniqueList = (filePath: string) => (state: RootState) => { export const selectScatterData = (filePath: string) => (state: RootState) => selectDisplayData(state).scatter[filePath]?.data ?? [] +export const selectScatterMeta = (filePath: string) => (state: RootState) => + selectDisplayData(state).scatter[filePath]?.meta + export const selectScatterDataIsInitialized = (filePath: string) => (state: RootState) => Object.keys(selectDisplayData(state).scatter).includes(filePath) @@ -174,6 +180,9 @@ export const selectScatterDataIsFulfilled = export const selectBarData = (filePath: string) => (state: RootState) => selectDisplayData(state).bar[filePath]?.data ?? [] +export const selectBarMeta = (filePath: string) => (state: RootState) => + selectDisplayData(state).bar[filePath]?.meta + export const selectBarIndex = (filePath: string) => (state: RootState) => selectDisplayData(state).bar[filePath]?.index ?? [] diff --git a/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts b/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts index 55f5a6caf..6fb5952f8 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts @@ -211,6 +211,7 @@ export const displayDataSlice = createSlice({ state.heatMap[path] = { type: 'heatMap', data: action.payload.data, + meta: action.payload.meta, columns: action.payload.columns, index: action.payload.index, pending: false, @@ -473,6 +474,7 @@ export const displayDataSlice = createSlice({ state.scatter[path] = { type: 'scatter', data: action.payload.data, + meta: action.payload.meta, pending: false, fulfilled: true, error: null, @@ -517,6 +519,7 @@ export const displayDataSlice = createSlice({ state.bar[path] = { type: 'bar', data: action.payload.data, + meta: action.payload.meta, columns: action.payload.columns, index: action.payload.index, pending: false, diff --git a/frontend/src/store/slice/DisplayData/DisplayDataType.ts b/frontend/src/store/slice/DisplayData/DisplayDataType.ts index b1178f05d..f6fd31f31 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataType.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataType.ts @@ -77,6 +77,12 @@ export const DATA_TYPE_SET = { export type DATA_TYPE = typeof DATA_TYPE_SET[keyof typeof DATA_TYPE_SET] +export type PlotMetaData = { + xlabel?: string + ylabel?: string + title?: string +} + interface BaseDisplay { type: T data: Data @@ -95,6 +101,7 @@ export interface HeatMapDisplayData extends BaseDisplay<'heatMap', HeatMapData> { columns: string[] index: string[] + meta?: PlotMetaData } export interface ImageDisplayData extends BaseDisplay<'image', ImageData> {} @@ -108,11 +115,14 @@ export interface RoiDisplayData extends BaseDisplay<'roi', RoiData> { } export interface ScatterDisplayData - extends BaseDisplay<'scatter', ScatterData> {} + extends BaseDisplay<'scatter', ScatterData> { + meta?: PlotMetaData +} export interface BarDisplayData extends BaseDisplay<'bar', BarData> { columns: string[] index: string[] + meta?: PlotMetaData } export interface HTMLDisplayData extends BaseDisplay<'html', HTMLData> {} diff --git a/studio/app/common/core/utils/file_reader.py b/studio/app/common/core/utils/file_reader.py index 52006beb7..e803c804e 100644 --- a/studio/app/common/core/utils/file_reader.py +++ b/studio/app/common/core/utils/file_reader.py @@ -1,6 +1,10 @@ import json -from studio.app.common.schemas.outputs import JsonTimeSeriesData, OutputData +from studio.app.common.schemas.outputs import ( + JsonTimeSeriesData, + OutputData, + PlotMetaData, +) class Reader: @@ -29,6 +33,7 @@ def read_as_output(cls, filepath) -> OutputData: data=json_data["data"], columns=json_data["columns"], index=json_data["index"], + meta=PlotMetaData(**json_data.get("meta", {})), ) @classmethod diff --git a/studio/app/common/core/utils/json_writer.py b/studio/app/common/core/utils/json_writer.py index cc8bd2449..50e603122 100644 --- a/studio/app/common/core/utils/json_writer.py +++ b/studio/app/common/core/utils/json_writer.py @@ -1,4 +1,7 @@ +import json import os +from dataclasses import asdict +from typing import Optional import numpy as np import pandas as pd @@ -8,16 +11,22 @@ create_directory, join_filepath, ) +from studio.app.common.schemas.outputs import PlotMetaData class JsonWriter: @classmethod - def write(cls, filepath, data): - pd.DataFrame(data).to_json(filepath, indent=4) + def write(cls, filepath, data, orient=None, meta: Optional[PlotMetaData] = None): + pd.DataFrame(data).to_json(indent=4) + df = json.loads(pd.DataFrame(data).to_json(indent=4, orient=orient)) + if meta: + df.update({"meta": asdict(meta)}) + with open(filepath, "w") as f: + json.dump(df, f, indent=4) @classmethod - def write_as_split(cls, filepath, data): - pd.DataFrame(data).to_json(filepath, indent=4, orient="split") + def write_as_split(cls, filepath, data, meta: Optional[PlotMetaData] = None): + cls.write(filepath, data, orient="split", meta=meta) def save_tiff2json(tiff_filepath, save_dirpath, start_index=None, end_index=None): diff --git a/studio/app/common/dataclass/bar.py b/studio/app/common/dataclass/bar.py index d80dc2a7a..997775332 100644 --- a/studio/app/common/dataclass/bar.py +++ b/studio/app/common/dataclass/bar.py @@ -1,3 +1,5 @@ +from typing import Optional + import numpy as np import pandas as pd @@ -5,11 +7,15 @@ from studio.app.common.core.utils.json_writer import JsonWriter from studio.app.common.core.workflow.workflow import OutputPath, OutputType from studio.app.common.dataclass.base import BaseData +from studio.app.common.schemas.outputs import PlotMetaData class BarData(BaseData): - def __init__(self, data, index=None, file_name="bar"): + def __init__( + self, data, index=None, file_name="bar", meta: Optional[PlotMetaData] = None + ): super().__init__(file_name) + self.meta = meta data = np.array(data) assert data.ndim <= 2, "Bar Dimension Error" @@ -33,7 +39,7 @@ def save_json(self, json_dir): self.data, index=self.index, ) - JsonWriter.write_as_split(self.json_path, df) + JsonWriter.write_as_split(self.json_path, df, self.meta) @property def output_path(self) -> OutputPath: diff --git a/studio/app/common/dataclass/heatmap.py b/studio/app/common/dataclass/heatmap.py index 76b68e900..5a90f2d15 100644 --- a/studio/app/common/dataclass/heatmap.py +++ b/studio/app/common/dataclass/heatmap.py @@ -1,3 +1,5 @@ +from typing import Optional + import numpy as np import pandas as pd @@ -5,12 +7,20 @@ from studio.app.common.core.utils.json_writer import JsonWriter from studio.app.common.core.workflow.workflow import OutputPath, OutputType from studio.app.common.dataclass.base import BaseData +from studio.app.common.schemas.outputs import PlotMetaData class HeatMapData(BaseData): - def __init__(self, data, columns=None, file_name="heatmap"): + def __init__( + self, + data, + columns=None, + file_name="heatmap", + meta: Optional[PlotMetaData] = None, + ): super().__init__(file_name) self.data = data + self.meta = meta # indexを指定 if columns is not None: @@ -24,7 +34,7 @@ def save_json(self, json_dir): self.data, columns=self.columns, ) - JsonWriter.write_as_split(self.json_path, df) + JsonWriter.write_as_split(self.json_path, df, self.meta) @property def output_path(self) -> OutputPath: diff --git a/studio/app/common/dataclass/scatter.py b/studio/app/common/dataclass/scatter.py index d077c98d9..26d96e9df 100644 --- a/studio/app/common/dataclass/scatter.py +++ b/studio/app/common/dataclass/scatter.py @@ -1,12 +1,16 @@ +from typing import Optional + from studio.app.common.core.utils.filepath_creater import join_filepath from studio.app.common.core.utils.json_writer import JsonWriter from studio.app.common.core.workflow.workflow import OutputPath, OutputType from studio.app.common.dataclass.base import BaseData +from studio.app.common.schemas.outputs import PlotMetaData class ScatterData(BaseData): - def __init__(self, data, file_name="scatter"): + def __init__(self, data, file_name="scatter", meta: Optional[PlotMetaData] = None): super().__init__(file_name) + self.meta = meta assert data.ndim <= 2, "Scatter Dimension Error" @@ -14,7 +18,7 @@ def __init__(self, data, file_name="scatter"): def save_json(self, json_dir): self.json_path = join_filepath([json_dir, f"{self.file_name}.json"]) - JsonWriter.write_as_split(self.json_path, self.data) + JsonWriter.write_as_split(self.json_path, self.data, self.meta) @property def output_path(self) -> OutputPath: diff --git a/studio/app/common/schemas/outputs.py b/studio/app/common/schemas/outputs.py index d6a425630..d61e2d304 100644 --- a/studio/app/common/schemas/outputs.py +++ b/studio/app/common/schemas/outputs.py @@ -1,5 +1,15 @@ -from dataclasses import dataclass -from typing import Dict, List, Union +from dataclasses import asdict, dataclass +from typing import Dict, List, Optional, Union + + +@dataclass +class PlotMetaData: + xlabel: Optional[str] = None + ylabel: Optional[str] = None + title: Optional[str] = None + + def value_present_dict(self): + return {k: v for k, v in asdict(self).items() if v is not None} @dataclass @@ -7,6 +17,7 @@ class OutputData: data: Union[List, Dict, str] columns: List[str] = None index: List[str] = None + meta: Optional[PlotMetaData] = None @dataclass From 65fdf4bb585cc4b7b8745700cf0305297d69b9b1 Mon Sep 17 00:00:00 2001 From: ReiHashimoto <42664619+ReiHashimoto@users.noreply.github.com> Date: Wed, 23 Aug 2023 19:43:35 +0900 Subject: [PATCH 2/4] remove unused class function --- studio/app/common/schemas/outputs.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/studio/app/common/schemas/outputs.py b/studio/app/common/schemas/outputs.py index d61e2d304..ee41942bb 100644 --- a/studio/app/common/schemas/outputs.py +++ b/studio/app/common/schemas/outputs.py @@ -1,4 +1,4 @@ -from dataclasses import asdict, dataclass +from dataclasses import dataclass from typing import Dict, List, Optional, Union @@ -8,9 +8,6 @@ class PlotMetaData: ylabel: Optional[str] = None title: Optional[str] = None - def value_present_dict(self): - return {k: v for k, v in asdict(self).items() if v is not None} - @dataclass class OutputData: From 3df9e1f4d5272f7d18a45477f8e9b2c07b2f2036 Mon Sep 17 00:00:00 2001 From: ReiHashimoto <42664619+ReiHashimoto@users.noreply.github.com> Date: Mon, 28 Aug 2023 13:16:51 +0900 Subject: [PATCH 3/4] separate plot metadata json file --- studio/app/common/core/utils/file_reader.py | 9 ++++++++- studio/app/common/core/utils/json_writer.py | 20 ++++++++++---------- studio/app/common/dataclass/bar.py | 4 +++- studio/app/common/dataclass/heatmap.py | 4 +++- studio/app/common/dataclass/scatter.py | 4 +++- studio/app/common/schemas/outputs.py | 5 ++++- 6 files changed, 31 insertions(+), 15 deletions(-) diff --git a/studio/app/common/core/utils/file_reader.py b/studio/app/common/core/utils/file_reader.py index e803c804e..82461f12a 100644 --- a/studio/app/common/core/utils/file_reader.py +++ b/studio/app/common/core/utils/file_reader.py @@ -1,4 +1,5 @@ import json +import os from studio.app.common.schemas.outputs import ( JsonTimeSeriesData, @@ -29,11 +30,17 @@ def read(cls, filepath): @classmethod def read_as_output(cls, filepath) -> OutputData: json_data = cls.read(filepath) + plot_metadata_path = f"{os.path.splitext(filepath)[0]}.plot-meta.json" + + plot_metadata = ( + cls.read(plot_metadata_path) if os.path.exists(plot_metadata_path) else {} + ) + return OutputData( data=json_data["data"], columns=json_data["columns"], index=json_data["index"], - meta=PlotMetaData(**json_data.get("meta", {})), + meta=PlotMetaData(**plot_metadata), ) @classmethod diff --git a/studio/app/common/core/utils/json_writer.py b/studio/app/common/core/utils/json_writer.py index 50e603122..4be5d2b04 100644 --- a/studio/app/common/core/utils/json_writer.py +++ b/studio/app/common/core/utils/json_writer.py @@ -1,6 +1,5 @@ import json import os -from dataclasses import asdict from typing import Optional import numpy as np @@ -16,17 +15,18 @@ class JsonWriter: @classmethod - def write(cls, filepath, data, orient=None, meta: Optional[PlotMetaData] = None): - pd.DataFrame(data).to_json(indent=4) - df = json.loads(pd.DataFrame(data).to_json(indent=4, orient=orient)) - if meta: - df.update({"meta": asdict(meta)}) - with open(filepath, "w") as f: - json.dump(df, f, indent=4) + def write(cls, filepath, data): + pd.DataFrame(data).to_json(filepath, indent=4) @classmethod - def write_as_split(cls, filepath, data, meta: Optional[PlotMetaData] = None): - cls.write(filepath, data, orient="split", meta=meta) + def write_as_split(cls, filepath, data): + pd.DataFrame(data).to_json(filepath, indent=4, orient="split") + + @classmethod + def write_plot_meta(cls, filepath, data: Optional[PlotMetaData]): + if data is not None: + with open(filepath, "w") as f: + json.dump(data.value_present_dict(), f, indent=4) def save_tiff2json(tiff_filepath, save_dirpath, start_index=None, end_index=None): diff --git a/studio/app/common/dataclass/bar.py b/studio/app/common/dataclass/bar.py index 997775332..7209845de 100644 --- a/studio/app/common/dataclass/bar.py +++ b/studio/app/common/dataclass/bar.py @@ -39,7 +39,9 @@ def save_json(self, json_dir): self.data, index=self.index, ) - JsonWriter.write_as_split(self.json_path, df, self.meta) + JsonWriter.write_as_split(self.json_path, df) + plot_meta_path = join_filepath([json_dir, f"{self.file_name}.plot-meta.json"]) + JsonWriter.write_plot_meta(plot_meta_path, self.meta) @property def output_path(self) -> OutputPath: diff --git a/studio/app/common/dataclass/heatmap.py b/studio/app/common/dataclass/heatmap.py index 5a90f2d15..e0069ae09 100644 --- a/studio/app/common/dataclass/heatmap.py +++ b/studio/app/common/dataclass/heatmap.py @@ -34,7 +34,9 @@ def save_json(self, json_dir): self.data, columns=self.columns, ) - JsonWriter.write_as_split(self.json_path, df, self.meta) + JsonWriter.write_as_split(self.json_path, df) + plot_meta_path = join_filepath([json_dir, f"{self.file_name}.plot-meta.json"]) + JsonWriter.write_plot_meta(plot_meta_path, self.meta) @property def output_path(self) -> OutputPath: diff --git a/studio/app/common/dataclass/scatter.py b/studio/app/common/dataclass/scatter.py index 26d96e9df..71f725091 100644 --- a/studio/app/common/dataclass/scatter.py +++ b/studio/app/common/dataclass/scatter.py @@ -18,7 +18,9 @@ def __init__(self, data, file_name="scatter", meta: Optional[PlotMetaData] = Non def save_json(self, json_dir): self.json_path = join_filepath([json_dir, f"{self.file_name}.json"]) - JsonWriter.write_as_split(self.json_path, self.data, self.meta) + JsonWriter.write_as_split(self.json_path, self.data) + plot_meta_path = join_filepath([json_dir, f"{self.file_name}.plot-meta.json"]) + JsonWriter.write_plot_meta(plot_meta_path, self.meta) @property def output_path(self) -> OutputPath: diff --git a/studio/app/common/schemas/outputs.py b/studio/app/common/schemas/outputs.py index ee41942bb..d61e2d304 100644 --- a/studio/app/common/schemas/outputs.py +++ b/studio/app/common/schemas/outputs.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import asdict, dataclass from typing import Dict, List, Optional, Union @@ -8,6 +8,9 @@ class PlotMetaData: ylabel: Optional[str] = None title: Optional[str] = None + def value_present_dict(self): + return {k: v for k, v in asdict(self).items() if v is not None} + @dataclass class OutputData: From 3a1785caceff42faf274397ca138c316a148f9b9 Mon Sep 17 00:00:00 2001 From: ReiHashimoto <42664619+ReiHashimoto@users.noreply.github.com> Date: Mon, 28 Aug 2023 15:43:23 +0900 Subject: [PATCH 4/4] apply to other dataclasses --- frontend/src/api/outputs/Outputs.ts | 39 +++++++++++------- .../Workspace/Visualize/Plot/ImagePlot.tsx | 10 ++++- .../Workspace/Visualize/Plot/RoiPlot.tsx | 10 ++++- .../Visualize/Plot/TimeSeriesPlot.tsx | 10 ++++- .../slice/DisplayData/DisplayDataActions.ts | 31 +++++++++----- .../slice/DisplayData/DisplayDataSelectors.ts | 15 +++++++ .../slice/DisplayData/DisplayDataSlice.ts | 7 ++++ .../slice/DisplayData/DisplayDataType.ts | 7 +--- studio/app/common/core/utils/file_reader.py | 12 +++--- studio/app/common/core/utils/json_writer.py | 3 +- studio/app/common/dataclass/bar.py | 3 +- studio/app/common/dataclass/csv.py | 9 ++++- studio/app/common/dataclass/heatmap.py | 3 +- studio/app/common/dataclass/html.py | 8 +++- studio/app/common/dataclass/image.py | 12 +++++- studio/app/common/dataclass/scatter.py | 3 +- studio/app/common/dataclass/timeseries.py | 13 +++++- studio/app/common/routers/outputs.py | 40 +++++++++---------- studio/app/optinist/dataclass/caiman.py | 9 ++++- studio/app/optinist/dataclass/fluo.py | 13 +++++- studio/app/optinist/dataclass/lccd.py | 7 +++- studio/app/optinist/dataclass/roi.py | 12 +++++- studio/app/optinist/dataclass/suite2p.py | 7 +++- 23 files changed, 210 insertions(+), 73 deletions(-) diff --git a/frontend/src/api/outputs/Outputs.ts b/frontend/src/api/outputs/Outputs.ts index d44fe8e55..f27a96427 100644 --- a/frontend/src/api/outputs/Outputs.ts +++ b/frontend/src/api/outputs/Outputs.ts @@ -8,9 +8,12 @@ export type TimeSeriesData = { } } -export async function getTimeSeriesInitDataApi( - path: string, -): Promise<{ data: TimeSeriesData; xrange: string[]; std: TimeSeriesData }> { +export async function getTimeSeriesInitDataApi(path: string): Promise<{ + data: TimeSeriesData + xrange: string[] + std: TimeSeriesData + meta?: PlotMetaData +}> { const response = await axios.get(`${BASE_URL}/outputs/inittimedata/${path}`) return response.data } @@ -18,7 +21,12 @@ export async function getTimeSeriesInitDataApi( export async function getTimeSeriesDataByIdApi( path: string, index: string, -): Promise<{ data: TimeSeriesData; xrange: string[]; std: TimeSeriesData }> { +): Promise<{ + data: TimeSeriesData + xrange: string[] + std: TimeSeriesData + meta?: PlotMetaData +}> { const response = await axios.get(`${BASE_URL}/outputs/timedata/${path}`, { params: { index: index, @@ -27,9 +35,12 @@ export async function getTimeSeriesDataByIdApi( return response.data } -export async function getTimeSeriesAllDataApi( - path: string, -): Promise<{ data: TimeSeriesData; xrange: string[]; std: TimeSeriesData }> { +export async function getTimeSeriesAllDataApi(path: string): Promise<{ + data: TimeSeriesData + xrange: string[] + std: TimeSeriesData + meta?: PlotMetaData +}> { const response = await axios.get(`${BASE_URL}/outputs/alltimedata/${path}`) return response.data } @@ -55,7 +66,7 @@ export async function getImageDataApi( startIndex?: number endIndex?: number }, -): Promise<{ data: ImageData }> { +): Promise<{ data: ImageData; meta?: PlotMetaData }> { const response = await axios.get(`${BASE_URL}/outputs/image/${path}`, { params: { workspace_id: params.workspaceId, @@ -71,7 +82,7 @@ export type CsvData = number[][] export async function getCsvDataApi( path: string, params: { workspaceId: number }, -): Promise<{ data: CsvData }> { +): Promise<{ data: CsvData; meta?: PlotMetaData }> { const response = await axios.get(`${BASE_URL}/outputs/csv/${path}`, { params: { workspace_id: params.workspaceId }, }) @@ -84,7 +95,7 @@ export type RoiData = number[][][] export async function getRoiDataApi( path: string, params: { workspaceId: number }, -): Promise<{ data: RoiData }> { +): Promise<{ data: RoiData; meta?: PlotMetaData }> { const response = await axios.get(`${BASE_URL}/outputs/image/${path}`, { params: { workspace_id: params.workspaceId }, }) @@ -124,7 +135,7 @@ export type HTMLData = string export async function getHTMLDataApi( path: string, -): Promise<{ data: HTMLData }> { +): Promise<{ data: HTMLData; meta?: PlotMetaData }> { const response = await axios.get(`${BASE_URL}/outputs/html/${path}`, {}) return response.data } @@ -132,7 +143,7 @@ export async function getHTMLDataApi( export async function addRoiApi( path: string, data: { posx: number; posy: number; sizex: number; sizey: number }, -): Promise<{ data: HTMLData }> { +): Promise<{ data: HTMLData; meta?: PlotMetaData }> { const response = await axios.post( `${BASE_URL}/outputs/image/${path}/add_roi`, data, @@ -143,7 +154,7 @@ export async function addRoiApi( export async function mergeRoiApi( path: string, data: { ids: number[] }, -): Promise<{ data: HTMLData }> { +): Promise<{ data: HTMLData; meta?: PlotMetaData }> { const response = await axios.post( `${BASE_URL}/outputs/image/${path}/merge_roi`, data, @@ -154,7 +165,7 @@ export async function mergeRoiApi( export async function deleteRoiApi( path: string, data: { ids: number[] }, -): Promise<{ data: HTMLData }> { +): Promise<{ data: HTMLData; meta?: PlotMetaData }> { const response = await axios.post( `${BASE_URL}/outputs/image/${path}/delete_roi`, data, diff --git a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx index e42f01323..1fd434f89 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx @@ -33,6 +33,7 @@ import { selectActiveImageData, selectRoiData, selectImageDataMaxSize, + selectImageMeta, } from 'store/slice/DisplayData/DisplayDataSelectors' import { getImageData, @@ -166,6 +167,7 @@ const ImagePlotChart = React.memo<{ selectActiveImageData(path, activeIndex), imageDataEqualtyFn, ) + const meta = useSelector(selectImageMeta(path)) const roiFilePath = useSelector(selectRoiItemFilePath(itemId)) const roiData = useSelector( @@ -299,6 +301,10 @@ const ImagePlotChart = React.memo<{ }) const layout = React.useMemo( () => ({ + title: { + text: meta?.title, + x: 0.1, + }, width: width, height: height - 130, margin: { @@ -308,6 +314,7 @@ const ImagePlotChart = React.memo<{ }, dragmode: selectMode ? 'select' : 'pan', xaxis: { + title: meta?.xlabel, autorange: true, showgrid: showgrid, showline: showline, @@ -317,6 +324,7 @@ const ImagePlotChart = React.memo<{ showticklabels: showticklabels, }, yaxis: { + title: meta?.ylabel, automargin: true, autorange: 'reversed', showgrid: showgrid, @@ -328,7 +336,7 @@ const ImagePlotChart = React.memo<{ }, }), //eslint-disable-next-line react-hooks/exhaustive-deps - [showgrid, showline, showticklabels, width, height, selectMode, isAddRoi], + [meta, showgrid, showline, showticklabels, width, height, selectMode, isAddRoi], ) const saveFileName = useSelector(selectVisualizeSaveFilename(itemId)) diff --git a/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx index 42c7a9039..9baf1ea0e 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx @@ -9,6 +9,7 @@ import { selectRoiDataIsFulfilled, selectRoiDataIsInitialized, selectRoiDataIsPending, + selectRoiMeta, } from 'store/slice/DisplayData/DisplayDataSelectors' import { LinearProgress, Typography } from '@mui/material' import { getRoiData } from 'store/slice/DisplayData/DisplayDataActions' @@ -50,6 +51,7 @@ export const RoiPlot = React.memo(() => { const RoiPlotImple = React.memo<{}>(() => { const { itemId, filePath: path } = React.useContext(DisplayDataContext) const imageData = useSelector(selectRoiData(path), imageDataEqualtyFn) + const meta = useSelector(selectRoiMeta(path)) const width = useSelector(selectVisualizeItemWidth(itemId)) const height = useSelector(selectVisualizeItemHeight(itemId)) @@ -93,6 +95,10 @@ const RoiPlotImple = React.memo<{}>(() => { const layout = React.useMemo( () => ({ + title: { + text: meta?.title, + x: 0.1, + }, width: width, height: height - 50, margin: { @@ -102,19 +108,21 @@ const RoiPlotImple = React.memo<{}>(() => { }, dragmode: 'pan', xaxis: { + title: meta?.xlabel, autorange: true, zeroline: false, autotick: true, ticks: '', }, yaxis: { + title: meta?.ylabel, autorange: 'reversed', zeroline: false, autotick: true, // todo ticks: '', }, }), - [width, height], + [meta, width, height], ) const saveFileName = useSelector(selectVisualizeSaveFilename(itemId)) diff --git a/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx index 93b893fb3..6fb9ac65b 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx @@ -13,6 +13,7 @@ import { selectTimeSeriesDataIsPending, selectTimeSeriesStd, selectTimeSeriesXrange, + selectTimesSeriesMeta, } from 'store/slice/DisplayData/DisplayDataSelectors' import { getTimeSeriesDataById, @@ -75,6 +76,7 @@ const TimeSeriesPlotImple = React.memo(() => { timeSeriesDataEqualityFn, ) + const meta = useSelector(selectTimesSeriesMeta(path)) const dataXrange = useSelector(selectTimeSeriesXrange(path)) const dataStd = useSelector(selectTimeSeriesStd(path)) const rangeUnit = useSelector(selectImageItemRangeUnit(itemId)) @@ -190,6 +192,10 @@ const TimeSeriesPlotImple = React.memo(() => { const layout = React.useMemo( () => ({ + title: { + text: meta?.title, + x: 0.1, + }, margin: { t: 60, // top l: 50, // left @@ -201,7 +207,7 @@ const TimeSeriesPlotImple = React.memo(() => { height: height - 50, xaxis: { title: { - text: rangeUnit, + text: meta?.xlabel ?? rangeUnit, }, titlefont: { size: 12, @@ -220,6 +226,7 @@ const TimeSeriesPlotImple = React.memo(() => { zeroline: zeroline, }, yaxis: { + title: meta?.ylabel, showgrid: showgrid, showline: showline, showticklabels: showticklabels, @@ -228,6 +235,7 @@ const TimeSeriesPlotImple = React.memo(() => { annotations: annotations, }), [ + meta, xrange, showgrid, showline, diff --git a/frontend/src/store/slice/DisplayData/DisplayDataActions.ts b/frontend/src/store/slice/DisplayData/DisplayDataActions.ts index dc6cfb8f0..c764f9193 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataActions.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataActions.ts @@ -31,7 +31,12 @@ import { PlotMetaData } from './DisplayDataType' import { DISPLAY_DATA_SLICE_NAME } from './DisplayDataType' export const getTimeSeriesInitData = createAsyncThunk< - { data: TimeSeriesData; xrange: string[]; std: TimeSeriesData }, + { + data: TimeSeriesData + xrange: string[] + std: TimeSeriesData + meta?: PlotMetaData + }, { path: string; itemId: number } >( `${DISPLAY_DATA_SLICE_NAME}/getTimeSeriesInitData`, @@ -46,7 +51,12 @@ export const getTimeSeriesInitData = createAsyncThunk< ) export const getTimeSeriesDataById = createAsyncThunk< - { data: TimeSeriesData; xrange: string[]; std: TimeSeriesData }, + { + data: TimeSeriesData + xrange: string[] + std: TimeSeriesData + meta?: PlotMetaData + }, { path: string; index: string } >( `${DISPLAY_DATA_SLICE_NAME}/getTimeSeriesDataById`, @@ -61,7 +71,12 @@ export const getTimeSeriesDataById = createAsyncThunk< ) export const getTimeSeriesAllData = createAsyncThunk< - { data: TimeSeriesData; xrange: string[]; std: TimeSeriesData }, + { + data: TimeSeriesData + xrange: string[] + std: TimeSeriesData + meta?: PlotMetaData + }, { path: string } >( `${DISPLAY_DATA_SLICE_NAME}/getTimeSeriesAllData`, @@ -93,7 +108,7 @@ export const getHeatMapData = createAsyncThunk< }) export const getImageData = createAsyncThunk< - { data: ImageData }, + { data: ImageData; meta?: PlotMetaData }, { path: string; workspaceId: number; startIndex?: number; endIndex?: number } >( `${DISPLAY_DATA_SLICE_NAME}/getImageData`, @@ -112,9 +127,7 @@ export const getImageData = createAsyncThunk< ) export const getCsvData = createAsyncThunk< - { - data: CsvData - }, + { data: CsvData; meta?: PlotMetaData }, { path: string; workspaceId: number } >( `${DISPLAY_DATA_SLICE_NAME}/getCsvData`, @@ -129,7 +142,7 @@ export const getCsvData = createAsyncThunk< ) export const getRoiData = createAsyncThunk< - { data: RoiData }, + { data: RoiData; meta?: PlotMetaData }, { path: string; workspaceId: number } >( `${DISPLAY_DATA_SLICE_NAME}/getRoiData`, @@ -168,7 +181,7 @@ export const getBarData = createAsyncThunk< }) export const getHTMLData = createAsyncThunk< - { data: HTMLData }, + { data: HTMLData; meta?: PlotMetaData }, { path: string } >(`${DISPLAY_DATA_SLICE_NAME}/getHTMLData`, async ({ path }, thunkAPI) => { try { diff --git a/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts b/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts index cf8a3aceb..e4613471a 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts @@ -5,6 +5,9 @@ const selectDisplayData = (state: RootState) => state.displayData export const selectTimeSeriesData = (filePath: string) => (state: RootState) => selectDisplayData(state).timeSeries[filePath].data +export const selectTimesSeriesMeta = (filePath: string) => (state: RootState) => + selectDisplayData(state).timeSeries[filePath].meta + export const selectTimeSeriesXrange = (filePath: string) => (state: RootState) => selectDisplayData(state).timeSeries[filePath].xrange @@ -67,6 +70,9 @@ export const selectHeatMapDataError = export const selectImageData = (filePath: string) => (state: RootState) => selectDisplayData(state).image[filePath] +export const selectImageMeta = (filePath: string) => (state: RootState) => + selectDisplayData(state).image[filePath].meta + export const selectImageDataIsInitialized = (filePath: string) => (state: RootState) => Object.keys(selectDisplayData(state).image).includes(filePath) @@ -103,6 +109,9 @@ export const selectActiveImageData = export const selectCsvData = (filePath: string) => (state: RootState) => selectDisplayData(state).csv[filePath].data +export const selectCsvMeta = (filePath: string) => (state: RootState) => + selectDisplayData(state).csv[filePath].meta + export const selectCsvDataIsInitialized = (filePath: string) => (state: RootState) => Object.keys(selectDisplayData(state).csv).includes(filePath) @@ -125,6 +134,9 @@ export const selectCsvDataIsFulfilled = export const selectRoiData = (filePath: string) => (state: RootState) => selectDisplayData(state).roi[filePath]?.data[0] ?? [] +export const selectRoiMeta = (filePath: string) => (state: RootState) => + selectDisplayData(state).roi[filePath].meta + export const selectRoiDataIsInitialized = (filePath: string) => (state: RootState) => Object.keys(selectDisplayData(state).roi).includes(filePath) @@ -208,6 +220,9 @@ export const selectBarDataIsFulfilled = export const selectHTMLData = (filePath: string) => (state: RootState) => selectDisplayData(state).html[filePath]?.data ?? '' +export const selectHTMLMeta = (filePath: string) => (state: RootState) => + selectDisplayData(state).html[filePath].meta + export const selectHTMLDataIsInitialized = (filePath: string) => (state: RootState) => Object.keys(selectDisplayData(state).html).includes(filePath) diff --git a/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts b/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts index 6fb5952f8..2d52389c4 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts @@ -96,6 +96,7 @@ export const displayDataSlice = createSlice({ state.timeSeries[path].fulfilled = true state.timeSeries[path].error = null + state.timeSeries[path].meta = action.payload.meta state.timeSeries[path].data[index] = action.payload.data[index] if (action.payload.std[index] !== undefined) { state.timeSeries[path].std[index] = action.payload.std[index] @@ -137,6 +138,7 @@ export const displayDataSlice = createSlice({ state.timeSeries[path].fulfilled = true state.timeSeries[path].error = null state.timeSeries[path].xrange = action.payload.xrange + state.timeSeries[path].meta = action.payload.meta state.timeSeries[path].data = action.payload.data if (action.payload.std !== undefined) { state.timeSeries[path].std = action.payload.std @@ -179,6 +181,7 @@ export const displayDataSlice = createSlice({ state.timeSeries[path].error = null state.timeSeries[path].xrange = action.payload.xrange + state.timeSeries[path].meta = action.payload.meta state.timeSeries[path].data = action.payload.data state.timeSeries[path].std = action.payload.std }) @@ -369,6 +372,7 @@ export const displayDataSlice = createSlice({ state.image[path] = { type: 'image', data: action.payload.data, + meta: action.payload.meta, pending: false, fulfilled: true, error: null, @@ -399,6 +403,7 @@ export const displayDataSlice = createSlice({ state.csv[path] = { type: 'csv', data: action.payload.data, + meta: action.payload.meta, pending: false, fulfilled: true, error: null, @@ -442,6 +447,7 @@ export const displayDataSlice = createSlice({ state.roi[path] = { type: 'roi', data: data, + meta: action.payload.meta, pending: false, fulfilled: true, error: null, @@ -542,6 +548,7 @@ export const displayDataSlice = createSlice({ state.html[path] = { type: 'html', data: action.payload.data, + meta: action.payload.meta, pending: false, fulfilled: true, error: null, diff --git a/frontend/src/store/slice/DisplayData/DisplayDataType.ts b/frontend/src/store/slice/DisplayData/DisplayDataType.ts index f6fd31f31..add50c969 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataType.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataType.ts @@ -86,6 +86,7 @@ export type PlotMetaData = { interface BaseDisplay { type: T data: Data + meta?: PlotMetaData pending: boolean error: string | null fulfilled: boolean @@ -101,7 +102,6 @@ export interface HeatMapDisplayData extends BaseDisplay<'heatMap', HeatMapData> { columns: string[] index: string[] - meta?: PlotMetaData } export interface ImageDisplayData extends BaseDisplay<'image', ImageData> {} @@ -115,14 +115,11 @@ export interface RoiDisplayData extends BaseDisplay<'roi', RoiData> { } export interface ScatterDisplayData - extends BaseDisplay<'scatter', ScatterData> { - meta?: PlotMetaData -} + extends BaseDisplay<'scatter', ScatterData> {} export interface BarDisplayData extends BaseDisplay<'bar', BarData> { columns: string[] index: string[] - meta?: PlotMetaData } export interface HTMLDisplayData extends BaseDisplay<'html', HTMLData> {} diff --git a/studio/app/common/core/utils/file_reader.py b/studio/app/common/core/utils/file_reader.py index 82461f12a..2b12fb459 100644 --- a/studio/app/common/core/utils/file_reader.py +++ b/studio/app/common/core/utils/file_reader.py @@ -31,16 +31,13 @@ def read(cls, filepath): def read_as_output(cls, filepath) -> OutputData: json_data = cls.read(filepath) plot_metadata_path = f"{os.path.splitext(filepath)[0]}.plot-meta.json" - - plot_metadata = ( - cls.read(plot_metadata_path) if os.path.exists(plot_metadata_path) else {} - ) + plot_metadata = cls.read_as_plot_meta(plot_metadata_path) return OutputData( data=json_data["data"], columns=json_data["columns"], index=json_data["index"], - meta=PlotMetaData(**plot_metadata), + meta=plot_metadata, ) @classmethod @@ -51,3 +48,8 @@ def read_as_timeseries(cls, filepath) -> JsonTimeSeriesData: data=json_data["data"], std=json_data["std"] if "std" in json_data else None, ) + + @classmethod + def read_as_plot_meta(cls, filepath) -> PlotMetaData: + json_data = cls.read(filepath) if os.path.exists(filepath) else {} + return PlotMetaData(**json_data) diff --git a/studio/app/common/core/utils/json_writer.py b/studio/app/common/core/utils/json_writer.py index 4be5d2b04..590f2f4ab 100644 --- a/studio/app/common/core/utils/json_writer.py +++ b/studio/app/common/core/utils/json_writer.py @@ -23,7 +23,8 @@ def write_as_split(cls, filepath, data): pd.DataFrame(data).to_json(filepath, indent=4, orient="split") @classmethod - def write_plot_meta(cls, filepath, data: Optional[PlotMetaData]): + def write_plot_meta(cls, dir_name, file_name, data: Optional[PlotMetaData]): + filepath = join_filepath([dir_name, f"{file_name}.plot-meta.json"]) if data is not None: with open(filepath, "w") as f: json.dump(data.value_present_dict(), f, indent=4) diff --git a/studio/app/common/dataclass/bar.py b/studio/app/common/dataclass/bar.py index 7209845de..40da1eee8 100644 --- a/studio/app/common/dataclass/bar.py +++ b/studio/app/common/dataclass/bar.py @@ -40,8 +40,7 @@ def save_json(self, json_dir): index=self.index, ) JsonWriter.write_as_split(self.json_path, df) - plot_meta_path = join_filepath([json_dir, f"{self.file_name}.plot-meta.json"]) - JsonWriter.write_plot_meta(plot_meta_path, self.meta) + JsonWriter.write_plot_meta(json_dir, self.file_name, self.meta) @property def output_path(self) -> OutputPath: diff --git a/studio/app/common/dataclass/csv.py b/studio/app/common/dataclass/csv.py index a201efd8b..2b07d7132 100644 --- a/studio/app/common/dataclass/csv.py +++ b/studio/app/common/dataclass/csv.py @@ -1,3 +1,5 @@ +from typing import Optional + import numpy as np import pandas as pd @@ -7,11 +9,15 @@ ) from studio.app.common.core.utils.json_writer import JsonWriter from studio.app.common.dataclass.base import BaseData +from studio.app.common.schemas.outputs import PlotMetaData class CsvData(BaseData): - def __init__(self, data, params, file_name="csv"): + def __init__( + self, data, params, file_name="csv", meta: Optional[PlotMetaData] = None + ): super().__init__(file_name) + self.meta = meta if isinstance(data, str): self.data = pd.read_csv(data, header=None).values @@ -32,6 +38,7 @@ def save_json(self, json_dir): # timeseriesだけはdirを返す self.json_path = join_filepath([json_dir, self.file_name]) create_directory(self.json_path) + JsonWriter.write_plot_meta(json_dir, self.file_name, self.meta) for i, data in enumerate(self.data): JsonWriter.write_as_split( diff --git a/studio/app/common/dataclass/heatmap.py b/studio/app/common/dataclass/heatmap.py index e0069ae09..5533fde0a 100644 --- a/studio/app/common/dataclass/heatmap.py +++ b/studio/app/common/dataclass/heatmap.py @@ -35,8 +35,7 @@ def save_json(self, json_dir): columns=self.columns, ) JsonWriter.write_as_split(self.json_path, df) - plot_meta_path = join_filepath([json_dir, f"{self.file_name}.plot-meta.json"]) - JsonWriter.write_plot_meta(plot_meta_path, self.meta) + JsonWriter.write_plot_meta(json_dir, self.file_name, self.meta) @property def output_path(self) -> OutputPath: diff --git a/studio/app/common/dataclass/html.py b/studio/app/common/dataclass/html.py index ce36ee6ea..62a987307 100644 --- a/studio/app/common/dataclass/html.py +++ b/studio/app/common/dataclass/html.py @@ -1,15 +1,21 @@ +from typing import Optional + from studio.app.common.core.utils.filepath_creater import join_filepath +from studio.app.common.core.utils.json_writer import JsonWriter from studio.app.common.core.workflow.workflow import OutputPath, OutputType from studio.app.common.dataclass.base import BaseData +from studio.app.common.schemas.outputs import PlotMetaData class HTMLData(BaseData): - def __init__(self, data, file_name="html"): + def __init__(self, data, file_name="html", meta: Optional[PlotMetaData] = None): super().__init__(file_name) self.data = data + self.meta = meta def save_json(self, json_dir): self.json_path = join_filepath([json_dir, f"{self.file_name}.html"]) + JsonWriter.write_plot_meta(json_dir, self.file_name, self.meta) with open(self.json_path, "w") as f: f.write(self.data) diff --git a/studio/app/common/dataclass/image.py b/studio/app/common/dataclass/image.py index afe33bfc1..54e529fee 100644 --- a/studio/app/common/dataclass/image.py +++ b/studio/app/common/dataclass/image.py @@ -1,4 +1,5 @@ import gc +from typing import Optional import imageio import numpy as np @@ -12,14 +13,22 @@ from studio.app.common.core.workflow.workflow import OutputPath, OutputType from studio.app.common.dataclass.base import BaseData from studio.app.common.dataclass.utils import create_images_list +from studio.app.common.schemas.outputs import PlotMetaData from studio.app.dir_path import DIRPATH class ImageData(BaseData): - def __init__(self, data, output_dir=DIRPATH.OUTPUT_DIR, file_name="image"): + def __init__( + self, + data, + output_dir=DIRPATH.OUTPUT_DIR, + file_name="image", + meta: Optional[PlotMetaData] = None, + ): super().__init__(file_name) self.json_path = None + self.meta = meta if data is None: self.path = None @@ -48,6 +57,7 @@ def data(self): def save_json(self, json_dir): self.json_path = join_filepath([json_dir, f"{self.file_name}.json"]) JsonWriter.write_as_split(self.json_path, create_images_list(self.data)) + JsonWriter.write_plot_meta(json_dir, self.file_name, self.meta) @property def output_path(self) -> OutputPath: diff --git a/studio/app/common/dataclass/scatter.py b/studio/app/common/dataclass/scatter.py index 71f725091..9ffe51088 100644 --- a/studio/app/common/dataclass/scatter.py +++ b/studio/app/common/dataclass/scatter.py @@ -19,8 +19,7 @@ def __init__(self, data, file_name="scatter", meta: Optional[PlotMetaData] = Non def save_json(self, json_dir): self.json_path = join_filepath([json_dir, f"{self.file_name}.json"]) JsonWriter.write_as_split(self.json_path, self.data) - plot_meta_path = join_filepath([json_dir, f"{self.file_name}.plot-meta.json"]) - JsonWriter.write_plot_meta(plot_meta_path, self.meta) + JsonWriter.write_plot_meta(json_dir, self.file_name, self.meta) @property def output_path(self) -> OutputPath: diff --git a/studio/app/common/dataclass/timeseries.py b/studio/app/common/dataclass/timeseries.py index 52fe9a9fb..191f718b2 100644 --- a/studio/app/common/dataclass/timeseries.py +++ b/studio/app/common/dataclass/timeseries.py @@ -1,3 +1,5 @@ +from typing import Optional + import numpy as np import pandas as pd @@ -8,13 +10,21 @@ from studio.app.common.core.utils.json_writer import JsonWriter from studio.app.common.core.workflow.workflow import OutputPath, OutputType from studio.app.common.dataclass.base import BaseData +from studio.app.common.schemas.outputs import PlotMetaData class TimeSeriesData(BaseData): def __init__( - self, data, std=None, index=None, cell_numbers=None, file_name="timeseries" + self, + data, + std=None, + index=None, + cell_numbers=None, + file_name="timeseries", + meta: Optional[PlotMetaData] = None, ): super().__init__(file_name) + self.meta = meta assert data.ndim <= 2, "TimeSeries Dimension Error" @@ -44,6 +54,7 @@ def save_json(self, json_dir): # timeseriesだけはdirを返す self.json_path = join_filepath([json_dir, self.file_name]) create_directory(self.json_path, delete_dir=True) + JsonWriter.write_plot_meta(json_dir, self.file_name, self.meta) for i, cell_i in enumerate(self.cell_numbers): data = self.data[i] diff --git a/studio/app/common/routers/outputs.py b/studio/app/common/routers/outputs.py index 234d0eae4..15ea454d5 100644 --- a/studio/app/common/routers/outputs.py +++ b/studio/app/common/routers/outputs.py @@ -18,6 +18,18 @@ router = APIRouter(prefix="/outputs", tags=["outputs"]) +def get_initial_timeseries_data(dirpath) -> JsonTimeSeriesData: + plot_meta_path = f"{dirpath}.plot-meta.json" + plot_meta = JsonReader.read_as_plot_meta(plot_meta_path) + + return JsonTimeSeriesData( + xrange=[], + data={}, + std={}, + meta=plot_meta, + ) + + @router.get("/inittimedata/{dirpath:path}", response_model=JsonTimeSeriesData) async def get_inittimedata(dirpath: str): file_numbers = sorted( @@ -34,12 +46,6 @@ async def get_inittimedata(dirpath: str): join_filepath([dirpath, f"{str(index)}.json"]) ) - return_data = JsonTimeSeriesData( - xrange=[], - data={}, - std={}, - ) - data = { str(i): {json_data.xrange[0]: json_data.data[json_data.xrange[0]]} for i in file_numbers @@ -51,12 +57,12 @@ async def get_inittimedata(dirpath: str): for i in file_numbers } - return_data = JsonTimeSeriesData( - xrange=json_data.xrange, - data=data, - std=std if json_data.std is not None else {}, - ) + return_data = get_initial_timeseries_data(dirpath) + return_data.xrange = json_data.xrange + if json_data.std is not None: + return_data.std = std + return_data.data = data return_data.data[str_index] = json_data.data if json_data.std is not None: return_data.std[str_index] = json_data.std @@ -70,11 +76,7 @@ async def get_timedata(dirpath: str, index: int): join_filepath([dirpath, f"{str(index)}.json"]) ) - return_data = JsonTimeSeriesData( - xrange=[], - data={}, - std={}, - ) + return_data = get_initial_timeseries_data(dirpath) str_index = str(index) return_data.data[str_index] = json_data.data @@ -86,11 +88,7 @@ async def get_timedata(dirpath: str, index: int): @router.get("/alltimedata/{dirpath:path}", response_model=JsonTimeSeriesData) async def get_alltimedata(dirpath: str): - return_data = JsonTimeSeriesData( - xrange=[], - data={}, - std={}, - ) + return_data = get_initial_timeseries_data(dirpath) for i, path in enumerate(glob(join_filepath([dirpath, "*.json"]))): str_idx = str(os.path.splitext(os.path.basename(path))[0]) diff --git a/studio/app/optinist/dataclass/caiman.py b/studio/app/optinist/dataclass/caiman.py index 890ae7889..0499bcf4f 100644 --- a/studio/app/optinist/dataclass/caiman.py +++ b/studio/app/optinist/dataclass/caiman.py @@ -1,15 +1,22 @@ import os +from typing import Optional import numpy as np +from studio.app.common.core.utils.json_writer import JsonWriter from studio.app.common.dataclass.base import BaseData +from studio.app.common.schemas.outputs import PlotMetaData class CaimanCnmfData(BaseData): - def __init__(self, data, file_name="caiman_cnmf"): + def __init__( + self, data, file_name="caiman_cnmf", meta: Optional[PlotMetaData] = None + ): super().__init__(file_name) self.data = data + self.meta = meta def save_json(self, json_dir): self.json_path = os.path.join(json_dir, f"{self.file_name}.npy") np.save(self.json_path, self.data) + JsonWriter.write_plot_meta(json_dir, self.file_name, self.meta) diff --git a/studio/app/optinist/dataclass/fluo.py b/studio/app/optinist/dataclass/fluo.py index 6c7d340b4..015fdd1bf 100644 --- a/studio/app/optinist/dataclass/fluo.py +++ b/studio/app/optinist/dataclass/fluo.py @@ -1,11 +1,22 @@ +from typing import Optional + from studio.app.common.dataclass.timeseries import TimeSeriesData +from studio.app.common.schemas.outputs import PlotMetaData class FluoData(TimeSeriesData): - def __init__(self, data, std=None, index=None, file_name="fluo"): + def __init__( + self, + data, + std=None, + index=None, + file_name="fluo", + meta: Optional[PlotMetaData] = None, + ): super().__init__( data=data, std=std, index=index, file_name=file_name, + meta=meta, ) diff --git a/studio/app/optinist/dataclass/lccd.py b/studio/app/optinist/dataclass/lccd.py index 2ebca3179..caf8ba61b 100644 --- a/studio/app/optinist/dataclass/lccd.py +++ b/studio/app/optinist/dataclass/lccd.py @@ -1,15 +1,20 @@ import os +from typing import Optional import numpy as np +from studio.app.common.core.utils.json_writer import JsonWriter from studio.app.common.dataclass.base import BaseData +from studio.app.common.schemas.outputs import PlotMetaData class LccdData(BaseData): - def __init__(self, data, file_name="lccd"): + def __init__(self, data, file_name="lccd", meta: Optional[PlotMetaData] = None): super().__init__(file_name) self.data = data + self.meta = meta def save_json(self, json_dir): self.json_path = os.path.join(json_dir, f"{self.file_name}.npy") np.save(self.json_path, self.data) + JsonWriter.write_plot_meta(json_dir, self.file_name, self.meta) diff --git a/studio/app/optinist/dataclass/roi.py b/studio/app/optinist/dataclass/roi.py index e34242570..b100c235e 100644 --- a/studio/app/optinist/dataclass/roi.py +++ b/studio/app/optinist/dataclass/roi.py @@ -1,4 +1,5 @@ import gc +from typing import Optional import imageio import numpy as np @@ -12,12 +13,20 @@ from studio.app.common.core.workflow.workflow import OutputPath, OutputType from studio.app.common.dataclass.base import BaseData from studio.app.common.dataclass.utils import create_images_list +from studio.app.common.schemas.outputs import PlotMetaData from studio.app.dir_path import DIRPATH class RoiData(BaseData): - def __init__(self, data, output_dir=DIRPATH.OUTPUT_DIR, file_name="roi"): + def __init__( + self, + data, + output_dir=DIRPATH.OUTPUT_DIR, + file_name="roi", + meta: Optional[PlotMetaData] = None, + ): super().__init__(file_name) + self.meta = meta images = create_images_list(data) @@ -40,6 +49,7 @@ def data(self): def save_json(self, json_dir): self.json_path = join_filepath([json_dir, f"{self.file_name}.json"]) JsonWriter.write_as_split(self.json_path, create_images_list(self.data)) + JsonWriter.write_plot_meta(json_dir, self.file_name, self.meta) @property def output_path(self) -> OutputPath: diff --git a/studio/app/optinist/dataclass/suite2p.py b/studio/app/optinist/dataclass/suite2p.py index b294fae4c..1ce81591a 100644 --- a/studio/app/optinist/dataclass/suite2p.py +++ b/studio/app/optinist/dataclass/suite2p.py @@ -1,15 +1,20 @@ import os +from typing import Optional import numpy as np +from studio.app.common.core.utils.json_writer import JsonWriter from studio.app.common.dataclass.base import BaseData +from studio.app.common.schemas.outputs import PlotMetaData class Suite2pData(BaseData): - def __init__(self, data, file_name="suite2p"): + def __init__(self, data, file_name="suite2p", meta: Optional[PlotMetaData] = None): super().__init__(file_name) self.data = data + self.meta = meta def save_json(self, json_dir): self.json_path = os.path.join(json_dir, f"{self.file_name}.npy") np.save(self.json_path, self.data) + JsonWriter.write_plot_meta(json_dir, self.file_name, self.meta)