Skip to content

Commit 090dbaf

Browse files
authored
Merge pull request #28 from PolicyEngine/remove-scp-inflation
Remove SCP inflation policy from dashboard
2 parents 78f1522 + 90ac85e commit 090dbaf

File tree

13 files changed

+122
-477
lines changed

13 files changed

+122
-477
lines changed

src/App.jsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ const POLICIES = [
1010
{ id: "income_tax_basic_uplift", name: "Basic rate threshold uplift", category: "cost" },
1111
{ id: "income_tax_intermediate_uplift", name: "Intermediate rate threshold uplift", category: "cost" },
1212
// SCP policies (costs to government)
13-
{ id: "scp_inflation", name: "SCP inflation adjustment", category: "cost" },
1413
{ id: "scp_baby_boost", name: "SCP Premium for under-ones", category: "cost" },
1514
// Threshold freezes (revenue raisers)
1615
{ id: "higher_rate_freeze", name: "Higher rate threshold freeze", category: "revenue" },
@@ -23,7 +22,6 @@ function App() {
2322
const [selectedPolicies, setSelectedPolicies] = useState([
2423
"income_tax_basic_uplift",
2524
"income_tax_intermediate_uplift",
26-
"scp_inflation",
2725
"scp_baby_boost",
2826
"higher_rate_freeze",
2927
"advanced_rate_freeze",

src/components/BudgetBarChart.jsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,37 @@ import {
1414
import "./BudgetBarChart.css";
1515
import { POLICY_COLORS, ALL_POLICY_NAMES, POLICY_NAMES } from "../utils/policyConfig";
1616

17+
// Custom tooltip that orders items correctly with Net impact last
18+
const CustomTooltip = ({ active, payload, label, formatValue, formatYear, activePolicies }) => {
19+
if (!active || !payload || payload.length === 0) return null;
20+
21+
// Sort payload: policies in activePolicies order, Net impact last
22+
const sortedPayload = [...payload].sort((a, b) => {
23+
if (a.name === "Net impact") return 1;
24+
if (b.name === "Net impact") return -1;
25+
const aIndex = activePolicies.indexOf(a.name);
26+
const bIndex = activePolicies.indexOf(b.name);
27+
return aIndex - bIndex;
28+
});
29+
30+
return (
31+
<div style={{
32+
background: "white",
33+
border: "1px solid #e5e7eb",
34+
borderRadius: "6px",
35+
padding: "10px 14px",
36+
boxShadow: "0 2px 8px rgba(0,0,0,0.1)"
37+
}}>
38+
<p style={{ margin: "0 0 8px 0", fontWeight: 600 }}>{formatYear(label)}</p>
39+
{sortedPayload.map((entry, index) => (
40+
<p key={index} style={{ margin: "4px 0", color: entry.color }}>
41+
{entry.name} : {formatValue(entry.value)}
42+
</p>
43+
))}
44+
</div>
45+
);
46+
};
47+
1748
// Custom label component for net impact values
1849
const NetImpactLabel = (props) => {
1950
const { x, y, value } = props;
@@ -148,11 +179,11 @@ export default function BudgetBarChart({ data, title, description, stacked = fal
148179
tick={{ fontSize: 12 }}
149180
/>
150181
<Tooltip
151-
formatter={(value, name) => [
152-
formatValue(value),
153-
name === "Net impact" ? "Net impact" : name
154-
]}
155-
labelFormatter={formatYear}
182+
content={<CustomTooltip
183+
formatValue={formatValue}
184+
formatYear={formatYear}
185+
activePolicies={activePolicies}
186+
/>}
156187
/>
157188
{stacked && (
158189
<Legend

src/components/Dashboard.jsx

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -141,38 +141,6 @@ const summaryStyle = { cursor: "pointer", color: "#2c6e49", fontWeight: "500" };
141141

142142
// Policy descriptions (active voice, clear impacts)
143143
const POLICY_INFO = {
144-
scp_inflation: {
145-
name: "SCP inflation adjustment",
146-
description: "Scottish Child Payment uprated from £27.15 to £28.20/week",
147-
explanation: (
148-
<li>
149-
<strong>SCP inflation adjustment</strong>: The Budget uprates the Scottish Child Payment
150-
from £27.15 to £28.20 per week (+3.9% for inflation). This benefits all families receiving
151-
SCP, providing approximately £55 extra per child per year.
152-
<details className="policy-table-details" style={{ marginTop: "12px" }}>
153-
<summary style={summaryStyle}>View SCP rates by year</summary>
154-
<table style={tableStyle}>
155-
<thead>
156-
<tr>
157-
<th style={thStyle}>Year</th>
158-
<th style={thRightStyle}>Weekly rate</th>
159-
<th style={thRightStyle}>Annual (per child)</th>
160-
</tr>
161-
</thead>
162-
<tbody>
163-
<tr><td style={tdStyle}>2025-26</td><td style={tdRightStyle}>£27.15</td><td style={tdRightStyle}>£1,412</td></tr>
164-
<tr><td style={tdStyle}>2026-27</td><td style={tdRightStyle}>£28.20</td><td style={tdRightStyle}>£1,466</td></tr>
165-
<tr><td style={tdStyle}>2027-28</td><td style={tdRightStyle}>£28.85</td><td style={tdRightStyle}>£1,500</td></tr>
166-
<tr><td style={tdStyle}>2028-29</td><td style={tdRightStyle}>£29.45</td><td style={tdRightStyle}>£1,531</td></tr>
167-
<tr><td style={tdStyle}>2029-30</td><td style={tdRightStyle}>£30.05</td><td style={tdRightStyle}>£1,563</td></tr>
168-
<tr><td style={{...tdStyle, borderBottom: "none"}}>2030-31</td><td style={{...tdRightStyle, borderBottom: "none"}}>£30.65</td><td style={{...tdRightStyle, borderBottom: "none"}}>£1,594</td></tr>
169-
</tbody>
170-
</table>
171-
<p style={noteStyle}>Note: Uprated annually by CPI (September of prior year). Source: <a href="https://www.gov.scot/publications/scottish-budget-2026-2027/" target="_blank" rel="noopener noreferrer">Scottish Budget 2026-27</a> | <a href="https://fiscalcommission.scot/publications/scotlands-economic-and-fiscal-forecasts-january-2026/" target="_blank" rel="noopener noreferrer">SFC January 2026</a></p>
172-
</details>
173-
</li>
174-
),
175-
},
176144
scp_baby_boost: {
177145
name: "SCP Premium for under-ones",
178146
description: "Scottish Child Payment raised to £40/week for babies under 1",
@@ -622,7 +590,6 @@ export default function Dashboard({ selectedPolicies = [] }) {
622590
<>
623591
{POLICY_INFO.income_tax_basic_uplift.explanation}
624592
{POLICY_INFO.income_tax_intermediate_uplift.explanation}
625-
{POLICY_INFO.scp_inflation.explanation}
626593
{POLICY_INFO.scp_baby_boost.explanation}
627594
{POLICY_INFO.higher_rate_freeze.explanation}
628595
{POLICY_INFO.advanced_rate_freeze.explanation}

src/components/DecileChart.jsx

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,37 @@ import {
1414
import "./DecileChart.css";
1515
import { POLICY_COLORS, ALL_POLICY_NAMES, REVENUE_POLICIES, POLICY_NAMES } from "../utils/policyConfig";
1616

17+
// Custom tooltip that orders items correctly with Net change last
18+
const CustomTooltip = ({ active, payload, label, formatValue, activePolicies }) => {
19+
if (!active || !payload || payload.length === 0) return null;
20+
21+
// Sort payload: policies in activePolicies order, Net change last
22+
const sortedPayload = [...payload].sort((a, b) => {
23+
if (a.name === "Net change") return 1;
24+
if (b.name === "Net change") return -1;
25+
const aIndex = activePolicies.indexOf(a.name);
26+
const bIndex = activePolicies.indexOf(b.name);
27+
return aIndex - bIndex;
28+
});
29+
30+
return (
31+
<div style={{
32+
background: "white",
33+
border: "1px solid #e5e7eb",
34+
borderRadius: "6px",
35+
padding: "10px 14px",
36+
boxShadow: "0 2px 8px rgba(0,0,0,0.1)"
37+
}}>
38+
<p style={{ margin: "0 0 8px 0", fontWeight: 600 }}>{label} decile</p>
39+
{sortedPayload.map((entry, index) => (
40+
<p key={index} style={{ margin: "4px 0", color: entry.color }}>
41+
{entry.name} : {formatValue(entry.value)}
42+
</p>
43+
))}
44+
</div>
45+
);
46+
};
47+
1748
// Custom label component for net change values
1849
const NetChangeLabel = ({ x, y, value, viewMode }) => {
1950
if (value === undefined || value === null) return null;
@@ -302,17 +333,10 @@ export default function DecileChart({
302333
/>
303334
)}
304335
<Tooltip
305-
formatter={(value, name) => [
306-
formatValue(value),
307-
name === "Net change" ? "Net change" : name
308-
]}
309-
labelFormatter={(label) => `${label} decile`}
310-
contentStyle={{
311-
background: "white",
312-
border: "1px solid #e5e7eb",
313-
borderRadius: "6px",
314-
padding: "8px 12px",
315-
}}
336+
content={<CustomTooltip
337+
formatValue={formatValue}
338+
activePolicies={activePolicies}
339+
/>}
316340
/>
317341
{stacked ? (
318342
activePolicies.map((policyName) => (

src/components/HouseholdCalculator.jsx

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ const CHART_COLORS = {
2828
total: "#0F766E", // Teal 700
2929
income_tax_basic_uplift: "#0D9488", // Teal 600
3030
income_tax_intermediate_uplift: "#0F766E", // Teal 700
31-
scp_inflation: "#14B8A6", // Teal 500
3231
scp_baby_boost: "#2DD4BF", // Teal 400
3332
higher_rate_freeze: "#78350F", // Amber 900 (darkest)
3433
advanced_rate_freeze: "#92400E", // Amber 800
@@ -53,15 +52,14 @@ function HouseholdCalculator() {
5352
const [childAgeInput, setChildAgeInput] = useState("");
5453
const [loading, setLoading] = useState(false);
5554
const [error, setError] = useState(null);
56-
const [selectedYear, setSelectedYear] = useState(2026);
55+
const [selectedYear, setSelectedYear] = useState(2027);
5756
const [showRealTerms, setShowRealTerms] = useState(false);
5857
const [impacts, setImpacts] = useState({
5958
income_tax_basic_uplift: 0,
6059
income_tax_intermediate_uplift: 0,
6160
higher_rate_freeze: 0,
6261
advanced_rate_freeze: 0,
6362
top_rate_freeze: 0,
64-
scp_inflation: 0,
6563
scp_baby_boost: 0,
6664
total: 0,
6765
});
@@ -152,7 +150,6 @@ function HouseholdCalculator() {
152150
higher_rate_freeze: result.impacts.higher_rate_freeze,
153151
advanced_rate_freeze: result.impacts.advanced_rate_freeze,
154152
top_rate_freeze: result.impacts.top_rate_freeze,
155-
scp_inflation: result.impacts.scp_inflation,
156153
scp_baby_boost: result.impacts.scp_baby_boost,
157154
total: result.total,
158155
});
@@ -234,15 +231,14 @@ function HouseholdCalculator() {
234231
(d.income_tax_basic_uplift || 0) + (d.income_tax_intermediate_uplift || 0),
235232
d.year
236233
),
237-
scp: toRealTerms((d.scp_inflation || 0) + (d.scp_baby_boost || 0), d.year),
234+
scp: toRealTerms(d.scp_baby_boost || 0, d.year),
238235
total: toRealTerms(d.total, d.year),
239236
// Keep individual reform values for tooltip
240237
income_tax_basic_uplift: toRealTerms(d.income_tax_basic_uplift || 0, d.year),
241238
income_tax_intermediate_uplift: toRealTerms(d.income_tax_intermediate_uplift || 0, d.year),
242239
higher_rate_freeze: toRealTerms(d.higher_rate_freeze || 0, d.year),
243240
advanced_rate_freeze: toRealTerms(d.advanced_rate_freeze || 0, d.year),
244241
top_rate_freeze: toRealTerms(d.top_rate_freeze || 0, d.year),
245-
scp_inflation: toRealTerms(d.scp_inflation || 0, d.year),
246242
scp_baby_boost: toRealTerms(d.scp_baby_boost || 0, d.year),
247243
}));
248244

@@ -254,7 +250,7 @@ function HouseholdCalculator() {
254250
.padding(0.3);
255251

256252
// Dynamic Y scale based on stacked values (sum positives and negatives separately)
257-
const policyKeysForScale = ['income_tax_basic_uplift', 'income_tax_intermediate_uplift', 'higher_rate_freeze', 'advanced_rate_freeze', 'top_rate_freeze', 'scp_inflation', 'scp_baby_boost'];
253+
const policyKeysForScale = ['income_tax_basic_uplift', 'income_tax_intermediate_uplift', 'higher_rate_freeze', 'advanced_rate_freeze', 'top_rate_freeze', 'scp_baby_boost'];
258254
let dataMax = 0;
259255
let dataMin = 0;
260256
processedData.forEach((d) => {
@@ -335,7 +331,6 @@ function HouseholdCalculator() {
335331
{ key: 'higher_rate_freeze', color: CHART_COLORS.higher_rate_freeze },
336332
{ key: 'advanced_rate_freeze', color: CHART_COLORS.advanced_rate_freeze },
337333
{ key: 'top_rate_freeze', color: CHART_COLORS.top_rate_freeze },
338-
{ key: 'scp_inflation', color: CHART_COLORS.scp_inflation },
339334
{ key: 'scp_baby_boost', color: CHART_COLORS.scp_baby_boost },
340335
];
341336

@@ -478,10 +473,6 @@ function HouseholdCalculator() {
478473
<span style="color:#B45309">Top rate freeze</span>
479474
<span style="font-weight:500">${formatVal(d.top_rate_freeze)}</span>
480475
</div>
481-
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
482-
<span style="color:#14B8A6">SCP inflation</span>
483-
<span style="font-weight:500">${formatVal(d.scp_inflation)}</span>
484-
</div>
485476
<div style="display:flex;justify-content:space-between;margin-bottom:6px">
486477
<span style="color:#2DD4BF">SCP baby boost</span>
487478
<span style="font-weight:500">${formatVal(d.scp_baby_boost)}</span>
@@ -605,13 +596,13 @@ function HouseholdCalculator() {
605596
.attr("fill", "#6B7280")
606597
.attr("font-size", "11px");
607598

608-
// Area fill
599+
// Area fill - use linear curve to show sharp cliffs (e.g., SCP eligibility)
609600
const area = d3
610601
.area()
611602
.x((d) => x(d.income))
612603
.y0(y(0))
613604
.y1((d) => y(d.total))
614-
.curve(d3.curveMonotoneX);
605+
.curve(d3.curveLinear);
615606

616607
// Split data into positive and negative areas
617608
const positiveData = byIncomeData.map((d) => ({
@@ -635,12 +626,12 @@ function HouseholdCalculator() {
635626
.attr("fill", "rgba(180, 83, 9, 0.2)")
636627
.attr("d", area);
637628

638-
// Line
629+
// Line - use linear curve to show sharp cliffs (e.g., SCP eligibility)
639630
const line = d3
640631
.line()
641632
.x((d) => x(d.income))
642633
.y((d) => y(d.total))
643-
.curve(d3.curveMonotoneX);
634+
.curve(d3.curveLinear);
644635

645636
g.append("path")
646637
.datum(byIncomeData)
@@ -727,10 +718,6 @@ function HouseholdCalculator() {
727718
<span style="color:#B45309">Top rate freeze</span>
728719
<span style="font-weight:500">${formatVal(closest.top_rate_freeze)}</span>
729720
</div>
730-
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
731-
<span style="color:#14B8A6">SCP inflation</span>
732-
<span style="font-weight:500">${formatVal(closest.scp_inflation)}</span>
733-
</div>
734721
<div style="display:flex;justify-content:space-between;margin-bottom:6px">
735722
<span style="color:#2DD4BF">SCP baby boost</span>
736723
<span style="font-weight:500">${formatVal(closest.scp_baby_boost)}</span>

src/components/LocalAreaSection.jsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ function parseCSVLine(line) {
4242

4343
const POLICY_DISPLAY_NAMES = {
4444
scp_baby_boost: "SCP Premium for under-ones",
45-
scp_inflation: "SCP inflation adjustment",
4645
income_tax_basic_uplift: "basic rate threshold uplift",
4746
income_tax_intermediate_uplift: "intermediate rate threshold uplift",
4847
};
@@ -153,6 +152,7 @@ export default function LocalAreaSection({
153152
}, [localAuthorityData]);
154153

155154
// Calculate fixed color extent across ALL years for consistent map coloring
155+
// Use symmetric round numbers for cleaner legend
156156
const fixedColorExtent = useMemo(() => {
157157
if (!rawLocalAuthorityData.length || !selectedPolicies.length) return null;
158158

@@ -184,16 +184,20 @@ export default function LocalAreaSection({
184184

185185
if (globalMin === Infinity || globalMax === -Infinity) return null;
186186

187-
// Determine type
188-
let type = 'mixed';
189-
if (globalMin >= 0) type = 'positive';
190-
else if (globalMax <= 0) type = 'negative';
187+
// Round to symmetric nice numbers - dynamically based on data range
188+
const maxAbs = Math.max(Math.abs(globalMin), Math.abs(globalMax));
191189

192-
return {
193-
min: Math.floor(globalMin),
194-
max: Math.ceil(globalMax),
195-
type
196-
};
190+
// Choose interval based on magnitude: 10, 25, 50, or 100
191+
let interval;
192+
if (maxAbs <= 30) interval = 10;
193+
else if (maxAbs <= 75) interval = 25;
194+
else if (maxAbs <= 150) interval = 50;
195+
else interval = 100;
196+
197+
const roundedMax = Math.ceil(maxAbs / interval) * interval;
198+
199+
// Always use symmetric range with both colors (mixed type)
200+
return { min: -roundedMax, max: roundedMax, type: 'mixed' };
197201
}, [rawLocalAuthorityData, selectedPolicies, availableYears]);
198202

199203
// Handle local authority selection from map
@@ -333,7 +337,7 @@ export default function LocalAreaSection({
333337
<li key={la.code} className="local-authority-list-item">
334338
<span className="local-authority-list-name">{la.name}</span>
335339
<span className={`local-authority-list-value ${la.avgGain >= 0 ? "positive" : "negative"}`}>
336-
£{la.avgGain.toFixed(2)}
340+
{la.avgGain >= 0 ? ${la.avgGain.toFixed(2)}` : `-£${Math.abs(la.avgGain).toFixed(2)}`}
337341
</span>
338342
</li>
339343
))}

src/components/SFCComparisonTable.jsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import "./SFCComparisonTable.css";
33

44
function SFCComparisonTable() {
55
const [comparisonData, setComparisonData] = useState(null);
6-
const [showBehavioural, setShowBehavioural] = useState(true);
6+
const [showBehavioural, setShowBehavioural] = useState(false);
77

88
useEffect(() => {
99
// Fetch both SFC comparison data and PolicyEngine budgetary impact data
@@ -252,8 +252,7 @@ function SFCComparisonTable() {
252252
the freeze was already in Budget 2025-26; SFC only costs the incremental
253253
extension through 2027-28/2028-29. SCP baby boost starts mid-2027-28.
254254
SFC reports combined basic + intermediate thresholds (~£50m total); we
255-
apportion using PolicyEngine microsimulation. SCP inflation uprating has
256-
no SFC costing as it's included in their baseline. See{" "}
255+
apportion using PolicyEngine microsimulation. See{" "}
257256
<a
258257
href="https://fiscalcommission.scot/wp-content/uploads/2026/01/Scotlands-Economic-and-Fiscal-Forecasts-January-2026-revised-13-01-2026.pdf"
259258
target="_blank"

0 commit comments

Comments
 (0)