feat(design-system): first pass at db report chart colours (#46787)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

This is a first draft at introducing semantic colours to our
Observability charts. This moves away from just random colours being
assigned to prop after prop. They're only scoped to the Database reports
right now, but if it flows nice, we can open it up to the other reports
too.

This also aims to tone down some of the harsher colours in our charts,
such as the orange which sometimes can look like a warning metric/prop.

| Before | After |
|--------|--------|
| <img width="839" height="336" alt="Screenshot 2026-06-10 at 09 14 56"
src="https://github.com/user-attachments/assets/222747c5-973b-4165-aa53-df7b93412ad3"
/> | <img width="950" height="341" alt="Screenshot 2026-09-14 at 18 14
47"
src="https://github.com/user-attachments/assets/836f3ddd-4a97-4064-b8cf-3a3b435417ac"
/> |

cc @supabase/design for additional thoughts.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added semantic chart color roles with light and dark theme variants
for consistent visualizations.
* Standardized colors and fills across database, networking, storage,
and connection charts.
  * Maximum-value lines now use configured chart colors when available.
  * Added chart palette reference and stress-test examples.
* Added stacked bar charts, customizable margins, and gradient-filled
line charts.
* Improved multi-series bar chart focus and date-range footer alignment.

* **Documentation**
* Documented the chart palette, theme variants, accessibility guidance,
and usage recommendations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-09-16 09:18:40 +01:00
1 parent 92fdb1d3c5
commit 7fce0a12d9
26 files changed
+540 -219

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
:root {
--chart-1: var(--color-brand-800);
--chart-2: var(--color-blue-900);
--chart-3: var(--color-pink-1000);
--chart-4: var(--color-violet-900);
--chart-5: var(--color-tomato-1000);
--chart-6: var(--color-indigo-1000);
--chart-7: var(--color-green-1100);
--chart-8: var(--color-purple-1000);
--chart-1-fill: var(--color-brand-500);
--chart-2-fill: var(--color-blue-500);
--chart-3-fill: var(--color-pink-500);
--chart-4-fill: var(--color-violet-500);
--chart-5-fill: var(--color-tomato-500);
--chart-6-fill: var(--color-indigo-500);
--chart-7-fill: var(--color-green-500);
--chart-8-fill: var(--color-purple-500);
--chart-in: var(--chart-2);
--chart-out: var(--chart-5);
--chart-in-fill: var(--chart-2-fill);
--chart-out-fill: var(--chart-5-fill);
--chart-reference: var(--color-scale-1000);
--chart-muted: var(--color-scale-700);
--chart-muted-fill: var(--color-scale-400);
--chart-status-success: hsl(var(--brand-default));
--chart-status-success-muted: hsl(var(--brand-500));
--chart-status-warning: var(--warning);
--chart-status-warning-muted: hsl(var(--warning-500));
--chart-status-destructive: var(--destructive);
--chart-status-destructive-muted: hsl(var(--destructive-500));
}
[data-theme*='dark'] {
--chart-1: var(--color-brand-900);
--chart-2: var(--color-blue-1100);
--chart-3: var(--color-pink-1100);
--chart-4: var(--color-violet-1100);
--chart-5: var(--color-tomato-1100);
--chart-6: var(--color-indigo-1100);
--chart-7: var(--color-green-1100);
--chart-8: var(--color-purple-1100);
--chart-muted: var(--color-scale-800);
}
+1
View File
@@ -26,6 +26,7 @@
* consumes them from @theme inline so Tailwind generates the utilities. */
@import './css/colors.css';
@import './css/theme.css';
@import './css/charts.css';
@import './css/animations.css';
@import './css/utilities.css';
@import './tailwind-plugins/hit-area.css';
@@ -15,6 +15,8 @@ import {
import type { CategoricalChartState } from 'recharts/types/chart/types'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, cn } from 'ui'
const DIMMED_FILL_OPACITY = 0.35
const CHART_COLORS = {
TICK: 'var(--background-overlay-hover)',
AXIS: 'var(--background-overlay-hover)',
@@ -66,6 +68,8 @@ export interface ChartBarProps {
showGrid?: boolean
showYAxis?: boolean
showXAxis?: boolean
isStacked?: boolean
margin?: { top?: number; right?: number; bottom?: number; left?: number }
XAxisProps?: {
tick?: boolean
tickFormatter?: (value: any) => string
@@ -102,6 +106,8 @@ export const ChartBar = ({
showGrid = false,
showYAxis = false,
showXAxis = false,
isStacked = false,
margin: marginProp,
XAxisProps,
YAxisProps,
}: ChartBarProps) => {
@@ -146,23 +152,28 @@ export const ChartBar = ({
...XAxisProps,
}
const yAxisWidth = showYAxis ? (YAxisProps?.width ?? 60) : 0
const yAxisConfig = {
tick: showYAxis
? { fill: 'var(--color-foreground-lighter)', fontSize: 10, fontFamily: 'var(--font-mono)' }
: false,
hide: !showYAxis,
tickMargin: showYAxis ? (YAxisProps?.tickMargin ?? 4) : 0,
width: showYAxis ? (YAxisProps?.width ?? 60) : 0,
width: yAxisWidth,
axisLine: { stroke: CHART_COLORS.AXIS },
tickLine: { stroke: CHART_COLORS.AXIS },
...YAxisProps,
}
const hasDateRangeFooter = xKey === 'timestamp' && data.length > 0
const margin = {
top: 0,
right: 0,
left: 0,
bottom: 0,
...marginProp,
}
return (
@@ -170,7 +181,10 @@ export const ChartBar = ({
data-testid="chart-bar"
className={cn('flex flex-col gap-y-3 w-full', isFullHeight ? 'h-full' : 'h-24', className)}
>
<ChartContainer className="w-full! h-full" config={chartConfig}>
<ChartContainer
className={cn('w-full!', hasDateRangeFooter ? 'h-[calc(100%-28px)]' : 'h-full')}
config={chartConfig}
>
<RechartBarChart
data={data}
syncId={syncId}
@@ -230,7 +244,6 @@ export const ChartBar = ({
/>
}
/>
{/* Selection highlight area */}
{showHighlightActions && (
<ReferenceArea
x1={chartHighlight?.coordinates.left}
@@ -251,7 +264,28 @@ export const ChartBar = ({
? keyConfig.theme.dark
: keyConfig.theme.light
: color)
return <Bar key={key} dataKey={key} fill={barColor} maxBarSize={24} />
return (
<Bar
key={key}
dataKey={key}
fill={barColor}
maxBarSize={24}
stackId={isStacked ? 'stack' : undefined}
>
{data.map((_entry: ChartBarTick, dataIndex: number) => (
<Cell
key={`${key}-${dataIndex}`}
className="cursor-pointer transition-opacity"
fill={barColor}
fillOpacity={
focusDataIndex === null || focusDataIndex === dataIndex
? 1
: DIMMED_FILL_OPACITY
}
/>
))}
</Bar>
)
})
) : (
<Bar dataKey={dataKey} fill={color} maxBarSize={24}>
@@ -267,8 +301,11 @@ export const ChartBar = ({
</RechartBarChart>
</ChartContainer>
{xKey === 'timestamp' && data && data.length > 0 && (
<div className="text-foreground-lighter -mt-6 flex items-center justify-between text-[10px] font-mono">
{hasDateRangeFooter && (
<div
className="text-foreground-lighter flex h-4 items-center justify-between text-[10px] font-mono"
style={{ paddingLeft: yAxisWidth + margin.left }}
>
<span>{dayjs(data[0][xKey]).format(DateTimeFormat)}</span>
<span>{dayjs(data[data.length - 1]?.[xKey]).format(DateTimeFormat)}</span>
</div>
@@ -2,7 +2,7 @@
import dayjs from 'dayjs'
import { useTheme } from 'next-themes'
import { ReactNode, useState } from 'react'
import { ReactNode, useId, useState } from 'react'
import {
Area,
CartesianGrid,
@@ -118,6 +118,7 @@ export const ChartLine = ({
referenceLines,
}: ChartLineProps) => {
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
const gradientPrefix = `chart-line-fill-${useId().replace(/:/g, '')}`
const { resolvedTheme } = useTheme()
const isDarkMode = resolvedTheme?.includes('dark')
@@ -157,18 +158,30 @@ export const ChartLine = ({
...XAxisProps,
}
const yAxisWidth = showYAxis ? (YAxisProps?.width ?? 60) : 0
const yAxisConfig = {
tick: showYAxis
? { fill: 'var(--color-foreground-lighter)', fontSize: 10, fontFamily: 'var(--font-mono)' }
: false,
hide: !showYAxis,
tickMargin: showYAxis ? (YAxisProps?.tickMargin ?? 4) : 0,
width: showYAxis ? (YAxisProps?.width ?? 60) : 0,
width: yAxisWidth,
axisLine: { stroke: CHART_COLORS.AXIS },
tickLine: { stroke: CHART_COLORS.AXIS },
...YAxisProps,
}
const hasDateRangeFooter = xKey === 'timestamp' && data.length > 0
const resolveSeriesColor = (key: string) => {
const keyConfig = chartConfig[key]
return (
keyConfig?.color ||
(keyConfig?.theme ? (isDarkMode ? keyConfig.theme.dark : keyConfig.theme.light) : color)
)
}
const margin = {
top: 0,
right: 0,
@@ -189,7 +202,10 @@ export const ChartLine = ({
data-testid="chart-line"
className={cn('flex flex-col gap-y-3 w-full', isFullHeight ? 'h-full' : 'h-24', className)}
>
<ChartContainer className="w-full! h-full" config={chartConfig}>
<ChartContainer
className={cn('w-full!', hasDateRangeFooter ? 'h-[calc(100%-28px)]' : 'h-full')}
config={chartConfig}
>
<RechartAreaChart
data={data}
syncId={syncId}
@@ -236,6 +252,21 @@ export const ChartLine = ({
if (onLineClick) onLineClick(datum, tooltipData)
}}
>
<defs>
{keysToRender.map((key, index) => (
<linearGradient
key={key}
id={`${gradientPrefix}-${index}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="5%" stopColor={resolveSeriesColor(key)} stopOpacity={0.8} />
<stop offset="95%" stopColor={resolveSeriesColor(key)} stopOpacity={0} />
</linearGradient>
))}
</defs>
{showGrid && <CartesianGrid vertical={false} stroke={CHART_COLORS.AXIS} />}
<YAxis {...yAxisConfig} />
<XAxis {...xAxisConfig} />
@@ -275,7 +306,6 @@ export const ChartLine = ({
/>
}
/>
{/* Selection highlight area */}
{showHighlightActions && (
<ReferenceArea
x1={chartHighlight?.coordinates.left}
@@ -295,38 +325,26 @@ export const ChartLine = ({
strokeDasharray={line.strokeDasharray ?? '4 4'}
/>
))}
{keysToRender.map((key, index) => {
const keyConfig = chartConfig[key]
const lineColor =
keyConfig?.color ||
(keyConfig?.theme
? isDarkMode
? keyConfig.theme.dark
: keyConfig.theme.light
: color)
const baseOpacity = 0.2
const opacityIncrement = 0.1
const maxOpacity = 0.6
const fillOpacity = Math.min(baseOpacity + index * opacityIncrement, maxOpacity)
return (
<Area
key={key}
type="step"
dataKey={key}
fill={lineColor}
fillOpacity={fillOpacity}
stroke={lineColor}
strokeWidth={strokeWidth}
stackId={keysToRender.length > 1 ? `stack-${key}` : undefined}
/>
)
})}
{keysToRender.map((key, index) => (
<Area
key={key}
type="step"
dataKey={key}
fill={`url(#${gradientPrefix}-${index})`}
fillOpacity={0.1}
stroke={resolveSeriesColor(key)}
strokeWidth={strokeWidth}
stackId={keysToRender.length > 1 ? `stack-${key}` : undefined}
/>
))}
</RechartAreaChart>
</ChartContainer>
{xKey === 'timestamp' && data && data.length > 0 && (
<div className="text-foreground-lighter -mt-6 flex items-center justify-between text-[10px] font-mono">
{hasDateRangeFooter && (
<div
className="text-foreground-lighter flex h-4 items-center justify-between text-[10px] font-mono"
style={{ paddingLeft: yAxisWidth + margin.left }}
>
<span>{dayjs(data[0][xKey]).format(DateTimeFormat)}</span>
<span>{dayjs(data[data.length - 1]?.[xKey]).format(DateTimeFormat)}</span>
</div>
@@ -13,8 +13,8 @@ const CHART_COLORS = {
GREEN_2: 'hsl(var(--brand-500))',
RED_1: 'hsl(var(--destructive-default))',
RED_2: 'hsl(var(--destructive-500))',
YELLOW_1: 'var(--chart-warning, hsl(var(--warning-default)))',
YELLOW_2: 'var(--chart-warning-muted, hsl(var(--warning-500)))',
YELLOW_1: 'var(--chart-status-warning)',
YELLOW_2: 'var(--chart-status-warning-muted)',
}
type LogsBarChartDatum = {