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 5ad3e949..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 } 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),
@@ -358,40 +387,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/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
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 070cd7e8..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,20 +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, points }))
- : [{ color: theme.primary, points: data }];
+ ).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: 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 (
<>
@@ -77,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 &&
@@ -123,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({