{"spec_id":"bar-diverging-likert","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nbar-diverging-likert: Likert Scale Diverging Bar Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-06-01\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent self-import: this file is named bokeh.py, so Python's path search would\n# find it before the installed bokeh package. Remove the script's own directory\n# from sys.path so imports resolve to the installed package.\n_own_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _own_dir]\n\nimport numpy as np\nfrom bokeh.io import export_png, output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, LabelSet, Legend, LegendItem, Span\nfrom bokeh.plotting import figure\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive chrome tokens (Imprint palette)\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data — employee engagement survey, 10 questions, 5-point Likert scale\nquestions = [\n    \"I feel valued at work\",\n    \"My manager provides feedback\",\n    \"I have growth opportunities\",\n    \"Work-life balance is good\",\n    \"I understand company goals\",\n    \"My team communicates well\",\n    \"I have the tools I need\",\n    \"I would recommend this company\",\n    \"Meetings are productive\",\n    \"My contributions are recognized\",\n]\n\nstrongly_disagree = [5, 12, 18, 8, 3, 6, 10, 15, 22, 7]\ndisagree = [10, 15, 20, 12, 8, 10, 14, 18, 25, 12]\nneutral = [15, 18, 22, 15, 12, 14, 16, 17, 20, 15]\nagree = [40, 30, 25, 35, 42, 38, 32, 28, 20, 36]\nstrongly_agree = [30, 25, 15, 30, 35, 32, 28, 22, 13, 30]\n\n# Sort questions by net agreement (agree + strongly agree) - (disagree + strongly disagree)\nnet_agreement = [\n    (sa + a) - (sd + d) for sa, a, sd, d in zip(strongly_agree, agree, strongly_disagree, disagree, strict=True)\n]\nsorted_idx = np.argsort(net_agreement)\nquestions_sorted = [questions[i] for i in sorted_idx]\nnet_sorted = [net_agreement[i] for i in sorted_idx]\nsd_sorted = [strongly_disagree[i] for i in sorted_idx]\nd_sorted = [disagree[i] for i in sorted_idx]\nn_sorted = [neutral[i] for i in sorted_idx]\na_sorted = [agree[i] for i in sorted_idx]\nsa_sorted = [strongly_agree[i] for i in sorted_idx]\n\n# Imprint palette — semantic mapping for diverging Likert:\n# negative→red, neutral→muted, positive→green (semantic exception per style guide)\nlikert_colors = {\n    \"Strongly Disagree\": \"#AE3030\",  # Imprint matte red (semantic: bad/negative)\n    \"Disagree\": \"#BD8233\",  # Imprint ochre (warm, moderate negative)\n    \"Neutral\": INK_MUTED,  # theme-adaptive muted gray\n    \"Agree\": \"#2ABCCD\",  # Imprint cyan (cool, moderate positive)\n    \"Strongly Agree\": \"#009E73\",  # Imprint brand green (strong positive)\n}\n\n# Contrasting text colors inside bar segments\nlabel_text_colors = {\n    \"Strongly Disagree\": \"#F0EFE8\",  # light on dark red\n    \"Disagree\": \"#F0EFE8\",  # light on ochre\n    \"Neutral\": \"#F0EFE8\",  # light on muted gray\n    \"Agree\": \"#1A1A17\",  # dark on light cyan\n    \"Strongly Agree\": \"#F0EFE8\",  # light on green\n}\n\n# Canvas — 3200×1800 (landscape, canonical bokeh)\n# x_range: max left extent ≈ -57, max right ≈ 83; labels extend to ~98\n# min_border_left enlarged to accommodate 34pt question labels (longest ~30 chars)\np = figure(\n    width=3200,\n    height=1800,\n    y_range=questions_sorted,\n    x_range=(-65, 105),\n    title=\"bar-diverging-likert · bokeh · anyplot.ai\",\n    toolbar_location=None,\n    min_border_bottom=180,\n    min_border_left=720,\n    min_border_top=120,\n    min_border_right=60,\n)\n\n# Build diverging bars: neutral centered at 0, disagree left, agree right\nlikert_categories = [\"Strongly Disagree\", \"Disagree\", \"Neutral\", \"Agree\", \"Strongly Agree\"]\ndata_by_cat = {\n    \"Strongly Disagree\": sd_sorted,\n    \"Disagree\": d_sorted,\n    \"Neutral\": n_sorted,\n    \"Agree\": a_sorted,\n    \"Strongly Agree\": sa_sorted,\n}\n\nlegend_items = []\nfor cat_name in likert_categories:\n    cat_values = data_by_cat[cat_name]\n    lefts = []\n    rights = []\n\n    for q_idx in range(len(questions_sorted)):\n        half_n = n_sorted[q_idx] / 2\n\n        if cat_name == \"Strongly Disagree\":\n            r = -(half_n + d_sorted[q_idx])\n            lft = r - sd_sorted[q_idx]\n        elif cat_name == \"Disagree\":\n            r = -half_n\n            lft = r - d_sorted[q_idx]\n        elif cat_name == \"Neutral\":\n            lft = -half_n\n            r = half_n\n        elif cat_name == \"Agree\":\n            lft = half_n\n            r = lft + a_sorted[q_idx]\n        else:  # Strongly Agree\n            lft = half_n + a_sorted[q_idx]\n            r = lft + sa_sorted[q_idx]\n\n        lefts.append(lft)\n        rights.append(r)\n\n    source = ColumnDataSource(\n        data={\n            \"question\": questions_sorted,\n            \"left\": lefts,\n            \"right\": rights,\n            \"value\": cat_values,\n            \"category\": [cat_name] * len(questions_sorted),\n        }\n    )\n\n    renderer = p.hbar(\n        y=\"question\",\n        left=\"left\",\n        right=\"right\",\n        height=0.72,\n        source=source,\n        color=likert_colors[cat_name],\n        line_color=PAGE_BG,\n        line_width=2,\n        alpha=0.92,\n    )\n    legend_items.append(LegendItem(label=cat_name, renderers=[renderer]))\n\n    hover = HoverTool(\n        renderers=[renderer], tooltips=[(\"Question\", \"@question\"), (\"Response\", \"@category\"), (\"Percentage\", \"@value%\")]\n    )\n    p.add_tools(hover)\n\n    # Percentage labels inside segments ≥10%\n    label_x = []\n    label_y = []\n    label_text_list = []\n    for q_idx in range(len(questions_sorted)):\n        if cat_values[q_idx] >= 10:\n            label_x.append((lefts[q_idx] + rights[q_idx]) / 2)\n            label_y.append(questions_sorted[q_idx])\n            label_text_list.append(f\"{cat_values[q_idx]}%\")\n\n    if label_text_list:\n        label_source = ColumnDataSource(data={\"x\": label_x, \"y\": label_y, \"text\": label_text_list})\n        labels = LabelSet(\n            x=\"x\",\n            y=\"y\",\n            text=\"text\",\n            source=label_source,\n            text_align=\"center\",\n            text_baseline=\"middle\",\n            text_font_size=\"18pt\",\n            text_color=label_text_colors[cat_name],\n            text_font_style=\"bold\",\n        )\n        p.add_layout(labels)\n\n# Zero-line baseline\ncenter_line = Span(location=0, dimension=\"height\", line_color=INK, line_width=2.5, line_alpha=0.5)\np.add_layout(center_line)\n\n# Net agreement annotations on the right — LabelSet supports categorical y values\n# Split into two sets to give positive/negative distinct Imprint colors\nnet_color_pos = \"#009E73\"  # Imprint green for positive net\nnet_color_neg = \"#AE3030\"  # Imprint red for negative net\n\npos_idx = [i for i, n in enumerate(net_sorted) if n >= 0]\nneg_idx = [i for i, n in enumerate(net_sorted) if n < 0]\n\nif pos_idx:\n    pos_src = ColumnDataSource(\n        data={\n            \"x\": [87] * len(pos_idx),\n            \"y\": [questions_sorted[i] for i in pos_idx],\n            \"text\": [f\"+{net_sorted[i]}%\" for i in pos_idx],\n        }\n    )\n    p.add_layout(\n        LabelSet(\n            x=\"x\",\n            y=\"y\",\n            text=\"text\",\n            source=pos_src,\n            text_align=\"left\",\n            text_baseline=\"middle\",\n            text_font_size=\"18pt\",\n            text_color=net_color_pos,\n            text_font_style=\"bold\",\n        )\n    )\n\nif neg_idx:\n    neg_src = ColumnDataSource(\n        data={\n            \"x\": [87] * len(neg_idx),\n            \"y\": [questions_sorted[i] for i in neg_idx],\n            \"text\": [f\"{net_sorted[i]}%\" for i in neg_idx],\n        }\n    )\n    p.add_layout(\n        LabelSet(\n            x=\"x\",\n            y=\"y\",\n            text=\"text\",\n            source=neg_src,\n            text_align=\"left\",\n            text_baseline=\"middle\",\n            text_font_size=\"18pt\",\n            text_color=net_color_neg,\n            text_font_style=\"bold\",\n        )\n    )\n\n# Legend at bottom, horizontal\nlegend = Legend(\n    items=legend_items,\n    orientation=\"horizontal\",\n    location=\"center\",\n    label_text_font_size=\"28pt\",\n    label_text_color=INK_SOFT,\n    label_standoff=12,\n    spacing=55,\n    padding=22,\n    margin=18,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.9,\n    border_line_color=INK_SOFT,\n    border_line_alpha=0.35,\n    glyph_height=42,\n    glyph_width=42,\n    click_policy=\"hide\",\n)\np.add_layout(legend, \"below\")\n\n# Typography — canonical bokeh sizing for 3200×1800\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.text_font_style = \"bold\"\n\np.xaxis.axis_label = \"← Disagree          Percentage          Agree →\"\np.xaxis.axis_label_text_font_size = \"38pt\"\np.xaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"30pt\"\np.xaxis.major_label_text_color = INK_SOFT\n\n# Y-axis: 34pt for question labels, large min_border_left reserves space for ~30-char labels\np.yaxis.major_label_text_font_size = \"28pt\"\np.yaxis.major_label_text_color = INK_SOFT\n\n# Grid — subtle x-only (low alpha, dashed)\np.xgrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.12\np.xgrid.grid_line_dash = [6, 4]\np.ygrid.grid_line_alpha = 0.0\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Remove axis lines and tick marks (clean, minimal look)\np.xaxis.axis_line_color = None\np.yaxis.axis_line_color = None\np.xaxis.major_tick_line_color = None\np.yaxis.major_tick_line_color = None\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\n# Save HTML (interactive catalog artifact)\noutput_file(f\"plot-{THEME}.html\")\nsave(p, title=\"bar-diverging-likert · bokeh · anyplot.ai\")\n\n# Export PNG via export_png (uses /usr/bin/chromedriver, which is the real driver in CI)\nexport_png(p, filename=f\"plot-{THEME}.png\")\n"}