{"spec_id":"area-stacked","library":"altair","language":"python","code":"\"\"\" anyplot.ai\narea-stacked: Stacked Area Chart\nLibrary: altair 6.2.2 | Python 3.13.15\nQuality: 93/100 | Updated: 2026-08-17\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 (Imprint palette — theme-adaptive chrome)\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: Monthly revenue by product category over two years\nnp.random.seed(42)\nmonths = pd.date_range(\"2023-01\", periods=24, freq=\"MS\")\n\n# Generate realistic revenue data with trends\nbase_software = 120 + np.cumsum(np.random.randn(24) * 5)\nbase_hardware = 80 + np.cumsum(np.random.randn(24) * 4)\nbase_services = 50 + np.cumsum(np.random.randn(24) * 3)\nbase_support = 30 + np.cumsum(np.random.randn(24) * 2)\n\n# Ensure all values are positive\nsoftware = np.maximum(base_software, 20)\nhardware = np.maximum(base_hardware, 15)\nservices = np.maximum(base_services, 10)\nsupport = np.maximum(base_support, 5)\n\n# Create long-form data for Altair\ndf = pd.DataFrame(\n    {\n        \"Month\": list(months) * 4,\n        \"Revenue\": np.concatenate([software, hardware, services, support]),\n        \"Category\": ([\"Software\"] * 24 + [\"Hardware\"] * 24 + [\"Services\"] * 24 + [\"Support\"] * 24),\n    }\n)\n\n# Define category order (largest at bottom for easier reading)\n# Stack order: 1=bottom, 4=top\ncategory_order = [\"Software\", \"Hardware\", \"Services\", \"Support\"]\nstack_order = {\"Software\": 1, \"Hardware\": 2, \"Services\": 3, \"Support\": 4}\ndf[\"StackOrder\"] = df[\"Category\"].map(stack_order)\n\n# Total revenue overlay, drawn as a dashed line tracing the top of the stack\ntotals = df.groupby(\"Month\", as_index=False)[\"Revenue\"].sum().rename(columns={\"Revenue\": \"Total\"})\n\n# Annotation: callout the sustained revenue decline that starts ~Mar 2024\ndecline_month = pd.Timestamp(\"2024-03-01\")\ndecline_total = float(totals.loc[totals[\"Month\"] == decline_month, \"Total\"].iloc[0])\nannotation_df = pd.DataFrame({\"Month\": [decline_month], \"Total\": [decline_total], \"Label\": [\"Revenue decline begins\"]})\n\n# Imprint palette: first series ALWAYS #009E73\ncolors = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n# Subtle back-to-front opacity gradient (bottom layer softer, top layer crisper)\nopacities = [0.80, 0.85, 0.90, 0.95]\n\n# Stacked areas\narea_chart = (\n    alt.Chart(df)\n    .mark_area(line=alt.MarkConfig(strokeWidth=1.5))\n    .encode(\n        x=alt.X(\n            \"Month:T\",\n            title=\"Month\",\n            axis=alt.Axis(\n                labelFontSize=10,\n                titleFontSize=12,\n                format=\"%b %Y\",\n                labelAngle=-45,\n                labelColor=INK_SOFT,\n                titleColor=INK,\n                grid=False,\n            ),\n        ),\n        y=alt.Y(\n            \"Revenue:Q\",\n            title=\"Revenue ($ thousands)\",\n            stack=\"zero\",\n            axis=alt.Axis(\n                labelFontSize=10, titleFontSize=12, labelColor=INK_SOFT, titleColor=INK, gridColor=INK, gridOpacity=0.12\n            ),\n        ),\n        color=alt.Color(\n            \"Category:N\",\n            scale=alt.Scale(domain=category_order, range=colors),\n            legend=alt.Legend(\n                title=[\"Product Category\", \"ordered by size\"],\n                titleFontSize=10,\n                titleFontWeight=\"bold\",\n                labelFontSize=10,\n                orient=\"right\",\n                symbolSize=80,\n                symbolStrokeWidth=0,\n                labelOffset=4,\n                rowPadding=6,\n                labelColor=INK_SOFT,\n                titleColor=INK,\n            ),\n        ),\n        opacity=alt.Opacity(\"Category:N\", scale=alt.Scale(domain=category_order, range=opacities), legend=None),\n        order=alt.Order(\"StackOrder:Q\", sort=\"ascending\"),\n        tooltip=[\n            alt.Tooltip(\"Month:T\", title=\"Month\", format=\"%B %Y\"),\n            alt.Tooltip(\"Category:N\", title=\"Category\"),\n            alt.Tooltip(\"Revenue:Q\", title=\"Revenue ($k)\", format=\".1f\"),\n        ],\n    )\n)\n\n# Dashed total-revenue trace on top of the stack — makes the combined trend explicit\ntotal_line = (\n    alt.Chart(totals)\n    .mark_line(color=INK, strokeDash=[6, 3], strokeWidth=2, opacity=0.5)\n    .encode(\n        x=\"Month:T\",\n        y=\"Total:Q\",\n        tooltip=[\n            alt.Tooltip(\"Month:T\", title=\"Month\", format=\"%B %Y\"),\n            alt.Tooltip(\"Total:Q\", title=\"Total Revenue ($k)\", format=\".1f\"),\n        ],\n    )\n)\n\n# Point + label calling out where the sustained decline begins\ndecline_point = (\n    alt.Chart(annotation_df)\n    .mark_point(shape=\"circle\", size=60, filled=True, color=INK, opacity=0.9)\n    .encode(x=\"Month:T\", y=\"Total:Q\")\n)\ndecline_text = (\n    alt.Chart(annotation_df)\n    .mark_text(align=\"right\", dx=-10, dy=-38, fontSize=10, fontWeight=\"bold\", color=INK)\n    .encode(x=\"Month:T\", y=\"Total:Q\", text=\"Label:N\")\n)\n\nchart = (\n    alt.layer(area_chart, total_line, decline_point, decline_text)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\"area-stacked · python · altair · anyplot.ai\", fontSize=16, anchor=\"middle\", color=INK),\n    )\n    .configure_axis(domainColor=INK_SOFT, tickColor=INK_SOFT)\n    .configure_view(stroke=None, fill=PAGE_BG)\n)\n\n# Save as PNG. Target: 3200 x 1800 (landscape).\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad (never crop) up to the exact canonical canvas — vl-convert's title/legend\n# padding lands short of the target, never over it, at this view size.\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\n# Save as HTML for interactivity\nchart.interactive().save(f\"plot-{THEME}.html\")\n"}