Skip to content

Commit

Permalink
Merge pull request #2370 from PolicyEngine/nikhilwoodruff/issue2369
Browse files Browse the repository at this point in the history
Add UK Parliamentary constituencies
  • Loading branch information
anth-volk authored Feb 27, 2025
2 parents da7da15 + 8999e73 commit 48e715d
Show file tree
Hide file tree
Showing 9 changed files with 617 additions and 7 deletions.
5 changes: 4 additions & 1 deletion src/layout/FolderPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
10 changes: 8 additions & 2 deletions src/pages/PolicyPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -159,7 +162,10 @@ export default function PolicyPage(props) {
</FolderPage>
);
} 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.", "");

Expand Down
51 changes: 50 additions & 1 deletion src/pages/policy/PolicyRightSidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -41,6 +41,11 @@ function RegionSelector(props) {
if (inputRegion === "enhanced_us") {
inputRegion = "us";
}
if (!(searchParams.get("uk_local_areas_beta") === "true")) {
options = options.filter(
(option) => !option.value.includes("constituency/"),
);
}
const [value] = useState(inputRegion || options[0].value);

return (
Expand Down Expand Up @@ -357,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 (
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
gap: "10px",
}}
>
<Switch
checked={isChecked}
size={displayCategory !== "mobile" && "small"}
onChange={handleChange}
/>
<p
style={{
margin: 0,
fontSize: displayCategory !== "mobile" && "0.95em",
}}
>
Enable UK local areas <Tag color={style.colors.TEAL_ACCENT}>BETA</Tag>
</p>
</div>
);
}

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
Expand Down Expand Up @@ -1109,6 +1155,9 @@ export default function PolicyRightSidebar(props) {
)}
<FullLiteToggle metadata={metadata} />
<BehavioralResponseToggle metadata={metadata} policy={policy} />
{metadata.countryId === "uk" && (
<LocalAreaFunctionalitySelector />
)}
</div>
}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/policy/output/Display.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down
6 changes: 6 additions & 0 deletions src/pages/policy/output/ImpactTypes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
123 changes: 123 additions & 0 deletions src/pages/policy/output/constituencies/AverageChangeByConstituency.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import Plot from "react-plotly.js";
import { ChartLogo } from "../../../../api/charts";
import { localeCode, formatCurrency } from "../../../../lang/format";
import style from "../../../../style";
import React from "react";
import ImpactChart from "../ImpactChart";
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}`);
}
const maxAbsValue = Math.max(...colorValues.map(Math.abs));
return (
<Plot
data={[
{
type: "scatter",
mode: "markers",
x: xValues,
y: yValues,
text: text,
marker: {
color: colorValues,
symbol: "hexagon",
size: 12,
coloraxis: "coloraxis",
},
showscale: true,
hoverinfo: "text",
},
]}
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 = (
<ImpactChart title={title(policyLabel, impact)}>
<ImpactPlot
data={impact?.constituency_impact?.by_constituency}
metadata={metadata}
mobile={mobile}
/>
</ImpactChart>
);
const csv = () => {
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 };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import Plot from "react-plotly.js";
import { ChartLogo } from "../../../../api/charts";
import { localeCode, formatPercent } from "../../../../lang/format";
import style from "../../../../style";
import React from "react";
import ImpactChart from "../ImpactChart";
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.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 maxAbsValue = Math.max(...colorValues.map(Math.abs));
return (
<Plot
data={[
{
type: "scatter",
mode: "markers",
x: xValues,
y: yValues,
text: text,
marker: {
color: colorValues,
symbol: "hexagon",
size: 12,
coloraxis: "coloraxis",
},
showscale: true,
hoverinfo: "text",
},
]}
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: ".1%",
},
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 = (
<ImpactChart title={title(policyLabel, impact)}>
<ImpactPlot
data={impact?.constituency_impact?.by_constituency}
metadata={metadata}
mobile={mobile}
/>
</ImpactChart>
);
const csv = () => {
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 };
}
Loading

0 comments on commit 48e715d

Please sign in to comment.