diff --git a/app/src/pages/report-output/SocietyWideOverview.tsx b/app/src/pages/report-output/SocietyWideOverview.tsx index 1975d40b3..39008ee8e 100644 --- a/app/src/pages/report-output/SocietyWideOverview.tsx +++ b/app/src/pages/report-output/SocietyWideOverview.tsx @@ -3,6 +3,7 @@ import { IconChartBar, IconCoin, IconHome, + IconInfoCircle, IconMap, IconScale, IconUsers, @@ -13,7 +14,16 @@ import { normalizeDistrictId } from '@/adapters/congressional-district/congressi import { SocietyWideReportOutput } from '@/api/societyWideCalculation'; import DashboardCard from '@/components/report/DashboardCard'; import MetricCard from '@/components/report/MetricCard'; -import { Group, Progress, SegmentedControl, Stack, Text } from '@/components/ui'; +import { + Group, + Progress, + SegmentedControl, + Stack, + Text, + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui'; import { MapTypeToggle } from '@/components/visualization/choropleth/MapTypeToggle'; import type { MapVisualizationType } from '@/components/visualization/choropleth/types'; import { USDistrictChoroplethMap } from '@/components/visualization/USDistrictChoroplethMap'; @@ -60,6 +70,10 @@ const GRID_GAP = 16; // outer: 200×3 + 16×2 = 632, minus border(2) + padding(32) + controls(39) = 559 // minus map Box border (2px) = 557 const CONGRESSIONAL_MAP_H = 557; +const CONGRESSIONAL_OUTCOME_TOOLTIP_TEXT = + 'Winner % and Loser % show the share of people in households whose net income changes by at least 0.1%. Winners gain more than 0.1%; losers lose at least 0.1%. These shares are people-weighted when household size data is available.'; +const CONGRESSIONAL_OUTCOME_TOOLTIP_LABEL = + 'Explain congressional district winner and loser percentages'; // Poverty segmented control types and options type PovertyDepth = 'regular' | 'deep'; @@ -1113,7 +1127,8 @@ export default function SocietyWideOverview({ const cardHeader = ( IconComponent: React.ComponentType<{ size: number; color: string; stroke: number }>, labelText: string, - hero = false + hero = false, + tooltipText?: string ) => (
{labelText} + {tooltipText && ( + + + + + + {tooltipText} + + + )} ); @@ -1514,7 +1555,12 @@ export default function SocietyWideOverview({ mode={modeOf('congressional')} zIndex={zOf('congressional')} gridGap={GRID_GAP} - header={cardHeader(IconMap, 'Congressional district impact')} + header={cardHeader( + IconMap, + 'Congressional district impact', + false, + CONGRESSIONAL_OUTCOME_TOOLTIP_TEXT + )} onToggleMode={() => toggle('congressional')} /> )} diff --git a/app/src/tests/unit/pages/report-output/SocietyWideOverview.test.tsx b/app/src/tests/unit/pages/report-output/SocietyWideOverview.test.tsx index 53bb187bf..fcf934bf5 100644 --- a/app/src/tests/unit/pages/report-output/SocietyWideOverview.test.tsx +++ b/app/src/tests/unit/pages/report-output/SocietyWideOverview.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@test-utils'; +import { render, screen, userEvent } from '@test-utils'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import SocietyWideOverview, { buildOutcomeMapData, @@ -273,6 +273,24 @@ describe('SocietyWideOverview', () => { expect(screen.getByText('Winners and losers')).toBeInTheDocument(); }); + test('congressional card exposes a tooltip explaining winner and loser percentages', async () => { + const user = userEvent.setup(); + + render( + + ); + + await user.hover( + screen.getByRole('button', { + name: 'Explain congressional district winner and loser percentages', + }) + ); + + expect(await screen.findByRole('tooltip')).toHaveTextContent( + /Winner % and Loser % show the share of people in households whose net income changes by at least 0.1%./ + ); + }); + test('buildOutcomeMapData reads winner shares from the district payload and tracks gaps', () => { const labelLookup = new Map([ ['AL-01', "Alabama's 1st congressional district"],