{"spec_id":"bar-stacked-percent","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nbar-stacked-percent: 100% Stacked Bar Chart\nLibrary: altair 6.2.2 | Python 3.13.15\nQuality: 93/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\n\nimport altair as alt\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 palette (first series is always #009E73)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Stack/legend order (must match the color domain so segment position is\n# traceable to the legend swatch above it)\nSOURCE_ORDER = [\"Fossil Fuels\", \"Nuclear\", \"Renewables\", \"Hydro\"]\n\n# Data - Energy mix by country\ndata = pd.DataFrame(\n    {\n        \"Country\": [\n            \"USA\",\n            \"USA\",\n            \"USA\",\n            \"USA\",\n            \"China\",\n            \"China\",\n            \"China\",\n            \"China\",\n            \"Germany\",\n            \"Germany\",\n            \"Germany\",\n            \"Germany\",\n            \"Brazil\",\n            \"Brazil\",\n            \"Brazil\",\n            \"Brazil\",\n            \"India\",\n            \"India\",\n            \"India\",\n            \"India\",\n        ],\n        \"Source\": [\"Fossil Fuels\", \"Nuclear\", \"Renewables\", \"Hydro\"] * 5,\n        \"Value\": [\n            60,\n            18,\n            15,\n            7,  # USA\n            65,\n            5,\n            18,\n            12,  # China\n            40,\n            12,\n            38,\n            10,  # Germany\n            15,\n            3,\n            12,\n            70,  # Brazil\n            72,\n            3,\n            18,\n            7,\n        ],  # India\n    }\n)\n\n# Rank each source to match the legend's domain order, so the stack reads\n# bottom-to-top in the same sequence as the legend top-to-bottom.\norder_map = {source: i for i, source in enumerate(SOURCE_ORDER)}\ndata[\"SourceOrder\"] = data[\"Source\"].map(order_map)\n\n# In-segment percentage labels for segments wide enough to hold text;\n# narrow slivers (e.g. Nuclear at 3-5%) are left unlabeled.\ndata[\"Label\"] = data[\"Value\"].apply(lambda v: f\"{v:.0f}%\" if v >= 10 else \"\")\n\n# Segment midpoint (as a 0-1 fraction of the stack) for centering labels;\n# rows are already ordered Fossil Fuels/Nuclear/Renewables/Hydro per country,\n# matching SOURCE_ORDER, so a plain cumsum reproduces the bars' stack order.\ngroup_total = data.groupby(\"Country\")[\"Value\"].transform(\"sum\")\ncum_end = data.groupby(\"Country\")[\"Value\"].cumsum() / group_total\ncum_start = cum_end - data[\"Value\"] / group_total\ndata[\"Mid\"] = (cum_start + cum_end) / 2\n\n# 100% stacked bars\nbars = (\n    alt.Chart(data)\n    .mark_bar(stroke=\"white\", strokeWidth=1)\n    .encode(\n        x=alt.X(\"Country:N\", axis=alt.Axis(labelFontSize=10, titleFontSize=12, labelAngle=0), title=\"Country\"),\n        y=alt.Y(\n            \"Value:Q\",\n            stack=\"normalize\",\n            axis=alt.Axis(labelFontSize=10, titleFontSize=12, format=\"%\"),\n            title=\"Share of Energy Mix (%)\",\n        ),\n        color=alt.Color(\n            \"Source:N\",\n            scale=alt.Scale(domain=SOURCE_ORDER, range=IMPRINT),\n            legend=alt.Legend(\n                title=\"Energy Source\",\n                titleFontSize=10,\n                labelFontSize=10,\n                orient=\"right\",\n                symbolSize=80,\n                symbolStrokeWidth=0,\n            ),\n        ),\n        order=alt.Order(\"SourceOrder:Q\", sort=\"ascending\"),\n        tooltip=[\n            alt.Tooltip(\"Country:N\", title=\"Country\"),\n            alt.Tooltip(\"Source:N\", title=\"Source\"),\n            alt.Tooltip(\"Value:Q\", title=\"Value\", format=\".1f\"),\n        ],\n    )\n)\n\n# Percentage labels centered within each segment via the precomputed midpoint\nlabels = (\n    alt.Chart(data)\n    .mark_text(fontSize=9, fontWeight=\"bold\", color=\"#FFFFFF\")\n    .encode(\n        x=alt.X(\"Country:N\"),\n        y=alt.Y(\"Mid:Q\", title=\"Share of Energy Mix (%)\", scale=alt.Scale(domain=[0, 1])),\n        text=alt.Text(\"Label:N\"),\n    )\n)\n\n# Create 100% stacked bar chart\nchart = (\n    (bars + labels)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\"bar-stacked-percent · python · altair · anyplot.ai\", fontSize=16, anchor=\"middle\", color=INK),\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0)\n    .configure_axis(\n        domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.15, labelColor=INK_SOFT, titleColor=INK\n    )\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save PNG and HTML\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n\n# Pad the saved PNG up to the canonical landscape canvas (3200x1800).\n# vl-convert pads the view with title/axis/legend extents outside width/height,\n# so the raw save rarely lands exactly on target - never crop, only pad.\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"}