{"spec_id":"scatter-connected-temporal","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nscatter-connected-temporal: Connected Scatter Plot with Temporal Path\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-06-09\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\n\n# This file is named altair.py — remove the script directory from sys.path so\n# importlib.import_module('altair') resolves the installed package, not this file.\n_path0 = sys.path.pop(0)\nalt = importlib.import_module(\"altair\")\nsys.path.insert(0, _path0)\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# --- Theme tokens — Imprint palette ---\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# --- Data — US-style unemployment vs inflation (Phillips curve, 1994–2023) ---\nnp.random.seed(42)\nyears = np.arange(1994, 2024)\nn = len(years)\n\nunemployment = np.zeros(n)\ninflation = np.zeros(n)\nunemployment[0] = 6.1\ninflation[0] = 2.6\n\nfor i in range(1, n):\n    unemployment[i] = unemployment[i - 1] + np.random.normal(-0.05, 0.6)\n    inflation[i] = inflation[i - 1] + np.random.normal(0.02, 0.5)\n    unemployment[i] = np.clip(unemployment[i], 3.0, 10.5)\n    inflation[i] = np.clip(inflation[i], -0.5, 6.0)\n\n# Recession spike around 2008–2010\nunemployment[14:17] += np.array([2.5, 4.0, 3.5])\ninflation[14:17] -= np.array([1.0, 1.5, 0.5])\nunemployment = np.clip(unemployment, 3.0, 10.5)\ninflation = np.clip(inflation, -0.5, 6.0)\n\ndf = pd.DataFrame(\n    {\"year\": years, \"unemployment\": np.round(unemployment, 1), \"inflation\": np.round(inflation, 1), \"order\": range(n)}\n)\n\n# Key year annotations with nudged positions to avoid crowding\nlabel_years = [1994, 2000, 2008, 2010, 2015, 2023]\ndf_labels = df[df[\"year\"].isin(label_years)].copy()\nnudge = {\n    1994: (0.28, 0.30),\n    2000: (0.28, 0.30),\n    2008: (0.25, -0.32),\n    2010: (-0.22, 0.35),\n    2015: (0.30, -0.38),\n    2023: (-0.28, -0.38),\n}\ndf_labels[\"label_x\"] = df_labels.apply(lambda r: r[\"unemployment\"] + nudge.get(r[\"year\"], (0, 0))[0], axis=1)\ndf_labels[\"label_y\"] = df_labels.apply(lambda r: r[\"inflation\"] + nudge.get(r[\"year\"], (0, 0))[1], axis=1)\n\n# --- Encodings ---\nx_scale = alt.Scale(domain=[2.5, 8.5], nice=False)\n# Tightened lower bound — previous [-1.5, 5.8] wasted space below data\ny_scale = alt.Scale(domain=[-0.8, 6.2], nice=False)\n\nx_enc = alt.X(\"unemployment:Q\", title=\"Unemployment Rate (%)\", scale=x_scale)\ny_enc = alt.Y(\"inflation:Q\", title=\"Inflation Rate (%)\", scale=y_scale)\n\n# Imprint sequential colormap for temporal progression: brand-green (1994) → blue (2023)\nimprint_seq_scale = alt.Scale(range=[\"#009E73\", \"#4467A3\"], domain=[1994, 2023])\nyear_legend = alt.Legend(\n    title=\"Year\", titleFontSize=10, labelFontSize=10, format=\"d\", gradientLength=160, gradientThickness=10\n)\n\n# --- Chart layers ---\n# Connecting path in temporal order — increased opacity (0.60) for legible trajectory\npath = alt.Chart(df).mark_line(strokeWidth=2.5, opacity=0.60, color=INK_SOFT).encode(x=x_enc, y=y_enc, order=\"order:Q\")\n\n# Points colored by temporal progression using Imprint sequential cmap\npoints = (\n    alt.Chart(df)\n    .mark_point(filled=True, size=160, opacity=0.85, stroke=\"white\", strokeWidth=1.2)\n    .encode(\n        x=x_enc,\n        y=y_enc,\n        color=alt.Color(\"year:Q\", scale=imprint_seq_scale, legend=year_legend),\n        tooltip=[\n            alt.Tooltip(\"year:Q\", title=\"Year\", format=\"d\"),\n            alt.Tooltip(\"unemployment:Q\", title=\"Unemployment (%)\", format=\".1f\"),\n            alt.Tooltip(\"inflation:Q\", title=\"Inflation (%)\", format=\".1f\"),\n        ],\n    )\n)\n\n# Year annotations for key time points\nannotations = (\n    alt.Chart(df_labels)\n    .mark_text(fontSize=11, fontWeight=\"bold\", color=INK, dy=-15)\n    .encode(x=alt.X(\"label_x:Q\"), y=alt.Y(\"label_y:Q\"), text=alt.Text(\"year:Q\", format=\"d\"))\n)\n\n# --- Compose + configure ---\n# Canvas: 620×320 inner view (landscape) → target PNG 3200×1800 after scale_factor=4\nchart = (\n    (path + points + annotations)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"scatter-connected-temporal · python · altair · anyplot.ai\",\n            fontSize=16,\n            color=INK,\n            subtitle=\"Unemployment vs. Inflation — tracing the Phillips curve path (1994–2023)\",\n            subtitleFontSize=10,\n            subtitleColor=INK_SOFT,\n            subtitlePadding=4,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0, continuousWidth=620, continuousHeight=320)\n    .configure_axis(\n        labelFontSize=10,\n        labelColor=INK_SOFT,\n        titleFontSize=12,\n        titleColor=INK,\n        titlePadding=8,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        grid=True,\n        gridOpacity=0.15,\n        gridColor=INK,\n        gridDash=[3, 3],\n    )\n    .configure_title(color=INK)\n    .configure_legend(\n        orient=\"right\",\n        padding=10,\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n    .interactive()\n)\n\n# --- Save ---\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# PAD-only to exact 3200×1800 — do NOT crop (cropping clips labels, triggers AR-09)\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Interactive HTML — untouched by padding\nchart.save(f\"plot-{THEME}.html\")\n"}