Live-Diagramm
Werte, die laufend hereinkommen, als gleitende Linie, wachsende Balken, Ereignisspur mit Liste, Live-Kacheln oder Latenzband mit Grenze. Es hält still, solange du liest, und hat einen Pausenknopf.
Eine von 6 Components in KPI-, Live- und Karten-Charts
Variants
Details
Was Live-Diagramm mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- KPI-, Live- und Karten-Charts
- Bereich
- Elements
- Variants
- 5
- Version
- 1.0.0 · seit 1.1.0
- Datei
- components/ui/live-chart.tsx
- Bringt mit
- utilskiticonchartchart-figure
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 12 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle LiveChartProps aus components/ui/live-chart.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von Omit<ChartA11y, 'label'>, ChartFigureProps an.
labeloptionalstringAccessible name of the chart. Default: the title.
variantoptionalLiveVariantlinebarseventsmultilatency
seriesoptionalLiveSeries[]line, bars: one series. multi: several. latency: two (typical and slow).
eventsoptionalLiveEvent[]events: the events so far
eventNamesoptionalstring[]events: names the simulation draws new events from, "#" becomes a running number
limitoptionalnumberlatency: the limit line
limitLabeloptionalstringwindowoptionalnumberPoints on screen. Default 40.
intervaloptionalnumberMilliseconds between ticks. Default 1000.
simulateoptionalbooleanInvent new values inside min and max (demos, previews)
formatoptionalIntl.NumberFormatOptions
Eingebaute Texte
Diese Texte schreibt Live-Diagramm selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 12 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| pause | Englisch Pause updates | Deutsch Aktualisierung anhalten |
| play | Englisch Resume updates | Deutsch Aktualisierung fortsetzen |
| live | Englisch Live | Deutsch Live |
| paused | Englisch Paused | Deutsch Angehalten |
| held | Englisch Held while you read | Deutsch Angehalten, solange du liest |
| ago | Englisch {n} s ago | Deutsch vor {n} s |
| now | Englisch now | Deutsch jetzt |
| latest | Englisch Latest | Deutsch Zuletzt |
| time | Englisch Time | Deutsch Zeit |
| value | Englisch Value | Deutsch Wert |
| event | Englisch Event | Deutsch Ereignis |
| perSec | Englisch per second | Deutsch pro Sekunde |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Live-Diagramm reagiert selbst auf die Tasten Escape, Enter, Pfeil links, Pfeil rechts und Leertaste.
- Setzt aria-label, aria-live, aria-pressed und aria-relevant, damit Screenreader Zweck und Zustand ansagen.
- Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
- Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 1 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 450 Zeilen
'use client';
import { useEffect, useId, useRef, useState } from 'react';
import { cn, prefersReducedMotion, round, useTexts } from '@/lib/kit';
import { Icon } from '@/components/ui/icon';
import { ChartRoot, ChartSurface, ChartTable, Swatch, barPath, linePath, niceTicks, numberFormatter, textWidth, tickFormatter, useEntrance, useIntlLocale, useWidth, type ChartA11y } from '@/components/ui/chart';
import { ChartFigure, HALO, PlotAxes, linear, pairColor, type ChartFigureProps } from '@/components/ui/chart-figure';
/*
* Values that keep coming in.
* line one measure as a line that slides left, the newest point pulses
* bars one bar per second, new bars grow in on the right
* events single events (orders, sign ups) as dots on a running time
* track, the newest in a short feed below
* multi several measures as small live tiles with their current value
* latency a typical and a slow value (p50 and p95) as a band, a limit line
*
* Feed it either way: pass new `data` from your source (socket, polling)
* and the chart slides, or set `simulate` for a demo that invents values
* inside a band. The chart stops while the pointer or the keyboard is on it,
* and a button pauses it for good (moving content needs a pause, WCAG
* 2.2.2). With reduced motion the values still update, nothing slides.
*/
export type LiveVariant = 'line' | 'bars' | 'events' | 'multi' | 'latency';
export interface LiveSeries {
label: string;
/** Oldest first, one value per tick */
values: number[];
/** Range of the simulation and of the scale */
min?: number;
max?: number;
format?: Intl.NumberFormatOptions;
/** Text after the value, e.g. " %" or " ms" */
suffix?: string;
}
export interface LiveEvent {
label: string;
value: number;
/** Seconds ago */
ago: number;
}
export interface LiveChartProps extends Omit<ChartA11y, 'label'>, ChartFigureProps {
/** Accessible name of the chart. Default: the title. */
label?: string;
variant?: LiveVariant;
/** line, bars: one series. multi: several. latency: two (typical and slow). */
series?: LiveSeries[];
/** events: the events so far */
events?: LiveEvent[];
/** events: names the simulation draws new events from, "#" becomes a running number */
eventNames?: string[];
/** latency: the limit line */
limit?: number;
limitLabel?: string;
/** Points on screen. Default 40. */
window?: number;
/** Milliseconds between ticks. Default 1000. */
interval?: number;
/** Invent new values inside min and max (demos, previews) */
simulate?: boolean;
format?: Intl.NumberFormatOptions;
className?: string;
}
const TEXTS = {
en: { pause: 'Pause updates', play: 'Resume updates', live: 'Live', paused: 'Paused', held: 'Held while you read', ago: '{n} s ago', now: 'now', latest: 'Latest', time: 'Time', value: 'Value', event: 'Event', perSec: 'per second' },
de: { pause: 'Aktualisierung anhalten', play: 'Aktualisierung fortsetzen', live: 'Live', paused: 'Angehalten', held: 'Angehalten, solange du liest', ago: 'vor {n} s', now: 'jetzt', latest: 'Zuletzt', time: 'Zeit', value: 'Wert', event: 'Ereignis', perSec: 'pro Sekunde' },
};
const CSS = '@keyframes kitlive-shift{from{transform:translateX(var(--kitlive-step))}}@keyframes kitlive-ping{0%{transform:scale(1);opacity:.55}100%{transform:scale(3);opacity:0}}@keyframes kitlive-in{from{opacity:0;transform:translateY(-6px)}}.kitlive-shift{animation:kitlive-shift var(--kitlive-t) linear both}.kitlive-ping{transform-box:fill-box;transform-origin:center;animation:kitlive-ping 1.6s cubic-bezier(.2,.7,.3,1) infinite}.kitlive-in{animation:kitlive-in .45s var(--ease-kit) both}@media (prefers-reduced-motion:reduce){.kitlive-shift,.kitlive-ping,.kitlive-in{animation:none!important}}';
/** The running buffers: follows new props, or invents values with simulate */
function useStream(series: LiveSeries[], window: number, interval: number, simulate: boolean, hold: boolean) {
const [bufs, setBufs] = useState(() => series.map((s) => s.values.slice(-window)));
const [tick, setTick] = useState(0);
const holdRef = useRef(hold);
holdRef.current = hold;
// New data from outside: take it and count a tick so the line slides
const sig = series.map((s) => s.values.length + ':' + s.values[s.values.length - 1]).join('|');
const first = useRef(true);
useEffect(() => {
if (first.current) {
first.current = false;
return;
}
if (simulate) return;
setBufs(series.map((s) => s.values.slice(-window)));
setTick((t) => t + 1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sig]);
useEffect(() => {
if (!simulate) return;
const id = setInterval(() => {
if (holdRef.current) return;
setBufs((prev) =>
prev.map((b, i) => {
const s = series[i];
const lo = s.min ?? Math.min(...s.values);
const hi = s.max ?? Math.max(...s.values);
const last = b[b.length - 1] ?? (lo + hi) / 2;
const span = hi - lo || 1;
// A walk that leans back to the middle, so it never sticks at an edge
const pull = ((lo + hi) / 2 - last) * 0.12;
const next = Math.max(lo, Math.min(hi, last + pull + (Math.random() - 0.5) * span * 0.22));
return [...b.slice(-(window - 1)), round(next, 2)];
}),
);
setTick((t) => t + 1);
}, interval);
return () => clearInterval(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [simulate, interval, window]);
return { bufs, tick };
}
/**
* Values that keep coming in: a sliding line, growing bars, an event track, live tiles or a latency band.
* <LiveChart variant="line" series={[{ label: 'Visitors now', values: last40, min: 0, max: 120 }]} simulate />
*/
export function LiveChart(props: LiveChartProps) {
const { variant = 'line', series = [], events = [], eventNames = [], limit, limitLabel, window: win = 40, interval = 1000, simulate = false, format, label, summary, labels, animate = true, title, value, delta, meta, source, framed, className } = props;
const t = useTexts(TEXTS, undefined);
const intl = useIntlLocale();
const [wrapRef, width] = useWidth<HTMLDivElement>(720);
const [rootRef, anim] = useEntrance<HTMLDivElement>(animate);
const [paused, setPaused] = useState(false);
const [hover, setHover] = useState<number | null>(null);
const [reduced, setReduced] = useState(false);
useEffect(() => setReduced(prefersReducedMotion()), []);
const hold = paused || hover !== null || anim === 'wait';
const S = variant === 'events' ? [{ label: 'events', values: [0] }] : series;
const { bufs, tick } = useStream(S, win, interval, simulate && variant !== 'events', hold);
const clipId = useId().replace(/:/g, '');
const name = label ?? title ?? series[0]?.label ?? '';
const phone = width < 520;
const fmtOf = (s?: LiveSeries) => {
const f = numberFormatter(intl, s?.format ?? format);
return (n: number) => f(n) + (s?.suffix ?? '');
};
const step = (w: number) => w / Math.max(1, win - 1);
const shiftStyle = (px: number) => (!reduced && tick > 0 && !hold ? ({ '--kitlive-step': `${round(px)}px`, '--kitlive-t': `${interval}ms` } as React.CSSProperties) : undefined);
// Events: their own simulation
const [evts, setEvts] = useState<LiveEvent[]>(events);
const [clock, setClock] = useState(0);
const counter = useRef(1040);
useEffect(() => {
if (variant !== 'events') return;
const id = setInterval(() => {
if (paused || hover !== null) return;
setClock((c) => c + 1);
setEvts((prev) => {
const aged = prev.map((e) => ({ ...e, ago: e.ago + interval / 1000 })).filter((e) => e.ago <= 60);
if (simulate && Math.random() < 0.45 && eventNames.length) {
counter.current += 1;
const nm = eventNames[Math.floor(Math.random() * eventNames.length)].replace('#', `#${counter.current}`);
const base = prev.length ? prev.reduce((a, e) => a + e.value, 0) / prev.length : 40;
aged.push({ label: nm, value: round(Math.max(5, base * (0.4 + Math.random() * 1.3)), 2), ago: 0 });
}
return aged;
});
}, interval);
return () => clearInterval(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variant, simulate, interval, paused, hover]);
const status = (
<button
type="button"
onClick={() => setPaused((p) => !p)}
aria-pressed={paused}
aria-label={paused ? t.play : t.pause}
className="relative inline-flex h-9 items-center gap-2 rounded-md border border-border px-2.5 text-[12px] font-medium text-foreground transition-colors hover:bg-accent after:absolute after:-inset-y-1 after:inset-x-0 after:content-['']"
>
<span aria-hidden className="relative flex size-2">
{!paused && hover === null && <span className="absolute inset-0 animate-ping rounded-full bg-success opacity-60 motion-reduce:animate-none" />}
<span className={cn('relative size-2 rounded-full', paused ? 'bg-muted-foreground' : 'bg-success')} />
</span>
{paused ? t.paused : t.live}
<Icon name={paused ? 'play' : 'pause'} size={14} className="text-muted-foreground" />
</button>
);
const agoText = (k: number, len: number) => {
const s = Math.round(((len - 1 - k) * interval) / 1000);
return s <= 0 ? t.now : t.ago.replace('{n}', String(s));
};
/* ---------------- multi: live tiles ---------------- */
if (variant === 'multi') {
const cols = width >= 720 ? Math.min(3, series.length) : width >= 440 ? 2 : 1;
return (
<ChartFigure title={title} value={value} delta={delta} meta={meta} source={source} framed={framed} actions={status} className={className}>
<ChartRoot anim={anim} rootRef={rootRef}>
<style>{CSS}</style>
<div ref={wrapRef}>
<ul className="grid gap-px overflow-hidden rounded-lg border border-border bg-border" style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }} onPointerEnter={(e) => e.pointerType === 'mouse' && setHover(0)} onPointerLeave={() => setHover(null)}>
{series.map((s, i) => {
const b = bufs[i] ?? [];
const f = fmtOf(s);
const lo = s.min ?? Math.min(...s.values);
const hi = s.max ?? Math.max(...s.values);
const w = Math.max(120, (width - (cols - 1)) / cols - 40);
const h = 56;
const X = (k: number) => round(k * step(w));
const Y = linear(lo, hi, h - 3, 3);
const last = b[b.length - 1] ?? 0;
const prev = b[b.length - 2] ?? last;
return (
<li key={s.label} className="bg-card px-5 pb-4 pt-4">
<p className="flex items-center justify-between gap-2 text-[13px] text-muted-foreground">
<span className="truncate">{s.label}</span>
<span className={cn('inline-flex items-center text-[12px] tabular-nums', last > prev ? 'text-foreground' : 'text-muted-foreground')}>
<Icon name={last >= prev ? 'arrow-up-right' : 'arrow-down-right'} size={13} strokeWidth={2} />
</span>
</p>
<p className="mt-1 text-[26px] font-semibold leading-none tracking-[-0.02em] text-foreground tabular-nums">{f(last)}</p>
<svg width="100%" height={h} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" aria-hidden className="mt-3 block overflow-hidden">
<g key={tick} className={shiftStyle(step(w)) ? 'kitlive-shift' : undefined} style={shiftStyle(step(w))}>
<path d={`${linePath(b.map((v, k) => [X(k + (win - b.length)), Y(v)]))}L${X(win - 1)} ${h}L${X(win - b.length)} ${h}Z`} fill={pairColor(i)} fillOpacity="0.1" />
<path d={linePath(b.map((v, k) => [X(k + (win - b.length)), Y(v)]))} fill="none" stroke={pairColor(i)} strokeWidth="2" vectorEffect="non-scaling-stroke" />
</g>
</svg>
</li>
);
})}
{/* Empty cells close the last row, so the hairline grid has no grey gap */}
{Array.from({ length: (cols - (series.length % cols)) % cols }, (_, k) => (
<li key={`fill${k}`} aria-hidden className="bg-card" />
))}
</ul>
</div>
<ChartTable caption={name} head={[t.value, t.latest]} rows={series.map((s, i) => [s.label, fmtOf(s)(bufs[i]?.[bufs[i].length - 1] ?? 0)])} />
</ChartRoot>
</ChartFigure>
);
}
/* ---------------- events ---------------- */
if (variant === 'events') {
const f = numberFormatter(intl, format);
const height = 96;
const left = 8;
const right = width - 8;
const X = (ago: number) => round(right - (ago / 60) * (right - left));
const vmax = Math.max(1, ...evts.map((e) => e.value));
const recent = [...evts].sort((a, b) => a.ago - b.ago).slice(0, phone ? 4 : 5);
const total = evts.reduce((a, e) => a + e.value, 0);
return (
<ChartFigure title={title} value={value} delta={delta} meta={meta} source={source} framed={framed} actions={status} className={className}>
<ChartRoot anim={anim} rootRef={rootRef}>
<style>{CSS}</style>
<div ref={wrapRef} onPointerEnter={(e) => e.pointerType === 'mouse' && setHover(0)} onPointerLeave={() => setHover(null)}>
<ChartSurface label={name} summary={summary ?? `${evts.length} · ${f(total)}`} labels={labels} live="" table={<ChartTable caption={name} head={[t.event, t.value, t.time]} rows={recent.map((e) => [e.label, f(e.value), t.ago.replace('{n}', String(Math.round(e.ago)))])} />} focusable={false}>
<svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden className="block overflow-visible">
<line x1={left} x2={right} y1={height / 2} y2={height / 2} stroke="var(--kitch-axis)" strokeWidth="1" />
{(phone ? [0, 30, 60] : [0, 15, 30, 45, 60]).map((s) => (
<g key={s}>
<line x1={X(s)} x2={X(s)} y1={height / 2 - 4} y2={height / 2 + 4} stroke="var(--kitch-axis)" strokeWidth="1" />
<text x={X(s)} y={height - 4} textAnchor={s === 0 ? 'end' : s === 60 ? 'start' : 'middle'} className="text-[11px] tabular-nums" fill="var(--muted-foreground)">
{s === 0 ? t.now : t.ago.replace('{n}', String(s))}
</text>
</g>
))}
{evts.map((e) => {
const r = round(4 + Math.sqrt(e.value / vmax) * 10);
return (
<g key={e.label} style={{ transform: `translateX(${X(e.ago)}px)`, transition: reduced ? undefined : `transform ${interval}ms linear` }}>
{e.ago < 1.5 && !reduced && <circle cx={0} cy={height / 2} r={r} fill="var(--primary)" className="kitlive-ping" />}
<circle cx={0} cy={height / 2} r={r} fill="var(--primary)" fillOpacity={0.35 + 0.65 * (1 - e.ago / 60)} stroke="var(--kitch-surface)" strokeWidth="2" />
</g>
);
})}
</svg>
</ChartSurface>
<ul aria-live="polite" aria-relevant="additions" className="mt-4 grid divide-y divide-border rounded-lg border border-border">
{recent.map((e) => (
<li key={e.label} className="kitlive-in flex items-center justify-between gap-3 px-3.5 py-2.5 text-[13px]">
<span className="flex min-w-0 items-center gap-2">
<span aria-hidden className="size-2 shrink-0 rounded-full bg-primary" />
<span className="truncate text-foreground">{e.label}</span>
</span>
<span className="flex shrink-0 items-baseline gap-3">
<span className="font-medium tabular-nums text-foreground">{f(e.value)}</span>
<span className="w-16 text-right text-[12px] tabular-nums text-muted-foreground">{e.ago < 1 ? t.now : t.ago.replace('{n}', String(Math.round(e.ago)))}</span>
</span>
</li>
))}
</ul>
<span className="sr-only">{clock}</span>
</div>
</ChartRoot>
</ChartFigure>
);
}
/* ---------------- line, bars, latency ---------------- */
const s0 = series[0];
const f0 = fmtOf(s0);
const lo = Math.min(...series.map((s) => s.min ?? Math.min(...s.values)), limit ?? Infinity);
const hi = Math.max(...series.map((s) => s.max ?? Math.max(...s.values)), limit ?? -Infinity);
const nice = niceTicks(variant === 'bars' ? 0 : lo, hi, 4);
const tk = tickFormatter(intl, s0?.format ?? format);
const yLabels = nice.ticks.map((v) => tk(v) + (s0?.suffix && !/[%]/.test(tk(v)) ? (s0.suffix.trim().length <= 2 ? s0.suffix : '') : ''));
const height = phone ? 220 : 260;
const left = Math.max(...yLabels.map((s) => textWidth(s, 11)), 16) + 14;
const right = width - (phone ? 10 : 64);
const top = 16;
const bottom = height - 26;
const Y = linear(nice.min, nice.max, bottom, top);
const w = right - left;
const X = (k: number) => round(left + k * step(w));
const b0 = bufs[0] ?? [];
const lastV = b0[b0.length - 1] ?? 0;
const pick = (clientX: number, el: HTMLElement) => {
const r = el.getBoundingClientRect();
const px = (clientX - r.left) * (width / (r.width || width));
const k = Math.round((px - left) / step(w)) - (win - b0.length);
setHover(k >= 0 && k < b0.length ? k : null);
};
const onKeyDown = (e: React.KeyboardEvent) => {
const cur = hover ?? b0.length;
let next: number | null = cur;
if (e.key === 'ArrowLeft') next = Math.max(0, cur - 1);
else if (e.key === 'ArrowRight') next = Math.min(b0.length - 1, cur + 1);
else if (e.key === 'Escape') next = null;
else if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
return setPaused((p) => !p);
} else return;
e.preventDefault();
setHover(next);
};
const off = (b: number[]) => win - b.length;
const shift = shiftStyle(step(w));
const hoverVal = hover !== null ? b0[hover] : null;
const legend =
series.length > 1 ? (
<ul className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-[12px] text-muted-foreground">
{series.map((s, i) => (
<li key={s.label} className="flex items-center gap-1.5">
<Swatch shape="line" color={pairColor(i)} />
{s.label}
<span className="font-medium tabular-nums text-foreground">{fmtOf(s)((bufs[i] ?? [])[(bufs[i] ?? []).length - 1] ?? 0)}</span>
</li>
))}
{limit !== undefined && (
<li className="flex items-center gap-1.5">
<Swatch shape="line" dashed color="var(--kitfg-down)" />
{limitLabel}
</li>
)}
</ul>
) : null;
return (
<ChartFigure title={title} value={value} delta={delta} meta={meta} source={source} framed={framed} actions={status} className={className}>
<ChartRoot anim={anim} rootRef={rootRef}>
<style>{CSS}</style>
{legend}
<div ref={wrapRef}>
<ChartSurface
label={name}
summary={summary ?? `${s0?.label ?? ''} ${f0(lastV)}`}
labels={labels}
live={hover !== null && hoverVal !== null && hoverVal !== undefined ? `${agoText(hover, b0.length)}. ${f0(hoverVal)}` : ''}
table={<ChartTable caption={name} head={[t.time, ...series.map((s) => s.label)]} rows={b0.slice(-10).map((_, k) => { const idx = b0.length - 10 + k; return [agoText(idx, b0.length), ...series.map((s, i) => fmtOf(s)((bufs[i] ?? [])[idx] ?? 0))]; })} />}
onKeyDown={onKeyDown}
onFocus={(e) => e.target === e.currentTarget && e.currentTarget.matches(':focus-visible') && setHover(b0.length - 1)}
onBlur={() => setHover(null)}
onPointerMove={(e) => pick(e.clientX, e.currentTarget)}
onPointerDown={(e) => pick(e.clientX, e.currentTarget)}
onPointerLeave={(e) => e.pointerType === 'mouse' && setHover(null)}
>
<svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden className="block overflow-visible">
<defs>
<clipPath id={clipId}>
<rect x={left} y={0} width={w + 2} height={height} />
</clipPath>
</defs>
<PlotAxes left={left} right={right} top={top} bottom={bottom} height={height} yTicks={nice.ticks.map((v, i) => ({ at: Y(v), label: yLabels[i], strong: v === 0 }))} />
<text x={right} y={height - 6} textAnchor="end" className="text-[11px]" fill="var(--muted-foreground)">{t.now}</text>
<text x={left} y={height - 6} className="text-[11px] tabular-nums" fill="var(--muted-foreground)">{t.ago.replace('{n}', String(Math.round(((win - 1) * interval) / 1000)))}</text>
{limit !== undefined && (
<g>
<line x1={left} x2={right} y1={Y(limit)} y2={Y(limit)} stroke="var(--kitfg-down)" strokeWidth="1.5" strokeDasharray="5 4" />
</g>
)}
<g clipPath={`url(#${clipId})`}>
<g key={tick} className={shift ? 'kitlive-shift' : undefined} style={shift}>
{variant === 'bars' &&
b0.map((v, k) => {
const bw = Math.max(2, step(w) * 0.62);
const x = X(k + off(b0)) - bw / 2;
return <path key={k} d={barPath(x, Y(v), bw, Y(nice.min) - Y(v), 2, 'up')} fill={k === b0.length - 1 ? 'var(--primary)' : 'color-mix(in oklab, var(--primary) 45%, var(--card))'} opacity={hover !== null && hover !== k ? 0.55 : 1} />;
})}
{variant === 'latency' && series.length > 1 && (
<path d={`${linePath((bufs[1] ?? []).map((v, k) => [X(k + off(bufs[1])), Y(v)]))}L${X(win - 1)} ${Y((bufs[0] ?? [])[(bufs[0] ?? []).length - 1] ?? 0)}${[...(bufs[0] ?? [])].map((v, k) => [X(k + off(bufs[0])), Y(v)] as [number, number]).reverse().map(([x, y]) => `L${x} ${y}`).join('')}Z`} fill="var(--primary)" fillOpacity="0.12" />
)}
{variant !== 'bars' &&
series.map((s, i) => {
const b = bufs[i] ?? [];
return <path key={s.label} d={linePath(b.map((v, k) => [X(k + off(b)), Y(v)]))} fill="none" stroke={variant === 'latency' ? (i === 0 ? 'var(--primary)' : 'var(--kitfg-3)') : pairColor(i)} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />;
})}
{variant === 'line' && <path d={`${linePath(b0.map((v, k) => [X(k + off(b0)), Y(v)]))}L${X(win - 1)} ${bottom}L${X(off(b0))} ${bottom}Z`} fill="var(--primary)" fillOpacity="0.08" />}
</g>
</g>
{/* Newest point, pulsing, and its value at the right edge */}
{variant !== 'bars' && b0.length > 0 && (
<g>
{!hold && !reduced && <circle cx={X(win - 1)} cy={Y(lastV)} r={4.5} fill="var(--primary)" className="kitlive-ping" />}
<circle cx={X(win - 1)} cy={Y(lastV)} r={4.5} fill="var(--primary)" stroke="var(--kitch-surface)" strokeWidth="2" className="transition-[cy] duration-300" />
</g>
)}
{!phone && (
<g style={{ transform: `translateY(${round(Y(lastV))}px)` }} className="transition-transform duration-300">
<rect x={right + 8} y={-10} width={textWidth(f0(lastV), 11) + 12} height={20} rx="4" fill="var(--foreground)" />
<text x={right + 14} y={0} dy="0.34em" className="text-[11px] font-semibold tabular-nums" fill="var(--background)">{f0(lastV)}</text>
</g>
)}
{hover !== null && hoverVal !== undefined && hoverVal !== null && (
<g>
<line x1={X(hover + off(b0))} x2={X(hover + off(b0))} y1={top} y2={bottom} stroke="var(--kitch-axis)" strokeWidth="1" />
<circle cx={X(hover + off(b0))} cy={Y(hoverVal)} r={4.5} fill="var(--primary)" stroke="var(--kitch-surface)" strokeWidth="2" />
<text x={X(hover + off(b0))} y={top - 4} textAnchor={X(hover + off(b0)) > right - 60 ? 'end' : X(hover + off(b0)) < left + 60 ? 'start' : 'middle'} className="text-[11px] font-semibold tabular-nums" fill="var(--foreground)" style={HALO}>
{`${f0(hoverVal)} · ${agoText(hover, b0.length)}`}
</text>
</g>
)}
</svg>
</ChartSurface>
<p className="mt-2 flex min-h-5 items-center gap-1.5 text-[12px] text-muted-foreground">
{hover !== null && !paused ? (
<>
<Icon name="pause" size={13} />
{t.held}
</>
) : variant === 'bars' ? (
`${s0?.label ?? ''} ${t.perSec}`
) : null}
</p>
</div>
</ChartRoot>
</ChartFigure>
);
}Häufige Fragen
Wie installiere ich Live-Diagramm?
Mit aiw add @ai-website/live-chart. Der Befehl legt die Datei components/ui/live-chart.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, chart und chart-figure. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Live-Diagramm?
5 Varianten, Gleitende Linie, Balken je Sekunde, Ereignisspur, Live-Kacheln und Latenzband. Du wählst sie mit der Prop variant, zum Beispiel variant="bars".
Kann ich die Texte von Live-Diagramm übersetzen?
Ja. Live-Diagramm bringt 12 eigene Texte auf Deutsch und Englisch mit, etwa „Aktualisierung anhalten“, „Aktualisierung fortsetzen“ und „Live“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Live-Diagramm einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Live-Diagramm mit aiw_components und installieren sie mit aiw_add @ai-website/live-chart. Nenn im Prompt die Variante, etwa variant="line", dann schreibt der Assistent deine Inhalte direkt in die Props.
Kann ich Live-Diagramm ohne Code anpassen?
Ja, im Studio. Alle 5 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden LiveChart-Code zum Kopieren.