11import 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" ;
44import { cn } from "~/utils/cn" ;
55import { Paragraph } from "../primitives/Paragraph" ;
6+ import { Popover , PopoverContent , PopoverTrigger } from "../primitives/Popover" ;
67import { Select , SelectItem } from "../primitives/Select" ;
78import { Switch } from "../primitives/Switch" ;
89import { 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
1517export 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
2629interface 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+
557634function TypeBadge ( { type } : { type : string } ) {
558635 // Simplify type for display
559636 let displayType = type ;
0 commit comments