Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
740d9c0
Redesign shell: header band with view tabs, title band, sticky sub-ta…
vahid-ahmadi Nov 3, 2026
71e3851
Add DESIGN.md and fix connect, paper and short-page layouts
vahid-ahmadi Nov 3, 2026
0e2987b
OBR pages: verdict, stat row, rail, where-next; each fact once
vahid-ahmadi Nov 3, 2026
d51b9fd
svar, frb-us: StepPanel Overview and Validation, verdict, rail, where…
vahid-ahmadi Nov 3, 2026
aaa3765
us-hank, olg: StepPanel Overview and Validation, verdict, rail, where…
vahid-ahmadi Nov 3, 2026
f425e8c
pe, define: StepPanel Overview and Validation, verdict, rail, where-next
vahid-ahmadi Nov 3, 2026
1a162a3
/models: verdicts in words, Combine as a StepPanel, phone question rows
vahid-ahmadi Nov 3, 2026
0e5538f
Homepage: entry cards, verdicts as links, joins as a StepPanel
vahid-ahmadi Nov 3, 2026
1404097
/connect as an MCP → CLI → Python StepPanel; /contact restyled
vahid-ahmadi Nov 3, 2026
73c576e
Drop the retired .grain div from the remaining hand-kept pages
vahid-ahmadi Nov 3, 2026
183f48b
Footer: back to PolicyEngine, GitHub, Contact (#133)
vahid-ahmadi Nov 3, 2026
f04e3a4
Tidy: score column spacing, no tab stops inside the hidden OLG code p…
vahid-ahmadi Nov 3, 2026
f90ac96
Shared /stepper.js, generated model sub-tabs, role tokens in method d…
vahid-ahmadi Nov 3, 2026
6450ab6
Charts: labels in HTML over the SVG, readable at 375 px
vahid-ahmadi Nov 3, 2026
85c7a52
Release notes: one stat tile, provenance, where next; regenerable
vahid-ahmadi Nov 3, 2026
57f76ce
Forecasts and data hubs, topic pages: rail, stat rows, where next, ea…
vahid-ahmadi Nov 3, 2026
b679e57
Review fixes: adaptive fan ticks, side-effect-free notes check
vahid-ahmadi Nov 3, 2026
18a6d07
Restore the March 2026 EFO vintage on the OBR paper lede; test the --…
vahid-ahmadi Nov 3, 2026
370c375
Forecasts limits: Bank Rate comes closest, no win survives multiple t…
vahid-ahmadi Nov 3, 2026
82ecdc2
Phone reading: stacked tables, scroll fades, readable method diagrams…
vahid-ahmadi Nov 3, 2026
d5092d0
Bump the stylesheet cache buster to 10; document phone reading (DESIG…
vahid-ahmadi Nov 3, 2026
36636ed
Method diagrams: fix labels that clipped at 11px
vahid-ahmadi Nov 3, 2026
fbc1183
pe-microsim pages: model card, one home for the HMRC benchmark, cut r…
vahid-ahmadi Nov 3, 2026
545e36a
IA: Forecasts ahead of Models; home tells the story in order; /models…
vahid-ahmadi Nov 3, 2026
460daca
Forecasts: scoreboard with benchmarks, score the unemployment outturn…
vahid-ahmadi Nov 3, 2026
e40899b
obr-macro pages: model card, outturn test promoted, HMRC benchmark se…
vahid-ahmadi Nov 3, 2026
4be051d
Home entry card within the 8-word budget; DESIGN 7.2 wording
vahid-ahmadi Nov 3, 2026
dd73587
Data hubs link to the one data store; calendar says which releases th…
vahid-ahmadi Nov 3, 2026
c9c7298
boe-svar pages: model card, sources to Methodology, all six shocks, r…
vahid-ahmadi Nov 3, 2026
ae9e1ca
US and long-run model pages: model cards, trust cards, each fact once
vahid-ahmadi Nov 3, 2026
5ee2d41
US forecasts leads with the no-record verdict; sub-tab 'Overview' bec…
vahid-ahmadi Nov 3, 2026
bab25aa
pe-microsim: reproduce command needs --runslow; drop a sentence repea…
vahid-ahmadi Nov 3, 2026
81284f2
OLG methodology links its paper; DEFINE horizon names both windows; F…
vahid-ahmadi Nov 3, 2026
1107ac5
Charts: strict colour roles, unit lines, label halo, outturn dots, lo…
vahid-ahmadi Nov 3, 2026
36c80e8
Store catalogue carries each series' release date and topic
vahid-ahmadi Nov 3, 2026
bc621c3
Merge branch 'vahid/redesign-r2-ia' into vahid/redesign-r2-integrate
vahid-ahmadi Nov 3, 2026
53ddcd0
Merge branch 'vahid/redesign-r2-uk-models' into vahid/redesign-r2-int…
vahid-ahmadi Nov 3, 2026
1ffe124
Merge branch 'vahid/redesign-r2-us-models' into vahid/redesign-r2-int…
vahid-ahmadi Nov 3, 2026
e0904c2
Merge branch 'vahid/redesign-r2-charts-use' into vahid/redesign-r2-in…
vahid-ahmadi Nov 3, 2026
0900c01
Regenerate OBR validation charts after merging the chart and UK-model…
vahid-ahmadi Nov 3, 2026
284f378
Merge branch 'vahid/redesign-r2-forecasts' into vahid/redesign-r2-int…
vahid-ahmadi Nov 3, 2026
28d57ec
Integrate round 2: redirect the 53 retired release notes to their dig…
vahid-ahmadi Nov 3, 2026
6cb4c69
Integrate round 2: one model card and trust card for all seven models…
vahid-ahmadi Nov 3, 2026
e31bb97
Bump STYLE_VERSION to 12 for the integration CSS and re-run site_nav …
vahid-ahmadi Nov 3, 2026
25fe627
Restyle the site in the Autumn Budget dashboard's look: Inter everywh…
vahid-ahmadi Nov 3, 2026
d401507
Autumn Budget look, part 2: sentence-case labels, phone title bar, da…
vahid-ahmadi Nov 3, 2026
ecde885
Autumn Budget look: wrapped section titles keep a normal line height
vahid-ahmadi Nov 3, 2026
12ece02
Fit the forecast scoreboard to the card width on desktop
vahid-ahmadi Nov 3, 2026
796e3e7
Home and Models: tight landing, human model names first, transposed c…
vahid-ahmadi Nov 5, 2026
ce9b926
Round 3 component vocabulary: data-icon section heads, .kv rows, .ful…
vahid-ahmadi Nov 5, 2026
435895e
Round 3: page-agent requests (kv-title, model-id, step expanders, hom…
vahid-ahmadi Nov 5, 2026
5721cc5
Model, paper and report pages: name h1s, icon section heads, prose in…
vahid-ahmadi Nov 5, 2026
bbf63a8
Forecasts: scoreboard as the full-width lead card, limits as label/va…
vahid-ahmadi Nov 5, 2026
e390db0
Model pages: phrase-valued label/value rows use kv-text
vahid-ahmadi Nov 5, 2026
88ee629
US forecasts, Use and Contact: page-name h1s, icon section headers, l…
vahid-ahmadi Nov 5, 2026
8a3812c
Round 3: kv-text rows, numeric table columns, flatter figures, panel …
vahid-ahmadi Nov 5, 2026
3a99fd5
Data hubs, topic pages and release digests: page-name h1s, icon secti…
vahid-ahmadi Nov 5, 2026
b2f180b
Step panels: the 3:2 grid applies from 900px only (it collapsed the p…
vahid-ahmadi Nov 5, 2026
ad75c5d
Model pages: short-step panels become single columns; long callouts l…
vahid-ahmadi Nov 5, 2026
75339ed
Forecasts: state the sparse-record caveat once; US rail label matches…
vahid-ahmadi Nov 5, 2026
b966c68
STYLE_VERSION 17 and site_nav --write; where-next head uses data-icon…
vahid-ahmadi Nov 5, 2026
ddb8ee0
Size model ids relative to their context so router pills read name first
vahid-ahmadi Nov 5, 2026
3cb839a
FRB/US: core PCE miss is -1.65pp (2.8% vs 4.4%); GDP within 0.3pp onl…
vahid-ahmadi Nov 6, 2026
ed4e53d
DEFINE-UK: withdraw "benefits fall / upper-leaning" on 1.78; drop the…
vahid-ahmadi Nov 6, 2026
823d73c
OBR: profits error is not the OSHH constant (23.9% identity floor fro…
vahid-ahmadi Nov 6, 2026
b977555
OLG: the 44% "government consumption" series is total public spending…
vahid-ahmadi Nov 6, 2026
1242690
US HANK: "Open models" tab -> "HANK survey"; status says what is vali…
vahid-ahmadi Nov 6, 2026
93553e6
BoE SVAR: label the 71% -> 57% coverage as the no-Covid-dummy run; du…
vahid-ahmadi Nov 6, 2026
8ee0068
obr_shock: solve to the last shocked quarter and cap periods at 25 (d…
vahid-ahmadi Nov 6, 2026
bc0c824
BoE SVAR methodology: production run is 744 accepted (7.4%), ESS 356.1
vahid-ahmadi Nov 6, 2026
e4f7500
OBR validation: recompute the hand-typed anchored table and free-runn…
vahid-ahmadi Nov 6, 2026
f3d4b01
Add changelog entry for #200
vahid-ahmadi Nov 6, 2026
84e9539
Changelog: shared [Unreleased] heading for the redesign and pipeline …
vahid-ahmadi Nov 6, 2026
f8e42ee
Consolidate the redesign stack's changelog entries on #200
vahid-ahmadi Nov 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@ The version below tracks the `policyengine-macro` package in `integration/`.
`CITATION.cff` and `integration/pyproject.toml` must agree with the topmost
heading here; `tests/test_repo_hygiene.py` asserts that they do.

## [Unreleased]

### Site redesign stack (#200)

Bullets by PR, in merge order. #226 and #229 wait on the decision in #218; #231 and #232 sit on #206. Drop a PR's bullet here if it has not merged into #200 when #200 merges.

- **#200: site redesign.** Every page uses the Autumn Budget 2026 layout (header band, title band, sub-tabs, cards with expanders), with `DESIGN.md` as the spec. URLs, numbers and data are unchanged.
- **#204: question router on `/models` and a new `/glossary`.** Model cards filter by question and country; `/connect` gives copy-paste setup and `/contact` lists self-serve routes. No numbers changed.
- **#206: model pages.** One-column methodology, one overview order, and model names instead of ids.
- **#206: FRB/US comparators.** The worked-shock reference moves from the 2014 FEDS Note (gap −0.4pp, core PCE −0.08pp) to Laforte (2018) for the current model (gap −0.486pp, core PCE −0.0345pp, unemployment +0.219pp at q8); multiplier ranges move to CBO's converted ranges (for example Coenen 0.7–1.0 → CBO 0.45–1.82).
- **#205: forecast-evaluation scoreboard.** `/forecasts` leads with the record (band coverage, mean absolute error beside the no-change benchmark, forecasts scored), the archive is flat, and charts carry units and sources. `forecasts/scorecard.json` is unchanged.
- **#219: `/models/compare`.** One table per question (six questions × five models) with the exact call, a sourced benchmark and a verdict. The FRB/US benchmark is Laforte (2018) (band −0.53…−0.40 → −0.486…−0.459).
- **#216: integration-layer scenario sweep fixes.** `score_reform(model="obr")` places PolicyEngine-UK costings on fiscal-year quarters (default window `2026Q1..2030Q4` → `2026Q2..2031Q1`); `svar_forecast` returns simple y/y rates rather than log differences; `frbus_shock` reports rates as level deviations; `hank_shock` returns the `tax` series; invalid inputs (NaN sizes, unknown question types, out-of-window years) are refused before solving.
- **#226: hosted OBR pin** `1ed742c` → `e4d7faa` (obr-macroeconomic-model main). After a redeploy, hosted Bank Rate +100bp ΔGDP q4/q8/q12 moves −446/−525/−490 → −431/−482/−416 £m, matching the site, and the default `score_reform` cumulative ΔGDP for a basic-rate rise moves +£5.6bn → −£18.3bn.
- **#229: weekly drift check.** `integration/drift/canaries.json` compares one cheap hosted call per model with the number the site quotes for it and reports value, provenance and revision mismatches.
- **#231: BoE SVAR replication figures** are labelled with their run vintage (boe-var-model `845e2a4`) and their `results/` links are pinned to it. No number changes.
- **#232: BoE SVAR figures updated to the 2026Q2 data refresh** (boe-var-model #24, `d4ec3e7`): accepted draws 744 → 765, ESS 356.1 → 368.0, global FEVD share of UK GDP 37.4 → 38.7 and of UK CPI 42.3 → 41.5, rolling origins 49 → 50, smallest adjusted q 0.36 → 0.39 (published spec 0.40 → 0.42), fast-config GDP/CPI 42.0/37.6 → 40.1/40.1.

<!-- Site redesign stack (#200) entries go above this line; data pipeline (#221, #228) entries go below it. Keeping them apart lets both stacks merge in either order. -->

## [0.2.0] — 2026-08-11

Access and verification release. Nothing about any model's behaviour or any
Expand Down
881 changes: 881 additions & 0 deletions DESIGN.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ verbatim — that registry is authoritative and this table is a copy of it.
| 02 | **`obr-macro`** — OBR macroeconometric emulator | validated for selected scenarios | [PolicyEngine/obr-macroeconomic-model](https://github.com/PolicyEngine/obr-macroeconomic-model) |
| 03 | **`boe-svar`** — Bank of England structural VAR replication | validated replication for selected outputs | [PolicyEngine/boe-var-model](https://github.com/PolicyEngine/boe-var-model) |
| 04 | **`frb-us`** — Federal Reserve FRB-US implementation | validated software replication with scope limits | [PolicyEngine/us-frb-model](https://github.com/PolicyEngine/us-frb-model) |
| 05 | **`us-hank`** — US two-asset HANK (Auclert–Bardóczy–Rognlie–Straub 2021) | validated replication for hosted stylized-shock experiments; VAR-free sequence-space HANK; not a forecaster; distributional outputs are first-order approximations | [PolicyEngine/us-hank-model](https://github.com/PolicyEngine/us-hank-model) |
| 05 | **`us-hank`** — US two-asset HANK (Auclert–Bardóczy–Rognlie–Straub 2021) | steady state replicates the paper (six of seven solved parameters); shock signs are tested, magnitudes are not validated; not a forecaster; distributional outputs are steady-state statistics and an MPC-proportional split of the aggregate | [PolicyEngine/us-hank-model](https://github.com/PolicyEngine/us-hank-model) |
| 06 | **`psl-og`** — OG-UK overlapping generations model | research prototype; calibrated counterfactual | [PSLmodels/OG-UK](https://github.com/PSLmodels/OG-UK) |
| 07 | **`define-uk`** — DEFINE-UK ecological stock-flow consistent model | experimental; partial replication — baseline macro block replicates manual Table 4; scenario deltas gated on the pinned oracle run, the published scenario definitions, and two paper anchors (no numeric v1.1 scenario results are published); unlicensed upstream is never hosted, so hosted calls return run instructions | [PolicyEngine/define-uk-model](https://github.com/PolicyEngine/define-uk-model) (upstream [DEFINE-model/DEFINE_UK_1.1](https://github.com/DEFINE-model/DEFINE_UK_1.1)) |
| — | More model classes (incl. OG-USA) | planned | — |
Expand Down
77 changes: 42 additions & 35 deletions assets/make_current_outlooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
import csv
import json
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
import chartkit # noqa: E402 (stdlib-only, repo root)


def points(values, xs, ymap):
Expand All @@ -28,40 +31,42 @@ def obr_chart() -> str:
def ymap(v):
return bot - (v - 99) / 11 * (bot - top)

top_v = ymap(110)
out = [
'<figure class="vfig current-outlook">',
'<svg class="vchart" viewBox="0 0 800 326" role="img" aria-labelledby="obr-current-t obr-current-d">',
f'<svg class="vchart" data-chart="obr-current-outlook" viewBox="{x0 - 6} {top_v - 6:.1f} '
f'{x1 - x0 + 12} {bot - top_v + 12:.1f}" role="img" aria-labelledby="obr-current-t obr-current-d">',
'<title id="obr-current-t">March 2026 OBR outlook for real GDP and household consumption</title>',
'<desc id="obr-current-d">Indexed line chart from 2026Q1 to 2031Q1. Real GDP rises from 100 to %.1f and household consumption from 100 to %.1f.</desc>'
% (g[-1], c[-1]),
'<text class="vc-lab" x="62" y="25">March 2026 EFO outlook · 2026Q1 = 100</text>',
]
for tick in (100, 102, 104, 106, 108, 110):
y = ymap(tick)
out += [
f'<line class="vc-grid" x1="{x0}" y1="{y:.1f}" x2="{x1}" y2="{y:.1f}"/>',
f'<text class="vc-tick" x="52" y="{y + 4:.1f}" text-anchor="end">{tick}</text>',
chartkit.ytick(x0, y, str(tick)),
]
# Both series are the OBR's published outlook, nothing here is "ours":
# GDP in the official ink, drawn solid (no model to contrast it with),
# consumption as the neutral. A legend, not end labels: the two paths
# cross and end close together, both rising into the edge.
out += [
f'<polyline class="vc-s1" points="{points(g, xs, ymap)}"/>',
f'<polyline class="vc-s2" points="{points(c, xs, ymap)}"/>',
f'<circle class="vc-s1-dot" cx="{xs[-1]:.1f}" cy="{ymap(g[-1]):.1f}" r="3.5"/>',
f'<circle class="vc-s2-dot" cx="{xs[-1]:.1f}" cy="{ymap(c[-1]):.1f}" r="3.5"/>',

f'<polyline class="vc-s2 vc-solid" points="{points(g, xs, ymap)}"/>',
f'<polyline class="vc-s3" points="{points(c, xs, ymap)}"/>',
f'<circle class="vc-s2-dot" cx="{xs[-1]:.1f}" cy="{ymap(g[-1]):.1f}" r="3.5"/>',
f'<circle class="vc-s3-dot" cx="{xs[-1]:.1f}" cy="{ymap(c[-1]):.1f}" r="3.5"/>',
]
for i in (0, 4, 8, 12, 16, 20):
out.append(
f'<text class="vc-tick" x="{xs[i]:.1f}" y="302" text-anchor="middle">{q[i]}</text>'
)
out += [
"</svg>",
'<div class="olg-legend">'
'<span class="li"><span class="ln ln-s1"></span>Real GDP</span>'
'<span class="li"><span class="ln ln-s2"></span>Household consumption</span>'
'</div>',
ticks_at = (0, 4, 8, 12, 16, 20)
for j, i in enumerate(ticks_at):
# Narrow: keep 2026Q1, 2028Q1 and the last tick (2030Q1 would touch it).
out.append(chartkit.xtick(xs[i], bot, q[i], j, len(ticks_at) - 1,
minor=j % 2 == 1 or j == len(ticks_at) - 2))
out.append("</svg>")
return "\n".join([
'<figure class="vfig current-outlook">',
chartkit.figure("\n".join(out), unit="March 2026 EFO outlook, index, 2026Q1 = 100",
legend=[("ln-s2s", "Real GDP"), ("ln-s3", "Household consumption")]),
"</figure>",
]
return "\n".join(out)
])


def boe_chart() -> str:
Expand Down Expand Up @@ -91,36 +96,38 @@ def band(lo, hi):

ticks = range(int(vlo), int(vhi) + 1)
out = [
'<figure class="vfig current-outlook">',
'<svg class="vchart" viewBox="0 0 800 326" role="img" aria-labelledby="boe-current-t boe-current-d">',
f'<svg class="vchart" data-chart="boe-current-outlook" viewBox="{x0 - 6} {top - 6} '
f'{x1 - x0 + 12} {bot - top + 12}" role="img" aria-labelledby="boe-current-t boe-current-d">',
'<title id="boe-current-t">Current boe-svar UK real GDP growth forecast</title>',
f'<desc id="boe-current-d">Fan chart from the latest complete quarterly data edge, {data["data_edge"]}, forecasting {q[0]} to {q[-1]}. Median with 68 and 90 percent predictive bands.</desc>',
f'<text class="vc-lab" x="62" y="25">Current forecast from {data["data_edge"]} · year-on-year %</text>',
]
for tick in ticks:
y = ymap(tick)
cls = "vc-axis" if tick == 0 else "vc-grid"
cls = "vc-zero" if tick == 0 else "vc-grid"
label = f"{tick:+d}%".replace("-", "\u2212") if tick else "0%"
out += [
f'<line class="{cls}" x1="{x0}" y1="{y:.1f}" x2="{x1}" y2="{y:.1f}"/>',
f'<text class="vc-tick" x="52" y="{y + 4:.1f}" text-anchor="end">{tick:+d}%</text>',
chartkit.ytick(x0, y, label),
]
out += [
f'<path class="vc-band90" d="{band(lo90, hi90)}"/>',
f'<path class="vc-band68" d="{band(lo68, hi68)}"/>',
f'<polyline class="vc-s1" points="{points(med, xs, ymap)}"/>',
f'<circle class="vc-s1-dot" cx="{xs[-1]:.1f}" cy="{ymap(med[-1]):.1f}" r="3.5"/>',
'<text class="vc-note" x="738" y="25" text-anchor="end">90% / 68% bands · median</text>',
]
for i in (0, 4, 8, 12):
out.append(
f'<text class="vc-tick" x="{xs[i]:.1f}" y="302" text-anchor="middle">{q[i]}</text>'
)
out += [
"</svg>",
ticks_at = (0, 4, 8, 12)
for j, i in enumerate(ticks_at):
out.append(chartkit.xtick(xs[i], bot, q[i], j, len(ticks_at) - 1))
out.append("</svg>")
return "\n".join([
'<figure class="vfig current-outlook">',
chartkit.figure("\n".join(out), legend=[
("ln-s1", "median"), ("sw-band68", "68% band"), ("sw-band90", "90% band"),
], unit="UK real GDP, % change on a year earlier",
notes=[f"Current forecast from {data['data_edge']}."]),
f"<figcaption>Bands are the model&rsquo;s raw posterior quantiles, not rescaled to measured coverage &mdash; the calibrated version is on the <a href='/forecasts'>forecast record</a>. Live-facing forecast, distinct from the frozen 2024Q2 validation experiment. Data through {data['data_edge']}; forecast begins {data['forecast_start']}. Estimated on {data['estimation_sample'].replace('-', '&ndash;')}; {data['draws']:,} posterior draws, {data['accepted']} accepted; five stochastic paths per accepted draw. Source and provenance: <code>papers/boe-svar/figures/current_forecast.json</code>.</figcaption>",
"</figure>",
]
return "\n".join(out)
])


def render(page: Path, name: str, content: str) -> str:
Expand Down
28 changes: 16 additions & 12 deletions assets/make_hero.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
import chartkit # noqa: E402 (stdlib-only, repo root)
PAGE = ROOT / "index.html"
SRC = ROOT / "papers" / "boe-svar" / "figures" / "current_forecast.json"

Expand Down Expand Up @@ -61,7 +63,9 @@ def build() -> str:
quarters, med, lo68, hi68, lo90, hi90 = load_fan()

W, H = 640, 400
x0, x1 = 30.0, 604.0
# The value ticks sit in a gutter right of the plot, clear of the bands and
# the median (at 1440 the "+1%" label used to sit on the median path).
x0, x1 = 30.0, 580.0
top, bot = 40.0, 344.0 # plot area for the value range below
v_lo, v_hi = -1.7, 3.4
step = (x1 - x0) / (len(quarters) - 1)
Expand Down Expand Up @@ -98,8 +102,8 @@ def band(lo, hi):
)

out = [
f"{MARK_OPEN}",
f'<svg class="hero-fan" viewBox="0 0 {W} {H}" preserveAspectRatio="xMidYMid meet" '
f'<svg class="hero-fan" data-chart="hero-fan" viewBox="{x0 - 6:g} {top - 6:g} '
f'{x1 - x0 + 12:g} {bot - top + 12:g}" '
f'role="img" aria-labelledby="hero-fan-t hero-fan-d">',
'<title id="hero-fan-t">boe-svar: UK real GDP growth forecast, median with 68% and 90% credible bands</title>',
f'<desc id="hero-fan-d">{esc(desc)}</desc>',
Expand All @@ -111,8 +115,9 @@ def band(lo, hi):
out.append(
f'<line class="{cls}" x1="{n(x0)}" y1="{n(y)}" x2="{n(x1)}" y2="{n(y)}"/>'
)
label = f"{tick:+d}%".replace("-", "\u2212") if tick else "0%"
out.append(
f'<text class="hf-tick" x="{n(x0)}" y="{n(y - 7)}">{tick:+d}%</text>'
f'<text class="vc-tick hf-tick vc-gutter" x="{n(x1 + 8)}" y="{n(y)}">{label}</text>'
)

out.append(f'<path class="hf-b90" d="{band(lo90, hi90)}"/>')
Expand All @@ -128,16 +133,15 @@ def band(lo, hi):
for i, q in enumerate(quarters):
year, qtr = q.split("Q")
if i == 0 or qtr == "1":
anchor = "start" if i == 0 else "middle"
out.append(
f'<text class="hf-tick hf-q" x="{n(xs[i])}" y="366">{esc(year)}</text>'
f'<text class="vc-tick hf-q vc-x" x="{n(xs[i])}" y="{n(bot)}" '
f'text-anchor="{anchor}">{esc(year)}</text>'
)
# No in-SVG caption: the <figcaption> next to this figure says the same thing
# at real body size. Below 1000px the viewBox scales to roughly half, which
# would render these 11px labels at ~6px, so style.css hides .hf-tick there
# and the figure reads as a shape with the prose caption carrying the detail.
# The labels are lifted into HTML by chartkit, so they stay 12 px at every
# width; the <figcaption> next to this figure carries the rest.
out.append("</svg>")
out.append(MARK_CLOSE)
return "\n".join(out)
return "\n".join([MARK_OPEN, chartkit.figure("\n".join(out), wrap_class="vc-hero"), MARK_CLOSE])


BLOCK_RE = re.compile(re.escape(MARK_OPEN) + r".*?" + re.escape(MARK_CLOSE), re.DOTALL)
Expand All @@ -146,7 +150,7 @@ def band(lo, hi):
def render_page(html: str) -> str:
if not BLOCK_RE.search(html):
sys.exit(f"no {MARK_OPEN} … {MARK_CLOSE} block in {PAGE}")
return BLOCK_RE.sub(lambda _m: build().replace("\\", "\\\\"), html)
return BLOCK_RE.sub(lambda _m: build(), html)


def main():
Expand Down
Loading
Loading