{"spec_id":"bar-diverging","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nbar-diverging: Diverging Bar Chart\nLibrary: bokeh 3.9.2 | Python 3.13.15\nQuality: 91/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Prevent this file (bokeh.py) from shadowing the installed bokeh package\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if not (p and os.path.abspath(p) == _this_dir)]\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, LabelSet, Span\nfrom bokeh.plotting import figure\nfrom PIL import Image\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens — Imprint palette\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\nBRAND_POS = \"#009E73\"  # Imprint position 1 — brand green, positive / promoter\nBRAND_NEG = \"#AE3030\"  # Imprint position 5 — semantic anchor for negative / detractor\n\n# Data — Customer satisfaction survey responses (Net Promoter Score style)\ncategories = [\n    \"Product Quality\",\n    \"Customer Service\",\n    \"Delivery Speed\",\n    \"Website Experience\",\n    \"Price Value\",\n    \"Return Policy\",\n    \"Mobile App\",\n    \"Warranty Service\",\n    \"Tech Support\",\n    \"Packaging\",\n]\n\n# Net satisfaction scores: positive = more promoters, negative = more detractors\nvalues = [45, 32, -15, 28, -8, 52, -22, 18, -35, 12]\n\n# Sort by value for pattern recognition\nsorted_data = sorted(zip(categories, values, strict=True), key=lambda x: x[1])\ncategories_sorted = [item[0] for item in sorted_data]\nvalues_sorted = [item[1] for item in sorted_data]\n\ncolors = [BRAND_POS if v >= 0 else BRAND_NEG for v in values_sorted]\nvalue_fmt = [f\"+{v}\" if v >= 0 else str(v) for v in values_sorted]\n\nsource = ColumnDataSource(\n    data={\"category\": categories_sorted, \"value\": values_sorted, \"color\": colors, \"value_fmt\": value_fmt}\n)\n\n# Positive/negative value labels need opposite text alignment — split by sign\nlabel_pos_src = ColumnDataSource(\n    data={\n        \"category\": [c for c, v in zip(categories_sorted, values_sorted, strict=True) if v >= 0],\n        \"value\": [v for v in values_sorted if v >= 0],\n        \"text\": [f\"+{v}\" for v in values_sorted if v >= 0],\n    }\n)\nlabel_neg_src = ColumnDataSource(\n    data={\n        \"category\": [c for c, v in zip(categories_sorted, values_sorted, strict=True) if v < 0],\n        \"value\": [v for v in values_sorted if v < 0],\n        \"text\": [str(v) for v in values_sorted if v < 0],\n    }\n)\n\n# Plot — 3200×1800 landscape canvas per hard contract\np = figure(\n    width=3200,\n    height=1800,\n    y_range=categories_sorted,\n    x_range=(-52, 65),\n    title=\"bar-diverging · bokeh · anyplot.ai\",\n    x_axis_label=\"Net Satisfaction Score\",\n    y_axis_label=\"Category\",\n    toolbar_location=None,  # prevents toolbar from shrinking canvas height\n    min_border_bottom=160,  # room for 34pt x-ticks + 42pt x-axis label\n    min_border_left=440,  # room for categorical y-axis labels + 42pt axis label\n    min_border_top=110,  # room for 50pt title\n    min_border_right=50,\n)\n\n# Horizontal bars diverging from zero (better for long category labels)\nbars = p.hbar(\n    y=\"category\", right=\"value\", left=0, height=0.62, color=\"color\", source=source, line_color=PAGE_BG, line_width=1.5\n)\n\n# HoverTool for interactive HTML exploration — distinctive Bokeh feature\nhover = HoverTool(renderers=[bars], tooltips=[(\"Category\", \"@category\"), (\"Net score\", \"@value_fmt\")])\np.add_tools(hover)\n\n# Value labels at bar ends, aligned away from the zero baseline\npos_labels = LabelSet(\n    x=\"value\",\n    y=\"category\",\n    text=\"text\",\n    source=label_pos_src,\n    text_font_size=\"24pt\",\n    text_color=INK_SOFT,\n    text_align=\"left\",\n    x_offset=14,\n    y_offset=-3,\n)\np.add_layout(pos_labels)\n\nneg_labels = LabelSet(\n    x=\"value\",\n    y=\"category\",\n    text=\"text\",\n    source=label_neg_src,\n    text_font_size=\"24pt\",\n    text_color=INK_SOFT,\n    text_align=\"right\",\n    x_offset=-14,\n    y_offset=-3,\n)\np.add_layout(neg_labels)\n\n# Zero baseline — the diverging focal point\nzero_line = Span(location=0, dimension=\"height\", line_color=INK, line_width=2.5, line_dash=\"solid\")\np.add_layout(zero_line)\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_font_size = \"50pt\"\np.title.text_color = INK\n\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 = \"30pt\"  # slightly smaller for long categorical labels\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\n# Grid styling - subtle, value-axis only\np.xgrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.15\np.ygrid.grid_line_alpha = 0.0\n\n# Save interactive HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome — CDP override is authoritative:\n# --window-size alone loses ~139 px to Chrome chrome in headless mode\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.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n\n# PIL safety net: pin to exact 3200×1800 in case of sub-pixel rounding\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\nif _img.size != (W, H):\n    _norm = Image.new(\"RGB\", (W, H), PAGE_BG)\n    _norm.paste(_img, ((W - _img.size[0]) // 2, (H - _img.size[1]) // 2))\n    _norm.save(f\"plot-{THEME}.png\")\n"}