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
@@ -49,27 +49,7 @@ We do not wrap Recharts. This means you're not locked into an abstraction. When
|
||||
|
||||
</Callout>
|
||||
|
||||
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)
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>Define your colors in your css file</Step>
|
||||
<Step>Pick a slot from the shared palette</Step>
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
<Callout className="mt-4">
|
||||
|
||||
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.
|
||||
|
||||
</Callout>
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
<ComponentPreview name="chart-palette" wide />
|
||||
|
||||
Every slot stacked together, to check adjacent segments stay separable in both themes.
|
||||
|
||||
<ComponentPreview name="chart-palette-stress" wide />
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Chart Types
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col gap-6 w-8/12">
|
||||
<Chart>
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="Every categorical slot on screen at once">
|
||||
Client connections by role
|
||||
</ChartTitle>
|
||||
</ChartHeader>
|
||||
<ChartContent>
|
||||
<div className="h-40">
|
||||
<ChartBar
|
||||
data={data}
|
||||
dataKey={SERIES[0].key}
|
||||
dataKeys={SERIES.map((s) => s.key)}
|
||||
config={config}
|
||||
isStacked
|
||||
isFullHeight
|
||||
showGrid
|
||||
showYAxis
|
||||
YAxisProps={{ width: 36 }}
|
||||
/>
|
||||
</div>
|
||||
</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||
<div
|
||||
className="border-default h-12 w-full rounded-md border"
|
||||
style={{ background: `var(${token})` }}
|
||||
/>
|
||||
<span className="text-foreground-lighter text-xs">{label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TokenCard({
|
||||
title,
|
||||
token,
|
||||
note,
|
||||
children,
|
||||
}: {
|
||||
title: ReactNode
|
||||
token: string
|
||||
note?: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="border-default bg-surface-100 flex flex-col gap-4 rounded-lg border p-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-foreground text-sm">{title}</div>
|
||||
<code className="text-foreground-lighter break-all font-mono text-xs">{token}</code>
|
||||
</div>
|
||||
<div className="flex gap-3">{children}</div>
|
||||
{note && <p className="text-foreground-lighter text-xs">{note}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Section({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
children: ReactNode
|
||||
className: string
|
||||
}) {
|
||||
return (
|
||||
<section className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-foreground text-sm">{title}</h3>
|
||||
<p className="text-foreground-lighter max-w-prose text-xs">{description}</p>
|
||||
</div>
|
||||
<div className={className}>{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ChartPalette() {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-10 p-6">
|
||||
<Section
|
||||
title="Categorical slots"
|
||||
description="Assigned in fixed order, never cycled. A ninth series folds into “Other”."
|
||||
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
|
||||
>
|
||||
{SLOTS.map((n) => (
|
||||
<TokenCard key={n} title={`Slot ${n}`} token={`--chart-${n}`}>
|
||||
<Swatch token={`--chart-${n}`} label="Stroke" />
|
||||
<Swatch token={`--chart-${n}-fill`} label="Fill" />
|
||||
</TokenCard>
|
||||
))}
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="Status"
|
||||
description="Reserved meaning. These point at the same tokens the rest of the UI uses and always ship with an icon or label, so state is never carried by color alone. Never assign one to a series."
|
||||
className="grid grid-cols-1 gap-4 sm:grid-cols-3"
|
||||
>
|
||||
{STATUS.map((d) => (
|
||||
<TokenCard
|
||||
key={d.name}
|
||||
title={d.name.replace('--chart-status-', '')}
|
||||
token={d.name}
|
||||
note={d.note}
|
||||
>
|
||||
<Swatch token={d.name} label="Base" />
|
||||
<Swatch token={d.muted} label="Muted" />
|
||||
</TokenCard>
|
||||
))}
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="Pinned pairs and rendering defaults"
|
||||
description="Chart authors do not pick these. Reference lines and headroom are applied by the chart."
|
||||
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
|
||||
>
|
||||
{DEFAULTS.map((d) => (
|
||||
<TokenCard
|
||||
key={d.name}
|
||||
title={d.name.replace('--chart-', '')}
|
||||
token={d.name}
|
||||
note={d.note}
|
||||
>
|
||||
<Swatch token={d.name} label="Color" />
|
||||
</TokenCard>
|
||||
))}
|
||||
</Section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -32,8 +32,8 @@ export default function Component() {
|
||||
<TooltipDemo
|
||||
label="Page Views"
|
||||
payload={[
|
||||
{ name: 'Desktop', value: 186, fill: 'hsl(var(--chart-1))' },
|
||||
{ name: 'Mobile', value: 80, fill: 'hsl(var(--chart-2))' },
|
||||
{ name: 'Desktop', value: 186, fill: 'var(--chart-1)' },
|
||||
{ name: 'Mobile', value: 80, fill: 'var(--chart-2)' },
|
||||
]}
|
||||
className="w-32"
|
||||
/>
|
||||
@@ -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() {
|
||||
<div className="hidden! md:flex!">
|
||||
<TooltipDemo
|
||||
label="Page Views"
|
||||
payload={[{ name: 'Desktop', value: 12486, fill: 'hsl(var(--chart-3))' }]}
|
||||
payload={[{ name: 'Desktop', value: 12486, fill: 'var(--chart-3)' }]}
|
||||
className="w-36"
|
||||
indicator="line"
|
||||
/>
|
||||
@@ -84,7 +84,7 @@ export default function Component() {
|
||||
<TooltipDemo
|
||||
label="Browser"
|
||||
hideLabel
|
||||
payload={[{ name: 'Chrome', value: 1286, fill: 'hsl(var(--chart-1))' }]}
|
||||
payload={[{ name: 'Chrome', value: 1286, fill: 'var(--chart-1)' }]}
|
||||
indicator="dot"
|
||||
className="w-32"
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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%);
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ export const CHART_TABS = [
|
||||
|
||||
export const LEGEND_ITEMS: Record<string, { label: string; color: string; dataKey: string }[]> = {
|
||||
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<string, { label: string; color: string; dataKe
|
||||
|
||||
export const CHART_TYPE = 'linear'
|
||||
|
||||
export const SEL_COLOR = 'hsl(var(--chart-blue))'
|
||||
export const SEL_COLOR = 'var(--chart-2)'
|
||||
@@ -156,15 +156,15 @@ export const ChartBlock = ({
|
||||
if (!threshold) return 'var(--chart-1)'
|
||||
if (threshold.check === 'gt') {
|
||||
return value >= 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)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +77,11 @@ import { useShortcut } from '@/state/shortcuts/useShortcut'
|
||||
export const CHART_CONFIG = {
|
||||
success: {
|
||||
label: <TooltipLabel level="success" />,
|
||||
color: 'var(--chart-success)',
|
||||
color: 'var(--chart-muted)',
|
||||
},
|
||||
warning: {
|
||||
label: <TooltipLabel level="warning" />,
|
||||
color: 'var(--chart-warning)',
|
||||
color: 'var(--chart-status-warning)',
|
||||
},
|
||||
error: {
|
||||
label: <TooltipLabel level="error" />,
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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') &&
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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%);
|
||||
|
||||
@@ -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