mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
26 files changed
+540
-219
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user