{"spec_id":"area-stacked-percent","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\narea-stacked-percent: 100% Stacked Area Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-05-12\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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# Okabe-Ito palette (first series ALWAYS #009E73)\nCOLORS = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\nCATEGORIES = [\"Product A\", \"Product B\", \"Product C\", \"Product D\"]\n\n# Data - Market share evolution over time\nnp.random.seed(42)\nyears = np.arange(2015, 2025)\n\n# Generate market share data with more variation to show compositional shifts\nproduct_a = 35 + np.cumsum(np.random.randn(len(years)) * 2.5)\nproduct_b = 28 + np.cumsum(np.random.randn(len(years)) * 2)\nproduct_c = 22 + np.cumsum(np.random.randn(len(years)) * 1.8)\nproduct_d = 15 + np.cumsum(np.random.randn(len(years)) * 1.5)\n\n# Ensure all values are positive\nproduct_a = np.maximum(product_a, 3)\nproduct_b = np.maximum(product_b, 3)\nproduct_c = np.maximum(product_c, 3)\nproduct_d = np.maximum(product_d, 3)\n\n# Normalize to 100%\ntotals = product_a + product_b + product_c + product_d\npct_a = (product_a / totals) * 100\npct_b = (product_b / totals) * 100\npct_c = (product_c / totals) * 100\npct_d = (product_d / totals) * 100\n\n# Calculate stacked positions\nstack_0 = np.zeros(len(years))\nstack_a = pct_a\nstack_ab = pct_a + pct_b\nstack_abc = pct_a + pct_b + pct_c\nstack_abcd = pct_a + pct_b + pct_c + pct_d  # Should be 100\n\n# Create figure\np = figure(\n    width=4800,\n    height=2700,\n    title=\"area-stacked-percent · bokeh · anyplot.ai\",\n    x_axis_label=\"Year\",\n    y_axis_label=\"Market Share (%)\",\n    x_range=(2014.5, 2024.5),\n    y_range=(0, 105),\n    toolbar_location=\"right\",\n)\n\n# Add HoverTool for Bokeh interactivity (key differentiator)\nhover = HoverTool(tooltips=[(\"Year\", \"@x{0}\"), (\"Percentage\", \"@y{0.0}%\")])\np.add_tools(hover)\n\n# Create patches for stacked areas (bottom to top)\n# Product A (bottom layer)\nsource_a = ColumnDataSource(\n    data={\"x\": np.concatenate([years, years[::-1]]), \"y\": np.concatenate([stack_a, stack_0[::-1]])}\n)\np.patch(\n    x=\"x\",\n    y=\"y\",\n    source=source_a,\n    fill_color=COLORS[0],\n    fill_alpha=0.85,\n    line_color=COLORS[0],\n    line_width=2,\n    legend_label=CATEGORIES[0],\n)\n\n# Product B\nsource_b = ColumnDataSource(\n    data={\"x\": np.concatenate([years, years[::-1]]), \"y\": np.concatenate([stack_ab, stack_a[::-1]])}\n)\np.patch(\n    x=\"x\",\n    y=\"y\",\n    source=source_b,\n    fill_color=COLORS[1],\n    fill_alpha=0.85,\n    line_color=COLORS[1],\n    line_width=2,\n    legend_label=CATEGORIES[1],\n)\n\n# Product C\nsource_c = ColumnDataSource(\n    data={\"x\": np.concatenate([years, years[::-1]]), \"y\": np.concatenate([stack_abc, stack_ab[::-1]])}\n)\np.patch(\n    x=\"x\",\n    y=\"y\",\n    source=source_c,\n    fill_color=COLORS[2],\n    fill_alpha=0.85,\n    line_color=COLORS[2],\n    line_width=2,\n    legend_label=CATEGORIES[2],\n)\n\n# Product D (top layer)\nsource_d = ColumnDataSource(\n    data={\"x\": np.concatenate([years, years[::-1]]), \"y\": np.concatenate([stack_abcd, stack_abc[::-1]])}\n)\np.patch(\n    x=\"x\",\n    y=\"y\",\n    source=source_d,\n    fill_color=COLORS[3],\n    fill_alpha=0.85,\n    line_color=COLORS[3],\n    line_width=2,\n    legend_label=CATEGORIES[3],\n)\n\n# Text sizing for 4800×2700 px\np.title.text_font_size = \"28pt\"\np.xaxis.axis_label_text_font_size = \"22pt\"\np.yaxis.axis_label_text_font_size = \"22pt\"\np.xaxis.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_font_size = \"18pt\"\n\n# Grid styling (subtle per default-style-guide.md)\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\n\n# Legend styling\np.legend.label_text_font_size = \"16pt\"\np.legend.location = \"top_right\"\np.legend.background_fill_color = ELEVATED_BG\np.legend.border_line_color = INK_SOFT\np.legend.label_text_color = INK_SOFT\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\np.title.text_color = INK\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\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\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\n\n# Axis tickers\np.xaxis.ticker = list(years)\np.yaxis.ticker = [0, 20, 40, 60, 80, 100]\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome via Selenium (required, not export_png)\nW, H = 4800, 2700\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()}\")\ntime.sleep(3)  # let bokeh's JS render the canvas\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}