Charts

Radial Metrics

Several metrics as one object: concentric arcs around a shared centre plus a legend with percentages and absolute numbers.

  • chart
  • radial
  • progress
  • metrics

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-020?lang=en

Release readiness
  • Покрытие тестами82%82 из 100
  • Готовность спринта64%64 из 100
  • Доля автосборок45%45 из 100

Единица измерения: percent of target

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-020" (Radial Metrics) 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-020.json

Registry item: https://vibeui.ru/r/chart-020.json
Installs to: components/vibeui/chart-020.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
Several metrics as one object: concentric arcs around a shared centre plus a legend with percentages and absolute numbers.

Radial progress for several metrics: concentric arcs around a shared centre, a track behind each arc, and a legend with the percentage and the absolute «value of ceiling». Zero dependencies, one file, no client JS.

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

<Chart020
  title="Release readiness"
  unit="percent of target"
  metrics={[{ label: "Test coverage", value: 82, max: 100 }]}
/>

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-020-* palette — do not swap it for your theme tokens (bg-card, --chart-1 and the like)
- the legend with percentages: arcs of different radius cannot be compared by eye at all
- the absolute «value of ceiling» beside the percentage — 80% of five and of five hundred are different news
- the track behind every arc: without it the end of the scale is invisible
- the −90° rotation so the arcs start at the top rather than on the right
- aria-hidden on the svg: the legend carries the meaning, not the picture

## 6. You may change
- the metrics array: label, value, the max ceiling and a hue
- title — the card heading
- unit — the unit named in the caption under the legend
- the accent through the accent prop — it colours the outer arc
- the radii and the arc thickness

## 7. Rules
- More than four metrics do not fit: the extras are dropped silently because there are exactly four radii.
- Concentric arcs cannot be compared by length: the outer one is always longer at the same percentage, which is why the legend is mandatory.
- The value is clamped between zero and max: overshooting stays invisible to the arc.
- 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-020.json
https://vibeui.ru/r/chart-020.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-chart-020-*. Серверный: хуков нет. Каждая метрика живёт на своей окружности: длина окружности считается из радиуса, заполнение задаётся stroke-dasharray, поворот на −90° переносит старт наверх. Радиусы лежат отдельным массивом, поэтому геометрию можно менять, не трогая математику. Кольцо помечено aria-hidden: смысл несёт легенда, где у каждой метрики есть и процент, и «значение из предела».

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 Chart020Metric = {
  label: string
  value: number
  max?: number
  hue?: number
}

export type Chart020Props = Omit<
  ComponentPropsWithoutRef<"figure">,
  "children" | "title"
> & {
  title?: string
  metrics?: Chart020Metric[]
  unit?: string
  accent?: string
}

// Идея компонента: несколько показателей одним объектом — концентрические
// дуги. Каждая дуга живёт на своей окружности и заполняется через
// stroke-dasharray, поэтому радиусы можно менять, не трогая математику.
// Кольца сами по себе неразличимы, поэтому легенда с процентами обязательна.
const STYLES = `
:where([data-vibeui-block="chart-020"]){
--vibeui-chart-020-bg:oklch(1 0 0);
--vibeui-chart-020-fg:oklch(0.22 0.014 265);
--vibeui-chart-020-muted:oklch(0.55 0.014 265);
--vibeui-chart-020-border:oklch(0.91 0.006 265);
--vibeui-chart-020-track:oklch(0.95 0.004 265);
--vibeui-chart-020-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="chart-020"]{
display:flex;flex-wrap:wrap;align-items:center;gap:0.875rem;
width:100%;max-width:26rem;box-sizing:border-box;margin:0;padding:0.875rem;
background:var(--vibeui-chart-020-bg);
border:1px solid var(--vibeui-chart-020-border);border-radius:0.875rem;
color:var(--vibeui-chart-020-fg);font-family:var(--vibeui-chart-020-font);
}
[data-vibeui-block="chart-020"] [data-part="title"]{flex:1 0 100%;margin:0;font-size:0.875rem;font-weight:650}
[data-vibeui-block="chart-020"] svg{display:block;width:8.75rem;height:8.75rem;flex:0 0 auto}
[data-vibeui-block="chart-020"] [data-part="track"]{
fill:none;stroke:var(--vibeui-chart-020-track);stroke-width:9;
}
[data-vibeui-block="chart-020"] [data-part="ring"]{
fill:none;stroke-width:9;stroke-linecap:round;
}
[data-vibeui-block="chart-020"] [data-part="legend"]{
flex:1 1 9rem;display:flex;flex-direction:column;gap:0.5rem;margin:0;padding:0;list-style:none;
}
[data-vibeui-block="chart-020"] [data-part="row"]{
display:grid;grid-template-columns:0.5rem 1fr auto;align-items:center;gap:0.5rem;
font-size:0.8125rem;
}
[data-vibeui-block="chart-020"] [data-part="chip"]{
width:0.5rem;height:0.5rem;border-radius:9999px;background:var(--vibeui-chart-020-ring);
}
[data-vibeui-block="chart-020"] [data-part="name"]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
[data-vibeui-block="chart-020"] [data-part="share"]{font-weight:650;font-variant-numeric:tabular-nums}
[data-vibeui-block="chart-020"] [data-part="raw"]{
grid-column:2 / -1;font-size:0.6875rem;color:var(--vibeui-chart-020-muted);
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="chart-020"] [data-part="unit"]{flex:1 0 100%;margin:0;font-size:0.75rem;color:var(--vibeui-chart-020-muted)}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="chart-020"] *{animation:none!important;transition:none!important}}
`

const CENTER = 60
const RADII = [50, 38, 26, 14]
const HUES = [265, 200, 150, 40]

const DEFAULT_METRICS: Chart020Metric[] = [
  { label: "Покрытие тестами", value: 82, max: 100 },
  { label: "Готовность спринта", value: 64, max: 100 },
  { label: "Доля автосборок", value: 45, max: 100 },
]

/**
 * Радиальный прогресс нескольких метрик концентрическими дугами.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Chart020({
  title = "Готовность релиза",
  metrics = DEFAULT_METRICS,
  unit = "процентов от цели",
  accent,
  className,
  style,
  ...props
}: Chart020Props) {
  const palette = {
    ...(accent ? { "--vibeui-chart-020-accent": accent } : null),
    ...style,
  } as CSSProperties

  const rings = metrics.slice(0, RADII.length).map((metric, index) => {
    const ceiling = metric.max ?? 100
    const share = Math.max(0, Math.min(1, metric.value / (ceiling || 1)))
    const radius = RADII[index]
    const circumference = 2 * Math.PI * radius

    return {
      metric,
      ceiling,
      share,
      radius,
      circumference,
      color:
        accent && index === 0
          ? accent
          : `oklch(0.6 0.15 ${metric.hue ?? HUES[index % HUES.length]})`,
    }
  })

  return (
    <>
      <style href="vibeui-chart-020" precedence="medium">
        {STYLES}
      </style>
      <figure
        {...props}
        data-vibeui-block="chart-020"
        className={className}
        style={palette}
      >
        <figcaption data-part="title">{title}</figcaption>
        <svg viewBox="0 0 120 120" aria-hidden="true" focusable="false">
          {rings.map((ring) => (
            <g key={ring.metric.label}>
              <circle
                data-part="track"
                cx={CENTER}
                cy={CENTER}
                r={ring.radius}
              />
              <circle
                data-part="ring"
                cx={CENTER}
                cy={CENTER}
                r={ring.radius}
                stroke={ring.color}
                strokeDasharray={`${ring.circumference * ring.share} ${ring.circumference}`}
                transform={`rotate(-90 ${CENTER} ${CENTER})`}
              />
            </g>
          ))}
        </svg>
        <ul data-part="legend">
          {rings.map((ring) => (
            <li key={ring.metric.label} data-part="row">
              <span
                data-part="chip"
                aria-hidden="true"
                style={
                  { "--vibeui-chart-020-ring": ring.color } as CSSProperties
                }
              />
              <span data-part="name">{ring.metric.label}</span>
              <span data-part="share">{Math.round(ring.share * 100)}%</span>
              <span data-part="raw">
                {ring.metric.value} из {ring.ceiling}
              </span>
            </li>
          ))}
        </ul>
        <p data-part="unit">Единица измерения: {unit}</p>
      </figure>
    </>
  )
}