From 0f9a4e76b96a6cbd57f646641bf2d883690edc5c Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 12 Aug 2026 17:11:24 +1000 Subject: [PATCH] PM-5861: Add totals to dashboard report tooltips What was broken Dashboard report tooltips listed each series value for a month but did not show the combined monthly total. Root cause The shared Highcharts configuration only defined the header and per-series rows. It had no formatter or footer that calculated a total, and stack totals would not cover the grouped Challenge Participation report. What was changed Extended the shared dashboard tooltip formatter to preserve the existing Highcharts content, sum every hovered series point, and append a comma-formatted Total row. Currency reports include the dollar prefix while count reports remain unit-free. Any added/updated tests Updated DashboardChart coverage for currency totals, stacked count totals, and grouped count totals. All 13 Reports suites (54 tests), lint, and the production build pass. The full repository test command was run twice and retains 18 unrelated failures in existing Work, Wallet Admin, Profiles, and Engagements tests. --- .../pages/dashboards/DashboardChart.spec.tsx | 90 ++++++++++++++++--- .../src/pages/dashboards/DashboardChart.tsx | 25 +++++- 2 files changed, 101 insertions(+), 14 deletions(-) diff --git a/src/apps/reports/src/pages/dashboards/DashboardChart.spec.tsx b/src/apps/reports/src/pages/dashboards/DashboardChart.spec.tsx index bc9eb3227..5d26cade2 100644 --- a/src/apps/reports/src/pages/dashboards/DashboardChart.spec.tsx +++ b/src/apps/reports/src/pages/dashboards/DashboardChart.spec.tsx @@ -8,21 +8,23 @@ import { import Highcharts from 'highcharts' import { + ChallengeParticipationDashboard, MemberPaymentByCustomerDashboard, NewSignupsDashboard, } from '../../lib/services' import { DashboardChart } from './DashboardChart' +let chartOptions: Highcharts.Options + jest.mock('highcharts-react-official', () => ({ __esModule: true, - default: (props: { - options: { - series?: Array<{ data?: number[]; name?: string }> - tooltip?: { pointFormat?: string } - } - }): JSX.Element => { - const series = props.options.series || [] + default: (props: { options: Highcharts.Options }): JSX.Element => { + chartOptions = props.options + const series = props.options.series as Array<{ + data?: number[] + name?: string + }> || [] return (
({ data-series-names={series.map(item => item.name) .join('|')} data-testid='dashboard-chart' - data-tooltip={props.options.tooltip?.pointFormat} /> ) }, @@ -96,10 +97,61 @@ const signupResponse: NewSignupsDashboard = { }, } +const challengeParticipationResponse: ChallengeParticipationDashboard = { + dashboard: 'challenge-participation', + endDate: '2026-02-01T00:00:00.000Z', + months: [{ + month: '2026-01-01', + registrants: 120, + submitters: 75, + }], + startDate: '2026-01-01T00:00:00.000Z', + summary: { + peakMonth: '2026-01-01', + peakMonthRegistrants: 120, + submissionRate: 62.5, + totalUniqueRegistrants: 120, + totalUniqueSubmitters: 75, + }, +} const pointValueToken = '{point.y:,.0f}' const countTooltipValue = `${pointValueToken}` const currencyTooltipValue = `$${pointValueToken}` +/** + * Invokes the tooltip formatter captured from the rendered chart. + * + * Tests use this helper to verify totals independently of Highcharts' DOM renderer. + * + * @param values Hovered series values for one month. + * @returns The formatter's tooltip HTML as one string. + * @throws Error when the chart has no formatter or it returns no HTML. + */ +function formatTooltip(values: number[]): string { + const formatter = chartOptions.tooltip?.formatter + if (!formatter) { + throw new Error('Tooltip formatter is missing') + } + + const context = { + points: values.map(y => ({ y })), + } as unknown as Highcharts.TooltipFormatterContextObject + const tooltip = { + defaultFormatter: () => ['Jan ’26
', 'series rows'], + } as unknown as Highcharts.Tooltip + const result = formatter.call(context, tooltip) + + if (typeof result === 'string') { + return result + } + + if (Array.isArray(result)) { + return result.join('') + } + + throw new Error('Tooltip formatter did not return HTML') +} + describe('DashboardChart', () => { it('formats tooltip thousands with commas', () => { expect(Highcharts.getOptions().lang?.thousandsSep) @@ -126,8 +178,10 @@ describe('DashboardChart', () => { 'data-series-data', '[[125000,140000],[80000,0],[20000,25000]]', ) - expect(chart.getAttribute('data-tooltip')) + expect(chartOptions.tooltip?.pointFormat) .toContain(currencyTooltipValue) + expect(formatTooltip([125_000, 80_000, 20_000])) + .toContain('Total: $225,000') expect(within(table) .getByRole('columnheader', { name: 'Customer A' })) .toBeInTheDocument() @@ -142,20 +196,30 @@ describe('DashboardChart', () => { .toBeInTheDocument() }) - it('keeps existing count dashboards unit-free', () => { + it('keeps count dashboard tooltips unit-free and adds their total', () => { render() - const chart = screen.getByTestId('dashboard-chart') const table = screen.getByRole('table', { name: 'New Signups by Month monthly data', }) - expect(chart.getAttribute('data-tooltip')) + expect(chartOptions.tooltip?.pointFormat) .toContain(countTooltipValue) - expect(chart.getAttribute('data-tooltip')) + expect(chartOptions.tooltip?.pointFormat) .not.toContain(currencyTooltipValue) + expect(formatTooltip([90, 10])) + .toContain('Total: 100') + expect(formatTooltip([90, 10])) + .not.toContain('$100') expect(within(table) .getByRole('cell', { name: '90' })) .toBeInTheDocument() }) + + it('adds a total to grouped report tooltips', () => { + render() + + expect(formatTooltip([120, 75])) + .toContain('Total: 195') + }) }) diff --git a/src/apps/reports/src/pages/dashboards/DashboardChart.tsx b/src/apps/reports/src/pages/dashboards/DashboardChart.tsx index 54182e43a..eeeab09db 100644 --- a/src/apps/reports/src/pages/dashboards/DashboardChart.tsx +++ b/src/apps/reports/src/pages/dashboards/DashboardChart.tsx @@ -50,7 +50,7 @@ function getSeriesValue(month: DashboardMonth, key: string): number { * * @param props Dashboard response and compact-card presentation flag. * @returns A stacked or grouped column chart with month categories along the - * bottom axis and an accessible monthly data table. + * bottom axis, monthly tooltip totals, and an accessible monthly data table. * @throws Does not throw. Invalid or absent point values are rendered as zero. */ export const DashboardChart: FC = props => { @@ -119,6 +119,29 @@ export const DashboardChart: FC = props => { text: undefined, }, tooltip: { + /** + * Appends the hovered month's total to Highcharts' shared tooltip. + * + * Highcharts invokes this callback for card and detail charts. + * + * @param tooltip Highcharts tooltip used to render the existing rows. + * @returns The default tooltip content followed by the formatted total. + * @throws Does not throw for the normalized chart-series values. + */ + formatter(tooltip: Highcharts.Tooltip) { + // Highcharts supplies the shared tooltip context through `this`. + // eslint-disable-next-line react/no-this-in-sfc + const points = this.points || [this] + const total = points.reduce((sum, point) => sum + (point.y ?? 0), 0) + // eslint-disable-next-line react/no-this-in-sfc + const content = tooltip.defaultFormatter.call(this, tooltip) + const totalRow = `Total: ${isCurrency ? '$' : ''}` + + `${Highcharts.numberFormat(total, 0)}` + + return Array.isArray(content) + ? [...content, totalRow] + : `${content}${totalRow}` + }, headerFormat: '{point.key}
', pointFormat: ' ' + `{series.name}: ${isCurrency ? '$' : ''}{point.y:,.0f}
`,