mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## 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>
164 lines
5.6 KiB
TypeScript
164 lines
5.6 KiB
TypeScript
import { useMemo } from 'react'
|
|
import { cn, type ChartConfig as ChartSeriesConfig } from 'ui'
|
|
import { Chart, ChartBar, ChartCard, ChartContent, ChartLine } from 'ui-patterns/Chart'
|
|
|
|
import { type QueryResult } from '../types'
|
|
import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder'
|
|
import {
|
|
computeYAxisWidth,
|
|
formatLogTick,
|
|
formatYAxisTick,
|
|
getCumulativeResults,
|
|
} from '@/components/ui/QueryBlock/QueryBlock.utils'
|
|
import { type ChartConfig } from '@/data/content/notebooks/notebook-schema'
|
|
|
|
interface QueryResultChartProps {
|
|
chart?: ChartConfig
|
|
result?: QueryResult
|
|
}
|
|
|
|
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
|
|
if (value === null || value === undefined) return ''
|
|
return String(value)
|
|
}
|
|
|
|
/**
|
|
* Y series are keyed by position rather than by the column name they came from.
|
|
*
|
|
* `ChartContainer` writes every config key into a `<style>` element as
|
|
* `--color-<key>`, and rrweb records `<style>` text verbatim: its text-node serializer
|
|
* skips masking whenever the parent is a `STYLE` element, so neither `maskTextFn` nor
|
|
* `maskAttributeFn` sees it. Keying by column name would therefore put the customer's own
|
|
* column names into a recording. The name still reaches the chart as `label`, which
|
|
* renders as text and is masked.
|
|
*
|
|
* The X column keeps its own name. Only config keys reach the `<style>`, and the X column
|
|
* is never a config key, so renaming it buys nothing. It also costs something: ChartBar
|
|
* and ChartLine branch on `xKey === 'timestamp'` to format tooltip dates and render the
|
|
* date-range footer.
|
|
*/
|
|
const seriesKeyFor = (index: number) => `series_${index}`
|
|
|
|
/**
|
|
* Guards the one collision the positional keys introduce: an X column literally named
|
|
* `series_0`. Appends underscores until the X key is distinct from every series key.
|
|
*/
|
|
export function xKeyFor(xColumn: string, seriesKeys: string[]): string {
|
|
let key = xColumn
|
|
while (seriesKeys.includes(key)) key = `${key}_`
|
|
return key
|
|
}
|
|
|
|
export const QueryResultChart = ({ chart, result }: QueryResultChartProps) => {
|
|
const { type, x_column, y_series = [], cumulative, show_labels, scale } = chart ?? {}
|
|
|
|
const hasConfig = !!x_column && y_series.length > 0
|
|
// Logarithmic scale only makes sense for a single series — DisplaySettingsButton
|
|
// resets `scale` to linear once a second Y column is added
|
|
const effectiveScale = y_series.length > 1 ? 'linear' : scale
|
|
|
|
const seriesKeys = useMemo(() => y_series.map((_, index) => seriesKeyFor(index)), [y_series])
|
|
|
|
const chartConfig: ChartSeriesConfig = useMemo(
|
|
() =>
|
|
y_series.reduce((acc, column, index) => {
|
|
acc[seriesKeyFor(index)] = { label: column, color: Y_SERIES_COLORS[index] }
|
|
return acc
|
|
}, {} as ChartSeriesConfig),
|
|
[y_series]
|
|
)
|
|
|
|
const xKey = useMemo(() => xKeyFor(x_column ?? '', seriesKeys), [x_column, seriesKeys])
|
|
|
|
const chartRows = useMemo(() => {
|
|
const sourceColumn = x_column ?? ''
|
|
return (result?.rows ?? []).map((row) => {
|
|
const chartRow: Record<string, string | number> = { [xKey]: toChartValue(row[sourceColumn]) }
|
|
y_series.forEach((column, index) => {
|
|
chartRow[seriesKeyFor(index)] = toChartValue(row[column])
|
|
})
|
|
return chartRow
|
|
})
|
|
}, [result, x_column, xKey, y_series])
|
|
|
|
const cumulativeResults = useMemo(
|
|
() => getCumulativeResults({ rows: chartRows }, { yKey: seriesKeys }),
|
|
[chartRows, seriesKeys]
|
|
)
|
|
const resultToRender = cumulative ? cumulativeResults : chartRows
|
|
|
|
const yAxisWidth = Math.max(
|
|
36,
|
|
...seriesKeys.map((key) =>
|
|
computeYAxisWidth(resultToRender, key, { isLogScale: effectiveScale === 'log' })
|
|
)
|
|
)
|
|
|
|
const yAxisProps = {
|
|
...(show_labels ? { width: yAxisWidth } : {}),
|
|
scale: effectiveScale === 'log' ? 'log' : 'auto',
|
|
domain: effectiveScale === 'log' ? ([1, 'auto'] as const) : undefined,
|
|
tickFormatter: effectiveScale === 'log' ? formatLogTick : formatYAxisTick,
|
|
}
|
|
|
|
if (!result || (result?.rows && result.rows.length === 0)) {
|
|
return (
|
|
<NoDataPlaceholder
|
|
className="border-0 min-h-0! py-8"
|
|
size="normal"
|
|
message="No results"
|
|
description="Your query returned no rows"
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (!hasConfig) {
|
|
return (
|
|
<NoDataPlaceholder
|
|
className="border-0 min-h-0! py-8"
|
|
size="normal"
|
|
message="Configure your chart"
|
|
description="Select your X and Y axis in the display settings"
|
|
/>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Chart className="flex flex-grow min-h-0">
|
|
<ChartCard className="flex flex-grow rounded-none border-0 min-h-0">
|
|
<ChartContent className={cn('min-h-0 h-full w-full', show_labels && 'pl-2 pb-2')}>
|
|
{type === 'bar' && (
|
|
<ChartBar
|
|
isFullHeight
|
|
xKey={xKey}
|
|
dataKey={seriesKeys[0]}
|
|
dataKeys={seriesKeys}
|
|
config={chartConfig}
|
|
showXAxis={show_labels}
|
|
showYAxis={show_labels}
|
|
data={resultToRender}
|
|
YAxisProps={yAxisProps}
|
|
/>
|
|
)}
|
|
{type === 'line' && (
|
|
<ChartLine
|
|
isFullHeight
|
|
xKey={xKey}
|
|
dataKey={seriesKeys[0]}
|
|
dataKeys={seriesKeys}
|
|
config={chartConfig}
|
|
showXAxis={show_labels}
|
|
showYAxis={show_labels}
|
|
data={resultToRender}
|
|
YAxisProps={yAxisProps}
|
|
/>
|
|
)}
|
|
</ChartContent>
|
|
</ChartCard>
|
|
</Chart>
|
|
)
|
|
}
|