""" DC/MD/VA Unemployment — Static HTML Report Generator Run: python3 generate_report.py Output: dc_md_va_unemployment_report.html (open in any browser, no internet required) """ import requests import json import os from datetime import datetime from config import BLS_API_KEY, BLS_API_BASE SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # --------------------------------------------------------------------------- # Series config # --------------------------------------------------------------------------- GEOS = ["DC", "MD", "VA", "DCMSA", "BAL", "RIC"] LABELS = { "DC": "District of Columbia", "MD": "Maryland", "VA": "Virginia", "DCMSA": "DC Metro MSA", "BAL": "Baltimore MSA", "RIC": "Richmond MSA", } COLORS = { "DC": "#c0392b", "MD": "#2471a3", "VA": "#1e8449", "DCMSA": "#7d3c98", "BAL": "#d68910", "RIC": "#117a65", } SERIES = { # District of Columbia (state FIPS 11) "DC_rate": "LAUST110000000000003", "DC_unemployed": "LAUST110000000000004", "DC_employed": "LAUST110000000000005", "DC_laborforce": "LAUST110000000000006", # Maryland (state FIPS 24) "MD_rate": "LAUST240000000000003", "MD_unemployed": "LAUST240000000000004", "MD_employed": "LAUST240000000000005", "MD_laborforce": "LAUST240000000000006", # Virginia (state FIPS 51) "VA_rate": "LAUST510000000000003", "VA_unemployed": "LAUST510000000000004", "VA_employed": "LAUST510000000000005", "VA_laborforce": "LAUST510000000000006", # DC-Arlington-Alexandria MSA (state 11, CBSA 47900) "DCMSA_rate": "LAUMT114790000000003", "DCMSA_unemployed":"LAUMT114790000000004", "DCMSA_employed": "LAUMT114790000000005", "DCMSA_laborforce":"LAUMT114790000000006", # Baltimore-Columbia-Towson MSA (state 24, CBSA 12580) "BAL_rate": "LAUMT241258000000003", "BAL_unemployed": "LAUMT241258000000004", "BAL_employed": "LAUMT241258000000005", "BAL_laborforce": "LAUMT241258000000006", # Richmond MSA (state 51, CBSA 40060) "RIC_rate": "LAUMT514006000000003", "RIC_unemployed": "LAUMT514006000000004", "RIC_employed": "LAUMT514006000000005", "RIC_laborforce": "LAUMT514006000000006", } # --------------------------------------------------------------------------- # Data fetching # --------------------------------------------------------------------------- def fetch(series_ids, start_year, end_year): payload = { "seriesid": series_ids, "startyear": str(start_year), "endyear": str(end_year), "registrationkey": BLS_API_KEY, "catalog": True, "calculations": True, "annualaverage": False, } r = requests.post(f"{BLS_API_BASE}/timeseries/data/", json=payload, timeout=30) r.raise_for_status() body = r.json() if body["status"] != "REQUEST_SUCCEEDED": raise RuntimeError(f"BLS API error: {body['message']}") return {s["seriesID"]: s["data"] for s in body["Results"]["series"]} def latest(data): for obs in data: if obs["value"] != "-": return obs return {} def chg(obs, window): try: return obs["calculations"]["net_changes"].get(window) except (KeyError, TypeError): return None # --------------------------------------------------------------------------- # Chart data builders # --------------------------------------------------------------------------- MONTH_ORDER = {f"M{i:02d}": i for i in range(1, 13)} MONTH_NAMES = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] def sort_obs(data): monthly = [o for o in data if o["period"].startswith("M") and o["value"] != "-"] return sorted(monthly, key=lambda o: (o["year"], MONTH_ORDER.get(o["period"], 0))) def build_chart_data(results, measure, months=24): """Build Chart.js labels + datasets for any measure (rate, employed, etc.).""" all_labels = {} for geo in GEOS: sid = SERIES[f"{geo}_{measure}"] for obs in sort_obs(results.get(sid, [])): key = (obs["year"], obs["period"]) m = MONTH_ORDER.get(obs["period"], 0) all_labels[key] = f"{MONTH_NAMES[m]} {obs['year']}" sorted_keys = sorted(all_labels.keys())[-months:] labels = [all_labels[k] for k in sorted_keys] datasets = [] for geo in GEOS: sid = SERIES[f"{geo}_{measure}"] obs_by_key = { (o["year"], o["period"]): float(o["value"]) for o in sort_obs(results.get(sid, [])) } datasets.append({ "label": LABELS[geo], "data": [obs_by_key.get(k) for k in sorted_keys], "borderColor": COLORS[geo], "backgroundColor": COLORS[geo] + "22", "borderWidth": 2, "pointRadius": 2, "tension": 0.3, "fill": False, }) return labels, datasets # --------------------------------------------------------------------------- # HTML helpers # --------------------------------------------------------------------------- def arrow(val): if val is None: return '—' try: n = float(val) if n > 0: return f'▲ {n:+.1f}' if n < 0: return f'▼ {n:.1f}' return f'— {n:.1f}' except (ValueError, TypeError): return '—' def fmt_rate(val): try: return f"{float(val):.1f}%" except (ValueError, TypeError): return "—" def fmt_num(val): try: return f"{float(val):,.0f}" except (ValueError, TypeError): return "—" def build_cards(results): cards_html = [] for geo in GEOS: sid = SERIES[f"{geo}_rate"] obs = latest(results.get(sid, [])) rate = fmt_rate(obs.get("value")) mom = arrow(chg(obs, "1")) yoy = arrow(chg(obs, "12")) period = f"{obs.get('periodName', '')} {obs.get('year', '')}" color = COLORS[geo] cards_html.append(f"""
| Measure | {geo_headers}
|---|
▲ Red = rate rising | ▼ Green = rate falling | Not seasonally adjusted
State totals (DC/MD/VA) are significantly larger than MSA figures. Hover to compare values.
MoM = month-over-month net change | YoY = year-over-year net change | pp = percentage points