Графики и статистикаchartsvgzero-dependencyringsgoalsprogressfitness
Activity Rings
Концентрические кольца целей со значениями и процентами; более 100% продолжает новый круг.
Живое превью
Daily goals
- Focus time5.2h/6h87%
- Tasks done11/8138%
- Reviews3/560%
View data
| Goal | Value | Target | Progress |
|---|---|---|---|
| Focus time | 5.2h | 6h | 87% |
| Tasks done | 11 | 8 | 138% |
| Reviews | 3 | 5 | 60% |
Использование
Скопируйте и вставьте этот код в ваш React-компонент:
tsx
import { ActivityRings } from 'react-component-kit-ui/charts';
<ActivityRings rings={[
{ label: 'Focus time', value: 5.2, goal: 6, unit: 'h' },
{ label: 'Tasks done', value: 11, goal: 8 },
]} />Ручная установка
Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/ActivityRings.tsx + ActivityRings.css:
ActivityRings.tsx
'use client';
import type { CSSProperties } from 'react';
import ChartFrame from './ChartFrame';
import { arcStroke, colorAt } from './chart-utils';
import './ActivityRings.css';
/** "a: b; --c: d" → style object */
function st(css: string | undefined): CSSProperties {
const o: Record<string, string> = {};
for (const decl of String(css ?? '').split(';')) {
const i = decl.indexOf(':');
if (i < 0) continue;
const k = decl.slice(0, i).trim();
if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
}
return o as CSSProperties;
}
/**
* ActivityRings — Concentric closed rings tracking daily goals (move, exercise,
* stand style). Values over 100% wrap with a stronger overlap stroke.
*
* @param {Array<{label: string, value: number, goal: number, unit?: string}>} rings - Up to 4 goals.
* @param {string[]} colors - Optional per-ring colors.
* @param {string} variant - Card style (glass|solid|ghost).
* @param {string} size - Density (sm|md|lg).
*/
export interface ActivityRingsProps {
title?: string;
subtitle?: string;
rings?: Array<{ label: string; value: number; goal: number; unit?: string }>;
colors?: string[];
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function ActivityRings({
title = 'Today',
subtitle = 'Daily goals',
rings = [
{ label: 'Focus time', value: 5.2, goal: 6, unit: 'h' },
{ label: 'Tasks done', value: 11, goal: 8 },
{ label: 'Reviews', value: 3, goal: 5 },
],
colors = ['var(--chart-5, #d55181)', 'var(--chart-3, #199e70)', 'var(--chart-1, #3987e5)'],
variant = 'glass',
size = 'md',
className = '',
}: ActivityRingsProps) {
const S = 220;
const c = S / 2;
const stroke = 20;
const items = rings.slice(0, 4).map((r, i) => {
const radius = c - stroke / 2 - 4 - i * (stroke + 4);
const pct = r.value / (r.goal || 1);
return { ...r, radius, pct, color: colorAt(i, colors) };
});
return (
<>
<ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className} table={{ headers: ['Goal', 'Value', 'Target', 'Progress'], rows: items.map((r) => [r.label, `${r.value}${r.unit ?? ''}`, `${r.goal}${r.unit ?? ''}`, `${Math.round(r.pct * 100)}%`]) }}>
<div className="ar" data-rk-2006ec="">
<svg viewBox={`0 0 ${S} ${S}`} role="img" aria-label={`${title}: ${items.map((r) => `${r.label} ${Math.round(r.pct * 100)}%`).join(', ')}`} data-rk-2006ec="">
{items.map((r) => {
const main = Math.min(r.pct, 0.9999);
const extra = r.pct > 1 ? Math.min(r.pct - 1, 0.9999) : 0;
return (
<g data-rk-2006ec="">
<circle cx={c} cy={c} r={r.radius} fill="none" stroke={r.color} strokeOpacity="0.16" strokeWidth={stroke} data-rk-2006ec="" />
{main > 0 && <path className="ar__arc" d={arcStroke(c, c, r.radius, 0, Math.PI * 2 * main)} fill="none" stroke={r.color} strokeWidth={stroke} strokeLinecap="round" data-rk-2006ec="" />}
{extra > 0 && <path d={arcStroke(c, c, r.radius, 0, Math.PI * 2 * extra)} fill="none" stroke={r.color} strokeWidth={stroke} strokeLinecap="round" style={{ filter: "drop-shadow(0 0 3px rgba(0,0,0,0.6)) brightness(1.15)" }} data-rk-2006ec="" />}
</g>
);
})}
</svg>
<ul className="ar__list" data-rk-2006ec="">
{items.map((r, __i: number) => (
<li key={__i} data-rk-2006ec="">
<span className="ar__label" data-rk-2006ec=""><i style={st(`background:${r.color}`)} aria-hidden="true" data-rk-2006ec=""></i>{r.label}</span>
<span className="ar__value" data-rk-2006ec="">{r.value}{r.unit ?? ''}<small data-rk-2006ec="">/{r.goal}{r.unit ?? ''}</small></span>
<span className="ar__pct" data-rk-2006ec="">{Math.round(r.pct * 100)}%{r.pct >= 1 && <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" aria-label="goal met" data-rk-2006ec=""><path d="M20 6 9 17l-5-5" data-rk-2006ec="" /></svg>}</span>
</li>
))}
</ul>
</div>
</ChartFrame>
</>
);
}
export default ActivityRings;ActivityRings.css
/* ActivityRings — ported from astro-component-kit (charts/ActivityRings.astro) */
.ar:where([data-rk-2006ec]) {
display: grid;
grid-template-columns: minmax(0, 220px) 1fr;
gap: 2rem;
align-items: center;
}
.ar__arc:where([data-rk-2006ec]) {
stroke-dasharray: 800;
stroke-dashoffset: 800;
animation: activity-rings-ar-draw 1.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes activity-rings-ar-draw { to { stroke-dashoffset: 0; } }
.ar__list:where([data-rk-2006ec]) { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.ar__list:where([data-rk-2006ec]) li:where([data-rk-2006ec]) { display: grid; grid-template-columns: 1fr auto; gap: 0.15rem 1rem; }
.ar__label:where([data-rk-2006ec]) { display: flex; align-items: center; gap: 0.45rem; font-size: 0.8125rem; color: var(--c-text-2, #a3a3a3); grid-column: 1 / -1; }
.ar__label:where([data-rk-2006ec]) i:where([data-rk-2006ec]) { width: 10px; height: 10px; border-radius: 50%; }
.ar__value:where([data-rk-2006ec]) { font-size: 1.25rem; font-weight: 700; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
.ar__value:where([data-rk-2006ec]) small:where([data-rk-2006ec]) { font-size: 0.8rem; color: var(--c-text-3, #8a8a8a); font-weight: 500; }
.ar__pct:where([data-rk-2006ec]) { display: inline-flex; align-items: center; gap: 0.3rem; align-self: end; font-size: 0.8125rem; color: var(--c-text-2, #a3a3a3); font-variant-numeric: tabular-nums; }
.ar__pct:where([data-rk-2006ec]) svg:where([data-rk-2006ec]) { color: var(--chart-pos, #34c38f); }
@media (max-width: 520px) { .ar:where([data-rk-2006ec]) { grid-template-columns: 1fr; justify-items: center; } .ar__list:where([data-rk-2006ec]) { width: 100%; } .ar:where([data-rk-2006ec]) svg:where([data-rk-2006ec]) { max-width: 200px; } }
@media (prefers-reduced-motion: reduce) { .ar__arc:where([data-rk-2006ec]) { animation: none; stroke-dashoffset: 0; } }Краткая информация
- Категория
- Графики и статистика
- Имя файла
ActivityRings.tsx- Зависимости
- Нет — чистый React + CSS
- Теги
- chartsvgzero-dependencyringsgoalsprogressfitness