{"spec_id":"violin-grouped-swarm","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nviolin-grouped-swarm: Grouped Violin Plot with Swarm Overlay\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-18\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\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 (first two colors for the two groups)\nCOLORS = [\"#009E73\", \"#C475FD\"]\n\n# Data - Response times across task types and expertise levels\nnp.random.seed(42)\n\ncategories = [\"Simple\", \"Moderate\", \"Complex\"]\ngroups = [\"Novice\", \"Expert\"]\n\n# Generate data: different distributions for each category-group combination\ndata = {}\npositions = {}\nwidth = 0.35\n\nfor i, cat in enumerate(categories):\n    for j, grp in enumerate(groups):\n        if grp == \"Novice\":\n            if cat == \"Simple\":\n                vals = np.random.normal(loc=1.2, scale=0.3, size=40)\n            elif cat == \"Moderate\":\n                vals = np.random.normal(loc=2.5, scale=0.5, size=40)\n            else:  # Complex\n                vals = np.random.normal(loc=4.5, scale=0.8, size=40)\n        else:  # Expert\n            if cat == \"Simple\":\n                vals = np.random.normal(loc=0.8, scale=0.2, size=40)\n            elif cat == \"Moderate\":\n                vals = np.random.normal(loc=1.5, scale=0.3, size=40)\n            else:  # Complex\n                vals = np.random.normal(loc=2.5, scale=0.5, size=40)\n        vals = np.maximum(vals, 0.1)\n        data[(cat, grp)] = vals\n        offset = -width / 2 if j == 0 else width / 2\n        positions[(cat, grp)] = i + offset\n\n# Plot\nfig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Draw violins for each group\nfor j, grp in enumerate(groups):\n    violin_data = [data[(cat, grp)] for cat in categories]\n    pos = [i + (-width / 2 if j == 0 else width / 2) for i in range(len(categories))]\n\n    parts = ax.violinplot(\n        violin_data, positions=pos, widths=width * 0.9, showmeans=False, showmedians=True, showextrema=False\n    )\n\n    # Style violins\n    for pc in parts[\"bodies\"]:\n        pc.set_facecolor(COLORS[j])\n        pc.set_edgecolor(INK_SOFT)\n        pc.set_alpha(0.5)\n        pc.set_linewidth(1.5)\n\n    # Style median lines\n    parts[\"cmedians\"].set_color(INK)\n    parts[\"cmedians\"].set_linewidth(2)\n\n# Overlay swarm points\nfor cat in categories:\n    for j, grp in enumerate(groups):\n        vals = data[(cat, grp)]\n        base_x = positions[(cat, grp)]\n\n        # Create swarm-like jitter\n        sorted_indices = np.argsort(vals)\n        n_points = len(vals)\n        jitter = np.zeros(n_points)\n\n        bin_width = (vals.max() - vals.min()) / 20\n        for idx in sorted_indices:\n            val = vals[idx]\n            nearby = np.abs(vals - val) < bin_width\n            nearby_count = np.sum(nearby & (np.arange(n_points) <= idx))\n            if nearby_count % 2 == 0:\n                jitter[idx] = (nearby_count // 2) * 0.015\n            else:\n                jitter[idx] = -((nearby_count + 1) // 2) * 0.015\n\n        x_positions = base_x + jitter\n\n        ax.scatter(x_positions, vals, s=70, c=COLORS[j], edgecolors=PAGE_BG, linewidths=0.8, alpha=0.9, zorder=3)\n\n# Style\nax.set_xticks(range(len(categories)))\nax.set_xticklabels(categories, fontsize=18, color=INK_SOFT)\nax.set_xlabel(\"Task Complexity\", fontsize=20, color=INK)\nax.set_ylabel(\"Response Time (seconds)\", fontsize=20, color=INK)\nax.set_title(\"violin-grouped-swarm · Python · matplotlib · anyplot.ai\", fontsize=24, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=16, colors=INK_SOFT)\n\n# Spines\nfor s in (\"left\", \"bottom\"):\n    ax.spines[s].set_color(INK_SOFT)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\n\n# Grid\nax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)\n\n# Legend\nlegend_handles = [\n    plt.Rectangle((0, 0), 1, 1, facecolor=COLORS[0], edgecolor=INK_SOFT, alpha=0.5, label=groups[0]),\n    plt.Rectangle((0, 0), 1, 1, facecolor=COLORS[1], edgecolor=INK_SOFT, alpha=0.5, label=groups[1]),\n]\nleg = ax.legend(handles=legend_handles, fontsize=16, title=\"Expertise\", title_fontsize=16, loc=\"upper left\")\nif leg:\n    leg.get_frame().set_facecolor(ELEVATED_BG)\n    leg.get_frame().set_edgecolor(INK_SOFT)\n    plt.setp(leg.get_texts(), color=INK_SOFT)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}