Charts

Dotted Trend

A line with every point marked and the last value called out on the chart itself: «where the readings are» and «where we are now» in one picture.

  • chart
  • line
  • trend
  • svg

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/chart-011?lang=en

Average order

Единица измерения: thousand roubles. Последнее значение подписано на графике.

Average order, thousand roubles
Апр18
Май24
Июн21
Июл33
Авг29
Сен42
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-011" (Dotted Trend) 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-011.json

Registry item: https://vibeui.ru/r/chart-011.json
Installs to: components/vibeui/chart-011.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 with every point marked and the last value called out on the chart itself: «where the readings are» and «where we are now» in one picture.

A line chart where every point is a dot and the last value carries a callout badge above it. The floor is lowered below the minimum, so the line never rests on the axis. The data is mirrored in a hidden table. Zero dependencies, one file, no client JS.

## 3. How to use it
import { Chart011 } from "@/components/vibeui/chart-011"

<Chart011
  title="Average order"
  unit="thousand roubles"
  points={[{ label: "Apr", value: 18 }, { label: "May", value: 24 }]}
/>

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-011-* palette — do not swap it for your theme tokens (bg-card, --chart-1 and the like)
- the lowered floor: a line resting on the axis reads as zero even when the data has no zero
- the callout on the last value: a trend without a number never says how much
- clamping the callout to the viewBox edge — otherwise the badge of the last point slides outside the chart
- dots on every point: they show where the real readings are rather than where the eye interpolates
- the hidden table together with aria-hidden on the svg — the data must be available as text exactly once

## 6. You may change
- the points array: a label and a value per reading
- title — the chart heading
- unit — the unit named in the caption under the chart
- the accent through the accent prop — it colours the line and the dots
- outer spacing through className

## 7. Rules
- Beyond fifteen points the dots merge into a dashed line: aggregate before rendering.
- The callout width is estimated from the digit count, so format very long values before passing them in.
- Do not add a second series here: 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-011.json
https://vibeui.ru/r/chart-011.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-chart-011-*. Серверный: хуков нет. Путь линии считается из массива точек в системе 300×136, нижняя граница опускается на четверть размаха ниже минимума, чтобы линия не лежала на оси. Последняя точка залита сплошным, над ней стоит тёмная плашка-выноска: её ширина оценивается по числу знаков, а позиция подрезается краем viewBox. Данные продублированы визуально скрытой таблицей.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Chart011Point = {
  label: string
  value: number
}

export type Chart011Props = Omit<
  ComponentPropsWithoutRef<"figure">,
  "children" | "title"
> & {
  title?: string
  points?: Chart011Point[]
  unit?: string
  accent?: string
}

// Идея компонента: линия, у которой каждая точка отмечена кружком, а последняя
// подписана выноской прямо на графике. Читатель ищет на тренде две вещи —
// «где узлы» и «сколько сейчас»; выноска отвечает на второй вопрос, не
// заставляя переводить взгляд в заголовок.
const STYLES = `
:where([data-vibeui-block="chart-011"]){
--vibeui-chart-011-bg:oklch(1 0 0);
--vibeui-chart-011-fg:oklch(0.22 0.014 265);
--vibeui-chart-011-muted:oklch(0.55 0.014 265);
--vibeui-chart-011-border:oklch(0.91 0.006 265);
--vibeui-chart-011-grid:oklch(0.94 0.005 265);
--vibeui-chart-011-accent:oklch(0.55 0.17 265);
--vibeui-chart-011-callout:oklch(0.22 0.014 265);
--vibeui-chart-011-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="chart-011"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:30rem;box-sizing:border-box;margin:0;padding:0.875rem;
background:var(--vibeui-chart-011-bg);
border:1px solid var(--vibeui-chart-011-border);border-radius:0.875rem;
color:var(--vibeui-chart-011-fg);font-family:var(--vibeui-chart-011-font);
}
[data-vibeui-block="chart-011"] [data-part="title"]{margin:0;font-size:0.875rem;font-weight:650}
[data-vibeui-block="chart-011"] svg{display:block;width:100%;height:auto}
[data-vibeui-block="chart-011"] [data-part="grid"]{stroke:var(--vibeui-chart-011-grid);stroke-width:1}
[data-vibeui-block="chart-011"] [data-part="line"]{
fill:none;stroke:var(--vibeui-chart-011-accent);stroke-width:2;
stroke-linejoin:round;stroke-linecap:round;
}
[data-vibeui-block="chart-011"] [data-part="dot"]{
fill:var(--vibeui-chart-011-bg);stroke:var(--vibeui-chart-011-accent);stroke-width:2;
}
[data-vibeui-block="chart-011"] [data-part="dot"][data-last="true"]{fill:var(--vibeui-chart-011-accent)}
[data-vibeui-block="chart-011"] [data-part="callout"]{fill:var(--vibeui-chart-011-callout)}
[data-vibeui-block="chart-011"] [data-part="callout-text"]{
fill:oklch(1 0 0);font-size:9px;font-weight:650;text-anchor:middle;
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="chart-011"] [data-part="name"]{
fill:var(--vibeui-chart-011-muted);font-size:8.5px;text-anchor:middle;
}
[data-vibeui-block="chart-011"] [data-part="unit"]{margin:0;font-size:0.75rem;color:var(--vibeui-chart-011-muted)}
[data-vibeui-block="chart-011"] [data-part="data"]{
position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
clip-path:inset(50%);white-space:nowrap;border:0;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="chart-011"] *{animation:none!important;transition:none!important}}
`

const LEFT = 12
const RIGHT = 288
const TOP = 26
const BASE = 116

const DEFAULT_POINTS: Chart011Point[] = [
  { label: "Апр", value: 18 },
  { label: "Май", value: 24 },
  { label: "Июн", value: 21 },
  { label: "Июл", value: 33 },
  { label: "Авг", value: 29 },
  { label: "Сен", value: 42 },
]

/**
 * Линия с отмеченными точками и выноской последнего значения.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Chart011({
  title = "Средний чек",
  points = DEFAULT_POINTS,
  unit = "тысяч рублей",
  accent,
  className,
  style,
  ...props
}: Chart011Props) {
  const values = points.map((point) => point.value)
  const max = Math.max(...values, 1)
  const min = Math.min(...values, 0)
  // Нижняя граница опускается ниже минимума: линия, лежащая на оси, читается
  // как ноль, хотя ноля в данных нет.
  const floor = min - (max - min) * 0.25
  const span = max - floor || 1

  const place = (value: number, index: number) => ({
    x:
      LEFT +
      (points.length > 1 ? (index / (points.length - 1)) * (RIGHT - LEFT) : 0),
    y: BASE - ((value - floor) / span) * (BASE - TOP),
  })

  const spots = points.map((point, index) => place(point.value, index))
  const path = spots
    .map((spot, index) => `${index === 0 ? "M" : "L"}${spot.x} ${spot.y}`)
    .join(" ")

  const last = spots[spots.length - 1]
  const lastValue = String(points[points.length - 1]?.value ?? "")
  const calloutWidth = lastValue.length * 6 + 14

  const palette = {
    ...(accent ? { "--vibeui-chart-011-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-chart-011" precedence="medium">
        {STYLES}
      </style>
      <figure
        {...props}
        data-vibeui-block="chart-011"
        className={className}
        style={palette}
      >
        <figcaption data-part="title">{title}</figcaption>
        <svg viewBox="0 0 300 136" aria-hidden="true" focusable="false">
          {[0, 0.5, 1].map((share) => {
            const y = TOP + share * (BASE - TOP)

            return (
              <line
                key={share}
                data-part="grid"
                x1={LEFT}
                y1={y}
                x2={RIGHT + 8}
                y2={y}
              />
            )
          })}
          <path data-part="line" d={path} />
          {spots.map((spot, index) => (
            <circle
              key={points[index].label}
              data-part="dot"
              data-last={index === spots.length - 1 ? "true" : undefined}
              cx={spot.x}
              cy={spot.y}
              r={3}
            />
          ))}
          <rect
            data-part="callout"
            x={Math.min(last.x - calloutWidth / 2, 300 - calloutWidth)}
            y={Math.max(last.y - 20, 2)}
            width={calloutWidth}
            height={14}
            rx={7}
          />
          <text
            data-part="callout-text"
            x={Math.min(last.x, 300 - calloutWidth / 2)}
            y={Math.max(last.y - 20, 2) + 10}
          >
            {lastValue}
          </text>
          {points.map((point, index) => (
            <text
              key={point.label}
              data-part="name"
              x={spots[index].x}
              y={BASE + 14}
            >
              {point.label}
            </text>
          ))}
        </svg>
        <p data-part="unit">
          Единица измерения: {unit}. Последнее значение подписано на графике.
        </p>
        <table data-part="data">
          <caption>
            {title}, {unit}
          </caption>
          <tbody>
            {points.map((point) => (
              <tr key={point.label}>
                <th scope="row">{point.label}</th>
                <td>{point.value}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </figure>
    </>
  )
}