{"spec_id":"violin-split","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nviolin-split: Split Violin Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 95/100 | Updated: 2026-05-08\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove script directory from path to avoid importing this file as altair\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if p != script_dir and p != \"\"]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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 (positions 1-2)\nIMPRINT = [\"#009E73\", \"#C475FD\"]\n\n# Data: Test scores (%) by department comparing control vs treatment groups\nnp.random.seed(42)\n\ndepartments = [\"Engineering\", \"Marketing\", \"Sales\", \"HR\"]\ndata = []\n\nfor dept in departments:\n    # Control group - baseline performance\n    if dept == \"Engineering\":\n        control_scores = np.random.normal(72, 12, 100)\n        treatment_scores = np.random.normal(85, 10, 100)  # Larger improvement\n    elif dept == \"Marketing\":\n        control_scores = np.random.normal(68, 15, 100)\n        treatment_scores = np.random.normal(78, 12, 100)\n    elif dept == \"Sales\":\n        control_scores = np.random.normal(75, 14, 100)\n        treatment_scores = np.random.normal(80, 11, 100)\n    else:  # HR\n        control_scores = np.random.normal(70, 10, 100)\n        treatment_scores = np.random.normal(76, 9, 100)\n\n    for score in control_scores:\n        data.append({\"Department\": dept, \"Score (%)\": np.clip(score, 30, 100), \"Group\": \"Control\"})\n    for score in treatment_scores:\n        data.append({\"Department\": dept, \"Score (%)\": np.clip(score, 30, 100), \"Group\": \"Treatment\"})\n\ndf = pd.DataFrame(data)\n\n# Calculate quartile statistics for inner markers\nquartile_data = (\n    df.groupby([\"Department\", \"Group\"])[\"Score (%)\"]\n    .agg(median=\"median\", q1=lambda x: x.quantile(0.25), q3=lambda x: x.quantile(0.75))\n    .reset_index()\n)\n\n# Merge quartile data back to main dataframe\ndf_with_quartiles = df.merge(quartile_data, on=[\"Department\", \"Group\"])\n\n# Create split violin using density transform with xOffset for split\nbase = alt.Chart().transform_density(\n    density=\"Score (%)\", as_=[\"Score (%)\", \"density\"], groupby=[\"Department\", \"Group\"], extent=[30, 100]\n)\n\n# For split violin: Control goes left (negative), Treatment goes right (positive)\nsplit_violin = (\n    base.transform_calculate(signed_density=\"datum.Group === 'Control' ? -datum.density : datum.density\")\n    .mark_area(orient=\"horizontal\", opacity=0.75)\n    .encode(\n        x=alt.X(\"signed_density:Q\", title=None, axis=alt.Axis(labels=False, ticks=False, domain=False), stack=None),\n        y=alt.Y(\"Score (%):Q\", title=\"Score (%)\", scale=alt.Scale(domain=[30, 100])),\n        color=alt.Color(\n            \"Group:N\",\n            scale=alt.Scale(domain=[\"Control\", \"Treatment\"], range=IMPRINT),\n            legend=alt.Legend(\n                title=\"Group\",\n                titleFontSize=20,\n                labelFontSize=18,\n                symbolSize=400,\n                orient=\"right\",\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n                titleColor=INK,\n                labelColor=INK_SOFT,\n            ),\n        ),\n    )\n)\n\n# IQR rule (vertical line from q1 to q3)\niqr_rule = (\n    alt.Chart()\n    .transform_aggregate(q1=\"min(q1)\", q3=\"max(q3)\", groupby=[\"Department\", \"Group\"])\n    .mark_rule(size=4, opacity=0.85)\n    .encode(\n        y=alt.Y(\"q1:Q\", scale=alt.Scale(domain=[30, 100])),\n        y2=\"q3:Q\",\n        xOffset=alt.XOffset(\"Group:N\", scale=alt.Scale(domain=[\"Control\", \"Treatment\"], range=[-20, 20])),\n        color=alt.Color(\"Group:N\", scale=alt.Scale(domain=[\"Control\", \"Treatment\"], range=IMPRINT)),\n    )\n)\n\n# Median point marker (diamond shape for visibility)\nmedian_marker = (\n    alt.Chart()\n    .transform_aggregate(median=\"min(median)\", groupby=[\"Department\", \"Group\"])\n    .mark_point(size=120, filled=True, shape=\"diamond\", opacity=1)\n    .encode(\n        y=alt.Y(\"median:Q\", scale=alt.Scale(domain=[30, 100])),\n        xOffset=alt.XOffset(\"Group:N\", scale=alt.Scale(domain=[\"Control\", \"Treatment\"], range=[-20, 20])),\n        color=alt.value(\"white\"),\n        stroke=alt.Color(\"Group:N\", scale=alt.Scale(domain=[\"Control\", \"Treatment\"], range=IMPRINT)),\n        strokeWidth=alt.value(2),\n    )\n)\n\n# Layer violin, IQR, and median markers with shared data\nlayered = alt.layer(split_violin, iqr_rule, median_marker, data=df_with_quartiles)\n\n# Facet by department\nchart = (\n    layered.properties(width=320, height=400)\n    .facet(column=alt.Column(\"Department:N\", title=None, header=alt.Header(labelFontSize=22, labelPadding=15)))\n    .resolve_scale(x=\"independent\")\n    .properties(title=alt.Title(\"violin-split · altair · anyplot.ai\", fontSize=28), background=PAGE_BG)\n    .configure_facet(spacing=50)\n    .configure_view(stroke=None, fill=PAGE_BG)\n    .configure_axis(\n        labelFontSize=18,\n        titleFontSize=22,\n        titleColor=INK,\n        labelColor=INK_SOFT,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.12,\n    )\n    .configure_title(anchor=\"middle\", offset=20, color=INK)\n)\n\n# Save (in script directory)\noutput_dir = os.path.dirname(os.path.abspath(__file__))\nchart.save(os.path.join(output_dir, f\"plot-{THEME}.png\"), scale_factor=3.0)\nchart.save(os.path.join(output_dir, f\"plot-{THEME}.html\"))\n"}