feat(views): show Total in daily token/cost chart tooltips (MUL-2282) (#2704)

* feat(views): show Total in daily token/cost chart tooltips (MUL-2282)

Add a Total row at the bottom of the daily-tokens-chart and daily-cost-chart
tooltips so users can see the precise stack sum on hover, in addition to the
per-stack breakdown.

Implemented by extending shared ChartTooltipContent with an optional `footer`
prop (ReactNode | (payload) => ReactNode) that renders below the items with a
top divider; backwards-compatible (no behavior change when footer is omitted).

Co-authored-by: multica-agent <github@multica.ai>

* fix(views): i18n Total label in chart tooltips (MUL-2282)

Lint rule i18next/no-literal-string flagged the hardcoded "Total" string
in daily-cost-chart and daily-tokens-chart tooltips. Move it to
runtimes.charts.tooltip_total and read via useT.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Jiayuan Zhang
2026-05-15 15:50:20 +02:00
committed by GitHub
parent d43961ed7a
commit 3698fd85d5
5 changed files with 56 additions and 2 deletions

View File

@@ -127,6 +127,7 @@ function ChartTooltipContent({
labelFormatter,
labelClassName,
formatter,
footer,
color,
nameKey,
labelKey,
@@ -137,6 +138,16 @@ function ChartTooltipContent({
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
footer?:
| React.ReactNode
| ((
payload: NonNullable<
RechartsPrimitive.DefaultTooltipContentProps<
TooltipValueType,
TooltipNameType
>["payload"]
>,
) => React.ReactNode)
} & Omit<
RechartsPrimitive.DefaultTooltipContentProps<
TooltipValueType,
@@ -266,6 +277,11 @@ function ChartTooltipContent({
)
})}
</div>
{footer != null && (
<div className="mt-0.5 border-t border-border/50 pt-1.5">
{typeof footer === "function" ? footer(payload) : footer}
</div>
)}
</div>
)
}

View File

@@ -154,7 +154,8 @@
},
"charts": {
"heatmap_less": "Less",
"heatmap_more": "More"
"heatmap_more": "More",
"tooltip_total": "Total"
},
"list": {
"col_runtime": "Runtime",

View File

@@ -149,7 +149,8 @@
},
"charts": {
"heatmap_less": "少",
"heatmap_more": "多"
"heatmap_more": "多",
"tooltip_total": "总计"
},
"list": {
"col_runtime": "运行时",

View File

@@ -12,6 +12,7 @@ import {
type ChartConfig,
} from "@multica/ui/components/ui/chart";
import type { DailyCostStackData } from "../../utils";
import { useT } from "../../../i18n";
// Three-segment stack (input / output / cache write) — keeps the user's
// attention on what's actually driving spend. Cache reads are excluded
@@ -29,6 +30,7 @@ export const costStackConfig = {
} satisfies ChartConfig;
export function DailyCostChart({ data }: { data: DailyCostStackData[] }) {
const { t } = useT("runtimes");
// No internal empty-state — the parent decides what to show in place of
// the chart (often a diagnostic explaining *why* there's no cost). Letting
// recharts render an empty axis would be both ugly and uninformative.
@@ -58,6 +60,22 @@ export function DailyCostChart({ data }: { data: DailyCostStackData[] }) {
? `$${value.toFixed(2)} ${name}`
: `${value} ${name}`
}
footer={(payload) => {
const total = payload.reduce(
(sum, item) =>
sum +
(typeof item.value === "number" ? item.value : 0),
0,
);
return (
<div className="flex items-center justify-between gap-2 font-medium">
<span>{t(($) => $.charts.tooltip_total)}</span>
<span className="font-mono tabular-nums">
${total.toFixed(2)}
</span>
</div>
);
}}
/>
}
/>

View File

@@ -12,6 +12,7 @@ import {
type ChartConfig,
} from "@multica/ui/components/ui/chart";
import { formatTokens, type DailyTokenData } from "../../utils";
import { useT } from "../../../i18n";
// Four-segment stack — input / output / cache read / cache write. Unlike the
// cost chart, cache reads ARE visible here: a typical day on Claude shows
@@ -32,6 +33,7 @@ export const tokenStackConfig = {
} satisfies ChartConfig;
export function DailyTokensChart({ data }: { data: DailyTokenData[] }) {
const { t } = useT("runtimes");
// No internal empty-state — same convention as DailyCostChart: the parent
// decides what to render when there's nothing to show.
return (
@@ -60,6 +62,22 @@ export function DailyTokensChart({ data }: { data: DailyTokenData[] }) {
? `${formatTokens(value)} ${name}`
: `${value} ${name}`
}
footer={(payload) => {
const total = payload.reduce(
(sum, item) =>
sum +
(typeof item.value === "number" ? item.value : 0),
0,
);
return (
<div className="flex items-center justify-between gap-2 font-medium">
<span>{t(($) => $.charts.tooltip_total)}</span>
<span className="font-mono tabular-nums">
{total.toLocaleString()}
</span>
</div>
);
}}
/>
}
/>