Charts
Line Chart
Линейный график на чистом SVG: путь считается из массива точек на сервере. Ни библиотеки графиков, ни клиентского JS — картинка видна до гидратации и печатается.
- chart
- line
- svg
- analytics
Превью
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 3. Агент откроет ссылку и поставит компонент из registry.
размести это в шапке: https://vibeui.ru/c/chart-001
Показать полную инструкцию
То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.
# Установка и размещение «chart-001» (Line Chart) из VibeUI
## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/chart-001.json
Registry item: https://vibeui.ru/r/chart-001.json
Ставится в: components/vibeui/chart-001.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.
Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.
## 2. Что это
Линейный график на чистом SVG: путь считается из массива точек на сервере. Ни библиотеки графиков, ни клиентского JS — картинка видна до гидратации и печатается.
Линейный график: сетка из трёх линий, кривая с заливкой под ней, отметка максимума и подписи по оси. Путь SVG считается из массива точек — библиотека графиков не нужна, клиентского JS нет, график виден до гидратации и корректно печатается. Ноль зависимостей, один файл, своя палитра.
## 3. Как использовать
import { Chart001 } from "@/components/vibeui/chart-001"
<Chart001
title="Посетители за неделю"
points={[{ label: "Пн", value: 1240 }, { label: "Вт", value: 1580 }]}
/>
Полный список пропсов — в установленном файле.
## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.
Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)
## 5. Сохранить как установлено
- локальную палитру --vibeui-chart-001-* и не подменять её токенами вашей темы (bg-card, --chart-1 и подобными)
- нормировку значений с опущенной нижней границей: линия, лежащая на оси, читается как ноль
- vector-effect:non-scaling-stroke — без него растянутый по ширине график даёт неровную толщину линии
- preserveAspectRatio="none" вместе с viewBox: график обязан занимать всю ширину контейнера
- заливку под линией с низкой непрозрачностью: она удерживает взгляд на области значений
- aria-label у svg с диапазоном значений — иначе для скринридера это пустая картинка
- разметку figure / figcaption: подпись принадлежит графику, а не соседнему тексту
- блок <style> внутри компонента — в нём вся палитра и геометрия
## 6. Можно менять
- массив points: подпись и значение каждой точки
- title — заголовок графика
- unit — единица у крупного значения справа
- markPeak — отмечать ли максимум
- акцент через проп accent — цвет линии, заливки и значения
- высоту графика и внешние отступы через className
## 7. Правила
- Наведения и подсказок нет: это статичный график. Интерактивность требует клиентского кода и другого компонента.
- Точек больше двадцати линия не выдержит — агрегируйте данные до отрисовки.
- Не показывайте одним графиком два ряда: для сравнения нужны две линии и легенда, а это другой компонент.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.
## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.Для разработчика
npx shadcn@latest add https://vibeui.ru/r/chart-001.jsonhttps://vibeui.ru/r/chart-001.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-chart-001-*. Путь линии и заливки собирается из точек прямо в разметке: значения нормируются к области 320×120, нижняя граница опускается на четверть размаха ниже минимума, чтобы линия не лежала на оси. Толщина линии не искажается при растяжении благодаря vector-effect:non-scaling-stroke. Весь график — figure с figcaption и aria-label у svg.
Исходник компонента
Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.
import type { CSSProperties } from "react"
export type Chart001Point = {
label: string
value: number
}
export type Chart001Props = {
points?: Chart001Point[]
title?: string
/** Единица у подписей значений: «₽», «%», « ч». */
unit?: string
/** Отмечать точку максимума подписью. */
markPeak?: boolean
accent?: string
className?: string
style?: CSSProperties
}
// Идея компонента: график рисуется разметкой, а не библиотекой. SVG считается
// на сервере из массива точек, поэтому клиентского JS нет вовсе, а сам график
// виден до гидратации и печатается. Заливка под линией — не украшение: она
// удерживает взгляд на области значений, а не на самой кривой.
const STYLES = `
:where([data-vibeui-block="chart-001"]){
--vibeui-chart-001-fg:oklch(0.24 0.016 265);
--vibeui-chart-001-muted:oklch(0.55 0.014 265);
--vibeui-chart-001-bg:oklch(1 0 0);
--vibeui-chart-001-border:oklch(0.91 0.006 265);
--vibeui-chart-001-grid:oklch(0.93 0.005 265);
--vibeui-chart-001-accent:oklch(0.55 0.2 262);
--vibeui-chart-001-radius:0.875rem;
--vibeui-chart-001-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="chart-001"]{
display:flex;flex-direction:column;gap:0.875rem;
width:100%;box-sizing:border-box;padding:1.125rem 1.25rem 1rem;
border:1px solid var(--vibeui-chart-001-border);
border-radius:var(--vibeui-chart-001-radius);
background:var(--vibeui-chart-001-bg);color:var(--vibeui-chart-001-fg);
font-family:var(--vibeui-chart-001-font);
}
[data-vibeui-block="chart-001"] [data-part="head"]{display:flex;align-items:baseline;justify-content:space-between;gap:1rem}
[data-vibeui-block="chart-001"] [data-part="title"]{margin:0;font-size:0.9375rem;font-weight:600}
[data-vibeui-block="chart-001"] [data-part="last"]{
font-size:1.125rem;font-weight:650;font-variant-numeric:tabular-nums;
color:var(--vibeui-chart-001-accent);
}
[data-vibeui-block="chart-001"] svg{display:block;width:100%;height:8rem;overflow:visible}
[data-vibeui-block="chart-001"] [data-part="grid"]{stroke:var(--vibeui-chart-001-grid);stroke-width:1}
[data-vibeui-block="chart-001"] [data-part="line"]{
fill:none;stroke:var(--vibeui-chart-001-accent);stroke-width:2;
stroke-linecap:round;stroke-linejoin:round;
vector-effect:non-scaling-stroke;
}
[data-vibeui-block="chart-001"] [data-part="area"]{fill:var(--vibeui-chart-001-accent);opacity:.1}
[data-vibeui-block="chart-001"] [data-part="peak"]{fill:var(--vibeui-chart-001-accent)}
[data-vibeui-block="chart-001"] [data-part="peak-ring"]{fill:var(--vibeui-chart-001-bg);stroke:var(--vibeui-chart-001-accent);stroke-width:2}
[data-vibeui-block="chart-001"] [data-part="axis"]{
display:flex;justify-content:space-between;gap:0.5rem;
font-size:0.6875rem;color:var(--vibeui-chart-001-muted);
}
[data-vibeui-block="chart-001"] [data-part="sr"]{
position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="chart-001"] *{animation:none!important;transition:none!important}}
`
const DEFAULT_POINTS: Chart001Point[] = [
{ label: "Пн", value: 1240 },
{ label: "Вт", value: 1580 },
{ label: "Ср", value: 1390 },
{ label: "Чт", value: 2180 },
{ label: "Пт", value: 2640 },
{ label: "Сб", value: 1980 },
{ label: "Вс", value: 2310 },
]
const WIDTH = 320
const HEIGHT = 120
/**
* Линейный график на SVG: путь считается из точек, зависимостей нет.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Chart001({
points = DEFAULT_POINTS,
title = "Посетители за неделю",
unit = "",
markPeak = true,
accent,
className,
style,
}: Chart001Props) {
const palette = {
...(accent ? { "--vibeui-chart-001-accent": accent } : null),
...style,
} as CSSProperties
const values = points.map((point) => point.value)
const max = Math.max(...values)
const min = Math.min(...values)
// Нижняя граница опускается ниже минимума: линия не должна лежать на оси.
const floor = min - (max - min) * 0.25
const span = max - floor || 1
const coords = points.map((point, index) => ({
x: (index / Math.max(1, points.length - 1)) * WIDTH,
y: HEIGHT - ((point.value - floor) / span) * HEIGHT,
point,
}))
const line = coords
.map((coord, index) => `${index === 0 ? "M" : "L"}${coord.x} ${coord.y}`)
.join(" ")
const area = `${line} L${WIDTH} ${HEIGHT} L0 ${HEIGHT} Z`
const peak = coords.reduce((best, coord) =>
coord.point.value > best.point.value ? coord : best,
)
const last = points[points.length - 1]
return (
<>
<style href="vibeui-chart-001" precedence="medium">
{STYLES}
</style>
<figure
data-vibeui-block="chart-001"
className={className}
style={palette}
>
<div data-part="head">
<figcaption data-part="title">{title}</figcaption>
<span data-part="last">
{last.value.toLocaleString("ru-RU")}
{unit}
</span>
</div>
<svg
viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
preserveAspectRatio="none"
role="img"
aria-label={`${title}: от ${min}${unit} до ${max}${unit}`}
>
{[0, 0.5, 1].map((ratio) => (
<line
key={ratio}
data-part="grid"
x1={0}
x2={WIDTH}
y1={HEIGHT * ratio}
y2={HEIGHT * ratio}
/>
))}
<path data-part="area" d={area} />
<path data-part="line" d={line} />
{markPeak ? (
<>
<circle data-part="peak-ring" cx={peak.x} cy={peak.y} r={4.5} />
<circle data-part="peak" cx={peak.x} cy={peak.y} r={2} />
</>
) : null}
</svg>
<div data-part="axis">
{points.map((point) => (
<span key={point.label}>{point.label}</span>
))}
</div>
</figure>
</>
)
}