{"spec_id":"violin-grouped-swarm","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nviolin-grouped-swarm: Grouped Violin Plot with Swarm Overlay\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-05-18\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom scipy import stats\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n_cwd = os.getcwd()\nsys.path = [p for p in sys.path if os.path.abspath(p) != os.path.dirname(__file__)]\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Legend, LegendItem\nfrom bokeh.plotting import figure\n\n\nsys.path.insert(0, os.path.dirname(__file__))\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\"\n\n# Okabe-Ito palette\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Data - Response times (ms) across 3 task types and 2 expertise levels\nnp.random.seed(42)\n\ncategories = [\"Simple\", \"Moderate\", \"Complex\"]\ngroups = [\"Novice\", \"Expert\"]\n\n# Generate realistic response time data for each combination\ndata = []\nfor cat_idx, category in enumerate(categories):\n    for _grp_idx, group in enumerate(groups):\n        # Experts faster, complex tasks take longer\n        base = 200 + cat_idx * 150\n        expert_adjust = -80 if group == \"Expert\" else 0\n        mean = base + expert_adjust\n        std = 30 + cat_idx * 15\n        values = np.random.normal(mean, std, 40)\n        values = np.clip(values, 50, 900)\n        for val in values:\n            data.append({\"category\": category, \"group\": group, \"value\": val})\n\n# Color mapping\ncolors = {group: IMPRINT[i] for i, group in enumerate(groups)}\n\n# Create figure\np = figure(\n    width=4800,\n    height=2700,\n    title=\"violin-grouped-swarm · Python · bokeh · anyplot.ai\",\n    x_axis_label=\"Task Type\",\n    y_axis_label=\"Response Time (ms)\",\n    x_range=[-0.5, 2.5],\n    y_range=[0, 750],\n    tools=\"\",\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_font_size = \"28pt\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"22pt\"\np.yaxis.axis_label_text_font_size = \"22pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_font_size = \"18pt\"\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\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\n\n# Positioning\ncat_positions = {cat: i for i, cat in enumerate(categories)}\ngroup_offsets = {\"Novice\": -0.2, \"Expert\": 0.2}\n\n# Store legend items\nlegend_items = []\n\n# Draw violin shapes and swarm points for each category-group combination\nfor _grp_idx, group in enumerate(groups):\n    first_violin = None\n\n    for _cat_idx, category in enumerate(categories):\n        # Get values for this category-group\n        values = np.array([d[\"value\"] for d in data if d[\"category\"] == category and d[\"group\"] == group])\n\n        base_x = cat_positions[category] + group_offsets[group]\n\n        # Compute kernel density estimate for violin\n        kde = stats.gaussian_kde(values)\n        y_range = np.linspace(values.min() - 15, values.max() + 15, 100)\n        density = kde(y_range)\n\n        # Scale density to reasonable width\n        max_width = 0.17\n        density_scaled = density / density.max() * max_width\n\n        # Create violin polygon\n        violin_x = np.concatenate([base_x - density_scaled, (base_x + density_scaled)[::-1]])\n        violin_y = np.concatenate([y_range, y_range[::-1]])\n\n        # Draw violin\n        v_glyph = p.patch(\n            violin_x, violin_y, fill_color=colors[group], fill_alpha=0.5, line_color=colors[group], line_width=3\n        )\n\n        if first_violin is None:\n            first_violin = v_glyph\n\n        # Create swarm points - bin values and assign jittered x positions\n        swarm_x = []\n        bin_width = 20\n        value_bins = {}\n\n        for val in values:\n            bin_key = int(val // bin_width)\n            if bin_key not in value_bins:\n                value_bins[bin_key] = 0\n            count = value_bins[bin_key]\n            # Alternate sides with increasing offset\n            offset = (count // 2 + 1) * 0.025 * (1 if count % 2 == 0 else -1)\n            if count == 0:\n                offset = 0\n            # Clamp offset within violin width\n            max_offset = density_scaled[min(int((val - y_range[0]) / (y_range[-1] - y_range[0]) * 99), 99)] * 0.7\n            offset = np.clip(offset, -max_offset, max_offset)\n            swarm_x.append(base_x + offset)\n            value_bins[bin_key] += 1\n\n        swarm_source = ColumnDataSource(\n            data={\"x\": swarm_x, \"y\": values, \"group\": [group] * len(values), \"category\": [category] * len(values)}\n        )\n\n        # Add hover tool for swarm points\n        hover = HoverTool(\n            tooltips=[(\"Task Type\", \"@category\"), (\"Expertise\", \"@group\"), (\"Response Time\", \"@y{0.0f} ms\")]\n        )\n        p.add_tools(hover)\n\n        p.scatter(\n            \"x\",\n            \"y\",\n            source=swarm_source,\n            size=15,\n            fill_color=colors[group],\n            fill_alpha=0.75,\n            line_color=\"white\",\n            line_width=2,\n        )\n\n    # Add legend item for this group\n    legend_items.append(LegendItem(label=group, renderers=[first_violin]))\n\n# Custom x-axis with category labels\np.xaxis.ticker = list(range(len(categories)))\np.xaxis.major_label_overrides = dict(enumerate(categories))\n\n# Add legend with larger sizing\nlegend = Legend(\n    items=legend_items,\n    location=\"top_right\",\n    label_text_font_size=\"20pt\",\n    label_text_color=INK_SOFT,\n    glyph_width=50,\n    glyph_height=50,\n    spacing=20,\n    padding=25,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.95,\n    border_line_color=INK_SOFT,\n    border_line_width=2,\n)\np.add_layout(legend, \"right\")\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with Selenium\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)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}