{"spec_id":"bar-stacked-percent","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nbar-stacked-percent: 100% Stacked Bar Chart\nLibrary: plotnine 0.15.8 | Python 3.13.15\nQuality: 88/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\nimport sys\n\n\nsys.path = [p for p in sys.path if os.path.abspath(p) != os.getcwd()]\n\nimport pandas as pd\nfrom mizani.formatters import percent_format\nfrom plotnine import (\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_bar,\n    geom_text,\n    ggplot,\n    labs,\n    position_fill,\n    scale_color_identity,\n    scale_fill_manual,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Background\" + \"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\"\nMUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"  # Imprint semantic anchor: other/rest\n\n# Imprint palette — named competitors take positions 1-3 in canonical order;\n# \"Others\" uses the muted semantic anchor since it is literally the aggregate\n# rest-of-market bucket, not a distinct company.\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data - smartphone market share by quarter\nquarters = [\"Q1 2023\", \"Q2 2023\", \"Q3 2023\", \"Q4 2023\", \"Q1 2024\", \"Q2 2024\"]\ncompanies_ordered = [\"Others\", \"Xiaomi\", \"Samsung\", \"Apple\"]\napple_share = [23, 21, 20, 22, 21, 20]\nsamsung_share = [22, 21, 20, 19, 20, 19]\nxiaomi_share = [12, 13, 14, 14, 15, 16]\nothers_share = [43, 45, 46, 45, 44, 45]\ndata = {\n    \"Quarter\": quarters * 4,\n    \"Company\": ([\"Apple\"] * 6 + [\"Samsung\"] * 6 + [\"Xiaomi\"] * 6 + [\"Others\"] * 6),\n    \"Share\": apple_share + samsung_share + xiaomi_share + others_share,\n}\ndf = pd.DataFrame(data)\n\n# Set categorical ordering for proper display\ndf[\"Quarter\"] = pd.Categorical(df[\"Quarter\"], categories=quarters, ordered=True)\ndf[\"Company\"] = pd.Categorical(df[\"Company\"], categories=companies_ordered, ordered=True)\n\n# Color mapping: Apple/Samsung/Xiaomi in canonical Imprint order, Others muted\ncolor_map = {\"Others\": MUTED, \"Xiaomi\": IMPRINT[2], \"Samsung\": IMPRINT[1], \"Apple\": IMPRINT[0]}\n\n# In-segment percentage labels (DE-03): pick whichever ink extreme has higher\n# WCAG contrast against each segment's own fill color, so labels stay legible\n# on both the mid-tone brand hues and the theme-adaptive \"Others\" gray.\nLIGHT_INK = \"#F0EFE8\"\nDARK_INK = \"#1A1A17\"\n\n\ndef _relative_luminance(hex_color):\n    r, g, b = (int(hex_color[i : i + 2], 16) / 255 for i in (1, 3, 5))\n\n    def _linearize(c):\n        return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4\n\n    r, g, b = _linearize(r), _linearize(g), _linearize(b)\n    return 0.2126 * r + 0.7152 * g + 0.0722 * b\n\n\ndef _contrast_ratio(l1, l2):\n    lighter, darker = max(l1, l2), min(l1, l2)\n    return (lighter + 0.05) / (darker + 0.05)\n\n\ndef _label_color(fill_hex):\n    fill_l = _relative_luminance(fill_hex)\n    light_contrast = _contrast_ratio(fill_l, _relative_luminance(LIGHT_INK))\n    dark_contrast = _contrast_ratio(fill_l, _relative_luminance(DARK_INK))\n    return LIGHT_INK if light_contrast >= dark_contrast else DARK_INK\n\n\ndf[\"Label\"] = df[\"Share\"].astype(str) + \"%\"\ndf[\"LabelColor\"] = df[\"Company\"].map(color_map).map(_label_color)\n\n# Precompute label y-positions explicitly (SC-03/VQ-02 fix): geom_bar and\n# geom_text each calling their own independent position_fill() can derive\n# mismatched per-group cumulative offsets when the source rows are grouped\n# by company rather than interleaved per quarter. Instead, compute the exact\n# fill-stack midpoint per (Quarter, Company) ourselves -- stacked bottom-to-top\n# as Apple/Samsung/Xiaomi/Others, i.e. the reverse of the legend/factor order,\n# matching plotnine's default stacking -- and feed it to geom_text via\n# position=\"identity\" so both layers are guaranteed to agree.\nstack_order_bottom_to_top = [\"Apple\", \"Samsung\", \"Xiaomi\", \"Others\"]\nstack_rank = {company: rank for rank, company in enumerate(stack_order_bottom_to_top)}\n# .map() on a Categorical column returns a Categorical result that inherits\n# the *original* category order, so sorting by it would sort by category\n# position rather than by the mapped rank value -- cast to plain strings\n# first so the mapped ranks are ordinary integers.\ndf[\"StackRank\"] = df[\"Company\"].astype(str).map(stack_rank)\ndf = df.sort_values([\"Quarter\", \"StackRank\"]).reset_index(drop=True)\ndf[\"Fraction\"] = df[\"Share\"] / 100\ncum_top = df.groupby(\"Quarter\", observed=True)[\"Fraction\"].cumsum()\ncum_bottom = cum_top - df[\"Fraction\"]\ndf[\"LabelY\"] = (cum_top + cum_bottom) / 2\n\n# Theme-adaptive chrome\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_border=element_blank(),\n    panel_grid_major_x=element_blank(),\n    panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.10),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=10),\n    axis_text=element_text(color=INK_SOFT, size=8),\n    axis_line=element_line(color=INK_SOFT, size=0.5),\n    plot_title=element_text(color=INK, size=12),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_title=element_text(color=INK, size=8),\n    legend_text=element_text(color=INK_SOFT, size=8),\n    legend_position=\"right\",\n    figure_size=(8, 4.5),\n)\n\n# Create 100% stacked bar chart with in-segment percentage labels\nplot = (\n    ggplot(df, aes(x=\"Quarter\", y=\"Share\", fill=\"Company\"))\n    + geom_bar(stat=\"identity\", position=position_fill(), width=0.7)\n    + geom_text(aes(y=\"LabelY\", label=\"Label\", color=\"LabelColor\"), position=\"identity\", size=2.8, show_legend=False)\n    + scale_fill_manual(values=color_map)\n    + scale_color_identity()\n    + scale_y_continuous(labels=percent_format())\n    + labs(title=\"bar-stacked-percent · python · plotnine · anyplot.ai\", x=\"Quarter\", y=\"Market Share\", fill=\"Company\")\n    + theme_minimal()\n    + anyplot_theme\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}