{"spec_id":"area-stacked","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\narea-stacked: Stacked Area Chart\nLibrary: seaborn 0.13.2 | Python 3.13.15\nQuality: 94/100 | Updated: 2026-08-17\n\"\"\"\n\nimport os\n\nimport matplotlib.dates as mdates\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nimport seaborn.objects as so\n\n\n# Theme tokens (see prompts/default-style-guide.md \"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# Imprint palette — canonical order, first series always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\nsns.set_palette(IMPRINT_PALETTE)\n\n# Data: monthly energy consumption by sector over two years, with seasonality\nnp.random.seed(42)\nmonths = pd.date_range(\"2024-01\", periods=24, freq=\"ME\")\n\n# Industrial is baseload-driven, so its seasonal swing stays flat; residential\n# is weather-driven, so its winter/summer peaks are cut sharper via a cubed wave.\nindustrial_base = 48 + np.sin(np.linspace(0, 4 * np.pi, 24)) * 2.5\nresidential_wave = np.sin(np.linspace(np.pi, 5 * np.pi, 24))\nresidential_base = 34 + np.sign(residential_wave) * np.abs(residential_wave) ** 0.6 * 10\ncommercial_base = 26 + np.sin(np.linspace(0.6, 4.6 * np.pi, 24)) * 5\ntransport_base = 16 + np.sin(np.linspace(1.2, 5.2 * np.pi, 24)) * 3\nagriculture_base = 9 + np.sin(np.linspace(1.8, 5.8 * np.pi, 24)) * 2\n\ngrowth = np.linspace(1.0, 1.18, 24)\nindustrial = (industrial_base * growth + np.random.randn(24) * 1.5).clip(30)\nresidential = (residential_base * growth + np.random.randn(24) * 1.5).clip(15)\ncommercial = (commercial_base * growth + np.random.randn(24) * 1.2).clip(12)\ntransport = (transport_base * growth + np.random.randn(24) * 0.8).clip(8)\nagriculture = (agriculture_base * growth + np.random.randn(24) * 0.5).clip(4)\n\nsectors = [\"Industrial\", \"Residential\", \"Commercial\", \"Transport\", \"Agriculture\"]\nseries = [industrial, residential, commercial, transport, agriculture]\n\n# Long-form frame for the seaborn.objects interface below — ordered by size\n# (largest first) so so.Stack() lays Industrial at the baseline, per spec.\nlong_df = pd.DataFrame(\n    {\"month\": np.tile(months, len(sectors)), \"sector\": np.repeat(sectors, len(months)), \"value\": np.concatenate(series)}\n)\nlong_df[\"sector\"] = pd.Categorical(long_df[\"sector\"], categories=sectors, ordered=True)\n\n# Plot — see default-style-guide.md \"Visual Sizing Defaults\" for canvas + sizing\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Stacked bands via the seaborn.objects interface (so.Area + so.Stack) — the\n# genuinely seaborn-native way to build a stacked area chart, rather than\n# reaching for matplotlib's ax.stackplot(). Rendered onto our own pre-sized\n# Axes so the Step-0 canvas contract still holds.\n(\n    so.Plot(long_df, x=\"month\", y=\"value\", color=\"sector\")\n    .add(so.Area(alpha=0.85, edgewidth=0.6, edgecolor=PAGE_BG), so.Stack())\n    .scale(color=IMPRINT_PALETTE[: len(sectors)])\n    .on(ax)\n    .plot()\n)\n# so.Plot always attaches its own legend to the *figure*; hide it and build an\n# axes-level legend instead so sns.move_legend (an Axes/Figure-only helper)\n# and the outside-right docking below behave exactly as on other libraries.\nfig.legends[0].set_visible(False)\n\n# A crisp ink-colored line traces the cumulative total for emphasis, drawn via\n# seaborn's own lineplot (not raw ax.plot) so the overlay is genuinely seaborn.\ntotal = np.sum(series, axis=0)\ntotal_df = pd.DataFrame({\"month\": months, \"total\": total})\nsns.lineplot(data=total_df, x=\"month\", y=\"total\", ax=ax, color=INK, linewidth=1.2, alpha=0.6, linestyle=(0, (1, 1.5)))\n\nax.set_xlabel(\"Month\", fontsize=12, color=INK)\nax.set_ylabel(\"Consumption (GWh)\", fontsize=12, color=INK)\nax.set_title(\"area-stacked · python · seaborn · anyplot.ai\", fontsize=13, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=10, colors=INK_SOFT)\n\nax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))\nax.xaxis.set_major_formatter(mdates.DateFormatter(\"%b %Y\"))\nplt.setp(ax.xaxis.get_majorticklabels(), rotation=40, ha=\"right\")\n\n# Legend sits outside the stacked area (fully filled top-to-bottom, no clear\n# gap to dock a legend inside) so it never occludes data. Handles are rebuilt\n# as flat swatches (so.Area's own legend proxies inherit its 0.85 fill alpha,\n# which reads muddier at legend-swatch size) and positioned with seaborn's\n# move_legend — a seaborn-only convenience for repositioning/restyling a\n# legend in one call — kept borderless for a lighter visual treatment.\nlegend_handles = [\n    mpatches.Patch(facecolor=color, label=sector) for color, sector in zip(IMPRINT_PALETTE, sectors, strict=True)\n]\nax.legend(handles=legend_handles, title=\"Sector\")\nsns.move_legend(\n    ax, \"upper left\", bbox_to_anchor=(1.01, 1.0), frameon=False, fontsize=9, title_fontsize=10, labelcolor=INK\n)\nax.get_legend().get_title().set_color(INK)\n\n# Subtle y-axis grid only, per style guide\nax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)\nax.set_axisbelow(True)\n\n# L-shaped frame\nsns.despine(ax=ax)\nax.spines[\"left\"].set_color(INK_SOFT)\nax.spines[\"bottom\"].set_color(INK_SOFT)\n\nax.set_ylim(bottom=0)\nax.margins(x=0)\n\n# Callout highlighting the key trend: total consumption growth over the\n# window. Anchored close to the final data point (short, local arrow) rather\n# than sweeping across the whole width, so it can't be mistaken for a second\n# trend line following a path the data doesn't actually take.\ngrowth_pct = (total[-1] - total[0]) / total[0] * 100\ntop = ax.get_ylim()[1]\nax.annotate(\n    f\"+{growth_pct:.0f}% growth over two years\",\n    xy=(months[-1], total[-1]),\n    xytext=(months[-9], top * 0.94),\n    fontsize=9,\n    color=INK,\n    ha=\"left\",\n    va=\"bottom\",\n    arrowprops={\"arrowstyle\": \"->\", \"color\": INK_SOFT, \"alpha\": 0.7, \"connectionstyle\": \"arc3,rad=0.1\"},\n)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}