From b3a25b34b48730f713e6e76149a29f03caf442af Mon Sep 17 00:00:00 2001 From: James Holcombe Date: Thu, 18 Sep 2025 11:56:06 +0100 Subject: [PATCH 1/3] temp --- .../src/components/data/plot/plots/common.tsx | 77 ++++++++++++++----- .../components/data/plot/plots/line-plot.tsx | 14 +++- 2 files changed, 71 insertions(+), 20 deletions(-) diff --git a/packages/app/src/components/data/plot/plots/common.tsx b/packages/app/src/components/data/plot/plots/common.tsx index c6ad2994..424ea6eb 100644 --- a/packages/app/src/components/data/plot/plots/common.tsx +++ b/packages/app/src/components/data/plot/plots/common.tsx @@ -3,7 +3,7 @@ import { type PlotDefinition, } from "@common/db/schema/plot"; -import { scaleLinear, scaleOrdinal } from "@visx/scale"; +import { scaleLinear, scaleLog, scaleOrdinal } from "@visx/scale"; import { extent } from "@visx/vendor/d3-array"; import { usePlotTheme } from "@/hooks/use-plot-theme"; import { @@ -362,40 +362,81 @@ export function getScaleConfig( const xValues = data.map((d) => Number(d.x)).filter((x) => !isNaN(x)); const yValues = data.map((d) => Number(d.y)).filter((y) => !isNaN(y)); - const xDomain = definition.xAxis.domainTickOptions + // Helper function to ensure log scale domains are valid (positive values only) + const ensureLogDomain = (domain: [number, number]): [number, number] => { + const [min, max] = domain; + // For log scales, we need positive values. If min <= 0, use a small positive value + const logMin = min <= 0 ? Math.max(0.001, max * 0.001) : min; + const logMax = max <= 0 ? 1 : max; + return [logMin, logMax]; + }; + + let xDomain: [number, number] = definition.xAxis.domainTickOptions ? [ definition.xAxis.domainTickOptions.min, definition.xAxis.domainTickOptions.max, ] : (extent(xValues) as [number, number]) || [0, 1]; - const yDomain = definition.yAxis.domainTickOptions + let yDomain: [number, number] = definition.yAxis.domainTickOptions ? [ definition.yAxis.domainTickOptions.min, definition.yAxis.domainTickOptions.max, ] : (extent(yValues) as [number, number]) || [0, 1]; + // Ensure domains are valid for log scales + if (definition.xAxis.scale === "log") { + xDomain = ensureLogDomain(xDomain); + } + if (definition.yAxis.scale === "log") { + yDomain = ensureLogDomain(yDomain); + } + // Only add padding if no custom domain is set const padding = 0.05; // 5% padding const xRange = xDomain[1] - xDomain[0]; const yRange = yDomain[1] - yDomain[0]; - const xScale = scaleLinear({ - domain: definition.xAxis.domainTickOptions - ? xDomain - : [xDomain[0] - xRange * padding, xDomain[1] + xRange * padding], - range: [0, width - MARGIN.left - MARGIN.right], - reverse: definition.xAxis.reversed, - }); - - const yScale = scaleLinear({ - domain: definition.yAxis.domainTickOptions - ? yDomain - : [yDomain[0] - yRange * padding, yDomain[1] + yRange * padding], - range: [height - MARGIN.top - MARGIN.bottom, 0], - reverse: definition.yAxis.reversed, - }); + // Create X scale based on scale type + const isXLog = definition.xAxis.scale === "log"; + const xScaleDomain = definition.xAxis.domainTickOptions + ? xDomain + : isXLog + ? xDomain // Don't add padding for log scales as it can cause issues with negative/zero values + : [xDomain[0] - xRange * padding, xDomain[1] + xRange * padding]; + + const xScale = isXLog + ? scaleLog({ + domain: xScaleDomain, + range: [0, width - MARGIN.left - MARGIN.right], + reverse: definition.xAxis.reversed, + }) + : scaleLinear({ + domain: xScaleDomain, + range: [0, width - MARGIN.left - MARGIN.right], + reverse: definition.xAxis.reversed, + }); + + // Create Y scale based on scale type + const isYLog = definition.yAxis.scale === "log"; + const yScaleDomain = definition.yAxis.domainTickOptions + ? yDomain + : isYLog + ? yDomain // Don't add padding for log scales as it can cause issues with negative/zero values + : [yDomain[0] - yRange * padding, yDomain[1] + yRange * padding]; + + const yScale = isYLog + ? scaleLog({ + domain: yScaleDomain, + range: [height - MARGIN.top - MARGIN.bottom, 0], + reverse: definition.yAxis.reversed, + }) + : scaleLinear({ + domain: yScaleDomain, + range: [height - MARGIN.top - MARGIN.bottom, 0], + reverse: definition.yAxis.reversed, + }); return { xScale, diff --git a/packages/app/src/components/data/plot/plots/line-plot.tsx b/packages/app/src/components/data/plot/plots/line-plot.tsx index 8b881265..57c52edf 100644 --- a/packages/app/src/components/data/plot/plots/line-plot.tsx +++ b/packages/app/src/components/data/plot/plots/line-plot.tsx @@ -65,8 +65,18 @@ function BaseLinePlot({ groups.set(color, group); return groups; }, new Map()), - ).map(([color, points]) => ({ color, points })) - : [{ color: theme.primary, points: data }]; + ).map(([color, points]) => ({ + color, + // Sort points by x-value to ensure proper line rendering + points: points.sort((a, b) => Number(a.x) - Number(b.x)), + })) + : [ + { + color: theme.primary, + // Sort points by x-value to ensure proper line rendering + points: [...data].sort((a, b) => Number(a.x) - Number(b.x)), + }, + ]; return ( <> From 7784f0b6c2126d8c2a9eeb969b22065cfb21ec64 Mon Sep 17 00:00:00 2001 From: James Holcombe Date: Thu, 18 Sep 2025 14:06:48 +0100 Subject: [PATCH 2/3] fixed line plots for logs --- package-lock.json | 15 ++ .../data/plot/form/grouping-form.tsx | 103 +++++++++-- .../src/components/data/plot/plots/common.tsx | 133 ++++++++------ .../data/plot/plots/histogram-plot.tsx | 2 +- .../components/data/plot/plots/line-plot.tsx | 163 +++++++++++++----- .../data/plot/plots/plot-wrapper.tsx | 48 +++--- .../data/plot/plots/tooltip/plot-tooltip.tsx | 2 +- packages/common/src/db/schema/plot.ts | 3 +- 8 files changed, 330 insertions(+), 139 deletions(-) diff --git a/package-lock.json b/package-lock.json index e811df8a..6aa5e85c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28474,6 +28474,21 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" + }, + "packages/docs/node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.30", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.30.tgz", + "integrity": "sha512-pVZMnFok5qEX4RT59mK2hEVtJX+XFfak+/rjHpyFh7juiT52r177bfFKhnlafm0UOSldhXjj32b+LZIOdswGTg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/packages/app/src/components/data/plot/form/grouping-form.tsx b/packages/app/src/components/data/plot/form/grouping-form.tsx index 9fee4cc4..bf3049be 100644 --- a/packages/app/src/components/data/plot/form/grouping-form.tsx +++ b/packages/app/src/components/data/plot/form/grouping-form.tsx @@ -4,6 +4,8 @@ import { useFormContext } from "react-hook-form"; import { Input } from "@/components/ui/input"; import { LabelWithBadge } from "./label-badge"; import AutoComplete from "@/components/ui/auto-complete"; +import { Badge } from "@/components/ui/badge"; +import { X } from "lucide-react"; import { type PlotDefinition } from "@common/db/schema/plot"; import { isDefined } from "@/utils/helpers"; import { @@ -42,15 +44,17 @@ export function GroupingForm({ label: column.label, })); - const handleColumnChange = async (value: string | null) => { - const newValue = isDefined(value) - ? { - ...currentValue, - column: value, - showInLegend: currentValue?.showInLegend ?? true, - schemeType: name === "color" ? "categorical" : undefined, - } - : null; + const handleMultiColumnChange = async (columns: string[]) => { + const newValue = + columns.length > 0 + ? { + ...currentValue, + columns: columns, + showInLegend: currentValue?.showInLegend ?? true, + schemeType: name === "color" ? "categorical" : undefined, + separator: currentValue?.separator ?? " | ", + } + : null; setValue(`grouping.${name}`, newValue, { shouldDirty: true, shouldTouch: true, @@ -59,6 +63,16 @@ export function GroupingForm({ handleSubmit(onSubmit)(); }; + const handleSeparatorChange = async (separator: string) => { + if (currentValue?.columns?.length) { + setValue(`grouping.${name}.separator`, separator, { + shouldDirty: true, + shouldTouch: true, + }); + handleSubmit(onSubmit)(); + } + }; + const handleShowInLegendChange = async (value: boolean) => { setValue(`grouping.${name}.showInLegend`, value, { shouldDirty: true, @@ -84,19 +98,59 @@ export function GroupingForm({
- Column + Columns + + {/* Selected columns display */} + {currentValue?.columns && currentValue.columns.length > 0 && ( +
+ {currentValue.columns.map((col, index) => { + const column = availableColumns.find((c) => c.name === col); + return ( + + {column?.label || col} + { + const newColumns = currentValue.columns!.filter( + (_, i) => i !== index, + ); + handleMultiColumnChange(newColumns); + }} + /> + + ); + })} +
+ )} + + {/* Add new column */} !currentValue?.columns?.includes(opt.value), + )} + selectedOption={null} + setSelectedOption={(value) => { + if (value) { + const currentColumns = currentValue?.columns || []; + handleMultiColumnChange([...currentColumns, value]); + } + }} label="" - showClearButton={true} + showClearButton={false} popoverClassName="w-[300px]" triggerClassName="max-w-[200px]" - placeholder="Select column" + placeholder={ + currentValue?.columns?.length + ? "Add another column" + : "Select first column" + } />
@@ -111,6 +165,23 @@ export function GroupingForm({ />
)} + {isDefined(currentValue) && + currentValue?.columns && + currentValue.columns.length > 1 && ( +
+ + Separator + + handleSeparatorChange(e.target.value)} + /> +
+ )} + {isDefined(currentValue) && (
diff --git a/packages/app/src/components/data/plot/plots/common.tsx b/packages/app/src/components/data/plot/plots/common.tsx index e6241a9a..b549f9ad 100644 --- a/packages/app/src/components/data/plot/plots/common.tsx +++ b/packages/app/src/components/data/plot/plots/common.tsx @@ -3,7 +3,7 @@ import { type PlotDefinition, } from "@common/db/schema/plot"; -import { scaleLinear, scaleLog, scaleOrdinal } from "@visx/scale"; +import { scaleLinear, scaleLog } from "@visx/scale"; import { extent } from "@visx/vendor/d3-array"; import { GlyphCircle, @@ -75,9 +75,12 @@ export interface DataPoint extends Record { export interface DataTransformationOptions { xColumn: string; yColumn?: string; - colorColumn?: string; - symbolColumn?: string; - lineColumn?: string; + colorColumns?: string[]; // Changed to array to support multiple columns + symbolColumns?: string[]; // Changed to array to support multiple columns + lineColumns?: string[]; // Changed to array to support multiple columns + colorSeparator?: string; // Separator for combining multiple color values + symbolSeparator?: string; // Separator for combining multiple symbol values + lineSeparator?: string; // Separator for combining multiple line values xAxisType?: "number" | "category"; } @@ -88,9 +91,12 @@ export function transformData( const { xColumn, yColumn, - colorColumn, - symbolColumn, - lineColumn, + colorColumns, + symbolColumns, + lineColumns, + colorSeparator = " | ", + symbolSeparator = " | ", + lineSeparator = " | ", xAxisType = "number", } = options; @@ -116,52 +122,74 @@ export function transformData( if (xAxisType === "number" && isNaN(xValue as number)) return null; if (yColumn && isNaN(y as number)) return null; - // Handle abbreviation objects for color - let colorValue: string | undefined; - let colorObject: - | { code: string; description?: string; color?: string } - | undefined; - if (colorColumn && row[colorColumn]) { - if (typeof row[colorColumn] === "object" && row[colorColumn] !== null) { - colorObject = row[colorColumn] as { + // Helper function to extract value from column (handles abbreviation objects) + const extractColumnValue = (columnName: string): string | undefined => { + const value = row[columnName]; + if (!value) return undefined; + + if (typeof value === "object" && value !== null) { + const objectValue = value as { code: string; description?: string; color?: string; }; - if (colorObject && colorObject.code) { - colorValue = colorObject.code; + return objectValue.code ? objectValue.code : undefined; + } + return String(value); + }; + + // Handle multiple color columns + let colorValue: string | undefined; + let colorObject: + | { code: string; description?: string; color?: string } + | undefined; + if (colorColumns && colorColumns.length > 0) { + const colorValues = colorColumns + .map((col) => extractColumnValue(col)) + .filter((val) => val !== undefined); + + if (colorValues.length > 0) { + colorValue = colorValues.join(colorSeparator); + + // For color objects, use the first column that has color information + for (const col of colorColumns) { + const value = row[col]; + if (typeof value === "object" && value !== null) { + const objValue = value as { + code: string; + description?: string; + color?: string; + }; + if (objValue.color) { + colorObject = objValue; + break; + } + } } - } else { - colorValue = String(row[colorColumn]); } } - // Handle abbreviation objects for symbol + // Handle multiple symbol columns let symbolValue: string | undefined; - if (symbolColumn && row[symbolColumn]) { - if ( - typeof row[symbolColumn] === "object" && - row[symbolColumn] !== null - ) { - const symbolObject = row[symbolColumn] as { code: string }; - if (symbolObject && symbolObject.code) { - symbolValue = symbolObject.code; - } - } else { - symbolValue = String(row[symbolColumn]); + if (symbolColumns && symbolColumns.length > 0) { + const symbolValues = symbolColumns + .map((col) => extractColumnValue(col)) + .filter((val) => val !== undefined); + + if (symbolValues.length > 0) { + symbolValue = symbolValues.join(symbolSeparator); } } - // Handle abbreviation objects for line + // Handle multiple line columns let lineValue: string | undefined; - if (lineColumn && row[lineColumn]) { - if (typeof row[lineColumn] === "object" && row[lineColumn] !== null) { - const lineObject = row[lineColumn] as { code: string }; - if (lineObject && lineObject.code) { - lineValue = lineObject.code; - } - } else { - lineValue = String(row[lineColumn]); + if (lineColumns && lineColumns.length > 0) { + const lineValues = lineColumns + .map((col) => extractColumnValue(col)) + .filter((val) => val !== undefined); + + if (lineValues.length > 0) { + lineValue = lineValues.join(lineSeparator); } } @@ -238,11 +266,12 @@ export function useFilteredData(points: T[]) { export function assignAbbreviationColorsAndSymbols( points: T[], - colorColumn?: string, - symbolColumn?: string, - lineColumn?: string, + colorColumns?: string[], + symbolColumns?: string[], + lineColumns?: string[], ): T[] { - if (!colorColumn && !symbolColumn && !lineColumn) return points; + if (!colorColumns?.length && !symbolColumns?.length && !lineColumns?.length) + return points; // Create color mappings that prioritize abbreviation colors const colorMap = new Map(); @@ -251,12 +280,12 @@ export function assignAbbreviationColorsAndSymbols( let defaultColorIndex = 0; // Get unique colors in sorted order for consistent assignment - const uniqueColors = colorColumn + const uniqueColors = colorColumns?.length ? Array.from(new Set(points.map((p) => p.color).filter(Boolean))).sort() : []; uniqueColors.forEach((color) => { - if (colorColumn && color && !processedColors.has(color)) { + if (colorColumns?.length && color && !processedColors.has(color)) { processedColors.add(color); // Find the point with this color to check for abbreviation color @@ -277,10 +306,10 @@ export function assignAbbreviationColorsAndSymbols( }); // Get unique values for symbol and line columns (sorted for consistent ordering) - const uniqueSymbols = symbolColumn + const uniqueSymbols = symbolColumns?.length ? Array.from(new Set(points.map((p) => p.symbol).filter(Boolean))).sort() : []; - const uniqueLines = lineColumn + const uniqueLines = lineColumns?.length ? Array.from(new Set(points.map((p) => p.line).filter(Boolean))).sort() : []; @@ -289,7 +318,7 @@ export function assignAbbreviationColorsAndSymbols( let defaultSymbolIndex = 0; uniqueSymbols.forEach((symbol) => { - if (symbolColumn && symbol) { + if (symbolColumns?.length && symbol) { symbolMap.set( symbol, AVAILABLE_SYMBOLS[defaultSymbolIndex % AVAILABLE_SYMBOLS.length], @@ -303,17 +332,17 @@ export function assignAbbreviationColorsAndSymbols( // Assign colors and symbols to points while preserving original values return points.map((point) => ({ ...point, - ...(colorColumn && + ...(colorColumns?.length && point.color && { originalColor: point.color, color: colorMap.get(point.color) || OBSERVABLE_COLORS[0], }), - ...(symbolColumn && + ...(symbolColumns?.length && point.symbol && { originalSymbol: point.symbol, symbol: symbolMap.get(point.symbol), }), - ...(lineColumn && + ...(lineColumns?.length && point.line && { originalLine: point.line, line: lineMap.get(point.line), diff --git a/packages/app/src/components/data/plot/plots/histogram-plot.tsx b/packages/app/src/components/data/plot/plots/histogram-plot.tsx index d3b23646..905e464f 100644 --- a/packages/app/src/components/data/plot/plots/histogram-plot.tsx +++ b/packages/app/src/components/data/plot/plots/histogram-plot.tsx @@ -106,7 +106,7 @@ function BaseHistogramPlot({ // Group data by color if grouping is enabled const colorGroups = new Map(); - if (grouping?.color) { + if (grouping?.color?.columns?.length) { // First pass: collect all unique colors const uniqueColors = Array.from( new Set(data.map((d) => String(d.originalColor || ""))), diff --git a/packages/app/src/components/data/plot/plots/line-plot.tsx b/packages/app/src/components/data/plot/plots/line-plot.tsx index f4471d92..b6eb440d 100644 --- a/packages/app/src/components/data/plot/plots/line-plot.tsx +++ b/packages/app/src/components/data/plot/plots/line-plot.tsx @@ -1,5 +1,5 @@ import { LinePath } from "@visx/shape"; -import { Plot, PlotDefinition } from "@common/db/schema/plot"; +import { Plot, PlotDefinition, GroupingValues } from "@common/db/schema/plot"; import { DataPoint } from "./common"; import { usePlotTheme } from "@/hooks/use-plot-theme"; import { getScaleConfig } from "./common"; @@ -20,12 +20,34 @@ interface LineGroup { points: DataPoint[]; } -export function getLineTooltipData(point: DataPoint): TooltipData { - return { +export function getLineTooltipData( + point: DataPoint, + grouping?: GroupingValues | null, +): TooltipData { + const tooltipData: TooltipData = { x: point.x, y: point.y, - color: point.originalColor as string | undefined, }; + + // Add color grouping data if available + if (grouping?.color?.columns && point.originalColor) { + const label = grouping.color.label || grouping.color.columns.join(", "); + tooltipData[label] = point.originalColor; + } + + // Add line grouping data if available + if (grouping?.line?.columns && point.originalLine) { + const label = grouping.line.label || grouping.line.columns.join(", "); + tooltipData[label] = point.originalLine; + } + + // Add symbol grouping data if available + if (grouping?.symbol?.columns && point.originalSymbol) { + const label = grouping.symbol.label || grouping.symbol.columns.join(", "); + tooltipData[label] = point.originalSymbol; + } + + return tooltipData; } function BaseLinePlot({ @@ -54,30 +76,62 @@ function BaseLinePlot({ const scaleConfig = getScaleConfig(plot.definition, data, width, height); const { xAxis, yAxis } = scaleConfig; - // Group data by color if color grouping is enabled - const dataByColor: LineGroup[] = grouping?.color?.column + // Group data by line grouping first (if available), then by color grouping + const hasLineGrouping = grouping?.line?.columns?.length; + const hasColorGrouping = grouping?.color?.columns?.length; + + const dataByLine: LineGroup[] = hasLineGrouping ? Array.from( data.reduce((groups, point) => { - const color = point.originalColor - ? String(point.originalColor) - : theme.primary; - const group = groups.get(color) || []; + // Use line grouping for separating lines + const lineKey = point.originalLine + ? String(point.originalLine) + : "default"; + + const group = groups.get(lineKey) || []; group.push(point); - groups.set(color, group); + groups.set(lineKey, group); return groups; }, new Map()), - ).map(([color, points]) => ({ - color, - // Sort points by x-value to ensure proper line rendering - points: points.sort((a, b) => Number(a.x) - Number(b.x)), - })) - : [ - { - color: theme.primary, + ).map(([lineKey, points]) => { + // Determine color for this line group + const firstPoint = points[0]; + const lineColor = hasColorGrouping + ? firstPoint?.originalColor + ? colorMapping?.get(String(firstPoint.originalColor)) || + theme.primary + : theme.primary + : colorMapping?.get(lineKey) || theme.primary; + + return { + color: lineColor, // Sort points by x-value to ensure proper line rendering - points: [...data].sort((a, b) => Number(a.x) - Number(b.x)), - }, - ]; + points: points.sort((a, b) => Number(a.x) - Number(b.x)), + }; + }) + : hasColorGrouping + ? Array.from( + data.reduce((groups, point) => { + const color = point.originalColor + ? String(point.originalColor) + : theme.primary; + const group = groups.get(color) || []; + group.push(point); + groups.set(color, group); + return groups; + }, new Map()), + ).map(([color, points]) => ({ + color: colorMapping?.get(color) || theme.primary, + // Sort points by x-value to ensure proper line rendering + points: points.sort((a, b) => Number(a.x) - Number(b.x)), + })) + : [ + { + color: theme.primary, + // Sort points by x-value to ensure proper line rendering + points: [...data].sort((a, b) => Number(a.x) - Number(b.x)), + }, + ]; return ( <> @@ -87,31 +141,43 @@ function BaseLinePlot({ xAxis={xAxis} yAxis={yAxis} > - {dataByColor.map(({ color, points }) => ( - xAxis.scale(Number(d.x)) ?? 0} - y={(d) => yAxis.scale(Number(d.y)) ?? 0} - stroke={colorMapping?.get(color) || theme.primary} - strokeWidth={2} - curve={ - plot.definition.curve === "natural" ? curveNatural : undefined - } - onMouseMove={(event) => { - const coords = localPoint(event.target as SVGPathElement, event); - if (coords) { - const tooltipData = getLineTooltipData({ - x: (xAxis.scale as any).invert(coords.x), - y: (yAxis.scale as any).invert(coords.y), - originalColor: color, - }); - showTooltip(createTooltipProps(tooltipData, plot, coords)); + {dataByLine.map(({ color, points }, index) => { + // Get the first point to extract the original identifiers + const firstPoint = points[0]; + return ( + xAxis.scale(Number(d.x)) ?? 0} + y={(d) => yAxis.scale(Number(d.y)) ?? 0} + stroke={color} + strokeWidth={2} + curve={ + plot.definition.curve === "natural" ? curveNatural : undefined } - }} - onMouseLeave={hideTooltip} - /> - ))} + onMouseMove={(event) => { + const coords = localPoint( + event.target as SVGPathElement, + event, + ); + if (coords) { + const tooltipData = getLineTooltipData( + { + x: (xAxis.scale as any).invert(coords.x), + y: (yAxis.scale as any).invert(coords.y), + originalColor: firstPoint?.originalColor, + originalLine: firstPoint?.originalLine, + originalSymbol: firstPoint?.originalSymbol, + }, + plot.definition.grouping, + ); + showTooltip(createTooltipProps(tooltipData, plot, coords)); + } + }} + onMouseLeave={hideTooltip} + /> + ); + })} {/* Add dots if showDots is true */} {plot.definition.showDots && @@ -133,7 +199,10 @@ function BaseLinePlot({ event, ); if (coords) { - const tooltipData = getLineTooltipData(point); + const tooltipData = getLineTooltipData( + point, + plot.definition.grouping, + ); showTooltip(createTooltipProps(tooltipData, plot, coords)); } }} diff --git a/packages/app/src/components/data/plot/plots/plot-wrapper.tsx b/packages/app/src/components/data/plot/plots/plot-wrapper.tsx index 16e547ea..6bf7fdbb 100644 --- a/packages/app/src/components/data/plot/plots/plot-wrapper.tsx +++ b/packages/app/src/components/data/plot/plots/plot-wrapper.tsx @@ -9,7 +9,6 @@ import { getUniqueValues, useFilteredData, DataPoint, - OBSERVABLE_COLORS, } from "./common"; import { PlotContainer } from "./common"; import { SVGLegend, getLegendHeight } from "./svg-legend"; @@ -55,8 +54,12 @@ export function PlotWrapper({ const xColumn = xAxis.column; const yColumn = yAxis?.column; - const colorColumn = grouping?.color?.column; - const symbolColumn = grouping?.symbol?.column; + const colorColumns = grouping?.color?.columns; + const symbolColumns = grouping?.symbol?.columns; + const lineColumns = grouping?.line?.columns; + const colorSeparator = grouping?.color?.separator || " | "; + const symbolSeparator = grouping?.symbol?.separator || " | "; + const lineSeparator = grouping?.line?.separator || " | "; if (!xColumn) { throw new Error("xAxis column is not defined"); @@ -81,16 +84,21 @@ export function PlotWrapper({ const points = transformData(rawData as Record[], { xColumn: xColumn, yColumn: yColumn, - colorColumn: colorColumn, - symbolColumn: symbolColumn, + colorColumns: colorColumns, + symbolColumns: symbolColumns, + lineColumns: lineColumns, + colorSeparator: colorSeparator, + symbolSeparator: symbolSeparator, + lineSeparator: lineSeparator, xAxisType, }); // Assign colors and symbols to all points first (for stable mappings) const processedPoints = assignAbbreviationColorsAndSymbols( points as DataPoint[], - colorColumn, - symbolColumn, + colorColumns, + symbolColumns, + lineColumns, ); // Apply filtering to the processed points @@ -103,29 +111,25 @@ export function PlotWrapper({ } = useFilteredData(processedPoints); // Get unique values for legends (from all data, not filtered) - const uniqueColors = colorColumn + const uniqueColors = colorColumns?.length ? getUniqueValues(processedPoints, "color") : []; - const uniqueSymbols = symbolColumn + const uniqueSymbols = symbolColumns?.length ? getUniqueValues(processedPoints, "symbol") : []; // Get color and symbol mappings for legend (based on all data, not filtered) - const colorMapping = colorColumn + const colorMapping = colorColumns?.length ? new Map( processedPoints - .filter((p) => p.originalColor) + .filter((p) => p.originalColor && p.color) .map( - (p) => - [ - p.originalColor!, - p.colorObject?.color || p.color || OBSERVABLE_COLORS[0], - ] as [string, string], + (p) => [p.originalColor!, p.color as string] as [string, string], ), ) : undefined; - const symbolMapping = symbolColumn + const symbolMapping = symbolColumns?.length ? new Map( processedPoints .filter((p) => p.originalSymbol && p.symbol) @@ -138,8 +142,8 @@ export function PlotWrapper({ uniqueColors, uniqueSymbols, width === "100%" ? 800 : (width as number), - Boolean(grouping?.color?.label || grouping?.color?.column), - Boolean(grouping?.symbol?.label || grouping?.symbol?.column), + Boolean(grouping?.color?.label || grouping?.color?.columns?.length), + Boolean(grouping?.symbol?.label || grouping?.symbol?.columns?.length), ); const theme = usePlotTheme(); @@ -289,11 +293,13 @@ export function PlotWrapper({ ; const groupingValueSchema = z.object({ - column: z.string(), + columns: z.array(z.string()).min(1), // Changed from single column to array of columns label: z.string().optional().nullable(), values: z.record(z.string()).optional().nullable(), showInLegend: z.boolean().optional().nullable().default(true), + separator: z.string().optional().nullable().default(" | "), // Separator for combining multiple column values }); const colorGroupingValueSchema = groupingValueSchema.extend({ From 22b4f582e1263eda6444210f0871e602b5ab5795 Mon Sep 17 00:00:00 2001 From: James Holcombe Date: Thu, 18 Sep 2025 14:18:00 +0100 Subject: [PATCH 3/3] lint --- packages/app/src/components/data/plot/plots/dimensions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/data/plot/plots/dimensions.ts b/packages/app/src/components/data/plot/plots/dimensions.ts index ebe525a4..77e70840 100644 --- a/packages/app/src/components/data/plot/plots/dimensions.ts +++ b/packages/app/src/components/data/plot/plots/dimensions.ts @@ -47,10 +47,10 @@ export function calculatePlotDimensions( const legendWidth = Math.max(plotWidth * 2, availableWidth); const legendHeight = plot.grouping ? getLegendHeight( - plot.grouping.color?.column + plot.grouping.color?.columns ? getUniqueValues(data as DataPoint[], "color") : [], - plot.grouping.symbol?.column + plot.grouping.symbol?.columns ? getUniqueValues(data as DataPoint[], "symbol") : [], legendWidth, // Use wider width for legend