cube-tui 0.1.7

Terminal UI timer and session manager for speedcubing, with optional web dashboard and BLE (GAN) timer support.
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import type { Time } from "../../types/types";
import { effectiveMs, formatMillis } from "../../utils/format";

interface TimeTrendModuleProps {
    times: Time[];
}

export function TimeTrendModule({ times }: TimeTrendModuleProps) {
    const chartData = times.map((time, index) => ({
        solve: index + 1,
        ms: effectiveMs(time),
    }));

    const validTimes = chartData.map((point) => point.ms).filter((ms): ms is number => ms !== null);

    const dnfCount = chartData.length - validTimes.length;

    if (validTimes.length === 0) {
        return (
            <section className="rounded-xl border border-border bg-surface animate-fade-in-up shadow-lg p-5">
                <p className="text-[10px] uppercase tracking-widest text-text-dim font-semibold mb-2">Time Trend</p>
                <p className="text-sm text-text-muted">No valid solves to chart.</p>
            </section>
        );
    }

    const minMs = Math.min(...validTimes);
    const maxMs = Math.max(...validTimes);
    const rangePadding = Math.max(1000, Math.round((maxMs - minMs) * 0.08));
    const yMin = Math.max(0, minMs - rangePadding);
    const yMax = maxMs + rangePadding;

    return (
        <section className="rounded-xl border border-border bg-surface animate-fade-in-up shadow-lg p-5">
            <div className="flex items-center justify-between mb-3">
                <p className="text-[10px] uppercase tracking-widest text-text-dim font-semibold">Time Trend</p>
                <span className="text-xs text-text-muted">
                    <span className="font-mono tabular-nums">{chartData.length}</span> solve{chartData.length === 1 ? "" : "s"} ยท{" "}
                    <span className="font-mono tabular-nums">{dnfCount}</span> DNF
                </span>
            </div>

            <div className="h-52">
                <ResponsiveContainer width="100%" height="100%">
                    <LineChart data={chartData} margin={{ top: 8, right: 8, bottom: 8, left: 8 }}>
                        <CartesianGrid stroke="var(--border)" strokeDasharray="3 3" vertical={false} />
                        <XAxis
                            dataKey="solve"
                            tick={{ fill: "var(--text-dim)", fontSize: 10 }}
                            tickLine={false}
                            axisLine={{ stroke: "var(--border)" }}
                            label={{
                                value: "Solve #",
                                position: "insideBottom",
                                offset: -4,
                                fill: "var(--text-dim)",
                                fontSize: 10,
                            }}
                        />
                        <YAxis
                            domain={[yMin, yMax]}
                            tick={{ fill: "var(--text-dim)", fontSize: 10 }}
                            tickLine={false}
                            axisLine={{ stroke: "var(--border)" }}
                            tickFormatter={(value) => formatMillis(value as number)}
                            width={64}
                        />
                        <Tooltip
                            cursor={{ stroke: "var(--border-hover)", strokeWidth: 1 }}
                            contentStyle={{
                                background: "var(--surface)",
                                border: "1px solid var(--border)",
                                borderRadius: "0.5rem",
                                color: "var(--text)",
                            }}
                            formatter={(value) => formatMillis(value as number)}
                            labelFormatter={(label) => `Solve ${label}`}
                        />
                        <Line
                            type="monotone"
                            dataKey="ms"
                            connectNulls={false}
                            stroke="var(--accent)"
                            strokeWidth={2.5}
                            dot={{ r: 2, fill: "var(--accent)", strokeWidth: 0 }}
                            activeDot={{ r: 4, fill: "var(--accent)" }}
                        />
                    </LineChart>
                </ResponsiveContainer>
            </div>

            <div className="mt-2 flex items-center justify-between text-[10px] text-text-dim font-mono tabular-nums">
                <span>{formatMillis(minMs)}</span>
                <span>{formatMillis(maxMs)}</span>
            </div>
        </section>
    );
}