From 5ad9bd5e8ab9d8d38d73d7eae6976b44028edf59 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Thu, 20 Feb 2025 17:08:28 +0000 Subject: [PATCH 01/16] Add UK Parliamentary constituencies Fixes #2369 --- src/pages/policy/output/ImpactTypes.jsx | 6 + .../AverageChangeByConstituency.jsx | 125 +++++++++++ .../RelativeChangeByConstituency.jsx | 125 +++++++++++ .../WinnersLosersByConstituency.jsx | 196 ++++++++++++++++++ src/pages/policy/output/tree.js | 18 ++ 5 files changed, 470 insertions(+) create mode 100644 src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx create mode 100644 src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx create mode 100644 src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx diff --git a/src/pages/policy/output/ImpactTypes.jsx b/src/pages/policy/output/ImpactTypes.jsx index 79427fb5f..9441ddad0 100644 --- a/src/pages/policy/output/ImpactTypes.jsx +++ b/src/pages/policy/output/ImpactTypes.jsx @@ -27,6 +27,9 @@ import { LaborSupplyDecileRelativeImpactTotal, } from "./laborSupply/LaborSupplyDecileRelativeImpacts"; import Analysis from "./Analysis"; +import WinnersLosersByConstituency from "./constituencies/WinnersLosersByConstituency"; +import RelativeChangeByConstituency from "./constituencies/RelativeChangeByConstituency"; +import AverageChangeByConstituency from "./constituencies/AverageChangeByConstituency"; const map = { "budgetaryImpact.overall": budgetaryImpact, @@ -37,11 +40,14 @@ const map = { "distributionalImpact.wealthDecile.relative": relativeImpactByWealthDecile, "winnersAndLosers.incomeDecile": intraDecileImpact, "winnersAndLosers.wealthDecile": intraWealthDecileImpact, + "winnersAndLosers.constituencies": WinnersLosersByConstituency, "povertyImpact.regular.byAge": povertyImpact, "povertyImpact.deep.byAge": deepPovertyImpact, "povertyImpact.regular.byGender": povertyImpactByGender, "povertyImpact.deep.byGender": deepPovertyImpactByGender, "povertyImpact.regular.byRace": povertyImpactByRace, + "constituencies.relative": RelativeChangeByConstituency, + "constituencies.average": AverageChangeByConstituency, inequalityImpact: inequalityImpact, cliffImpact: cliffImpact, "laborSupplyImpact.earnings.overall.absolute": LaborSupplyResponseAbsolute, diff --git a/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx b/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx new file mode 100644 index 000000000..b0107f435 --- /dev/null +++ b/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx @@ -0,0 +1,125 @@ +import { useContext } from "react"; +import Plot from "react-plotly.js"; +import { ChartLogo } from "../../../../api/charts"; +import { + ordinal, + localeCode, + formatCurrency, + precision, + formatPercent, +} from "../../../../lang/format"; +import { + ChartWidthContext, + HoverCardContext, +} from "../../../../layout/HoverCard"; +import style from "../../../../style"; +import { plotLayoutFont } from "../utils"; +import React from "react"; +import ImpactChart, { absoluteChangeMessage, wordWrap } from "../ImpactChart"; +import { title } from "./WinnersLosersByConstituency"; + +export function ImpactPlot(props) { + const { data, policyLabel, metadata, mobile } = props; + + let xValues = Object.values(data).map((item) => item.x); + const yValues = Object.values(data).map((item) => item.y); + for (let i = 0; i < xValues.length; i++) { + if (yValues[i] % 2 === 0) { + xValues[i] = xValues[i] + 0.5; + } + } + const colorValues = Object.values(data).map( + (item) => item.average_household_income_change, + ); + const maxAbsValue = Math.max(...colorValues.map(Math.abs)); + return ( + + formatCurrency(value, metadata.countryId, { + minimumFractionDigits: 0, + }), + ), + marker: { + color: colorValues, + symbol: "hexagon", + size: 12, + coloraxis: "coloraxis", + }, + showscale: true, + }, + ]} + layout={{ + xaxis: { + visible: false, + showgrid: false, + showline: false, + }, + yaxis: { + visible: false, + showgrid: false, + showline: false, + }, + height: 600, + showlegend: false, + coloraxis: { + showscale: true, + cmin: -maxAbsValue, + cmax: maxAbsValue, + colorbar: { + outlinewidth: 0, + thickness: 10, + tickformat: "$,.0f", + }, + colorscale: [ + [0, style.colors.DARK_GRAY], + [0.2, style.colors.MEDIUM_LIGHT_GRAY], + [0.4, style.colors.LIGHT_GRAY], + [0.6, style.colors.BLUE_LIGHT], + [1, style.colors.BLUE], + ], + }, + margin: { + t: 0, + b: 80, + l: 80, + r: 0, + }, + ...ChartLogo(1.1, -0.05), + }} + style={{ + width: "90%", + marginLeft: 20, + marginBottom: !mobile && 50, + }} + config={{ + displayModeBar: false, + responsive: true, + locale: localeCode(metadata.countryId), + }} + /> + ); +} + +export default function AverageChangeByConstituency(props) { + const { impact, policyLabel, metadata, mobile } = props; + + const chart = ( + + + + ); + const csv = () => { + return null; + }; + return { chart: chart, csv: csv }; +} diff --git a/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx b/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx new file mode 100644 index 000000000..20b2ba559 --- /dev/null +++ b/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx @@ -0,0 +1,125 @@ +import { useContext } from "react"; +import Plot from "react-plotly.js"; +import { ChartLogo } from "../../../../api/charts"; +import { + ordinal, + localeCode, + formatCurrency, + precision, + formatPercent, +} from "../../../../lang/format"; +import { + ChartWidthContext, + HoverCardContext, +} from "../../../../layout/HoverCard"; +import style from "../../../../style"; +import { plotLayoutFont } from "../utils"; +import React from "react"; +import ImpactChart, { absoluteChangeMessage, wordWrap } from "../ImpactChart"; +import { title } from "./WinnersLosersByConstituency"; + +export function ImpactPlot(props) { + const { data, policyLabel, metadata, mobile } = props; + + let xValues = Object.values(data).map((item) => item.x); + const yValues = Object.values(data).map((item) => item.y); + for (let i = 0; i < xValues.length; i++) { + if (yValues[i] % 2 === 0) { + xValues[i] = xValues[i] + 0.5; + } + } + const colorValues = Object.values(data).map( + (item) => item.relative_household_income_change, + ); + const maxAbsValue = Math.max(...colorValues.map(Math.abs)); + return ( + + formatPercent(value, metadata.countryId, { + minimumFractionDigits: 1, + }), + ), + marker: { + color: colorValues, + symbol: "hexagon", + size: 12, + coloraxis: "coloraxis", + }, + showscale: true, + }, + ]} + layout={{ + xaxis: { + visible: false, + showgrid: false, + showline: false, + }, + yaxis: { + visible: false, + showgrid: false, + showline: false, + }, + height: 600, + showlegend: false, + coloraxis: { + showscale: true, + cmin: -maxAbsValue, + cmax: maxAbsValue, + colorbar: { + outlinewidth: 0, + thickness: 10, + tickformat: ".0%", + }, + colorscale: [ + [0, style.colors.DARK_GRAY], + [0.2, style.colors.MEDIUM_LIGHT_GRAY], + [0.4, style.colors.LIGHT_GRAY], + [0.6, style.colors.BLUE_LIGHT], + [1, style.colors.BLUE], + ], + }, + margin: { + t: 0, + b: 80, + l: 80, + r: 0, + }, + ...ChartLogo(1.1, -0.05), + }} + style={{ + width: "90%", + marginLeft: 20, + marginBottom: !mobile && 50, + }} + config={{ + displayModeBar: false, + responsive: true, + locale: localeCode(metadata.countryId), + }} + /> + ); +} + +export default function RelativeChangeByConstituency(props) { + const { impact, policyLabel, metadata, mobile } = props; + + const chart = ( + + + + ); + const csv = () => { + return null; + }; + return { chart: chart, csv: csv }; +} diff --git a/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx new file mode 100644 index 000000000..81b8baec0 --- /dev/null +++ b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx @@ -0,0 +1,196 @@ +import { useContext } from "react"; +import Plot from "react-plotly.js"; +import { ChartLogo } from "../../../../api/charts"; +import { + ordinal, + localeCode, + formatCurrency, + precision, + formatPercent, +} from "../../../../lang/format"; +import { + ChartWidthContext, + HoverCardContext, +} from "../../../../layout/HoverCard"; +import style from "../../../../style"; +import { plotLayoutFont } from "../utils"; +import React from "react"; +import ImpactChart, { absoluteChangeMessage, wordWrap } from "../ImpactChart"; + +export function ImpactPlot(props) { + const { yaxistitle, all, policyLabel, metadata, mobile } = props; + + const chartWidth = useContext(ChartWidthContext); + + const colorMap = { + "Gain more than 5%": style.colors.BLUE, + "Gain less than 5%": style.colors.BLUE_LIGHT, + "No change": style.colors.LIGHT_GRAY, + "Lose less than 5%": style.colors.MEDIUM_LIGHT_GRAY, + "Lose more than 5%": style.colors.DARK_GRAY, + }; + + const hoverTextMap = { + "Gain more than 5%": "gain more than 5% of", + "Gain less than 5%": "gain less than 5% of", + "No change": "neither gain nor lose", + "Lose less than 5%": "lose less than 5% of", + "Lose more than 5%": "lose more than 5% of", + }; + + const legendTextMap = { + "Gain more than 5%": "Gain more than 5%", + "Gain less than 5%": "Gain less than 5%", + "No change": "No change", + "Lose less than 5%": "Loss less than 5%", + "Lose more than 5%": "Loss more than 5%", + }; + + // type1: "all" | "deciles" + // type2: "Gain more than 5%" | "Gain less than 5%" | "No change" | "Lose less than 5%" | "Lose more than 5%" + function trace(type1, type2) { + const hoverTitle = (y) => (y === "All" ? `All households` : `Decile ${y}`); + function hoverMessage(x, y) { + const term1 = + type1 === "all" + ? "Of all households," + : `Of households in the ${ordinal(y)} decile,`; + const term2 = x; + const msg = `${policyLabel} would cause ${term2} constituencies to ${hoverTextMap[type2]} their net income.`; + return wordWrap(msg, 50).replaceAll("\n", "
"); + } + const xArray = [all[type2]]; + const yArray = ["All"]; + return { + x: xArray, + y: yArray, + xaxis: "x", + yaxis: "y", + type: "bar", + name: legendTextMap[type2], + legendgroup: type2, + showlegend: true, + marker: { + color: colorMap[type2], + }, + orientation: "h", + text: xArray, + textposition: "inside", + textangle: 0, + customdata: xArray.map((x, i) => { + const y = yArray[i]; + return { title: hoverTitle(y), msg: hoverMessage(x, y) }; + }), + hovertemplate: `%{customdata.title}

%{customdata.msg}`, + }; + } + + const product = (a, b) => + a.reduce((p, x) => [...p, ...b.map((y) => [x, y])], []); + + const data = product( + ["all"], + [ + "Gain more than 5%", + "Gain less than 5%", + "No change", + "Lose less than 5%", + "Lose more than 5%", + ], + ); + + const plotData = data.map((types) => trace(types[0], types[1])); + + return ( + ", + font: { + family: "Roboto Serif", + }, + }, + font: { + family: "Roboto Serif", + }, + }, + }} + config={{ + displayModeBar: false, + locale: localeCode(metadata.countryId), + }} + /> + ); +} + +export function title(policyLabel, impact) { + const count_benefiting = + impact?.constituency_impact?.overall["Gain more than 5%"] + + impact?.constituency_impact?.overall["Gain less than 5%"]; + const count_losing = + impact?.constituency_impact?.overall["Lose more than 5%"] + + impact?.constituency_impact?.overall["Lose less than 5%"]; + const count_no_change = impact?.constituency_impact?.overall["No change"]; + if (count_benefiting > count_no_change + count_no_change) { + return `${policyLabel} would raise net income on average in a majority (of ${count_benefiting - count_losing - count_no_change}) of Parliamentary constituencies`; + } else if (count_no_change > count_benefiting + count_losing) { + return `${policyLabel} would lower net income on average in a majority (of ${count_losing - count_benefiting - count_no_change}) of Parliamentary constituencies`; + } else if (count_benefiting > count_losing) { + return `${policyLabel} would raise net income on average in ${count_benefiting} Parliamentary constituencies`; + } else if (count_benefiting < count_losing) { + return `${policyLabel} would lower net income on average in ${count_losing} Parliamentary constituencies`; + } + return `${policyLabel} would not change net income on average in any Parliamentary constituency`; +} + +export default function WinnersLosersByConstituency(props) { + const { impact, policyLabel, metadata, mobile } = props; + + const chart = ( + + + + ); + const csv = () => { + return null; + }; + return { chart: chart, csv: csv }; +} diff --git a/src/pages/policy/output/tree.js b/src/pages/policy/output/tree.js index 1d1a620e0..0e365138e 100644 --- a/src/pages/policy/output/tree.js +++ b/src/pages/policy/output/tree.js @@ -114,6 +114,10 @@ export function getPolicyOutputTree(countryId) { name: "policyOutput.winnersAndLosers.wealthDecile", label: "By wealth decile", }, + countryId === "uk" && { + name: "policyOutput.winnersAndLosers.constituencies", + label: "By Parliamentary constituency", + }, ].filter((x) => x), }, { @@ -162,6 +166,20 @@ export function getPolicyOutputTree(countryId) { name: "policyOutput.cliffImpact", label: "Cliff impact", }, + countryId === "uk" && { + name: "policyOutput.constituencies", + label: "Parliamentary constituencies (experimental)", + children: [ + { + name: "policyOutput.constituencies.relative", + label: "Relative change", + }, + { + name: "policyOutput.constituencies.average", + label: "Average change", + }, + ], + }, { name: "policyOutput.laborSupplyImpact", label: From c477d856974d24fa53611f13de294d2bef3e34af Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Thu, 20 Feb 2025 19:31:11 +0000 Subject: [PATCH 02/16] Hide behind uk_local_areas_beta=true --- src/layout/FolderPage.jsx | 5 ++++- src/pages/PolicyPage.jsx | 10 ++++++++-- src/pages/policy/output/Display.jsx | 2 +- src/pages/policy/output/tree.js | 8 ++++---- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/layout/FolderPage.jsx b/src/layout/FolderPage.jsx index c50cb55f5..afca9e47f 100644 --- a/src/layout/FolderPage.jsx +++ b/src/layout/FolderPage.jsx @@ -14,7 +14,10 @@ function FolderPageDescription(props) { const { metadata, inPolicySide } = props; const mobile = useMobile(); if (!metadata) return null; - const POLICY_OUTPUT_TREE = getPolicyOutputTree(metadata.countryId); + const POLICY_OUTPUT_TREE = getPolicyOutputTree( + metadata.countryId, + searchParams, + ); let currentNode; if (focus && focus.startsWith("policyOutput")) { diff --git a/src/pages/PolicyPage.jsx b/src/pages/PolicyPage.jsx index a96d1681f..bd432d9e4 100644 --- a/src/pages/PolicyPage.jsx +++ b/src/pages/PolicyPage.jsx @@ -54,7 +54,10 @@ export function ParameterSearch(props) { function PolicyLeftSidebar(props) { const { metadata } = props; const [searchParams, setSearchParams] = useSearchParams(); - const POLICY_OUTPUT_TREE = getPolicyOutputTree(metadata.countryId); + const POLICY_OUTPUT_TREE = getPolicyOutputTree( + metadata.countryId, + searchParams, + ); const selected = searchParams.get("focus") || ""; const onSelect = (name) => { @@ -159,7 +162,10 @@ export default function PolicyPage(props) { ); } else if (isOutput) { - const POLICY_OUTPUT_TREE = getPolicyOutputTree(metadata.countryId); + const POLICY_OUTPUT_TREE = getPolicyOutputTree( + metadata.countryId, + searchParams, + ); const validFocusValues = impactKeys; const stripped_focus = focus.replace("policyOutput.", ""); diff --git a/src/pages/policy/output/Display.jsx b/src/pages/policy/output/Display.jsx index 63db36f0f..af6d2f5fd 100644 --- a/src/pages/policy/output/Display.jsx +++ b/src/pages/policy/output/Display.jsx @@ -296,7 +296,7 @@ export function LowLevelDisplay(props) { const dataset = urlParams.get("dataset"); const selectedVersion = urlParams.get("version") || metadata.version; const region = urlParams.get("region"); - const policyOutputTree = getPolicyOutputTree(metadata.countryId); + const policyOutputTree = getPolicyOutputTree(metadata.countryId, urlParams); const url = encodeURIComponent(window.location.href); const encodedPolicyLabel = encodeURIComponent(getPolicyLabel(policy)); const twitterLink = `https://twitter.com/intent/tweet?url=${url}&text=${encodedPolicyLabel}%2C%20on%20PolicyEngine`; diff --git a/src/pages/policy/output/tree.js b/src/pages/policy/output/tree.js index 0e365138e..aa6e3a19e 100644 --- a/src/pages/policy/output/tree.js +++ b/src/pages/policy/output/tree.js @@ -44,7 +44,7 @@ export const policyOutputs = { codeReproducibility: "Reproduce in Python", }; -export function getPolicyOutputTree(countryId) { +export function getPolicyOutputTree(countryId, searchParams = {}) { const tree = [ { name: "policyOutput", @@ -114,7 +114,7 @@ export function getPolicyOutputTree(countryId) { name: "policyOutput.winnersAndLosers.wealthDecile", label: "By wealth decile", }, - countryId === "uk" && { + searchParams.get("uk_local_areas_beta") && { name: "policyOutput.winnersAndLosers.constituencies", label: "By Parliamentary constituency", }, @@ -166,7 +166,7 @@ export function getPolicyOutputTree(countryId) { name: "policyOutput.cliffImpact", label: "Cliff impact", }, - countryId === "uk" && { + searchParams.get("uk_local_areas_beta") && { name: "policyOutput.constituencies", label: "Parliamentary constituencies (experimental)", children: [ @@ -263,7 +263,7 @@ export function getPolicyOutputTree(countryId) { name: "policyOutput.codeReproducibility", label: "Reproduce in Python", }, - ], + ].filter((x) => x), }, ]; From 6b75c3c4e91e7997f7df877fd220571e50e80c93 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 23 Feb 2025 13:44:44 +0000 Subject: [PATCH 03/16] Split out by region --- src/pages/policy/output/ImpactTypes.jsx | 6 +- .../AverageChangeByConstituency.jsx | 46 ++++----- .../RelativeChangeByConstituency.jsx | 34 +++---- .../WinnersLosersByConstituency.jsx | 94 ++++++++++++------- 4 files changed, 94 insertions(+), 86 deletions(-) diff --git a/src/pages/policy/output/ImpactTypes.jsx b/src/pages/policy/output/ImpactTypes.jsx index 9441ddad0..ae15f5318 100644 --- a/src/pages/policy/output/ImpactTypes.jsx +++ b/src/pages/policy/output/ImpactTypes.jsx @@ -71,11 +71,7 @@ const map = { // get representations of the impact as a chart and a csv. The returned object // has type {chart: , csv: }. export const getImpactReps = (impactKey, props) => { - try { - return map[impactKey](props); - } catch (e) { - throw new Error(`Impact type ${impactKey} not found`); - } + return map[impactKey](props); }; export const impactKeys = Object.keys(map); diff --git a/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx b/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx index b0107f435..4476fe038 100644 --- a/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx +++ b/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx @@ -1,36 +1,34 @@ -import { useContext } from "react"; import Plot from "react-plotly.js"; import { ChartLogo } from "../../../../api/charts"; import { - ordinal, localeCode, formatCurrency, - precision, - formatPercent, } from "../../../../lang/format"; -import { - ChartWidthContext, - HoverCardContext, -} from "../../../../layout/HoverCard"; import style from "../../../../style"; -import { plotLayoutFont } from "../utils"; import React from "react"; -import ImpactChart, { absoluteChangeMessage, wordWrap } from "../ImpactChart"; +import ImpactChart from "../ImpactChart"; import { title } from "./WinnersLosersByConstituency"; export function ImpactPlot(props) { - const { data, policyLabel, metadata, mobile } = props; - - let xValues = Object.values(data).map((item) => item.x); - const yValues = Object.values(data).map((item) => item.y); - for (let i = 0; i < xValues.length; i++) { - if (yValues[i] % 2 === 0) { - xValues[i] = xValues[i] + 0.5; + const { data, metadata, mobile } = props; + + let xValues = Object.values(data).map((item) => item.x); + const constituencyNames = Object.keys(data); + let text = []; + const yValues = Object.values(data).map((item) => item.y); + const colorValues = Object.values(data).map( + (item) => item.average_household_income_change, + ); + let valueStr; + for (let i = 0; i < xValues.length; i++) { + if (yValues[i] % 2 === 0) { + xValues[i] = xValues[i] + 0.5; + } + valueStr = formatCurrency(colorValues[i], metadata.countryId); + text.push( + `${constituencyNames[i]}: ${valueStr}`, + ) } - } - const colorValues = Object.values(data).map( - (item) => item.average_household_income_change, - ); const maxAbsValue = Math.max(...colorValues.map(Math.abs)); return ( - formatCurrency(value, metadata.countryId, { - minimumFractionDigits: 0, - }), - ), + text: text, marker: { color: colorValues, symbol: "hexagon", diff --git a/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx b/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx index 20b2ba559..118b9e95e 100644 --- a/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx +++ b/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx @@ -1,36 +1,36 @@ -import { useContext } from "react"; import Plot from "react-plotly.js"; import { ChartLogo } from "../../../../api/charts"; import { - ordinal, localeCode, - formatCurrency, - precision, formatPercent, } from "../../../../lang/format"; -import { - ChartWidthContext, - HoverCardContext, -} from "../../../../layout/HoverCard"; import style from "../../../../style"; -import { plotLayoutFont } from "../utils"; import React from "react"; -import ImpactChart, { absoluteChangeMessage, wordWrap } from "../ImpactChart"; +import ImpactChart from "../ImpactChart"; import { title } from "./WinnersLosersByConstituency"; export function ImpactPlot(props) { - const { data, policyLabel, metadata, mobile } = props; + const { data, metadata, mobile } = props; let xValues = Object.values(data).map((item) => item.x); + const constituencyNames = Object.keys(data); + let text = []; const yValues = Object.values(data).map((item) => item.y); + const colorValues = Object.values(data).map( + (item) => item.relative_household_income_change, + ); + let valueStr; for (let i = 0; i < xValues.length; i++) { if (yValues[i] % 2 === 0) { xValues[i] = xValues[i] + 0.5; } + valueStr = formatPercent(colorValues[i], metadata.countryId, { + minimumFractionDigits: 1, + }); + text.push( + `${constituencyNames[i]}: ${valueStr}`, + ) } - const colorValues = Object.values(data).map( - (item) => item.relative_household_income_change, - ); const maxAbsValue = Math.max(...colorValues.map(Math.abs)); return ( - formatPercent(value, metadata.countryId, { - minimumFractionDigits: 1, - }), - ), + text: text, marker: { color: colorValues, symbol: "hexagon", diff --git a/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx index 81b8baec0..afab54f7a 100644 --- a/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx +++ b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx @@ -1,26 +1,15 @@ -import { useContext } from "react"; import Plot from "react-plotly.js"; import { ChartLogo } from "../../../../api/charts"; import { - ordinal, localeCode, - formatCurrency, - precision, - formatPercent, } from "../../../../lang/format"; -import { - ChartWidthContext, - HoverCardContext, -} from "../../../../layout/HoverCard"; import style from "../../../../style"; import { plotLayoutFont } from "../utils"; import React from "react"; -import ImpactChart, { absoluteChangeMessage, wordWrap } from "../ImpactChart"; +import ImpactChart, { wordWrap } from "../ImpactChart"; export function ImpactPlot(props) { - const { yaxistitle, all, policyLabel, metadata, mobile } = props; - - const chartWidth = useContext(ChartWidthContext); + const { yaxistitle, outcomes_by_region, policyLabel, metadata, mobile } = props; const colorMap = { "Gain more than 5%": style.colors.BLUE, @@ -53,32 +42,38 @@ export function ImpactPlot(props) { function hoverMessage(x, y) { const term1 = type1 === "all" - ? "Of all households," - : `Of households in the ${ordinal(y)} decile,`; + ? "Of all constituencies," + : `Of households in ${y},`; const term2 = x; - const msg = `${policyLabel} would cause ${term2} constituencies to ${hoverTextMap[type2]} their net income.`; + const msg = `${term1} ${policyLabel} would cause ${term2} constituencies to ${hoverTextMap[type2]} their net income.`; return wordWrap(msg, 50).replaceAll("\n", "
"); } - const xArray = [all[type2]]; - const yArray = ["All"]; + const regions = type1 === "all" ? ["uk"] : ["england", "wales", "scotland", "northern_ireland"].reverse(); + const regionLabels = type1 === "all" ? ["All"] : ["England", "Wales", "Scotland", "Northern Ireland"].reverse(); + const counstituencyCounts = regions.map(region => outcomes_by_region[region][type2]); + const totalConstituencies = regions.map(region => Object.values(outcomes_by_region[region]).reduce((a, b) => a+b)); + let percentages = [] + for (let i = 0; i < counstituencyCounts.length; i++) { + percentages.push(counstituencyCounts[i] / totalConstituencies[i]) + } return { - x: xArray, - y: yArray, - xaxis: "x", - yaxis: "y", + x: percentages, + y: regionLabels, + xaxis: type1 === "all" ? "x" : "x2", + yaxis: type1 === "all" ? "y" : "y2", type: "bar", name: legendTextMap[type2], legendgroup: type2, - showlegend: true, + showlegend: type1 === "all", marker: { color: colorMap[type2], }, orientation: "h", - text: xArray, + text: counstituencyCounts, textposition: "inside", textangle: 0, - customdata: xArray.map((x, i) => { - const y = yArray[i]; + customdata: counstituencyCounts.map((x, i) => { + const y = regionLabels[i]; return { title: hoverTitle(y), msg: hoverMessage(x, y) }; }), hovertemplate: `%{customdata.title}

%{customdata.msg}`, @@ -89,7 +84,7 @@ export function ImpactPlot(props) { a.reduce((p, x) => [...p, ...b.map((y) => [x, y])], []); const data = product( - ["all"], + ["all", "regions"], [ "Gain more than 5%", "Gain less than 5%", @@ -107,19 +102,40 @@ export function ImpactPlot(props) { layout={{ barmode: "stack", orientation: "h", + grid: { + rows: 2, + columns: 1, + }, yaxis: { title: "", tickvals: ["All"], domain: [0.8, 1], }, xaxis: { - title: "Constituencies", + title: "", + tickformat: ".0%", + anchor: "y", + matches: "x2", + showgrid: false, + showticklabels: false, + fixedrange: true, + }, + xaxis2: { + title: "Constituency share", + tickformat: ".0%", + anchor: "y2", + fixedrange: true, }, - ...ChartLogo(0.9, 0.3), + yaxis2: { + title: yaxistitle, + anchor: "x2", + domain: [0, 0.75], + }, + ...ChartLogo(0.97, -0.3), margin: { t: 0, b: 80, - l: 40, + l: 130, r: 0, }, hoverlabel: { @@ -137,6 +153,7 @@ export function ImpactPlot(props) { legend: { x: 1, y: 1.25, + tracegroupgap: 10, title: { text: "Change in income
", font: { @@ -152,18 +169,23 @@ export function ImpactPlot(props) { displayModeBar: false, locale: localeCode(metadata.countryId), }} + style={{ + width: "100%", + marginBottom: !mobile && 50, + }} /> ); } export function title(policyLabel, impact) { + const outcomes = impact?.constituency_impact?.outcomes_by_region?.uk const count_benefiting = - impact?.constituency_impact?.overall["Gain more than 5%"] + - impact?.constituency_impact?.overall["Gain less than 5%"]; + outcomes["Gain more than 5%"] + + outcomes["Gain less than 5%"]; const count_losing = - impact?.constituency_impact?.overall["Lose more than 5%"] + - impact?.constituency_impact?.overall["Lose less than 5%"]; - const count_no_change = impact?.constituency_impact?.overall["No change"]; + outcomes["Lose more than 5%"] + + outcomes["Lose less than 5%"]; + const count_no_change = outcomes["No change"]; if (count_benefiting > count_no_change + count_no_change) { return `${policyLabel} would raise net income on average in a majority (of ${count_benefiting - count_losing - count_no_change}) of Parliamentary constituencies`; } else if (count_no_change > count_benefiting + count_losing) { @@ -183,7 +205,7 @@ export default function WinnersLosersByConstituency(props) { From 1d88fa321052babafae4e997ed1eb704f08a6f6c Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Mon, 24 Feb 2025 17:01:21 +0000 Subject: [PATCH 04/16] Format --- .../AverageChangeByConstituency.jsx | 37 ++++++++--------- .../RelativeChangeByConstituency.jsx | 9 +--- .../WinnersLosersByConstituency.jsx | 41 +++++++++++-------- 3 files changed, 41 insertions(+), 46 deletions(-) diff --git a/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx b/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx index 4476fe038..6a7c2f02e 100644 --- a/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx +++ b/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx @@ -1,9 +1,6 @@ import Plot from "react-plotly.js"; import { ChartLogo } from "../../../../api/charts"; -import { - localeCode, - formatCurrency, -} from "../../../../lang/format"; +import { localeCode, formatCurrency } from "../../../../lang/format"; import style from "../../../../style"; import React from "react"; import ImpactChart from "../ImpactChart"; @@ -11,24 +8,22 @@ import { title } from "./WinnersLosersByConstituency"; export function ImpactPlot(props) { const { data, metadata, mobile } = props; - - let xValues = Object.values(data).map((item) => item.x); - const constituencyNames = Object.keys(data); - let text = []; - const yValues = Object.values(data).map((item) => item.y); - const colorValues = Object.values(data).map( - (item) => item.average_household_income_change, - ); - let valueStr; - for (let i = 0; i < xValues.length; i++) { - if (yValues[i] % 2 === 0) { - xValues[i] = xValues[i] + 0.5; - } - valueStr = formatCurrency(colorValues[i], metadata.countryId); - text.push( - `${constituencyNames[i]}: ${valueStr}`, - ) + + let xValues = Object.values(data).map((item) => item.x); + const constituencyNames = Object.keys(data); + let text = []; + const yValues = Object.values(data).map((item) => item.y); + const colorValues = Object.values(data).map( + (item) => item.average_household_income_change, + ); + let valueStr; + for (let i = 0; i < xValues.length; i++) { + if (yValues[i] % 2 === 0) { + xValues[i] = xValues[i] + 0.5; } + valueStr = formatCurrency(colorValues[i], metadata.countryId); + text.push(`${constituencyNames[i]}: ${valueStr}`); + } const maxAbsValue = Math.max(...colorValues.map(Math.abs)); return ( (y === "All" ? `All households` : `Decile ${y}`); function hoverMessage(x, y) { const term1 = - type1 === "all" - ? "Of all constituencies," - : `Of households in ${y},`; + type1 === "all" ? "Of all constituencies," : `Of households in ${y},`; const term2 = x; const msg = `${term1} ${policyLabel} would cause ${term2} constituencies to ${hoverTextMap[type2]} their net income.`; return wordWrap(msg, 50).replaceAll("\n", "
"); } - const regions = type1 === "all" ? ["uk"] : ["england", "wales", "scotland", "northern_ireland"].reverse(); - const regionLabels = type1 === "all" ? ["All"] : ["England", "Wales", "Scotland", "Northern Ireland"].reverse(); - const counstituencyCounts = regions.map(region => outcomes_by_region[region][type2]); - const totalConstituencies = regions.map(region => Object.values(outcomes_by_region[region]).reduce((a, b) => a+b)); - let percentages = [] + const regions = + type1 === "all" + ? ["uk"] + : ["england", "wales", "scotland", "northern_ireland"].reverse(); + const regionLabels = + type1 === "all" + ? ["All"] + : ["England", "Wales", "Scotland", "Northern Ireland"].reverse(); + const counstituencyCounts = regions.map( + (region) => outcomes_by_region[region][type2], + ); + const totalConstituencies = regions.map((region) => + Object.values(outcomes_by_region[region]).reduce((a, b) => a + b), + ); + let percentages = []; for (let i = 0; i < counstituencyCounts.length; i++) { - percentages.push(counstituencyCounts[i] / totalConstituencies[i]) + percentages.push(counstituencyCounts[i] / totalConstituencies[i]); } return { x: percentages, @@ -178,13 +185,11 @@ export function ImpactPlot(props) { } export function title(policyLabel, impact) { - const outcomes = impact?.constituency_impact?.outcomes_by_region?.uk + const outcomes = impact?.constituency_impact?.outcomes_by_region?.uk; const count_benefiting = - outcomes["Gain more than 5%"] + - outcomes["Gain less than 5%"]; + outcomes["Gain more than 5%"] + outcomes["Gain less than 5%"]; const count_losing = - outcomes["Lose more than 5%"] + - outcomes["Lose less than 5%"]; + outcomes["Lose more than 5%"] + outcomes["Lose less than 5%"]; const count_no_change = outcomes["No change"]; if (count_benefiting > count_no_change + count_no_change) { return `${policyLabel} would raise net income on average in a majority (of ${count_benefiting - count_losing - count_no_change}) of Parliamentary constituencies`; From 4f86d641d63ea78c83c822cc7a11400d10262c40 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Mon, 24 Feb 2025 17:23:29 +0000 Subject: [PATCH 05/16] Adjust title per @anth-volk's suggestion --- .../constituencies/WinnersLosersByConstituency.jsx | 10 +++++----- src/pages/policy/output/tree.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx index 2d4486031..2e1020c63 100644 --- a/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx +++ b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx @@ -192,15 +192,15 @@ export function title(policyLabel, impact) { outcomes["Lose more than 5%"] + outcomes["Lose less than 5%"]; const count_no_change = outcomes["No change"]; if (count_benefiting > count_no_change + count_no_change) { - return `${policyLabel} would raise net income on average in a majority (of ${count_benefiting - count_losing - count_no_change}) of Parliamentary constituencies`; + return `${policyLabel} would raise net income on average in a majority (of ${Math.abs(count_benefiting - count_losing - count_no_change)}) of parliamentary constituencies`; } else if (count_no_change > count_benefiting + count_losing) { - return `${policyLabel} would lower net income on average in a majority (of ${count_losing - count_benefiting - count_no_change}) of Parliamentary constituencies`; + return `${policyLabel} would lower net income on average in a majority (of ${Math.abs(count_losing - count_benefiting - count_no_change)}) of parliamentary constituencies`; } else if (count_benefiting > count_losing) { - return `${policyLabel} would raise net income on average in ${count_benefiting} Parliamentary constituencies`; + return `${policyLabel} would raise net income on average in ${count_benefiting} parliamentary constituencies`; } else if (count_benefiting < count_losing) { - return `${policyLabel} would lower net income on average in ${count_losing} Parliamentary constituencies`; + return `${policyLabel} would lower net income on average in ${count_losing} parliamentary constituencies`; } - return `${policyLabel} would not change net income on average in any Parliamentary constituency`; + return `${policyLabel} would not change net income on average in any parliamentary constituency`; } export default function WinnersLosersByConstituency(props) { diff --git a/src/pages/policy/output/tree.js b/src/pages/policy/output/tree.js index aa6e3a19e..e822324aa 100644 --- a/src/pages/policy/output/tree.js +++ b/src/pages/policy/output/tree.js @@ -116,7 +116,7 @@ export function getPolicyOutputTree(countryId, searchParams = {}) { }, searchParams.get("uk_local_areas_beta") && { name: "policyOutput.winnersAndLosers.constituencies", - label: "By Parliamentary constituency", + label: "By parliamentary constituency", }, ].filter((x) => x), }, From 13d70015fcf54ba618596e46124a9a8645b21229 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Mon, 24 Feb 2025 23:41:47 +0000 Subject: [PATCH 06/16] Add CSV outputs --- .../AverageChangeByConstituency.jsx | 10 +++++- .../RelativeChangeByConstituency.jsx | 10 +++++- .../WinnersLosersByConstituency.jsx | 33 ++++++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx b/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx index 6a7c2f02e..d218bf9c4 100644 --- a/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx +++ b/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx @@ -108,7 +108,15 @@ export default function AverageChangeByConstituency(props) {
); const csv = () => { - return null; + const header = ["Constituency", "Average Change"]; + const constituencyData = impact?.constituency_impact?.by_constituency || {}; + const data = [ + header, + ...Object.entries(constituencyData).map(([constituency, data]) => { + return [constituency, data.average_household_income_change]; + }), + ]; + return data; }; return { chart: chart, csv: csv }; } diff --git a/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx b/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx index deb7b138a..a8219a32d 100644 --- a/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx +++ b/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx @@ -110,7 +110,15 @@ export default function RelativeChangeByConstituency(props) { ); const csv = () => { - return null; + const header = ["Constituency", "Relative Change"]; + const constituencyData = impact?.constituency_impact?.by_constituency || {}; + const data = [ + header, + ...Object.entries(constituencyData).map(([constituency, data]) => { + return [constituency, data.relative_household_income_change]; + }), + ]; + return data; }; return { chart: chart, csv: csv }; } diff --git a/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx index 2e1020c63..a154633b6 100644 --- a/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx +++ b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx @@ -217,7 +217,38 @@ export default function WinnersLosersByConstituency(props) { ); const csv = () => { - return null; + const header = ["Region", "Category", "Count", "Percentage"]; + const outcomesData = impact?.constituency_impact?.outcomes_by_region || {}; + + const regions = ["uk", "england", "wales", "scotland", "northern_ireland"]; + const regionLabels = ["All", "England", "Wales", "Scotland", "Northern Ireland"]; + const categories = [ + "Gain more than 5%", + "Gain less than 5%", + "No change", + "Lose less than 5%", + "Lose more than 5%" + ]; + + const data = [header]; + + for (let i = 0; i < regions.length; i++) { + const region = regions[i]; + const regionLabel = regionLabels[i]; + + if (outcomesData[region]) { + const totalConstituencies = Object.values(outcomesData[region]).reduce((a, b) => a + b, 0); + + for (const category of categories) { + const count = outcomesData[region][category] || 0; + const percentage = totalConstituencies > 0 ? count / totalConstituencies : 0; + + data.push([regionLabel, category, count, percentage]); + } + } + } + + return data; }; return { chart: chart, csv: csv }; } From b564b8528ba01dcea9a4ee263e5a00f8bb71e6fc Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Tue, 25 Feb 2025 11:56:18 +0000 Subject: [PATCH 07/16] Hide region options when not in beta --- src/pages/policy/PolicyRightSidebar.jsx | 6 ++++ .../WinnersLosersByConstituency.jsx | 32 ++++++++++++------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/pages/policy/PolicyRightSidebar.jsx b/src/pages/policy/PolicyRightSidebar.jsx index f3da96713..3746cfbb2 100644 --- a/src/pages/policy/PolicyRightSidebar.jsx +++ b/src/pages/policy/PolicyRightSidebar.jsx @@ -41,6 +41,12 @@ function RegionSelector(props) { if (inputRegion === "enhanced_us") { inputRegion = "us"; } + if (!searchParams.get("uk_local_areas_beta")) { + console.log(options); + options = options.filter( + (option) => !option.value.includes("constituency/"), + ); + } const [value] = useState(inputRegion || options[0].value); return ( diff --git a/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx index a154633b6..97c6a8eff 100644 --- a/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx +++ b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx @@ -219,35 +219,45 @@ export default function WinnersLosersByConstituency(props) { const csv = () => { const header = ["Region", "Category", "Count", "Percentage"]; const outcomesData = impact?.constituency_impact?.outcomes_by_region || {}; - + const regions = ["uk", "england", "wales", "scotland", "northern_ireland"]; - const regionLabels = ["All", "England", "Wales", "Scotland", "Northern Ireland"]; + const regionLabels = [ + "All", + "England", + "Wales", + "Scotland", + "Northern Ireland", + ]; const categories = [ "Gain more than 5%", "Gain less than 5%", "No change", "Lose less than 5%", - "Lose more than 5%" + "Lose more than 5%", ]; - + const data = [header]; - + for (let i = 0; i < regions.length; i++) { const region = regions[i]; const regionLabel = regionLabels[i]; - + if (outcomesData[region]) { - const totalConstituencies = Object.values(outcomesData[region]).reduce((a, b) => a + b, 0); - + const totalConstituencies = Object.values(outcomesData[region]).reduce( + (a, b) => a + b, + 0, + ); + for (const category of categories) { const count = outcomesData[region][category] || 0; - const percentage = totalConstituencies > 0 ? count / totalConstituencies : 0; - + const percentage = + totalConstituencies > 0 ? count / totalConstituencies : 0; + data.push([regionLabel, category, count, percentage]); } } } - + return data; }; return { chart: chart, csv: csv }; From 90beca6c30a1b6f28431c8885460e15b9b919a11 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Tue, 25 Feb 2025 14:34:15 +0000 Subject: [PATCH 08/16] Fix test --- src/pages/policy/output/tree.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pages/policy/output/tree.js b/src/pages/policy/output/tree.js index e822324aa..6a580b8f0 100644 --- a/src/pages/policy/output/tree.js +++ b/src/pages/policy/output/tree.js @@ -45,6 +45,15 @@ export const policyOutputs = { }; export function getPolicyOutputTree(countryId, searchParams = {}) { + // Helper to safely check if a URL parameter exists + const hasParam = (param) => { + if (!searchParams) return false; + if (typeof searchParams.get === 'function') { + return !!searchParams.get(param); + } + return !!searchParams[param]; + }; + const tree = [ { name: "policyOutput", @@ -114,7 +123,7 @@ export function getPolicyOutputTree(countryId, searchParams = {}) { name: "policyOutput.winnersAndLosers.wealthDecile", label: "By wealth decile", }, - searchParams.get("uk_local_areas_beta") && { + hasParam("uk_local_areas_beta") && { name: "policyOutput.winnersAndLosers.constituencies", label: "By parliamentary constituency", }, @@ -166,7 +175,7 @@ export function getPolicyOutputTree(countryId, searchParams = {}) { name: "policyOutput.cliffImpact", label: "Cliff impact", }, - searchParams.get("uk_local_areas_beta") && { + hasParam("uk_local_areas_beta") && { name: "policyOutput.constituencies", label: "Parliamentary constituencies (experimental)", children: [ @@ -268,4 +277,4 @@ export function getPolicyOutputTree(countryId, searchParams = {}) { ]; return tree; -} +} \ No newline at end of file From a402b6ff8bb912326d38e6fb57350f95e9bac4ce Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Tue, 25 Feb 2025 15:14:41 +0000 Subject: [PATCH 09/16] Format --- src/pages/policy/output/tree.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/policy/output/tree.js b/src/pages/policy/output/tree.js index 6a580b8f0..a82beaff1 100644 --- a/src/pages/policy/output/tree.js +++ b/src/pages/policy/output/tree.js @@ -45,12 +45,12 @@ export const policyOutputs = { }; export function getPolicyOutputTree(countryId, searchParams = {}) { - // Helper to safely check if a URL parameter exists + // Helper to safely check if a URL parameter exists const hasParam = (param) => { if (!searchParams) return false; - if (typeof searchParams.get === 'function') { + if (typeof searchParams.get === "function") { return !!searchParams.get(param); - } + } return !!searchParams[param]; }; @@ -277,4 +277,4 @@ export function getPolicyOutputTree(countryId, searchParams = {}) { ]; return tree; -} \ No newline at end of file +} From 3ca96f886f8a287418029144dc230e51a680cb81 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Wed, 26 Feb 2025 11:37:00 +0000 Subject: [PATCH 10/16] Adjust title --- .../output/constituencies/WinnersLosersByConstituency.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx index 97c6a8eff..ecb25b9a3 100644 --- a/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx +++ b/src/pages/policy/output/constituencies/WinnersLosersByConstituency.jsx @@ -193,7 +193,7 @@ export function title(policyLabel, impact) { const count_no_change = outcomes["No change"]; if (count_benefiting > count_no_change + count_no_change) { return `${policyLabel} would raise net income on average in a majority (of ${Math.abs(count_benefiting - count_losing - count_no_change)}) of parliamentary constituencies`; - } else if (count_no_change > count_benefiting + count_losing) { + } else if (count_losing > count_benefiting + count_no_change) { return `${policyLabel} would lower net income on average in a majority (of ${Math.abs(count_losing - count_benefiting - count_no_change)}) of parliamentary constituencies`; } else if (count_benefiting > count_losing) { return `${policyLabel} would raise net income on average in ${count_benefiting} parliamentary constituencies`; From 1a2b768b24da75e52f10a133c7a73a316986f304 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Wed, 26 Feb 2025 13:18:51 +0000 Subject: [PATCH 11/16] Fix chart legend --- .../output/constituencies/RelativeChangeByConstituency.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx b/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx index a8219a32d..435a34023 100644 --- a/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx +++ b/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx @@ -65,7 +65,7 @@ export function ImpactPlot(props) { colorbar: { outlinewidth: 0, thickness: 10, - tickformat: ".0%", + tickformat: ".1%", }, colorscale: [ [0, style.colors.DARK_GRAY], From 495394359575bcdb2e1916f0c085001520beabc9 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Thu, 27 Feb 2025 10:37:41 +0000 Subject: [PATCH 12/16] Add selector for beta entry --- src/pages/policy/PolicyRightSidebar.jsx | 50 +++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/src/pages/policy/PolicyRightSidebar.jsx b/src/pages/policy/PolicyRightSidebar.jsx index 3746cfbb2..639dc1216 100644 --- a/src/pages/policy/PolicyRightSidebar.jsx +++ b/src/pages/policy/PolicyRightSidebar.jsx @@ -18,7 +18,7 @@ import SearchOptions from "../../controls/SearchOptions"; import SearchParamNavButton from "../../controls/SearchParamNavButton"; import style from "../../style"; import PolicySearch from "./PolicySearch"; -import { Alert, Modal, Switch, Tooltip } from "antd"; +import { Alert, Modal, Switch, Tag, Tooltip } from "antd"; import { ExclamationCircleOutlined } from "@ant-design/icons"; import { defaultYear } from "data/constants"; import useDisplayCategory from "../../hooks/useDisplayCategory"; @@ -41,8 +41,7 @@ function RegionSelector(props) { if (inputRegion === "enhanced_us") { inputRegion = "us"; } - if (!searchParams.get("uk_local_areas_beta")) { - console.log(options); + if (!(searchParams.get("uk_local_areas_beta") === "true")) { options = options.filter( (option) => !option.value.includes("constituency/"), ); @@ -363,6 +362,47 @@ function BehavioralResponseToggle(props) { ); } +function LocalAreaFunctionalitySelector() { + const [searchParams, setSearchParams] = useSearchParams(); + const [isChecked, setIsChecked] = useState( + searchParams.get("uk_local_areas_beta") === "true", + ); + const displayCategory = useDisplayCategory(); + + function handleChange(value) { + let newSearch = copySearchParams(searchParams); + newSearch.set("uk_local_areas_beta", value); + setSearchParams(newSearch); + setIsChecked(value); + } + + return ( +
+ +

+ Enable UK local areas BETA +

+
+ ); +} + function FullLiteToggle() { // Selector like the dataset selector that toggles between 'full' and 'lite' versions of the dataset. // should set a query param with mode=light or mode=full @@ -1115,6 +1155,10 @@ export default function PolicyRightSidebar(props) { )} + {metadata.countryId === "uk" && + ( + + )} } /> From 6c8c4cecd553153d3f435b0f17680c7b9d30894f Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Thu, 27 Feb 2025 10:37:46 +0000 Subject: [PATCH 13/16] Fix query param usage --- src/pages/policy/output/tree.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/policy/output/tree.js b/src/pages/policy/output/tree.js index a82beaff1..df56b975f 100644 --- a/src/pages/policy/output/tree.js +++ b/src/pages/policy/output/tree.js @@ -53,6 +53,8 @@ export function getPolicyOutputTree(countryId, searchParams = {}) { } return !!searchParams[param]; }; + const uk_local_areas_beta = hasParam("uk_local_areas_beta") ? + searchParams.get("uk_local_areas_beta") : false; const tree = [ { @@ -123,7 +125,7 @@ export function getPolicyOutputTree(countryId, searchParams = {}) { name: "policyOutput.winnersAndLosers.wealthDecile", label: "By wealth decile", }, - hasParam("uk_local_areas_beta") && { + uk_local_areas_beta && { name: "policyOutput.winnersAndLosers.constituencies", label: "By parliamentary constituency", }, @@ -175,7 +177,7 @@ export function getPolicyOutputTree(countryId, searchParams = {}) { name: "policyOutput.cliffImpact", label: "Cliff impact", }, - hasParam("uk_local_areas_beta") && { + uk_local_areas_beta && { name: "policyOutput.constituencies", label: "Parliamentary constituencies (experimental)", children: [ @@ -196,6 +198,10 @@ export function getPolicyOutputTree(countryId, searchParams = {}) { ? "Labour supply impact (experimental)" : "Labor supply impact (experimental)", children: [ + uk_local_areas_beta && { + name: "policyOutput.constituencies.laborSupplyFTEs", + label: "By parliamentary constituency", + }, countryId === "us" && { name: "policyOutput.laborSupplyImpact.hours", label: "Hours worked", From c46141a345edc47cdc137f326137c6ec55f9e7e1 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Thu, 27 Feb 2025 10:39:35 +0000 Subject: [PATCH 14/16] Re-add error handling --- src/pages/policy/output/ImpactTypes.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/policy/output/ImpactTypes.jsx b/src/pages/policy/output/ImpactTypes.jsx index ae15f5318..9441ddad0 100644 --- a/src/pages/policy/output/ImpactTypes.jsx +++ b/src/pages/policy/output/ImpactTypes.jsx @@ -71,7 +71,11 @@ const map = { // get representations of the impact as a chart and a csv. The returned object // has type {chart:
, csv: }. export const getImpactReps = (impactKey, props) => { - return map[impactKey](props); + try { + return map[impactKey](props); + } catch (e) { + throw new Error(`Impact type ${impactKey} not found`); + } }; export const impactKeys = Object.keys(map); From 272dfd28821a43d506b7d304bd0360a82643f4b3 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Thu, 27 Feb 2025 10:42:21 +0000 Subject: [PATCH 15/16] Remove x-y coords from trace hovercards --- .../policy/output/constituencies/AverageChangeByConstituency.jsx | 1 + .../output/constituencies/RelativeChangeByConstituency.jsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx b/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx index d218bf9c4..145190a8b 100644 --- a/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx +++ b/src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx @@ -41,6 +41,7 @@ export function ImpactPlot(props) { coloraxis: "coloraxis", }, showscale: true, + hoverinfo: "text", }, ]} layout={{ diff --git a/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx b/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx index 435a34023..5f0b88288 100644 --- a/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx +++ b/src/pages/policy/output/constituencies/RelativeChangeByConstituency.jsx @@ -43,6 +43,7 @@ export function ImpactPlot(props) { coloraxis: "coloraxis", }, showscale: true, + hoverinfo: "text", }, ]} layout={{ From 8999e73842959418f8387954369cdb6971687438 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Thu, 27 Feb 2025 10:44:07 +0000 Subject: [PATCH 16/16] Lint --- src/pages/policy/PolicyRightSidebar.jsx | 7 +++---- src/pages/policy/output/tree.js | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/policy/PolicyRightSidebar.jsx b/src/pages/policy/PolicyRightSidebar.jsx index 639dc1216..b5a4988be 100644 --- a/src/pages/policy/PolicyRightSidebar.jsx +++ b/src/pages/policy/PolicyRightSidebar.jsx @@ -1155,10 +1155,9 @@ export default function PolicyRightSidebar(props) { )} - {metadata.countryId === "uk" && - ( - - )} + {metadata.countryId === "uk" && ( + + )} } /> diff --git a/src/pages/policy/output/tree.js b/src/pages/policy/output/tree.js index df56b975f..53196f720 100644 --- a/src/pages/policy/output/tree.js +++ b/src/pages/policy/output/tree.js @@ -53,8 +53,9 @@ export function getPolicyOutputTree(countryId, searchParams = {}) { } return !!searchParams[param]; }; - const uk_local_areas_beta = hasParam("uk_local_areas_beta") ? - searchParams.get("uk_local_areas_beta") : false; + const uk_local_areas_beta = hasParam("uk_local_areas_beta") + ? searchParams.get("uk_local_areas_beta") + : false; const tree = [ {