{"spec_id":"violin-swarm","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nviolin-swarm: Violin Plot with Overlaid Swarm Points\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-05-18\n\"\"\"\n\nimport os\nimport sys\n\n\n_original_path = sys.path[:]\nsys.path = [p for p in sys.path if p not in (\"\", \".\", os.getcwd())]\nimport altair as alt\n\n\nsys.path = _original_path\n\nimport numpy as np\nimport pandas as pd\nfrom scipy import stats\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 series is ALWAYS #009E73)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data: Reaction times (ms) across 4 experimental conditions\nnp.random.seed(42)\n\nconditions = [\"Condition A\", \"Condition B\", \"Condition C\", \"Condition D\"]\nn_per_group = 50\n\ndata = []\n# Different distributions to show variety\nfor i, condition in enumerate(conditions):\n    if i == 0:\n        # Normal distribution\n        values = np.random.normal(320, 40, n_per_group)\n    elif i == 1:\n        # Slightly skewed with higher values\n        values = np.random.gamma(8, 30, n_per_group) + 200\n    elif i == 2:\n        # Bimodal-ish (mix of two normals)\n        values = np.concatenate(\n            [np.random.normal(280, 25, n_per_group // 2), np.random.normal(380, 30, n_per_group // 2)]\n        )\n    else:\n        # Higher mean, tighter spread\n        values = np.random.normal(400, 25, n_per_group)\n\n    for v in values:\n        data.append({\"Condition\": condition, \"Reaction Time (ms)\": v})\n\ndf = pd.DataFrame(data)\n\n# Compute kernel density estimates for violin shapes\nviolin_data = []\ny_min = df[\"Reaction Time (ms)\"].min() - 20\ny_max = df[\"Reaction Time (ms)\"].max() + 20\ny_range = np.linspace(y_min, y_max, 100)\n\nfor condition in conditions:\n    subset = df[df[\"Condition\"] == condition][\"Reaction Time (ms)\"]\n    kde = stats.gaussian_kde(subset, bw_method=0.3)\n    density = kde(y_range)\n    # Normalize density to create symmetric violin width\n    density_norm = density / density.max() * 0.4\n\n    for y_val, d in zip(y_range, density_norm, strict=True):\n        violin_data.append({\"Condition\": condition, \"y\": y_val, \"width\": d})\n\nviolin_df = pd.DataFrame(violin_data)\n\n# Add jitter for swarm-like point distribution\nnp.random.seed(42)\ndf[\"jitter\"] = np.random.uniform(-0.2, 0.2, len(df))\n\n# Map conditions to x positions\ncondition_to_x = {c: i for i, c in enumerate(conditions)}\ndf[\"x\"] = df[\"Condition\"].map(condition_to_x)\ndf[\"x_jittered\"] = df[\"x\"] + df[\"jitter\"]\nviolin_df[\"x\"] = violin_df[\"Condition\"].map(condition_to_x)\nviolin_df[\"x_left\"] = violin_df[\"x\"] - violin_df[\"width\"]\nviolin_df[\"x_right\"] = violin_df[\"x\"] + violin_df[\"width\"]\n\n# Color scale using Okabe-Ito palette\ncolor_scale = alt.Scale(domain=conditions, range=IMPRINT)\n\n# Y axis scale\ny_scale = alt.Scale(domain=[y_min - 10, y_max + 10])\n\n# X axis scale (fixed with padding)\nx_scale = alt.Scale(domain=[-0.6, 3.6])\n\n# Violin shapes using area marks\nviolin = (\n    alt.Chart(violin_df)\n    .mark_area(opacity=0.4, interpolate=\"monotone\", line=False)\n    .encode(\n        y=alt.Y(\n            \"y:Q\",\n            title=\"Reaction Time (ms)\",\n            scale=y_scale,\n            axis=alt.Axis(labelFontSize=18, titleFontSize=22, grid=True, gridOpacity=0.10),\n        ),\n        x=alt.X(\"x_left:Q\", scale=x_scale, axis=None),\n        x2=\"x_right:Q\",\n        color=alt.Color(\"Condition:N\", scale=color_scale, legend=None),\n    )\n)\n\n# Swarm-like points with category colors\npoints = (\n    alt.Chart(df)\n    .mark_circle(size=100, opacity=0.85)\n    .encode(\n        y=alt.Y(\"Reaction Time (ms):Q\", scale=y_scale, axis=None),\n        x=alt.X(\"x_jittered:Q\", scale=x_scale, axis=None),\n        color=alt.Color(\"Condition:N\", scale=color_scale, legend=None),\n        tooltip=[\n            alt.Tooltip(\"Condition:N\", title=\"Condition\"),\n            alt.Tooltip(\"Reaction Time (ms):Q\", title=\"Time (ms)\", format=\".1f\"),\n        ],\n    )\n)\n\n# X-axis labels as text marks\nlabel_df = pd.DataFrame({\"x\": [0, 1, 2, 3], \"label\": conditions, \"y\": [y_min - 30] * 4})\n\nx_labels = (\n    alt.Chart(label_df)\n    .mark_text(fontSize=18, fontWeight=\"bold\", color=INK)\n    .encode(x=alt.X(\"x:Q\", scale=x_scale), y=alt.value(820), text=\"label:N\")\n)\n\n# Combine layers\nchart = (\n    alt.layer(violin, points, x_labels)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\"violin-swarm · Python · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", color=INK),\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0)\n    .configure_axis(\n        domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.10, labelColor=INK_SOFT, titleColor=INK\n    )\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}