{"spec_id":"violin-swarm","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nviolin-swarm: Violin Plot with Overlaid Swarm Points\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-18\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n\n# Theme tokens (read from environment)\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\nBRAND = \"#009E73\"  # violin fill\nACCENT = \"#C475FD\"  # swarm points\n\n# Data - Reaction times (ms) across 4 experimental conditions\nnp.random.seed(42)\n\nconditions = [\"Control\", \"Treatment A\", \"Treatment B\", \"Treatment C\"]\nn_per_group = 50\n\n# Generate different distributions for each condition\ndata = {\n    \"Control\": np.random.normal(450, 60, n_per_group),\n    \"Treatment A\": np.random.normal(380, 45, n_per_group),\n    \"Treatment B\": np.random.normal(420, 80, n_per_group),\n    \"Treatment C\": np.concatenate(\n        [np.random.normal(350, 30, n_per_group // 2), np.random.normal(450, 30, n_per_group // 2)]\n    ),  # Bimodal\n}\n\n# Create figure\nfig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Prepare data for violin plot\nviolin_data = [data[cond] for cond in conditions]\npositions = np.arange(len(conditions))\n\n# Draw violin plot with transparency\nparts = ax.violinplot(violin_data, positions=positions, showmeans=False, showmedians=False, showextrema=False)\n\n# Style violins with Okabe-Ito brand color and transparency\nfor pc in parts[\"bodies\"]:\n    pc.set_facecolor(BRAND)\n    pc.set_edgecolor(INK_SOFT)\n    pc.set_alpha(0.4)\n    pc.set_linewidth(2)\n\n# Overlay swarm points\nfor cond, pos in zip(conditions, positions, strict=True):\n    y = data[cond]\n    # Add jitter to spread points horizontally (swarm-like effect)\n    # Calculate density-based jitter\n    n_points = len(y)\n    jitter = np.zeros(n_points)\n\n    # Sort points and assign horizontal positions based on local density\n    sorted_indices = np.argsort(y)\n    sorted_y = y[sorted_indices]\n\n    # Calculate jitter based on nearby point density\n    bandwidth = (np.max(y) - np.min(y)) / 20\n    for j, (idx, val) in enumerate(zip(sorted_indices, sorted_y, strict=True)):\n        # Count nearby points\n        nearby = np.sum(np.abs(sorted_y - val) < bandwidth)\n        # Assign alternating jitter based on position within group\n        local_idx = np.sum(np.abs(sorted_y[: j + 1] - val) < bandwidth) - 1\n        max_jitter = 0.25 * (nearby / n_points) ** 0.5 + 0.05\n        jitter[idx] = (local_idx % 2 * 2 - 1) * max_jitter * ((local_idx // 2 + 1) / (nearby / 2 + 1))\n\n    x = np.full(n_points, pos) + jitter\n    ax.scatter(\n        x,\n        y,\n        s=110,\n        alpha=0.8,\n        color=ACCENT,\n        edgecolor=INK_SOFT,\n        linewidth=0.8,\n        zorder=3,\n        label=\"Individual observations\" if pos == 0 else \"\",\n    )\n\n# Add median lines\nfor i, pos in enumerate(positions):\n    median = np.median(violin_data[i])\n    ax.hlines(median, pos - 0.2, pos + 0.2, color=INK, linewidth=3, zorder=4)\n\n# Styling\nax.set_xticks(positions)\nax.set_xticklabels(conditions, fontsize=18, color=INK_SOFT)\nax.set_xlabel(\"Experimental Condition\", fontsize=20, color=INK)\nax.set_ylabel(\"Reaction Time (ms)\", fontsize=20, color=INK)\nax.set_title(\"violin-swarm · Python · matplotlib · anyplot.ai\", fontsize=24, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=16, colors=INK_SOFT)\n\n# Grid styling\nax.yaxis.grid(True, alpha=0.10, linewidth=0.8, color=INK)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nfor s in (\"left\", \"bottom\"):\n    ax.spines[s].set_color(INK_SOFT)\n\n# Legend\nleg = ax.legend(loc=\"upper right\", fontsize=16, title=\"Distribution\", title_fontsize=16)\nif leg:\n    leg.get_frame().set_facecolor(ELEVATED_BG)\n    leg.get_frame().set_edgecolor(INK_SOFT)\n    leg.get_frame().set_alpha(0.9)\n    plt.setp(leg.get_texts(), color=INK_SOFT)\n    plt.setp(leg.get_title(), color=INK)\n\n# Set y-axis limits with padding\nall_values = np.concatenate(violin_data)\ny_min, y_max = np.min(all_values), np.max(all_values)\ny_padding = (y_max - y_min) * 0.1\nax.set_ylim(y_min - y_padding, y_max + y_padding)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}