{"spec_id":"bar-grouped","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nbar-grouped: Grouped Bar Chart\nLibrary: bokeh 3.9.2 | Python 3.13.14\nQuality: 95/100 | Updated: 2026-08-05\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, FactorRange, Legend, LegendItem\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (Imprint)\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 categorical palette (first series is always #009E73)\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data - quarterly revenue by product line (in thousands of USD)\ncategories = [\"Q1\", \"Q2\", \"Q3\", \"Q4\"]\ngroups = [\"Electronics\", \"Clothing\", \"Home & Garden\"]\ndata = {\"Electronics\": [245, 278, 312, 385], \"Clothing\": [180, 165, 210, 295], \"Home & Garden\": [125, 198, 245, 178]}\ngroup_colors = IMPRINT_PALETTE[: len(groups)]\n\n# Nested (category, group) factors for the grouped categorical axis\nx = [(cat, group) for cat in categories for group in groups]\nvalues = [data[group][categories.index(cat)] for cat, group in x]\nbar_colors = [group_colors[groups.index(group)] for _cat, group in x]\n\n# Highlight the single largest bar so the peak reads as a focal point, not just\n# another data point — addresses the \"no visual hierarchy\" review note.\npeak_i = max(range(len(values)), key=lambda i: values[i])\npeak_factor = x[peak_i]\npeak_value = values[peak_i]\n\nsource = ColumnDataSource(\n    data={\n        \"x\": x,\n        \"values\": values,\n        \"color\": bar_colors,\n        \"line_color\": [INK if i == peak_i else PAGE_BG for i in range(len(x))],\n        \"line_width\": [4 if i == peak_i else 2 for i in range(len(x))],\n    }\n)\n\ntitle = \"Quarterly Revenue by Product · bar-grouped · python · bokeh · anyplot.ai\"\n\np = figure(\n    x_range=FactorRange(*x, group_padding=0.4, factor_padding=0.08),\n    width=3200,\n    height=1800,\n    title=title,\n    toolbar_location=None,  # bokeh's default toolbar adds ~30-50px above the canvas\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\nbars = p.vbar(\n    x=\"x\", top=\"values\", width=0.82, source=source, fill_color=\"color\", line_color=\"line_color\", line_width=\"line_width\"\n)\n\n# Value labels on top of every bar\nfor factor, value in zip(x, values, strict=True):\n    is_peak = factor == peak_factor\n    p.text(\n        x=[factor],\n        y=[value + 8],\n        text=[f\"${value}K\"],\n        text_align=\"center\",\n        text_baseline=\"bottom\",\n        text_font_size=\"28pt\" if is_peak else \"24pt\",\n        text_font_style=\"bold\" if is_peak else \"normal\",\n        text_color=INK,\n    )\n\n# Callout marking the peak quarter as the focal point of the chart\np.text(\n    x=[peak_factor],\n    y=[peak_value + 60],\n    text=[\"Peak quarter\"],\n    text_align=\"center\",\n    text_baseline=\"bottom\",\n    text_font_size=\"20pt\",\n    text_font_style=\"bold\",\n    text_color=IMPRINT_PALETTE[0],\n)\n\n# Title styling\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\n\n# X-axis styling — the leaf-level tick labels (Electronics/Clothing/Home &\n# Garden repeated under every bar) are redundant with the legend and collide\n# at this canvas width, so hide them and show only the Q1-Q4 group labels.\np.xaxis.axis_label = \"Quarter\"\np.xaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"0pt\"\np.xaxis.group_text_font_size = \"34pt\"\np.xaxis.group_text_color = INK_SOFT\np.xaxis.group_label_orientation = \"horizontal\"\np.xaxis.separator_line_color = INK_SOFT\np.xaxis.separator_line_alpha = 0.3\n\n# Y-axis styling\np.yaxis.axis_label = \"Revenue ($ Thousands)\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_color = INK_SOFT\np.yaxis.axis_label_text_color = INK\n\n# Grid styling — y-axis only, subtle\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.12\n\n# Background and L-shaped frame (drop the boxed outline, keep left/bottom axis lines)\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\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# Legend with correct color swatch for each group (indices 0-2 are the three\n# bars of the first category, one per group, in canonical order)\nlegend_items = [\n    LegendItem(label=groups[0], renderers=[bars], index=0),\n    LegendItem(label=groups[1], renderers=[bars], index=1),\n    LegendItem(label=groups[2], renderers=[bars], index=2),\n]\nlegend = Legend(items=legend_items, location=\"top_right\", orientation=\"vertical\")\nlegend.label_text_font_size = \"34pt\"\nlegend.label_text_color = INK_SOFT\nlegend.background_fill_color = ELEVATED_BG\nlegend.background_fill_alpha = 1.0\nlegend.border_line_color = INK_SOFT\nlegend.glyph_height = 34\nlegend.glyph_width = 34\nlegend.spacing = 18\nlegend.padding = 20\np.add_layout(legend)\n\n# Y-axis range with headroom for the value labels and the peak callout\np.y_range.start = 0\np.y_range.end = round(max(values) * 1.32)\n\n# Save HTML output (required catalog artifact)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome (Selenium) — bokeh.io.export_png is unreliable\n# in this environment, see prompts/library/bokeh.md.\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# Pin the viewport exactly via CDP — headless Chrome's --window-size sets the\n# OUTER window and still reserves a phantom title-bar height even headless.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)  # let bokeh's JS render the canvas\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}