onSelect(name)}
- whileHover={{ backgroundColor: "white" }}
+ onClick={disabled ? () => undefined : () => onSelect(name)}
+ whileHover={!disabled && { backgroundColor: "white" }}
transition={{ duration: 0.001 }}
>
-
+
{selected === name && (
BETA;
const isBeta = label.includes(" (experimental)");
const adjustedLabel = isBeta ? label.slice(0, -14) : label;
@@ -84,6 +91,7 @@ function MenuItemGroup(props) {
if (child.children) {
expandedChildren.push(
undefined : toggleExpanded}
style={{
- color: style.colors.BLACK,
+ color: disabled ? style.colors.DARK_GRAY : style.colors.BLACK,
paddingTop: 5,
paddingBottom: 5,
- cursor: "pointer",
+ cursor: disabled ? "not-allowed" : "pointer",
paddingLeft: 10,
borderRadius: 8,
display: "flex",
alignItems: "center",
flexDirection: "row",
}}
- whileHover={{ backgroundColor: "white" }}
+ whileHover={!disabled && { backgroundColor: "white" }}
>
{selected === name && (
;
diff --git a/src/pages/BlogPage.jsx b/src/pages/BlogPage.jsx
index 8f5a96736..b2c067c40 100644
--- a/src/pages/BlogPage.jsx
+++ b/src/pages/BlogPage.jsx
@@ -125,7 +125,6 @@ function NotebookCell({ data }) {
outputCellComponent = null;
} else {
const outputType = Object.keys(outputCell)[0];
- console.log(outputType);
if (outputType === "text/plain") {
outputCellComponent = (
@@ -213,7 +212,6 @@ function NotebookOutputMarkdown({ data }) {
}
function NotebookOutputPlotly({ data }) {
- console.log(data);
const title = data.layout?.title?.text;
const displayCategory = useDisplayCategory();
diff --git a/src/pages/PolicyPage.jsx b/src/pages/PolicyPage.jsx
index f024e71b5..9a81d0514 100644
--- a/src/pages/PolicyPage.jsx
+++ b/src/pages/PolicyPage.jsx
@@ -20,6 +20,7 @@ import SearchParamNavButton from "../controls/SearchParamNavButton";
import style from "../style";
import DeprecationModal from "../modals/DeprecationModal";
import { impactKeys } from "../pages/policy/output/ImpactTypes.jsx";
+import { determineIfMultiYear } from "./policy/output/utils.js";
export function ParameterSearch(props) {
const { metadata, callback } = props;
@@ -110,6 +111,7 @@ export default function PolicyPage(props) {
const [searchParams, setSearchParams] = useSearchParams();
const focus = searchParams.get("focus") || "";
+ const isMultiYear = determineIfMultiYear(searchParams);
const isOutput = focus.includes("policyOutput");
// Evaluate if policy is deprecated or not
@@ -188,6 +190,7 @@ export default function PolicyPage(props) {
// Check if the current focus is within validFocusValues
if (
focus === "policyOutput.policyBreakdown" ||
+ (isMultiYear && focus === "policyOutput.budgetaryImpact") ||
focus === "policyOutput.codeReproducibility" ||
validFocusValues.includes(stripped_focus)
) {
diff --git a/src/pages/UserProfilePage.jsx b/src/pages/UserProfilePage.jsx
index 0fb3398a7..2e132c82d 100644
--- a/src/pages/UserProfilePage.jsx
+++ b/src/pages/UserProfilePage.jsx
@@ -444,8 +444,6 @@ function UserProfileSection(props) {
function PolicySimulationCard(props) {
const { metadata, userPolicy, keyValue } = props;
- console.log(userPolicy);
-
const CURRENT_API_VERSION = metadata?.version;
const geography =
metadata.economy_options.region.filter(
diff --git a/src/pages/policy/PolicyRightSidebar.jsx b/src/pages/policy/PolicyRightSidebar.jsx
index 52f0cf99b..5f539cba9 100644
--- a/src/pages/policy/PolicyRightSidebar.jsx
+++ b/src/pages/policy/PolicyRightSidebar.jsx
@@ -26,6 +26,10 @@ import { defaultForeverYear, defaultStartDate } from "../../data/constants";
import Collapsible from "../../layout/Collapsible";
import { formatFullDate } from "../../lang/format";
import useCountryId from "../../hooks/useCountryId";
+import MultiYearSelector, {
+ MULTI_YEAR_SELECTOR_PERMITTED_COUNTRIES,
+} from "./rightSidebar/MultiYearSelector";
+import { determineIfMultiYear } from "./output/utils";
function RegionSelector(props) {
const { metadata } = props;
@@ -838,6 +842,8 @@ export default function PolicyRightSidebar(props) {
const stateAbbreviation = focus.split(".")[2];
const hasHousehold = searchParams.get("household") !== null;
+ const isMultiYear = determineIfMultiYear(searchParams);
+
let dataset = searchParams.get("dataset");
const options = metadata.economy_options.region.map((stateAbbreviation) => {
@@ -909,7 +915,11 @@ export default function PolicyRightSidebar(props) {
});
} else {
let newSearch = copySearchParams(searchParams);
- newSearch.set("focus", "policyOutput.policyBreakdown");
+ if (isMultiYear) {
+ newSearch.set("focus", "policyOutput.budgetaryImpact");
+ } else {
+ newSearch.set("focus", "policyOutput.policyBreakdown");
+ }
setSearchParams(newSearch, { replace: true });
}
};
@@ -1140,6 +1150,14 @@ export default function PolicyRightSidebar(props) {
timePeriod={timePeriod}
/>
)}
+ {MULTI_YEAR_SELECTOR_PERMITTED_COUNTRIES.includes(
+ metadata.countryId,
+ ) && (
+
+ )}
{metadata.countryId === "uk" && (
diff --git a/src/pages/policy/output/Display.jsx b/src/pages/policy/output/Display.jsx
index 20c03666a..e5e278ded 100644
--- a/src/pages/policy/output/Display.jsx
+++ b/src/pages/policy/output/Display.jsx
@@ -12,13 +12,14 @@ import PolicyReproducibility from "./PolicyReproducibility";
import useMobile from "layout/Responsive";
import ErrorPage from "layout/ErrorPage";
import ResultActions from "layout/ResultActions";
-import { downloadCsv } from "./utils";
+import { determineIfMultiYear, downloadCsv } from "./utils";
import { useReactToPrint } from "react-to-print";
import PolicyBreakdown from "./PolicyBreakdown";
import { Helmet } from "react-helmet";
import useCountryId from "../../../hooks/useCountryId";
import BottomImpactDescription from "../../../layout/BottomImpactDescription";
import { Link } from "react-router-dom";
+import MultiYearBudgetaryImpact from "./budget/MultiYearBudgetaryImpact";
/**
*
@@ -152,7 +153,14 @@ function getPolicyLabel(policy) {
* performing actions such as downloading data and sharing results
*/
export function DisplayImpact(props) {
- const { impact, policy, metadata, showPolicyImpactPopup } = props;
+ const {
+ impact,
+ multiYearImpact,
+ singleYearResults,
+ policy,
+ metadata,
+ showPolicyImpactPopup,
+ } = props;
const countryId = useCountryId();
const urlParams = new URLSearchParams(window.location.search);
const focus = urlParams.get("focus");
@@ -164,6 +172,8 @@ export function DisplayImpact(props) {
const filename = impactType + `${policyLabel}`;
let pane, downloadCsvFn;
+ const isMultiYear = determineIfMultiYear(urlParams);
+
if (impactType === "analysis") {
pane = (
);
+ } else if (isMultiYear && impactType === "budgetaryImpact") {
+ pane = (
+
+ );
} else if (impactType === "policyBreakdown") {
pane = (
{
+ /**
+ * Setup multi-year request objects required by makeSequentialRequests
+ * @param {String} countryId
+ * @param {Number} reformPolicyId
+ * @param {Number} baselinePolicyId
+ * @param {String} region
+ * @param {String | Number} timePeriod
+ * @param {String} version
+ * @param {String | Number} maxHouseholds
+ * @param {String} dataset
+ * @param {String | Number} simYears
+ * @returns {Array} Array of RequestSetup objects
+ */
+ function setupMultiYearRequests(
+ countryId,
+ reformPolicyId,
+ baselinePolicyId,
+ region,
+ timePeriod,
+ version,
+ maxHouseholds,
+ dataset,
+ simYears,
+ ) {
+ const INTERVAL = 1_000; // Chosen to match single-sim run interval lengths
+ let requests = [];
+
+ const datasetInput = dataset ? `&dataset=${dataset}` : "";
+ const maxHouseholdInput = maxHouseholds
+ ? `&max_households=${maxHouseholds}`
+ : "";
+
+ for (let i = 0; i < simYears; i++) {
+ const yearOfSim = Number(timePeriod) + i;
+ const url =
+ `/${countryId}/economy/${reformPolicyId}/over/` +
+ `${baselinePolicyId}?region=${region}&time_period=${yearOfSim}` +
+ `&version=${version}${maxHouseholdInput}${datasetInput}`;
+
+ requests.push(
+ SimulationRequestSetup.cast({
+ year: yearOfSim,
+ path: url,
+ interval: INTERVAL,
+ firstInterval: INTERVAL,
+ }),
+ );
+ }
+
+ return requests;
+ }
+
+ /**
+ * Runs multi-year requests
+ * @param {String} countryId
+ * @param {Number} reformPolicyId
+ * @param {Number} baselinePolicyId
+ * @param {String} region
+ * @param {String | Number} timePeriod
+ * @param {String} version
+ * @param {String | Number} maxHouseholds
+ * @param {String} dataset
+ * @param {Number} simYears
+ * @returns {Object, SocietyWideImpact>} Object containing singleYearResults and aggregatedResult
+ */
+ async function runMultiYearRequests(
+ countryId,
+ reformPolicyId,
+ baselinePolicyId,
+ region,
+ timePeriod,
+ version,
+ maxHouseholds,
+ dataset,
+ simYears,
+ ) {
+ // Set up requests array
+ const requests = setupMultiYearRequests(
+ countryId,
+ reformPolicyId,
+ baselinePolicyId,
+ region,
+ timePeriod,
+ version,
+ maxHouseholds,
+ dataset,
+ simYears,
+ );
+
+ // Make sequential requests
+ const collection = await makeSequentialSimulationRequests(requests);
+
+ const singleYearResults = collection.results.map((item) => item);
+ const singleYearImpacts = singleYearResults.map((item) => item.result);
+
+ // Aggregate budgetary impacts and place into Impact-like object with budget key
+ const aggregatedResult = {
+ budget: aggregateMultiYearBudgets(countryId, singleYearImpacts),
+ };
+
+ return {
+ singleYearResults: singleYearResults,
+ aggregatedResult: aggregatedResult,
+ };
+ }
+
if (
areObjectsSame(policy?.reform?.data, policyRef.current?.reform?.data) &&
areObjectsSame(
@@ -81,7 +197,33 @@ export function FetchAndDisplayImpact(props) {
`${baselinePolicyId}?region=${region}&time_period=${timePeriod}` +
`&version=${selectedVersion}${maxHouseholdString}${datasetString}`;
setImpact(null);
+ setMultiYearImpact(null);
+ setSingleYearResults(null);
setError(null);
+ // If user requests valid multi-year value, make sequential requests
+ if (isMultiYear) {
+ runMultiYearRequests(
+ metadata.countryId,
+ reformPolicyId,
+ baselinePolicyId,
+ region,
+ timePeriod,
+ selectedVersion,
+ maxHouseholds,
+ dataset,
+ simYears,
+ )
+ .then((aggregatedData) => {
+ setMultiYearImpact(aggregatedData.aggregatedResult);
+ setSingleYearResults(aggregatedData.singleYearResults);
+ })
+ .catch((err) => {
+ setError(err);
+ });
+
+ return;
+ }
+
// start counting (but stop when the API call finishes)
const interval = setInterval(() => {
setSecondsElapsed((secondsElapsed) => secondsElapsed + 1);
@@ -163,6 +305,7 @@ export function FetchAndDisplayImpact(props) {
reformPolicyId,
baselinePolicyId,
maxHouseholds,
+ simYears,
]);
useEffect(() => {
@@ -184,7 +327,10 @@ export function FetchAndDisplayImpact(props) {
return ;
}
- if (!impact) {
+ if (
+ (!isMultiYear && !impact) ||
+ (isMultiYear && !multiYearImpact && !singleYearResults)
+ ) {
return (
{
+ return item.simulationRequestSetup.year;
+ });
+
+ const columns = [
+ {
+ title: `Net revenue impact (billions)`,
+ dataIndex: "netRevenueImpact",
+ key: "netRevenueImpact",
+ },
+ ...years.map((year) => mapFinancialYearToColumn(year, metadata.countryId)),
+ {
+ title: getYearRangeFromArray(years, metadata.countryId),
+ dataIndex: "yearRange",
+ key: "yearRange",
+ },
+ ];
+
+ const countryDataSource = dataSourcesByCountry[region];
+
+ const dataSources = countryDataSource.map((item) => {
+ return {
+ netRevenueImpact: item.header,
+ ...getYearlyImpacts(
+ singleYearResults,
+ metadata.countryId,
+ item.budgetKey ? item.budgetKey : null,
+ item.formula ? item.formula : null,
+ ),
+ yearRange: roundToBillions(
+ item.budgetKey
+ ? impact.budget[item.budgetKey]
+ : item.formula(impact.budget),
+ roundingPrecisionByCountry[metadata.countryId] ||
+ roundingPrecisionByCountry.default,
+ ),
+ };
+ });
+
+ const displayPeriod = getYearRangeFromArray(years, metadata.countryId);
+
+ const regionObj = metadata.economy_options.region.find(
+ (elem) => elem.name === region,
+ );
+
+ let regionLabel = "undefined region";
+ if (regionObj) {
+ regionLabel = regionObj.label;
+ }
+
+ const recordStylingOverrides = (record, index) => {
+ let styles = {};
+
+ if (
+ record.netRevenueImpact === "Federal budget" ||
+ record.netRevenueImpact === "Total"
+ ) {
+ styles.fontWeight = "bold";
+ }
+
+ if (record.netRevenueImpact === "State tax") {
+ styles.fontStyle = "italic";
+ }
+
+ return {
+ style: styles,
+ };
+ };
+
+ const title = `${policyLabel} in ${regionLabel}, ${displayPeriod}`;
+
+ return (
+
+ );
+}
+
+/**
+ * Create yearly impacts structured data for chart component
+ * @param {SequentialSimulationResult} singleYearResults
+ * @param {String} countryId
+ * @param {String | null} budgetKey The specific budgetary impact key for a table row to use, unless formula is provided
+ * @param {Callable | null} formula If necessary, a more complex formula drawn from the budgetary impact object to use instead of the budgetKey
+ * @returns {Object} The yearly impacts object expected by Ant Design Table component
+ */
+export function getYearlyImpacts(
+ singleYearResults,
+ countryId,
+ budgetKey = null,
+ formula = null,
+) {
+ const yearlyImpacts = {};
+ singleYearResults.forEach((item) => {
+ const year = item.simulationRequestSetup.year;
+ let impact = null;
+ if (formula) {
+ impact = formula(item.result.budget);
+ } else {
+ impact = item.result.budget[budgetKey];
+ }
+
+ yearlyImpacts[year] = roundToBillions(
+ impact,
+ roundingPrecisionByCountry[countryId] ||
+ roundingPrecisionByCountry.default,
+ );
+ });
+ return yearlyImpacts;
+}
+
+export function getYearRangeFromArray(years, country) {
+ const financialYearType =
+ financialYearTypeByCountry[country] || financialYearTypeByCountry.default;
+
+ const startYear = years[0];
+ let endYear = years[years.length - 1];
+
+ // If the financial year type is mixed, we need to add one to the end year
+ if (financialYearType === "mixed") {
+ endYear++;
+ }
+
+ const endYearLastTwoDigits = endYear.toString().slice(-2);
+
+ return `${startYear}-${endYearLastTwoDigits}`;
+}
+
+export function roundToBillions(number, decimals = 0) {
+ return (number / 1e9).toFixed(decimals);
+}
+
+export function getFederalBudgetImpact(budget) {
+ return budget.tax_revenue_impact - budget.state_tax_revenue_impact;
+}
diff --git a/src/pages/policy/output/tree.js b/src/pages/policy/output/tree.js
index 71803106f..1bce140d3 100644
--- a/src/pages/policy/output/tree.js
+++ b/src/pages/policy/output/tree.js
@@ -1,6 +1,9 @@
+import { determineIfMultiYear } from "./utils";
+
export const policyOutputs = {
policyBreakdown: "Overview",
netIncome: "Budgetary impact",
+ budgetaryImpact: "Budgetary impact",
detailedBudgetaryImpact: "Budgetary impact by program",
decileAverageImpact: "Absolute impact by income decile",
wealthDecileAverageImpact: "Absolute impact by wealth decile",
@@ -51,6 +54,8 @@ export function getPolicyOutputTree(countryId, searchParams = {}) {
? searchParams.get("uk_local_areas_beta") === "true"
: false;
+ const isMultiYear = determineIfMultiYear(searchParams);
+
const tree = [
{
name: "policyOutput",
@@ -59,50 +64,63 @@ export function getPolicyOutputTree(countryId, searchParams = {}) {
{
name: "policyOutput.policyBreakdown",
label: "Overview",
+ disabled: isMultiYear,
},
- {
- name: "policyOutput.budgetaryImpact",
- label: "Budgetary impact",
- children: [
- {
- name: "policyOutput.budgetaryImpact.overall",
- label: "Overall",
- },
- countryId === "uk" && {
- name: "policyOutput.budgetaryImpact.byProgram",
- label: "By program",
+ isMultiYear
+ ? {
+ name: "policyOutput.budgetaryImpact",
+ label: "Budgetary impact",
+ }
+ : {
+ name: "policyOutput.budgetaryImpact",
+ label: "Budgetary impact",
+ children: [
+ {
+ name: "policyOutput.budgetaryImpact.overall",
+ label: "Overall",
+ },
+ countryId === "uk" && {
+ name: "policyOutput.budgetaryImpact.byProgram",
+ label: "By program",
+ },
+ ].filter((x) => x),
},
- ].filter((x) => x),
- },
{
name: "policyOutput.distributionalImpact",
label: "Distributional impact",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.distributionalImpact.incomeDecile",
label: "By income decile",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.distributionalImpact.incomeDecile.relative",
label: "Relative",
+ disabled: isMultiYear,
},
{
name: "policyOutput.distributionalImpact.incomeDecile.average",
label: "Average",
+ disabled: isMultiYear,
},
],
},
countryId === "uk" && {
name: "policyOutput.distributionalImpact.wealthDecile",
label: "By wealth decile",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.distributionalImpact.wealthDecile.relative",
label: "Relative",
+ disabled: isMultiYear,
},
{
name: "policyOutput.distributionalImpact.wealthDecile.average",
label: "Average",
+ disabled: isMultiYear,
},
],
},
@@ -111,14 +129,17 @@ export function getPolicyOutputTree(countryId, searchParams = {}) {
{
name: "policyOutput.winnersAndLosers",
label: "Winners and losers",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.winnersAndLosers.incomeDecile",
label: "By income decile",
+ disabled: isMultiYear,
},
countryId === "uk" && {
name: "policyOutput.winnersAndLosers.wealthDecile",
label: "By wealth decile",
+ disabled: isMultiYear,
},
uk_local_areas_beta && {
name: "policyOutput.winnersAndLosers.constituencies",
@@ -129,36 +150,44 @@ export function getPolicyOutputTree(countryId, searchParams = {}) {
{
name: "policyOutput.povertyImpact",
label: "Poverty impact",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.povertyImpact.regular",
label: "Regular poverty",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.povertyImpact.regular.byAge",
label: "By age",
+ disabled: isMultiYear,
},
countryId === "us" && {
name: "policyOutput.povertyImpact.regular.byRace",
label: "By race",
+ disabled: isMultiYear,
},
{
name: "policyOutput.povertyImpact.regular.byGender",
label: "By gender",
+ disabled: isMultiYear,
},
].filter((x) => x),
},
{
name: "policyOutput.povertyImpact.deep",
label: "Deep poverty",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.povertyImpact.deep.byAge",
label: "By age",
+ disabled: isMultiYear,
},
{
name: "policyOutput.povertyImpact.deep.byGender",
label: "By gender",
+ disabled: isMultiYear,
},
],
},
@@ -167,22 +196,27 @@ export function getPolicyOutputTree(countryId, searchParams = {}) {
{
name: "policyOutput.inequalityImpact",
label: "Inequality impact",
+ disabled: isMultiYear,
},
{
name: "policyOutput.cliffImpact",
label: "Cliff impact",
+ disabled: isMultiYear,
},
uk_local_areas_beta && {
name: "policyOutput.constituencies",
label: "Parliamentary constituencies (experimental)",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.constituencies.relative",
label: "Relative change",
+ disabled: isMultiYear,
},
{
name: "policyOutput.constituencies.average",
label: "Average change",
+ disabled: isMultiYear,
},
],
},
@@ -192,70 +226,86 @@ export function getPolicyOutputTree(countryId, searchParams = {}) {
countryId === "uk"
? "Labour supply impact (experimental)"
: "Labor supply impact (experimental)",
+ disabled: isMultiYear,
children: [
uk_local_areas_beta && {
name: "policyOutput.constituencies.laborSupplyFTEs",
label: "By parliamentary constituency",
+ disabled: isMultiYear,
},
countryId === "us" && {
name: "policyOutput.laborSupplyImpact.hours",
label: "Hours worked",
+ disabled: isMultiYear,
},
{
name: "policyOutput.laborSupplyImpact.earnings",
label: "Earnings",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.laborSupplyImpact.earnings.overall",
label: "Overall",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.laborSupplyImpact.earnings.overall.relative",
label: "Relative",
+ disabled: isMultiYear,
},
{
name: "policyOutput.laborSupplyImpact.earnings.overall.absolute",
label: "Absolute",
+ disabled: isMultiYear,
},
],
},
{
name: "policyOutput.laborSupplyImpact.earnings.byDecile",
label: "By decile",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.laborSupplyImpact.earnings.byDecile.relative",
label: "Relative",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.laborSupplyImpact.earnings.byDecile.relative.total",
label: "Total",
+ disabled: isMultiYear,
},
{
name: "policyOutput.laborSupplyImpact.earnings.byDecile.relative.income",
label: "Income effect",
+ disabled: isMultiYear,
},
{
name: "policyOutput.laborSupplyImpact.earnings.byDecile.relative.substitution",
label: "Substitution effect",
+ disabled: isMultiYear,
},
],
},
{
name: "policyOutput.laborSupplyImpact.earnings.byDecile.absolute",
label: "Absolute",
+ disabled: isMultiYear,
children: [
{
name: "policyOutput.laborSupplyImpact.earnings.byDecile.absolute.total",
label: "Total",
+ disabled: isMultiYear,
},
{
name: "policyOutput.laborSupplyImpact.earnings.byDecile.absolute.income",
label: "Income effect",
+ disabled: isMultiYear,
},
{
name: "policyOutput.laborSupplyImpact.earnings.byDecile.absolute.substitution",
label: "Substitution effect",
+ disabled: isMultiYear,
},
],
},
@@ -268,10 +318,12 @@ export function getPolicyOutputTree(countryId, searchParams = {}) {
{
name: "policyOutput.analysis",
label: "AI summary (experimental)",
+ disabled: isMultiYear,
},
{
name: "policyOutput.codeReproducibility",
label: "Reproduce in Python",
+ disabled: isMultiYear,
},
].filter((x) => x),
},
diff --git a/src/pages/policy/output/utils.js b/src/pages/policy/output/utils.js
index a093bc898..66afd1230 100644
--- a/src/pages/policy/output/utils.js
+++ b/src/pages/policy/output/utils.js
@@ -57,10 +57,24 @@ function copyLink() {
message.info("Link copied to clipboard");
}
+/**
+ * Determines if a given policy output is multi-year based on search params
+ * @param {Object} searchParams - The search params object
+ * @returns {boolean} - True if the policy output is multi-year, false otherwise
+ */
+function determineIfMultiYear(searchParams) {
+ return (
+ searchParams.get("simYears") &&
+ !Number.isNaN(searchParams.get("simYears")) &&
+ searchParams.get("simYears") > 1
+ );
+}
+
export {
avgChangeDirection,
plotLayoutFont,
downloadPng,
downloadCsv,
copyLink,
+ determineIfMultiYear,
};
diff --git a/src/pages/policy/rightSidebar/MultiYearSelector.jsx b/src/pages/policy/rightSidebar/MultiYearSelector.jsx
new file mode 100644
index 000000000..4fca0f5dc
--- /dev/null
+++ b/src/pages/policy/rightSidebar/MultiYearSelector.jsx
@@ -0,0 +1,162 @@
+import { useSearchParams } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { Select, Switch } from "antd";
+import { useDisplayCategory } from "../../../layout/Responsive";
+
+const DEFAULT_SIM_LENGTH = {
+ us: 10,
+ uk: 5,
+ default: 5,
+};
+
+export const MULTI_YEAR_SELECTOR_PERMITTED_COUNTRIES = ["us", "uk"];
+
+export default function MultiYearSelector(props) {
+ const { metadata, startYear } = props;
+
+ const defaultSimLength = calculateDefaultSimLength(metadata, startYear);
+
+ const [searchParams, setSearchParams] = useSearchParams();
+ const inboundSimYears = searchParams.get("simYears");
+ const isInboundSimYearsValid = validateSimYears(
+ inboundSimYears,
+ metadata,
+ startYear,
+ );
+ const [simLength, setSimLength] = useState(
+ isInboundSimYearsValid ? inboundSimYears : defaultSimLength,
+ );
+ const [isMultiYearActive, setIsMultiYearActive] = useState(
+ !!searchParams.get("simYears"),
+ );
+ const dC = useDisplayCategory();
+
+ const simYearsMenuItems = generateSimYearsMenuItems(metadata, startYear);
+
+ useEffect(() => {
+ setSimLength(isInboundSimYearsValid ? inboundSimYears : defaultSimLength);
+ setIsMultiYearActive(!!searchParams.get("simYears"));
+ }, [
+ startYear,
+ searchParams,
+ isInboundSimYearsValid,
+ inboundSimYears,
+ defaultSimLength,
+ ]);
+
+ function handleSwitchChange(checked) {
+ const isOnOutput = searchParams.get("focus").includes("policyOutput");
+
+ setIsMultiYearActive(checked);
+
+ if (checked) {
+ setSearchParams((prev) => {
+ const newSearch = new URLSearchParams(prev);
+ newSearch.set("simYears", simLength);
+ if (isOnOutput) {
+ newSearch.set("focus", "policyOutput.budgetaryImpact");
+ }
+ return newSearch;
+ });
+ return;
+ }
+
+ setSearchParams((prev) => {
+ const newSearch = new URLSearchParams(prev);
+ newSearch.delete("simYears");
+ if (isOnOutput) {
+ newSearch.set("focus", "policyOutput.policyBreakdown");
+ }
+ return newSearch;
+ });
+ return;
+ }
+
+ function handleSimYearsChange(value) {
+ setSimLength(value);
+ if (isMultiYearActive) {
+ setSearchParams((prev) => {
+ const newSearch = new URLSearchParams(prev);
+ newSearch.set("simYears", value);
+ return newSearch;
+ });
+ }
+ }
+
+ return (
+
+
+
+ Extend impacts over
+
+
+
+ years
+
+
+ );
+}
+
+export function generateSimYearsMenuItems(metadata, startYear) {
+ const lastSimYear = findLastSimYearFromMetadata(metadata);
+ const maxSimLength = lastSimYear - startYear + 1;
+
+ const simLengths = [];
+ for (let simLength = 1; simLength <= maxSimLength; simLength++) {
+ simLengths.push({ label: simLength, value: simLength });
+ }
+
+ return simLengths;
+}
+
+export function findLastSimYearFromMetadata(metadata) {
+ const allYearsSorted = metadata.economy_options.time_period.sort(
+ (a, b) => a.name - b.name,
+ );
+ const lastSimYear = allYearsSorted[allYearsSorted.length - 1].name;
+ return lastSimYear;
+}
+
+export function calculateDefaultSimLength(metadata, startYear) {
+ const lastSimYear = findLastSimYearFromMetadata(metadata);
+ const maxSimLength = lastSimYear - startYear + 1;
+
+ const defaultSimLength =
+ DEFAULT_SIM_LENGTH[metadata.countryId] || DEFAULT_SIM_LENGTH["default"];
+ return Math.min(maxSimLength, defaultSimLength);
+}
+
+export function validateSimYears(simYearParam, metadata, startYear) {
+ if (simYearParam === null || simYearParam === undefined) {
+ return false;
+ }
+ const maxSimLength = findLastSimYearFromMetadata(metadata) - startYear + 1;
+ return simYearParam >= 1 && simYearParam <= maxSimLength;
+}
diff --git a/src/schemas/aggregatedSocietyWideImpact.js b/src/schemas/aggregatedSocietyWideImpact.js
new file mode 100644
index 000000000..b6640526f
--- /dev/null
+++ b/src/schemas/aggregatedSocietyWideImpact.js
@@ -0,0 +1,12 @@
+import * as yup from "yup";
+import {
+ BudgetaryImpactModule,
+ IntraDecileModule,
+ PovertyByAgeModule,
+} from "./societyWideModules";
+
+export const AggregatedSocietyWideImpact = yup.object({
+ budget: BudgetaryImpactModule.required(),
+ poverty: PovertyByAgeModule.required(),
+ intra_decile: IntraDecileModule.required(),
+});