{"spec_id":"span-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nspan-basic: Basic Span Plot (Highlighted Region)\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-07-25\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens\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# Imprint categorical palette — 8 hues, theme-independent, hybrid-v3 sort\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nANYPLOT_AMBER = \"#DDCC77\"  # semantic anchor — warning / caution (outside the categorical pool)\n\nBRAND = IMPRINT_PALETTE[0]  # position 1 — always the primary series (price line)\n# Semantic exception (see default-style-guide.md \"Color Philosophy\"): the recession\n# is a bad/loss period, so it uses the deferred red anchor (position 5) rather than\n# the next ordinal slot; the warning threshold band uses the dedicated amber anchor.\n# Both spans carry an explicit text label (\"Recession Period\" / \"Warning Zone\") so\n# the semantic mapping is unambiguous, per the style guide's requirement.\nRECESSION_COLOR = IMPRINT_PALETTE[4]  # matte red — bad/loss semantic anchor\nWARNING_COLOR = ANYPLOT_AMBER  # amber — warning semantic anchor\n# Amber text on the pale-cream light bg falls below WCAG 3:1 (documented amber/light\n# tension in the style guide); darken the \"Warning Zone\" label text only in light mode\n# — the span fill/edge rules keep the true amber anchor in both themes.\nWARNING_LABEL_COLOR = \"#6B5518\" if THEME == \"light\" else WARNING_COLOR\n\n# Data — stock price with recession dip and warning threshold zone\nnp.random.seed(42)\ndates = pd.date_range(start=\"2007-01-01\", periods=36, freq=\"MS\")\n\nbase_price = 100\nprices = [base_price]\nfor i in range(1, 36):\n    if 12 <= i < 24:\n        drift = -0.01\n    else:\n        drift = 0.008\n    change = drift + np.random.randn() * 0.03\n    prices.append(prices[-1] * (1 + change))\n\ndf = pd.DataFrame({\"Date\": dates, \"Price\": prices})\n\nrecession_start = pd.Timestamp(\"2008-01-01\")\nrecession_end = pd.Timestamp(\"2009-12-01\")\nthreshold_low = 85\nthreshold_high = 95\n\nprice_scale = alt.Scale(domain=[60, 130])\n\n# Base line chart\nline = (\n    alt.Chart(df)\n    .mark_line(strokeWidth=3, color=BRAND)\n    .encode(\n        x=alt.X(\n            \"Date:T\",\n            title=\"Date\",\n            axis=alt.Axis(\n                labelFontSize=10, titleFontSize=12, tickCount={\"interval\": \"month\", \"step\": 6}, format=\"%b %Y\"\n            ),\n        ),\n        y=alt.Y(\n            \"Price:Q\", title=\"Stock Price ($)\", scale=price_scale, axis=alt.Axis(labelFontSize=10, titleFontSize=12)\n        ),\n        tooltip=[alt.Tooltip(\"Date:T\", format=\"%b %Y\"), alt.Tooltip(\"Price:Q\", format=\".2f\", title=\"Price ($)\")],\n    )\n)\n\npoints = (\n    alt.Chart(df)\n    .mark_point(size=90, color=BRAND, filled=True)\n    .encode(\n        x=\"Date:T\",\n        y=alt.Y(\"Price:Q\", scale=price_scale),\n        tooltip=[alt.Tooltip(\"Date:T\", format=\"%b %Y\"), alt.Tooltip(\"Price:Q\", format=\".2f\", title=\"Price ($)\")],\n    )\n)\n\n# Vertical span — recession period\nrecession_span_data = pd.DataFrame({\"start\": [recession_start], \"end\": [recession_end]})\nvertical_span = (\n    alt.Chart(recession_span_data)\n    .mark_rect(opacity=0.30, color=RECESSION_COLOR)\n    .encode(x=alt.X(\"start:T\"), x2=alt.X2(\"end:T\"))\n)\n\nleft_edge = (\n    alt.Chart(pd.DataFrame({\"x\": [recession_start]}))\n    .mark_rule(strokeWidth=2, strokeDash=[6, 4], color=RECESSION_COLOR)\n    .encode(x=\"x:T\")\n)\n\nright_edge = (\n    alt.Chart(pd.DataFrame({\"x\": [recession_end]}))\n    .mark_rule(strokeWidth=2, strokeDash=[6, 4], color=RECESSION_COLOR)\n    .encode(x=\"x:T\")\n)\n\n# Horizontal span — warning threshold zone\nthreshold_span_data = pd.DataFrame({\"y\": [threshold_low], \"y2\": [threshold_high]})\nhorizontal_span = (\n    alt.Chart(threshold_span_data)\n    .mark_rect(opacity=0.2, color=WARNING_COLOR)\n    .encode(y=alt.Y(\"y:Q\", scale=price_scale), y2=alt.Y2(\"y2:Q\"))\n)\n\nbottom_edge = (\n    alt.Chart(pd.DataFrame({\"y\": [threshold_low]}))\n    .mark_rule(strokeWidth=2, strokeDash=[6, 4], color=WARNING_COLOR)\n    .encode(y=alt.Y(\"y:Q\", scale=price_scale))\n)\n\ntop_edge = (\n    alt.Chart(pd.DataFrame({\"y\": [threshold_high]}))\n    .mark_rule(strokeWidth=2, strokeDash=[6, 4], color=WARNING_COLOR)\n    .encode(y=alt.Y(\"y:Q\", scale=price_scale))\n)\n\n# Text labels for span regions\nrecession_label = (\n    alt.Chart(pd.DataFrame({\"x\": [pd.Timestamp(\"2008-07-01\")], \"y\": [125], \"text\": [\"Recession Period\"]}))\n    .mark_text(fontSize=12, fontWeight=\"bold\", color=RECESSION_COLOR)\n    .encode(x=\"x:T\", y=alt.Y(\"y:Q\", scale=price_scale), text=\"text:N\")\n)\n\nthreshold_label = (\n    alt.Chart(pd.DataFrame({\"x\": [pd.Timestamp(\"2007-06-01\")], \"y\": [90], \"text\": [\"Warning Zone\"]}))\n    .mark_text(fontSize=11, fontWeight=\"bold\", color=WARNING_LABEL_COLOR)\n    .encode(x=\"x:T\", y=alt.Y(\"y:Q\", scale=price_scale), text=\"text:N\")\n)\n\n# Combine all layers with theme-adaptive chrome\nchart = (\n    alt.layer(\n        horizontal_span,\n        bottom_edge,\n        top_edge,\n        vertical_span,\n        left_edge,\n        right_edge,\n        line,\n        points,\n        recession_label,\n        threshold_label,\n    )\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\"span-basic · python · altair · anyplot.ai\", fontSize=16, color=INK),\n    )\n    .configure_view(fill=PAGE_BG, continuousWidth=620, continuousHeight=320, strokeWidth=0)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK_SOFT,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n    .configure_title(color=INK, fontSize=16)\n)\n\n# Save — hard target: 3200 x 1800 (landscape). See prompts/library/altair.md \"Canvas\".\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n\n# PAD-only to the exact canonical canvas — vl-convert's title/axis/legend padding\n# means the saved PNG rarely lands exactly on target. Never crop (would clip text).\nTW, TH = 3200, 1800\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}x{_h}, exceeds target {TW}x{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"}