{"spec_id":"violin-swarm","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nviolin-swarm: Violin Plot with Overlaid Swarm Points\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 82/100 | Updated: 2026-05-18\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, FactorRange, HoverTool\nfrom bokeh.plotting import figure\nfrom bokeh.resources import CDN\nfrom scipy import stats\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme-adaptive colors\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# Data color (brand green, theme-independent)\nDATA_COLOR = \"#009E73\"\n\n# Data - Reaction times (ms) across 4 experimental conditions\nnp.random.seed(42)\n\ncategories = [\"Control\", \"Low Dose\", \"Medium Dose\", \"High Dose\"]\nn_per_group = 50\n\n# Generate different distributions for each condition\ndata = {\n    \"Control\": np.random.normal(350, 50, n_per_group),\n    \"Low Dose\": np.random.normal(320, 45, n_per_group),\n    \"Medium Dose\": np.random.normal(280, 60, n_per_group),\n    \"High Dose\": np.random.normal(250, 40, n_per_group),\n}\n\n# Create figure with padding for violins\np = figure(\n    width=4800,\n    height=2700,\n    title=\"violin-swarm · Python · bokeh · anyplot.ai\",\n    x_range=FactorRange(*categories, range_padding=0.15),\n    y_axis_label=\"Reaction Time (ms)\",\n    x_axis_label=\"Experimental Condition\",\n)\n\n# Background and chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\n# Styling - larger text for 4800x2700 canvas\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\np.xgrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.10\n\n# Build violin shapes and swarm points\nviolin_patches_x = []\nviolin_patches_y = []\nswarm_x = []\nswarm_y = []\nswarm_categories = []\n\nfor i, cat in enumerate(categories):\n    values = data[cat]\n\n    # Kernel density estimation for violin\n    kde = stats.gaussian_kde(values)\n    y_range = np.linspace(values.min() - 20, values.max() + 20, 200)\n    density = kde(y_range)\n\n    # Normalize density to max width of 0.4 (so violin fits within category space)\n    max_width = 0.35\n    density_normalized = density / density.max() * max_width\n\n    # Create violin shape (mirrored density)\n    x_violin = np.concatenate([i - density_normalized, (i + density_normalized)[::-1]])\n    y_violin = np.concatenate([y_range, y_range[::-1]])\n\n    violin_patches_x.append(x_violin.tolist())\n    violin_patches_y.append(y_violin.tolist())\n\n    # Create swarm points (jitter within violin boundary)\n    for val in values:\n        # Get the density at this y value to determine jitter range\n        val_density = kde(val)[0]\n        jitter_range = (val_density / density.max()) * max_width * 0.8\n        jitter = np.random.uniform(-jitter_range, jitter_range)\n        swarm_x.append(i + jitter)\n        swarm_y.append(val)\n        swarm_categories.append(cat)\n\n# Draw violins as patches (semi-transparent)\nfor vx, vy in zip(violin_patches_x, violin_patches_y, strict=True):\n    p.patch(vx, vy, fill_color=DATA_COLOR, fill_alpha=0.4, line_color=DATA_COLOR, line_width=2)\n\n# Draw swarm points with HoverTool\nswarm_source = ColumnDataSource(data={\"x\": swarm_x, \"y\": swarm_y, \"category\": swarm_categories})\nhover = HoverTool(tooltips=[(\"Category\", \"@category\"), (\"Value (ms)\", \"@y{0.0}\")])\np.add_tools(hover)\np.scatter(\"x\", \"y\", source=swarm_source, size=12, color=DATA_COLOR, alpha=0.7, line_color=INK_SOFT, line_width=1)\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p, resources=CDN)\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)\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"}