Charts
Line Chart
A line chart in plain SVG: the path is computed from an array of points on the server. No charting library and no client JS — it renders before hydration and prints correctly.
- chart
- line
- svg
- analytics
Preview
Use it with AI
- 1. Copy the link.
- 2. Write to your agent in your own words and drop the link into the sentence.
- 3. The agent opens the link and installs the component from the registry.
put this in the header: https://vibeui.ru/c/chart-001?lang=en
Show the full instructions
What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.
# Install and place "chart-001" (Line Chart) from VibeUI
## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/chart-001.json
Registry item: https://vibeui.ru/r/chart-001.json
Installs to: components/vibeui/chart-001.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.
Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.
## 2. What it is
A line chart in plain SVG: the path is computed from an array of points on the server. No charting library and no client JS — it renders before hydration and prints correctly.
A line chart: three grid lines, a curve with a fill beneath it, a peak marker and axis labels. The SVG path is computed from the points array — no charting library, no client JS, visible before hydration and correct in print. Zero dependencies, one file, its own palette.
## 3. How to use it
import { Chart001 } from "@/components/vibeui/chart-001"
<Chart001
title="Visitors this week"
points={[{ label: "Mon", value: 1240 }, { label: "Tue", value: 1580 }]}
/>
Read the installed file for the full prop list.
## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.
Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)
## 5. Keep exactly as installed
- the local --vibeui-chart-001-* palette — do not swap it for your theme tokens (bg-card, --chart-1 and the like)
- the normalisation with a lowered floor: a line resting on the axis reads as zero
- vector-effect:non-scaling-stroke — without it a chart stretched across the width gets an uneven stroke
- preserveAspectRatio="none" together with the viewBox: the chart has to fill the container width
- the low-opacity fill under the line: it holds the eye on the value area
- the aria-label on the svg carrying the range — otherwise it is an empty image to a screen reader
- the figure / figcaption markup: the caption belongs to the chart, not to neighbouring text
- the <style> block inside the component — it holds the palette and the geometry
## 6. You may change
- the points array: a label and a value per point
- title — the chart heading
- unit — the unit beside the large current value
- markPeak — whether to mark the maximum
- the accent through the accent prop — it colours the line, the fill and the value
- chart height and outer spacing through className
## 7. Rules
- There is no hover or tooltip: this is a static chart. Interactivity needs client code and a different component.
- Beyond twenty points the line stops holding up — aggregate before rendering.
- Do not put two series in one chart: comparison needs two lines and a legend, which is a different component.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.
## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.For developers
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.
Component source
The same file your agent installs. Here in case you would rather copy it by hand.
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>
</>
)
}