{sub}
} +{label}
+ {/* Proportional figures: tabular-nums makes a big standalone number look loose. */} +{value}
+ {sub &&{sub}
} +{empty}
; + return ( +{empty}
; + // Thin the x labels once the bars get narrow. A label under EVERY one of 48 + // hourly bars is unreadable mush — the axis exists to orient, not to enumerate + // (the value is in the tooltip, and every number is in the table view). + const every = data.length <= 16 ? 1 : Math.ceil(data.length / 12); + return ( + // The container includes the x-axis band — a fixed height that excludes it is + // how cards end up with a tiny nested scrollbar. +{empty}
; + + const W = 1000, H = 100, peak = Math.max(1, ...data.map((d) => d.count)); + const x = (i: number) => (i / (data.length - 1)) * W; + const y = (v: number) => H - (v / peak) * H; + const line = data.map((d, i) => `${i === 0 ? 'M' : 'L'}${x(i).toFixed(1)},${y(d.count).toFixed(1)}`).join(' '); + const area = `${line} L${W},${H} L0,${H} Z`; + const peakIdx = data.reduce((b, d, i) => (d.count > data[b].count ? i : b), 0); + const h = hover ?? -1; + + return ( +{empty}
; + + const slots = data.slice(0, 8); // never generate a 9th hue + const R = 58, SW = 22, C = 2 * Math.PI * R; + let acc = 0; + const arcs = slots.map((d, i) => { + const frac = d.count / total; + // A 2px surface gap between segments — the correct separator, not a border. + const len = Math.max(0, frac * C - 2); + const a = { key: d.key, i, len, off: -acc * C, frac }; + acc += frac; + return a; + }); + + return ( +{title}
+| {d.key} | +{nf(d.count)} | ++ {total ? ((d.count / total) * 100).toFixed(1) + '%' : '—'} + | +
| — |
{t('stats.avgWindow')}
+{stats.avg_per_hour.toFixed(1)} /h
+{t('stats.avgActive')}
+{stats.avg_per_active.toFixed(1)} /h
+{t('stats.best60')}
+{nf(stats.best_60)}
+{t('stats.activeHours')}
++ {dur(stats.on_air_minutes)} + / {Math.round(stats.window_hours)} h +
+{t('stats.rateTooLong')}
} +{t('stats.noGaps')}
+ ) : ( +