diff --git a/apps/design-system/content/docs/components/chart.mdx b/apps/design-system/content/docs/components/chart.mdx index 671491da8e8..68e336bdcf2 100644 --- a/apps/design-system/content/docs/components/chart.mdx +++ b/apps/design-system/content/docs/components/chart.mdx @@ -49,27 +49,7 @@ We do not wrap Recharts. This means you're not locked into an abstraction. When -Add the following colors to your CSS file in your app. - -```css -@layer base { - :root { - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - } - - .dark { - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } -} -``` +Chart colors are already defined for every app in `packages/config/css/charts.css`, which ships through the shared Tailwind config. It provides eight categorical slots, `--chart-1` through `--chart-8`, each with a matching `-fill` token, resolved per theme. See the [Charts](/docs/ui-patterns/charts) pattern page for the palette and the rules for assigning slots. ## Your First Chart @@ -327,25 +307,19 @@ Charts has built-in support for theming. You can use css variables (recommended) -Define your colors in your css file +Pick a slot from the shared palette -```css {6-7,14-15} title="globals.css" -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - // ... - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - } +```css title="packages/config/css/charts.css" +:root { + --chart-1: var(--color-brand-800); + --chart-2: var(--color-blue-900); + /* ... */ +} - .dark: { - --background: 240 10% 3.9%; - --foreground: 0 0% 100%; - // ... - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - } +[data-theme*='dark'] { + --chart-1: var(--color-brand-900); + --chart-2: var(--color-blue-1100); + /* ... */ } ``` @@ -355,28 +329,18 @@ Charts has built-in support for theming. You can use css variables (recommended) const chartConfig = { desktop: { label: 'Desktop', - color: 'hsl(var(--chart-1))', + color: 'var(--chart-1)', }, mobile: { label: 'Mobile', - color: 'hsl(var(--chart-2))', + color: 'var(--chart-2)', }, } satisfies ChartConfig ``` -We're wrapping the value in `hsl()` here because we define the colors without color space function. - -This is not required. You can use full color values, such as hex, hsl or oklch. - -```css ---chart-1: oklch(70% 0.227 154.59); -``` - -```tsx -color: "var(--chart-1)", -``` +The slots are full color values, so pass them as `var(--chart-1)`. Do not wrap them in `hsl()`; that form is for bare HSL triplets and produces an invalid color here. @@ -472,11 +436,11 @@ const chartConfig = { }, chrome: { label: 'Chrome', - color: 'hsl(var(--chart-1))', + color: 'var(--chart-1)', }, safari: { label: 'Safari', - color: 'hsl(var(--chart-2))', + color: 'var(--chart-2)', }, } satisfies ChartConfig ``` @@ -516,11 +480,11 @@ const chartData = [ const chartConfig = { chrome: { label: 'Chrome', - color: 'hsl(var(--chart-1))', + color: 'var(--chart-1)', }, safari: { label: 'Safari', - color: 'hsl(var(--chart-2))', + color: 'var(--chart-2)', }, } satisfies ChartConfig ``` diff --git a/apps/design-system/content/docs/ui-patterns/charts.mdx b/apps/design-system/content/docs/ui-patterns/charts.mdx index ef2b6cf8ad9..db889d46815 100644 --- a/apps/design-system/content/docs/ui-patterns/charts.mdx +++ b/apps/design-system/content/docs/ui-patterns/charts.mdx @@ -23,6 +23,29 @@ Our charts use a combination of our own presentational components and [Recharts] 3. **Keep it simple**: Try to avoid abstracting the chart content too much. These components should cover most of your presentational needs. +## Color + +Series colors come from eight categorical slots, `--chart-1` through `--chart-8`, defined in +`packages/config/css/charts.css`. Assign them in order and never cycle: a ninth series folds +into "Other" or becomes small multiples. Each slot has a matching `-fill` token. Slots resolve +per theme, so pass `var(--chart-n)` and never branch on light/dark in code. Adjacent slots +alternate hue families and clear colorblind separation in both themes. + +Reference lines use `--chart-reference`. Headroom, idle and unused capacity use `--chart-muted`. +Directional pairs use `--chart-in` / `--chart-out` so read and write keep the same hue across +charts. + +Status colors (`--chart-status-success`, `-warning`, `-destructive`, each with a `-muted` tier) +are reserved for state and always ship with an icon or label. Never use one as a series color: +amber on a neutral metric reads as a problem. Warm hues are otherwise limited to tomato, slot 5, +because no amber or yellow step is legible on the dark surface. + + + +Every slot stacked together, to check adjacent segments stay separable in both themes. + + + ## Examples ### Basic Chart Types diff --git a/apps/design-system/registry/charts.ts b/apps/design-system/registry/charts.ts index 9a42e5095a0..a272b16eede 100644 --- a/apps/design-system/registry/charts.ts +++ b/apps/design-system/registry/charts.ts @@ -57,4 +57,20 @@ export const charts: Registry = [ category: 'Charts', subcategory: 'Composed', }, + { + name: 'chart-palette', + type: 'components:block', + registryDependencies: ['chart'], + files: ['block/chart-palette.tsx'], + category: 'Charts', + subcategory: 'Palette', + }, + { + name: 'chart-palette-stress', + type: 'components:block', + registryDependencies: ['chart'], + files: ['block/chart-palette-stress.tsx'], + category: 'Charts', + subcategory: 'Palette', + }, ] diff --git a/apps/design-system/registry/default/block/chart-bar-interactive.tsx b/apps/design-system/registry/default/block/chart-bar-interactive.tsx index 128664bb431..c235862f1a0 100644 --- a/apps/design-system/registry/default/block/chart-bar-interactive.tsx +++ b/apps/design-system/registry/default/block/chart-bar-interactive.tsx @@ -116,11 +116,11 @@ const chartConfig = { }, desktop: { label: 'Desktop', - color: 'hsl(var(--chart-1))', + color: 'var(--chart-1)', }, mobile: { label: 'Mobile', - color: 'hsl(var(--chart-2))', + color: 'var(--chart-2)', }, } satisfies ChartConfig diff --git a/apps/design-system/registry/default/block/chart-composed-basic.tsx b/apps/design-system/registry/default/block/chart-composed-basic.tsx index 99be1076b08..d185c5e79bb 100644 --- a/apps/design-system/registry/default/block/chart-composed-basic.tsx +++ b/apps/design-system/registry/default/block/chart-composed-basic.tsx @@ -52,11 +52,11 @@ export default function ComposedChartBasic() { }, performance: { label: 'Performance', - color: 'hsl(var(--chart-2))', + color: 'var(--chart-2)', }, efficiency: { label: 'Efficiency', - color: 'hsl(var(--chart-5))', + color: 'var(--chart-5)', }, } diff --git a/apps/design-system/registry/default/block/chart-palette-stress.tsx b/apps/design-system/registry/default/block/chart-palette-stress.tsx new file mode 100644 index 00000000000..740e5c882e6 --- /dev/null +++ b/apps/design-system/registry/default/block/chart-palette-stress.tsx @@ -0,0 +1,72 @@ +'use client' + +import { + Chart, + ChartBar, + ChartCard, + ChartContent, + ChartHeader, + ChartTitle, + type ChartBarTick, + type ChartConfig, +} from 'ui-patterns/Chart' + +const SERIES = [ + { key: 'postgres', label: 'Postgres' }, + { key: 'postgrest', label: 'PostgREST' }, + { key: 'reserved', label: 'Reserved' }, + { key: 'auth', label: 'Auth' }, + { key: 'storage', label: 'Storage' }, + { key: 'realtime', label: 'Realtime' }, + { key: 'cron', label: 'Cron' }, + { key: 'other', label: 'Other roles' }, +] + +const config: ChartConfig = Object.fromEntries( + SERIES.map((s, i) => [s.key, { label: s.label, color: `var(--chart-${i + 1})` }]) +) + +export default function ChartPaletteStress() { + const data: ChartBarTick[] = Array.from({ length: 40 }, (_, i) => { + const date = new Date() + date.setMinutes(date.getMinutes() - (40 - i) * 3) + const row: ChartBarTick = { timestamp: date.toISOString() } + + const trend = Math.sin((i / 40) * Math.PI * 2) + SERIES.forEach((s, idx) => { + const phase = Math.sin(i / 3.5 + idx * 1.7) + const jitter = Math.sin(i * 2.3 + idx * 0.9) * 1.5 + row[s.key] = Math.max(1, Math.round(5 + idx * 1.8 + phase * 3 + trend * 2 + jitter)) + }) + return row + }) + + return ( +
+ + + + + Client connections by role + + + +
+ s.key)} + config={config} + isStacked + isFullHeight + showGrid + showYAxis + YAxisProps={{ width: 36 }} + /> +
+
+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/block/chart-palette.tsx b/apps/design-system/registry/default/block/chart-palette.tsx new file mode 100644 index 00000000000..8d44644e59d --- /dev/null +++ b/apps/design-system/registry/default/block/chart-palette.tsx @@ -0,0 +1,135 @@ +import { ReactNode } from 'react' + +const SLOTS = [1, 2, 3, 4, 5, 6, 7, 8] + +const STATUS = [ + { name: '--chart-status-success', muted: '--chart-status-success-muted', note: 'Healthy, ok' }, + { + name: '--chart-status-warning', + muted: '--chart-status-warning-muted', + note: 'Threshold breach', + }, + { + name: '--chart-status-destructive', + muted: '--chart-status-destructive-muted', + note: 'Error, failure', + }, +] + +const DEFAULTS = [ + { name: '--chart-in', note: 'Pinned: network in, disk read' }, + { name: '--chart-out', note: 'Pinned: network out, disk write' }, + { name: '--chart-reference', note: 'Reference lines, max values' }, + { name: '--chart-muted', note: 'Headroom, idle, unused capacity' }, +] + +function Swatch({ token, label }: { token: string; label: string }) { + return ( +
+
+ {label} +
+ ) +} + +function TokenCard({ + title, + token, + note, + children, +}: { + title: ReactNode + token: string + note?: string + children: ReactNode +}) { + return ( +
+
+
{title}
+ {token} +
+
{children}
+ {note &&

{note}

} +
+ ) +} + +function Section({ + title, + description, + children, + className, +}: { + title: string + description: string + children: ReactNode + className: string +}) { + return ( +
+
+

{title}

+

{description}

+
+
{children}
+
+ ) +} + +export default function ChartPalette() { + return ( +
+
+ {SLOTS.map((n) => ( + + + + + ))} +
+ +
+ {STATUS.map((d) => ( + + + + + ))} +
+ +
+ {DEFAULTS.map((d) => ( + + + + ))} +
+
+ ) +} diff --git a/apps/design-system/registry/default/example/chart-tooltip-demo.tsx b/apps/design-system/registry/default/example/chart-tooltip-demo.tsx index 5fffac2424a..af4c7735696 100644 --- a/apps/design-system/registry/default/example/chart-tooltip-demo.tsx +++ b/apps/design-system/registry/default/example/chart-tooltip-demo.tsx @@ -32,8 +32,8 @@ export default function Component() { @@ -64,8 +64,8 @@ export default function Component() { label="Browser" hideLabel payload={[ - { name: 'Chrome', value: 1286, fill: 'hsl(var(--chart-3))' }, - { name: 'Firefox', value: 1000, fill: 'hsl(var(--chart-4))' }, + { name: 'Chrome', value: 1286, fill: 'var(--chart-3)' }, + { name: 'Firefox', value: 1000, fill: 'var(--chart-4)' }, ]} indicator="dashed" className="w-32" @@ -74,7 +74,7 @@ export default function Component() {
@@ -84,7 +84,7 @@ export default function Component() { diff --git a/apps/design-system/styles/globals.css b/apps/design-system/styles/globals.css index 32c9d5d396b..0a55d08da3e 100644 --- a/apps/design-system/styles/globals.css +++ b/apps/design-system/styles/globals.css @@ -36,22 +36,6 @@ } @layer base { - :root { - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - } - - .dark { - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } - * { @apply border-border; } diff --git a/apps/learn/styles/globals.css b/apps/learn/styles/globals.css index 1478d871f62..a0fc87d4f3f 100644 --- a/apps/learn/styles/globals.css +++ b/apps/learn/styles/globals.css @@ -30,11 +30,6 @@ --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); - --color-chart-1: hsl(var(--chart-1)); - --color-chart-2: hsl(var(--chart-2)); - --color-chart-3: hsl(var(--chart-3)); - --color-chart-4: hsl(var(--chart-4)); - --color-chart-5: hsl(var(--chart-5)); } @layer base { @@ -58,11 +53,6 @@ --border: hsl(220 13% 91%); --input: hsl(220 13% 91%); --ring: hsl(220 13% 70.8%); - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; --radius: 0.625rem; --sidebar: hsl(210 20% 98.5%); --sidebar-foreground: hsl(224 71.4% 4.1%); @@ -94,11 +84,6 @@ --border: hsl(215 27.9% 16.9%); --input: hsl(215 27.9% 16.9%); --ring: hsl(217.9 10.6% 55.6%); - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; --sidebar: hsl(220.9 39.3% 11%); --sidebar-foreground: hsl(210 20% 98%); --sidebar-primary: hsl(220 70% 50%); diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx index 32e018133a6..25412b9ea91 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx @@ -17,11 +17,7 @@ interface QueryResultChartProps { result?: QueryResult } -const Y_SERIES_COLORS = [ - 'hsl(var(--brand-default))', - 'hsl(var(--chart-blue))', - 'hsl(var(--chart-3))', -] +const Y_SERIES_COLORS = ['hsl(var(--brand-default))', 'var(--chart-2)', 'var(--chart-3)'] const toChartValue = (value: unknown): string | number => { if (typeof value === 'number' || typeof value === 'string') return value diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.ts index dbb91f1aed2..a0d90f31fd9 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.ts +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.utils.ts @@ -88,7 +88,7 @@ export const INVOCATION_CHART_CONFIG = { }, warning_count: { label: 'Warnings', - color: 'var(--chart-warning)', + color: 'var(--chart-status-warning)', }, error_count: { label: 'Errors', diff --git a/apps/studio/components/interfaces/QueryInsights/QueryInsightsChart/QueryInsightsChart.constants.ts b/apps/studio/components/interfaces/QueryInsights/QueryInsightsChart/QueryInsightsChart.constants.ts index 35ec04d9a08..ebcad57322f 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryInsightsChart/QueryInsightsChart.constants.ts +++ b/apps/studio/components/interfaces/QueryInsights/QueryInsightsChart/QueryInsightsChart.constants.ts @@ -9,7 +9,7 @@ export const CHART_TABS = [ export const LEGEND_ITEMS: Record = { query_latency: [ - { label: 'P50', color: 'hsl(var(--chart-4))', dataKey: 'p50' }, + { label: 'P50', color: 'var(--chart-4)', dataKey: 'p50' }, { label: 'P95', color: CHART_COLORS.GREEN_1, dataKey: 'p95' }, ], rows_read: [{ label: 'Rows Read', color: CHART_COLORS.GREEN_1, dataKey: 'rows_read' }], @@ -19,4 +19,4 @@ export const LEGEND_ITEMS: Record= threshold.danger - ? 'var(--chart-destructive)' + ? 'var(--chart-status-destructive)' : value >= threshold.warning - ? 'var(--chart-warning)' + ? 'var(--chart-status-warning)' : 'var(--chart-1)' } else { return value <= threshold.danger - ? 'var(--chart-destructive)' + ? 'var(--chart-status-destructive)' : value <= threshold.warning - ? 'var(--chart-warning)' + ? 'var(--chart-status-warning)' : 'var(--chart-1)' } } diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx index f67b3f1040a..b5099542a44 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx @@ -77,11 +77,11 @@ import { useShortcut } from '@/state/shortcuts/useShortcut' export const CHART_CONFIG = { success: { label: , - color: 'var(--chart-success)', + color: 'var(--chart-muted)', }, warning: { label: , - color: 'var(--chart-warning)', + color: 'var(--chart-status-warning)', }, error: { label: , diff --git a/apps/studio/components/ui/Charts/ComposedChart.tsx b/apps/studio/components/ui/Charts/ComposedChart.tsx index 90b1babe16e..757556976d4 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.tsx @@ -40,6 +40,7 @@ import { CustomTooltip, getStackId, MultiAttribute, + resolveChartColor, } from './ComposedChart.utils' import NoDataPlaceholder from './NoDataPlaceholder' import { ChartHighlight } from './useChartHighlight' @@ -307,7 +308,7 @@ export function ComposedChart({ const maxAttribute = attributes.find((a) => a.isMaxValue) const maxAttributeData = { name: maxAttribute?.attribute, - color: CHART_COLORS.REFERENCE_LINE, + color: resolveChartColor(maxAttribute?.color, isDarkMode) ?? CHART_COLORS.REFERENCE_LINE, } const referenceLines = attributes.filter((attribute) => { @@ -342,16 +343,12 @@ export function ComposedChart({ const attribute = attributes.find((attr) => attr.attribute === att.name) return { ...att, - color: attribute?.color - ? isDarkMode - ? attribute.color.dark - : attribute.color.light - : STACKED_CHART_COLORS[index % STACKED_CHART_COLORS.length], - fill: attribute?.fill - ? isDarkMode - ? attribute.fill.dark - : attribute.fill.light - : STACKED_CHART_FILLS[index % STACKED_CHART_FILLS.length], + color: + resolveChartColor(attribute?.color, isDarkMode) ?? + STACKED_CHART_COLORS[index % STACKED_CHART_COLORS.length], + fill: + resolveChartColor(attribute?.fill, isDarkMode) ?? + STACKED_CHART_FILLS[index % STACKED_CHART_FILLS.length], } }) : [] @@ -600,7 +597,9 @@ export function ComposedChart({ key={maxAttribute.attribute} type="linear" dataKey={maxAttribute.attribute} - stroke={CHART_COLORS.REFERENCE_LINE} + stroke={ + resolveChartColor(maxAttribute.color, isDarkMode) ?? CHART_COLORS.REFERENCE_LINE + } strokeWidth={2} strokeDasharray={maxAttribute.strokeDasharray ?? '3 3'} dot={false} @@ -616,7 +615,7 @@ export function ComposedChart({ key={line.attribute} y={line.value} strokeWidth={1} - stroke={isDarkMode ? line.color?.dark : line.color?.light} + stroke={resolveChartColor(line.color, isDarkMode)} strokeDasharray={line.strokeDasharray ?? '3 3'} label={undefined} > diff --git a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx index 3aa6b215d15..60690c6bdb2 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx @@ -40,18 +40,25 @@ export interface ReportAttributes { export type Provider = 'infra-monitoring' | 'daily-stats' | 'mock' | 'reference-line' | 'logs' +export type ThemedColor = { + light?: string + dark?: string +} + +export const resolveChartColor = ( + color: string | ThemedColor | undefined, + isDarkMode: boolean | undefined +) => { + if (typeof color === 'string') return color + return isDarkMode ? color?.dark : color?.light +} + export type MultiAttribute = { attribute: string provider?: Provider label?: string - color?: { - light: string - dark: string - } - fill?: { - light?: string - dark?: string - } + color?: string | { light: string; dark: string } + fill?: string | ThemedColor statusCode?: string grantType?: string providerType?: string diff --git a/apps/studio/components/ui/DataTable/DataTable.utils.ts b/apps/studio/components/ui/DataTable/DataTable.utils.ts index 54366ba78c7..b26e17c4d38 100644 --- a/apps/studio/components/ui/DataTable/DataTable.utils.ts +++ b/apps/studio/components/ui/DataTable/DataTable.utils.ts @@ -63,9 +63,9 @@ export function getLevelColor( case 'success': return { text: 'text-muted', - bg: 'bg-[var(--chart-success)] group-data-[state=selected]/row:bg-foreground-lighter', + bg: 'bg-[var(--chart-muted)] group-data-[state=selected]/row:bg-foreground-lighter', border: - 'border-[var(--chart-success)] group-data-[state=selected]/row:border-foreground-lighter', + 'border-[var(--chart-muted)] group-data-[state=selected]/row:border-foreground-lighter', } case 'warning': return { diff --git a/apps/studio/data/reports/database-charts.ts b/apps/studio/data/reports/database-charts.ts index 65e300266e0..45682dbdc16 100644 --- a/apps/studio/data/reports/database-charts.ts +++ b/apps/studio/data/reports/database-charts.ts @@ -75,6 +75,8 @@ export const getReportAttributesV2: ( attribute: 'ram_usage_used', provider: 'infra-monitoring', label: 'Used', + color: 'var(--chart-1)', + fill: 'var(--chart-1-fill)', tooltip: 'RAM in use by Postgres and the operating system. Sustained high usage may indicate memory pressure', }, @@ -82,6 +84,8 @@ export const getReportAttributesV2: ( attribute: 'ram_usage_cache_and_buffers', provider: 'infra-monitoring', label: 'Cache + Buffers', + color: 'var(--chart-2)', + fill: 'var(--chart-2-fill)', tooltip: 'RAM used by the operating system page cache and PostgreSQL buffers to accelerate disk reads/writes', }, @@ -89,6 +93,8 @@ export const getReportAttributesV2: ( attribute: 'ram_usage_free', provider: 'infra-monitoring', label: 'Free', + color: 'var(--chart-muted)', + fill: 'var(--chart-muted-fill)', tooltip: 'Unallocated memory available for use. A small portion is always reserved by the operating system', }, @@ -105,6 +111,7 @@ export const getReportAttributesV2: ( provider: 'infra-monitoring', label: 'Swap', omitFromTotal: true, + color: 'var(--chart-3)', tooltip: 'Swap space in use by the operating system. Sustained swap usage indicates memory pressure and may degrade database performance', }, @@ -132,6 +139,8 @@ export const getReportAttributesV2: ( attribute: 'ram_commit_used', provider: 'infra-monitoring', label: 'Committed', + color: 'var(--chart-1)', + fill: 'var(--chart-1-fill)', tooltip: 'Total memory the kernel has promised to processes (RAM plus swap). Sustained values near or above the commit limit indicate overcommitment and a high risk of out-of-memory failures', }, @@ -139,6 +148,7 @@ export const getReportAttributesV2: ( attribute: 'ram_commit_limit', provider: 'infra-monitoring', label: 'Commit limit', + color: 'var(--chart-reference)', isMaxValue: true, omitFromTotal: true, tooltip: @@ -201,8 +211,8 @@ export const getReportAttributesV2: ( provider: 'infra-monitoring', label: 'System', format: '%', - color: { light: '#EDC35E', dark: '#EDD35E' }, - fill: { light: '#F6D99F', dark: '#5C5230' }, + color: 'var(--chart-2)', + fill: 'var(--chart-2-fill)', tooltip: 'CPU time spent on kernel operations (e.g., process scheduling, memory management). High values may indicate system overhead', }, @@ -211,8 +221,8 @@ export const getReportAttributesV2: ( provider: 'infra-monitoring', label: 'User', format: '%', - color: { light: '#0063E8', dark: '#65BCD9' }, - fill: { light: '#80B1F4', dark: '#2A3D45' }, + color: 'var(--chart-1)', + fill: 'var(--chart-1-fill)', tooltip: 'CPU time used by database queries and user-space processes. High values may suggest CPU-intensive queries', }, @@ -221,8 +231,8 @@ export const getReportAttributesV2: ( provider: 'infra-monitoring', label: 'IOwait', format: '%', - color: { light: '#DB3A34', dark: '#FF6B6B' }, - fill: { light: '#F2A7A3', dark: '#5C2A2A' }, + color: 'var(--chart-3)', + fill: 'var(--chart-3-fill)', tooltip: 'CPU time waiting for disk or network I/O. High values may indicate disk bottlenecks', }, @@ -231,8 +241,8 @@ export const getReportAttributesV2: ( provider: 'infra-monitoring', label: 'IRQs', format: '%', - color: { light: '#DA760B', dark: '#DA760B' }, - fill: { light: '#FFB885', dark: '#5C3D0A' }, + color: 'var(--chart-4)', + fill: 'var(--chart-4-fill)', tooltip: 'CPU time handling hardware interrupt requests (IRQ)', }, { @@ -240,11 +250,30 @@ export const getReportAttributesV2: ( provider: 'infra-monitoring', label: 'Other', format: '%', - color: { light: '#B616A6', dark: '#DB8DF9' }, - fill: { light: '#DB8BD3', dark: '#4A3D5C' }, + color: 'var(--chart-5)', + fill: 'var(--chart-5-fill)', tooltip: 'CPU time spent on other tasks (e.g., background processes, software interrupts)', }, + { + attribute: 'cpu_usage_busy_idle', + provider: 'infra-monitoring', + label: 'Idle', + format: '%', + omitFromTotal: true, + color: 'var(--chart-muted)', + fill: 'var(--chart-muted-fill)', + tooltip: 'CPU time spent idle and available for new work', + }, + { + attribute: 'cpu_usage_max', + provider: 'reference-line', + label: 'Max', + value: 100, + color: 'var(--chart-reference)', + tooltip: 'Max CPU usage', + isMaxValue: true, + }, ], }, { @@ -269,12 +298,16 @@ export const getReportAttributesV2: ( attribute: 'network_receive_bytes', provider: 'infra-monitoring', label: 'Network in', + color: 'var(--chart-in)', + fill: 'var(--chart-in-fill)', tooltip: 'Inbound network throughput (bytes per second)', }, { attribute: 'network_transmit_bytes', provider: 'infra-monitoring', label: 'Network out', + color: 'var(--chart-out)', + fill: 'var(--chart-out-fill)', tooltip: 'Outbound network throughput (bytes per second)', }, ], @@ -301,6 +334,8 @@ export const getReportAttributesV2: ( attribute: 'disk_iops_write', provider: 'infra-monitoring', label: 'Write IOPS', + color: 'var(--chart-out)', + fill: 'var(--chart-out-fill)', tooltip: 'Number of write operations per second. High values indicate frequent data writes, logging, or transaction activity', }, @@ -308,6 +343,8 @@ export const getReportAttributesV2: ( attribute: 'disk_iops_read', provider: 'infra-monitoring', label: 'Read IOPS', + color: 'var(--chart-in)', + fill: 'var(--chart-in-fill)', tooltip: 'Number of read operations per second. High values suggest frequent disk reads due to queries or poor caching', }, @@ -315,6 +352,7 @@ export const getReportAttributesV2: ( attribute: 'disk_iops_max', provider: 'reference-line', label: 'Max IOPS', + color: 'var(--chart-reference)', value: effectiveMaxIops, tooltip: 'Effective maximum IOPS for your current compute and disk configuration. Equal to the lower of the compute IOPS limit and the provisioned disk IOPS', @@ -345,18 +383,23 @@ export const getReportAttributesV2: ( attribute: 'disk_bytes_read', provider: 'infra-monitoring', label: 'Read throughput', + color: 'var(--chart-in)', + fill: 'var(--chart-in-fill)', tooltip: 'Disk read throughput (bytes per second)', }, { attribute: 'disk_bytes_written', provider: 'infra-monitoring', label: 'Write throughput', + color: 'var(--chart-out)', + fill: 'var(--chart-out-fill)', tooltip: 'Disk write throughput (bytes per second)', }, { attribute: 'disk_throughput_max', provider: 'reference-line', label: 'Max throughput', + color: 'var(--chart-reference)', value: diskConfig?.attributes?.type === 'gp3' && typeof diskConfig.attributes.throughput_mbps === 'number' @@ -418,12 +461,15 @@ export const getReportAttributesV2: ( attribute: 'pg_stat_database_num_backends', provider: 'infra-monitoring', label: 'Total connections', + color: 'var(--chart-1)', + fill: 'var(--chart-1-fill)', tooltip: 'Total number of active database connections', }, { attribute: 'max_db_connections', provider: 'reference-line', label: 'Max connections', + color: 'var(--chart-reference)', value: maxConnections?.maxConnections, tooltip: 'Max available connections for your current compute size', isMaxValue: true, @@ -452,6 +498,8 @@ export const getReportAttributesV2: ( attribute: 'client_connections_postgres', provider: 'infra-monitoring', label: 'Postgres', + color: 'var(--chart-1)', + fill: 'var(--chart-1-fill)', tooltip: 'Direct connections to the Postgres database from your application and external clients', }, @@ -459,12 +507,16 @@ export const getReportAttributesV2: ( attribute: 'client_connections_authenticator', provider: 'infra-monitoring', label: 'PostgREST', + color: 'var(--chart-2)', + fill: 'var(--chart-2-fill)', tooltip: 'Connection pool managed by PostgREST', }, { attribute: 'client_connections_supabase_admin', provider: 'infra-monitoring', label: 'Reserved', + color: 'var(--chart-3)', + fill: 'var(--chart-3-fill)', tooltip: 'Administrative connections used by various Supabase services for internal operations and maintenance tasks', }, @@ -472,24 +524,31 @@ export const getReportAttributesV2: ( attribute: 'client_connections_supabase_auth_admin', provider: 'infra-monitoring', label: 'Auth', + color: 'var(--chart-4)', + fill: 'var(--chart-4-fill)', tooltip: 'Connection pool managed by Supabase Auth', }, { attribute: 'client_connections_supabase_storage_admin', provider: 'infra-monitoring', label: 'Storage', + color: 'var(--chart-5)', + fill: 'var(--chart-5-fill)', tooltip: 'Connection pool managed by Supabase Storage', }, { attribute: 'client_connections_other', provider: 'infra-monitoring', label: 'Other roles', + color: 'var(--chart-6)', + fill: 'var(--chart-6-fill)', tooltip: "Miscellaneous database connections that don't fall into other categories.", }, { attribute: 'max_db_connections', provider: 'reference-line', label: 'Max connections', + color: 'var(--chart-reference)', value: maxConnections?.maxConnections, tooltip: 'Max available connections for your current compute size', isMaxValue: true, @@ -522,12 +581,15 @@ export const getReportAttributesV2: ( attribute: 'client_connections_pgbouncer', provider: 'infra-monitoring', label: isHighAvailability ? 'multipooler' : 'pgbouncer', + color: 'var(--chart-1)', + fill: 'var(--chart-1-fill)', tooltip: isHighAvailability ? 'Multipooler connections' : 'PgBouncer connections', }, { attribute: 'pg_pooler_max_connections', provider: 'reference-line', label: 'Max pooler connections', + color: 'var(--chart-reference)', value: pgBouncerMaxConnections, tooltip: 'Maximum allowed pooler connections for your current compute size', isMaxValue: true, @@ -555,6 +617,8 @@ export const getReportAttributesV2: ( attribute: 'supavisor_connections_active', provider: 'infra-monitoring', label: 'supavisor', + color: 'var(--chart-1)', + fill: 'var(--chart-1-fill)', tooltip: 'Supavisor connections', }, ], @@ -582,6 +646,8 @@ export const getReportAttributesV2: ( provider: 'infra-monitoring', format: 'bytes', label: 'System', + color: 'var(--chart-3)', + fill: 'var(--chart-3-fill)', tooltip: 'Reserved space for the system to ensure your database runs smoothly', }, { @@ -589,6 +655,8 @@ export const getReportAttributesV2: ( provider: 'infra-monitoring', format: 'bytes', label: 'WAL', + color: 'var(--chart-2)', + fill: 'var(--chart-2-fill)', tooltip: 'Disk usage by the write-ahead log. The usage depends on your WAL settings and the amount of data being written to the database', }, @@ -597,6 +665,8 @@ export const getReportAttributesV2: ( provider: 'infra-monitoring', format: 'bytes', label: 'Database', + color: 'var(--chart-1)', + fill: 'var(--chart-1-fill)', tooltip: 'Disk usage by your database (tables, indexes, data, ...)', }, { @@ -605,6 +675,7 @@ export const getReportAttributesV2: ( isMaxValue: true, format: 'bytes', label: 'Disk Size', + color: 'var(--chart-reference)', tooltip: 'Disk Size refers to the total space your project occupies on disk', }, entitledFeatures.includes('database') && diff --git a/apps/studio/styles/globals.css b/apps/studio/styles/globals.css index 44e8f55b3c1..d75d9ca72f4 100644 --- a/apps/studio/styles/globals.css +++ b/apps/studio/styles/globals.css @@ -138,16 +138,6 @@ } :root { - --chart-1: hsl(var(--brand-default)); - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --chart-blue: 217 91% 60%; - --chart-warning: hsl(var(--warning-500)); - --chart-warning-muted: hsl(var(--warning-400)); - --chart-destructive: hsl(var(--destructive-default)); - --chart-success: color-mix(in oklch, var(--foreground-muted) 50%, white); --sidebar-background: var(--background-dash-sidebar); --sidebar-foreground: var(--foreground-default); --sidebar-primary: var(--foreground-default); @@ -161,16 +151,6 @@ [data-theme='dark'], .dark { - --chart-1: hsl(var(--brand-default)); - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --chart-blue: 217 91% 65%; - --chart-warning: hsl(var(--warning-default)); - --chart-warning-muted: hsl(var(--warning-500)); - --chart-destructive: hsl(var(--destructive-default)); - --chart-success: color-mix(in oklch, var(--foreground-muted) 55%, var(--background)); --sidebar-background: var(--background-dash-sidebar); --sidebar-foreground: var(--foreground-default); --sidebar-primary: var(--foreground-default); diff --git a/apps/ui-library/styles/globals.css b/apps/ui-library/styles/globals.css index 6d89a7d3595..f1cb2bfd4d2 100644 --- a/apps/ui-library/styles/globals.css +++ b/apps/ui-library/styles/globals.css @@ -37,11 +37,6 @@ /* --color-border: var(--border); */ --color-input: var(--input); --color-ring: var(--ring); - --color-chart-1: hsl(var(--chart-1)); - --color-chart-2: hsl(var(--chart-2)); - --color-chart-3: hsl(var(--chart-3)); - --color-chart-4: hsl(var(--chart-4)); - --color-chart-5: hsl(var(--chart-5)); } @layer base { @@ -65,11 +60,6 @@ --border: hsl(0 0% 92.2%); --input: hsl(0 0% 92.2%); --ring: hsl(0 0% 70.8%); - --chart-1: 41.1 22.2% 64.6%; - --chart-2: 184.7 11.8% 60%; - --chart-3: 227.4 7% 39.8%; - --chart-4: 84.4 18.9% 82.8%; - --chart-5: 70.1 18.8% 76.9%; --radius: 0.625rem; --sidebar: hsl(0 0% 98.5%); --sidebar-foreground: hsl(0 0% 14.5%); @@ -102,11 +92,6 @@ --border: hsl(0 0% 26.9%); --input: hsl(0 0% 26.9%); --ring: hsl(0 0% 55.6%); - --chart-1: 264.4 24.3% 48.8%; - --chart-2: 162.5 17% 69.6%; - --chart-3: 70.1 18.8% 76.9%; - --chart-4: 303.9 26.5% 62.7%; - --chart-5: 16.4 24.6% 64.5%; --sidebar: hsl(0 0% 20.5%); --sidebar-foreground: hsl(0 0% 98.5%); --sidebar-primary: hsl(264.4 24.3% 48.8%); diff --git a/packages/config/css/charts.css b/packages/config/css/charts.css new file mode 100644 index 00000000000..806419ac650 --- /dev/null +++ b/packages/config/css/charts.css @@ -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); +} diff --git a/packages/config/tailwind.config.css b/packages/config/tailwind.config.css index 329638a906a..28f167840a1 100644 --- a/packages/config/tailwind.config.css +++ b/packages/config/tailwind.config.css @@ -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'; diff --git a/packages/ui-patterns/src/Chart/charts/chart-bar.tsx b/packages/ui-patterns/src/Chart/charts/chart-bar.tsx index 49825a36265..1418ac7ab9a 100644 --- a/packages/ui-patterns/src/Chart/charts/chart-bar.tsx +++ b/packages/ui-patterns/src/Chart/charts/chart-bar.tsx @@ -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)} > - + } /> - {/* Selection highlight area */} {showHighlightActions && ( + return ( + + {data.map((_entry: ChartBarTick, dataIndex: number) => ( + + ))} + + ) }) ) : ( @@ -267,8 +301,11 @@ export const ChartBar = ({ - {xKey === 'timestamp' && data && data.length > 0 && ( -
+ {hasDateRangeFooter && ( +
{dayjs(data[0][xKey]).format(DateTimeFormat)} {dayjs(data[data.length - 1]?.[xKey]).format(DateTimeFormat)}
diff --git a/packages/ui-patterns/src/Chart/charts/chart-line.tsx b/packages/ui-patterns/src/Chart/charts/chart-line.tsx index c100f8100fd..9ef6069064e 100644 --- a/packages/ui-patterns/src/Chart/charts/chart-line.tsx +++ b/packages/ui-patterns/src/Chart/charts/chart-line.tsx @@ -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(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)} > - + + + {keysToRender.map((key, index) => ( + + + + + ))} + {showGrid && } @@ -275,7 +306,6 @@ export const ChartLine = ({ /> } /> - {/* Selection highlight area */} {showHighlightActions && ( ))} - {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 ( - 1 ? `stack-${key}` : undefined} - /> - ) - })} + {keysToRender.map((key, index) => ( + 1 ? `stack-${key}` : undefined} + /> + ))} - {xKey === 'timestamp' && data && data.length > 0 && ( -
+ {hasDateRangeFooter && ( +
{dayjs(data[0][xKey]).format(DateTimeFormat)} {dayjs(data[data.length - 1]?.[xKey]).format(DateTimeFormat)}
diff --git a/packages/ui-patterns/src/LogsBarChart/index.tsx b/packages/ui-patterns/src/LogsBarChart/index.tsx index c22c003eda2..3d05eaed38e 100644 --- a/packages/ui-patterns/src/LogsBarChart/index.tsx +++ b/packages/ui-patterns/src/LogsBarChart/index.tsx @@ -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 = {