From 3698fd85d5f0d791c19bffb7076e41e499125985 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Fri, 15 May 2026 15:50:20 +0200 Subject: [PATCH] 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 * 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 --------- Co-authored-by: multica-agent --- packages/ui/components/ui/chart.tsx | 16 ++++++++++++++++ packages/views/locales/en/runtimes.json | 3 ++- packages/views/locales/zh-Hans/runtimes.json | 3 ++- .../components/charts/daily-cost-chart.tsx | 18 ++++++++++++++++++ .../components/charts/daily-tokens-chart.tsx | 18 ++++++++++++++++++ 5 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/ui/components/ui/chart.tsx b/packages/ui/components/ui/chart.tsx index 72f6fb116..8d09b7322 100644 --- a/packages/ui/components/ui/chart.tsx +++ b/packages/ui/components/ui/chart.tsx @@ -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({ ) })} + {footer != null && ( +
+ {typeof footer === "function" ? footer(payload) : footer} +
+ )} ) } diff --git a/packages/views/locales/en/runtimes.json b/packages/views/locales/en/runtimes.json index 8dfcc7623..32ed75fc1 100644 --- a/packages/views/locales/en/runtimes.json +++ b/packages/views/locales/en/runtimes.json @@ -154,7 +154,8 @@ }, "charts": { "heatmap_less": "Less", - "heatmap_more": "More" + "heatmap_more": "More", + "tooltip_total": "Total" }, "list": { "col_runtime": "Runtime", diff --git a/packages/views/locales/zh-Hans/runtimes.json b/packages/views/locales/zh-Hans/runtimes.json index 9ecdc0f17..c1aba9772 100644 --- a/packages/views/locales/zh-Hans/runtimes.json +++ b/packages/views/locales/zh-Hans/runtimes.json @@ -149,7 +149,8 @@ }, "charts": { "heatmap_less": "少", - "heatmap_more": "多" + "heatmap_more": "多", + "tooltip_total": "总计" }, "list": { "col_runtime": "运行时", diff --git a/packages/views/runtimes/components/charts/daily-cost-chart.tsx b/packages/views/runtimes/components/charts/daily-cost-chart.tsx index 625716618..5f9ee2032 100644 --- a/packages/views/runtimes/components/charts/daily-cost-chart.tsx +++ b/packages/views/runtimes/components/charts/daily-cost-chart.tsx @@ -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 ( +
+ {t(($) => $.charts.tooltip_total)} + + ${total.toFixed(2)} + +
+ ); + }} /> } /> diff --git a/packages/views/runtimes/components/charts/daily-tokens-chart.tsx b/packages/views/runtimes/components/charts/daily-tokens-chart.tsx index 27da0408e..48110e6f1 100644 --- a/packages/views/runtimes/components/charts/daily-tokens-chart.tsx +++ b/packages/views/runtimes/components/charts/daily-tokens-chart.tsx @@ -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 ( +
+ {t(($) => $.charts.tooltip_total)} + + {total.toLocaleString()} + +
+ ); + }} /> } />