{"spec_id":"violin-box","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nviolin-box: Violin Plot with Embedded Box Plot\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-12\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, Legend, LegendItem\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (see prompts/default-style-guide.md)\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# Okabe-Ito palette - first series is always #009E73\nIMPRINT = [\n    \"#009E73\",  # bluish green - first series\n    \"#C475FD\",  # vermillion\n    \"#4467A3\",  # blue\n    \"#BD8233\",  # reddish purple\n]\n\n# Data - Test scores by study method (educational context)\nnp.random.seed(42)\ncategories = [\"Method A\", \"Method B\", \"Method C\", \"Method D\"]\n\n# Create distributions with different characteristics to showcase features\ndata = {\n    \"Method A\": np.random.normal(72, 12, 120),  # Standard distribution\n    \"Method B\": np.concatenate(\n        [\n            np.random.normal(78, 8, 100),  # Main group\n            np.array([48, 52, 95, 98]),  # Outliers both ends\n        ]\n    ),\n    \"Method C\": np.random.normal(65, 15, 120),  # Wider spread\n    \"Method D\": np.concatenate(\n        [\n            np.random.normal(80, 6, 90),  # Tight main group\n            np.random.normal(55, 3, 20),  # Bimodal lower group\n            np.array([38, 40, 42]),  # Low outliers\n        ]\n    ),\n}\n\n# Create figure with categorical x-axis\np = figure(\n    width=4800,\n    height=2700,\n    title=\"violin-box · bokeh · pyplots.ai\",\n    x_axis_label=\"Study Method\",\n    y_axis_label=\"Test Score\",\n    x_range=categories,\n    toolbar_location=None,\n)\n\n# Theme-adaptive styling\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\np.title.text_color = INK\np.title.text_font_size = \"28pt\"\n\np.xaxis.axis_label_text_color = INK\np.xaxis.axis_label_text_font_size = \"22pt\"\np.yaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_font_size = \"22pt\"\n\np.xaxis.major_label_text_color = INK_SOFT\np.xaxis.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_font_size = \"18pt\"\n\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\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.10\np.ygrid.grid_line_dash = \"solid\"\n\n# Violin width scaling (0.4 = 40% of category spacing)\nviolin_width = 0.38\n\n# Track renderers for legend\nviolin_renderer = None\nbox_renderer = None\nmedian_renderer = None\noutlier_renderer = None\n\n# Collect outlier data for all categories\nall_outliers_x = []\nall_outliers_y = []\n\n# Draw violins with embedded box plots for each category\nfor i, cat in enumerate(categories):\n    values = np.array(data[cat])\n    n = len(values)\n\n    # Compute KDE using Gaussian kernel (Silverman's rule for bandwidth)\n    std = np.std(values)\n    iqr = np.percentile(values, 75) - np.percentile(values, 25)\n    bandwidth = 0.9 * min(std, iqr / 1.34) * n ** (-0.2)\n    bandwidth = max(bandwidth, 0.1)\n\n    y_grid = np.linspace(values.min() - std, values.max() + std, 100)\n    density = np.zeros_like(y_grid, dtype=float)\n    for xi in values:\n        density += np.exp(-0.5 * ((y_grid - xi) / bandwidth) ** 2)\n    density /= n * bandwidth * np.sqrt(2 * np.pi)\n\n    # Scale density to violin width\n    density_scaled = density / density.max() * violin_width\n\n    # Create violin shape (mirrored on both sides)\n    x_left = -density_scaled\n    x_right = density_scaled\n\n    # Convert to categorical offset format for bokeh\n    xs_left = [(cat, float(xl)) for xl in x_left]\n    xs_right = [(cat, float(xr)) for xr in x_right[::-1]]\n\n    # Draw violin patch with Okabe-Ito color\n    violin_color = IMPRINT[i % len(IMPRINT)]\n    vr = p.patch(\n        xs_left + xs_right,\n        list(y_grid) + list(y_grid[::-1]),\n        fill_color=violin_color,\n        fill_alpha=0.6,\n        line_color=violin_color,\n        line_width=3,\n    )\n    if violin_renderer is None:\n        violin_renderer = vr\n\n    # Compute box plot statistics\n    q1 = np.percentile(values, 25)\n    median = np.percentile(values, 50)\n    q3 = np.percentile(values, 75)\n    iqr_val = q3 - q1\n    whisker_low = max(values.min(), q1 - 1.5 * iqr_val)\n    whisker_high = min(values.max(), q3 + 1.5 * iqr_val)\n\n    # Draw box inside violin (IQR from Q1 to Q3)\n    box_width = 0.08\n    box_fill_color = violin_color  # Same color as violin\n    br = p.quad(\n        left=[(cat, -box_width)],\n        right=[(cat, box_width)],\n        top=[q3],\n        bottom=[q1],\n        fill_color=box_fill_color,\n        fill_alpha=0.9,\n        line_color=INK,\n        line_width=3,\n    )\n    if box_renderer is None:\n        box_renderer = br\n\n    # Draw median line\n    mr = p.segment(\n        x0=[(cat, -box_width * 1.3)],\n        y0=[median],\n        x1=[(cat, box_width * 1.3)],\n        y1=[median],\n        line_color=INK,\n        line_width=5,\n    )\n    if median_renderer is None:\n        median_renderer = mr\n\n    # Whiskers (vertical lines from box to whisker limits)\n    p.segment(x0=[cat], y0=[q1], x1=[cat], y1=[whisker_low], line_color=INK_SOFT, line_width=3)\n    p.segment(x0=[cat], y0=[q3], x1=[cat], y1=[whisker_high], line_color=INK_SOFT, line_width=3)\n\n    # Whisker caps\n    cap_width = 0.05\n    p.segment(\n        x0=[(cat, -cap_width)],\n        y0=[whisker_low],\n        x1=[(cat, cap_width)],\n        y1=[whisker_low],\n        line_color=INK_SOFT,\n        line_width=3,\n    )\n    p.segment(\n        x0=[(cat, -cap_width)],\n        y0=[whisker_high],\n        x1=[(cat, cap_width)],\n        y1=[whisker_high],\n        line_color=INK_SOFT,\n        line_width=3,\n    )\n\n    # Collect outliers\n    outliers = values[(values < whisker_low) | (values > whisker_high)]\n    for out in outliers:\n        all_outliers_x.append(cat)\n        all_outliers_y.append(out)\n\n# Draw all outliers\nif len(all_outliers_x) > 0:\n    outlier_source = ColumnDataSource(data={\"x\": all_outliers_x, \"y\": all_outliers_y})\n    outlier_renderer = p.scatter(\n        x=\"x\", y=\"y\", source=outlier_source, size=18, fill_color=\"white\", line_color=INK, line_width=3, marker=\"circle\"\n    )\n\n# Create legend\nlegend_items = [\n    LegendItem(label=\"Distribution (KDE)\", renderers=[violin_renderer]),\n    LegendItem(label=\"IQR (Q1-Q3)\", renderers=[box_renderer]),\n    LegendItem(label=\"Median\", renderers=[median_renderer]),\n]\nif outlier_renderer is not None:\n    legend_items.append(LegendItem(label=\"Outliers\", renderers=[outlier_renderer]))\n\nlegend = Legend(items=legend_items, location=\"top_right\")\nlegend.label_text_color = INK_SOFT\nlegend.label_text_font_size = \"16pt\"\nlegend.background_fill_color = ELEVATED_BG\nlegend.border_line_color = INK_SOFT\nlegend.glyph_height = 30\nlegend.glyph_width = 30\nlegend.spacing = 15\nlegend.padding = 20\np.add_layout(legend, \"right\")\n\n# Save HTML output\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome\nW, H = 4800, 2700\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)\n\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)  # let bokeh's JS render the canvas\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}