Charts
Charts built on Recharts — ready-made AreaChart, BarChart, LineChart, DonutChart and Sparkline, plus primitives for your own charts.
- Tushum
- Xarajat
'use client';
import { AreaChart } from '@akhatmirzo/ui/charts';
const sales = [
{ month: 'Yan', tushum: 42_500_000, xarajat: 28_300_000 },
{ month: 'Fev', tushum: 38_900_000, xarajat: 26_100_000 },
{ month: 'Mar', tushum: 51_200_000, xarajat: 30_400_000 },
{ month: 'Apr', tushum: 47_800_000, xarajat: 31_900_000 },
{ month: 'May', tushum: 58_300_000, xarajat: 33_200_000 },
{ month: 'Iyn', tushum: 63_100_000, xarajat: 35_700_000 },
];
/** Qisqa ko'rinish: 42,5 mln so'm */
const short = (v: number) => `${(v / 1_000_000).toFixed(1).replace('.', ',')} mln so'm`;
export default function ChartsArea() {
return (
<AreaChart
aria-label="Oylik tushum va xarajat, yanvar–iyun"
className="w-full"
data={sales}
index="month"
series={[
{ key: 'tushum', label: 'Tushum' },
{ key: 'xarajat', label: 'Xarajat', color: 'amber' },
]}
valueFormat={short}
height={280}
/>
);
}
Installation
Charts live in a separate entry point — @akhatmirzo/ui/charts. They are built on
Recharts, which is an optional peer dependency: if you don't use
charts, you don't need to install it, and the main @akhatmirzo/ui import never pulls it in.
pnpm add rechartsImport
import { AreaChart, BarChart, LineChart, DonutChart, Sparkline } from '@akhatmirzo/ui/charts';When to use
- Change over time —
AreaChartorLineChart(monthly revenue, daily visits). - Comparing categories —
BarChart(regions, products). For long names useorientation="horizontal". - Parts of a whole —
DonutChart, with no more than 5–6 slices; group the rest into "Other". - A small trend —
Sparklinein KPI cards and table cells. - When exact values matter — show a table instead of, or next to, the chart (see Accessibility).
Ready-made charts
They all share the same props: data is a list of objects, index is the category field (x axis)
and series lists the numeric fields to draw. Colors come only from tokens, so charts adapt to dark
mode on their own.
AreaChart
The example above. stacked stacks the series on top of each other; curve is monotone
(default), linear or step.
BarChart
stacked puts the series into one bar, showing the total and its breakdown together.
- Onlayn
- Do'kondan
'use client';
import { formatNumber } from '@akhatmirzo/core';
import { BarChart } from '@akhatmirzo/ui/charts';
const orders = [
{ region: 'Toshkent', onlayn: 1240, dokon: 860 },
{ region: 'Samarqand', onlayn: 610, dokon: 540 },
{ region: "Farg'ona", onlayn: 480, dokon: 520 },
{ region: 'Buxoro', onlayn: 350, dokon: 290 },
{ region: 'Namangan', onlayn: 410, dokon: 380 },
];
export default function ChartsBar() {
return (
<BarChart
aria-label="Viloyatlar bo'yicha buyurtmalar: onlayn va do'kondan"
className="w-full"
data={orders}
index="region"
series={[
{ key: 'onlayn', label: 'Onlayn' },
{ key: 'dokon', label: "Do'kondan", color: 'sky' },
]}
valueFormat={(v) => `${formatNumber(v)} ta`}
stacked
/>
);
}
orientation="horizontal" draws horizontal bars — handy for long names and rankings. The prop is
named after what you see (vertical — upright columns) and deliberately differs from Recharts'
layout, where layout="vertical" means the opposite (horizontal bars).
'use client';
import { BarChart } from '@akhatmirzo/ui/charts';
const products = [
{ name: 'Smartfonlar', tushum: 184_000_000 },
{ name: 'Noutbuklar', tushum: 142_500_000 },
{ name: 'Quloqchinlar', tushum: 61_200_000 },
{ name: 'Aqlli soatlar', tushum: 48_900_000 },
{ name: 'Aksessuarlar', tushum: 23_400_000 },
];
const short = (v: number) => `${Math.round(v / 1_000_000)} mln so'm`;
export default function ChartsBarHorizontal() {
return (
<BarChart
aria-label="Eng ko'p sotilgan toifalar, tushum bo'yicha"
className="w-full"
data={products}
index="name"
series={[{ key: 'tushum', label: 'Tushum', color: 'emerald' }]}
valueFormat={short}
orientation="horizontal"
legend={false}
height={240}
/>
);
}
LineChart
For comparing several metrics. This one uses curve="linear".
- Yangi
- Qaytgan
'use client';
import { LineChart } from '@akhatmirzo/ui/charts';
const visits = [
{ day: 'Du', yangi: 320, qaytgan: 540 },
{ day: 'Se', yangi: 410, qaytgan: 580 },
{ day: 'Ch', yangi: 380, qaytgan: 610 },
{ day: 'Pa', yangi: 520, qaytgan: 640 },
{ day: 'Ju', yangi: 610, qaytgan: 700 },
{ day: 'Sh', yangi: 450, qaytgan: 520 },
{ day: 'Ya', yangi: 300, qaytgan: 410 },
];
export default function ChartsLine() {
return (
<LineChart
aria-label="Haftalik tashriflar: yangi va qaytgan foydalanuvchilar"
className="w-full"
data={visits}
index="day"
series={[
{ key: 'yangi', label: 'Yangi' },
{ key: 'qaytgan', label: 'Qaytgan', color: 'success' },
]}
curve="linear"
height={240}
/>
);
}
DonutChart
index is the slice name and value is the value field. The center shows centerLabel and the
total (formatted with valueFormat); override it with centerValue or hide it with null.
- Oziq-ovqat
- Transport
- Kommunal
- Ko'ngilochar
- Boshqalar
'use client';
import { formatMoney } from '@akhatmirzo/core';
import { DonutChart } from '@akhatmirzo/ui/charts';
const expenses = [
{ name: 'Oziq-ovqat', total: 3_850_000 },
{ name: 'Transport', total: 1_240_000 },
{ name: 'Kommunal', total: 960_000 },
{ name: "Ko'ngilochar", total: 720_000 },
{ name: 'Boshqalar', total: 430_000 },
];
const total = expenses.reduce((sum, e) => sum + e.total, 0);
export default function ChartsDonut() {
return (
<DonutChart
aria-label="Oktabr xarajatlari toifalar bo'yicha"
className="w-full max-w-sm"
data={expenses}
index="name"
value="total"
valueFormat={(v) => formatMoney(v)}
centerLabel="Jami, so'm"
centerValue={`${(total / 1_000_000).toFixed(1).replace('.', ',')} mln`}
height={220}
/>
);
}
Sparkline
A tiny line with no axes, grid or tooltip. If the value is already shown as text next to it, mark
it as decorative with aria-hidden; otherwise give it an aria-label.
Tushum
63 100 000 so'm
Qaytarishlar
38 ta
'use client';
import { formatMoney } from '@akhatmirzo/core';
import { Card } from '@akhatmirzo/ui';
import { Sparkline } from '@akhatmirzo/ui/charts';
const kpis = [
{ title: 'Tushum', value: formatMoney(63_100_000), trend: [42, 39, 51, 48, 58, 63], up: true },
{ title: 'Qaytarishlar', value: '38 ta', trend: [52, 47, 49, 44, 41, 38], up: false },
];
export default function ChartsSparkline() {
return (
<div className="grid w-full max-w-xl gap-3 sm:grid-cols-2">
{kpis.map((kpi) => (
<Card key={kpi.title} padding="sm" className="gap-2">
<p className="text-sm text-text-muted">{kpi.title}</p>
<div className="flex items-end justify-between gap-3">
<p className="text-xl font-bold whitespace-nowrap tabular-nums">{kpi.value}</p>
{/* Qiymat matn bilan berilgan — grafik faqat bezak */}
<Sparkline
aria-hidden
data={kpi.trend}
color={kpi.up ? 'success' : 'danger'}
className="w-24"
/>
</div>
</Card>
))}
</div>
);
}
Formatting
valueFormat controls the tooltip and the value axis (default formatNumber: 1 250 000);
indexFormat controls the category axis and the tooltip title. Use formatMoney for money; if
long values crowd the axis, pass a short form (42,5 mln so'm).
import { formatMoney } from '@akhatmirzo/core';
<BarChart valueFormat={(v) => formatMoney(v)} indexFormat={(d) => d.slice(0, 3)} … />States
loading shows a Skeleton in place of the chart (aria-busy); empty data shows an EmptyState
("Ma'lumot yo'q"). Pass your own empty state with emptyState and change the texts with labels.
'use client';
import { SegmentedControl } from '@akhatmirzo/ui';
import { AreaChart } from '@akhatmirzo/ui/charts';
import { useState } from 'react';
const data = [
{ week: '1-hafta', buyurtma: 120 },
{ week: '2-hafta', buyurtma: 164 },
{ week: '3-hafta', buyurtma: 142 },
{ week: '4-hafta', buyurtma: 190 },
];
export default function ChartsStates() {
const [state, setState] = useState('ready');
return (
<div className="flex w-full flex-col gap-4">
<SegmentedControl value={state} onValueChange={setState} aria-label="Holat" size="sm">
<SegmentedControl.Item value="ready">Ma'lumot</SegmentedControl.Item>
<SegmentedControl.Item value="loading">Yuklanmoqda</SegmentedControl.Item>
<SegmentedControl.Item value="empty">Bo'sh</SegmentedControl.Item>
</SegmentedControl>
<AreaChart
aria-label="Haftalik buyurtmalar"
data={state === 'empty' ? [] : data}
index="week"
series={[{ key: 'buyurtma', label: 'Buyurtmalar' }]}
loading={state === 'loading'}
legend={false}
height={220}
/>
</div>
);
}
Primitives: build your own chart
When a ready-made chart isn't enough (dual axes, mixed series, custom shapes), compose Recharts directly — the primitives give it the library look:
ChartContainer— a responsive wrapper:role="figure"+aria-label(required),description, andconfig→ CSS variables (revenue→var(--chart-revenue)). Set the height withheightorclassName="h-72"(default 280px).ChartTooltip—<Tooltip content={<ChartTooltip format={formatMoney} />} />; takes series names fromlabelinconfig.ChartLegend—<Legend content={<ChartLegend />} />, or outside a chart withpayload.chartColors(5 series colors),axisProps,gridProps,cursorProps,resolveChartColor.
'use client';
import {
ChartContainer,
ChartLegend,
ChartTooltip,
axisProps,
cursorProps,
gridProps,
type ChartConfig,
} from '@akhatmirzo/ui/charts';
import { Bar, CartesianGrid, ComposedChart, Legend, Line, Tooltip, XAxis, YAxis } from 'recharts';
const data = [
{ month: 'Yan', buyurtma: 820, konversiya: 2.4 },
{ month: 'Fev', buyurtma: 760, konversiya: 2.1 },
{ month: 'Mar', buyurtma: 940, konversiya: 2.8 },
{ month: 'Apr', buyurtma: 1010, konversiya: 3.1 },
{ month: 'May', buyurtma: 1180, konversiya: 3.4 },
];
// Har kalit → `var(--chart-<kalit>)`; label — tooltip va legend'da
const config: ChartConfig = {
buyurtma: { label: 'Buyurtmalar', color: 'primary' },
konversiya: { label: 'Konversiya, %', color: 'amber' },
};
export default function ChartsCustom() {
return (
<ChartContainer aria-label="Buyurtmalar va konversiya" config={config} className="h-72">
<ComposedChart data={data} margin={{ top: 8, right: 4, left: 0, bottom: 0 }}>
<CartesianGrid {...gridProps} />
<XAxis dataKey="month" {...axisProps} tickMargin={8} />
<YAxis yAxisId="left" {...axisProps} width="auto" />
<YAxis yAxisId="right" orientation="right" {...axisProps} width="auto" />
<Tooltip cursor={cursorProps} content={<ChartTooltip />} />
<Legend content={<ChartLegend className="pt-3" />} />
<Bar yAxisId="left" dataKey="buyurtma" fill="var(--chart-buyurtma)" radius={[6, 6, 0, 0]} />
<Line
yAxisId="right"
dataKey="konversiya"
stroke="var(--chart-konversiya)"
strokeWidth={2}
dot={false}
/>
</ComposedChart>
</ChartContainer>
);
}
Props
Ready-made charts pass the remaining <div> attributes to the root (className, ref, id...).
One of aria-label or title is required — TypeScript enforces it.
Shared (AreaChart, BarChart, LineChart)
| Prop | Type | Default | Description |
|---|---|---|---|
datarequired | readonly T[] | — | The data |
indexrequired | keyof T | — | Category (x axis) field |
seriesrequired | { key: keyof T; label?: string; color?: ChartColor }[] | — | Series to draw. Without a color, the next one from chartColors is used |
aria-label / titlerequired | string | — | Accessible name (either one) |
description | ReactNode | — | Short summary (aria-describedby) |
valueFormat | (value: number) => string | formatNumber | Tooltip and value axis |
indexFormat | (value: T[K]) => string | — | Category axis and tooltip title |
height | number | 280 | Height in px |
legend | boolean | true | |
grid | boolean | true | Dashed grid lines |
tooltip | boolean | true | |
loading | boolean | false | Shows a Skeleton |
emptyState | ReactNode | — | Empty state. Defaults to EmptyState ("Ma'lumot yo'q") |
labels | { empty?: string; loading?: string } | { empty: "Ma'lumot yo'q", loading: 'Yuklanmoqda' } |
Extra
| Prop | Type | Default | Description |
|---|---|---|---|
stacked | boolean | false | AreaChart, BarChart — stack the series |
curve | 'monotone' | 'linear' | 'step' | 'monotone' | AreaChart, LineChart |
orientation | 'vertical' | 'horizontal' | 'vertical' | BarChart: vertical — upright columns, horizontal — horizontal bars |
DonutChart
data, aria-label/title, description, valueFormat, indexFormat, height, legend,
tooltip, loading, emptyState and labels work as above (there is no series or grid).
| Prop | Type | Default | Description |
|---|---|---|---|
indexrequired | keyof T | — | Slice name field |
valuerequired | keyof T | — | Slice value field |
colors | ChartColor[] | — | Color sequence. Defaults to chartColors |
centerLabel | ReactNode | — | Caption in the center ("Total") |
centerValue | ReactNode | valueFormat(total) | Value in the center; null hides it |
Sparkline
| Prop | Type | Default | Description |
|---|---|---|---|
datarequired | readonly number[] | — | |
aria-label / title / aria-hiddenrequired | string | true | — | A name, or mark it decorative |
color | ChartColor | 'primary' | |
variant | 'area' | 'line' | 'area' | |
curve | 'monotone' | 'linear' | 'step' | 'monotone' | |
height | number | 36 | Height in px |
ChartContainer
| Prop | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | — | A Recharts chart |
aria-label / titlerequired | string | — | Accessible name |
config | Record<string, { label?: ReactNode; color?: ChartColor }> | — | Key → var(--chart-<key>); label is used by the tooltip and legend |
height | number | string | — | Defaults to 280px (h-70) |
description | ReactNode | — | Short summary (aria-describedby) |
ChartColor is a token name (primary, success, warning, danger, info, neutral, sky,
emerald, amber, rose) or any CSS value (var(--ak-indigo-400)). Hex values don't adapt to
dark mode — prefer tokens.
Data attributes
On the root of ready-made charts: data-state (ready · loading · empty), data-loading.
Accessibility
- A name is required — every chart gets
role="figure"and anaria-label(ortitle) — notimg, because the keyboard layer inside is interactive andimghides its children;Sparklineusesimg. Say what is shown and for which period: "Monthly revenue, January–June". - Summary — put the key takeaway in
description("Revenue reached 63M so'm in June"); it is read througharia-describedby. - Keyboard — Recharts'
accessibilityLayeris on: Tab into the chart and move between points with←→; the tooltip follows.Sparklinehas no tooltip, so it doesn't take focus. - Offer a table view too — charts are bad at conveying exact values. In important reports add a "Chart / Table" switch or a table below the chart:
'use client';
import { formatMoney } from '@akhatmirzo/core';
import { SegmentedControl } from '@akhatmirzo/ui';
import { BarChart } from '@akhatmirzo/ui/charts';
import { useState } from 'react';
const data = [
{ quarter: 'I chorak', tushum: 132_600_000 },
{ quarter: 'II chorak', tushum: 169_200_000 },
{ quarter: 'III chorak', tushum: 151_800_000 },
];
export default function ChartsTableView() {
const [view, setView] = useState('chart');
return (
<div className="flex w-full flex-col gap-4">
<SegmentedControl value={view} onValueChange={setView} aria-label="Ko'rinish" size="sm">
<SegmentedControl.Item value="chart">Grafik</SegmentedControl.Item>
<SegmentedControl.Item value="table">Jadval</SegmentedControl.Item>
</SegmentedControl>
{view === 'chart' ? (
<BarChart
aria-label="Choraklar bo'yicha tushum"
data={data}
index="quarter"
series={[{ key: 'tushum', label: 'Tushum' }]}
valueFormat={(v) => formatMoney(v)}
legend={false}
height={220}
/>
) : (
<table className="w-full text-sm">
<caption className="sr-only">Choraklar bo'yicha tushum</caption>
<thead className="text-left text-text-muted">
<tr>
<th className="py-2 font-medium">Chorak</th>
<th className="py-2 text-right font-medium">Tushum</th>
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={row.quarter} className="border-t border-border">
<td className="py-2">{row.quarter}</td>
<td className="py-2 text-right tabular-nums">{formatMoney(row.tushum)}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}
- Color is not the only signal — the legend and tooltip name each series in text.
- Motion — with
prefers-reduced-motionenabled, Recharts turns off its entry animation.