mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user