Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/components/claude/daily-by-user-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import { useMemo } from "react";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Legend } from "recharts";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid } from "recharts";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
} from "@/components/ui/chart";
import type { ChartConfig } from "@/components/ui/chart";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
Expand Down Expand Up @@ -86,7 +88,7 @@ export function DailyByUserChart({ data }: { data: DailyByUserResult }) {
}
return row;
}),
[data.days, stackedSeries]
[data.days, stackedSeries],
);

const periodTotal = data.topUsers.reduce((s, u) => s + u.totalCents, 0);
Expand All @@ -102,7 +104,9 @@ export function DailyByUserChart({ data }: { data: DailyByUserResult }) {
{periodTotal > 0 && (
<>
{" · "}
<span className="tabular-nums">{formatCurrency(periodTotal)}</span>{" "}
<span className="tabular-nums">
{formatCurrency(periodTotal)}
</span>{" "}
this period
</>
)}
Expand Down Expand Up @@ -147,7 +151,10 @@ export function DailyByUserChart({ data }: { data: DailyByUserResult }) {
/>
}
/>
<Legend wrapperStyle={{ paddingTop: 8 }} verticalAlign="top" />
<ChartLegend
verticalAlign="top"
content={<ChartLegendContent />}
/>
{stackedSeries.map((s) => (
<Bar
key={s.key}
Expand Down
6 changes: 4 additions & 2 deletions src/components/claude/global-metrics-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import { useState, useTransition, useMemo, useEffect } from "react";
import { useSearchParams } from "next/navigation";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Legend } from "recharts";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid } from "recharts";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
} from "@/components/ui/chart";
import type { ChartConfig } from "@/components/ui/chart";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
Expand Down Expand Up @@ -388,7 +390,7 @@ export function GlobalMetricsClient({
/>
}
/>
<Legend wrapperStyle={{ paddingTop: 8 }} />
<ChartLegend content={<ChartLegendContent />} />
{stackedSeries.map((s) => (
<Bar
key={s.key}
Expand Down
48 changes: 45 additions & 3 deletions src/components/reports/budget/plan-vs-actual-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import type { ComponentProps } from "react";
import {
Bar,
BarChart,
Expand Down Expand Up @@ -37,7 +38,36 @@ const chartConfig: ChartConfig = {
forecast: { label: "Forecast", color: "var(--chart-4)" },
};

export function PlanVsActualChart({ periods, forecast }: PlanVsActualChartProps) {
// "Billed" segments turn red (var(--destructive)) on any month where actual
// spend exceeds the plan — that's a conditional alert state on the Billed
// series, not a series of its own. Recharts colors each legend entry from its
// <Bar>'s fill and ignores per-<Cell> overrides, so the red would otherwise
// never appear in the legend. Append an explicit "Over budget" swatch (only
// when a breach exists) so the red bars are accounted for.
function PlanVsActualLegend({
showOverBudget,
...props
}: ComponentProps<typeof ChartLegendContent> & { showOverBudget: boolean }) {
return (
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3">
<ChartLegendContent {...props} className="!p-0" />
{showOverBudget && (
<div className="flex items-center gap-1.5">
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{ backgroundColor: "var(--destructive)" }}
/>
Over budget
</div>
)}
</div>
);
}

export function PlanVsActualChart({
periods,
forecast,
}: PlanVsActualChartProps) {
const today = new Date();
const projection = buildProjectionLookup(forecast);

Expand All @@ -58,6 +88,12 @@ export function PlanVsActualChart({ periods, forecast }: PlanVsActualChartProps)
? periods.reduce((s, p) => s + p.plannedAmountCents, 0) / periods.length
: 0;

// Mirror the per-Cell red condition below so the legend only explains the
// alert color when at least one month actually breaches its plan.
const anyOverBudget = data.some(
(d) => d.planned > 0 && d.billed + d.running > d.planned,
);

return (
<ChartContainer config={chartConfig} className="h-[340px] w-full">
<BarChart data={data} margin={{ top: 16, right: 16, left: 0, bottom: 0 }}>
Expand Down Expand Up @@ -103,8 +139,14 @@ export function PlanVsActualChart({ periods, forecast }: PlanVsActualChartProps)
/>
}
/>
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="planned" fill="var(--color-planned)" radius={[4, 4, 0, 0]} />
<ChartLegend
content={<PlanVsActualLegend showOverBudget={anyOverBudget} />}
/>
<Bar
dataKey="planned"
fill="var(--color-planned)"
radius={[4, 4, 0, 0]}
/>
<Bar
dataKey="billed"
stackId="actual"
Expand Down
Loading
Loading