diff --git a/dotcom-rendering/src/components/ElectionTrackers/StackedProgress.stories.tsx b/dotcom-rendering/src/components/ElectionTrackers/StackedProgress.stories.tsx new file mode 100644 index 0000000000..deba2d610a --- /dev/null +++ b/dotcom-rendering/src/components/ElectionTrackers/StackedProgress.stories.tsx @@ -0,0 +1,152 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { allModes } from '../../../.storybook/modes'; +import { palette } from '../../palette'; +import { StackedProgress } from './StackedProgress'; + +const meta = { + title: 'Components/Election Trackers/Stacked Progress', + component: StackedProgress, + decorators: (Story) => ( +
+ +
+ ), + parameters: { + viewport: { + defaultViewport: 'mobileLandscape', + }, + chromatic: { + modes: { + 'vertical mobileLandscape': + allModes['vertical mobileLandscape'], + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const UKGeneral = { + args: { + total: 650, + toWinCopy: 'for majority', + sections: [ + { + name: 'Labour', + colour: palette('--uk-elections-labour'), + value: 400, + align: 'left', + }, + { + name: 'Conservative', + colour: palette('--uk-elections-conservative'), + value: 100, + align: 'right', + }, + { + name: 'Lib Dem', + colour: palette('--uk-elections-lib-dem'), + value: 70, + align: 'left', + }, + { + name: 'SNP', + colour: palette('--uk-elections-snp'), + value: 10, + align: 'left', + }, + { + name: 'Reform', + colour: palette('--uk-elections-reform'), + value: 5, + align: 'right', + }, + ], + }, +} satisfies Story; + +export const USPresidential = { + args: { + total: 538, + toWinCopy: 'to win', + sections: [ + { + name: 'Harris', + colour: palette('--us-elections-democrats'), + value: 200, + align: 'left', + }, + { + name: 'Trump', + colour: palette('--us-elections-republicans'), + value: 200, + align: 'right', + }, + ], + }, +} satisfies Story; + +export const EUParliament = { + args: { + total: 720, + toWinCopy: undefined, + sections: [ + { + colour: palette('--eu-parliament-theleft'), + name: 'Left', + value: 40, + align: 'left', + }, + { + name: 'S&D', + colour: palette('--eu-parliament-sd'), + value: 100, + align: 'left', + }, + { + name: 'Grn/EFA', + colour: palette('--eu-parliament-greensefa'), + value: 40, + align: 'left', + }, + { + name: 'Renew', + colour: palette('--eu-parliament-renew'), + value: 60, + align: 'left', + }, + { + name: 'EPP', + colour: palette('--eu-parliament-epp'), + value: 150, + align: 'left', + }, + { + name: 'ECR', + colour: palette('--eu-parliament-ecr'), + value: 60, + align: 'left', + }, + { + name: 'NI', + colour: palette('--eu-parliament-ni'), + value: 30, + align: 'left', + }, + { + name: 'PfE', + colour: palette('--eu-parliament-unknown'), + value: 70, + align: 'left', + }, + { + name: 'ESN', + colour: palette('--eu-parliament-unknown'), + value: 20, + align: 'left', + }, + ], + }, +} satisfies Story; diff --git a/dotcom-rendering/src/components/ElectionTrackers/StackedProgress.tsx b/dotcom-rendering/src/components/ElectionTrackers/StackedProgress.tsx new file mode 100644 index 0000000000..53f4eaa7ff --- /dev/null +++ b/dotcom-rendering/src/components/ElectionTrackers/StackedProgress.tsx @@ -0,0 +1,207 @@ +import { from, textSans12 } from '@guardian/source/foundations'; +import type { ReactNode } from 'react'; +import { palette } from '../../palette'; + +type Props = { + /** + * The sections into which the stacked progress bar will be broken. For more + * information see {@linkcode Section}. + */ + sections: Section[]; + /** + * The maximum number the stacked progress bar can reach. For an election, + * this would be the number of results expected. Must be an integer (a whole + * number). + * + * **Examples:** number of constituencies up for election; total electoral + * college votes. + */ + total: number; + /** + * When this is specified, the bar will include a line down the centre that + * represents a target needed to win the election by achieving a majority. + * The groups being elected can then be arranged on either side of this line + * by setting their {@linkcode Section.align|align} property. The majority + * needed will be calculated automatically based on the + * {@linkcode Props.total|total}. + * + * The copy specified here will be prefixed by the majority number and used + * to label the stacked progress bar, and will appear above the central + * line. + * + * **Examples:** Specify {@linkcode Props.total|total} as 538 and this prop + * as "to win" to get "270 to win"; specify {@linkcode Props.total|total} as + * 650 and this prop as "for majority" to get "326 for majority". + */ + toWinCopy: string | undefined; +}; + +/** + * A section of the stacked progress bar. For an election each section would + * represent a group that's running. Examples: seats won by a party; votes won + * by a candidate. + */ +type Section = { + /** + * The colour used to represent the group in the stacked progress bar. It + * expects a CSS `color` value (e.g. a hex string). To ensure dark mode + * support a {@linkcode palette} colour can be used; i.e. this property + * can be set to the return value of the {@linkcode palette} function. + */ + colour: string; + /** + * The size of a particular section of the progress bar, less than the + * {@linkcode Props.total|total}. For an election, this would be the result + * for the group in question. + * + * **Examples:** seats won by a party; votes won by a candidate. + */ + value: number; + /** + * The name of the section in the stacked progress bar. For an election, + * this would be the name of the group. It will be used to provide an + * accessible description of that section and as a React "key" for the + * element, so each section's `name` should be unique relative to the + * other sections. + * + * **Examples:** name of a candidate; name of a party. + */ + name: string; + /** + * Aligns a section to the left or right side of the stacked progress bar. + * For an election this can be used to represent two or more groups in + * opposition to one another. When used in conjunction with + * {@linkcode Props.toWinCopy|toWinCopy} it can be used to show two or more + * groups competing for a majority. + */ + align: 'left' | 'right'; +}; + +/** + * Represents progress towards a goal divided into groups. Designed to be used + * in election trackers, where it can be used to show progress through an + * election divided up by each group running. + * + * It's generic, so the kinds of groups it can represent varies. For example: + * + * - Candidates in a US presidential election + * - Parties in a UK general election + * - Party groups in an EU parliamentary election + * + * These examples are demonstrated in the stories for this component. + */ +export const StackedProgress = ({ sections, total, toWinCopy }: Props) => { + const value = sections.reduce((acc, section) => acc + section.value, 0); + + return ( + + ); +}; + +type SectionDivProps = { + section: Section; + total: number; +}; + +const SectionDiv = ({ section, total }: SectionDivProps) => ( +
+); + +type LabelProps = { + children: ReactNode; + total: number; + toWinCopy: string | undefined; +}; + +const Label = ({ children, total, toWinCopy }: LabelProps) => + toWinCopy === undefined ? ( + <>{children} + ) : ( + + ); + +const spacer = (total: number, value: number): Section => ({ + colour: palette('--stacked-progress-background'), + value: total - value, + name: 'spacer', + align: 'left', +}); + +const toWin = (total: number): number => Math.floor(total / 2) + 1; + +const valueText = (value: number, sections: Section[]): string => + `Progress so far: ${value}, values: ${sections + .map((section) => `${section.name} ${section.value}`) + .join(', ')}`; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 30c15bcd8c..0f4bc889cf 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -6378,6 +6378,38 @@ const paletteColours = { light: emailSignupTextSubduedLight, dark: emailSignupTextSubduedDark, }, + '--eu-parliament-ecr': { + light: () => sourcePalette.brand[500], + dark: () => '#009AE1', + }, + '--eu-parliament-epp': { + light: () => '#3DBBE2', + dark: () => '#3DBBE2', + }, + '--eu-parliament-greensefa': { + light: () => '#39A566', + dark: () => '#39A566', + }, + '--eu-parliament-ni': { + light: () => sourcePalette.neutral[20], + dark: () => '#A1A1A1', + }, + '--eu-parliament-renew': { + light: () => '#FF7F0F', + dark: () => '#FF7F0F', + }, + '--eu-parliament-sd': { + light: () => sourcePalette.news[400], + dark: () => '#DC2E1C', + }, + '--eu-parliament-theleft': { + light: () => '#8B0000', + dark: () => '#B23C2D', + }, + '--eu-parliament-unknown': { + light: () => '#848484', + dark: () => sourcePalette.neutral[46], + }, '--expandable-atom-background': { light: expandableAtomBackgroundLight, dark: expandableAtomBackgroundDark, @@ -6995,6 +7027,18 @@ const paletteColours = { light: speechBubbleBackgroundLight, dark: speechBubbleBackgroundLight, }, + '--stacked-progress-background': { + light: () => sourcePalette.neutral[86], + /** + * Custom colour to prevent clashes with the neutral palette, which + * is sometimes used for sections of the stacked progress bar. + */ + dark: () => '#606060', + }, + '--stacked-progress-to-win': { + light: () => sourcePalette.neutral[7], + dark: () => sourcePalette.neutral[86], + }, '--staff-contributor-badge': { light: staffBadgeLight, dark: staffBadgeDark, @@ -7195,6 +7239,34 @@ const paletteColours = { light: () => sourcePalette.neutral[20], dark: () => sourcePalette.neutral[73], }, + '--uk-elections-conservative': { + light: () => sourcePalette.sport[400], + dark: () => '#009AE1', + }, + '--uk-elections-labour': { + light: () => sourcePalette.news[400], + dark: () => '#DC2E1C', + }, + '--uk-elections-lib-dem': { + light: () => sourcePalette.opinion[450], + dark: () => sourcePalette.opinion[500], + }, + '--uk-elections-reform': { + light: () => '#3DBBE2', + dark: () => '#3DBBE2', + }, + '--uk-elections-snp': { + light: () => '#F5DC00', + dark: () => '#F5DC00', + }, + '--us-elections-democrats': { + light: () => '#093CA3', + dark: () => '#3261DB', + }, + '--us-elections-republicans': { + light: () => sourcePalette.news[400], + dark: () => '#DC2E1C', + }, '--weather-icon': { light: () => sourcePalette.neutral[97], dark: () => sourcePalette.neutral[7],