{"spec_id":"cat-box-strip","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ncat-box-strip: Box Plot with Strip Overlay\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 82/100 | Updated: 2026-05-13\n\"\"\"\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import export_png, output_file, save\nfrom bokeh.models import ColumnDataSource, Whisker\nfrom bokeh.plotting import figure\nfrom bokeh.transform import jitter\n\n\n# Data - Plant growth measurements across different soil types\nnp.random.seed(42)\n\ncategories = [\"Sandy\", \"Clay\", \"Loamy\", \"Silty\"]\nn_per_group = [35, 40, 45, 38]\n\n# Generate data with different distributions per group\ndata = []\nfor cat, n in zip(categories, n_per_group, strict=True):\n    if cat == \"Sandy\":\n        values = np.random.normal(25, 6, n)  # Lower growth, moderate variance\n    elif cat == \"Clay\":\n        values = np.random.normal(32, 8, n)  # Medium growth, high variance\n        values = np.append(values, [55, 58])  # Add outliers\n    elif cat == \"Loamy\":\n        values = np.random.normal(42, 5, n)  # High growth, low variance\n    else:  # Silty\n        values = np.random.normal(35, 7, n)  # Medium-high growth\n        values = np.append(values, [12, 14])  # Add low outliers\n\n    for v in values:\n        data.append({\"category\": cat, \"value\": v})\n\ndf = pd.DataFrame(data)\n\n# Calculate box plot statistics for each category\nbox_data = {\"category\": [], \"q1\": [], \"q2\": [], \"q3\": [], \"upper\": [], \"lower\": []}\n\nfor cat in categories:\n    group = df[df[\"category\"] == cat][\"value\"]\n    q1 = group.quantile(0.25)\n    q2 = group.quantile(0.50)\n    q3 = group.quantile(0.75)\n    iqr = q3 - q1\n    upper_whisker = group[group <= q3 + 1.5 * iqr].max()\n    lower_whisker = group[group >= q1 - 1.5 * iqr].min()\n\n    box_data[\"category\"].append(cat)\n    box_data[\"q1\"].append(q1)\n    box_data[\"q2\"].append(q2)\n    box_data[\"q3\"].append(q3)\n    box_data[\"upper\"].append(upper_whisker)\n    box_data[\"lower\"].append(lower_whisker)\n\nbox_source = ColumnDataSource(data=box_data)\n\n# Create figure with categorical x-axis\np = figure(\n    width=4800,\n    height=2700,\n    x_range=categories,\n    title=\"cat-box-strip · bokeh · pyplots.ai\",\n    x_axis_label=\"Soil Type\",\n    y_axis_label=\"Plant Growth (cm)\",\n    tools=\"\",\n    toolbar_location=None,\n)\n\n# Styling - scaled for 4800x2700 canvas\np.title.text_font_size = \"36pt\"\np.xaxis.axis_label_text_font_size = \"28pt\"\np.yaxis.axis_label_text_font_size = \"28pt\"\np.xaxis.major_label_text_font_size = \"24pt\"\np.yaxis.major_label_text_font_size = \"22pt\"\np.xaxis.axis_label_standoff = 25\np.yaxis.axis_label_standoff = 25\n\n# Grid styling\np.grid.grid_line_alpha = 0.3\np.grid.grid_line_dash = [6, 4]\np.xgrid.grid_line_color = None\n\n# Background\np.background_fill_color = \"#fafafa\"\n\n# Draw whiskers using the Whisker annotation\nupper_whisker = Whisker(\n    source=box_source, base=\"category\", upper=\"upper\", lower=\"q3\", line_color=\"#306998\", line_width=2.5\n)\nupper_whisker.upper_head.size = 30\nupper_whisker.upper_head.line_color = \"#306998\"\nupper_whisker.upper_head.line_width = 2.5\nupper_whisker.lower_head.size = 0\np.add_layout(upper_whisker)\n\nlower_whisker = Whisker(\n    source=box_source, base=\"category\", upper=\"q1\", lower=\"lower\", line_color=\"#306998\", line_width=2.5\n)\nlower_whisker.lower_head.size = 30\nlower_whisker.lower_head.line_color = \"#306998\"\nlower_whisker.lower_head.line_width = 2.5\nlower_whisker.upper_head.size = 0\np.add_layout(lower_whisker)\n\n# Draw boxes (IQR range) - upper half\np.vbar(\n    x=\"category\",\n    top=\"q3\",\n    bottom=\"q2\",\n    width=0.5,\n    source=box_source,\n    fill_color=\"#306998\",\n    fill_alpha=0.4,\n    line_color=\"#306998\",\n    line_width=3,\n)\n\n# Draw boxes (IQR range) - lower half\np.vbar(\n    x=\"category\",\n    top=\"q2\",\n    bottom=\"q1\",\n    width=0.5,\n    source=box_source,\n    fill_color=\"#306998\",\n    fill_alpha=0.4,\n    line_color=\"#306998\",\n    line_width=3,\n)\n\n# Median line (horizontal segment across the box)\np.segment(x0=\"category\", x1=\"category\", y0=\"q2\", y1=\"q2\", source=box_source, line_color=\"#1a3d5c\", line_width=4)\n\n# Strip plot overlay with jitter\nstrip_source = ColumnDataSource(data={\"category\": df[\"category\"], \"value\": df[\"value\"]})\n\np.scatter(\n    x=jitter(\"category\", width=0.3, range=p.x_range),\n    y=\"value\",\n    source=strip_source,\n    size=16,\n    fill_color=\"#FFD43B\",\n    fill_alpha=0.75,\n    line_color=\"#b8860b\",\n    line_width=2,\n)\n\n# Save PNG\nexport_png(p, filename=\"plot.png\")\n\n# Save HTML for interactive version\noutput_file(\"plot.html\")\nsave(p)\n"}