diff --git a/scripts/data/README.md b/scripts/data/README.md new file mode 100644 index 00000000..a25cafb2 --- /dev/null +++ b/scripts/data/README.md @@ -0,0 +1,69 @@ +# scripts/data + +Checked-in intermediates for the `scripts/gen-*.mjs` generators. Each one is a +small, human-auditable extract of a larger official file, so the generators can +run with no extra dependencies and a reviewer can diff the numbers. + +## toronto-ward-census-2021.csv + +Per-ward figures for Toronto's 25-ward model, extracted from the City of +Toronto's own ward profiles. Consumed by `scripts/gen-ward-profiles.mjs`, which +emits `src/app/toronto/vote/2026/wardStats.ts`. + +- Dataset: Ward Profiles (25-Ward Model), City of Toronto Open Data + https://open.toronto.ca/dataset/ward-profiles-25-ward-model/ +- Underlying source: Statistics Canada, 2021 and 2016 Census of Population +- Files used: + - `2023-wardprofiles-2011-2021-censusdata_rev0719.xlsx` + (sheets "2021 One Variable" and "2016 Census One Variable") + - `2023-wardprofiles-geographicareas.xlsx` (ward land area) + +The `ward` column is the zero-padded ward token used throughout the app +("01".."25"), plus one `toronto` row holding the citywide figure for each +column so the UI can show every ward against the city. + +Column provenance, by row number in the "2021 One Variable" sheet: + +| column | source row | +| --- | --- | +| `pop_2021` | 19 — Total - Age | +| `pop_2016` | 19 of the "2016 Census One Variable" sheet | +| `median_age` | 40 — Median age | +| `dwellings_total` | 44 — Total - Occupied private dwellings by structural type | +| `single_detached` | 45 — Single-detached house | +| `apt_5plus` | 50 — Apartment in a building that has five or more storeys | +| `tenure_total` | 55 — Total - Tenure (includes band housing) | +| `owned` | 56 — Owned | +| `rented` | 59 — Rented | +| `hhtype_total` | 114 — Total - Household type | +| `one_person_hh` | 124 — One-person households | +| `privhh_pop` | 144 — Total - Immigrant status and period of immigration | +| `immigrants` | 146 — Immigrants | +| `edu_total_15plus` | 997 — Total - Highest certificate, diploma or degree | +| `bachelor_plus` | 1007 — Bachelor's degree or higher | +| `med_hh_income` | 1385 — Median total income of households in 2020 ($) | +| `avg_rent` | 1392 — Average monthly shelter costs for rented dwellings ($) | +| `tenant_burden_30` | 1393 — % of tenant households spending 30%+ on shelter | +| `avg_owner_cost` | 1396 — Average monthly shelter costs for owned dwellings ($) | +| `owner_burden_30` | 1397 — % of owner households spending 30%+ on shelter | +| `lim_at_rate` | 1405 — Prevalence of low income (LIM-AT), % | +| `area_sq_km` | ward land area, from the geographic-areas file | + +Counts are Statistics Canada's, and are rounded to the nearest 5 at source, so +percentages derived from them carry that rounding. Refresh this file when the +City publishes ward profiles for the 2026 Census. + +## toronto-ward-councillors.csv + +The councillor sitting for each of Toronto's 25 wards. Consumed by the same +generator, which emits `src/app/toronto/vote/2026/wardCouncillors.ts`. + +- Dataset: Ward and Elected Councillor, City of Toronto Open Data + https://open.toronto.ca/dataset/wards-and-elected-councillors/ +- Columns: the zero-padded `ward` token, the `councillor` holding it, and the + `date_effective` the City records for that row. + +Refresh after any by-election, and again once the 2026 results are declared. +Whether a councillor is seeking re-election is deliberately *not* recorded +here — that is read off the live candidate roster at render time, so it cannot +go stale. See `wardProfile()` in `src/app/toronto/vote/2026/wardProfiles.ts`. diff --git a/scripts/data/toronto-ward-census-2021.csv b/scripts/data/toronto-ward-census-2021.csv new file mode 100644 index 00000000..26fc1ac0 --- /dev/null +++ b/scripts/data/toronto-ward-census-2021.csv @@ -0,0 +1,27 @@ +ward,pop_2021,pop_2016,median_age,dwellings_total,single_detached,apt_5plus,tenure_total,owned,rented,hhtype_total,one_person_hh,privhh_pop,immigrants,edu_total_15plus,bachelor_plus,med_hh_income,avg_rent,tenant_burden_30,avg_owner_cost,owner_burden_30,lim_at_rate,area_sq_km +toronto,2761285,2691665,39.6,1160895,270350,542625,1160885,602910,557970,1160890,385530,2761285,1286140,2377950,976620,84000,1562,40,2038,25.6,13.2, +01,115120,116955,38,38135,11270,15980,38130,20790,17340,38130,7785,115115,64675,96620,22120,81000,1328,34.2,1664,23.5,12.9,48.37 +02,117200,116055,45.2,45345,21860,17860,45345,30565,14780,45345,11105,117205,48485,99900,37540,100000,1574,39.2,1930,18.8,7.9,37.35 +03,139920,127525,40.8,65575,18585,30645,65570,37310,28260,65575,24520,139920,54670,121460,54175,90000,1592,40.9,2108,24.7,11,39.99 +04,104715,106445,40,49445,8855,18955,49440,21610,27835,49440,19625,104715,32400,89700,45405,85000,1492,39.7,2280,20.3,12,15.33 +05,115675,115130,40.4,45055,13190,18300,45050,21685,23370,45050,12850,115675,58205,97205,17910,72000,1196,34.8,1660,22.3,14.7,24.98 +06,107355,103760,41.6,41265,10350,17860,41270,20535,20735,41270,11095,107360,59525,91800,30650,82000,1448,36.1,1918,25.3,11.5,35.36 +07,111200,107725,37.6,37675,4690,15480,37675,18070,19605,37675,8280,111200,62700,92430,16310,73000,1261,33.2,1622,24,15.1,30.7 +08,114820,112920,41.2,45910,17190,15340,45910,24515,21395,45915,14090,114820,44725,95710,45065,97000,1588,38,2418,20.3,10.1,22.66 +09,104730,107395,38.4,45670,6470,8520,45670,23650,22025,45670,14995,104730,39645,91630,34340,85000,1552,41.3,1910,21.6,10.8,12.1 +10,135400,114315,32.8,80730,500,71070,80735,33845,46885,80735,42205,135405,53910,126130,82560,89000,1988,43.7,2290,35.2,14.1,12.81 +11,102385,100520,36.8,53510,4180,27355,53505,23085,30420,53505,24065,102385,34755,93395,57405,84000,1976,48.8,2592,29.8,15.3,13.6 +12,114095,104940,39.6,58110,8850,32900,58110,22430,35680,58110,25500,114095,40430,100635,57100,86000,1768,44.6,2492,24.8,12.5,13.12 +13,116930,99590,34.8,68965,250,58730,68965,20665,48300,68965,37570,116935,48730,107625,58605,65000,1520,43.1,2172,35.1,22.2,5.86 +14,104555,105395,40.8,46235,8410,10185,46235,25380,20855,46235,16025,104555,31915,88370,40500,93000,1424,37.8,2138,18.2,11.5,20.8 +15,101025,101790,41.2,40030,14730,16035,40030,20815,19215,40030,11845,101025,41420,83725,46220,102000,1716,40.9,2968,21.8,12.6,30.3 +16,94335,93165,41.2,38285,5425,22545,38285,16940,21340,38285,11695,94335,50390,79750,29065,78500,1492,39.1,1846,26.1,14.4,22.96 +17,112590,109055,40.4,45780,8825,25675,45785,25990,19790,45785,12555,112590,68280,98425,48980,84000,1754,43.6,1904,31.4,14.4,24.42 +18,117130,117400,39.2,50550,12425,30705,50555,29340,21210,50555,15970,117125,70535,103695,57030,81000,1880,49.3,2202,37.6,17.8,19.76 +19,108500,108440,41.2,45705,12790,11505,45700,25515,20190,45700,14595,108500,34420,89815,38480,89000,1370,38.6,2168,18.3,11.9,16.8 +20,110095,108290,40.8,41905,14850,14260,41905,22735,19165,41905,11655,110095,50370,91970,28980,79000,1196,32.3,1872,22.7,13.9,28.14 +21,111560,110445,40,40565,13880,16840,40560,23045,17520,40560,10070,111560,61670,94595,27210,78000,1338,35.8,1668,26,13.3,28.21 +22,103690,104230,45.2,38345,11075,18080,38350,25370,12975,38350,9200,103690,66065,90390,28905,77000,1358,36,1640,32,14.7,21.4 +23,94025,97610,42.8,29545,11790,6630,29545,23465,6085,29545,4420,94025,60740,80925,21660,87000,1408,33.3,1636,28.8,11.8,30.44 +24,102755,101110,38.4,36240,12225,17140,36245,19925,16320,36245,8655,102755,54840,85580,25115,78000,1197,31.6,1776,25.4,14.7,26.11 +25,101485,101445,42,32315,17685,4025,32315,25650,6665,32315,5160,101485,52660,86445,25270,105000,1362,32.7,1802,20.7,7.8,54.08 diff --git a/scripts/data/toronto-ward-councillors.csv b/scripts/data/toronto-ward-councillors.csv new file mode 100644 index 00000000..fe4ba05d --- /dev/null +++ b/scripts/data/toronto-ward-councillors.csv @@ -0,0 +1,26 @@ +ward,councillor,date_effective +01,Vincent Crisanti,2025-04-16 +02,Stephen Holyday,2025-04-16 +03,Amber Morley,2025-04-16 +04,Gord Perks,2025-04-16 +05,Frances Nunziata,2025-04-16 +06,James Pasternak,2025-04-16 +07,Anthony Perruzza,2025-04-16 +08,Mike Colle,2025-04-16 +09,Alejandra Bravo,2025-04-16 +10,Ausma Malik,2025-04-16 +11,Dianne Saxe,2025-04-16 +12,Josh Matlow,2025-04-16 +13,Chris Moise,2025-04-16 +14,Paula Fletcher,2025-04-16 +15,Rachel Chernos Lin,2025-04-16 +16,Jon Burnside,2025-04-16 +17,Shelley Carroll,2025-04-16 +18,Lily Cheng,2025-04-16 +19,Brad Bradford,2025-04-16 +20,Parthi Kandavel,2025-04-16 +21,Michael Thompson,2025-04-16 +22,Nick Mantas,2025-04-16 +23,Jamaal Myers,2025-04-16 +24,Paul Ainslie,2025-04-16 +25,Neethan Shan,2025-04-16 diff --git a/scripts/gen-ward-profiles.mjs b/scripts/gen-ward-profiles.mjs new file mode 100644 index 00000000..f80c5afe --- /dev/null +++ b/scripts/gen-ward-profiles.mjs @@ -0,0 +1,216 @@ +// Generate Toronto's per-ward Census statistics (wardStats.ts) from the +// checked-in extract of the City's own ward profiles. +// +// node scripts/gen-ward-profiles.mjs +// +// Inputs scripts/data/toronto-ward-census-2021.csv (see scripts/data/README.md +// for every column's provenance in the City's XLSX) +// scripts/data/toronto-ward-councillors.csv +// Outputs src/app/toronto/vote/2026/wardStats.ts +// src/app/toronto/vote/2026/wardCouncillors.ts +// +// The statistics are the generated half of a ward's profile; the hand-written +// half — the brief prose — lives in wardProfiles.ts and this script never +// touches it. Same split as wardGeo.ts (generated) and candidates.ts (by hand). +// +// Every stat is emitted pre-formatted as a display string, with the citywide +// figure alongside it, because the only consumer is a UI that wants to print +// "45.5%" next to "48.1% citywide" — not to do arithmetic. Deliberately +// dependency-free: this runs once per Census release. + +import { readFileSync, writeFileSync } from "node:fs"; + +const INPUT = "scripts/data/toronto-ward-census-2021.csv"; +const OUT = "src/app/toronto/vote/2026/wardStats.ts"; +const COUNCILLORS_INPUT = "scripts/data/toronto-ward-councillors.csv"; +const COUNCILLORS_OUT = "src/app/toronto/vote/2026/wardCouncillors.ts"; + +// ── Read ─────────────────────────────────────────────────────────────────── + +const lines = readFileSync(INPUT, "utf8").trim().split(/\r?\n/); +const header = lines[0].split(","); +const rows = new Map( + lines.slice(1).map((line) => { + const cells = line.split(","); + const row = Object.fromEntries(header.map((h, i) => [h, cells[i]])); + return [row.ward, row]; + }), +); + +const city = rows.get("toronto"); +if (!city) throw new Error(`${INPUT}: no "toronto" row for citywide figures`); + +const WARDS = [...rows.keys()].filter((w) => w !== "toronto").sort(); +if (WARDS.length !== 25) { + throw new Error(`${INPUT}: expected 25 wards, found ${WARDS.length}`); +} + +// ── Format ───────────────────────────────────────────────────────────────── + +const num = (row, key) => { + const raw = row[key]; + if (raw === undefined || raw === "") return null; + const n = Number(raw); + if (!Number.isFinite(n)) throw new Error(`${key}: not a number (${raw})`); + return n; +}; + +const need = (row, key) => { + const n = num(row, key); + if (n === null) throw new Error(`ward ${row.ward}: ${key} is empty`); + return n; +}; + +const count = (n) => n.toLocaleString("en-CA"); +const money = (n) => `$${n.toLocaleString("en-CA")}`; +const pct = (n) => `${n.toFixed(1)}%`; +const share = (part, whole) => pct((part / whole) * 100); +const signedPct = (n) => `${n >= 0 ? "+" : "−"}${Math.abs(n).toFixed(1)}%`; + +/** Population change since the 2016 Census, as a display string. A ward that + * moved by less than 0.05% would print as "+0.0% since 2016", which reads as + * a rounding artefact rather than the fact it is, so it is spelled out. */ +const popChange = (row) => { + const change = (need(row, "pop_2021") / need(row, "pop_2016") - 1) * 100; + if (Math.abs(change) < 0.05) return "No change since 2016"; + return `${signedPct(change)} since 2016`; +}; + +// Each entry is one row of the ward profile: a label, the ward's value, the +// citywide value to read it against, and an optional note under it. `city` +// returns null where a citywide comparison is meaningless (density). +const STATS = [ + { + label: "Population", + ward: (r) => count(need(r, "pop_2021")), + note: (r) => popChange(r), + }, + { + label: "Residents per km²", + ward: (r) => count(Math.round(need(r, "pop_2021") / need(r, "area_sq_km"))), + city: () => null, + note: (r) => `${need(r, "area_sq_km").toFixed(1)} km² of land`, + }, + { + label: "Median household income", + ward: (r) => money(need(r, "med_hh_income")), + }, + { + label: "Renter households", + ward: (r) => share(need(r, "rented"), need(r, "tenure_total")), + note: (r) => `${money(need(r, "avg_rent"))} average monthly rent`, + }, + { + label: "Renters spending 30%+ of income on housing", + ward: (r) => pct(need(r, "tenant_burden_30")), + }, + { + label: "Homes in buildings of five storeys or more", + ward: (r) => share(need(r, "apt_5plus"), need(r, "dwellings_total")), + note: (r) => + `${share(need(r, "single_detached"), need(r, "dwellings_total"))} single-detached`, + }, + { + label: "Residents born outside Canada", + ward: (r) => share(need(r, "immigrants"), need(r, "privhh_pop")), + }, + { + label: "Residents in low income", + ward: (r) => pct(need(r, "lim_at_rate")), + note: () => "Low-income measure, after tax", + }, +]; + +const statsFor = (row) => + STATS.map((stat) => { + const entry = { label: stat.label, value: stat.ward(row) }; + const cityValue = stat.city ? stat.city(row) : stat.ward(city); + if (cityValue !== null) entry.cityValue = cityValue; + const note = stat.note ? stat.note(row) : null; + if (note !== null) entry.note = note; + return entry; + }); + +// Serialised by hand rather than with JSON.stringify over an object, because +// JS reorders integer-like keys ("10" before "01") and the emitted file should +// read 01..25 for whoever reviews the diff. +const table = + "{\n" + + WARDS.map( + (w) => + ` "${w}": ${JSON.stringify(statsFor(rows.get(w)), null, 2).replace(/\n/g, "\n ")},`, + ).join("\n") + + "\n}"; + +// ── Emit ─────────────────────────────────────────────────────────────────── + +const file = `// AUTO-GENERATED — do not edit by hand. +// Toronto's per-ward Census statistics, formatted for display. +// Source: Ward Profiles (25-Ward Model), City of Toronto Open Data, from +// Statistics Canada's 2021 and 2016 Census of Population. +// https://open.toronto.ca/dataset/ward-profiles-25-ward-model/ +// Regenerate with: node scripts/gen-ward-profiles.mjs +// The hand-written briefs that accompany these live in ./wardProfiles.ts. + +import type { WardStat } from "@/components/elections/WardProfile"; + +/** Statistics for each ward, keyed by the zero-padded ward token ("01".."25"), + * in the order they should be shown. */ +export const WARD_STATS: Record = ${table}; + +/** Where these figures come from, worded for the source line shown under a + * ward's statistics. */ +export const WARD_STATS_SOURCE = + "the 2021 Census of Population, via the City of Toronto\u2019s ward profiles"; +`; + +writeFileSync(OUT, file); +console.log( + `${OUT}: ${WARDS.length} wards × ${STATS.length} stats, ${(Buffer.byteLength(file) / 1024).toFixed(1)} KB`, +); + +// ── Sitting councillors ──────────────────────────────────────────────────── + +/* Who holds each seat today, so a ward page can name the incumbent when they + register to run again. Whether they have is not knowable here — it comes + from the live candidate roster — so this file only says who sits, and the + page decides whether to show them. */ + +const councillorLines = readFileSync(COUNCILLORS_INPUT, "utf8") + .trim() + .split(/\r?\n/); +const councillorHeader = councillorLines[0].split(","); +const councillors = councillorLines.slice(1).map((line) => { + const cells = line.split(","); + return Object.fromEntries(councillorHeader.map((h, i) => [h, cells[i]])); +}); + +if (councillors.length !== 25) { + throw new Error( + `${COUNCILLORS_INPUT}: expected 25 councillors, found ${councillors.length}`, + ); +} + +const effective = [...new Set(councillors.map((c) => c.date_effective))].sort(); + +const councillorFile = `// AUTO-GENERATED — do not edit by hand. +// The councillor sitting for each Toronto ward, as of ${effective[effective.length - 1]}. +// Source: Ward and Elected Councillor, City of Toronto Open Data. +// https://open.toronto.ca/dataset/wards-and-elected-councillors/ +// Regenerate with: node scripts/gen-ward-profiles.mjs +// +// Refresh this after any by-election, and again once the 2026 results are in. +// Nothing here says whether a councillor is seeking re-election — that is read +// off the live candidate roster; see wardProfiles.ts. + +/** The sitting councillor for each ward, keyed by the zero-padded ward token + * ("01".."25"). */ +export const WARD_COUNCILLORS: Record = { +${councillors + .map((c) => ` "${c.ward}": ${JSON.stringify(c.councillor)},`) + .join("\n")} +}; +`; + +writeFileSync(COUNCILLORS_OUT, councillorFile); +console.log(`${COUNCILLORS_OUT}: ${councillors.length} councillors`); diff --git a/src/app/toronto/vote/2026/wardCouncillors.ts b/src/app/toronto/vote/2026/wardCouncillors.ts new file mode 100644 index 00000000..3bd8772e --- /dev/null +++ b/src/app/toronto/vote/2026/wardCouncillors.ts @@ -0,0 +1,39 @@ +// AUTO-GENERATED — do not edit by hand. +// The councillor sitting for each Toronto ward, as of 2025-04-16. +// Source: Ward and Elected Councillor, City of Toronto Open Data. +// https://open.toronto.ca/dataset/wards-and-elected-councillors/ +// Regenerate with: node scripts/gen-ward-profiles.mjs +// +// Refresh this after any by-election, and again once the 2026 results are in. +// Nothing here says whether a councillor is seeking re-election — that is read +// off the live candidate roster; see wardProfiles.ts. + +/** The sitting councillor for each ward, keyed by the zero-padded ward token + * ("01".."25"). */ +export const WARD_COUNCILLORS: Record = { + "01": "Vincent Crisanti", + "02": "Stephen Holyday", + "03": "Amber Morley", + "04": "Gord Perks", + "05": "Frances Nunziata", + "06": "James Pasternak", + "07": "Anthony Perruzza", + "08": "Mike Colle", + "09": "Alejandra Bravo", + "10": "Ausma Malik", + "11": "Dianne Saxe", + "12": "Josh Matlow", + "13": "Chris Moise", + "14": "Paula Fletcher", + "15": "Rachel Chernos Lin", + "16": "Jon Burnside", + "17": "Shelley Carroll", + "18": "Lily Cheng", + "19": "Brad Bradford", + "20": "Parthi Kandavel", + "21": "Michael Thompson", + "22": "Nick Mantas", + "23": "Jamaal Myers", + "24": "Paul Ainslie", + "25": "Neethan Shan", +}; diff --git a/src/app/toronto/vote/2026/wardProfiles.ts b/src/app/toronto/vote/2026/wardProfiles.ts new file mode 100644 index 00000000..8aa473ae --- /dev/null +++ b/src/app/toronto/vote/2026/wardProfiles.ts @@ -0,0 +1,141 @@ +// ───────────────────────────────────────────────────────────────────────── +// Toronto 2026 election — ward briefs (edit this file by hand). +// +// A ward's profile has two halves. The statistics are generated from the +// City's own ward profiles into ./wardStats — never edit those by hand, run +// `node scripts/gen-ward-profiles.mjs` instead. The brief is the half a +// person writes: what the ward actually is, and what the numbers next to it +// mean. This file holds the briefs, and combines the two. +// +// WRITING A BRIEF +// Two to four sentences. Say where the ward is and which neighbourhoods it +// takes in, then what distinguishes it — ideally something a reader can see +// confirmed in the statistics beside it. Describe the place, not the race: +// these are read by voters weighing candidates, so they must not favour any +// of them. Anything that turns over during a campaign — who holds the seat, +// which applications are at committee — belongs on the candidates, not here. +// +// Neighbourhood names are the City's own (the 158-neighbourhood model), +// assigned to wards by which ward contains each neighbourhood's centroid. +// +// A ward with no brief simply shows its statistics, so it is safe to leave +// one blank rather than write filler. +// ───────────────────────────────────────────────────────────────────────── + +import { WARD_STATS, WARD_STATS_SOURCE } from "./wardStats"; +import { WARD_COUNCILLORS } from "./wardCouncillors"; +import { nameKey } from "@/lib/elections/election-data"; +import { candidateSlug } from "@/lib/elections/candidate-profile"; +import { getElection } from "@/lib/elections/registry"; +import type { + WardProfile, + WardStat, +} from "@/components/elections/WardProfile"; + +// The shapes live with the component that renders them, as WardGeo does with +// . Re-exported so ./wardStats and this file's callers have one +// import site for a ward's data. +export type { WardProfile, WardStat }; + +// ── Briefs ───────────────────────────────────────────────────────────────── + +/** Keyed by the zero-padded ward token ("01".."25"), as everything else in + * this directory is. */ +export const WARD_BRIEFS: Record = { + "01": "Toronto's northwest corner, running from the Humber River to the airport lands and taking in Rexdale, Mount Olive-Silverstone-Jamestown, Kingsview Village-The Westway and Thistletown. Much of its 48 square kilometres is the Clairville employment lands rather than housing, which is why one of the city's largest wards is also one of its least dense. Well over half its residents were born outside Canada, and its households are among the largest in Toronto \u2014 just over three people on average, against a citywide 2.4.", + + "02": "The postwar heart of Etobicoke — Islington, Princess-Rosethorn, Markland Wood, Eringate-Centennial-West Deane and Willowridge-Martingrove-Richview. Close to half its homes are single-detached houses and only a third of households rent, both far from the city average. It has Toronto's oldest median age alongside Scarborough-Agincourt, and one of the two lowest rates of low income in the city.", + + "03": "Toronto's southwest lakeshore, from The Kingsway and Stonegate-Queensway down through Mimico, New Toronto, Long Branch and Alderwood. It grew almost 10 per cent between the 2016 and 2021 Censuses — among the fastest in the city — on condominium towers at Humber Bay Shores and around Etobicoke City Centre, which now sit beside streets of prewar lakeside cottages and postwar bungalows.", + + "04": "The west end between the Humber and Dufferin, taking in Roncesvalles, South Parkdale, High Park-Swansea, High Park North and Runnymede-Bloor West Village. More than half of households rent, but unusually little of that housing is in towers: fewer than one home in five is single-detached and fewer than two in five are in a building of five storeys or more, so most of the ward lives in the walk-up apartments, duplexes and converted houses in between. Its population edged down between Censuses.", + + "05": "The old City of York and the valleys north of it — Weston, Mount Dennis, Rockcliffe-Smythe, Brookhaven-Amesbury and Keelesdale-Eglinton West. It has one of the lowest median household incomes of any Toronto ward and, with Scarborough Southwest, the lowest average rent. The Eglinton Crosstown's western end runs through it, and Mount Dennis sits at the line's terminus.", + + "06": "North York west of Bathurst — Downsview, Bathurst Manor, Clanton Park, Westminster-Branson and York University Heights. More than half its residents were born outside Canada. The former Downsview Airport lands, one of the largest redevelopment sites anywhere in the city, fall inside this ward.", + + "07": "The northwest tower neighbourhoods, from Humber Summit and Humbermede down through Glenfield-Jane Heights, Oakdale-Beverley Heights and Black Creek. Only about one home in eight is single-detached and two in five are in buildings of five storeys or more, a housing mix that looks nothing like the suburbs around it. It has the lowest share of residents holding a bachelor's degree or higher in Toronto and one of the highest rates of low income.", + + "08": "A north-south strip of midtown and North York taking in Lawrence Park, Bedford Park-Nortown, Forest Hill North, Englemount-Lawrence, Briar Hill-Belgravia and Yorkdale-Glen Park. Its median household income is among the city's highest, but that single figure spans a wide range — some of Toronto's most expensive streets and some of its apartment neighbourhoods sit within a few blocks of each other here. The Eglinton Crosstown crosses its southern end.", + + "09": "A dense, mostly low-rise wedge of the west end: Little Portugal, Dufferin Grove, Dovercourt Village, Corso Italia-Davenport, Caledonia-Fairbank and Junction-Wallace Emerson. It is the closest thing Toronto has to a missing-middle ward — under a fifth of homes are in buildings of five storeys or more, and under a sixth are single-detached, with the rest in semis, rows, duplexes and small apartment buildings. Its population fell between the 2016 and 2021 Censuses, among the steeper declines in the city.", + + "10": "The downtown waterfront and the ward built on former rail and industrial land: Liberty Village, CityPlace, Fort York, the East Bayfront, St. Lawrence, the Toronto Islands, West Queen West and Trinity-Bellwoods. It grew 18 per cent between Censuses, the fastest in Toronto, and is now the most vertical ward in the city — 88 per cent of its homes are in buildings of five storeys or more and fewer than one in a hundred is single-detached. It also has the youngest median age, the highest share of degree holders and the highest average rent.", + + "11": "Downtown's western and northern core, from Kensington-Chinatown and Palmerston-Little Italy up through the Annex and the University of Toronto's campus to Rosedale-Moore Park. It holds two very different housing markets at once: student and rental housing around the university, and some of the oldest and most expensive houses in Toronto a short walk north. Nearly half of its renter households spend 30 per cent or more of their income on housing.", + + "12": "Midtown along the Yonge and Eglinton corridors, taking in Yonge-Eglinton, Davisville, North Toronto, Yonge-St. Clair, Casa Loma, Forest Hill South, Wychwood, Humewood-Cedarvale and Oakwood Village. It grew almost 9 per cent between Censuses on tower development around the Yonge-Eglinton and St. Clair subway stations, and more than 60 per cent of its households now rent — the second-highest share in the city.", + + "13": "The smallest ward in Toronto by land area and by far the densest, at roughly 20,000 residents per square kilometre. It covers Regent Park, Moss Park, St. James Town, Cabbagetown, Church-Wellesley and Downtown Yonge East. Seven in ten households rent and 85 per cent of homes are in buildings of five storeys or more. It has the lowest median household income and the highest rate of low income of any ward, and it grew more than 17 per cent between Censuses.", + + "14": "The east end from the Don Valley to Coxwell, taking in Riverdale, the Danforth, Greenwood-Coxwell, Playter Estates, Blake-Jones, Broadview North and Old East York. Housing here is mostly the semi-detached and row housing of the streetcar city — barely a fifth of homes are in buildings of five storeys or more. Its population has been essentially flat across recent Censuses.", + + "15": "The ward with the sharpest internal contrast in Toronto. It runs from the Bridle Path and St. Andrew-Windfields, among the wealthiest addresses in Canada, through Leaside and Mount Pleasant East, down to the apartment towers of Thorncliffe Park. Its median household income is among the highest in the city while its rate of low income sits close to the city average, because the ward's two halves are averaged into one number.", + + "16": "The Don Mills area and the neighbourhoods around it — Banbury-Don Mills, Flemingdon Park, Victoria Village, Parkwoods-O'Connor Hills and Fenside-Parkwoods. Don Mills was among Canada's first planned postwar communities, and the towers-in-parkland form it pioneered still shapes the ward: nearly 60 per cent of homes are in buildings of five storeys or more and more than half of households rent.", + + "17": "North York east of Bayview — Bayview Village, Don Valley Village, Hillcrest Village, Henry Farm, Pleasant View and Bayview Woods-Steeles. Three in five residents were born outside Canada, among the highest shares in Toronto, and a similar share of homes are in buildings of five storeys or more, concentrated around Sheppard and the Don Valley.", + + "18": "The Yonge Street corridor through North York, from Lansing-Westgate up to Newtonbrook at Steeles, including Yonge-Doris, Willowdale West and Avondale. Sixty per cent of its residents were born outside Canada and 60 per cent of its homes are in towers, most of them along Yonge. It has the highest share of rent-burdened tenant households in Toronto — nearly half spend 30 per cent or more of their income on housing — and one of the highest rates of low income.", + + "19": "The lakeshore east of Coxwell and the neighbourhoods above it: The Beaches, East End-Danforth, Woodbine Corridor, Woodbine-Lumsden, O'Connor-Parkview and Taylor-Massey. Its housing is almost entirely low-rise: a quarter of its homes are in buildings of five storeys or more and a little over a quarter are single-detached, with the rest in the semis, rows and walk-ups in between. Incomes vary widely across it, from the streets near the boardwalk to the apartment blocks along Victoria Park.", + + "20": "Scarborough's southwest corner along the Bluffs — Birchcliffe-Cliffside, Cliffcrest, Clairlea-Birchmount, Kennedy Park and Oakridge. It has the lowest average rent in Toronto alongside York South-Weston, and one of the lowest shares of rent-burdened tenant households of any ward. Roughly a third of its homes are single-detached houses.", + + "21": "The centre of Scarborough, taking in Wexford/Maryvale, Dorset Park, Bendale, Eglinton East and Ionview, and wrapping around the Scarborough Centre transit and civic hub. More than half its residents were born outside Canada. Its housing splits almost evenly between single-detached houses and apartment buildings, with little in between.", + + "22": "Northern Scarborough west of McCowan — Tam O'Shanter-Sullivan, L'Amoreaux and Steeles. It has Toronto's oldest median age alongside Etobicoke Centre, and nearly two-thirds of its residents were born outside Canada. Only a third of its households rent, though nearly half its homes are in apartment buildings, so much of the ward is owner-occupied condominium.", + + "23": "Agincourt, Milliken and Malvern West, at Toronto's northeast edge. It has the highest share of residents born outside Canada of any ward — close to two in three — and, with Scarborough-Rouge Park, the lowest share of renters, at about one household in five. Its population fell almost 4 per cent between the 2016 and 2021 Censuses, the steepest decline in the city.", + + "24": "Scarborough between Highland Creek and Bellamy: Woburn, Golfdale-Cedarbrae-Woburn, Morningside, Guildwood and Scarborough Village. About a third of its homes are single-detached and nearly half are in buildings of five storeys or more \u2014 the towers along Kingston Road, Eglinton and Markham Road, with subdivisions on the streets behind them. It has one of the lowest shares of rent-burdened tenant households in the city.", + + "25": "Toronto's eastern edge and its largest ward by land area, taking in West Hill, West Rouge, Highland Creek, Centennial Scarborough, Morningside Heights and Malvern East, along with much of Rouge National Urban Park. It is the least dense ward in the city, at under 1,900 residents per square kilometre. It also has the highest median household income, the largest share of single-detached houses, the smallest share of tower housing and the lowest rate of low income — and its population did not grow at all between the 2016 and 2021 Censuses.", +}; + +// ── Accessor ─────────────────────────────────────────────────────────────── + +/** One candidate on this ward's ballot, as much of them as the incumbent + * check needs. */ +type Standing = { name: string; withdrawn?: boolean }; + +/** + * The profile for one ward, or null for a ward with neither brief nor + * statistics — so a caller can render the section only when there is one. + * + * `standing` is the ward's council candidates as the roster has them right + * now. The sitting councillor is named only if they appear among the ones who + * have not withdrawn, which is the only honest way to say "running again": + * incumbency is a fact about the seat, candidacy is a fact about the roster, + * and the second changes daily. Matching is by `nameKey`, so the Clerk's + * spelling and the City's need only agree up to case, accents and + * punctuation. Pass nothing and no incumbent is named. + */ +export function wardProfile( + wardToken: string, + standing: Standing[] = [], +): WardProfile | null { + const stats = WARD_STATS[wardToken]; + const brief = WARD_BRIEFS[wardToken] ?? ""; + if (!stats?.length && !brief) return null; + + const councillor = WARD_COUNCILLORS[wardToken]; + const running = + councillor !== undefined && + standing.some( + (candidate) => + !candidate.withdrawn && nameKey(candidate.name) === nameKey(councillor), + ); + + return { + brief, + stats: stats ?? [], + source: stats?.length ? WARD_STATS_SOURCE : undefined, + incumbent: running + ? { + name: councillor, + href: `${getElection("toronto-2026").basePath}/candidates/${candidateSlug(councillor)}`, + } + : undefined, + }; +} diff --git a/src/app/toronto/vote/2026/wardStats.ts b/src/app/toronto/vote/2026/wardStats.ts new file mode 100644 index 00000000..30e32473 --- /dev/null +++ b/src/app/toronto/vote/2026/wardStats.ts @@ -0,0 +1,1169 @@ +// AUTO-GENERATED — do not edit by hand. +// Toronto's per-ward Census statistics, formatted for display. +// Source: Ward Profiles (25-Ward Model), City of Toronto Open Data, from +// Statistics Canada's 2021 and 2016 Census of Population. +// https://open.toronto.ca/dataset/ward-profiles-25-ward-model/ +// Regenerate with: node scripts/gen-ward-profiles.mjs +// The hand-written briefs that accompany these live in ./wardProfiles.ts. + +import type { WardStat } from "@/components/elections/WardProfile"; + +/** Statistics for each ward, keyed by the zero-padded ward token ("01".."25"), + * in the order they should be shown. */ +export const WARD_STATS: Record = { + "01": [ + { + "label": "Population", + "value": "115,120", + "cityValue": "2,761,285", + "note": "−1.6% since 2016" + }, + { + "label": "Residents per km²", + "value": "2,380", + "note": "48.4 km² of land" + }, + { + "label": "Median household income", + "value": "$81,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "45.5%", + "cityValue": "48.1%", + "note": "$1,328 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "34.2%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "41.9%", + "cityValue": "46.7%", + "note": "29.6% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "56.2%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "12.9%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "02": [ + { + "label": "Population", + "value": "117,200", + "cityValue": "2,761,285", + "note": "+1.0% since 2016" + }, + { + "label": "Residents per km²", + "value": "3,138", + "note": "37.4 km² of land" + }, + { + "label": "Median household income", + "value": "$100,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "32.6%", + "cityValue": "48.1%", + "note": "$1,574 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "39.2%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "39.4%", + "cityValue": "46.7%", + "note": "48.2% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "41.4%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "7.9%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "03": [ + { + "label": "Population", + "value": "139,920", + "cityValue": "2,761,285", + "note": "+9.7% since 2016" + }, + { + "label": "Residents per km²", + "value": "3,499", + "note": "40.0 km² of land" + }, + { + "label": "Median household income", + "value": "$90,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "43.1%", + "cityValue": "48.1%", + "note": "$1,592 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "40.9%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "46.7%", + "cityValue": "46.7%", + "note": "28.3% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "39.1%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "11.0%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "04": [ + { + "label": "Population", + "value": "104,715", + "cityValue": "2,761,285", + "note": "−1.6% since 2016" + }, + { + "label": "Residents per km²", + "value": "6,831", + "note": "15.3 km² of land" + }, + { + "label": "Median household income", + "value": "$85,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "56.3%", + "cityValue": "48.1%", + "note": "$1,492 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "39.7%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "38.3%", + "cityValue": "46.7%", + "note": "17.9% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "30.9%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "12.0%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "05": [ + { + "label": "Population", + "value": "115,675", + "cityValue": "2,761,285", + "note": "+0.5% since 2016" + }, + { + "label": "Residents per km²", + "value": "4,631", + "note": "25.0 km² of land" + }, + { + "label": "Median household income", + "value": "$72,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "51.9%", + "cityValue": "48.1%", + "note": "$1,196 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "34.8%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "40.6%", + "cityValue": "46.7%", + "note": "29.3% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "50.3%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "14.7%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "06": [ + { + "label": "Population", + "value": "107,355", + "cityValue": "2,761,285", + "note": "+3.5% since 2016" + }, + { + "label": "Residents per km²", + "value": "3,036", + "note": "35.4 km² of land" + }, + { + "label": "Median household income", + "value": "$82,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "50.2%", + "cityValue": "48.1%", + "note": "$1,448 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "36.1%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "43.3%", + "cityValue": "46.7%", + "note": "25.1% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "55.4%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "11.5%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "07": [ + { + "label": "Population", + "value": "111,200", + "cityValue": "2,761,285", + "note": "+3.2% since 2016" + }, + { + "label": "Residents per km²", + "value": "3,622", + "note": "30.7 km² of land" + }, + { + "label": "Median household income", + "value": "$73,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "52.0%", + "cityValue": "48.1%", + "note": "$1,261 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "33.2%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "41.1%", + "cityValue": "46.7%", + "note": "12.4% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "56.4%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "15.1%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "08": [ + { + "label": "Population", + "value": "114,820", + "cityValue": "2,761,285", + "note": "+1.7% since 2016" + }, + { + "label": "Residents per km²", + "value": "5,067", + "note": "22.7 km² of land" + }, + { + "label": "Median household income", + "value": "$97,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "46.6%", + "cityValue": "48.1%", + "note": "$1,588 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "38.0%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "33.4%", + "cityValue": "46.7%", + "note": "37.4% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "39.0%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "10.1%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "09": [ + { + "label": "Population", + "value": "104,730", + "cityValue": "2,761,285", + "note": "−2.5% since 2016" + }, + { + "label": "Residents per km²", + "value": "8,655", + "note": "12.1 km² of land" + }, + { + "label": "Median household income", + "value": "$85,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "48.2%", + "cityValue": "48.1%", + "note": "$1,552 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "41.3%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "18.7%", + "cityValue": "46.7%", + "note": "14.2% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "37.9%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "10.8%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "10": [ + { + "label": "Population", + "value": "135,400", + "cityValue": "2,761,285", + "note": "+18.4% since 2016" + }, + { + "label": "Residents per km²", + "value": "10,570", + "note": "12.8 km² of land" + }, + { + "label": "Median household income", + "value": "$89,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "58.1%", + "cityValue": "48.1%", + "note": "$1,988 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "43.7%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "88.0%", + "cityValue": "46.7%", + "note": "0.6% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "39.8%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "14.1%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "11": [ + { + "label": "Population", + "value": "102,385", + "cityValue": "2,761,285", + "note": "+1.9% since 2016" + }, + { + "label": "Residents per km²", + "value": "7,528", + "note": "13.6 km² of land" + }, + { + "label": "Median household income", + "value": "$84,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "56.9%", + "cityValue": "48.1%", + "note": "$1,976 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "48.8%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "51.1%", + "cityValue": "46.7%", + "note": "7.8% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "33.9%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "15.3%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "12": [ + { + "label": "Population", + "value": "114,095", + "cityValue": "2,761,285", + "note": "+8.7% since 2016" + }, + { + "label": "Residents per km²", + "value": "8,696", + "note": "13.1 km² of land" + }, + { + "label": "Median household income", + "value": "$86,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "61.4%", + "cityValue": "48.1%", + "note": "$1,768 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "44.6%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "56.6%", + "cityValue": "46.7%", + "note": "15.2% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "35.4%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "12.5%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "13": [ + { + "label": "Population", + "value": "116,930", + "cityValue": "2,761,285", + "note": "+17.4% since 2016" + }, + { + "label": "Residents per km²", + "value": "19,954", + "note": "5.9 km² of land" + }, + { + "label": "Median household income", + "value": "$65,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "70.0%", + "cityValue": "48.1%", + "note": "$1,520 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "43.1%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "85.2%", + "cityValue": "46.7%", + "note": "0.4% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "41.7%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "22.2%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "14": [ + { + "label": "Population", + "value": "104,555", + "cityValue": "2,761,285", + "note": "−0.8% since 2016" + }, + { + "label": "Residents per km²", + "value": "5,027", + "note": "20.8 km² of land" + }, + { + "label": "Median household income", + "value": "$93,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "45.1%", + "cityValue": "48.1%", + "note": "$1,424 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "37.8%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "22.0%", + "cityValue": "46.7%", + "note": "18.2% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "30.5%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "11.5%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "15": [ + { + "label": "Population", + "value": "101,025", + "cityValue": "2,761,285", + "note": "−0.8% since 2016" + }, + { + "label": "Residents per km²", + "value": "3,334", + "note": "30.3 km² of land" + }, + { + "label": "Median household income", + "value": "$102,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "48.0%", + "cityValue": "48.1%", + "note": "$1,716 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "40.9%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "40.1%", + "cityValue": "46.7%", + "note": "36.8% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "41.0%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "12.6%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "16": [ + { + "label": "Population", + "value": "94,335", + "cityValue": "2,761,285", + "note": "+1.3% since 2016" + }, + { + "label": "Residents per km²", + "value": "4,109", + "note": "23.0 km² of land" + }, + { + "label": "Median household income", + "value": "$78,500", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "55.7%", + "cityValue": "48.1%", + "note": "$1,492 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "39.1%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "58.9%", + "cityValue": "46.7%", + "note": "14.2% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "53.4%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "14.4%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "17": [ + { + "label": "Population", + "value": "112,590", + "cityValue": "2,761,285", + "note": "+3.2% since 2016" + }, + { + "label": "Residents per km²", + "value": "4,611", + "note": "24.4 km² of land" + }, + { + "label": "Median household income", + "value": "$84,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "43.2%", + "cityValue": "48.1%", + "note": "$1,754 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "43.6%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "56.1%", + "cityValue": "46.7%", + "note": "19.3% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "60.6%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "14.4%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "18": [ + { + "label": "Population", + "value": "117,130", + "cityValue": "2,761,285", + "note": "−0.2% since 2016" + }, + { + "label": "Residents per km²", + "value": "5,928", + "note": "19.8 km² of land" + }, + { + "label": "Median household income", + "value": "$81,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "42.0%", + "cityValue": "48.1%", + "note": "$1,880 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "49.3%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "60.7%", + "cityValue": "46.7%", + "note": "24.6% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "60.2%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "17.8%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "19": [ + { + "label": "Population", + "value": "108,500", + "cityValue": "2,761,285", + "note": "+0.1% since 2016" + }, + { + "label": "Residents per km²", + "value": "6,458", + "note": "16.8 km² of land" + }, + { + "label": "Median household income", + "value": "$89,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "44.2%", + "cityValue": "48.1%", + "note": "$1,370 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "38.6%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "25.2%", + "cityValue": "46.7%", + "note": "28.0% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "31.7%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "11.9%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "20": [ + { + "label": "Population", + "value": "110,095", + "cityValue": "2,761,285", + "note": "+1.7% since 2016" + }, + { + "label": "Residents per km²", + "value": "3,912", + "note": "28.1 km² of land" + }, + { + "label": "Median household income", + "value": "$79,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "45.7%", + "cityValue": "48.1%", + "note": "$1,196 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "32.3%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "34.0%", + "cityValue": "46.7%", + "note": "35.4% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "45.8%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "13.9%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "21": [ + { + "label": "Population", + "value": "111,560", + "cityValue": "2,761,285", + "note": "+1.0% since 2016" + }, + { + "label": "Residents per km²", + "value": "3,955", + "note": "28.2 km² of land" + }, + { + "label": "Median household income", + "value": "$78,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "43.2%", + "cityValue": "48.1%", + "note": "$1,338 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "35.8%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "41.5%", + "cityValue": "46.7%", + "note": "34.2% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "55.3%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "13.3%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "22": [ + { + "label": "Population", + "value": "103,690", + "cityValue": "2,761,285", + "note": "−0.5% since 2016" + }, + { + "label": "Residents per km²", + "value": "4,845", + "note": "21.4 km² of land" + }, + { + "label": "Median household income", + "value": "$77,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "33.8%", + "cityValue": "48.1%", + "note": "$1,358 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "36.0%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "47.2%", + "cityValue": "46.7%", + "note": "28.9% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "63.7%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "14.7%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "23": [ + { + "label": "Population", + "value": "94,025", + "cityValue": "2,761,285", + "note": "−3.7% since 2016" + }, + { + "label": "Residents per km²", + "value": "3,089", + "note": "30.4 km² of land" + }, + { + "label": "Median household income", + "value": "$87,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "20.6%", + "cityValue": "48.1%", + "note": "$1,408 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "33.3%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "22.4%", + "cityValue": "46.7%", + "note": "39.9% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "64.6%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "11.8%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "24": [ + { + "label": "Population", + "value": "102,755", + "cityValue": "2,761,285", + "note": "+1.6% since 2016" + }, + { + "label": "Residents per km²", + "value": "3,935", + "note": "26.1 km² of land" + }, + { + "label": "Median household income", + "value": "$78,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "45.0%", + "cityValue": "48.1%", + "note": "$1,197 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "31.6%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "47.3%", + "cityValue": "46.7%", + "note": "33.7% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "53.4%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "14.7%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], + "25": [ + { + "label": "Population", + "value": "101,485", + "cityValue": "2,761,285", + "note": "No change since 2016" + }, + { + "label": "Residents per km²", + "value": "1,877", + "note": "54.1 km² of land" + }, + { + "label": "Median household income", + "value": "$105,000", + "cityValue": "$84,000" + }, + { + "label": "Renter households", + "value": "20.6%", + "cityValue": "48.1%", + "note": "$1,362 average monthly rent" + }, + { + "label": "Renters spending 30%+ of income on housing", + "value": "32.7%", + "cityValue": "40.0%" + }, + { + "label": "Homes in buildings of five storeys or more", + "value": "12.5%", + "cityValue": "46.7%", + "note": "54.7% single-detached" + }, + { + "label": "Residents born outside Canada", + "value": "51.9%", + "cityValue": "46.6%" + }, + { + "label": "Residents in low income", + "value": "7.8%", + "cityValue": "13.2%", + "note": "Low-income measure, after tax" + } + ], +}; + +/** Where these figures come from, worded for the source line shown under a + * ward's statistics. */ +export const WARD_STATS_SOURCE = + "the 2021 Census of Population, via the City of Toronto’s ward profiles"; diff --git a/src/app/toronto/vote/2026/wards/[ward]/page.tsx b/src/app/toronto/vote/2026/wards/[ward]/page.tsx index 85b0fe52..9eea2486 100644 --- a/src/app/toronto/vote/2026/wards/[ward]/page.tsx +++ b/src/app/toronto/vote/2026/wards/[ward]/page.tsx @@ -5,6 +5,7 @@ import { WardMap, WardMapDefs } from "@/components/elections/WardMap"; import { rosterSurvey } from "@/lib/elections/survey-answers"; import { ELECTION, WARD_NUMBERS, getToronto2026, getToronto2026Ward } from "../../data"; import { WARD_GEO, WARD_SHAPES } from "../../wardGeo"; +import { wardProfile } from "../../wardProfiles"; export function generateStaticParams() { return WARD_NUMBERS.map((n) => ({ ward: n })); @@ -57,6 +58,12 @@ export default async function WardDetailPage({ nominationCloseLabel={view.nominationCloseLabel} surveyAnswers={surveyAnswers} surveyShape={surveyShape} + profile={ + wardProfile( + data.ward.n, + data.councilRaces.flatMap((race) => race.candidates), + ) ?? undefined + } wardMapDefs={} wardMap={ w.number === ward.number); @@ -258,6 +266,9 @@ export function WardDetail({ )} + {/* ── About this ward ────────────────────────────────── */} + {profile && } + {/* ── Prev / next ward ───────────────────────────────── */}
+
+

+ About this ward +

+ {incumbent && ( +

+ + {incumbent.href ? ( + + {incumbent.name} + + ) : ( + + {incumbent.name} + + )} + + Seeking re-election + +

+ )} + {brief && ( +

+ {brief} +

+ )} +
+ + {stats.length > 0 && ( + <> + {/* Borders are drawn on the leading edge of each cell rather than the + trailing one, so the grid never paints a rule down its outer + right edge on a row that happens to be full. */} +
+ {stats.map((stat) => ( +
+
+ {stat.value} +
+
+ {stat.label} +
+ {(stat.note || stat.cityValue) && ( +

+ {stat.note} + {stat.note && stat.cityValue && " · "} + {stat.cityValue && ( + + {stat.cityValue} citywide + + )} +

+ )} +
+ ))} +
+ {source && ( +

+ Figures from {source}. +

+ )} + + )} +
+ ); +}