fix(panel): charts get real axes, humanized ticks, and dark-theme tooltips (#611)

Every chart hand-rolled its own k-only formatter (22596k-style ticks, left-
clipped y labels), used recharts' default white tooltip (invisible header on
the dark theme), and two charts pinned a numeric XAxis interval that collapses
short series to a single tick. The agent Token Activity chart had no axes at
all and blanked its tooltip date on purpose.

One shared formatTokens/formatBucket (lib/format.ts) and one shared themed
tooltip style (components/charts/chart-tooltip.tsx) now feed all 10 charts;
axis widths/margins sized to the labels; preserveStartEnd tick intervals.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-20 20:29:45 +02:00
committed by GitHub
co-authored by Renn F
parent 3c5ee46347
commit 7248e5b722
15 changed files with 200 additions and 105 deletions
+10 -14
View File
@@ -18,6 +18,7 @@ import {
useUsageSessions,
} from "@/hooks/use-usage";
import type { UsagePeriod } from "@/lib/api/usage";
import { formatTokens } from "@/lib/format";
import { TaskStatus, Team } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
@@ -78,13 +79,6 @@ function humanizeCount(n: number): string {
return String(n);
}
/** Format token counts (same as humanizeCount but used for token display). */
function fmtTokens(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + "M";
if (n >= 1_000) return (n / 1_000).toFixed(1) + "K";
return String(n);
}
// ─── Shared sub-components ────────────────────────────────────────────────────
interface MetricCardProps {
@@ -493,14 +487,14 @@ function TokenUsageCostsSection() {
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 2xl:grid-cols-6">
<SummaryCard
title="Tokens Input"
value={summary ? fmtTokens(summary.tokens_input) : undefined}
value={summary ? formatTokens(summary.tokens_input) : undefined}
icon={<Zap className="h-4 w-4 text-yellow-500" />}
isLoading={loadingSnap}
tip="Prompt/context tokens sent to the model in the selected window"
/>
<SummaryCard
title="Tokens Output"
value={summary ? fmtTokens(summary.tokens_output) : undefined}
value={summary ? formatTokens(summary.tokens_output) : undefined}
icon={<Zap className="h-4 w-4 text-blue-500" />}
isLoading={loadingSnap}
tip="Tokens generated by the model in response, in the selected window"
@@ -531,7 +525,7 @@ function TokenUsageCostsSection() {
/>
<SummaryCard
title="Total Tokens"
value={summary ? fmtTokens(summary.total_tokens) : undefined}
value={summary ? formatTokens(summary.total_tokens) : undefined}
icon={<Activity className="h-4 w-4 text-blue-500" />}
isLoading={loadingSnap}
tip="Input + output tokens combined in the selected window"
@@ -697,8 +691,8 @@ function CacheEfficiencyCard({
<div>
<div className="text-3xl font-bold">{pct.toFixed(1)}%</div>
<p className="text-xs text-muted-foreground mt-1">
{cacheStats ? fmtTokens(cacheStats.tokens_cache_read) : "—"} cache
reads · saved $
{cacheStats ? formatTokens(cacheStats.tokens_cache_read) : "—"}{" "}
cache reads · saved $
{cacheStats?.cost_saved_by_cache_usd.toFixed(4) ?? "—"}
</p>
<Progress value={pct} className="mt-2" />
@@ -892,8 +886,10 @@ const VALID_METRICS_TABS: MetricsTab[] = [
const METRICS_TAB_HINTS: Record<MetricsTab, string> = {
performance: "Task velocity, status counts, agent load, and team health",
"token-usage": "Token spend, cost projections, cache efficiency, and per-session detail",
delivery: "Cycle time, bottlenecks, and rework rate reconstructed from the audit log",
"token-usage":
"Token spend, cost projections, cache efficiency, and per-session detail",
delivery:
"Cycle time, bottlenecks, and rework rate reconstructed from the audit log",
scorecards: "Per-agent and per-team delivery scorecards",
};
@@ -7,6 +7,8 @@ import {
AreaChart,
ResponsiveContainer,
Tooltip as RTooltip,
XAxis,
YAxis,
} from "recharts";
import {
Card,
@@ -21,6 +23,8 @@ import { GitBranch, BookOpen } from "lucide-react";
import { useUsageTimeSeries } from "@/hooks/use-usage";
import { useWorkSessions } from "@/hooks/use-work-sessions";
import { useAgentJournalEntries } from "@/hooks/use-journals";
import { formatTokens, formatBucket } from "@/lib/format";
import { chartTooltipStyle } from "@/components/charts/chart-tooltip";
import { WorkSessionStatus, type UsageTimePoint } from "@/types";
interface AgentActivityPanelProps {
@@ -36,11 +40,6 @@ const SESSION_STATUS_LABEL: Record<WorkSessionStatus, string> = {
[WorkSessionStatus.ABANDONED]: "Abandoned",
};
function fmtK(n: number): string {
if (n >= 1_000) return (n / 1_000).toFixed(0) + "k";
return String(n);
}
type TimelineItem = {
kind: "session" | "journal";
title: string;
@@ -49,8 +48,18 @@ type TimelineItem = {
};
function mergeTimeline(
sessions: { task_id: string; branch_name: string; status: WorkSessionStatus; started_at: string }[],
journals: { title: string; type: string; timestamp: string; task_id: string | null }[],
sessions: {
task_id: string;
branch_name: string;
status: WorkSessionStatus;
started_at: string;
}[],
journals: {
title: string;
type: string;
timestamp: string;
task_id: string | null;
}[],
): TimelineItem[] {
const items: TimelineItem[] = [
...sessions.map((s) => ({
@@ -62,12 +71,17 @@ function mergeTimeline(
...journals.map((j) => ({
kind: "journal" as const,
title: j.title,
subtitle: j.task_id ? `Task ${j.task_id.slice(0, 8)} · ${j.type}` : j.type,
subtitle: j.task_id
? `Task ${j.task_id.slice(0, 8)} · ${j.type}`
: j.type,
timestamp: j.timestamp,
})),
];
return items
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.sort(
(a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
)
.slice(0, 8);
}
@@ -92,7 +106,9 @@ export function AgentActivityPanel({
[sessions, journals],
);
const timelineLoading = sessionsLoading || journalsLoading;
const hasTokens = (series ?? []).some((p: UsageTimePoint) => p.total_tokens > 0);
const hasTokens = (series ?? []).some(
(p: UsageTimePoint) => p.total_tokens > 0,
);
return (
<div className="grid gap-4 md:grid-cols-2">
@@ -127,13 +143,27 @@ export function AgentActivityPanel({
/>
</linearGradient>
</defs>
<XAxis
dataKey="bucket"
tickFormatter={formatBucket}
tick={{ fontSize: 9 }}
axisLine={false}
tickLine={false}
/>
<YAxis
tickFormatter={formatTokens}
tick={{ fontSize: 9 }}
axisLine={false}
tickLine={false}
width={36}
/>
<RTooltip
{...chartTooltipStyle}
formatter={(value) => [
fmtK(typeof value === "number" ? value : 0),
formatTokens(typeof value === "number" ? value : 0),
"Tokens",
]}
contentStyle={{ fontSize: 12 }}
labelFormatter={() => ""}
labelFormatter={(label) => formatBucket(String(label))}
/>
<Area
type="monotone"
@@ -212,4 +242,4 @@ export function AgentActivityPanel({
</Card>
</div>
);
}
}
@@ -12,6 +12,8 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { formatBucket } from "@/lib/format";
import { chartTooltipStyle } from "@/components/charts/chart-tooltip";
import type { UsageTimePoint } from "@/types";
interface SpendTrendChartProps {
@@ -19,11 +21,6 @@ interface SpendTrendChartProps {
isLoading: boolean;
}
function formatBucket(bucket: string): string {
const d = new Date(bucket);
return d.getMonth() + 1 + "/" + d.getDate();
}
function fmtCost(n: number): string {
return "$" + n.toFixed(2);
}
@@ -63,7 +60,7 @@ export function SpendTrendChart({ data, isLoading }: SpendTrendChartProps) {
<XAxis
dataKey="day"
tick={{ fontSize: 9 }}
interval={4}
interval="preserveStartEnd"
axisLine={false}
tickLine={false}
/>
@@ -75,13 +72,17 @@ export function SpendTrendChart({ data, isLoading }: SpendTrendChartProps) {
width={44}
/>
<Tooltip
{...chartTooltipStyle}
formatter={(value) => [
fmtCost(typeof value === "number" ? value : 0),
"Spend",
]}
contentStyle={{ fontSize: 12 }}
/>
<Bar dataKey="Spend" fill="var(--chart-3)" radius={[3, 3, 0, 0]} />
<Bar
dataKey="Spend"
fill="var(--chart-3)"
radius={[3, 3, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
)}
@@ -0,0 +1,18 @@
import type { CSSProperties } from "react";
// Shared recharts <Tooltip> styling — a bare `contentStyle={{fontSize:12}}`
// renders recharts' default white content box with the default (also-white)
// label text, so on the dark theme the tooltip is a white box with an
// invisible date header. Pull both colors from the popover CSS vars instead.
export const chartTooltipStyle: {
contentStyle: CSSProperties;
labelStyle: CSSProperties;
} = {
contentStyle: {
backgroundColor: "var(--popover)",
color: "var(--popover-foreground)",
border: "1px solid var(--border)",
fontSize: 12,
},
labelStyle: { color: "var(--popover-foreground)" },
};
@@ -12,6 +12,8 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { formatBucket } from "@/lib/format";
import { chartTooltipStyle } from "@/components/charts/chart-tooltip";
import type { UsageTimePoint } from "@/types";
interface CostTrendChartProps {
@@ -19,11 +21,6 @@ interface CostTrendChartProps {
isLoading: boolean;
}
function formatBucket(bucket: string): string {
const d = new Date(bucket);
return d.getMonth() + 1 + "/" + d.getDate();
}
function fmtCost(n: number): string {
return "$" + n.toFixed(2);
}
@@ -88,11 +85,11 @@ export function CostTrendChart({ data, isLoading }: CostTrendChartProps) {
width={44}
/>
<Tooltip
{...chartTooltipStyle}
formatter={(value) => [
fmtCost(typeof value === "number" ? value : 0),
"Cost",
]}
contentStyle={{ fontSize: 12 }}
/>
<Area
type="monotone"
@@ -15,6 +15,8 @@ import { Skeleton } from "@/components/ui/skeleton";
import { SegmentedControl } from "@/components/ui/segmented-control";
import { HelpTip } from "@/components/ui/help-tip";
import { useIsMobile } from "@/hooks/use-is-mobile";
import { formatTokens } from "@/lib/format";
import { chartTooltipStyle } from "@/components/charts/chart-tooltip";
import type { AgentUsageRow } from "@/types";
interface AgentUsageChartProps {
@@ -22,11 +24,6 @@ interface AgentUsageChartProps {
isLoading: boolean;
}
function fmtK(n: number): string {
if (n >= 1_000) return (n / 1_000).toFixed(0) + "k";
return String(n);
}
const VIEW_OPTIONS = [
{ value: "chart", label: "Chart" },
{ value: "table", label: "Table" },
@@ -101,7 +98,7 @@ export function AgentUsageChart({ data, isLoading }: AgentUsageChartProps) {
<ResponsiveContainer width="100%" height={208}>
<BarChart
data={chartData}
margin={{ top: 4, right: 8, left: 0, bottom: 24 }}
margin={{ top: 4, right: 8, left: 0, bottom: isMobile ? 40 : 32 }}
>
<CartesianGrid strokeDasharray="3 3" className="opacity-20" />
<XAxis
@@ -114,18 +111,18 @@ export function AgentUsageChart({ data, isLoading }: AgentUsageChartProps) {
tickLine={false}
/>
<YAxis
tickFormatter={fmtK}
tickFormatter={formatTokens}
tick={{ fontSize: 10 }}
axisLine={false}
tickLine={false}
width={36}
width={46}
/>
<Tooltip
{...chartTooltipStyle}
formatter={(value) => [
fmtK(typeof value === "number" ? value : 0),
formatTokens(typeof value === "number" ? value : 0),
"Tokens",
]}
contentStyle={{ fontSize: 12 }}
/>
<Bar
dataKey="Tokens"
@@ -25,6 +25,7 @@ import {
useRework,
useTeamScorecard,
} from "@/hooks/use-observability";
import { chartTooltipStyle } from "@/components/charts/chart-tooltip";
import type { Scorecard } from "@/types";
const CELLS = ["backend", "frontend", "ux_ui"] as const;
@@ -91,8 +92,8 @@ function CycleTimeCard() {
tickFormatter={(v) => v + "h"}
/>
<Tooltip
{...chartTooltipStyle}
formatter={(value) => [value + "h", "Avg"]}
contentStyle={{ fontSize: 12 }}
/>
<Bar
dataKey="Hours"
@@ -12,6 +12,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { useIsMobile } from "@/hooks/use-is-mobile";
import { chartTooltipStyle } from "@/components/charts/chart-tooltip";
import type { ModelUsageSlice } from "@/types";
// Design-system chart tokens — resolves to theme-aware palette
@@ -71,12 +72,12 @@ export function ModelUsageDonut({ data, isLoading }: ModelUsageDonutProps) {
))}
</Pie>
<Tooltip
{...chartTooltipStyle}
formatter={(value, name) => [
(typeof value === "number" ? value : 0).toLocaleString() +
" tokens",
name,
]}
contentStyle={{ fontSize: 12 }}
/>
<Legend wrapperStyle={{ fontSize: isMobile ? 9 : 11 }} />
</PieChart>
+19 -15
View File
@@ -21,6 +21,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { ChevronUp, ChevronDown } from "lucide-react";
import { formatTokens } from "@/lib/format";
import type { UsageSession } from "@/types";
const PAGE_SIZE = 10;
@@ -46,8 +47,16 @@ interface Column {
}
const COLUMNS: Column[] = [
{ key: "agent_slug", label: "Agent", tip: "The agent slug that ran this session — click to sort" },
{ key: "model", label: "Model", tip: "Claude/Grok model used for this session — click to sort" },
{
key: "agent_slug",
label: "Agent",
tip: "The agent slug that ran this session — click to sort",
},
{
key: "model",
label: "Model",
tip: "Claude/Grok model used for this session — click to sort",
},
{
key: "started_at",
label: "Started",
@@ -87,11 +96,6 @@ function formatTime(ts: string): string {
});
}
function fmtK(n: number): string {
if (n >= 1_000) return (n / 1_000).toFixed(1) + "k";
return String(n);
}
interface SessionsTableProps {
data: UsageSession[] | undefined;
isLoading: boolean;
@@ -198,16 +202,16 @@ export function SessionsTable({ data, isLoading }: SessionsTableProps) {
{formatTime(s.started_at)}
</TableCell>
<TableCell className="text-xs">
{fmtK(s.total_tokens)}
{formatTokens(s.total_tokens)}
</TableCell>
<TableCell className="text-xs">
{fmtK(s.tokens_input)}
{formatTokens(s.tokens_input)}
</TableCell>
<TableCell className="text-xs">
{fmtK(s.tokens_output)}
{formatTokens(s.tokens_output)}
</TableCell>
<TableCell className="text-xs">
{fmtK(s.tokens_cache)}
{formatTokens(s.tokens_cache)}
</TableCell>
<TableCell className="text-xs">
${s.cost.toFixed(4)}
@@ -241,16 +245,16 @@ export function SessionsTable({ data, isLoading }: SessionsTableProps) {
{formatTime(s.started_at)}
</ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Total">
{fmtK(s.total_tokens)}
{formatTokens(s.total_tokens)}
</ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Input">
{fmtK(s.tokens_input)}
{formatTokens(s.tokens_input)}
</ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Output">
{fmtK(s.tokens_output)}
{formatTokens(s.tokens_output)}
</ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Cache">
{fmtK(s.tokens_cache)}
{formatTokens(s.tokens_cache)}
</ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Cost">
${s.cost.toFixed(4)}
@@ -11,6 +11,7 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { chartTooltipStyle } from "@/components/charts/chart-tooltip";
interface TaskStatusSlice {
name: string;
@@ -73,11 +74,11 @@ export function TaskStatusChart({ slices, isLoading }: TaskStatusChartProps) {
))}
</Pie>
<Tooltip
{...chartTooltipStyle}
formatter={(value, name) => [
`${typeof value === "number" ? value : 0} tasks`,
name,
]}
contentStyle={{ fontSize: 12 }}
/>
<Legend wrapperStyle={{ fontSize: 11 }} />
</PieChart>
@@ -86,4 +87,4 @@ export function TaskStatusChart({ slices, isLoading }: TaskStatusChartProps) {
</CardContent>
</Card>
);
}
}
@@ -15,6 +15,8 @@ import { Skeleton } from "@/components/ui/skeleton";
import { SegmentedControl } from "@/components/ui/segmented-control";
import { HelpTip } from "@/components/ui/help-tip";
import { useIsMobile } from "@/hooks/use-is-mobile";
import { formatTokens } from "@/lib/format";
import { chartTooltipStyle } from "@/components/charts/chart-tooltip";
import type { TeamUsageRow } from "@/types";
interface TeamUsageChartProps {
@@ -22,11 +24,6 @@ interface TeamUsageChartProps {
isLoading: boolean;
}
function fmtK(n: number): string {
if (n >= 1_000) return (n / 1_000).toFixed(0) + "k";
return String(n);
}
const VIEW_OPTIONS = [
{ value: "chart", label: "Chart" },
{ value: "table", label: "Table" },
@@ -104,7 +101,7 @@ export function TeamUsageChart({ data, isLoading }: TeamUsageChartProps) {
top: 4,
right: 8,
left: 0,
bottom: isMobile ? 24 : 8,
bottom: isMobile ? 40 : 8,
}}
>
<CartesianGrid strokeDasharray="3 3" className="opacity-20" />
@@ -118,18 +115,18 @@ export function TeamUsageChart({ data, isLoading }: TeamUsageChartProps) {
tickLine={false}
/>
<YAxis
tickFormatter={fmtK}
tickFormatter={formatTokens}
tick={{ fontSize: 10 }}
axisLine={false}
tickLine={false}
width={36}
width={46}
/>
<Tooltip
{...chartTooltipStyle}
formatter={(value) => [
fmtK(typeof value === "number" ? value : 0),
formatTokens(typeof value === "number" ? value : 0),
"Tokens",
]}
contentStyle={{ fontSize: 12 }}
/>
<Bar
dataKey="Tokens"
@@ -14,6 +14,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { useIsMobile } from "@/hooks/use-is-mobile";
import { formatTokens, formatBucket } from "@/lib/format";
import { chartTooltipStyle } from "@/components/charts/chart-tooltip";
import type { UsageTimePoint } from "@/types";
interface UsageTimeSeriesChartProps {
@@ -21,23 +23,6 @@ interface UsageTimeSeriesChartProps {
isLoading: boolean;
}
function formatBucket(bucket: string): string {
const d = new Date(bucket);
// If the bucket has a non-zero time component it is an hourly bucket → show HH:00.
// Otherwise it is a daily bucket → show MM/DD.
const isHourly =
d.getMinutes() === 0 && (d.getHours() !== 0 || bucket.includes("T"));
if (isHourly && d.getSeconds() === 0 && !bucket.endsWith("T00:00:00.000Z")) {
return d.getHours().toString().padStart(2, "0") + ":00";
}
return d.getMonth() + 1 + "/" + d.getDate();
}
function fmtK(n: number): string {
if (n >= 1_000) return (n / 1_000).toFixed(0) + "k";
return String(n);
}
export function UsageTimeSeriesChart({
data,
isLoading,
@@ -99,23 +84,23 @@ export function UsageTimeSeriesChart({
<XAxis
dataKey="hour"
tick={{ fontSize: isMobile ? 9 : 10 }}
interval={isMobile ? 5 : 3}
interval="preserveStartEnd"
axisLine={false}
tickLine={false}
/>
<YAxis
tickFormatter={fmtK}
tickFormatter={formatTokens}
tick={{ fontSize: 10 }}
axisLine={false}
tickLine={false}
width={36}
width={46}
/>
<Tooltip
{...chartTooltipStyle}
formatter={(value, name) => [
fmtK(typeof value === "number" ? value : 0),
formatTokens(typeof value === "number" ? value : 0),
name,
]}
contentStyle={{ fontSize: 12 }}
/>
<Legend wrapperStyle={{ fontSize: isMobile ? 10 : 12 }} />
<Area
@@ -13,6 +13,7 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { chartTooltipStyle } from "@/components/charts/chart-tooltip";
import type { WorkSessionSummary } from "@/types";
interface SessionTrendChartProps {
@@ -112,13 +113,17 @@ export function SessionTrendChart({
width={28}
/>
<Tooltip
{...chartTooltipStyle}
formatter={(value) => [
typeof value === "number" ? value : 0,
"Sessions started",
]}
contentStyle={{ fontSize: 12 }}
/>
<Bar dataKey="count" fill="var(--chart-1)" radius={[3, 3, 0, 0]} />
<Bar
dataKey="count"
fill="var(--chart-1)"
radius={[3, 3, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
)}
+39
View File
@@ -0,0 +1,39 @@
import { describe, it, expect } from "vitest";
import { formatTokens, formatBucket } from "@/lib/format";
describe("formatTokens", () => {
it("renders raw digits below 1000", () => {
expect(formatTokens(0)).toBe("0");
expect(formatTokens(999)).toBe("999");
});
it("renders K with one decimal at the 1000 boundary", () => {
expect(formatTokens(1_000)).toBe("1.0K");
expect(formatTokens(22_596)).toBe("22.6K");
expect(formatTokens(999_999)).toBe("1000.0K");
});
it("renders M with two decimals at the 1,000,000 boundary", () => {
expect(formatTokens(1_000_000)).toBe("1.00M");
expect(formatTokens(22_596_000)).toBe("22.60M");
});
});
describe("formatBucket", () => {
it("formats a midnight-UTC daily bucket as MM/DD", () => {
// Regression guard: a plain "T00:00:00.000Z" bucket must never be
// mistaken for an hourly bucket just because minutes/seconds are 0.
expect(formatBucket("2026-07-15T00:00:00.000Z")).toBe(
new Date("2026-07-15T00:00:00.000Z").getMonth() +
1 +
"/" +
new Date("2026-07-15T00:00:00.000Z").getDate(),
);
});
it("formats a non-midnight hourly bucket as HH:00", () => {
const bucket = "2026-07-15T14:00:00.000Z";
const expectedHour = new Date(bucket).getHours().toString().padStart(2, "0");
expect(formatBucket(bucket)).toBe(`${expectedHour}:00`);
});
});
+23
View File
@@ -0,0 +1,23 @@
// Shared number/time formatters for usage & metrics charts — was
// reimplemented per-chart (inconsistent K/M rounding caused "22596k"-style
// ticks); one copy so every chart humanizes the same way.
/** Format token counts with a K/M suffix — M keeps 2 decimals, K keeps 1. */
export function formatTokens(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + "M";
if (n >= 1_000) return (n / 1_000).toFixed(1) + "K";
return String(n);
}
/** Format a usage time-bucket ISO string as "HH:00" (hourly) or "MM/DD" (daily). */
export function formatBucket(bucket: string): string {
const d = new Date(bucket);
// If the bucket has a non-zero time component it is an hourly bucket → show HH:00.
// Otherwise it is a daily bucket → show MM/DD.
const isHourly =
d.getMinutes() === 0 && (d.getHours() !== 0 || bucket.includes("T"));
if (isHourly && d.getSeconds() === 0 && !bucket.endsWith("T00:00:00.000Z")) {
return d.getHours().toString().padStart(2, "0") + ":00";
}
return d.getMonth() + 1 + "/" + d.getDate();
}