Stats: activity heatmap as a rolling window of the last 14 days
One row per calendar day (today at the bottom) instead of weekdays aggregated over the selected range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
01c1484033
commit
d14c609576
5 files changed
+46
-23
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { Icon } from "./Icon";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useFetch } from "../hooks";
|
||||
import { fmtCompact, fmtCost, fmtDate, fmtNumber, fmtPercent, getLang, useT, weekdayNames, type Key } from "../i18n";
|
||||
import { fmtCompact, fmtCost, fmtDate, fmtNumber, fmtPercent, getLang, useT, type Key } from "../i18n";
|
||||
import { navigate } from "../router";
|
||||
import { useStore } from "../store";
|
||||
import type { User } from "../types";
|
||||
@@ -19,7 +19,8 @@ interface StatsData {
|
||||
by_provider: Row<"provider">[];
|
||||
by_agent: Row<"agent">[];
|
||||
by_user: Row<"user">[];
|
||||
heatmap: (Aggregate & { slot: number })[];
|
||||
/** Last 14 days: slot = "YYYY-MM-DD HH" (local time). */
|
||||
heatmap: (Aggregate & { slot: string })[];
|
||||
top_sessions: (Aggregate & { session_id: string; title: string })[];
|
||||
currency: string;
|
||||
from: number;
|
||||
@@ -106,6 +107,8 @@ function AggTable({ rows, keyName, label, currency, link }: { rows: (Aggregate &
|
||||
);
|
||||
}
|
||||
|
||||
const HEAT_DAYS = 14;
|
||||
|
||||
export function Stats({ onMenu }: { onMenu: () => void }) {
|
||||
const t = useT();
|
||||
const [range, setRange] = useState("30");
|
||||
@@ -146,12 +149,31 @@ export function Stats({ onMenu }: { onMenu: () => void }) {
|
||||
const d = data.by_day.find((x) => x.day === c);
|
||||
return d ? { input: d.input_tokens + d.cache_write_tokens, cache: d.cache_read_tokens, output: d.output_tokens } : {};
|
||||
});
|
||||
const cells = Array<number>(168).fill(0);
|
||||
// Rolling window: the last 14 days, today at the bottom.
|
||||
const iso = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
const heatDays = Array.from({ length: HEAT_DAYS }, (_, i) => {
|
||||
const d = new Date();
|
||||
d.setHours(12, 0, 0, 0);
|
||||
d.setDate(d.getDate() - (HEAT_DAYS - 1 - i));
|
||||
return iso(d);
|
||||
});
|
||||
const rowOf = new Map(heatDays.map((d, i) => [d, i]));
|
||||
const cells = Array<number>(HEAT_DAYS * 24).fill(0);
|
||||
for (const h of data.heatmap) {
|
||||
cells[h.slot] =
|
||||
const [day, hour] = h.slot.split(" ");
|
||||
const row = rowOf.get(day);
|
||||
if (row === undefined) continue;
|
||||
cells[row * 24 + Number(hour)] =
|
||||
heat === "requests" ? h.requests : heat === "cost" ? h.cost : h.input_tokens + h.cache_read_tokens + h.cache_write_tokens + h.output_tokens;
|
||||
}
|
||||
return { cats, modelSeries, costByDay, tokenSeries, tokensByDay, cells };
|
||||
const wd = new Intl.DateTimeFormat(getLang(), { weekday: "short" });
|
||||
const dm = new Intl.DateTimeFormat(getLang(), { day: "numeric", month: "numeric" });
|
||||
const full = new Intl.DateTimeFormat(getLang(), { weekday: "short", day: "numeric", month: "short" });
|
||||
const heatRows = heatDays.map((d) => {
|
||||
const date = new Date(`${d}T12:00:00`);
|
||||
return { label: `${wd.format(date)} ${dm.format(date)}`, title: full.format(date) };
|
||||
});
|
||||
return { cats, modelSeries, costByDay, tokenSeries, tokensByDay, cells, heatRows };
|
||||
}, [data, heat, t]);
|
||||
|
||||
const dayLabel = (d: string) =>
|
||||
@@ -242,7 +264,7 @@ export function Stats({ onMenu }: { onMenu: () => void }) {
|
||||
<option value="cost">{t("stats.cost")}</option>
|
||||
</select>
|
||||
</h3>
|
||||
<Heatmap cells={view.cells} weekdays={weekdayNames()} format={heatFormat} />
|
||||
<Heatmap cells={view.cells} rows={view.heatRows} format={heatFormat} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user