From 72f9817e4ca80290767acda5c1bc0f55d15f55e4 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Tue, 15 Sep 2026 17:32:49 -0700 Subject: [PATCH 1/4] fix: Forced annotations to look up annotated cell indices to prevent jumping when selections are toggled --- src/containers/MainPlotContainer/selectors.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/containers/MainPlotContainer/selectors.ts b/src/containers/MainPlotContainer/selectors.ts index 00098c04..a863d749 100644 --- a/src/containers/MainPlotContainer/selectors.ts +++ b/src/containers/MainPlotContainer/selectors.ts @@ -306,14 +306,9 @@ const getAnnotationData = createSelector( const thumbnailPath = data[THUMBNAIL_PATH] || ""; const cellIds = filteredCellData.labels[ARRAY_OF_CELL_IDS_KEY]; - // FileInfo is typed with `index` as optional because it gets added to the - // data from the database. However, at this point, index will always be defined, but since - // typescript doesn't know that, we still have this backup to find it in the - // id array but that code should never be executed. - const pointIndex = - data.index !== undefined ? data.index : findIndex(cellIds, (id) => id === cellID); - const x = filteredCellData.values[xAxis][pointIndex]; - const y = filteredCellData.values[yAxis][pointIndex]; + const pointIndex = cellIds.indexOf(cellID); + const x = filteredCellData.values[xAxis][pointIndex] ?? null; + const y = filteredCellData.values[yAxis][pointIndex] ?? null; if (pointIndex >= 0 && x !== null && y !== null) { acc.push({ cellID, From 32b2b89c1852ab026106bacdad5889690319b38f Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Wed, 16 Sep 2026 10:41:15 -0700 Subject: [PATCH 2/4] refactor: Optimize to prevent `indexOf` lookup for every annotation --- src/containers/MainPlotContainer/selectors.ts | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/containers/MainPlotContainer/selectors.ts b/src/containers/MainPlotContainer/selectors.ts index a863d749..d5456b55 100644 --- a/src/containers/MainPlotContainer/selectors.ts +++ b/src/containers/MainPlotContainer/selectors.ts @@ -1,4 +1,4 @@ -import { includes, map, find, findIndex, isEmpty } from "lodash"; +import { includes, map, find, findIndex, isEmpty, filter } from "lodash"; import { PlotData } from "plotly.js"; import { createSelector } from "reselect"; @@ -13,7 +13,11 @@ import { SELECTIONS_PLOT_NAME, THUMBNAIL_PATH, } from "../../constants"; -import { getCategoricalFeatureKeys, getMeasuredFeaturesDefs } from "../../state/metadata/selectors"; +import { + getCategoricalFeatureKeys, + getMeasuredFeaturesDefs, + getPerCellDataForPlot, +} from "../../state/metadata/selectors"; import { DataForPlot, FileInfo, @@ -46,6 +50,7 @@ import { getLineMovingAverageWindow, getConnectByFeature, getConnectByCategory, + getFiltersToExclude, } from "../../state/selection/selectors"; import { ColorForPlot, @@ -288,15 +293,25 @@ export function calculateLinePlotData( } const getAnnotationData = createSelector( - [getFilteredCellData, getClickedCellsFileInfo, getPlotByOnX, getPlotByOnY, getHoveredCardId], + [ + getPerCellDataForPlot, + getClickedCellsFileInfo, + getFiltersToExclude, + getPlotByOnX, + getPlotByOnY, + getGroupingCategoryNamesAsArray, + getHoveredCardId, + ], ( - filteredCellData: DataForPlot, + allCellData: DataForPlot, clickedCellsFileInfo: FileInfo[], + filtersToExclude: string[], xAxis, yAxis, + groupByCategoryNames, currentHoveredCellId ): AnnotationData[] => { - if (isEmpty(filteredCellData.values) || isEmpty(filteredCellData.labels)) { + if (isEmpty(allCellData.values) || isEmpty(allCellData.labels)) { return []; } const initAcc: AnnotationData[] = []; @@ -305,10 +320,23 @@ const getAnnotationData = createSelector( const fovID = data[FOV_ID_KEY] || ""; const thumbnailPath = data[THUMBNAIL_PATH] || ""; - const cellIds = filteredCellData.labels[ARRAY_OF_CELL_IDS_KEY]; - const pointIndex = cellIds.indexOf(cellID); - const x = filteredCellData.values[xAxis][pointIndex] ?? null; - const y = filteredCellData.values[yAxis][pointIndex] ?? null; + const cellIds = allCellData.labels[ARRAY_OF_CELL_IDS_KEY]; + // `FileInfo.index` is typed as optional because it is added from + // the database, but it will always be defined here. Note that + // `index` is the position in the full array of cell IDs, and does + // not account for filtering, so filtering needs to be done + // manually. + const pointIndex = + data.index !== undefined ? data.index : findIndex(cellIds, (id) => id === cellID); + const x = allCellData.values[xAxis][pointIndex] ?? null; + const y = allCellData.values[yAxis][pointIndex] ?? null; + + // Apply filtering + const groupName = groupByCategoryNames[pointIndex] ?? null; + if (filtersToExclude.includes(groupName)) { + return acc; + } + if (pointIndex >= 0 && x !== null && y !== null) { acc.push({ cellID, From ae3673320bb6c4ba58c96aabd085009f74107075 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Wed, 16 Sep 2026 10:43:28 -0700 Subject: [PATCH 3/4] refactor: Remove unused import --- src/containers/MainPlotContainer/selectors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/MainPlotContainer/selectors.ts b/src/containers/MainPlotContainer/selectors.ts index d5456b55..97304a4c 100644 --- a/src/containers/MainPlotContainer/selectors.ts +++ b/src/containers/MainPlotContainer/selectors.ts @@ -1,4 +1,4 @@ -import { includes, map, find, findIndex, isEmpty, filter } from "lodash"; +import { includes, map, find, findIndex, isEmpty } from "lodash"; import { PlotData } from "plotly.js"; import { createSelector } from "reselect"; From 9173d126b63fb56854fc7f76eb74bd4d9a53cb1d Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Wed, 16 Sep 2026 11:09:04 -0700 Subject: [PATCH 4/4] feat: Add regression test for getAnnotations selector --- .../MainPlotContainer/test/selectors.test.ts | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/containers/MainPlotContainer/test/selectors.test.ts b/src/containers/MainPlotContainer/test/selectors.test.ts index 6f9d4aed..7dcf2299 100644 --- a/src/containers/MainPlotContainer/test/selectors.test.ts +++ b/src/containers/MainPlotContainer/test/selectors.test.ts @@ -51,8 +51,7 @@ describe("MainPlotContainer selectors", () => { const result: PlotlyAnnotation[] = getAnnotations(state); expect(result).to.have.lengthOf(2); }); - }); - describe("getAnnotations selector", () => { + it("it filters out non visible points", () => { const state: State = { ...newMockState, @@ -66,8 +65,26 @@ describe("MainPlotContainer selectors", () => { const result: PlotlyAnnotation[] = getAnnotations(state); expect(result).to.have.lengthOf(0); }); - }); - describe("getAnnotations selector", () => { + + it("it excludes filtered groups from annotations", () => { + const state: State = { + ...newMockState, + selection: { + ...newMockState.selection, + plotByOnX: "apical-proximity", + plotByOnY: "apical-proximity", + // 2 cells are selected, but the first (cell ID 1) will be + // excluded by filtering + selectedPoints: selectedCellFileInfo, + filterExclude: ["Paxillin"], + }, + }; + const result: PlotlyAnnotation[] = getAnnotations(state); + expect(result).to.have.lengthOf(1); + expect(result[0].pointIndex).to.equal(1); + expect(result[0].cellID).to.equal("2"); + }); + it("it still shows points whose values are zero", () => { const state: State = { ...newMockState, @@ -82,6 +99,7 @@ describe("MainPlotContainer selectors", () => { expect(result).to.have.lengthOf(2); }); }); + describe("getXDisplayName", () => { it("returns the displayName for a continuous feature", () => { const state: State = {