{"spec_id":"violin-split","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nviolin-split: Split Violin Plot\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-08\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Okabe-Ito palette: position 1 (brand green) and position 2 (vermillion)\nCOLOR_1 = \"#009E73\"  # Before\nCOLOR_2 = \"#C475FD\"  # After\n\n# Data - Patient recovery scores before/after treatment across clinics\nnp.random.seed(42)\ncategories = [\"Clinic A\", \"Clinic B\", \"Clinic C\", \"Clinic D\"]\nsplit_groups = [\"Before\", \"After\"]\n\n# Generate realistic before/after data with different improvements per clinic\ndata = {}\nfor cat in categories:\n    data[cat] = {}\n    if cat == \"Clinic A\":\n        data[cat][\"Before\"] = np.random.normal(45, 12, 80)\n        data[cat][\"After\"] = np.random.normal(72, 10, 80)\n    elif cat == \"Clinic B\":\n        data[cat][\"Before\"] = np.random.normal(50, 15, 80)\n        data[cat][\"After\"] = np.random.normal(68, 12, 80)\n    elif cat == \"Clinic C\":\n        data[cat][\"Before\"] = np.random.normal(42, 10, 80)\n        data[cat][\"After\"] = np.random.normal(78, 8, 80)\n    else:  # Clinic D\n        data[cat][\"Before\"] = np.random.normal(55, 18, 80)\n        data[cat][\"After\"] = np.random.normal(65, 14, 80)\n\n# Clip to realistic 0-100 range\nfor cat in categories:\n    for group in split_groups:\n        data[cat][group] = np.clip(data[cat][group], 10, 95)\n\n# Custom style for 4800x2700 px canvas\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    guide_stroke_color=INK_MUTED,\n    colors=(COLOR_1, COLOR_2),\n    title_font_size=28,\n    label_font_size=22,\n    major_label_font_size=18,\n    legend_font_size=16,\n    value_font_size=14,\n    opacity=0.7,\n    opacity_hover=0.9,\n)\n\n# Create XY chart for split violin plot\nchart = pygal.XY(\n    width=4800,\n    height=2700,\n    style=custom_style,\n    title=\"violin-split · pygal · anyplot.ai\",\n    x_title=\"Clinic\",\n    y_title=\"Recovery Score (0-100)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=2,\n    stroke=True,\n    fill=True,\n    dots_size=0,\n    show_x_guides=False,\n    show_y_guides=True,\n    range=(0, 100),\n    xrange=(0, 5.5),\n    margin=60,\n)\n\n# Parameters for violin shapes\nviolin_width = 0.38\nn_points = 80\nmarker_width = 0.04\n\n# Pre-compute all violin shapes and markers\nbefore_violins = []\nafter_violins = []\nbefore_markers = []\nafter_markers = []\n\nfor i, category in enumerate(categories):\n    center_x = i + 1.25\n\n    for group in split_groups:\n        values = data[category][group]\n\n        # Create range of y values for density\n        y_min, y_max = values.min(), values.max()\n        padding = (y_max - y_min) * 0.15\n        y_range = np.linspace(y_min - padding, y_max + padding, n_points)\n\n        # Compute Gaussian KDE using Silverman's rule (inlined)\n        n = len(values)\n        std = np.std(values)\n        iqr = np.percentile(values, 75) - np.percentile(values, 25)\n        bandwidth = 0.9 * min(std, iqr / 1.34) * n ** (-0.2)\n\n        density = np.zeros_like(y_range)\n        for v in values:\n            density += np.exp(-0.5 * ((y_range - v) / bandwidth) ** 2)\n        density /= n * bandwidth * np.sqrt(2 * np.pi)\n\n        # Normalize density to desired width\n        density = density / density.max() * violin_width\n\n        # Compute quartile statistics\n        median = float(np.median(values))\n        q1 = float(np.percentile(values, 25))\n        q3 = float(np.percentile(values, 75))\n\n        # Create half-violin shape (split violin - each group on one side)\n        if group == \"Before\":\n            # Left half - density extends to the left\n            half_points = [(center_x - d, y) for y, d in zip(y_range, density, strict=True)]\n            half_points = [(center_x, y_range[0])] + half_points + [(center_x, y_range[-1]), (center_x, y_range[0])]\n            before_violins.append(half_points)\n            before_markers.append((center_x, median, q1, q3, -0.08))\n        else:\n            # Right half - density extends to the right\n            half_points = [(center_x + d, y) for y, d in zip(y_range, density, strict=True)]\n            half_points = [(center_x, y_range[0])] + half_points + [(center_x, y_range[-1]), (center_x, y_range[0])]\n            after_violins.append(half_points)\n            after_markers.append((center_x, median, q1, q3, 0.08))\n\n# Add all \"Before\" violins first (green, with legend entry for first one only)\nfor i, violin in enumerate(before_violins):\n    label = \"Before\" if i == 0 else None\n    chart.add(label, violin, show_dots=False)\n\n# Add all \"After\" violins (orange, with legend entry for first one only)\nfor i, violin in enumerate(after_violins):\n    label = \"After\" if i == 0 else None\n    chart.add(label, violin, show_dots=False)\n\n# Add quartile markers for \"Before\" group (no legend entries)\nfor center_x, median, q1, q3, offset in before_markers:\n    # IQR line (thin vertical line)\n    iqr_line = [(center_x + offset, q1), (center_x + offset, q3)]\n    chart.add(None, iqr_line, stroke=True, fill=False, show_dots=False, stroke_style={\"width\": 8})\n    # Median marker (small horizontal line)\n    median_line = [(center_x + offset - marker_width, median), (center_x + offset + marker_width, median)]\n    chart.add(None, median_line, stroke=True, fill=False, show_dots=False, stroke_style={\"width\": 12})\n\n# Add quartile markers for \"After\" group (no legend entries)\nfor center_x, median, q1, q3, offset in after_markers:\n    # IQR line\n    iqr_line = [(center_x + offset, q1), (center_x + offset, q3)]\n    chart.add(None, iqr_line, stroke=True, fill=False, show_dots=False, stroke_style={\"width\": 8})\n    # Median marker\n    median_line = [(center_x + offset - marker_width, median), (center_x + offset + marker_width, median)]\n    chart.add(None, median_line, stroke=True, fill=False, show_dots=False, stroke_style={\"width\": 12})\n\n# X-axis labels for categories\nchart.x_labels = [\n    {\"value\": 0, \"label\": \"\"},\n    {\"value\": 1.25, \"label\": \"Clinic A\"},\n    {\"value\": 2.25, \"label\": \"Clinic B\"},\n    {\"value\": 3.25, \"label\": \"Clinic C\"},\n    {\"value\": 4.25, \"label\": \"Clinic D\"},\n    {\"value\": 5.5, \"label\": \"\"},\n]\n\n# Save outputs\nchart.render_to_file(f\"plot-{THEME}.html\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}