From 149e5fcaa03a11699cfb29728eebff636c9d1b96 Mon Sep 17 00:00:00 2001 From: Miles Date: Mon, 6 Jan 2025 13:09:58 +0900 Subject: [PATCH 01/15] cleanup files --- .../Visualize/Editor/TimeSeriesItemEditor.tsx | 9 + .../Workspace/Visualize/Plot/ImagePlot.tsx | 169 +++++++++++------- .../Visualize/Plot/TimeSeriesPlot.tsx | 18 +- .../slice/DisplayData/DisplayDataActions.ts | 6 +- .../slice/DisplayData/DisplayDataSelectors.ts | 13 +- .../slice/DisplayData/DisplayDataSlice.ts | 40 ++++- .../VisualizeItem/VisualizeItemActions.ts | 9 +- .../VisualizeItem/VisualizeItemSelectors.ts | 3 + .../slice/VisualizeItem/VisualizeItemSlice.ts | 47 ++++- .../slice/VisualizeItem/VisualizeItemType.ts | 5 +- 10 files changed, 235 insertions(+), 84 deletions(-) diff --git a/frontend/src/components/Workspace/Visualize/Editor/TimeSeriesItemEditor.tsx b/frontend/src/components/Workspace/Visualize/Editor/TimeSeriesItemEditor.tsx index 3871f6ec0..02074e487 100644 --- a/frontend/src/components/Workspace/Visualize/Editor/TimeSeriesItemEditor.tsx +++ b/frontend/src/components/Workspace/Visualize/Editor/TimeSeriesItemEditor.tsx @@ -50,6 +50,7 @@ import { setTimeSeriesItemZeroLine, setTimeSeriesItemDrawOrderList, changeRangeUnit, + setClickedData, } from "store/slice/VisualizeItem/VisualizeItemSlice" import { AppDispatch } from "store/store" import { arrayEqualityFn } from "utils/EqualityUtils" @@ -290,6 +291,14 @@ const LegendSelect: FC = () => { drawOrderList: newDrawOrderList, }), ) + dispatch( + setClickedData({ + itemId, + clickedDataId: event.target.checked ? index : null, + }), + ), + // eslint-disable-next-line no-console + console.log("#############\n In TimeSeriesItemEditor.tsx, clickedDataId") if (filePath !== null) { dispatch(getTimeSeriesDataById({ path: filePath, index: index })) diff --git a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx index a0b3aff5b..2e672d4e9 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx @@ -35,11 +35,12 @@ import { cancelRoi, commitRoi, deleteRoi, + mergeRoi, + clickRoi, getImageData, getRoiData, getStatus, getTimeSeriesInitData, - mergeRoi, } from "store/slice/DisplayData/DisplayDataActions" import { selectImageDataError, @@ -54,7 +55,7 @@ import { } from "store/slice/DisplayData/DisplayDataSelectors" import { selectingImageArea, - setImageItemClikedDataId, + setImageItemClickedDataId, } from "store/slice/VisualizeItem/VisualizeItemActions" import { selectImageItemShowticklabels, @@ -77,6 +78,7 @@ import { selectImageItemAlpha, selectRoiItemOutputKeys, selectVisualizeItems, + selectClickedRoi, } from "store/slice/VisualizeItem/VisualizeItemSelectors" import { incrementImageActiveIndex, @@ -100,6 +102,7 @@ export type StatusROI = { temp_add_roi: number[] temp_delete_roi: number[] temp_merge_roi: number[] + temp_selected_roi: number[] } const ADD_ROI = "Add ROI" @@ -222,14 +225,27 @@ const ImagePlotChart = memo(function ImagePlotChart({ const roiAlpha = useSelector(selectImageItemRoiAlpha(itemId)) const width = useSelector(selectVisualizeItemWidth(itemId)) const height = useSelector(selectVisualizeItemHeight(itemId)) - const statusRoi = useSelector(selectStatusRoi) const [sizeDrag, setSizeDrag] = useState(initSizeDrag) const [startDragAddRoi, setStartDragAddRoi] = useState(false) const [action, setAction] = useState("") const [positionDrag, setChangeSize] = useState() + const clickedDataId = useSelector(selectClickedRoi(itemId)) const outputKey: string | null = useSelector(selectRoiItemOutputKeys(itemId)) + const selectedStatus = useSelector(selectStatusRoi) + + const statusRoi = useMemo(() => { + return ( + selectedStatus || { + temp_add_roi: [], + temp_delete_roi: [], + temp_merge_roi: [], + temp_selected_roi: [], + } + ) + }, [selectedStatus]) + const refPageXSize = useRef(0) const refPageYSize = useRef(0) @@ -266,6 +282,25 @@ const ImagePlotChart = memo(function ImagePlotChart({ //eslint-disable-next-line react-hooks/exhaustive-deps }, [roiFilePath]) + useEffect(() => { + if (statusRoi && roiDataState.length > 0) { + const newPointClick = (statusRoi.temp_selected_roi || []) + .map((z) => { + // Find the coordinates of the ROI center + const yIndex = roiDataState.findIndex((row) => row.includes(z)) + const xIndex = + roiDataState[yIndex]?.findIndex((val) => val === z) ?? -1 + return { + x: xIndex, + y: yIndex, + z: z, + } + }) + .filter((point) => point.x !== -1 && point.y !== -1) + setPointClick(newPointClick) + } + }, [statusRoi, roiDataState]) + const data = useMemo( () => [ { @@ -307,37 +342,34 @@ const ImagePlotChart = memo(function ImagePlotChart({ const offset: number = i / timeDataMaxIndex const rgba = colorscaleRoi[new_i] const hex = rgba2hex(rgba, roiAlpha) - if (!action) { - if (statusRoi.temp_delete_roi.includes(i)) - return [offset, "#ffffff"] - if (statusRoi.temp_merge_roi.includes(i)) return [offset, "#e134eb"] - if (statusRoi.temp_add_roi.includes(i)) return [offset, "#3483eb"] - } - if (action === ADD_ROI) { - if (statusRoi.temp_delete_roi.includes(i)) - return [offset, "#ffffff"] - if (statusRoi.temp_add_roi.includes(i)) return [offset, "#3483eb"] - if (statusRoi.temp_merge_roi.includes(i)) return [offset, "#e134eb"] - } - if (action === DELETE_ROI) { - if ( - pointClick.find((e) => e.z === i) || - statusRoi.temp_delete_roi.includes(i) - ) - return [offset, "#ffffff"] - if (statusRoi.temp_add_roi.includes(i)) return [offset, "#3483eb"] - if (statusRoi.temp_merge_roi.includes(i)) return [offset, "#e134eb"] + + const isClickPoint = pointClick.some((point) => point.z === i) + const isSelected = statusRoi?.temp_selected_roi?.includes(i) || false + const isDeleted = statusRoi?.temp_delete_roi?.includes(i) || false + const isMerged = statusRoi?.temp_merge_roi?.includes(i) || false + const isAdded = statusRoi?.temp_add_roi?.includes(i) || false + + if (isClickPoint || isSelected || isDeleted || isMerged || isAdded) { + switch (action) { + case DELETE_ROI: + if (isClickPoint || isSelected || isDeleted) + return [offset, "#FFA500"] // orange + break + case MERGE_ROI: + if (isClickPoint || isSelected || isMerged) + return [offset, "#e134eb"] // purple + break + case ADD_ROI: + if (isAdded) return [offset, "3483eb"] // red + break + default: + if (isClickPoint || isSelected) return [offset, "#ffffff"] // white + } } - if (action === MERGE_ROI) { - if (statusRoi.temp_delete_roi.includes(i)) - return [offset, "#ffffff"] - if ( - pointClick.find((e) => e.z === i) || - statusRoi.temp_merge_roi.includes(i) - ) - return [offset, "#e134eb"] - if (statusRoi.temp_add_roi.includes(i)) return [offset, "#3483eb"] + if (clickedDataId !== null && i.toString() === clickedDataId) { + return [offset, "#FF4500"] // Bright orange-red for clicked ROI } + return [offset, hex] }), zmin: 0, @@ -360,6 +392,7 @@ const ImagePlotChart = memo(function ImagePlotChart({ pointClick, action, statusRoi, + clickedDataId, ], ) @@ -457,38 +490,50 @@ const ImagePlotChart = memo(function ImagePlotChart({ const onChartClick = (event: PlotMouseEvent) => { // use as unknown because original PlotDatum does not have z property const point: PlotDatum = event.points[0] as unknown as PlotDatum - if (point.curveNumber >= 1 && outputKey === "cell_roi") { - setSelectRoi({ - x: Number(point.x), - y: Number(point.y), - z: Number(point.z), - }) - } if (point.curveNumber >= 1 && point.z >= 0) { - dispatch( - setImageItemClikedDataId({ - itemId, - clickedDataId: point.z.toString(), - }), - ) + if (outputKey === "cell_roi") { + setSelectRoi({ + x: Number(point.x), + y: Number(point.y), + z: Number(point.z), + }) + } else { + dispatch( + setImageItemClickedDataId({ + itemId, + clickedDataId: point.z.toString(), + }), + ) + } } } const setSelectRoi = (point: PointClick) => { - if (![MERGE_ROI, DELETE_ROI].includes(action)) return if (isNaN(Number(point.z))) return - let newPoints - if (statusRoi.temp_delete_roi.includes(point.z)) { - return + const roiIndex = Number(point.z) + + if (action) { + dispatch( + clickRoi({ + roiIndex, + }), + ) } - const check = pointClick.findIndex((item) => item.z === point.z) - if (check < 0) { - newPoints = [...pointClick, point] + + dispatch( + setImageItemClickedDataId({ + itemId, + clickedDataId: roiIndex.toString(), + }), + ) + + const checkIndex = pointClick.findIndex((item) => item.z === roiIndex) + if (checkIndex < 0) { + setPointClick([...pointClick, point]) } else { - newPoints = pointClick.filter((item) => item.z !== point.z) + setPointClick(pointClick.filter((item) => item.z !== roiIndex)) } - setPointClick(newPoints) } const onCancel = async () => { @@ -591,7 +636,7 @@ const ImagePlotChart = memo(function ImagePlotChart({ } const addOrSelectRoi = async () => { - if (!roiFilePath || !workspaceId) return + if (!roiFilePath || !workspaceId || !statusRoi) return if (action === ADD_ROI) { const sizeX = roiDataState[0].length - 1 const sizeY = roiDataState.length - 1 @@ -613,28 +658,28 @@ const ImagePlotChart = memo(function ImagePlotChart({ onCancelAdd() } if (action === MERGE_ROI) { - if (pointClick.length < 2) return + if (statusRoi.temp_selected_roi.length < 2) return dispatch(resetAllOrderList()) dispatch( mergeRoi({ path: roiFilePath, workspaceId, data: { - ids: pointClick.map((point) => point.z), + ids: statusRoi.temp_selected_roi, }, }), ) setPointClick([]) workspaceId && dispatch(getRoiData({ path: roiFilePath, workspaceId })) } else if (action === DELETE_ROI) { - if (!pointClick.length) return + if (!statusRoi.temp_selected_roi.length) return dispatch(resetAllOrderList()) await dispatch( deleteRoi({ path: roiFilePath, workspaceId, data: { - ids: pointClick.map((point) => point.z), + ids: statusRoi.temp_selected_roi, }, }), ) @@ -699,7 +744,7 @@ const ImagePlotChart = memo(function ImagePlotChart({ <> {action !== ADD_ROI ? ( - ROI Selecteds: [{pointClick.map((item) => item.z).join(",")}] + ROI Selecteds: [{statusRoi?.temp_selected_roi?.join(",") || ""}] ) : null} @@ -709,8 +754,8 @@ const ImagePlotChart = memo(function ImagePlotChart({ action === DELETE_ROI ? "#F84E1B" : action === MERGE_ROI - ? "#6619A9" - : "default", + ? "#6619A9" + : "default", display: "flex", gap: 1, textDecoration: "none", diff --git a/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx index 3405c3395..3c4ae8ab0 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx @@ -10,6 +10,7 @@ import { LinearProgress, Typography } from "@mui/material" import { TimeSeriesData } from "api/outputs/Outputs" import { DisplayDataContext } from "components/Workspace/Visualize/DataContext" import { + clickRoi, getTimeSeriesDataById, getTimeSeriesInitData, } from "store/slice/DisplayData/DisplayDataActions" @@ -281,11 +282,11 @@ const TimeSeriesPlotImple = memo(function TimeSeriesPlotImple() { } const onLegendClick = (event: LegendClickEvent) => { - const clickNumber = dataKeys[event.curveNumber] + const clickedSeriesId = dataKeys[event.curveNumber] - const newDrawOrderList = drawOrderList.includes(clickNumber) - ? drawOrderList.filter((value) => value !== clickNumber) - : [...drawOrderList, clickNumber] + const newDrawOrderList = drawOrderList.includes(clickedSeriesId) + ? drawOrderList.filter((value) => value !== clickedSeriesId) + : [...drawOrderList, clickedSeriesId] dispatch( setTimeSeriesItemDrawOrderList({ @@ -293,10 +294,15 @@ const TimeSeriesPlotImple = memo(function TimeSeriesPlotImple() { drawOrderList: newDrawOrderList, }), ) + dispatch( + clickRoi({ + roiIndex: Number(clickedSeriesId), + }), + ) // set DisplayNumbers - if (!drawOrderList.includes(clickNumber)) { - dispatch(getTimeSeriesDataById({ path, index: clickNumber })) + if (!drawOrderList.includes(clickedSeriesId)) { + dispatch(getTimeSeriesDataById({ path, index: clickedSeriesId })) } return false diff --git a/frontend/src/store/slice/DisplayData/DisplayDataActions.ts b/frontend/src/store/slice/DisplayData/DisplayDataActions.ts index b39e95ab1..ed2dc6aa1 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataActions.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataActions.ts @@ -1,4 +1,4 @@ -import { createAsyncThunk } from "@reduxjs/toolkit" +import { createAsyncThunk, createAction } from "@reduxjs/toolkit" import { TimeSeriesData, @@ -286,6 +286,10 @@ export const getStatus = createAsyncThunk< }, ) +export const clickRoi = createAction<{ + roiIndex: number +}>(`${DISPLAY_DATA_SLICE_NAME}/clickRoi`) + export const getScatterData = createAsyncThunk< { data: ScatterData; meta?: PlotMetaData }, { path: string } diff --git a/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts b/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts index 079c182b7..83d699e1c 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts @@ -1,3 +1,6 @@ +import { createSelector } from "@reduxjs/toolkit" + +import { StatusROI } from "components/Workspace/Visualize/Plot/ImagePlot" import { RootState } from "store/store" const selectDisplayData = (state: RootState) => state.displayData @@ -362,4 +365,12 @@ export const selectPolarDataError = (filePath: string) => (state: RootState) => ? selectDisplayData(state).polar[filePath].error : null -export const selectStatusRoi = (state: RootState) => state.displayData.statusRoi +export const selectStatusRoi = createSelector( + [(state: RootState) => state.displayData.statusRoi], + (statusRoi): StatusROI => ({ + temp_add_roi: statusRoi?.temp_add_roi || [], + temp_delete_roi: statusRoi?.temp_delete_roi || [], + temp_merge_roi: statusRoi?.temp_merge_roi || [], + temp_selected_roi: statusRoi?.temp_selected_roi || [], + }), +) diff --git a/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts b/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts index 42b8501d4..c39564b1d 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts @@ -1,4 +1,4 @@ -import { createSlice, isAnyOf } from "@reduxjs/toolkit" +import { createSlice, isAnyOf, PayloadAction } from "@reduxjs/toolkit" import { getCsvData, @@ -20,6 +20,7 @@ import { addRoi, deleteRoi, commitRoi, + clickRoi, getStatus, } from "store/slice/DisplayData/DisplayDataActions" import { @@ -49,7 +50,12 @@ const initialState: DisplayData = { polar: {}, loading: false, loadingStack: [], - statusRoi: { temp_add_roi: [], temp_delete_roi: [], temp_merge_roi: [] }, + statusRoi: { + temp_add_roi: [], + temp_delete_roi: [], + temp_merge_roi: [], + temp_selected_roi: [], + }, isEditRoiCommitting: false, } @@ -601,6 +607,7 @@ export const displayDataSlice = createSlice({ temp_add_roi: [], temp_delete_roi: [], temp_merge_roi: [], + temp_selected_roi: [], } state.loadingStack.pop() @@ -611,6 +618,35 @@ export const displayDataSlice = createSlice({ state.loadingStack.push((state.loading = true)) }) + .addCase( + clickRoi, + (state, action: PayloadAction<{ roiIndex: number }>) => { + const { roiIndex } = action.payload + if (!state.statusRoi) { + state.statusRoi = { + temp_add_roi: [], + temp_delete_roi: [], + temp_merge_roi: [], + temp_selected_roi: [], + } + } + state.statusRoi.temp_add_roi = state.statusRoi.temp_add_roi || [] + state.statusRoi.temp_delete_roi = + state.statusRoi.temp_delete_roi || [] + state.statusRoi.temp_merge_roi = state.statusRoi.temp_merge_roi || [] + state.statusRoi.temp_selected_roi = + state.statusRoi.temp_selected_roi || [] + + const index = state.statusRoi.temp_selected_roi.indexOf(roiIndex) + if (index === -1) { + // If not selected, add it + state.statusRoi.temp_selected_roi.push(roiIndex) + } else { + // If already selected, remove it + state.statusRoi.temp_selected_roi.splice(index, 1) + } + }, + ) .addMatcher( isAnyOf( cancelRoi.pending, diff --git a/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts b/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts index 2c9566e3b..5a748f47b 100644 --- a/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts +++ b/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts @@ -4,6 +4,7 @@ import { getTimeSeriesDataById } from "store/slice/DisplayData/DisplayDataAction import { selectRoiData } from "store/slice/DisplayData/DisplayDataSelectors" import { DATA_TYPE } from "store/slice/DisplayData/DisplayDataType" import { selectVisualizeItems } from "store/slice/VisualizeItem/VisualizeItemSelectors" +import { setClickedData } from "store/slice/VisualizeItem/VisualizeItemSlice" import { VISUALIZE_ITEM_SLICE_NAME } from "store/slice/VisualizeItem/VisualizeItemType" import { isImageItem, @@ -11,19 +12,21 @@ import { } from "store/slice/VisualizeItem/VisualizeItemUtils" import { ThunkApiConfig } from "store/store" -export const setImageItemClikedDataId = createAsyncThunk< +export const setImageItemClickedDataId = createAsyncThunk< void, - { itemId: number; clickedDataId: string }, + { itemId: number; clickedDataId: string | null }, ThunkApiConfig >( - `${VISUALIZE_ITEM_SLICE_NAME}/setImageItemClikedDataId`, + `${VISUALIZE_ITEM_SLICE_NAME}/setImageItemClickedDataId`, ({ itemId, clickedDataId }, thunkAPI) => { + thunkAPI.dispatch(setClickedData({ itemId, clickedDataId })) const items = selectVisualizeItems(thunkAPI.getState()) Object.values(items).forEach((item) => { if ( isTimeSeriesItem(item) && item.filePath != null && item.refImageItemId === itemId && + clickedDataId && !item.drawOrderList.includes(clickedDataId) ) { thunkAPI.dispatch( diff --git a/frontend/src/store/slice/VisualizeItem/VisualizeItemSelectors.ts b/frontend/src/store/slice/VisualizeItem/VisualizeItemSelectors.ts index 25c4c4837..d747b50a7 100644 --- a/frontend/src/store/slice/VisualizeItem/VisualizeItemSelectors.ts +++ b/frontend/src/store/slice/VisualizeItem/VisualizeItemSelectors.ts @@ -608,3 +608,6 @@ export const selectImageItemRangeUnit = throw new Error("invalid VisualaizeItemType") } } + +export const selectClickedRoi = (itemId: number) => (state: RootState) => + state.visualaizeItem.clickedRois[itemId] || null diff --git a/frontend/src/store/slice/VisualizeItem/VisualizeItemSlice.ts b/frontend/src/store/slice/VisualizeItem/VisualizeItemSlice.ts index 2ba706b28..7f190dfc2 100644 --- a/frontend/src/store/slice/VisualizeItem/VisualizeItemSlice.ts +++ b/frontend/src/store/slice/VisualizeItem/VisualizeItemSlice.ts @@ -9,7 +9,7 @@ import { run, runByCurrentUid } from "store/slice/Pipeline/PipelineActions" import { deleteDisplayItem, selectingImageArea, - setImageItemClikedDataId, + setImageItemClickedDataId, setNewDisplayDataPath, } from "store/slice/VisualizeItem/VisualizeItemActions" import { @@ -50,6 +50,7 @@ export const initialState: VisualaizeItem = { items: {}, selectedItemId: null, layout: [], + clickedRois: {}, } const displayDataCommonInitialValue = { itemType: VISUALIZE_ITEM_TYPE_SET.DISPLAY_DATA, @@ -857,6 +858,17 @@ export const visualaizeItemSlice = createSlice({ targetItem.selectedIndex = action.payload.selectedIndex } }, + + setClickedData: ( + state, + action: PayloadAction<{ + itemId: number + clickedDataId: string | null + }>, + ) => { + const { itemId, clickedDataId } = action.payload + state.clickedRois[itemId] = clickedDataId + }, }, extraReducers: (builder) => { builder @@ -906,20 +918,38 @@ export const visualaizeItemSlice = createSlice({ } resetImageActiveIndexFn(state, { itemId }) }) - .addCase(setImageItemClikedDataId.fulfilled, (state, action) => { + .addCase(setImageItemClickedDataId.fulfilled, (state, action) => { const { itemId: imageItemId, clickedDataId } = action.meta.arg - const targetItem = state.items[imageItemId] - if (isImageItem(targetItem)) { - targetItem.clickedDataId = clickedDataId + // Update clickedRois + if (clickedDataId === state.clickedRois[imageItemId]) { + state.clickedRois[imageItemId] = null + } else { + state.clickedRois[imageItemId] = clickedDataId } + // Update drawOrderList for related time series items Object.values(state.items).forEach((item) => { if (isTimeSeriesItem(item)) { if ( item.refImageItemId != null && - imageItemId === item.refImageItemId && - !item.drawOrderList.includes(clickedDataId) + imageItemId === item.refImageItemId ) { - item.drawOrderList.push(clickedDataId) + if (clickedDataId) { + // If clickedDataId exists, toggle its presence in drawOrderList + const index = item.drawOrderList.indexOf(clickedDataId) + if (index === -1) { + item.drawOrderList.push(clickedDataId) + } else { + item.drawOrderList.splice(index, 1) + } + } else { + // If clickedDataId is null (deselection), remove the previous selection + const previousSelection = state.clickedRois[imageItemId] + if (previousSelection) { + item.drawOrderList = item.drawOrderList.filter( + (id) => id !== previousSelection, + ) + } + } } } }) @@ -1025,6 +1055,7 @@ export const { setHistogramItemBins, setLineItemSelectedIndex, setPolartemItemSelectedIndex, + setClickedData, resetAllOrderList, reset, } = visualaizeItemSlice.actions diff --git a/frontend/src/store/slice/VisualizeItem/VisualizeItemType.ts b/frontend/src/store/slice/VisualizeItem/VisualizeItemType.ts index e563d55d6..8cb4911a8 100644 --- a/frontend/src/store/slice/VisualizeItem/VisualizeItemType.ts +++ b/frontend/src/store/slice/VisualizeItem/VisualizeItemType.ts @@ -11,6 +11,9 @@ export type VisualaizeItem = { [itemId: number | string]: VisualaizeItemType } layout: ItemLayout + clickedRois: { + [itemId: number | string]: string | null + } } export type ItemLayout = number[][] // itemIdをrow,columnで並べる @@ -77,7 +80,7 @@ export interface ImageItem extends DisplayDataItemBaseType { roiItem: RoiItem | null roiAlpha: number duration: number - clickedDataId?: string + clickedDataId?: string | null } export interface TimeSeriesItem extends DisplayDataItemBaseType { From f6b3669010315c47cb503760f440b69b06b34548 Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Mon, 10 Feb 2025 13:57:57 +0700 Subject: [PATCH 02/15] add colour change --- .../Workspace/Visualize/Plot/ImagePlot.tsx | 154 +++++------------- .../Visualize/Plot/TimeSeriesPlot.tsx | 27 +-- .../Workspace/Visualize/Visualize.tsx | 5 +- .../Workspace/Visualize/VisualizeContext.tsx | 133 +++++++++++++++ .../Workspace/Visualize/VisualizeItem.tsx | 22 ++- .../slice/DisplayData/DisplayDataActions.ts | 6 +- .../slice/DisplayData/DisplayDataSelectors.ts | 1 - .../slice/DisplayData/DisplayDataSlice.ts | 34 +--- .../slice/VisualizeItem/VisualizeItemSlice.ts | 45 ++--- 9 files changed, 225 insertions(+), 202 deletions(-) create mode 100644 frontend/src/components/Workspace/Visualize/VisualizeContext.tsx diff --git a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx index f00a54c00..16b34a630 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx @@ -31,13 +31,13 @@ import { styled } from "@mui/material/styles" import Switch from "@mui/material/Switch" import { DisplayDataContext } from "components/Workspace/Visualize/DataContext" +import { useVisualize } from "components/Workspace/Visualize/VisualizeContext" import { addRoi, cancelRoi, commitRoi, deleteRoi, mergeRoi, - clickRoi, getImageData, getRoiData, getStatus, @@ -77,9 +77,7 @@ import { selectVisualizeSaveFilename, selectVisualizeSaveFormat, selectImageItemAlpha, - selectRoiItemOutputKeys, selectVisualizeItems, - selectClickedRoi, selectImageItemShowRoiLabels, } from "store/slice/VisualizeItem/VisualizeItemSelectors" import { @@ -94,17 +92,10 @@ import { setDataCancel } from "store/slice/Workspace/WorkspaceSlice" import { AppDispatch, RootState } from "store/store" import { twoDimarrayEqualityFn } from "utils/EqualityUtils" -interface PointClick { - x: number - y: number - z: number -} - export type StatusROI = { temp_add_roi: number[] temp_delete_roi: number[] temp_merge_roi: number[] - temp_selected_roi: number[] } const ADD_ROI = "Add ROI" @@ -213,8 +204,6 @@ const ImagePlotChart = memo(function ImagePlotChart({ ) const [roiDataState, setRoiDataState] = useState(roiData) - const [pointClick, setPointClick] = useState([]) - const itemsVisual = useSelector(selectVisualizeItems) const showticklabels = useSelector(selectImageItemShowticklabels(itemId)) const showline = useSelector(selectImageItemShowLine(itemId)) @@ -231,10 +220,14 @@ const ImagePlotChart = memo(function ImagePlotChart({ const [startDragAddRoi, setStartDragAddRoi] = useState(false) const [action, setAction] = useState("") const [positionDrag, setChangeSize] = useState() - const clickedDataId = useSelector(selectClickedRoi(itemId)) const showRoiLabels = useSelector(selectImageItemShowRoiLabels(itemId)) - const outputKey: string | null = useSelector(selectRoiItemOutputKeys(itemId)) + const allowEditRoi = useMemo( + () => roiFilePath?.includes(CELL_ROI), + [roiFilePath], + ) + const { setRoisClick, roisClick, resetRoisClick } = useVisualize() + const roiClicked = useMemo(() => roisClick[itemId] || [], [itemId, roisClick]) const selectedStatus = useSelector(selectStatusRoi) @@ -244,7 +237,6 @@ const ImagePlotChart = memo(function ImagePlotChart({ temp_add_roi: [], temp_delete_roi: [], temp_merge_roi: [], - temp_selected_roi: [], } ) }, [selectedStatus]) @@ -285,25 +277,6 @@ const ImagePlotChart = memo(function ImagePlotChart({ //eslint-disable-next-line react-hooks/exhaustive-deps }, [roiFilePath]) - useEffect(() => { - if (statusRoi && roiDataState.length > 0) { - const newPointClick = (statusRoi.temp_selected_roi || []) - .map((z) => { - // Find the coordinates of the ROI center - const yIndex = roiDataState.findIndex((row) => row.includes(z)) - const xIndex = - roiDataState[yIndex]?.findIndex((val) => val === z) ?? -1 - return { - x: xIndex, - y: yIndex, - z: z, - } - }) - .filter((point) => point.x !== -1 && point.y !== -1) - setPointClick(newPointClick) - } - }, [statusRoi, roiDataState]) - const data = useMemo( () => [ { @@ -346,34 +319,31 @@ const ImagePlotChart = memo(function ImagePlotChart({ const rgba = colorscaleRoi[new_i] const hex = rgba2hex(rgba, roiAlpha) - const isClickPoint = pointClick.some((point) => point.z === i) - const isSelected = statusRoi?.temp_selected_roi?.includes(i) || false + const isClickPoint = + !roiClicked.length || roiClicked.some((point) => point === i) const isDeleted = statusRoi?.temp_delete_roi?.includes(i) || false const isMerged = statusRoi?.temp_merge_roi?.includes(i) || false const isAdded = statusRoi?.temp_add_roi?.includes(i) || false - if (isClickPoint || isSelected || isDeleted || isMerged || isAdded) { + if ( + allowEditRoi && + (isClickPoint || isDeleted || isMerged || isAdded) + ) { switch (action) { case DELETE_ROI: - if (isClickPoint || isSelected || isDeleted) - return [offset, "#FFA500"] // orange + if (isClickPoint || isDeleted) return [offset, "#FFA500"] // orange break case MERGE_ROI: - if (isClickPoint || isSelected || isMerged) - return [offset, "#e134eb"] // purple + if (isClickPoint || isMerged) return [offset, "#e134eb"] // purple break case ADD_ROI: if (isAdded) return [offset, "3483eb"] // red break default: - if (isClickPoint || isSelected) return [offset, "#ffffff"] // white + if (isClickPoint) return [offset, hex] } } - if (clickedDataId !== null && i.toString() === clickedDataId) { - return [offset, "#FF4500"] // Bright orange-red for clicked ROI - } - - return [offset, hex] + return [offset, rgba2hex(rgba, 0.3)] }), zmin: 0, zmax: timeDataMaxIndex, @@ -384,18 +354,18 @@ const ImagePlotChart = memo(function ImagePlotChart({ ], [ imageData, - roiDataState, - zsmooth, - showscale, colorscale, - colorscaleRoi, + showscale, + zsmooth, + roiDataState, + action, timeDataMaxIndex, - roiAlpha, alpha, - pointClick, - action, + colorscaleRoi, + roiAlpha, + roiClicked, statusRoi, - clickedDataId, + allowEditRoi, ], ) @@ -548,48 +518,10 @@ const ImagePlotChart = memo(function ImagePlotChart({ // use as unknown because original PlotDatum does not have z property const point: PlotDatum = event.points[0] as unknown as PlotDatum if (point.curveNumber >= 1 && point.z >= 0) { - if (outputKey === "cell_roi") { - setSelectRoi({ - x: Number(point.x), - y: Number(point.y), - z: Number(point.z), - }) - } else { - dispatch( - setImageItemClickedDataId({ - itemId, - clickedDataId: point.z.toString(), - }), - ) - } - } - } - - const setSelectRoi = (point: PointClick) => { - if (isNaN(Number(point.z))) return - - const roiIndex = Number(point.z) - - if (action) { dispatch( - clickRoi({ - roiIndex, - }), + setImageItemClickedDataId({ itemId, clickedDataId: String(point.z) }), ) - } - - dispatch( - setImageItemClickedDataId({ - itemId, - clickedDataId: roiIndex.toString(), - }), - ) - - const checkIndex = pointClick.findIndex((item) => item.z === roiIndex) - if (checkIndex < 0) { - setPointClick([...pointClick, point]) - } else { - setPointClick(pointClick.filter((item) => item.z !== roiIndex)) + setRoisClick(itemId, point.z) } } @@ -602,7 +534,6 @@ const ImagePlotChart = memo(function ImagePlotChart({ ) { return } - setPointClick([]) try { await dispatch(cancelRoi({ path: refRoiFilePath.current, workspaceId })) } finally { @@ -637,7 +568,6 @@ const ImagePlotChart = memo(function ImagePlotChart({ setEdit(true) setSizeDrag(initSizeDrag) setChangeSize(undefined) - setPointClick([]) } const onMouseDownDragAddRoi = () => { @@ -715,32 +645,28 @@ const ImagePlotChart = memo(function ImagePlotChart({ onCancelAdd() } if (action === MERGE_ROI) { - if (statusRoi.temp_selected_roi.length < 2) return + if (roiClicked.length < 2) return dispatch(resetAllOrderList()) dispatch( mergeRoi({ path: roiFilePath, workspaceId, - data: { - ids: statusRoi.temp_selected_roi, - }, + data: { ids: roiClicked }, }), ) - setPointClick([]) + resetRoisClick(itemId) workspaceId && dispatch(getRoiData({ path: roiFilePath, workspaceId })) } else if (action === DELETE_ROI) { - if (!statusRoi.temp_selected_roi.length) return + if (!roiClicked.length) return dispatch(resetAllOrderList()) await dispatch( deleteRoi({ path: roiFilePath, workspaceId, - data: { - ids: statusRoi.temp_selected_roi, - }, + data: { ids: roiClicked }, }), ) - setPointClick([]) + resetRoisClick(itemId) workspaceId && dispatch(getRoiData({ path: roiFilePath, workspaceId })) } setAction("") @@ -795,14 +721,12 @@ const ImagePlotChart = memo(function ImagePlotChart({ } const renderActionRoi = () => { - if (!roiFilePath || !roiFilePath.includes(CELL_ROI)) return null + if (!allowEditRoi) return null if (action) { return ( <> {action !== ADD_ROI ? ( - - ROI Selecteds: [{statusRoi?.temp_selected_roi?.join(",") || ""}] - + ROI Selecteds: [{roiClicked?.join(",") || ""}] ) : null} Object.keys(links).find((key) => links[key] === itemId), + [itemId, links], + ) const roiUniqueList = useSelector(selectRoiUniqueList(roiPath), shallowEqual) @@ -343,27 +348,11 @@ const TimeSeriesPlotImple = memo(function TimeSeriesPlotImple() { const onLegendClick = (event: LegendClickEvent) => { const clickedSeriesId = dataKeys[event.curveNumber] - - const newDrawOrderList = drawOrderList.includes(clickedSeriesId) - ? drawOrderList.filter((value) => value !== clickedSeriesId) - : [...drawOrderList, clickedSeriesId] if (dialogFilterNodeId) { setRoiSelected(Number(clickedSeriesId)) return false } - - dispatch( - setTimeSeriesItemDrawOrderList({ - itemId, - drawOrderList: newDrawOrderList, - }), - ) - dispatch( - clickRoi({ - roiIndex: Number(clickedSeriesId), - }), - ) - + if (itemIdRoi) setRoisClick(Number(itemIdRoi), Number(clickedSeriesId)) // set DisplayNumbers if (!drawOrderList.includes(clickedSeriesId)) { dispatch(getTimeSeriesDataById({ path, index: clickedSeriesId })) diff --git a/frontend/src/components/Workspace/Visualize/Visualize.tsx b/frontend/src/components/Workspace/Visualize/Visualize.tsx index 487c51de0..af45200a2 100644 --- a/frontend/src/components/Workspace/Visualize/Visualize.tsx +++ b/frontend/src/components/Workspace/Visualize/Visualize.tsx @@ -6,6 +6,7 @@ import { styled } from "@mui/material/styles" import { CurrentPipelineInfo } from "components/common/CurrentPipelineInfo" import { FlexItemList } from "components/Workspace/Visualize/FlexItemList" +import { VisualizeProvider } from "components/Workspace/Visualize/VisualizeContext" import { VisualizeItemEditor } from "components/Workspace/Visualize/VisualizeItemEditor" import { CONTENT_HEIGHT, DRAWER_WIDTH } from "const/Layout" @@ -25,7 +26,9 @@ const Visualize: FC = () => { - + + + ) diff --git a/frontend/src/components/Workspace/Visualize/VisualizeContext.tsx b/frontend/src/components/Workspace/Visualize/VisualizeContext.tsx new file mode 100644 index 000000000..1e2ac3bc3 --- /dev/null +++ b/frontend/src/components/Workspace/Visualize/VisualizeContext.tsx @@ -0,0 +1,133 @@ +import { + createContext, + PropsWithChildren, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react" +import { useDispatch } from "react-redux" + +import { getTimeSeriesDataById } from "store/slice/DisplayData/DisplayDataActions" +import { selectVisualizeDataFilePath } from "store/slice/VisualizeItem/VisualizeItemSelectors" +import { + setTimeSeriesItemDrawOrder, + setTimeSeriesItemDrawOrderList, +} from "store/slice/VisualizeItem/VisualizeItemSlice" +import { AppDispatch, RootState, store } from "store/store" + +type TVisualizeContext = { + roisClick: { [itemId: string]: number[] } + setRoisClick: (itemId: number, roi: number) => void + resetRoisClick: (itemId: number) => void + links: { [linkItemId: string]: string | number } + setLinks: (itemId: number, linkItemId: number) => () => void +} + +const VisualizeContext = createContext({ + roisClick: {}, + setRoisClick: () => null, + resetRoisClick: () => null, + links: {}, + setLinks: () => () => null, +}) + +export const useVisualize = () => useContext(VisualizeContext) + +export const VisualizeProvider = ({ children }: PropsWithChildren) => { + const [rois, setRois] = useState<{ [key: string]: number[] }>({}) + const [links, _setLinks] = useState<{ [itemId: string]: string | number }>({}) + const dispatch = useDispatch() + + const linksRef = useRef(links) + const roisRef = useRef(rois) + + useEffect(() => { + linksRef.current = links + }, [links]) + + useEffect(() => { + roisRef.current = rois + }, [rois]) + + const setRoisClick = useCallback( + (itemId: string | number, roi: number) => { + setRois((pre) => ({ + ...pre, + [itemId]: pre[itemId]?.some((e) => e === roi) + ? pre[itemId].filter((e) => e !== roi) + : [...(pre[itemId] || []), roi], + })) + const fluorescenceId = linksRef.current[itemId] + if (fluorescenceId !== undefined) { + dispatch( + setTimeSeriesItemDrawOrder({ + itemId: Number(fluorescenceId), + drawOrder: String(roi), + }), + ) + } + }, + [dispatch], + ) + + const resetRoisClick = useCallback( + (itemId: string | number) => { + setRois((pre) => ({ ...pre, [itemId]: [] })) + const fluorescenceId = linksRef.current[itemId] + if (fluorescenceId !== undefined) { + dispatch( + setTimeSeriesItemDrawOrderList({ + itemId: Number(fluorescenceId), + drawOrderList: [], + }), + ) + } + }, + [dispatch], + ) + + const setLinks = useCallback( + (itemId: number, linkItemId: number) => { + _setLinks((pre) => ({ ...pre, [linkItemId]: itemId })) + const drawOrders = roisRef.current[linkItemId] || [] + dispatch( + setTimeSeriesItemDrawOrderList({ + itemId: Number(itemId), + drawOrderList: drawOrders.map((e) => String(e)), + }), + ) + drawOrders.forEach(async (e) => { + const path = selectVisualizeDataFilePath(itemId)( + store.getState() as RootState, + ) + if (!path) return + await dispatch( + getTimeSeriesDataById({ path, index: String(e) }), + ).unwrap() + }) + return () => { + dispatch( + setTimeSeriesItemDrawOrderList({ + itemId: Number(itemId), + drawOrderList: [], + }), + ) + _setLinks((pre) => { + delete pre[linkItemId] + return pre + }) + } + }, + [dispatch], + ) + + return ( + + {children} + + ) +} diff --git a/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx b/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx index 8f9b03332..338e19949 100644 --- a/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx +++ b/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx @@ -1,4 +1,11 @@ -import { memo, useCallback, useState, MouseEvent, useEffect } from "react" +import { + memo, + useCallback, + useState, + MouseEvent, + useEffect, + useRef, +} from "react" import { useDispatch, useSelector } from "react-redux" import { Close, Numbers } from "@mui/icons-material" @@ -14,6 +21,7 @@ import Loading from "components/common/Loading" import { useMouseDragHandler } from "components/utils/MouseDragUtil" import { DisplayDataItem } from "components/Workspace/Visualize/DisplayDataItem" import { FilePathSelect } from "components/Workspace/Visualize/FilePathSelect" +import { useVisualize } from "components/Workspace/Visualize/VisualizeContext" import { selectLoading, selectIsEditRoiCommitting, @@ -260,6 +268,8 @@ const FilePathSelectItem = memo(function FilePathSelectItem({ const RefImageItemIdSelect = memo(function RefImageItemIdSelect({ itemId, }: ItemIdProps) { + const { setLinks } = useVisualize() + const refSub = useRef<() => void>() const dispatch = useDispatch() const itemIdList = useSelector( selectVisualizeImageAndRoiItemIdList, @@ -273,10 +283,20 @@ const RefImageItemIdSelect = memo(function RefImageItemIdSelect({ refImageItemId: isNaN(value) ? null : value, }), ) + if (isNaN(value)) refSub.current?.() + else refSub.current = setLinks(itemId, value) } const selectedRefImageItemId = useSelector( selectTimeSeriesItemRefImageItemId(itemId), ) + + useEffect(() => { + if (selectedRefImageItemId || selectedRefImageItemId === 0) { + refSub.current = setLinks(itemId, selectedRefImageItemId) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + return ( Link to box (#) diff --git a/frontend/src/store/slice/DisplayData/DisplayDataActions.ts b/frontend/src/store/slice/DisplayData/DisplayDataActions.ts index 40d4a89fe..d6e469422 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataActions.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataActions.ts @@ -1,4 +1,4 @@ -import { createAsyncThunk, createAction } from "@reduxjs/toolkit" +import { createAsyncThunk } from "@reduxjs/toolkit" import { TimeSeriesData, @@ -286,10 +286,6 @@ export const getStatus = createAsyncThunk< }, ) -export const clickRoi = createAction<{ - roiIndex: number -}>(`${DISPLAY_DATA_SLICE_NAME}/clickRoi`) - export const getScatterData = createAsyncThunk< { data: ScatterData; meta?: PlotMetaData }, { path: string } diff --git a/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts b/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts index bbe496083..d57a7c2a2 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts @@ -376,6 +376,5 @@ export const selectStatusRoi = createSelector( temp_add_roi: statusRoi?.temp_add_roi || [], temp_delete_roi: statusRoi?.temp_delete_roi || [], temp_merge_roi: statusRoi?.temp_merge_roi || [], - temp_selected_roi: statusRoi?.temp_selected_roi || [], }), ) diff --git a/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts b/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts index c39564b1d..8d08cac42 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataSlice.ts @@ -1,4 +1,4 @@ -import { createSlice, isAnyOf, PayloadAction } from "@reduxjs/toolkit" +import { createSlice, isAnyOf } from "@reduxjs/toolkit" import { getCsvData, @@ -20,7 +20,6 @@ import { addRoi, deleteRoi, commitRoi, - clickRoi, getStatus, } from "store/slice/DisplayData/DisplayDataActions" import { @@ -54,7 +53,6 @@ const initialState: DisplayData = { temp_add_roi: [], temp_delete_roi: [], temp_merge_roi: [], - temp_selected_roi: [], }, isEditRoiCommitting: false, } @@ -607,7 +605,6 @@ export const displayDataSlice = createSlice({ temp_add_roi: [], temp_delete_roi: [], temp_merge_roi: [], - temp_selected_roi: [], } state.loadingStack.pop() @@ -618,35 +615,6 @@ export const displayDataSlice = createSlice({ state.loadingStack.push((state.loading = true)) }) - .addCase( - clickRoi, - (state, action: PayloadAction<{ roiIndex: number }>) => { - const { roiIndex } = action.payload - if (!state.statusRoi) { - state.statusRoi = { - temp_add_roi: [], - temp_delete_roi: [], - temp_merge_roi: [], - temp_selected_roi: [], - } - } - state.statusRoi.temp_add_roi = state.statusRoi.temp_add_roi || [] - state.statusRoi.temp_delete_roi = - state.statusRoi.temp_delete_roi || [] - state.statusRoi.temp_merge_roi = state.statusRoi.temp_merge_roi || [] - state.statusRoi.temp_selected_roi = - state.statusRoi.temp_selected_roi || [] - - const index = state.statusRoi.temp_selected_roi.indexOf(roiIndex) - if (index === -1) { - // If not selected, add it - state.statusRoi.temp_selected_roi.push(roiIndex) - } else { - // If already selected, remove it - state.statusRoi.temp_selected_roi.splice(index, 1) - } - }, - ) .addMatcher( isAnyOf( cancelRoi.pending, diff --git a/frontend/src/store/slice/VisualizeItem/VisualizeItemSlice.ts b/frontend/src/store/slice/VisualizeItem/VisualizeItemSlice.ts index ad1259163..e2be44609 100644 --- a/frontend/src/store/slice/VisualizeItem/VisualizeItemSlice.ts +++ b/frontend/src/store/slice/VisualizeItem/VisualizeItemSlice.ts @@ -689,6 +689,23 @@ export const visualaizeItemSlice = createSlice({ targetItem.drawOrderList = drawOrderList } }, + setTimeSeriesItemDrawOrder: ( + state, + action: PayloadAction<{ + itemId: number + drawOrder: string + }>, + ) => { + const { itemId, drawOrder } = action.payload + const targetItem = state.items[itemId] + if (isTimeSeriesItem(targetItem)) { + if (targetItem.drawOrderList.some((e) => e === drawOrder)) { + targetItem.drawOrderList = targetItem.drawOrderList.filter( + (e) => e !== drawOrder, + ) + } else targetItem.drawOrderList.push(drawOrder) + } + }, resetAllOrderList: (state) => { Object.keys(state.items).forEach((id: string | number) => { const targetItem = state.items[id] @@ -721,7 +738,6 @@ export const visualaizeItemSlice = createSlice({ const targetItem = state.items[itemId] if (isTimeSeriesItem(targetItem)) { targetItem.refImageItemId = refImageItemId ?? null - targetItem.drawOrderList = [] } }, setHeatMapItemShowScale: ( @@ -940,32 +956,6 @@ export const visualaizeItemSlice = createSlice({ state.clickedRois[imageItemId] = clickedDataId } // Update drawOrderList for related time series items - Object.values(state.items).forEach((item) => { - if (isTimeSeriesItem(item)) { - if ( - item.refImageItemId != null && - imageItemId === item.refImageItemId - ) { - if (clickedDataId) { - // If clickedDataId exists, toggle its presence in drawOrderList - const index = item.drawOrderList.indexOf(clickedDataId) - if (index === -1) { - item.drawOrderList.push(clickedDataId) - } else { - item.drawOrderList.splice(index, 1) - } - } else { - // If clickedDataId is null (deselection), remove the previous selection - const previousSelection = state.clickedRois[imageItemId] - if (previousSelection) { - item.drawOrderList = item.drawOrderList.filter( - (id) => id !== previousSelection, - ) - } - } - } - } - }) }) .addCase(selectingImageArea.fulfilled, (state, action) => { const { itemId: imageItemId } = action.meta.arg @@ -1055,6 +1045,7 @@ export const { setTimeSeriesItemXrangeLeft, setTimeSeriesItemXrangeRight, setTimeSeriesItemDrawOrderList, + setTimeSeriesItemDrawOrder, setTimeSeriesItemMaxIndex, setTimeSeriesRefImageItemId, setHeatMapItemShowScale, From 6cf67ff5e57a58fd2eed830644b155d27db4d8de Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Mon, 10 Feb 2025 16:25:49 +0700 Subject: [PATCH 03/15] Fix highlight --- frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx index 16b34a630..9032fbb35 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx @@ -343,6 +343,7 @@ const ImagePlotChart = memo(function ImagePlotChart({ if (isClickPoint) return [offset, hex] } } + if (!allowEditRoi && isClickPoint) return [offset, hex] return [offset, rgba2hex(rgba, 0.3)] }), zmin: 0, From b9f62de55a5b96ddb5321531dc233d16af2e2736 Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Tue, 11 Feb 2025 15:51:32 +0700 Subject: [PATCH 04/15] block click roi add not commit --- .../src/components/Workspace/Visualize/Plot/ImagePlot.tsx | 1 + frontend/src/components/Workspace/Visualize/VisualizeItem.tsx | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx index 9032fbb35..b989628c1 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx @@ -519,6 +519,7 @@ const ImagePlotChart = memo(function ImagePlotChart({ // use as unknown because original PlotDatum does not have z property const point: PlotDatum = event.points[0] as unknown as PlotDatum if (point.curveNumber >= 1 && point.z >= 0) { + if (statusRoi.temp_add_roi.includes(point.z)) return dispatch( setImageItemClickedDataId({ itemId, clickedDataId: String(point.z) }), ) diff --git a/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx b/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx index 338e19949..c432285e8 100644 --- a/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx +++ b/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx @@ -402,6 +402,9 @@ const RoiSelect = memo(function RoiSelect({ const dispatch = useDispatch() const roiItemNodeId = useSelector(selectRoiItemNodeId(itemId)) const roiItemFilePath = useSelector(selectRoiItemFilePath(itemId)) + + const { resetRoisClick } = useVisualize() + useEffect(() => { if (!roiItemFilePath) return setRoiFilePath?.(roiItemFilePath) @@ -413,6 +416,7 @@ const RoiSelect = memo(function RoiSelect({ dataType: string, outputKey?: string, ) => { + resetRoisClick(itemId) dispatch(setRoiItemFilePath({ itemId, nodeId, filePath, outputKey })) } return ( From 9a0ccbd723b191a3a481fff7594ec58f8eef4c3a Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Tue, 11 Feb 2025 16:29:45 +0700 Subject: [PATCH 05/15] reset roi click when commit roi --- frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx index b989628c1..5fe9b1f65 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx @@ -699,6 +699,7 @@ const ImagePlotChart = memo(function ImagePlotChart({ variant: "success", }) resetTimeSeries() + resetRoisClick(itemId) } catch (error) { enqueueSnackbar("Failed to commit Edit ROI.", { variant: "error" }) } finally { From bf6dda7416b654c1991940e251a31490a1bf09fb Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Tue, 11 Feb 2025 18:27:01 +0700 Subject: [PATCH 06/15] Fix call api getTimeSeriesDataById when roi is temp add --- .../Workspace/Visualize/Plot/ImagePlot.tsx | 1 - .../Workspace/Visualize/VisualizeContext.tsx | 17 ++++++++++++----- .../slice/DisplayData/DisplayDataSelectors.ts | 5 +++++ .../slice/VisualizeItem/VisualizeItemActions.ts | 9 +++++++-- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx index 5fe9b1f65..d4d41eaa2 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx @@ -519,7 +519,6 @@ const ImagePlotChart = memo(function ImagePlotChart({ // use as unknown because original PlotDatum does not have z property const point: PlotDatum = event.points[0] as unknown as PlotDatum if (point.curveNumber >= 1 && point.z >= 0) { - if (statusRoi.temp_add_roi.includes(point.z)) return dispatch( setImageItemClickedDataId({ itemId, clickedDataId: String(point.z) }), ) diff --git a/frontend/src/components/Workspace/Visualize/VisualizeContext.tsx b/frontend/src/components/Workspace/Visualize/VisualizeContext.tsx index 1e2ac3bc3..c49852ac1 100644 --- a/frontend/src/components/Workspace/Visualize/VisualizeContext.tsx +++ b/frontend/src/components/Workspace/Visualize/VisualizeContext.tsx @@ -7,9 +7,10 @@ import { useRef, useState, } from "react" -import { useDispatch } from "react-redux" +import { useSelector, useDispatch, shallowEqual } from "react-redux" import { getTimeSeriesDataById } from "store/slice/DisplayData/DisplayDataActions" +import { selectStatusRoiTempAdd } from "store/slice/DisplayData/DisplayDataSelectors" import { selectVisualizeDataFilePath } from "store/slice/VisualizeItem/VisualizeItemSelectors" import { setTimeSeriesItemDrawOrder, @@ -39,6 +40,10 @@ export const VisualizeProvider = ({ children }: PropsWithChildren) => { const [rois, setRois] = useState<{ [key: string]: number[] }>({}) const [links, _setLinks] = useState<{ [itemId: string]: string | number }>({}) const dispatch = useDispatch() + const selectedStatusTempAdd = useSelector( + selectStatusRoiTempAdd, + shallowEqual, + ) const linksRef = useRef(links) const roisRef = useRef(rois) @@ -103,9 +108,11 @@ export const VisualizeProvider = ({ children }: PropsWithChildren) => { store.getState() as RootState, ) if (!path) return - await dispatch( - getTimeSeriesDataById({ path, index: String(e) }), - ).unwrap() + if (!selectedStatusTempAdd.includes(Number(e))) { + await dispatch( + getTimeSeriesDataById({ path, index: String(e) }), + ).unwrap() + } }) return () => { dispatch( @@ -120,7 +127,7 @@ export const VisualizeProvider = ({ children }: PropsWithChildren) => { }) } }, - [dispatch], + [dispatch, selectedStatusTempAdd], ) return ( diff --git a/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts b/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts index d57a7c2a2..f2e2ef9e2 100644 --- a/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts +++ b/frontend/src/store/slice/DisplayData/DisplayDataSelectors.ts @@ -378,3 +378,8 @@ export const selectStatusRoi = createSelector( temp_merge_roi: statusRoi?.temp_merge_roi || [], }), ) + +export const selectStatusRoiTempAdd = createSelector( + [(state: RootState) => state.displayData.statusRoi], + (statusRoi): number[] => statusRoi?.temp_add_roi, +) diff --git a/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts b/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts index 5a748f47b..d9144b4ca 100644 --- a/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts +++ b/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts @@ -1,7 +1,10 @@ import { createAsyncThunk, createAction } from "@reduxjs/toolkit" import { getTimeSeriesDataById } from "store/slice/DisplayData/DisplayDataActions" -import { selectRoiData } from "store/slice/DisplayData/DisplayDataSelectors" +import { + selectRoiData, + selectStatusRoiTempAdd, +} from "store/slice/DisplayData/DisplayDataSelectors" import { DATA_TYPE } from "store/slice/DisplayData/DisplayDataType" import { selectVisualizeItems } from "store/slice/VisualizeItem/VisualizeItemSelectors" import { setClickedData } from "store/slice/VisualizeItem/VisualizeItemSlice" @@ -21,13 +24,15 @@ export const setImageItemClickedDataId = createAsyncThunk< ({ itemId, clickedDataId }, thunkAPI) => { thunkAPI.dispatch(setClickedData({ itemId, clickedDataId })) const items = selectVisualizeItems(thunkAPI.getState()) + const tempAdd = selectStatusRoiTempAdd(thunkAPI.getState()) Object.values(items).forEach((item) => { if ( isTimeSeriesItem(item) && item.filePath != null && item.refImageItemId === itemId && clickedDataId && - !item.drawOrderList.includes(clickedDataId) + !item.drawOrderList.includes(clickedDataId) && + !tempAdd?.includes(Number(clickedDataId)) ) { thunkAPI.dispatch( getTimeSeriesDataById({ path: item.filePath, index: clickedDataId }), From 5eebc2ea9a368ac39d14a1f76cfb8a10289d74cb Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Wed, 12 Feb 2025 13:21:13 +0700 Subject: [PATCH 07/15] remove check --- .../src/store/slice/VisualizeItem/VisualizeItemActions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts b/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts index d9144b4ca..f4d77abe3 100644 --- a/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts +++ b/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts @@ -24,15 +24,15 @@ export const setImageItemClickedDataId = createAsyncThunk< ({ itemId, clickedDataId }, thunkAPI) => { thunkAPI.dispatch(setClickedData({ itemId, clickedDataId })) const items = selectVisualizeItems(thunkAPI.getState()) - const tempAdd = selectStatusRoiTempAdd(thunkAPI.getState()) + // const tempAdd = selectStatusRoiTempAdd(thunkAPI.getState()) Object.values(items).forEach((item) => { if ( isTimeSeriesItem(item) && item.filePath != null && item.refImageItemId === itemId && clickedDataId && - !item.drawOrderList.includes(clickedDataId) && - !tempAdd?.includes(Number(clickedDataId)) + !item.drawOrderList.includes(clickedDataId) + // && !tempAdd?.includes(Number(clickedDataId)) ) { thunkAPI.dispatch( getTimeSeriesDataById({ path: item.filePath, index: clickedDataId }), From e12c1db1f428fc3493f25e88a0e7c507d3293670 Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Wed, 12 Feb 2025 13:36:23 +0700 Subject: [PATCH 08/15] revert fix error call api --- .../src/store/slice/VisualizeItem/VisualizeItemActions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts b/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts index f4d77abe3..d9144b4ca 100644 --- a/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts +++ b/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts @@ -24,15 +24,15 @@ export const setImageItemClickedDataId = createAsyncThunk< ({ itemId, clickedDataId }, thunkAPI) => { thunkAPI.dispatch(setClickedData({ itemId, clickedDataId })) const items = selectVisualizeItems(thunkAPI.getState()) - // const tempAdd = selectStatusRoiTempAdd(thunkAPI.getState()) + const tempAdd = selectStatusRoiTempAdd(thunkAPI.getState()) Object.values(items).forEach((item) => { if ( isTimeSeriesItem(item) && item.filePath != null && item.refImageItemId === itemId && clickedDataId && - !item.drawOrderList.includes(clickedDataId) - // && !tempAdd?.includes(Number(clickedDataId)) + !item.drawOrderList.includes(clickedDataId) && + !tempAdd?.includes(Number(clickedDataId)) ) { thunkAPI.dispatch( getTimeSeriesDataById({ path: item.filePath, index: clickedDataId }), From f416ae4c4ba5c7689b8dacd323e5afa967cb79af Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Tue, 18 Feb 2025 17:45:38 +0700 Subject: [PATCH 09/15] Fix change colour --- frontend/src/components/Workspace/Visualize/VisualizeItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx b/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx index c432285e8..9c9cf4aea 100644 --- a/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx +++ b/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx @@ -283,8 +283,8 @@ const RefImageItemIdSelect = memo(function RefImageItemIdSelect({ refImageItemId: isNaN(value) ? null : value, }), ) - if (isNaN(value)) refSub.current?.() - else refSub.current = setLinks(itemId, value) + refSub.current?.() + refSub.current = setLinks(itemId, value) } const selectedRefImageItemId = useSelector( selectTimeSeriesItemRefImageItemId(itemId), From de83bea7ba88a6c09488ecbcd78e6e11f13d7542 Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Tue, 18 Feb 2025 18:10:00 +0700 Subject: [PATCH 10/15] fix roiAlpha --- frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx index ce576b03f..20e3d97f9 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx @@ -70,7 +70,6 @@ import { selectImageItemEndIndex, selectRoiItemFilePath, selectRoiItemIndex, - selectImageItemRoiAlpha, selectImageItemDuration, selectVisualizeItemWidth, selectVisualizeItemHeight, @@ -317,7 +316,7 @@ const ImagePlotChart = memo(function ImagePlotChart({ colorscale: [...Array(timeDataMaxIndex + 1)].map((_, i) => { const offset: number = i / timeDataMaxIndex const rgba = getRoiColor(i) - const hex = rgba2hex(rgba, roiAlpha) + const hex = rgba2hex(rgba, 1) const isClickPoint = !roiClicked.length || roiClicked.some((point) => point === i) From f28ee6ed6efe6e3ad0bbd5ab9d877feacb2fbf3a Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Wed, 19 Feb 2025 11:52:02 +0700 Subject: [PATCH 11/15] Reset all roi clicked when add, edit or delete roi --- frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx index 20e3d97f9..c5a0c5b6f 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx @@ -669,6 +669,7 @@ const ImagePlotChart = memo(function ImagePlotChart({ } setAction("") setEdit(true) + resetRoisClick(itemId) dispatch(getStatus({ path: roiFilePath, workspaceId })) } From c31aae5a9fe5313020d4a3ff8bf83e6110675d5e Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Wed, 19 Feb 2025 18:23:49 +0700 Subject: [PATCH 12/15] Add link all roi --- .../Workspace/Visualize/Plot/RoiPlot.tsx | 44 ++++++++++++------- .../Workspace/Visualize/VisualizeContext.tsx | 40 ++++++++++++++++- .../Workspace/Visualize/VisualizeItem.tsx | 2 + 3 files changed, 68 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx index 2a42a0ee6..fc3f45e79 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx @@ -13,6 +13,7 @@ import { } from "components/Workspace/FlowChart/Dialog/DialogContext" import { useBoxFilter } from "components/Workspace/FlowChart/Dialog/FilterContext" import { DisplayDataContext } from "components/Workspace/Visualize/DataContext" +import { useVisualize } from "components/Workspace/Visualize/VisualizeContext" import { getRoiData } from "store/slice/DisplayData/DisplayDataActions" import { selectRoiData, @@ -69,6 +70,9 @@ const RoiPlotImple = memo(function RoiPlotImple() { const { setRoiSelected, roisSelected, setMaxRoi } = useRoisSelected() const { filterParam, setRoiPath } = useBoxFilter() + const { setRoisClickWithGetTime, roisClick, isVisualize } = useVisualize() + + const roiVisualSelected = roisClick[itemId] useEffect(() => { setRoiPath(path) @@ -108,33 +112,39 @@ const RoiPlotImple = memo(function RoiPlotImple() { const onChartClick = (event: PlotMouseEvent) => { const point = event.points[0] as unknown as { z: number } setRoiSelected(point.z) + setRoisClickWithGetTime(itemId, point.z) } const colorscale = useMemo(() => { - if (!dialogFilterNodeId || timeDataMaxIndex < 1) { - return colorscaleRoi.map((value, idx) => { - if (timeDataMaxIndex < 1 && !roisSelected.includes(0)) { - return [ - String(idx / (nshades - 1)), - `${value}${(77).toString(16).toUpperCase()}`, - ] + if ((dialogFilterNodeId || isVisualize) && timeDataMaxIndex >= 1) { + return [...Array(timeDataMaxIndex + 1)].map((_, i) => { + const new_i = Math.floor(((i % 10) * 10 + i / 10) % nshades) + const offset: number = i / timeDataMaxIndex + const rgba = colorscaleRoi[new_i] + if ( + (!dialogFilterNodeId && !roiVisualSelected?.length) || + [...roisSelected, ...roiVisualSelected].includes(i) + ) { + return [offset, rgba] } - return [String(idx / (nshades - 1)), value] + return [offset, `${rgba}${(77).toString(16).toUpperCase()}`] }) } - return [...Array(timeDataMaxIndex + 1)].map((_, i) => { - const new_i = Math.floor(((i % 10) * 10 + i / 10) % nshades) - const offset: number = i / timeDataMaxIndex - const rgba = colorscaleRoi[new_i] - if (!dialogFilterNodeId || roisSelected.includes(i)) { - return [offset, rgba] + return colorscaleRoi.map((value, idx) => { + if (timeDataMaxIndex < 1 && !roisSelected.includes(0)) { + return [ + String(idx / (nshades - 1)), + `${value}${(77).toString(16).toUpperCase()}`, + ] } - return [offset, `${rgba}${(77).toString(16).toUpperCase()}`] + return [String(idx / (nshades - 1)), value] }) }, [ colorscaleRoi, dialogFilterNodeId, + isVisualize, nshades, + roiVisualSelected, roisSelected, timeDataMaxIndex, ]) @@ -149,14 +159,14 @@ const RoiPlotImple = memo(function RoiPlotImple() { hoverongaps: false, // zsmooth: zsmooth, // ['best', 'fast', false] zsmooth: false, - showscale: !dialogFilterNodeId, + showscale: false, zmin: 0, zmax: timeDataMaxIndex, showlegend: true, hovertemplate: "ROI: %{z}", }, ], - [imageData, dialogFilterNodeId, colorscale, timeDataMaxIndex], + [imageData, colorscale, timeDataMaxIndex], ) const layout = useMemo( diff --git a/frontend/src/components/Workspace/Visualize/VisualizeContext.tsx b/frontend/src/components/Workspace/Visualize/VisualizeContext.tsx index c49852ac1..3be853690 100644 --- a/frontend/src/components/Workspace/Visualize/VisualizeContext.tsx +++ b/frontend/src/components/Workspace/Visualize/VisualizeContext.tsx @@ -21,17 +21,21 @@ import { AppDispatch, RootState, store } from "store/store" type TVisualizeContext = { roisClick: { [itemId: string]: number[] } setRoisClick: (itemId: number, roi: number) => void + setRoisClickWithGetTime: (itemId: number, roi: number) => void resetRoisClick: (itemId: number) => void links: { [linkItemId: string]: string | number } setLinks: (itemId: number, linkItemId: number) => () => void + isVisualize?: boolean } const VisualizeContext = createContext({ roisClick: {}, setRoisClick: () => null, + setRoisClickWithGetTime: () => null, resetRoisClick: () => null, links: {}, setLinks: () => () => null, + isVisualize: false, }) export const useVisualize = () => useContext(VisualizeContext) @@ -76,6 +80,32 @@ export const VisualizeProvider = ({ children }: PropsWithChildren) => { }, [dispatch], ) + const setRoisClickWithGetTime = useCallback( + async (itemId: string | number, roi: number) => { + setRois((pre) => ({ + ...pre, + [itemId]: pre[itemId]?.some((e) => e === roi) + ? pre[itemId].filter((e) => e !== roi) + : [...(pre[itemId] || []), roi], + })) + const fluorescenceId = linksRef.current[itemId] + if (fluorescenceId !== undefined) { + dispatch( + setTimeSeriesItemDrawOrder({ + itemId: Number(fluorescenceId), + drawOrder: String(roi), + }), + ) + const path = selectVisualizeDataFilePath(fluorescenceId as number)( + store.getState() as RootState, + ) + await dispatch( + getTimeSeriesDataById({ path: path!, index: String(roi) }), + ).unwrap() + } + }, + [dispatch], + ) const resetRoisClick = useCallback( (itemId: string | number) => { @@ -132,7 +162,15 @@ export const VisualizeProvider = ({ children }: PropsWithChildren) => { return ( {children} diff --git a/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx b/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx index 9c9cf4aea..6a77ec1b6 100644 --- a/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx +++ b/frontend/src/components/Workspace/Visualize/VisualizeItem.tsx @@ -223,6 +223,7 @@ const FilePathSelectItem = memo(function FilePathSelectItem({ itemId, }: ItemIdProps) { const dispatch = useDispatch() + const { resetRoisClick } = useVisualize() const dataType = useSelector(selectVisualizeDataType(itemId)) const selectedNodeId = useSelector(selectVisualizeDataNodeId(itemId)) const selectedFilePath = useSelector(selectImageItemFilePath(itemId)) @@ -254,6 +255,7 @@ const FilePathSelectItem = memo(function FilePathSelectItem({ }, ), ) + resetRoisClick(itemId) } return ( From 4598adb25b0179aeea22966e4a39ccb1cdddca62 Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Fri, 21 Feb 2025 13:56:05 +0700 Subject: [PATCH 13/15] fix roiVisualSelected undefined --- frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx index fc3f45e79..2a4f295cf 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx @@ -123,7 +123,7 @@ const RoiPlotImple = memo(function RoiPlotImple() { const rgba = colorscaleRoi[new_i] if ( (!dialogFilterNodeId && !roiVisualSelected?.length) || - [...roisSelected, ...roiVisualSelected].includes(i) + [...roisSelected, ...(roiVisualSelected || [])].includes(i) ) { return [offset, rgba] } From 43fe89bd0246857454808e2b1080afcce9ef4ac9 Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Tue, 25 Feb 2025 16:43:07 +0700 Subject: [PATCH 14/15] Add link fluorescence to cell roi visualize --- .../Dialog/AlgorithmOutputDialog.tsx | 2 +- .../Workspace/FlowChart/Dialog/BoxFilter.tsx | 4 -- .../FlowChart/Dialog/FilterContext.tsx | 43 +++++++++++++--- .../Workspace/Visualize/Plot/RoiPlot.tsx | 6 +-- .../Visualize/Plot/TimeSeriesPlot.tsx | 51 +++---------------- .../VisualizeItem/VisualizeItemSelectors.ts | 7 ++- 6 files changed, 50 insertions(+), 63 deletions(-) diff --git a/frontend/src/components/Workspace/FlowChart/Dialog/AlgorithmOutputDialog.tsx b/frontend/src/components/Workspace/FlowChart/Dialog/AlgorithmOutputDialog.tsx index 7d4eb6a63..23a2c3829 100644 --- a/frontend/src/components/Workspace/FlowChart/Dialog/AlgorithmOutputDialog.tsx +++ b/frontend/src/components/Workspace/FlowChart/Dialog/AlgorithmOutputDialog.tsx @@ -59,7 +59,7 @@ export const AlgorithmOutputDialog = memo(function AlgorithmOutputDialog({ nodeId={nodeId} /> - + {open && } {open && dialogFilterNodeId ? : null} diff --git a/frontend/src/components/Workspace/FlowChart/Dialog/BoxFilter.tsx b/frontend/src/components/Workspace/FlowChart/Dialog/BoxFilter.tsx index f865e160c..8ca50e63f 100644 --- a/frontend/src/components/Workspace/FlowChart/Dialog/BoxFilter.tsx +++ b/frontend/src/components/Workspace/FlowChart/Dialog/BoxFilter.tsx @@ -186,10 +186,6 @@ const BoxFilter = ({ nodeId }: { nodeId: string }) => { return { dim1: dim1?.filter(Boolean), roi: roi?.filter(Boolean) } }, [filterParam]) - useEffect(() => { - setFilterParam(filterSelector) - }, [filterSelector, setFilterParam]) - const getData = useCallback( (value?: TDim[]) => value diff --git a/frontend/src/components/Workspace/FlowChart/Dialog/FilterContext.tsx b/frontend/src/components/Workspace/FlowChart/Dialog/FilterContext.tsx index 765aac523..d1619451d 100644 --- a/frontend/src/components/Workspace/FlowChart/Dialog/FilterContext.tsx +++ b/frontend/src/components/Workspace/FlowChart/Dialog/FilterContext.tsx @@ -4,32 +4,61 @@ import { PropsWithChildren, SetStateAction, useContext, + useEffect, + useMemo, useState, } from "react" +import { useSelector, useDispatch, shallowEqual } from "react-redux" +import { DialogContext } from "components/Workspace/FlowChart/Dialog/DialogContext" +import { selectAlgorithmDataFilterParam } from "store/slice/AlgorithmNode/AlgorithmNodeSelectors" import { TDataFilterParam } from "store/slice/AlgorithmNode/AlgorithmNodeType" +import { getRoiData } from "store/slice/DisplayData/DisplayDataActions" +import { selectOutputFilePathCellRoi } from "store/slice/Pipeline/PipelineSelectors" +import { selectCurrentWorkspaceId } from "store/slice/Workspace/WorkspaceSelector" +import { AppDispatch } from "store/store" const BoxFilterContext = createContext<{ filterParam?: TDataFilterParam - setFilterParam: Dispatch> - setRoiPath: Dispatch> roiPath: string + setFilterParam: Dispatch> }>({ filterParam: undefined, setFilterParam: () => null, roiPath: "", - setRoiPath: () => null, }) export const useBoxFilter = () => useContext(BoxFilterContext) -export const BoxFilterProvider = ({ children }: PropsWithChildren) => { - const [filterParam, setFilterParam] = useState() - const [roiPath, setRoiPath] = useState("") +export const BoxFilterProvider = ({ + children, + nodeId, +}: PropsWithChildren<{ nodeId: string }>) => { + const filterSelector = useSelector( + selectAlgorithmDataFilterParam(nodeId), + shallowEqual, + ) + const [filterParam, setFilterParam] = useState( + filterSelector, + ) + const { isOutput } = useContext(DialogContext) + const dispatch = useDispatch() + const workspaceId = useSelector(selectCurrentWorkspaceId) + const isExistFilterRoi = useMemo( + () => filterParam?.roi?.length, + [filterParam?.roi?.length], + ) + const path = useSelector(selectOutputFilePathCellRoi(nodeId)) + + useEffect(() => { + if (isOutput && path && isExistFilterRoi && workspaceId) { + dispatch(getRoiData({ workspaceId, path })) + } + }, [dispatch, path, isExistFilterRoi, isOutput, workspaceId]) return ( {children} diff --git a/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx index 2a4f295cf..639f88e2c 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/RoiPlot.tsx @@ -69,15 +69,11 @@ const RoiPlotImple = memo(function RoiPlotImple() { const timeDataMaxIndex = useSelector(selectRoiItemIndex(itemId, path)) const { setRoiSelected, roisSelected, setMaxRoi } = useRoisSelected() - const { filterParam, setRoiPath } = useBoxFilter() + const { filterParam } = useBoxFilter() const { setRoisClickWithGetTime, roisClick, isVisualize } = useVisualize() const roiVisualSelected = roisClick[itemId] - useEffect(() => { - setRoiPath(path) - }, [path, setRoiPath]) - useEffect(() => { setMaxRoi?.(Math.max(...imageDataSelector.flat()) + 1) }, [imageDataSelector, setMaxRoi]) diff --git a/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx index 96bb5eff5..37f177b3d 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/TimeSeriesPlot.tsx @@ -15,7 +15,7 @@ import { LegendClickEvent } from "plotly.js" import { LinearProgress, Typography } from "@mui/material" -import { getRoiDataApi, TimeSeriesData } from "api/outputs/Outputs" +import { TimeSeriesData } from "api/outputs/Outputs" import { DialogContext, useRoisSelected, @@ -40,10 +40,7 @@ import { selectTimesSeriesMeta, } from "store/slice/DisplayData/DisplayDataSelectors" import { selectFrameRate } from "store/slice/Experiments/ExperimentsSelectors" -import { - selectOutputFilePathCellRoi, - selectPipelineLatestUid, -} from "store/slice/Pipeline/PipelineSelectors" +import { selectPipelineLatestUid } from "store/slice/Pipeline/PipelineSelectors" import { selectTimeSeriesItemDrawOrderList, selectTimeSeriesItemOffset, @@ -60,7 +57,6 @@ import { selectVisualizeSaveFormat, selectImageItemRangeUnit, } from "store/slice/VisualizeItem/VisualizeItemSelectors" -import { selectCurrentWorkspaceId } from "store/slice/Workspace/WorkspaceSelector" import { AppDispatch } from "store/store" export const TimeSeriesPlot = memo(function TimeSeriesPlot() { @@ -103,7 +99,6 @@ const TimeSeriesPlotImple = memo(function TimeSeriesPlotImple() { selectAlgorithmDataFilterParam(nodeId), shallowEqual, ) - const workspaceId = useSelector(selectCurrentWorkspaceId) const meta = useSelector(selectTimesSeriesMeta(path)) const dataXrange = useSelector(selectTimeSeriesXrange(path)) const dataStd = useSelector(selectTimeSeriesStd(path)) @@ -127,50 +122,18 @@ const TimeSeriesPlotImple = memo(function TimeSeriesPlotImple() { const { dialogFilterNodeId, isOutput } = useContext(DialogContext) const { setRoiSelected, setMaxDim } = useRoisSelected() const { filterParam = filterSelector, roiPath } = useBoxFilter() - const roiUniqueListSelector = useSelector( - selectRoiUniqueList(roiPath), - shallowEqual, - ) - const [roiUniqueList, setRoiUniqueList] = useState( - roiUniqueListSelector, - ) + const roiUniqueList = useSelector(selectRoiUniqueList(roiPath)) const { setRoisClick, links } = useVisualize() - const itemIdRoi = useMemo( - () => Object.keys(links).find((key) => links[key] === itemId), - [itemId, links], - ) - const pathCellRoi = useSelector(selectOutputFilePathCellRoi(nodeId)) const isExistFilterRoi = useMemo( () => filterParam?.roi?.length, [filterParam?.roi?.length], ) - const getRoiUniqueList = useCallback(() => { - if (workspaceId && pathCellRoi) { - getRoiDataApi(pathCellRoi, { workspaceId }).then(({ data }) => { - const roi1Ddata: number[] = data[0] - .map((row) => - Array.from(new Set(row.filter((value) => value != null))), - ) - .flat() - const roiUniqueIds = Array.from(new Set(roi1Ddata)) - .sort((n1, n2) => n1 - n2) - .map(String) - setRoiUniqueList(roiUniqueIds) - }) - } - }, [pathCellRoi, workspaceId]) - - useEffect(() => { - if (isOutput && isExistFilterRoi) getRoiUniqueList() - }, [getRoiUniqueList, isExistFilterRoi, isOutput]) - - useEffect(() => { - if (!isOutput || dialogFilterNodeId) { - setRoiUniqueList(roiUniqueListSelector) - } - }, [dialogFilterNodeId, isOutput, roiUniqueListSelector]) + const itemIdRoi = useMemo( + () => Object.keys(links).find((key) => links[key] === itemId), + [itemId, links], + ) useEffect(() => { if (!timeSeriesData) return diff --git a/frontend/src/store/slice/VisualizeItem/VisualizeItemSelectors.ts b/frontend/src/store/slice/VisualizeItem/VisualizeItemSelectors.ts index 7e1569bc3..7df8fc4f7 100644 --- a/frontend/src/store/slice/VisualizeItem/VisualizeItemSelectors.ts +++ b/frontend/src/store/slice/VisualizeItem/VisualizeItemSelectors.ts @@ -402,8 +402,11 @@ export const selectTimeSeriesItemRefRoiUniqueList = if (isTimeSeriesItem(item)) { if (item.refImageItemId != null) { const imageItem = selectVisualizeItems(state)[item.refImageItemId] - if (isImageItem(imageItem) && imageItem.roiItem?.filePath != null) { - return selectRoiUniqueList(imageItem.roiItem.filePath)(state) + if (isImageItem(imageItem) && imageItem.roiItem?.filePath) { + return selectRoiUniqueList(imageItem.roiItem?.filePath)(state) + } + if (isRoiItem(imageItem) && imageItem.filePath) { + return selectRoiUniqueList(imageItem.filePath)(state) } } return null From 36987b24b0868eff3819be300b4bc422b900b848 Mon Sep 17 00:00:00 2001 From: Sang Le vinh Date: Thu, 6 Mar 2025 13:39:38 +0700 Subject: [PATCH 15/15] Fix drag select roi --- .../Workspace/Visualize/Plot/ImagePlot.tsx | 22 ++++++++++++++++++- .../VisualizeItem/VisualizeItemActions.ts | 19 ++++------------ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx index c5a0c5b6f..015c61878 100644 --- a/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx +++ b/frontend/src/components/Workspace/Visualize/Plot/ImagePlot.tsx @@ -387,10 +387,30 @@ const ImagePlotChart = memo(function ImagePlotChart({ const handleChange = (event: ChangeEvent) => { setSelectMode(event.target.checked) } + + const setRoiClickWhenSelect = (rois: string[]) => { + resetRoisClick(itemId) + rois.forEach((roi) => { + setRoisClick(itemId, Number(roi)) + dispatch( + setImageItemClickedDataId({ + itemId, + clickedDataId: roi, + }), + ) + }) + } + // debounceでイベントを間引きする。onSelectedはそれっぽい名前だが動かなかった。 const onSelecting = debounce((event: PlotSelectionEvent) => { if (event.range != null) { - dispatch(selectingImageArea({ itemId, range: event.range })) + dispatch( + selectingImageArea({ + itemId, + range: event.range, + callback: setRoiClickWhenSelect, + }), + ) } }) diff --git a/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts b/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts index d9144b4ca..834f6a7ce 100644 --- a/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts +++ b/frontend/src/store/slice/VisualizeItem/VisualizeItemActions.ts @@ -50,11 +50,12 @@ export const selectingImageArea = createAsyncThunk< x: number[] y: number[] } + callback?: (rois: string[]) => void }, ThunkApiConfig >( `${VISUALIZE_ITEM_SLICE_NAME}/selectingImageArea`, - ({ itemId, range }, thunkAPI) => { + ({ itemId, range, callback }, thunkAPI) => { const { x, y } = range const [x1, x2] = x.map(Math.round) const [y1, y2] = y.map(Math.round) @@ -77,20 +78,8 @@ export const selectingImageArea = createAsyncThunk< } } Object.values(items).forEach((item) => { - if ( - isTimeSeriesItem(item) && - item.filePath != null && - item.refImageItemId === itemId - ) { - const path = item.filePath - selectedZList.forEach((selectedZ) => { - thunkAPI.dispatch( - getTimeSeriesDataById({ - path, - index: String(selectedZ), - }), - ) - }) + if (isTimeSeriesItem(item) && item.filePath != null) { + callback?.(selectedZList) } }) }