Графики и статистикаchartsvgzero-dependencygoalburnuppacetarget
Goal Progress Chart
Накопленный прогресс к цели против идеального линейного темпа с показателем опережения/отставания.
Живое превью
Cumulative vs. linear pace
$291k / $520kBehind pace by $29k
- Achieved
- Linear pace
View data
| Period | Amount | Cumulative | Pace |
|---|---|---|---|
| W1 | $22k | $22k | $40k |
| W2 | $31k | $53k | $80k |
| W3 | $28k | $81k | $120k |
| W4 | $40k | $121k | $160k |
| W5 | $36k | $157k | $200k |
| W6 | $44k | $201k | $240k |
| W7 | $51k | $252k | $280k |
| W8 | $39k | $291k | $320k |
| W9 | — | — | $360k |
| W10 | — | — | $400k |
| W11 | — | — | $440k |
| W12 | — | — | $480k |
| W13 | — | — | $520k |
Использование
Скопируйте и вставьте этот код в ваш React-компонент:
tsx
import { GoalProgressChart } from 'react-component-kit-ui/charts';
<GoalProgressChart periods={['W1', 'W2', 'W3', 'W4', 'W5', 'W6']} values={[22, 31, 28]} goal={160} prefix="$" suffix="k" />Ручная установка
Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/GoalProgressChart.tsx + GoalProgressChart.css:
GoalProgressChart.tsx
'use client';
import ChartFrame from './ChartFrame';
import { scaleLinear, niceTicks, linePath, areaPath, formatValue, type Point } from './chart-utils';
/**
* GoalProgressChart — Cumulative progress toward a goal over a period, versus
* the ideal linear pace, with an "ahead/behind pace" readout.
*
* @param {string[]} periods - X-axis labels for the whole period.
* @param {number[]} values - Per-period amounts achieved so far (may be shorter than periods).
* @param {number} goal - Target for the end of the period.
* @param {string} prefix - Value prefix.
* @param {string} suffix - Value suffix.
* @param {string} color - Progress color.
* @param {string} variant - Card style (glass|solid|ghost).
* @param {string} size - Density (sm|md|lg).
*/
export interface GoalProgressChartProps {
title?: string;
subtitle?: string;
periods?: string[];
values?: number[];
goal?: number;
prefix?: string;
suffix?: string;
height?: number;
color?: string;
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function GoalProgressChart({
title = 'Q4 bookings goal',
subtitle = 'Cumulative vs. linear pace',
periods = ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W8', 'W9', 'W10', 'W11', 'W12', 'W13'],
values = [22, 31, 28, 40, 36, 44, 51, 39],
goal = 520,
prefix = '$',
suffix = 'k',
height = 270,
color = 'var(--chart-1, #3987e5)',
variant = 'glass',
size = 'md',
className = '',
}: GoalProgressChartProps) {
const W = 640;
const H = height;
const m = { t: 14, r: 16, b: 28, l: 48 };
const n = periods.length;
const cum: number[] = [];
values.forEach((v, i) => cum.push((cum[i - 1] ?? 0) + v));
const achieved = cum[cum.length - 1] ?? 0;
const pace = (goal / n) * values.length;
const ahead = achieved >= pace;
const ticks = niceTicks(0, Math.max(goal, achieved), 4);
const y = scaleLinear(0, ticks[ticks.length - 1], H - m.b, m.t);
const x = (i: number) => m.l + ((i + 1) / n) * (W - m.l - m.r);
const x0 = m.l;
const pts: Point[] = [[x0, y(0)], ...cum.map((v, i): Point => [x(i), y(v)])];
const fmt = (v: number) => formatValue(v, prefix, suffix);
const pct = (achieved / goal) * 100;
return (
<>
<ChartFrame title={title} subtitle={subtitle} value={`${fmt(achieved)} / ${fmt(goal)}`} delta={`${ahead ? 'Ahead of' : 'Behind'} pace by ${fmt(Math.abs(achieved - pace))}`} deltaPositive={ahead} variant={variant} size={size} className={className} legend={[{ label: 'Achieved', color }, { label: 'Linear pace', color: 'var(--c-text-3, #8a8a8a)' }]} table={{ headers: ['Period', 'Amount', 'Cumulative', 'Pace'], rows: periods.map((p, i) => [p, values[i] !== undefined ? fmt(values[i]) : '—', cum[i] !== undefined ? fmt(cum[i]) : '—', fmt((goal / n) * (i + 1))]) }}>
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: ${fmt(achieved)} of ${fmt(goal)} (${pct.toFixed(0)}%), ${ahead ? 'ahead of' : 'behind'} pace`}>
{ticks.map((t, __i: number) => (
<g key={__i}>
<line className="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
<text className="cf-label" x={m.l - 8} y={y(t) + 4} textAnchor="end">{fmt(t)}</text>
</g>
))}
{periods.map((p, i) => i % 2 === 0 && <text className="cf-label" x={x(i)} y={H - 8} textAnchor="middle">{p}</text>)}
<line x1={m.l} x2={W - m.r} y1={y(goal)} y2={y(goal)} stroke="var(--chart-4, #c98500)" strokeWidth="1.5" aria-hidden="true" />
<text className="cf-label cf-label--strong" x={W - m.r} y={y(goal) - 6} textAnchor="end">Goal {fmt(goal)}</text>
<line x1={x0} y1={y(0)} x2={x(n - 1)} y2={y(goal)} stroke="var(--c-text-3, #8a8a8a)" strokeWidth="1.5" strokeDasharray="5 5" aria-hidden="true" />
<path d={areaPath(pts, y(0), 'linear')} fill={color} fillOpacity="0.15" aria-hidden="true" />
<path d={linePath(pts)} fill="none" stroke={color} strokeWidth="2.5" strokeLinejoin="round" aria-hidden="true" />
{cum.map((v, i) => (
<circle key={i} className="cf-mark" cx={x(i)} cy={y(v)} r={i === cum.length - 1 ? 6 : 3.5} fill={color} stroke="var(--chart-surface, #0d0d0f)" strokeWidth="2" tabIndex={0} role="img" aria-label={`${periods[i]}: cumulative ${fmt(v)}`} data-tip-title={periods[i]} data-tip={`${color}::Cumulative: ${fmt(v)}|This period: ${fmt(values[i])}|Pace: ${fmt((goal / n) * (i + 1))}`} />
))}
</svg>
</ChartFrame>
</>
);
}
export default GoalProgressChart;Краткая информация
- Категория
- Графики и статистика
- Имя файла
GoalProgressChart.tsx- Зависимости
- Нет — чистый React + CSS
- Теги
- chartsvgzero-dependencygoalburnuppacetarget