diff --git a/src/containers/MainPlotContainer/selectors.ts b/src/containers/MainPlotContainer/selectors.ts index 7809aef0..01cd189d 100644 --- a/src/containers/MainPlotContainer/selectors.ts +++ b/src/containers/MainPlotContainer/selectors.ts @@ -45,8 +45,14 @@ import { getFilteredConnectByCategoryValues, getLineMovingAverageWindow, getConnectByFeature, + getConnectByCategory, } from "../../state/selection/selectors"; -import { MainPlotSettings, SelectedPointData, TickConversion } from "../../state/selection/types"; +import { + ColorForPlot, + MainPlotSettings, + SelectedPointData, + TickConversion, +} from "../../state/selection/types"; import { AnnotationData, ContinuousPlotData, @@ -191,8 +197,23 @@ export const getLinePlotData = createSelector( getFilteredConnectByFeatureValues, getShowConnectLines, getLineMovingAverageWindow, + // Line group coloring + getCategoryGroupColorsAndNames, + getColorBySelection, + getConnectByCategory, ], - calculateLinePlotData + (xValues, yValues, catValues, featValues, showLines, window, colors, colorBy, connectBy) => + calculateLinePlotData( + xValues, + yValues, + catValues, + featValues, + showLines, + window, + colors, + colorBy, + connectBy + ) ); export function calculateLinePlotData( @@ -201,7 +222,10 @@ export function calculateLinePlotData( connectByCategoryValues: (number | null)[], connectByFeatureValues: (number | null)[], showConnectingLines: boolean, - movingAverageWindow: number + movingAverageWindow: number, + colorsForPlot: ColorForPlot[], + colorByFeature: string | number, + connectByCategory: string ): LinePlotData[] | null { if (!showConnectingLines) { return null; @@ -225,7 +249,7 @@ export function calculateLinePlotData( } const lineData: LinePlotData[] = []; - for (const indices of indicesByGroup.values()) { + for (const [group, indices] of indicesByGroup.entries()) { const x: (number | null)[] = []; const y: (number | null)[] = []; // Sort each category's data by the feature values. @@ -238,7 +262,7 @@ export function calculateLinePlotData( x.push(xValues[i]); y.push(yValues[i]); } - lineData.push({ x: x, y: y }); + lineData.push({ x: x, y: y, groupIndex: group }); } // Apply moving average to each line @@ -249,6 +273,17 @@ export function calculateLinePlotData( } } + if (colorByFeature === connectByCategory) { + // Apply group colors to the lines + const groupToColors: { [key: number]: string } = {}; + for (const colorSetting of colorsForPlot) { + groupToColors[colorSetting.key] = colorSetting.color; + } + for (const line of lineData) { + line.color = groupToColors[line.groupIndex]; + } + } + return lineData; } @@ -403,6 +438,12 @@ function makeScatterPlotData( // TODO: Add the ability to adjust the line settings via an additional selector function makeLinePlotTrace(data: LinePlotData, settings: MainPlotSettings): Partial { + let color = settings.connectionLineDefaultColor; + if (data.color) { + // Color is a hex string; apply transparency + const opacityHex = Math.round(settings.unselectedCircleOpacity * 255); + color = data.color + opacityHex.toString(16).padStart(2, "0"); + } return { type: "scattergl", mode: "lines", @@ -412,7 +453,7 @@ function makeLinePlotTrace(data: LinePlotData, settings: MainPlotSettings): Part showlegend: false, line: { width: settings.connectionLineWidth, - color: settings.connectionLineDefaultColor, + color: color, }, }; } diff --git a/src/containers/MainPlotContainer/test/selectors.test.ts b/src/containers/MainPlotContainer/test/selectors.test.ts index ec45ab18..6f9d4aed 100644 --- a/src/containers/MainPlotContainer/test/selectors.test.ts +++ b/src/containers/MainPlotContainer/test/selectors.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import { mockState, selectedCellFileInfo } from "../../../state/test/mocks"; -import type { State, AnnotationData } from "../../../state/types"; +import type { State, AnnotationData, LinePlotData } from "../../../state/types"; import { calculateLinePlotData, getAnnotations, @@ -14,6 +14,7 @@ import { } from "../selectors"; import type { PlotlyAnnotation } from "../../../components/MainPlot"; import { CELL_ID_KEY, PALETTE } from "../../../constants"; +import type { ColorForPlot } from "../../../state/selection/types"; describe("MainPlotContainer selectors", () => { const newMockState = mockState; @@ -315,13 +316,66 @@ describe("MainPlotContainer selectors", () => { }); describe("calculateLinePlotData", () => { + const defaultParams = { + xValues: [], + yValues: [], + connectByCategoryValues: [], + connectByFeatureValues: [], + showConnectingLines: true, + movingAverageWindow: 1, + colorsForPlot: [], + colorByFeature: "color_feature", + connectByCategory: "category_feature", + }; + + function calculate( + params: Partial<{ + xValues: (number | null)[]; + yValues: (number | null)[]; + connectByCategoryValues: (number | null)[]; + connectByFeatureValues: (number | null)[]; + showConnectingLines: boolean; + movingAverageWindow: number; + colorsForPlot: ColorForPlot[]; + colorByFeature: string; + connectByCategory: string; + }> + ): LinePlotData[] | null { + const paramsWithDefaults = { ...defaultParams, ...params }; + return calculateLinePlotData( + paramsWithDefaults.xValues, + paramsWithDefaults.yValues, + paramsWithDefaults.connectByCategoryValues, + paramsWithDefaults.connectByFeatureValues, + paramsWithDefaults.showConnectingLines, + paramsWithDefaults.movingAverageWindow, + paramsWithDefaults.colorsForPlot, + paramsWithDefaults.colorByFeature, + paramsWithDefaults.connectByCategory + ); + } + it("handles empty data", () => { - const result = calculateLinePlotData([], [], [], [], true, 1); + const result = calculate({ + xValues: [], + yValues: [], + connectByCategoryValues: [], + connectByFeatureValues: [], + showConnectingLines: true, + movingAverageWindow: 1, + }); expect(result).to.deep.equal([]); }); it("returns null when connecting lines are disabled", () => { - const result = calculateLinePlotData([1, 2], [3, 4], [1, 1], [5, 6], false, 1); + const result = calculate({ + xValues: [1, 2], + yValues: [3, 4], + connectByCategoryValues: [1, 1], + connectByFeatureValues: [5, 6], + showConnectingLines: false, + movingAverageWindow: 1, + }); expect(result).to.equal(null); }); @@ -330,18 +384,18 @@ describe("MainPlotContainer selectors", () => { const yValues = [6, 7, 8, 9, 10]; const connectByCategoryValues = [1, 1, 2, 2, 3]; const connectByFeatureValues = [1, 2, 3, 4, 5]; - const result = calculateLinePlotData( + const result = calculate({ xValues, yValues, connectByCategoryValues, connectByFeatureValues, - true, - 1 - ); + showConnectingLines: true, + movingAverageWindow: 1, + }); expect(result).to.deep.equal([ - { x: [1, 2], y: [6, 7] }, - { x: [3, 4], y: [8, 9] }, - { x: [5], y: [10] }, + { x: [1, 2], y: [6, 7], groupIndex: 1 }, + { x: [3, 4], y: [8, 9], groupIndex: 2 }, + { x: [5], y: [10], groupIndex: 3 }, ]); }); @@ -350,15 +404,17 @@ describe("MainPlotContainer selectors", () => { const yValues = [1, 2, 3, 4, 5]; const connectByCategoryValues = [1, 1, 1, 1, 1]; const connectByFeatureValues = [5, 4, 3, 2, 1]; - const result = calculateLinePlotData( + const result = calculate({ xValues, yValues, connectByCategoryValues, connectByFeatureValues, - true, - 1 - ); - expect(result).to.deep.equal([{ x: [5, 4, 3, 2, 1], y: [5, 4, 3, 2, 1] }]); + showConnectingLines: true, + movingAverageWindow: 1, + }); + expect(result).to.deep.equal([ + { x: [5, 4, 3, 2, 1], y: [5, 4, 3, 2, 1], groupIndex: 1 }, + ]); }); it("applies moving average to x and y values", () => { @@ -366,15 +422,52 @@ describe("MainPlotContainer selectors", () => { const yValues = [10, 20, 30, 40, 50]; const connectByCategoryValues = [1, 1, 1, 1, 1]; const connectByFeatureValues = [1, 2, 3, 4, 5]; - const result = calculateLinePlotData( + const result = calculate({ xValues, yValues, connectByCategoryValues, connectByFeatureValues, - true, - 3 - ); - expect(result).to.deep.equal([{ x: [15, 20, 30, 40, 45], y: [15, 20, 30, 40, 45] }]); + showConnectingLines: true, + movingAverageWindow: 3, + }); + expect(result).to.deep.equal([ + { x: [15, 20, 30, 40, 45], y: [15, 20, 30, 40, 45], groupIndex: 1 }, + ]); + }); + + it("applies colors when colorby feature matches category", () => { + const xValues = [1, 2, 3, 4, 5]; + const yValues = [1, 2, 3, 4, 5]; + const connectByCategoryValues = [1, 2, 3, 4, 5]; + const connectByFeatureValues = [1, 2, 3, 4, 5]; + const connectByCategory = "category_feature"; + const colorByFeature = "category_feature"; + const colorsForPlot: ColorForPlot[] = [ + { color: "red", name: "Group 1", label: "Group 1", key: 1 }, + { color: "green", name: "Group 2", label: "Group 2", key: 2 }, + { color: "#FF00FF", name: "Group 5", label: "Group 5", key: 5 }, + { color: "#FFFF00", name: "Group 4", label: "Group 4", key: 4 }, + { color: "#0000FF", name: "Group 3", label: "Group 3", key: 3 }, + ]; + + const result = calculate({ + xValues, + yValues, + connectByCategoryValues, + connectByFeatureValues, + showConnectingLines: true, + movingAverageWindow: 1, + connectByCategory, + colorByFeature, + colorsForPlot, + }); + expect(result).to.deep.equal([ + { x: [1], y: [1], groupIndex: 1, color: "red" }, + { x: [2], y: [2], groupIndex: 2, color: "green" }, + { x: [3], y: [3], groupIndex: 3, color: "#0000FF" }, + { x: [4], y: [4], groupIndex: 4, color: "#FFFF00" }, + { x: [5], y: [5], groupIndex: 5, color: "#FF00FF" }, + ]); }); }); }); diff --git a/src/state/selection/selectors.ts b/src/state/selection/selectors.ts index a7bae2e1..35024625 100755 --- a/src/state/selection/selectors.ts +++ b/src/state/selection/selectors.ts @@ -188,6 +188,7 @@ export const getCategoryGroupColorsAndNames = createSelector( color: option.color, name: id, label: option.name, + key: Number.parseFloat(key), }; }); @@ -211,6 +212,7 @@ export const getCategoryGroupColorsAndNames = createSelector( color: MISSING_CATEGORY_COLOR, label: MISSING_CATEGORY_LABEL, name: categoryToGroupBy === categoryToColorBy ? "" : "null", + key: NaN, }; return [...colorForPlot, missingColorOption]; } diff --git a/src/state/selection/types.ts b/src/state/selection/types.ts index 07e2477f..eb8b4dde 100755 --- a/src/state/selection/types.ts +++ b/src/state/selection/types.ts @@ -159,6 +159,11 @@ export interface ColorForPlot { color: string; name: string; label: string; + /** + * Original numeric value that corresponds with this group in the feature + * data. + */ + key: number; } export interface RequestFileInfoByCellIDAction { diff --git a/src/state/types.ts b/src/state/types.ts index de9016a9..158c32f6 100755 --- a/src/state/types.ts +++ b/src/state/types.ts @@ -109,7 +109,8 @@ export interface GroupedPlotData { export interface LinePlotData { x: (number | null)[]; y: (number | null)[]; - // TODO: Add additional per-line options here, like group color + groupIndex: number; + color?: string; } export interface SelectedGroup {