{"spec_id":"area-stacked","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\narea-stacked: Stacked Area Chart\nLibrary: bokeh 3.9.2 | Python 3.13.15\nQuality: 93/100 | Updated: 2026-08-17\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove current directory from sys.path to avoid shadowing bokeh module\nif \"\" in sys.path:\n    sys.path.remove(\"\")\nif \".\" in sys.path:\n    sys.path.remove(\".\")\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, FixedTicker, HoverTool, Label, Legend\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# Imprint palette (first series is always #009E73)\nIMPRINT = [\n    \"#009E73\",  # brand green\n    \"#C475FD\",  # lavender\n    \"#4467A3\",  # blue\n    \"#BD8233\",  # ochre\n]\n\n# Data - Monthly revenue by product category over 2 years\nnp.random.seed(42)\nmonths = pd.date_range(\"2023-01-01\", periods=24, freq=\"MS\")\n\n# Generate realistic revenue data with trends\nbase_electronics = 150 + np.arange(24) * 3 + np.random.randn(24) * 15\nbase_clothing = 100 + np.sin(np.linspace(0, 4 * np.pi, 24)) * 20 + np.random.randn(24) * 10\nbase_home = 80 + np.arange(24) * 1.5 + np.random.randn(24) * 8\nbase_sports = 50 + np.cos(np.linspace(0, 4 * np.pi, 24)) * 15 + np.random.randn(24) * 5\n\n# Ensure all values are positive\nelectronics = np.maximum(base_electronics, 20)\nclothing = np.maximum(base_clothing, 15)\nhome_garden = np.maximum(base_home, 10)\nsports = np.maximum(base_sports, 8)\n\n# Order series by size (largest at bottom for better reading)\n# Average values: Electronics (150+), Clothing (100+), Home (80+), Sports (50+)\nseries_data = [(\"Electronics\", electronics), (\"Clothing\", clothing), (\"Home & Garden\", home_garden), (\"Sports\", sports)]\n\nx_values = np.arange(len(months))\nx_labels = [d.strftime(\"%b %Y\") for d in months]\n\n# Total-revenue trend line, used below as a focal-point overlay tracing the\n# combined stack instead of leaving the top edge to speak for itself.\ntotal = sum(values for _, values in series_data)\ngrowth_pct = (total[-1] / total[0] - 1) * 100\n\n# Create figure\ntitle = \"area-stacked · python · bokeh · anyplot.ai\"\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Month\",\n    y_axis_label=\"Revenue ($K)\",\n    x_range=(-0.5, 23.5),\n    y_range=(0, total.max() * 1.18),\n    toolbar_location=None,  # avoids the ~30-50px toolbar band shrinking the PNG below 3200x1800\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Stack the series bottom-to-top with bokeh's purpose-built varea_stack()\n# helper: it derives the running y1/y2 bounds from the source columns itself\n# and tags each renderer's `name` with the stacker column, which HoverTool's\n# special `$name` variable then resolves per-band without a manual loop.\ncategory_names = [name for name, _ in series_data]\nsource = ColumnDataSource(data={\"x\": x_values, \"month\": x_labels, **dict(series_data)})\nrenderers = p.varea_stack(stackers=category_names, x=\"x\", color=IMPRINT, fill_alpha=0.85, source=source)\nlegend_items = [(name, [renderer]) for name, renderer in zip(category_names, renderers, strict=True)]\n\nhover = HoverTool(\n    renderers=renderers, tooltips=[(\"Month\", \"@month\"), (\"Category\", \"$name\"), (\"Value\", \"@$name{0,0} $K\")]\n)\np.add_tools(hover)\n\n# Total-revenue trace: a thin neutral dashed line along the stack's top edge,\n# with a marker + callout on the final month. Gives the composition a single\n# explicit focal point (overall growth) on top of the implicit stacking story.\ntotal_source = ColumnDataSource(data={\"x\": x_values, \"y\": total, \"month\": x_labels})\ntotal_line = p.line(x=\"x\", y=\"y\", source=total_source, line_color=INK, line_alpha=0.55, line_width=3, line_dash=[10, 6])\np.add_tools(HoverTool(renderers=[total_line], tooltips=[(\"Month\", \"@month\"), (\"Total\", \"@y{0,0} $K\")]))\np.scatter(x=[x_values[-1]], y=[total[-1]], size=16, fill_color=INK, line_color=PAGE_BG, line_width=2)\n\ngrowth_label = Label(\n    x=x_values[-1] - 5.5,\n    y=total[-1] + total.max() * 0.045,\n    text=f\"Total +{growth_pct:.0f}% over 2 years\",\n    text_font_size=\"26pt\",\n    text_color=INK,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.9,\n    border_line_color=INK_SOFT,\n    padding=12,\n)\np.add_layout(growth_label)\n\n# Add legend\nlegend = Legend(items=legend_items, location=\"top_left\")\nlegend.label_text_font_size = \"34pt\"\nlegend.glyph_height = 46\nlegend.glyph_width = 46\nlegend.spacing = 15\nlegend.padding = 20\nlegend.background_fill_color = ELEVATED_BG\nlegend.background_fill_alpha = 0.9\nlegend.border_line_color = INK_SOFT\nlegend.label_text_color = INK_SOFT\np.add_layout(legend, \"right\")\n\n# Style text sizes for large canvas\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\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# Custom x-axis tick labels (show every 3 months)\np.xaxis.ticker = FixedTicker(ticks=[0, 3, 6, 9, 12, 15, 18, 21, 23])\np.xaxis.major_label_overrides = {i: x_labels[i] for i in range(len(x_labels))}\np.xaxis.major_label_orientation = 0.6\n\n# Grid styling\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\n\n# Background and borders\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\n# Axis styling\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# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome\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)\n\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 (reserves a phantom\n# title-bar height even headless), so 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"}