Charts
Plan Versus Fact
A bullet chart of plan versus fact: a thin fact bar over qualitative ranges, a target tick across it and the completion percentage spelled out.
- chart
- bullet
- target
- kpi
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-016?lang=en
- Выручка8400 из 9000 · 93%
- Новые клиенты320 из 280 · 114%
- Продления145 из 180 · 81%
- Средний чек26 из 25 · 104%
- Факт
- План
Единица измерения: fact against plan
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-016" (Plan Versus Fact) 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-016.json
Registry item: https://vibeui.ru/r/chart-016.json
Installs to: components/vibeui/chart-016.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 bullet chart of plan versus fact: a thin fact bar over qualitative ranges, a target tick across it and the completion percentage spelled out.
A bullet chart: one row per metric, a track with three qualitative ranges, a thin fact bar, a vertical target tick and a «fact of plan · percent» caption. Zero dependencies, one file, no client JS.
## 3. How to use it
import { Chart016 } from "@/components/vibeui/chart-016"
<Chart016
title="Quarter plan progress"
scaleMax={1.25}
rows={[{ label: "Revenue", fact: 8400, plan: 9000 }]}
/>
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-016-* palette — do not swap it for your theme tokens (bg-card, --chart-1 and the like)
- normalising every row against its own plan: metrics of different nature are otherwise incomparable
- the target as its own element: a plan is a threshold, not another bar
- the fact changing colour once the plan is met — overshooting must read without reading digits
- the «fact of plan · percent» numbers beside the track: the bar says «roughly», the text says «exactly»
- the qualitative ranges as a track background rather than elements: they then stay out of the layout
## 6. You may change
- the rows array: label, fact and plan per metric
- title — the card heading
- unit — the unit note under the list
- scaleMax — how much longer than the plan a row's scale runs
- the accent through the accent prop — it colours the fact bar
## 7. Rules
- A fact beyond scaleMax is clipped at the track edge: if the plan doubled, raise scaleMax too.
- A plan of zero breaks the normalisation: filter such rows out before rendering.
- Beyond six rows the card becomes a table: then a table is the honest choice.
- 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-016.jsonhttps://vibeui.ru/r/chart-016.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-chart-016-*. Серверный: хуков нет. Шкала каждой строки — план, растянутый до scaleMax, поэтому риска плана стоит в одном и том же месте у всех строк и глаз сравнивает перевыполнение, а не абсолютные величины разной природы. Качественные диапазоны нарисованы одним linear-gradient фона дорожки, факт меняет цвет при достижении плана. Все числа продублированы текстом в строке.
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 Chart016Row = {
label: string
fact: number
plan: number
}
export type Chart016Props = Omit<
ComponentPropsWithoutRef<"figure">,
"children" | "title"
> & {
title?: string
rows?: Chart016Row[]
unit?: string
scaleMax?: number
accent?: string
}
// Идея компонента: bullet-график «план против факта». Тонкая полоса факта
// лежит на светлой шкале, риска плана стоит поперёк, а процент выполнения
// написан числом. Одна строка отвечает сразу на три вопроса, при этом занимает
// меньше места, чем пара столбиков.
const STYLES = `
:where([data-vibeui-block="chart-016"]){
--vibeui-chart-016-bg:oklch(1 0 0);
--vibeui-chart-016-fg:oklch(0.22 0.014 265);
--vibeui-chart-016-muted:oklch(0.55 0.014 265);
--vibeui-chart-016-border:oklch(0.91 0.006 265);
--vibeui-chart-016-band:oklch(0.96 0.004 265);
--vibeui-chart-016-band-2:oklch(0.93 0.005 265);
--vibeui-chart-016-band-3:oklch(0.89 0.006 265);
--vibeui-chart-016-accent:oklch(0.45 0.15 265);
--vibeui-chart-016-over:oklch(0.55 0.15 155);
--vibeui-chart-016-plan:oklch(0.3 0.02 265);
--vibeui-chart-016-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="chart-016"]{
display:flex;flex-direction:column;gap:0.625rem;
width:100%;max-width:26rem;box-sizing:border-box;margin:0;padding:0.875rem;
background:var(--vibeui-chart-016-bg);
border:1px solid var(--vibeui-chart-016-border);border-radius:0.875rem;
color:var(--vibeui-chart-016-fg);font-family:var(--vibeui-chart-016-font);
}
[data-vibeui-block="chart-016"] [data-part="title"]{margin:0;font-size:0.875rem;font-weight:650}
[data-vibeui-block="chart-016"] ul{display:flex;flex-direction:column;gap:0.625rem;margin:0;padding:0;list-style:none}
[data-vibeui-block="chart-016"] [data-part="row"]{
display:grid;grid-template-columns:1fr auto;gap:0.25rem 0.75rem;font-size:0.8125rem;
}
[data-vibeui-block="chart-016"] [data-part="label"]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
[data-vibeui-block="chart-016"] [data-part="numbers"]{
font-variant-numeric:tabular-nums;color:var(--vibeui-chart-016-muted);
}
[data-vibeui-block="chart-016"] [data-part="numbers"] b{color:var(--vibeui-chart-016-fg)}
/* Три ступени фона — качественные диапазоны: «мало», «приемлемо», «хорошо».
Они дают факту контекст, которого не даёт голая полоса. */
[data-vibeui-block="chart-016"] [data-part="track"]{
grid-column:1 / -1;position:relative;height:1.125rem;border-radius:0.25rem;overflow:hidden;
background:linear-gradient(to right,
var(--vibeui-chart-016-band-3) 0 60%,
var(--vibeui-chart-016-band-2) 60% 85%,
var(--vibeui-chart-016-band) 85% 100%);
}
[data-vibeui-block="chart-016"] [data-part="fact"]{
position:absolute;top:50%;left:0;height:0.4375rem;transform:translateY(-50%);
border-radius:0 0.125rem 0.125rem 0;background:var(--vibeui-chart-016-accent);
}
[data-vibeui-block="chart-016"] [data-part="fact"][data-over="true"]{background:var(--vibeui-chart-016-over)}
[data-vibeui-block="chart-016"] [data-part="plan"]{
position:absolute;top:0.125rem;bottom:0.125rem;width:2px;border-radius:1px;
background:var(--vibeui-chart-016-plan);
}
[data-vibeui-block="chart-016"] [data-part="unit"]{margin:0;font-size:0.75rem;color:var(--vibeui-chart-016-muted)}
[data-vibeui-block="chart-016"] [data-part="key"]{
display:flex;flex-wrap:wrap;gap:0.25rem 0.875rem;margin:0;padding:0;list-style:none;
font-size:0.6875rem;color:var(--vibeui-chart-016-muted);
}
[data-vibeui-block="chart-016"] [data-part="key"] li{display:flex;align-items:center;gap:0.375rem}
[data-vibeui-block="chart-016"] [data-part="swatch"]{
width:0.75rem;height:0.375rem;border-radius:1px;background:var(--vibeui-chart-016-accent);
}
[data-vibeui-block="chart-016"] [data-part="swatch"][data-kind="plan"]{
width:2px;height:0.75rem;background:var(--vibeui-chart-016-plan);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="chart-016"] *{animation:none!important;transition:none!important}}
`
const DEFAULT_ROWS: Chart016Row[] = [
{ label: "Выручка", fact: 8_400, plan: 9_000 },
{ label: "Новые клиенты", fact: 320, plan: 280 },
{ label: "Продления", fact: 145, plan: 180 },
{ label: "Средний чек", fact: 26, plan: 25 },
]
/**
* Bullet-график «план против факта» строками с риской плана.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Chart016({
title = "Выполнение плана квартала",
rows = DEFAULT_ROWS,
unit = "факт против плана",
scaleMax = 1.25,
accent,
className,
style,
...props
}: Chart016Props) {
const palette = {
...(accent ? { "--vibeui-chart-016-accent": accent } : null),
...style,
} as CSSProperties
return (
<>
<style href="vibeui-chart-016" precedence="medium">
{STYLES}
</style>
<figure
{...props}
data-vibeui-block="chart-016"
className={className}
style={palette}
>
<figcaption data-part="title">{title}</figcaption>
<ul>
{rows.map((row) => {
// Шкала строки — план, растянутый до scaleMax: так риска плана
// стоит в одном и том же месте у всех строк, и глаз сравнивает
// перевыполнение, а не абсолютные величины разной природы.
const ceiling = row.plan * scaleMax || 1
const done = Math.round((row.fact / (row.plan || 1)) * 100)
return (
<li key={row.label} data-part="row">
<span data-part="label">{row.label}</span>
<span data-part="numbers">
<b>{row.fact}</b> из {row.plan} · {done}%
</span>
<span data-part="track" aria-hidden="true">
<span
data-part="fact"
data-over={row.fact >= row.plan ? "true" : undefined}
style={{
width: `${Math.min((row.fact / ceiling) * 100, 100)}%`,
}}
/>
<span
data-part="plan"
style={{ left: `${(row.plan / ceiling) * 100}%` }}
/>
</span>
</li>
)
})}
</ul>
<ul data-part="key">
<li>
<span data-part="swatch" aria-hidden="true" />
Факт
</li>
<li>
<span data-part="swatch" data-kind="plan" aria-hidden="true" />
План
</li>
</ul>
<p data-part="unit">Единица измерения: {unit}</p>
</figure>
</>
)
}