{"spec_id":"dumbbell-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ndumbbell-basic: Basic Dumbbell Chart\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-06-30\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove script's own directory from sys.path to prevent self-shadowing\n# (this file is named bokeh.py; without this, `import bokeh` would find itself)\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _here]\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, LabelSet\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\"\nBRAND = \"#009E73\"  # Imprint palette position 1 — \"Before\" dots\nACCENT = \"#C475FD\"  # Imprint palette position 2 — \"After\" dots\n\n# Data — Employee satisfaction scores before and after policy changes\ncategories = [\n    \"Engineering\",\n    \"Marketing\",\n    \"Sales\",\n    \"Customer Support\",\n    \"Human Resources\",\n    \"Finance\",\n    \"Operations\",\n    \"Research & Development\",\n]\nstart_values = [62, 58, 71, 55, 68, 78, 60, 65]\nend_values = [78, 74, 82, 70, 85, 72, 73, 88]\n\n# Sort ascending by change so the largest improvements sit at the top\ndeltas = [e - s for s, e in zip(start_values, end_values, strict=True)]\nordered = sorted(zip(categories, start_values, end_values, deltas, strict=True), key=lambda d: d[3])\ncategories = [d[0] for d in ordered]\nstart_values = [d[1] for d in ordered]\nend_values = [d[2] for d in ordered]\ndeltas = [d[3] for d in ordered]\n\n# Plot\nTITLE = \"dumbbell-basic · python · bokeh · anyplot.ai\"\np = figure(\n    width=3200,\n    height=1800,\n    y_range=categories,\n    x_range=(45, 95),\n    title=TITLE,\n    x_axis_label=\"Satisfaction Score\",\n    y_axis_label=\"Department\",\n    background_fill_color=PAGE_BG,\n    border_fill_color=PAGE_BG,\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=280,\n    min_border_top=110,\n    min_border_right=60,\n)\n\n# Connecting segments — color-coded by direction: green = improvement, red = regression\nseg_source = ColumnDataSource(\n    data={\n        \"y\": categories,\n        \"x_start\": start_values,\n        \"x_end\": end_values,\n        \"delta\": [f\"{d:+d}\" for d in deltas],\n        \"seg_color\": [BRAND if d > 0 else \"#AE3030\" for d in deltas],\n    }\n)\np.segment(\n    x0=\"x_start\", x1=\"x_end\", y0=\"y\", y1=\"y\", source=seg_source, line_color=\"seg_color\", line_alpha=0.55, line_width=6\n)\n\n# Delta labels on connecting segments — makes improvement/regression story explicit\nlabel_source = ColumnDataSource(\n    data={\n        \"x\": [(s + e) / 2 for s, e in zip(start_values, end_values, strict=True)],\n        \"y\": categories,\n        \"text\": [f\"{d:+d}\" for d in deltas],\n    }\n)\np.add_layout(\n    LabelSet(\n        x=\"x\",\n        y=\"y\",\n        text=\"text\",\n        source=label_source,\n        text_align=\"center\",\n        text_baseline=\"bottom\",\n        text_font_size=\"26pt\",\n        text_color=INK_SOFT,\n        y_offset=24,\n    )\n)\n\n# \"Before\" dots — Imprint palette position 1 (brand green)\nbefore_source = ColumnDataSource(\n    data={\n        \"x\": start_values,\n        \"y\": categories,\n        \"phase\": [\"Before\"] * len(categories),\n        \"after\": end_values,\n        \"delta\": [f\"{d:+d} pts\" for d in deltas],\n    }\n)\nbefore_glyph = p.scatter(\n    x=\"x\",\n    y=\"y\",\n    source=before_source,\n    size=28,\n    fill_color=BRAND,\n    line_color=PAGE_BG,\n    line_width=3,\n    legend_label=\"Before policy changes\",\n)\n\n# \"After\" dots — Imprint palette position 2 (lavender)\nafter_source = ColumnDataSource(\n    data={\n        \"x\": end_values,\n        \"y\": categories,\n        \"phase\": [\"After\"] * len(categories),\n        \"before\": start_values,\n        \"delta\": [f\"{d:+d} pts\" for d in deltas],\n    }\n)\nafter_glyph = p.scatter(\n    x=\"x\",\n    y=\"y\",\n    source=after_source,\n    size=28,\n    fill_color=ACCENT,\n    line_color=PAGE_BG,\n    line_width=3,\n    legend_label=\"After policy changes\",\n)\n\n# Hover tooltip — shows department, phase, score, and delta change together\np.add_tools(\n    HoverTool(\n        renderers=[before_glyph, after_glyph],\n        tooltips=[(\"Department\", \"@y\"), (\"Phase\", \"@phase\"), (\"Score\", \"@x\"), (\"Δ Change\", \"@delta\")],\n    )\n)\n\n# Typography — canonical sizes for 3200×1800 per bokeh.md\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.text_font_style = \"normal\"\np.title.align = \"center\"\n\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"36pt\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\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_label_standoff = 18\np.yaxis.axis_label_standoff = 18\n\n# Spines — keep x-axis, remove y-axis for a cleaner look\np.outline_line_color = None\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = None\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = None\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\n# Grid — subtle vertical guides only\np.xgrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_color = None\n\n# Legend\np.legend.location = \"bottom_left\"\np.legend.background_fill_color = ELEVATED_BG\np.legend.background_fill_alpha = 0.95\np.legend.border_line_color = INK_SOFT\np.legend.border_line_alpha = 0.4\np.legend.label_text_color = INK_SOFT\np.legend.label_text_font_size = \"34pt\"\np.legend.spacing = 10\np.legend.padding = 18\np.legend.margin = 24\n\n# Save interactive HTML (required catalog artifact)\nhtml_path = Path(f\"plot-{THEME}.html\")\noutput_file(str(html_path), title=TITLE)\nsave(p)\n\n# Inject body background CSS to prevent thin border artifact in headless-Chrome screenshot\nhtml_content = html_path.read_text()\nbody_style = f\"<style>body{{margin:0;padding:0;background:{PAGE_BG};}}</style>\"\nhtml_content = html_content.replace(\"</head>\", f\"{body_style}\\n</head>\", 1)\nhtml_path.write_text(html_content)\n\n# Screenshot via headless Chrome — use CDP to set exact viewport to match figure dimensions\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://{html_path.resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}