Skip to content

Commit 7784f0b

Browse files
committed
fixed line plots for logs
1 parent adfcf3e commit 7784f0b

File tree

8 files changed

+330
-139
lines changed

8 files changed

+330
-139
lines changed

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app/src/components/data/plot/form/grouping-form.tsx

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { useFormContext } from "react-hook-form";
44
import { Input } from "@/components/ui/input";
55
import { LabelWithBadge } from "./label-badge";
66
import AutoComplete from "@/components/ui/auto-complete";
7+
import { Badge } from "@/components/ui/badge";
8+
import { X } from "lucide-react";
79
import { type PlotDefinition } from "@common/db/schema/plot";
810
import { isDefined } from "@/utils/helpers";
911
import {
@@ -42,15 +44,17 @@ export function GroupingForm({
4244
label: column.label,
4345
}));
4446

45-
const handleColumnChange = async (value: string | null) => {
46-
const newValue = isDefined(value)
47-
? {
48-
...currentValue,
49-
column: value,
50-
showInLegend: currentValue?.showInLegend ?? true,
51-
schemeType: name === "color" ? "categorical" : undefined,
52-
}
53-
: null;
47+
const handleMultiColumnChange = async (columns: string[]) => {
48+
const newValue =
49+
columns.length > 0
50+
? {
51+
...currentValue,
52+
columns: columns,
53+
showInLegend: currentValue?.showInLegend ?? true,
54+
schemeType: name === "color" ? "categorical" : undefined,
55+
separator: currentValue?.separator ?? " | ",
56+
}
57+
: null;
5458
setValue(`grouping.${name}`, newValue, {
5559
shouldDirty: true,
5660
shouldTouch: true,
@@ -59,6 +63,16 @@ export function GroupingForm({
5963
handleSubmit(onSubmit)();
6064
};
6165

66+
const handleSeparatorChange = async (separator: string) => {
67+
if (currentValue?.columns?.length) {
68+
setValue(`grouping.${name}.separator`, separator, {
69+
shouldDirty: true,
70+
shouldTouch: true,
71+
});
72+
handleSubmit(onSubmit)();
73+
}
74+
};
75+
6276
const handleShowInLegendChange = async (value: boolean) => {
6377
setValue(`grouping.${name}.showInLegend`, value, {
6478
shouldDirty: true,
@@ -84,19 +98,59 @@ export function GroupingForm({
8498
<div className="grid grid-cols-2 gap-6">
8599
<div className="col-span-2 space-y-2">
86100
<LabelWithBadge
87-
description={`Group data points by ${name} based on a column's values`}
101+
description={`Group data points by ${name} based on one or more column values`}
88102
>
89-
Column
103+
Columns
90104
</LabelWithBadge>
105+
106+
{/* Selected columns display */}
107+
{currentValue?.columns && currentValue.columns.length > 0 && (
108+
<div className="mb-2 flex flex-wrap gap-2">
109+
{currentValue.columns.map((col, index) => {
110+
const column = availableColumns.find((c) => c.name === col);
111+
return (
112+
<Badge
113+
key={index}
114+
variant="secondary"
115+
className="flex items-center gap-1"
116+
>
117+
{column?.label || col}
118+
<X
119+
className="h-3 w-3 cursor-pointer hover:text-destructive"
120+
onClick={() => {
121+
const newColumns = currentValue.columns!.filter(
122+
(_, i) => i !== index,
123+
);
124+
handleMultiColumnChange(newColumns);
125+
}}
126+
/>
127+
</Badge>
128+
);
129+
})}
130+
</div>
131+
)}
132+
133+
{/* Add new column */}
91134
<AutoComplete
92-
options={columnOptions}
93-
selectedOption={currentValue?.column ?? null}
94-
setSelectedOption={handleColumnChange}
135+
options={columnOptions.filter(
136+
(opt) => !currentValue?.columns?.includes(opt.value),
137+
)}
138+
selectedOption={null}
139+
setSelectedOption={(value) => {
140+
if (value) {
141+
const currentColumns = currentValue?.columns || [];
142+
handleMultiColumnChange([...currentColumns, value]);
143+
}
144+
}}
95145
label=""
96-
showClearButton={true}
146+
showClearButton={false}
97147
popoverClassName="w-[300px]"
98148
triggerClassName="max-w-[200px]"
99-
placeholder="Select column"
149+
placeholder={
150+
currentValue?.columns?.length
151+
? "Add another column"
152+
: "Select first column"
153+
}
100154
/>
101155
</div>
102156

@@ -111,6 +165,23 @@ export function GroupingForm({
111165
/>
112166
</div>
113167
)}
168+
{isDefined(currentValue) &&
169+
currentValue?.columns &&
170+
currentValue.columns.length > 1 && (
171+
<div className="space-y-2">
172+
<LabelWithBadge
173+
description={`Separator used to combine multiple ${name} values`}
174+
>
175+
Separator
176+
</LabelWithBadge>
177+
<Input
178+
placeholder=" | "
179+
value={currentValue?.separator ?? " | "}
180+
onChange={(e) => handleSeparatorChange(e.target.value)}
181+
/>
182+
</div>
183+
)}
184+
114185
{isDefined(currentValue) && (
115186
<div className="space-y-2">
116187
<LabelWithBadge description={`Show the ${name} in the legend`}>

packages/app/src/components/data/plot/plots/common.tsx

Lines changed: 81 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
type PlotDefinition,
44
} from "@common/db/schema/plot";
55

6-
import { scaleLinear, scaleLog, scaleOrdinal } from "@visx/scale";
6+
import { scaleLinear, scaleLog } from "@visx/scale";
77
import { extent } from "@visx/vendor/d3-array";
88
import {
99
GlyphCircle,
@@ -75,9 +75,12 @@ export interface DataPoint extends Record<string, unknown> {
7575
export interface DataTransformationOptions {
7676
xColumn: string;
7777
yColumn?: string;
78-
colorColumn?: string;
79-
symbolColumn?: string;
80-
lineColumn?: string;
78+
colorColumns?: string[]; // Changed to array to support multiple columns
79+
symbolColumns?: string[]; // Changed to array to support multiple columns
80+
lineColumns?: string[]; // Changed to array to support multiple columns
81+
colorSeparator?: string; // Separator for combining multiple color values
82+
symbolSeparator?: string; // Separator for combining multiple symbol values
83+
lineSeparator?: string; // Separator for combining multiple line values
8184
xAxisType?: "number" | "category";
8285
}
8386

@@ -88,9 +91,12 @@ export function transformData(
8891
const {
8992
xColumn,
9093
yColumn,
91-
colorColumn,
92-
symbolColumn,
93-
lineColumn,
94+
colorColumns,
95+
symbolColumns,
96+
lineColumns,
97+
colorSeparator = " | ",
98+
symbolSeparator = " | ",
99+
lineSeparator = " | ",
94100
xAxisType = "number",
95101
} = options;
96102

@@ -116,52 +122,74 @@ export function transformData(
116122
if (xAxisType === "number" && isNaN(xValue as number)) return null;
117123
if (yColumn && isNaN(y as number)) return null;
118124

119-
// Handle abbreviation objects for color
120-
let colorValue: string | undefined;
121-
let colorObject:
122-
| { code: string; description?: string; color?: string }
123-
| undefined;
124-
if (colorColumn && row[colorColumn]) {
125-
if (typeof row[colorColumn] === "object" && row[colorColumn] !== null) {
126-
colorObject = row[colorColumn] as {
125+
// Helper function to extract value from column (handles abbreviation objects)
126+
const extractColumnValue = (columnName: string): string | undefined => {
127+
const value = row[columnName];
128+
if (!value) return undefined;
129+
130+
if (typeof value === "object" && value !== null) {
131+
const objectValue = value as {
127132
code: string;
128133
description?: string;
129134
color?: string;
130135
};
131-
if (colorObject && colorObject.code) {
132-
colorValue = colorObject.code;
136+
return objectValue.code ? objectValue.code : undefined;
137+
}
138+
return String(value);
139+
};
140+
141+
// Handle multiple color columns
142+
let colorValue: string | undefined;
143+
let colorObject:
144+
| { code: string; description?: string; color?: string }
145+
| undefined;
146+
if (colorColumns && colorColumns.length > 0) {
147+
const colorValues = colorColumns
148+
.map((col) => extractColumnValue(col))
149+
.filter((val) => val !== undefined);
150+
151+
if (colorValues.length > 0) {
152+
colorValue = colorValues.join(colorSeparator);
153+
154+
// For color objects, use the first column that has color information
155+
for (const col of colorColumns) {
156+
const value = row[col];
157+
if (typeof value === "object" && value !== null) {
158+
const objValue = value as {
159+
code: string;
160+
description?: string;
161+
color?: string;
162+
};
163+
if (objValue.color) {
164+
colorObject = objValue;
165+
break;
166+
}
167+
}
133168
}
134-
} else {
135-
colorValue = String(row[colorColumn]);
136169
}
137170
}
138171

139-
// Handle abbreviation objects for symbol
172+
// Handle multiple symbol columns
140173
let symbolValue: string | undefined;
141-
if (symbolColumn && row[symbolColumn]) {
142-
if (
143-
typeof row[symbolColumn] === "object" &&
144-
row[symbolColumn] !== null
145-
) {
146-
const symbolObject = row[symbolColumn] as { code: string };
147-
if (symbolObject && symbolObject.code) {
148-
symbolValue = symbolObject.code;
149-
}
150-
} else {
151-
symbolValue = String(row[symbolColumn]);
174+
if (symbolColumns && symbolColumns.length > 0) {
175+
const symbolValues = symbolColumns
176+
.map((col) => extractColumnValue(col))
177+
.filter((val) => val !== undefined);
178+
179+
if (symbolValues.length > 0) {
180+
symbolValue = symbolValues.join(symbolSeparator);
152181
}
153182
}
154183

155-
// Handle abbreviation objects for line
184+
// Handle multiple line columns
156185
let lineValue: string | undefined;
157-
if (lineColumn && row[lineColumn]) {
158-
if (typeof row[lineColumn] === "object" && row[lineColumn] !== null) {
159-
const lineObject = row[lineColumn] as { code: string };
160-
if (lineObject && lineObject.code) {
161-
lineValue = lineObject.code;
162-
}
163-
} else {
164-
lineValue = String(row[lineColumn]);
186+
if (lineColumns && lineColumns.length > 0) {
187+
const lineValues = lineColumns
188+
.map((col) => extractColumnValue(col))
189+
.filter((val) => val !== undefined);
190+
191+
if (lineValues.length > 0) {
192+
lineValue = lineValues.join(lineSeparator);
165193
}
166194
}
167195

@@ -238,11 +266,12 @@ export function useFilteredData<T extends DataPoint>(points: T[]) {
238266

239267
export function assignAbbreviationColorsAndSymbols<T extends DataPoint>(
240268
points: T[],
241-
colorColumn?: string,
242-
symbolColumn?: string,
243-
lineColumn?: string,
269+
colorColumns?: string[],
270+
symbolColumns?: string[],
271+
lineColumns?: string[],
244272
): T[] {
245-
if (!colorColumn && !symbolColumn && !lineColumn) return points;
273+
if (!colorColumns?.length && !symbolColumns?.length && !lineColumns?.length)
274+
return points;
246275

247276
// Create color mappings that prioritize abbreviation colors
248277
const colorMap = new Map<string, string>();
@@ -251,12 +280,12 @@ export function assignAbbreviationColorsAndSymbols<T extends DataPoint>(
251280
let defaultColorIndex = 0;
252281

253282
// Get unique colors in sorted order for consistent assignment
254-
const uniqueColors = colorColumn
283+
const uniqueColors = colorColumns?.length
255284
? Array.from(new Set(points.map((p) => p.color).filter(Boolean))).sort()
256285
: [];
257286

258287
uniqueColors.forEach((color) => {
259-
if (colorColumn && color && !processedColors.has(color)) {
288+
if (colorColumns?.length && color && !processedColors.has(color)) {
260289
processedColors.add(color);
261290

262291
// Find the point with this color to check for abbreviation color
@@ -277,10 +306,10 @@ export function assignAbbreviationColorsAndSymbols<T extends DataPoint>(
277306
});
278307

279308
// Get unique values for symbol and line columns (sorted for consistent ordering)
280-
const uniqueSymbols = symbolColumn
309+
const uniqueSymbols = symbolColumns?.length
281310
? Array.from(new Set(points.map((p) => p.symbol).filter(Boolean))).sort()
282311
: [];
283-
const uniqueLines = lineColumn
312+
const uniqueLines = lineColumns?.length
284313
? Array.from(new Set(points.map((p) => p.line).filter(Boolean))).sort()
285314
: [];
286315

@@ -289,7 +318,7 @@ export function assignAbbreviationColorsAndSymbols<T extends DataPoint>(
289318
let defaultSymbolIndex = 0;
290319

291320
uniqueSymbols.forEach((symbol) => {
292-
if (symbolColumn && symbol) {
321+
if (symbolColumns?.length && symbol) {
293322
symbolMap.set(
294323
symbol,
295324
AVAILABLE_SYMBOLS[defaultSymbolIndex % AVAILABLE_SYMBOLS.length],
@@ -303,17 +332,17 @@ export function assignAbbreviationColorsAndSymbols<T extends DataPoint>(
303332
// Assign colors and symbols to points while preserving original values
304333
return points.map((point) => ({
305334
...point,
306-
...(colorColumn &&
335+
...(colorColumns?.length &&
307336
point.color && {
308337
originalColor: point.color,
309338
color: colorMap.get(point.color) || OBSERVABLE_COLORS[0],
310339
}),
311-
...(symbolColumn &&
340+
...(symbolColumns?.length &&
312341
point.symbol && {
313342
originalSymbol: point.symbol,
314343
symbol: symbolMap.get(point.symbol),
315344
}),
316-
...(lineColumn &&
345+
...(lineColumns?.length &&
317346
point.line && {
318347
originalLine: point.line,
319348
line: lineMap.get(point.line),

packages/app/src/components/data/plot/plots/histogram-plot.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ function BaseHistogramPlot({
106106

107107
// Group data by color if grouping is enabled
108108
const colorGroups = new Map<string, DataPoint[]>();
109-
if (grouping?.color) {
109+
if (grouping?.color?.columns?.length) {
110110
// First pass: collect all unique colors
111111
const uniqueColors = Array.from(
112112
new Set(data.map((d) => String(d.originalColor || ""))),

0 commit comments

Comments
 (0)