import { type KeyboardEvent, type PointerEvent, useState } from "react" import { useBreite } from "@/lib/breite" import { flaechenPfad, linienPfad } from "@/lib/diagramm" import { type Einheit, obergrenze, wertText, zeitText } from "@/lib/messung" import type { Messpunkt } from "@/lib/typen" export interface Reihe { name: string punkte: Messpunkt[] /** CSS-Farbe der Linie, z. B. var(--reihe-1). Text trägt sie nie — nur die Marke daneben. */ farbe: string } const RAND = { rechts: 12, oben: 10, unten: 24 } /** * Zeitverlauf einer oder zweier Messreihen: dünne Linien, bei einer Reihe eine zarte Fläche, haarfeines Raster. * Ein Fadenkreuz folgt Zeiger oder Pfeiltasten und zeigt alle Reihen am selben Zeitpunkt. Darunter liegen die * Werte zum Aufklappen als Tabelle — der Tooltip ist nie der einzige Weg zu einer Zahl. */ export function Verlauf({ reihen, einheit, langerZeitraum, hoehe = 180, titel }: { reihen: Reihe[] einheit: Einheit /** 7 Tage: Zeitachse mit Wochentag. */ langerZeitraum: boolean hoehe?: number titel: string }) { const [ref, breite] = useBreite() const [index, setIndex] = useState(null) const basis = reihen[0]?.punkte ?? [] const n = basis.length const max = obergrenze(reihen.map((r) => r.punkte), einheit) const ticksY = einheit === "prozent" ? [0, 25, 50, 75, 100] : [0, max / 2, max] // Der linke Rand richtet sich nach der längsten Beschriftung („500 KB/s“ braucht mehr als „75 %“). const links = Math.max(40, Math.max(...ticksY.map((v) => wertText(v, einheit).length)) * 7 + 12) const RAND_L = { ...RAND, links } const innenB = breite - RAND_L.links - RAND.rechts const innenH = hoehe - RAND.oben - RAND.unten const t0 = basis[0]?.[0] ?? 0 const t1 = basis[n - 1]?.[0] ?? 1 const x = (t: number) => RAND_L.links + (t1 > t0 ? ((t - t0) / (t1 - t0)) * innenB : 0) const y = (v: number) => RAND.oben + innenH - (Math.min(v, max) / (max || 1)) * innenH const grund = RAND.oben + innenH const ticksX = n > 1 ? [0, 0.25, 0.5, 0.75, 1].map((f) => basis[Math.round(f * (n - 1))][0]) : [] const hatWerte = reihen.some((r) => r.punkte.some(([, v]) => v !== null)) function zeigen(e: PointerEvent) { if (n === 0) return const r = e.currentTarget.getBoundingClientRect() const anteil = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)) setIndex(Math.round(anteil * (n - 1))) } function taste(e: KeyboardEvent) { if (n === 0) return if (e.key === "ArrowLeft") setIndex((i) => Math.max(0, (i ?? n - 1) - 1)) else if (e.key === "ArrowRight") setIndex((i) => Math.min(n - 1, (i ?? n - 1) + 1)) else return e.preventDefault() } const aktiv = index !== null && basis[index] ? basis[index][0] : null const tipLinks = aktiv !== null ? x(aktiv) : 0 const letzte = basis.slice(-12) return (
{reihen.length > 1 && (
    {reihen.map((r) => (
  • {r.name}
  • ))}
)}
setIndex((i) => i ?? (n ? n - 1 : null))} onBlur={() => setIndex(null)} className="block max-w-full outline-none focus-visible:rounded-md focus-visible:ring-2 focus-visible:ring-cyan/60" > {ticksY.map((v) => ( {wertText(v, einheit)} ))} {ticksX.map((t, i) => ( {zeitText(t, langerZeitraum)} ))} {hatWerte ? ( reihen.map((r) => ( {reihen.length === 1 && } )) ) : ( Noch keine Messwerte in diesem Zeitraum )} {aktiv !== null && ( {reihen.map((r) => { const v = r.punkte[index!]?.[1] return v === null || v === undefined ? null : ( ) })} )} {/* Trefferfläche: die ganze Zeichenfläche — niemand muss eine 2-px-Linie treffen. */} setIndex(null)} /> {aktiv !== null && (
breite / 2 ? { right: breite - tipLinks + 10 } : { left: tipLinks + 10 }} > {zeitText(aktiv, true)} {reihen.map((r) => ( {wertText(r.punkte[index!]?.[1], einheit)} {reihen.length > 1 && {r.name}} ))}
)}
Werte als Tabelle {reihen.map((r) => )} {letzte.map(([t], i) => ( {reihen.map((r) => ( ))} ))}
Zeit{r.name}
{zeitText(t, langerZeitraum)} {wertText(r.punkte[n - letzte.length + i]?.[1], einheit)}
) }