import React, { useEffect, useMemo, useRef, useState } from 'react' export const nf = new Intl.NumberFormat('en-US') export const fmt = (n) => (n == null || isNaN(n) ? '—' : nf.format(Math.round(n))) export const pct = (g) => (g == null || isNaN(g) ? '—' : (g > 0 ? '+' : '') + (g * 100).toFixed(0) + '%') export function useCountUp(target, dur = 900) { const [v, setV] = useState(0) useEffect(() => { let raf const t0 = performance.now() const tick = (t) => { const p = Math.min(1, (t - t0) / dur) setV(Math.round(target * (1 - Math.pow(1 - p, 3)))) if (p < 1) raf = requestAnimationFrame(tick) } raf = requestAnimationFrame(tick) return () => cancelAnimationFrame(raf) }, [target, dur]) return v } export function Stat({ k, v, d, dCls = '', sub }) { const n = useCountUp(v) return (
{k}
{fmt(n)}
{d != null &&
{d}
} {sub &&
{sub}
}
) } export function Sparkline({ data, w = 90, h = 26, color = 'var(--accent)', area = true }) { if (!data || data.length < 2) return null const max = Math.max(1, ...data) const min = Math.min(...data) const span = Math.max(1e-9, max - min) const pts = data.map((v, i) => [ (i / (data.length - 1)) * (w - 4) + 2, h - 3 - ((v - min) / span) * (h - 8), ]) const line = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ') return ( ) } // columnar series: {day:[], dl_all:[], dl30:[], likes:[]} // bars = daily downloads (diff of dl_all), line = cumulative export function BigChart({ series, height = 300 }) { const wrapRef = useRef(null) const [hover, setHover] = useState(null) const { days, daily, cum } = useMemo(() => { const out = { days: [], daily: [], cum: [] } if (!series || !series.day || series.day.length < 2) return out for (let i = 1; i < series.day.length; i++) { out.days.push(series.day[i]) out.daily.push(Math.max(0, series.dl_all[i] - series.dl_all[i - 1])) out.cum.push(series.dl_all[i]) } return out }, [series]) if (days.length < 2) { return

Not enough history for this model yet.

} const W = 1000, H = height, pad = { l: 56, r: 14, t: 16, b: 26 } const iw = W - pad.l - pad.r, ih = H - pad.t - pad.b const dmax = Math.max(1, ...daily) const cmax = Math.max(1, ...cum) const x = (i) => pad.l + (i / (daily.length - 1)) * iw const yd = (v) => pad.t + ih - (v / dmax) * ih const yc = (v) => pad.t + ih - (v / cmax) * ih const bw = Math.max(1, iw / daily.length - 1.2) const bars = daily.map((v, i) => ( 3 ? 1.5 : 0} fill={hover === i ? 'var(--accent)' : 'var(--bar)'} opacity={hover == null || hover === i ? 1 : 0.85} /> )) const cpts = cum.map((v, i) => x(i).toFixed(1) + ',' + yc(v).toFixed(1)).join(' ') const grid = [0, 1, 2, 3, 4].map((g) => { const gy = pad.t + ih - (g / 4) * ih return ( {fmt((dmax * g) / 4)} ) }) const onMove = (e) => { const rect = wrapRef.current.getBoundingClientRect() const rel = (e.clientX - rect.left) / rect.width setHover(Math.max(0, Math.min(daily.length - 1, Math.round(rel * (daily.length - 1))))) } return (
setHover(null)}> {grid} {bars} {hover != null && ( )} {[days[0], days[Math.floor(days.length / 2)], days[days.length - 1]].map((t, i) => ( {t} ))} {hover != null && (
{days[hover]}
{fmt(daily[hover])} downloads/day
{fmt(cum[hover])} all time
)}
) } // simple area/line chart from (days[], vals[]) — used for the hub-wide pulse export function AreaChart({ days, vals, height = 240 }) { const wrapRef = useRef(null) const [hover, setHover] = useState(null) if (!days || days.length < 2) return null const W = 1000, H = height, pad = { l: 56, r: 14, t: 14, b: 26 } const iw = W - pad.l - pad.r, ih = H - pad.t - pad.b const max = Math.max(1, ...vals) const x = (i) => pad.l + (i / (days.length - 1)) * iw const y = (v) => pad.t + ih - (v / max) * ih const pts = vals.map((v, i) => x(i).toFixed(1) + ',' + y(v).toFixed(1)).join(' ') const grid = [0, 1, 2, 3, 4].map((g) => { const gy = pad.t + ih - (g / 4) * ih return ( {fmt((max * g) / 4)} ) }) const onMove = (e) => { const rect = wrapRef.current.getBoundingClientRect() const rel = (e.clientX - rect.left) / rect.width setHover(Math.max(0, Math.min(days.length - 1, Math.round(rel * (days.length - 1))))) } return (
setHover(null)}> {grid} {hover != null && ( )} {[days[0], days[Math.floor(days.length / 2)], days[days.length - 1]].map((t, i) => ( {t} ))} {hover != null && (
{days[hover]}
{fmt(vals[hover])} downloads
)}
) }