Skip to content

Commit fe93b6d

Browse files
committed
Customizable series colours. Automatic status colours
1 parent a16ed9a commit fe93b6d

File tree

6 files changed

+319
-49
lines changed

6 files changed

+319
-49
lines changed

apps/webapp/app/components/code/ChartConfigPanel.tsx

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { OutputColumnMetadata } from "@internal/clickhouse";
2-
import { BarChart, LineChart, Plus, XIcon } from "lucide-react";
3-
import { useCallback, useEffect, useMemo, useRef } from "react";
2+
import { BarChart, CheckIcon, LineChart, Plus, XIcon } from "lucide-react";
3+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
44
import { cn } from "~/utils/cn";
55
import { Paragraph } from "../primitives/Paragraph";
6+
import { Popover, PopoverContent, PopoverTrigger } from "../primitives/Popover";
67
import { Select, SelectItem } from "../primitives/Select";
78
import { Switch } from "../primitives/Switch";
89
import { Button } from "../primitives/Buttons";
@@ -11,6 +12,7 @@ import {
1112
type ChartConfiguration,
1213
type SortDirection,
1314
} from "../metrics/QueryWidget";
15+
import { CHART_COLORS_BY_HUE, getSeriesColor } from "./chartColors";
1416

1517
export const defaultChartConfig: ChartConfiguration = {
1618
chartType: "bar",
@@ -21,6 +23,7 @@ export const defaultChartConfig: ChartConfiguration = {
2123
sortByColumn: null,
2224
sortDirection: "asc",
2325
aggregation: "sum",
26+
seriesColors: {},
2427
};
2528

2629
interface ChartConfigPanelProps {
@@ -320,21 +323,40 @@ export function ChartConfigPanel({ columns, config, onChange, className }: Chart
320323
{/* Always show at least one dropdown, even if yAxisColumns is empty */}
321324
{(config.yAxisColumns.length === 0 ? [""] : config.yAxisColumns).map((col, index) => (
322325
<div key={index} className="flex items-center gap-1">
326+
{col && !config.groupByColumn && (
327+
<SeriesColorPicker
328+
color={config.seriesColors?.[col] ?? getSeriesColor(index)}
329+
onColorChange={(color) => {
330+
updateConfig({
331+
seriesColors: { ...config.seriesColors, [col]: color },
332+
});
333+
}}
334+
/>
335+
)}
323336
<Select
324337
value={col}
325338
setValue={(value) => {
326339
const newColumns = [...config.yAxisColumns];
340+
const updates: Partial<ChartConfiguration> = {};
327341
if (value) {
328342
// If this is a new slot (empty string), add it
329343
if (index >= config.yAxisColumns.length) {
330344
newColumns.push(value);
331345
} else {
346+
// If the column name changed, migrate the color
347+
const oldCol = newColumns[index];
348+
if (oldCol && oldCol !== value && config.seriesColors?.[oldCol]) {
349+
const newSeriesColors = { ...config.seriesColors };
350+
newSeriesColors[value] = newSeriesColors[oldCol];
351+
delete newSeriesColors[oldCol];
352+
updates.seriesColors = newSeriesColors;
353+
}
332354
newColumns[index] = value;
333355
}
334356
} else if (index < config.yAxisColumns.length) {
335357
newColumns.splice(index, 1);
336358
}
337-
updateConfig({ yAxisColumns: newColumns });
359+
updateConfig({ ...updates, yAxisColumns: newColumns });
338360
}}
339361
variant="tertiary/small"
340362
placeholder="Select column"
@@ -355,12 +377,21 @@ export function ChartConfigPanel({ columns, config, onChange, className }: Chart
355377
))
356378
}
357379
</Select>
380+
358381
{index > 0 && (
359382
<button
360383
type="button"
361384
onClick={() => {
385+
const removedCol = config.yAxisColumns[index];
362386
const newColumns = config.yAxisColumns.filter((_, i) => i !== index);
363-
updateConfig({ yAxisColumns: newColumns });
387+
const updates: Partial<ChartConfiguration> = { yAxisColumns: newColumns };
388+
// Clean up the color entry for the removed series
389+
if (removedCol && config.seriesColors?.[removedCol]) {
390+
const newSeriesColors = { ...config.seriesColors };
391+
delete newSeriesColors[removedCol];
392+
updates.seriesColors = newSeriesColors;
393+
}
394+
updateConfig(updates);
364395
}}
365396
className="rounded p-1 text-text-dimmed hover:bg-charcoal-700 hover:text-text-bright"
366397
title="Remove series"
@@ -554,6 +585,52 @@ function SortDirectionToggle({
554585
);
555586
}
556587

588+
function SeriesColorPicker({
589+
color,
590+
onColorChange,
591+
}: {
592+
color: string;
593+
onColorChange: (color: string) => void;
594+
}) {
595+
const [open, setOpen] = useState(false);
596+
597+
return (
598+
<Popover open={open} onOpenChange={setOpen}>
599+
<PopoverTrigger asChild>
600+
<button
601+
type="button"
602+
className="flex-shrink-0 rounded p-0.5 hover:bg-charcoal-700"
603+
title="Change series color"
604+
>
605+
<span
606+
className="block h-4 w-4 rounded-full border border-white/30"
607+
style={{ backgroundColor: color }}
608+
/>
609+
</button>
610+
</PopoverTrigger>
611+
<PopoverContent align="start" className="w-auto p-2">
612+
<div className="grid grid-cols-6 gap-1.5">
613+
{CHART_COLORS_BY_HUE.map((c) => (
614+
<button
615+
key={c}
616+
type="button"
617+
onClick={() => {
618+
onColorChange(c);
619+
setOpen(false);
620+
}}
621+
className="group/swatch flex h-6 w-6 items-center justify-center rounded-full border border-white/30"
622+
style={{ backgroundColor: c }}
623+
title={c}
624+
>
625+
{c === color && <CheckIcon className="h-3.5 w-3.5 text-white drop-shadow-md" />}
626+
</button>
627+
))}
628+
</div>
629+
</PopoverContent>
630+
</Popover>
631+
);
632+
}
633+
557634
function TypeBadge({ type }: { type: string }) {
558635
// Simplify type for display
559636
let displayType = type;

apps/webapp/app/components/code/QueryResultsChart.tsx

Lines changed: 14 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,8 @@ import type { ChartConfig } from "~/components/primitives/charts/Chart";
44
import { Chart } from "~/components/primitives/charts/ChartCompound";
55
import { Paragraph } from "../primitives/Paragraph";
66
import { AggregationType, ChartConfiguration } from "../metrics/QueryWidget";
7-
8-
// Color palette for chart series - 30 distinct colors for large datasets
9-
const CHART_COLORS = [
10-
// Primary colors
11-
"#7655fd", // Purple
12-
"#22c55e", // Green
13-
"#f59e0b", // Amber
14-
"#ef4444", // Red
15-
"#06b6d4", // Cyan
16-
"#ec4899", // Pink
17-
"#8b5cf6", // Violet
18-
"#14b8a6", // Teal
19-
"#f97316", // Orange
20-
"#6366f1", // Indigo
21-
// Extended palette
22-
"#84cc16", // Lime
23-
"#0ea5e9", // Sky
24-
"#f43f5e", // Rose
25-
"#a855f7", // Fuchsia
26-
"#eab308", // Yellow
27-
"#10b981", // Emerald
28-
"#3b82f6", // Blue
29-
"#d946ef", // Magenta
30-
"#78716c", // Stone
31-
"#facc15", // Gold
32-
// Additional distinct colors
33-
"#2dd4bf", // Turquoise
34-
"#fb923c", // Light orange
35-
"#a3e635", // Yellow-green
36-
"#38bdf8", // Light blue
37-
"#c084fc", // Light purple
38-
"#4ade80", // Light green
39-
"#fbbf24", // Light amber
40-
"#f472b6", // Light pink
41-
"#67e8f9", // Light cyan
42-
"#818cf8", // Light indigo
43-
];
44-
45-
function getSeriesColor(index: number): string {
46-
return CHART_COLORS[index % CHART_COLORS.length];
47-
}
7+
import { getRunStatusHexColor } from "~/components/runs/v3/TaskRunStatus";
8+
import { getSeriesColor } from "./chartColors";
489

4910
interface QueryResultsChartProps {
5011
rows: Record<string, unknown>[];
@@ -828,17 +789,25 @@ export const QueryResultsChart = memo(function QueryResultsChart({
828789
// Create dynamic Y-axis formatter based on data range
829790
const yAxisFormatter = useMemo(() => createYAxisFormatter(data, series), [data, series]);
830791

792+
// Check if the group-by column has a runStatus customRenderType
793+
const groupByIsRunStatus = useMemo(() => {
794+
if (!groupByColumn) return false;
795+
const col = columns.find((c) => c.name === groupByColumn);
796+
return col?.customRenderType === "runStatus";
797+
}, [groupByColumn, columns]);
798+
831799
// Build chart config for colors/labels
832800
const chartConfig = useMemo(() => {
833801
const cfg: ChartConfig = {};
834802
series.forEach((s, i) => {
803+
const statusColor = groupByIsRunStatus ? getRunStatusHexColor(s) : undefined;
835804
cfg[s] = {
836805
label: s,
837-
color: getSeriesColor(i),
806+
color: statusColor ?? config.seriesColors?.[s] ?? getSeriesColor(i),
838807
};
839808
});
840809
return cfg;
841-
}, [series]);
810+
}, [series, groupByIsRunStatus, config.seriesColors]);
842811

843812
// Custom tooltip label formatter for better date display
844813
const tooltipLabelFormatter = useMemo(() => {
@@ -882,8 +851,8 @@ export const QueryResultsChart = memo(function QueryResultsChart({
882851
return [min, "auto"] as [number, string];
883852
}, [data, series]);
884853

885-
// Determine appropriate angle for X-axis labels based on granularity
886-
const xAxisAngle = timeGranularity === "hours" || timeGranularity === "seconds" ? -45 : 0;
854+
// Angle all date-based labels for consistent appearance and to avoid overlap
855+
const xAxisAngle = isDateBased ? -45 : 0;
887856
const xAxisHeight = xAxisAngle !== 0 ? 65 : undefined;
888857

889858
// Check if the data would produce duplicate labels at the current granularity.

0 commit comments

Comments
 (0)