Chart
Dostępne wykresy dopasowane do motywu, zbudowane na Recharts — ze sprawdzoną paletą kategorialną, stonowaną oprawą i podpowiedziami spójnymi z systemem.
"use client"
import { Area, AreaChart } from "recharts"
import { ChartContainer, type ChartConfig } from "@/components/ui/chart"
const data = [
{ day: "pon", visitors: 3120 },
{ day: "wt", visitors: 3480 },
{ day: "śr", visitors: 3290 },
{ day: "czw", visitors: 4010 },
{ day: "pt", visitors: 4620 },
{ day: "sob", visitors: 3890 },
{ day: "niedz", visitors: 4980 },
]
const config = {
visitors: { label: "Odwiedzający", color: "var(--chart-1)" },
} satisfies ChartConfig
export default function ChartDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 shadow-raised">
<div className="flex flex-col gap-1">
<span className="text-body-xs text-muted-foreground">
Odwiedzający w tym tygodniu
</span>
<span className="text-heading-lg">27 390</span>
</div>
<ChartContainer config={config} className="aspect-auto h-20 w-full">
<AreaChart
data={data}
margin={{ top: 4, right: 4, bottom: 4, left: 4 }}
>
<Area
dataKey="visitors"
type="monotone"
stroke="var(--color-visitors)"
strokeWidth={2}
fill="var(--color-visitors)"
fillOpacity={0.1}
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>
</div>
)
}Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/chart.jsonUżycie
Wykres to Recharts wewnątrz ChartContainer. Kontener przyjmuje config, który nazywa każdą serię i przypisuje jej kolor; kolor każdej serii staje się zmienną CSS --color-<key> o zasięgu ograniczonym do tego wykresu.
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const config = {
visitors: { label: "Visitors", color: "var(--chart-1)" },
} satisfies ChartConfig
<ChartContainer config={config} className="aspect-auto h-64 w-full">
<AreaChart data={data}>
<CartesianGrid vertical={false} />
<XAxis dataKey="day" tickLine={false} axisLine={false} />
<ChartTooltip content={<ChartTooltipContent indicator="line" />} />
<Area
dataKey="visitors"
stroke="var(--color-visitors)"
strokeWidth={2}
fill="var(--color-visitors)"
fillOpacity={0.1}
/>
</AreaChart>
</ChartContainer>Nadaj kontenerowi jawną wysokość, która mieści etykiety osi — aspect-auto h-64 — żeby osie nigdy nie zostały przycięte ani nie trafiły do zagnieżdżonego obszaru przewijania.
Przykłady
Warstwowy
Jedna seria opowiada jedną historię, więc nie potrzebuje legendy — nazywa ją tytuł. Narysuj linię o grubości 2 px, a pod nią wypełnienie tym samym kolorem z kryciem 10% — spokojne tło, nigdy pełna plama koloru.
"use client"
import { TrendingUpIcon } from "lucide-react"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const data = [
{ month: "paź", requests: 1.82 },
{ month: "lis", requests: 1.94 },
{ month: "gru", requests: 2.11 },
{ month: "sty", requests: 2.03 },
{ month: "lut", requests: 2.38 },
{ month: "mar", requests: 2.56 },
{ month: "kwi", requests: 2.49 },
{ month: "maj", requests: 2.81 },
{ month: "cze", requests: 3.07 },
{ month: "lip", requests: 3.02 },
{ month: "sie", requests: 3.36 },
{ month: "wrz", requests: 3.78 },
]
const config = {
requests: { label: "Żądania (mln)", color: "var(--chart-1)" },
} satisfies ChartConfig
export default function ChartArea() {
return (
<Card className="w-full max-w-2xl">
<CardHeader>
<CardTitle>Żądania API</CardTitle>
<CardDescription>
Liczba żądań miesięcznie, w milionach, z ostatnich 12 miesięcy.
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={config} className="aspect-auto h-64 w-full">
<AreaChart data={data} margin={{ left: 0, right: 8, top: 8 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
width={48}
domain={[0, 4]}
ticks={[0, 1, 2, 3, 4]}
tickFormatter={(value: number) => `${value} mln`}
/>
<ChartTooltip
cursor
content={<ChartTooltipContent indicator="line" />}
/>
<Area
dataKey="requests"
type="monotone"
stroke="var(--color-requests)"
strokeWidth={2}
fill="var(--color-requests)"
fillOpacity={0.1}
activeDot={{ r: 4, strokeWidth: 2, stroke: "var(--card)" }}
/>
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter className="gap-2 text-sm">
<TrendingUpIcon className="size-4 text-success-11" aria-hidden="true" />
<span>
<span className="font-medium">Wzrost o 108%</span>{" "}
<span className="text-muted-foreground">rok do roku.</span>
</span>
</CardFooter>
</Card>
)
}Liniowy
Przy dwóch lub więcej seriach legenda jest zawsze widoczna. Podpisz koniec każdej linii jej ostatnią wartością, zamiast umieszczać liczbę przy każdym punkcie.
"use client"
import {
CartesianGrid,
LabelList,
Line,
LineChart,
XAxis,
YAxis,
} from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const data = [
{ week: "tydz. 1", "web": 4210, "ios": 2680, "android": 1940 },
{ week: "tydz. 2", "web": 4390, "ios": 2810, "android": 2120 },
{ week: "tydz. 3", "web": 4280, "ios": 3050, "android": 2260 },
{ week: "tydz. 4", "web": 4620, "ios": 3210, "android": 2410 },
{ week: "tydz. 5", "web": 4810, "ios": 3390, "android": 2690 },
{ week: "tydz. 6", "web": 4760, "ios": 3620, "android": 2880 },
{ week: "tydz. 7", "web": 5040, "ios": 3890, "android": 3170 },
{ week: "tydz. 8", "web": 5220, "ios": 4010, "android": 3420 },
]
const config = {
"web": { label: "Web", color: "var(--chart-1)" },
"ios": { label: "iOS", color: "var(--chart-2)" },
"android": { label: "Android", color: "var(--chart-3)" },
} satisfies ChartConfig
const number = new Intl.NumberFormat("pl-PL")
/** Labels only the last point of each line — selective, never every point. */
function EndLabel(props: {
x?: number | string
y?: number | string
index?: number
value?: number | string
}) {
if (props.index !== data.length - 1) return null
return (
<text
x={Number(props.x) + 8}
y={Number(props.y)}
dy="0.32em"
className="fill-foreground text-xs font-medium"
>
{number.format(Number(props.value))}
</text>
)
}
export default function ChartLine() {
return (
<div className="flex w-full max-w-2xl flex-col gap-2">
<div className="flex flex-col gap-1">
<span className="text-heading-sm">Aktywni użytkownicy w tygodniu według platformy</span>
<span className="text-body-xs text-muted-foreground">Ostatnie 8 tygodni</span>
</div>
<ChartContainer config={config} className="aspect-auto h-72 w-full">
<LineChart data={data} margin={{ left: 0, right: 48, top: 12 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="week"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
width={52}
tickFormatter={(value: number) => number.format(value)}
/>
<ChartTooltip
cursor
content={<ChartTooltipContent indicator="line" />}
/>
<ChartLegend verticalAlign="top" content={<ChartLegendContent />} />
{(["web", "ios", "android"] as const).map((key) => (
<Line
key={key}
dataKey={key}
type="monotone"
stroke={`var(--color-${key})`}
strokeWidth={2}
strokeLinecap="round"
dot={false}
activeDot={{ r: 4, strokeWidth: 2, stroke: "var(--background)" }}
>
<LabelList dataKey={key} content={<EndLabel />} />
</Line>
))}
</LineChart>
</ChartContainer>
</div>
)
}Słupkowy
Słupki mają najwyżej 24 px grubości, wyrastają z jednej linii bazowej i są zaokrąglone tylko na końcu z danymi (4 px). Stykające się słupki rozdziela przerwa 2 px.
"use client"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const data = [
{ month: "kwi", signups: 1840, activated: 1120 },
{ month: "maj", signups: 2130, activated: 1390 },
{ month: "cze", signups: 1960, activated: 1310 },
{ month: "lip", signups: 2480, activated: 1720 },
{ month: "sie", signups: 2710, activated: 1960 },
{ month: "wrz", signups: 3020, activated: 2290 },
]
const config = {
signups: { label: "Rejestracje", color: "var(--chart-1)" },
activated: { label: "Aktywacje", color: "var(--chart-2)" },
} satisfies ChartConfig
const number = new Intl.NumberFormat("pl-PL")
export default function ChartBar() {
return (
<div className="flex w-full max-w-2xl flex-col gap-2">
<div className="flex flex-col gap-1">
<span className="text-heading-sm">Rejestracje i aktywacje</span>
<span className="text-body-xs text-muted-foreground">
Konta miesięcznie, 2026
</span>
</div>
<ChartContainer config={config} className="aspect-auto h-72 w-full">
<BarChart
data={data}
barGap={2}
barCategoryGap="28%"
margin={{ left: 0, right: 8, top: 12 }}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
width={52}
tickFormatter={(value: number) => number.format(value)}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend verticalAlign="top" content={<ChartLegendContent />} />
<Bar
dataKey="signups"
fill="var(--color-signups)"
radius={[4, 4, 0, 0]}
maxBarSize={24}
/>
<Bar
dataKey="activated"
fill="var(--color-activated)"
radius={[4, 4, 0, 0]}
maxBarSize={24}
/>
</BarChart>
</ChartContainer>
</div>
)
}Skumulowany
Do porównań części z całością w wielu kategoriach używaj poziomych słupków skumulowanych. Segmenty rozdziela 2-pikselowa linia w kolorze powierzchni — oddziela je przerwa, a nie obrys.
"use client"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const data = [
{ team: "Platforma", "production": 3120, "preview": 1840, "development": 620 },
{ team: "Web", "production": 2410, "preview": 2260, "development": 910 },
{ team: "Mobile", "production": 1680, "preview": 1190, "development": 1340 },
{ team: "Dane", "production": 2890, "preview": 640, "development": 480 },
{ team: "Growth", "production": 1240, "preview": 1580, "development": 390 },
]
const config = {
"production": { label: "Produkcyjne", color: "var(--chart-1)" },
"preview": { label: "Podglądowe", color: "var(--chart-2)" },
"development": { label: "Deweloperskie", color: "var(--chart-3)" },
} satisfies ChartConfig
const number = new Intl.NumberFormat("pl-PL")
const keys = ["production", "preview", "development"] as const
export default function ChartStacked() {
return (
<div className="flex w-full max-w-2xl flex-col gap-2">
<div className="flex flex-col gap-1">
<span className="text-heading-sm">Minuty kompilacji według środowiska</span>
<span className="text-body-xs text-muted-foreground">
Wrzesień 2026, w podziale na zespoły
</span>
</div>
<ChartContainer config={config} className="aspect-auto h-72 w-full">
<BarChart
data={data}
layout="vertical"
margin={{ left: 0, right: 12, top: 12 }}
>
<CartesianGrid horizontal={false} />
<YAxis
dataKey="team"
type="category"
tickLine={false}
axisLine={false}
tickMargin={8}
width={68}
/>
<XAxis
type="number"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value: number) => number.format(value)}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend verticalAlign="top" content={<ChartLegendContent />} />
{keys.map((key, index) => (
<Bar
key={key}
dataKey={key}
stackId="minutes"
fill={`var(--color-${key})`}
// The surface-colored stroke is the 2px gap between segments.
stroke="var(--background)"
strokeWidth={2}
maxBarSize={24}
radius={index === keys.length - 1 ? [0, 4, 4, 0] : 0}
/>
))}
</BarChart>
</ChartContainer>
</div>
)
}Pierścieniowy
Wykres pierścieniowy na pierwszy rzut oka pokazuje udział części w całości — przy najwyżej sześciu segmentach. Do porównywania zbliżonych wartości użyj wykresu słupkowego — kąty trudno porównywać.
"use client"
import { Label, Pie, PieChart } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const data = [
{ source: "organic", "visits": 18420, fill: "var(--color-organic)" },
{ source: "direct", "visits": 11260, fill: "var(--color-direct)" },
{ source: "referral", "visits": 6890, fill: "var(--color-referral)" },
{ source: "social", "visits": 4310, fill: "var(--color-social)" },
]
const config = {
"visits": { label: "Wizyty" },
organic: { label: "Wyszukiwanie organiczne", color: "var(--chart-1)" },
direct: { label: "Bezpośrednie", color: "var(--chart-2)" },
referral: { label: "Witryny odsyłające", color: "var(--chart-3)" },
social: { label: "Media społecznościowe", color: "var(--chart-4)" },
} satisfies ChartConfig
const total = data.reduce((sum, item) => sum + item.visits, 0)
const totalLabel = "Wizyty łącznie"
export default function ChartDonut() {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-2">
<span className="text-heading-sm">Ruch według źródła</span>
<ChartContainer config={config} className="aspect-square h-72 w-full">
<PieChart>
<ChartTooltip
content={<ChartTooltipContent nameKey="source" hideLabel />}
/>
<Pie
data={data}
dataKey="visits"
nameKey="source"
innerRadius="58%"
outerRadius="82%"
stroke="var(--background)"
strokeWidth={2}
>
<Label
content={({ viewBox }) => {
if (!viewBox || !("cx" in viewBox)) return null
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-2xl font-semibold"
>
{total.toLocaleString("pl-PL")}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy ?? 0) + 22}
className="fill-muted-foreground text-xs"
>
{totalLabel}
</tspan>
</text>
)
}}
/>
</Pie>
<ChartLegend
content={
<ChartLegendContent nameKey="source" className="flex-wrap" />
}
/>
</PieChart>
</ChartContainer>
</div>
)
}Statystyka z wykresem przebiegu
Często najlepszym wykresem jest po prostu liczba. Kafelek statystyki zaczyna od wartości, podaje zmianę względem nazwanego okresu i dodaje 12-punktowy wykres przebiegu (sparkline): historię w spokojnej szarości, bieżący okres w kolorze akcentu. Pokoloruj zmianę zależnie od tego, czy to dobra wiadomość, i dodaj do niej strzałkę, żeby kierunek nigdy nie zależał od koloru.
"use client"
import { ArrowDownRightIcon, ArrowUpRightIcon } from "lucide-react"
import { Line, LineChart, YAxis } from "recharts"
import { ChartContainer, type ChartConfig } from "@/components/ui/chart"
import { cn } from "@/lib/utils"
type Stat = {
label: string
value: string
delta: string
direction: "up" | "down"
/** Whether the direction is good news — drives the delta color. */
good: boolean
trend: number[]
}
const stats: Stat[] = [
{
label: "Przychody cykliczne",
value: "84 320 $",
delta: "+8,2%",
direction: "up",
good: true,
trend: [61, 63, 62, 66, 68, 67, 71, 73, 72, 77, 80, 84],
},
{
label: "Opóźnienie p95",
value: "182 ms",
delta: "−24 ms",
direction: "down",
good: true,
trend: [240, 236, 228, 231, 219, 214, 209, 206, 198, 195, 190, 182],
},
{
label: "Odsetek błędów",
value: "0,42%",
delta: "+0,09 p.p.",
direction: "up",
good: false,
trend: [
0.3, 0.31, 0.29, 0.33, 0.32, 0.35, 0.34, 0.36, 0.33, 0.38, 0.4, 0.42,
],
},
]
const config = {
value: { label: "Wartość", color: "var(--gray-8)" },
} satisfies ChartConfig
function Sparkline({ trend }: { trend: number[] }) {
const data = trend.map((value, index) => ({ index, value }))
const last = data.length - 1
return (
<ChartContainer
config={config}
className="aspect-auto h-10 w-full"
aria-hidden="true"
>
{/* Decorative: hidden from assistive technology, so it mustn't take focus either. */}
<LineChart
data={data}
margin={{ top: 5, right: 5, bottom: 5, left: 5 }}
accessibilityLayer={false}
>
{/* A sparkline shows shape, not magnitude: fit the range, not zero. */}
<YAxis hide domain={["dataMin", "dataMax"]} />
<Line
dataKey="value"
type="monotone"
stroke="var(--color-value)"
strokeWidth={2}
strokeLinecap="round"
isAnimationActive={false}
dot={(props: { cx?: number; cy?: number; index?: number }) =>
props.index === last ? (
<circle
key="current"
cx={props.cx}
cy={props.cy}
r={4}
fill="var(--chart-1)"
stroke="var(--card)"
strokeWidth={2}
/>
) : (
<g key={props.index} />
)
}
/>
</LineChart>
</ChartContainer>
)
}
export default function ChartStat() {
return (
<div className="grid w-full max-w-3xl gap-4 sm:grid-cols-3">
{stats.map((stat) => {
const Icon =
stat.direction === "up" ? ArrowUpRightIcon : ArrowDownRightIcon
return (
<div
key={stat.label}
className="flex flex-col gap-2 rounded-xl border bg-card p-4 shadow-raised"
>
<span className="truncate text-body-xs text-muted-foreground">
{stat.label}
</span>
{/* Proportional figures: tabular digits look loose at display size. */}
<span className="text-heading-lg whitespace-nowrap">
{stat.value}
</span>
<span
className={cn(
"flex items-center gap-1 text-xs font-medium whitespace-nowrap",
stat.good ? "text-success-11" : "text-danger-11"
)}
>
<Icon className="size-3.5" aria-hidden="true" />
{stat.delta}
<span className="font-normal text-muted-foreground">
wobec sierpnia
</span>
</span>
<Sparkline trend={stat.trend} />
</div>
)
})}
</div>
)
}Wyróżnienie
Gdy chodzi o jedną serię, powiedz to wprost: pokaż ją w kolorze akcentu, a całą resztę na szaro. To najbardziej niedoceniana forma wykresu i często uczciwa odpowiedź na prośbę „zrób ten wykres czytelniejszym”.
"use client"
import { Bar, BarChart, Cell, LabelList, XAxis } from "recharts"
import { ChartContainer, type ChartConfig } from "@/components/ui/chart"
const data = [
{ region: "Frankfurt", ms: 38 },
{ region: "Londyn", ms: 41 },
{ region: "Wirginia", ms: 92 },
{ region: "São Paulo", ms: 141 },
{ region: "Singapur", ms: 188 },
{ region: "Sydney", ms: 214 },
]
const highlight = "Sydney"
const config = {
ms: { label: "Mediana opóźnienia (ms)", color: "var(--gray-8)" },
} satisfies ChartConfig
export default function ChartEmphasis() {
return (
<div className="flex w-full max-w-2xl flex-col gap-2">
<div className="flex flex-col gap-1">
<span className="text-heading-sm">
W Sydney czeka się 5,6× dłużej niż we Frankfurcie
</span>
<span className="text-body-xs text-muted-foreground">
Mediana opóźnienia z Krakowa, w milisekundach
</span>
</div>
<ChartContainer config={config} className="aspect-auto h-64 w-full">
<BarChart data={data} margin={{ top: 24, left: 8, right: 8 }}>
<XAxis
dataKey="region"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<Bar
dataKey="ms"
radius={[4, 4, 0, 0]}
maxBarSize={24}
isAnimationActive={false}
>
{data.map((item) => (
<Cell
key={item.region}
fill={
item.region === highlight
? "var(--chart-1)"
: "var(--color-ms)"
}
/>
))}
<LabelList
dataKey="ms"
position="top"
offset={8}
className="fill-muted-foreground text-xs"
/>
</Bar>
</BarChart>
</ChartContainer>
</div>
)
}Z widokiem tabeli
Każdy wykres powinien mieć bliźniaczą tabelę. To najbardziej dostępna postać danych, to z niej kopiuje się liczby, a wartości można w niej odczytać bez najeżdżania kursorem.
"use client"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
const data = [
{ quarter: "Q4 2025", tickets: 1284 },
{ quarter: "Q1 2026", tickets: 1172 },
{ quarter: "Q2 2026", tickets: 968 },
{ quarter: "Q3 2026", tickets: 811 },
]
const config = {
tickets: { label: "Zgłoszenia do pomocy technicznej", color: "var(--chart-1)" },
} satisfies ChartConfig
const number = new Intl.NumberFormat("pl-PL")
export default function ChartWithTable() {
return (
<Tabs defaultValue="chart" className="w-full max-w-xl">
<div className="flex items-center justify-between gap-4">
<span className="text-heading-sm">Zgłoszenia do pomocy technicznej na kwartał</span>
<TabsList>
<TabsTrigger value="chart">Wykres</TabsTrigger>
<TabsTrigger value="table">Tabela</TabsTrigger>
</TabsList>
</div>
<TabsContent value="chart">
<ChartContainer config={config} className="aspect-auto h-56 w-full">
<BarChart data={data} margin={{ left: 0, right: 8, top: 8 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="quarter"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
width={52}
tickFormatter={(value: number) => number.format(value)}
/>
<ChartTooltip content={<ChartTooltipContent hideIndicator />} />
<Bar
dataKey="tickets"
fill="var(--color-tickets)"
radius={[4, 4, 0, 0]}
maxBarSize={24}
/>
</BarChart>
</ChartContainer>
</TabsContent>
<TabsContent value="table">
<div className="rounded-xl border">
<Table>
<TableHeader>
<TableRow>
<TableHead scope="col">Kwartał</TableHead>
<TableHead scope="col" className="text-right">
Zgłoszenia
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row) => (
<TableRow key={row.quarter}>
<TableCell>{row.quarter}</TableCell>
<TableCell className="text-right">
{number.format(row.tickets)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TabsContent>
</Tabs>
)
}Kolor
O kolorze decydujesz na końcu: najpierw wybierz formę, a potem daj każdemu kolorowi dokładnie jedno zadanie.
Paleta kategorialna
chart-1chart-2chart-3chart-4chart-5Kolejność — kolor marki, morski, fiolet, pomarańcz, róż — nie wynika z estetyki. Wybrano ją, sprawdzając wszystkie możliwe kolejności i zostawiając tę, w której różnica kolorów między sąsiadami jest największa przy symulowanej protanopii i deuteranopii. Cztery barwy mieszczą się w czytelnym zakresie jasności i przekraczają minimalne nasycenie; pierwsza seria ma kolor marki, domyślnie czerń lub biel, i odróżnia się od nich jasnością. Każda seria zachowuje kontrast co najmniej 3:1 z powierzchnią wykresu, w obu trybach. Wartości z tabeli przelicza z tokenów test tests/tokens.test.ts:
Wynikają z tego zasady:
- Przypisuj po kolei, bez pomijania i bez zapętlania. Pierwsza seria to zawsze
chart-1. Szósta seria trafia do kategorii Inne, na osobny mały wykres (small multiples) albo do tabeli — nigdy nie dostaje wygenerowanego szóstego odcienia. - Kolor należy do obiektu, a nie do jego pozycji. Gdy filtr usuwa serię, pozostałe zachowują swoje kolory.
- Wykresy punktowe i mapy mają najwyżej trzy serie — tam dowolne dwa kolory mogą znaleźć się obok siebie.
- Kolory statusów są zarezerwowane. Zieleń i czerwień oznaczają w całym prfct „dobrze” i „źle”; nigdy nie używaj ich dla „serii 4”.
Wytyczne
Najpierw wybierz formę
Stonowana oprawa
Tylko dane mają prawo być głośne. Linie siatki są ciągłe i cienkie, o jeden stopień skali różne od powierzchni — nigdy przerywane. Etykiety osi używają muted-foreground. Usuń linie osi i kreski podziałki, a wartości na osiach niech będą okrągłymi liczbami z separatorem tysięcy.
Jedna oś
Nigdy nie pokazuj dwóch miar na dwóch osiach Y: wzajemne dopasowanie skal jest arbitralne, więc wykres zmyśla korelacje. Użyj dwóch wykresów, małych wykresów obok siebie albo przelicz obie serie na indeks równy 100 w punkcie startowym.
Etykiety i legendy
- Przy dwóch lub więcej seriach legenda jest zawsze widoczna; pojedynczą serię nazywa tytuł.
- Podpisuj wybiórczo — ostatnią wartość, wartość skrajną, tę jedną serię, o której opowiada wykres. Na wykresach liniowych i gęstych nigdy nie umieszczaj liczby przy każdym punkcie; kilka słupków bez osi wartości może mieć podpisaną każdą wartość, jak w przykładzie Wyróżnienie.
- Tekst używa tokenów tekstu. Wartości, etykiety i legendy pozostają w
foregroundlubmuted-foreground; o tożsamości serii mówi kolorowy znacznik obok nich.
Dostępność
- Klawiatura. Warstwa dostępności Recharts jest domyślnie włączona: przejdź na wykres klawiszem Tab, co rysuje wokół niego pierścień fokusu, i poruszaj się między punktami danych strzałkami; podpowiedź podąża za fokusem dokładnie tak jak za kursorem.
- Alternatywa tekstowa. Dołącz do każdego wykresu widok tabeli albo opisz główny wniosek w tytule i podpisie — „Liczba rejestracji wzrosła o 64% od kwietnia” mówi więcej niż „Rejestracje według miesięcy”.
- Nigdy sam kolor. Serie identyfikują legendy i bezpośrednie etykiety; zmiany statusu łączą kolor z ikoną i słowami.
- Kontrast. Każdy kolor kategorialny zachowuje w obu trybach kontrast co najmniej 3:1 z powierzchnią wykresu.
- Ruch. Animacje Recharts mają domyślnie wartość
"auto", która respektujeprefers-reduced-motion. Nie zmieniaj tego.
| Klawisz | Działanie |
|---|---|
Tab | Przenosi fokus na wykres. |
←→ | Przechodzi między punktami danych i aktualizuje podpowiedź. |
Dokumentacja API
ChartContainer
Opakowuje wykres Recharts, ogranicza zasięg jego kolorów i sprawia, że jest responsywny. Przyjmuje wszystkie propsy div.
ChartConfig
type ChartConfig = Record<
string,
{
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string } // one color for both modes
| { theme: { light: string; dark: string } } // a color per mode
)
>ChartTooltipContent
Używaj w ten sposób: <ChartTooltip content={<ChartTooltipContent />} />.
ChartLegendContent
Używaj w ten sposób: <ChartLegend content={<ChartLegendContent />} />. ChartLegend wymienia serie w kolejności kluczy w config, czyli w tej samej, w której przypisujesz kolory; samo Recharts posortowałoby je alfabetycznie. Inną kolejność ustawisz propem itemSorter.
ChartStyle
Wstrzykiwany przez ChartContainer; eksportowany na potrzeby własnych kontenerów, które potrzebują tych samych zmiennych kolorów o ograniczonym zasięgu.