{"spec_id":"bar-stacked-percent","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nbar-stacked-percent: 100% Stacked Bar Chart\nLibrary: bokeh 3.9.2 | Python 3.13.15\nQuality: 94/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Workaround: Remove current directory from import path to avoid circular import\n# when the file bokeh.py conflicts with the bokeh package name\noriginal_path = sys.path.copy()\nsys.path = [p for p in sys.path if p != \"\" and not (os.path.isfile(os.path.join(p, \"bokeh.py\")) if p else False)]\n\ntry:\n    import pandas as pd\n    from bokeh.io import output_file, save\n    from bokeh.models import ColumnDataSource, FixedTicker, LabelSet\n    from bokeh.plotting import figure\n    from selenium import webdriver\n    from selenium.webdriver.chrome.options import Options\nfinally:\n    sys.path = original_path\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# Data: Market share of smartphone brands over quarters\ncategories = [\"Q1 2024\", \"Q2 2024\", \"Q3 2024\", \"Q4 2024\", \"Q1 2025\"]\ncomponents = [\"Apple\", \"Samsung\", \"Xiaomi\", \"Others\"]\n\nraw_data = {\n    \"Apple\": [28, 25, 22, 31, 27],\n    \"Samsung\": [23, 24, 26, 22, 24],\n    \"Xiaomi\": [14, 16, 18, 15, 17],\n    \"Others\": [35, 35, 34, 32, 32],\n}\n\n# Calculate percentages (already sum to 100, but normalize for safety)\ndf = pd.DataFrame(raw_data, index=categories)\ntotals = df.sum(axis=1)\ndf_percent = df.div(totals, axis=0) * 100\n\n# Calculate bottom positions for stacking\nbottoms = {}\ncumulative = [0.0] * len(categories)\nfor comp in components:\n    bottoms[comp] = cumulative.copy()\n    cumulative = [c + v for c, v in zip(cumulative, df_percent[comp], strict=True)]\n\n# Numeric x positions (not a categorical FactorRange) so the flow ribbons\n# below can interpolate between bar edges with real coordinates.\nx_positions = list(range(len(categories)))\nBAR_WIDTH = 0.6\n\n# Create figure — `width`/`height` are the TOTAL canvas; min_border_* reserves\n# room for the 34-42pt tick/axis-label stack so nothing clips at the edges.\np = figure(\n    width=3200,\n    height=1800,\n    x_range=(-0.5, len(categories) - 0.5),\n    y_range=(0, 100),\n    title=\"bar-stacked-percent · bokeh · anyplot.ai\",\n    toolbar_location=None,  # default toolbar shrinks the saved PNG below target height\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\np.xaxis.ticker = FixedTicker(ticks=x_positions)\np.xaxis.major_label_overrides = dict(zip(x_positions, categories, strict=True))\n\n# Flow ribbons between adjacent bars: a low-alpha quad tracing each\n# component's segment boundary from one bar's right edge to the next bar's\n# left edge. Drawn *before* the bars so the bars sit on top and the ribbons\n# only show in the inter-bar gaps — this turns the quarter-to-quarter share\n# change into a visible continuity cue instead of four disconnected columns,\n# without adding any spec-unrequested text annotation.\nfor i, comp in enumerate(components):\n    for j in range(len(categories) - 1):\n        left_x = x_positions[j] + BAR_WIDTH / 2\n        right_x = x_positions[j + 1] - BAR_WIDTH / 2\n        left_bottom, left_top = bottoms[comp][j], bottoms[comp][j] + df_percent[comp].iloc[j]\n        right_bottom, right_top = bottoms[comp][j + 1], bottoms[comp][j + 1] + df_percent[comp].iloc[j + 1]\n        p.patch(\n            x=[left_x, right_x, right_x, left_x],\n            y=[left_bottom, right_bottom, right_top, left_top],\n            fill_color=IMPRINT[i],\n            fill_alpha=0.18,\n            line_color=None,\n        )\n\n# Draw stacked bars\nrenderers = []\nfor i, comp in enumerate(components):\n    source = ColumnDataSource(\n        data={\n            \"x\": x_positions,\n            \"top\": [b + v for b, v in zip(bottoms[comp], df_percent[comp], strict=True)],\n            \"bottom\": bottoms[comp],\n            \"value\": df_percent[comp].tolist(),\n        }\n    )\n    r = p.vbar(\n        x=\"x\",\n        top=\"top\",\n        bottom=\"bottom\",\n        source=source,\n        width=BAR_WIDTH,\n        color=IMPRINT[i],\n        legend_label=comp,\n        line_color=PAGE_BG,\n        line_width=3,\n    )\n    renderers.append(r)\n\n# Move the auto-built legend outside the plot frame (dedicated right column)\n# so it never overlaps the rightmost bar's segment labels — with 5 categories\n# the \"top_right\" in-frame position sat directly on top of the Q1 2025 bar.\np.add_layout(p.legend[0], \"right\")\n\n# Add percentage labels inside each segment\nfor i, comp in enumerate(components):\n    values = df_percent[comp].tolist()\n    mids = [(b + b + v) / 2 for b, v in zip(bottoms[comp], values, strict=True)]\n\n    # Only show labels for segments >= 10%\n    labels = [f\"{v:.0f}%\" if v >= 10 else \"\" for v in values]\n\n    label_source = ColumnDataSource(data={\"x\": x_positions, \"y\": mids, \"text\": labels})\n\n    # Text color: white on dark colors (first series), INK on light colors\n    text_color = \"white\" if i == 0 else INK\n\n    label_set = LabelSet(\n        x=\"x\",\n        y=\"y\",\n        text=\"text\",\n        source=label_source,\n        text_align=\"center\",\n        text_baseline=\"middle\",\n        text_font_size=\"30pt\",\n        text_color=text_color,\n        text_font_style=\"bold\",\n    )\n    p.add_layout(label_set)\n\n# Styling for 3200x1800 canvas — see prompts/library/bokeh.md \"Sizing\"\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.xaxis.axis_label = \"Quarter\"\np.yaxis.axis_label = \"Market Share (%)\"\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\n# Axis colors\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\n# Grid styling\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK_SOFT\np.ygrid.grid_line_alpha = 0.10\n\n# Background and border\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Legend styling (positioned in the dedicated right column via add_layout above)\np.legend.label_text_font_size = \"34pt\"\np.legend.label_text_color = INK_SOFT\np.legend.background_fill_color = ELEVATED_BG\np.legend.background_fill_alpha = 0.95\np.legend.border_line_color = INK_SOFT\np.legend.glyph_width = 40\np.legend.glyph_height = 40\np.legend.spacing = 12\np.legend.padding = 16\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome via Selenium\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\n# Headless Chrome's --window-size sets the OUTER window, which still reserves\n# a phantom title-bar height even headless — pin the viewport exactly via CDP.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}