{"spec_id":"cat-box-strip","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncat-box-strip: Box Plot with Strip Overlay\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-13\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens from environment\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 (colorblind-safe, first series is brand green #009E73)\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Data - Test scores across education levels\nnp.random.seed(42)\ncategories = [\"High School\", \"Bachelor's\", \"Master's\", \"PhD\"]\ndata = {\n    \"High School\": np.random.normal(68, 12, 45),\n    \"Bachelor's\": np.random.normal(78, 10, 50),\n    \"Master's\": np.random.normal(82, 8, 38),\n    \"PhD\": np.random.normal(85, 7, 42),\n}\n\n# Add realistic outliers\ndata[\"High School\"] = np.append(data[\"High School\"], [95, 48])\ndata[\"Bachelor's\"] = np.append(data[\"Bachelor's\"], [58, 98])\ndata[\"Master's\"] = np.append(data[\"Master's\"], [62, 95])\ndata[\"PhD\"] = np.append(data[\"PhD\"], [65, 100])\n\n# Clamp all values to 0-100 range\nfor key in data:\n    data[key] = np.clip(data[key], 0, 100)\n\n# Color sequence: 6 colors for box plot elements + 4 for strip points (one per category)\ncolor_sequence = []\nfor i in range(4):\n    color_sequence.extend([IMPRINT[i]] * 6)  # 6 box elements per category\nfor i in range(4):\n    color_sequence.append(IMPRINT[i])  # 1 strip series per category\n\n# Custom style for 4800x2700 px canvas with theme-adaptive colors\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    colors=tuple(color_sequence),\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)\n\n# Create XY chart for combined box plot with strip overlay\nchart = pygal.XY(\n    width=4800,\n    height=2700,\n    style=custom_style,\n    title=\"cat-box-strip · pygal · anyplot.ai\",\n    x_title=\"Education Level\",\n    y_title=\"Test Score\",\n    show_legend=False,\n    stroke=True,\n    fill=True,\n    dots_size=0,\n    show_x_guides=True,\n    show_y_guides=True,\n    xrange=(0, 5),\n    range=(0, 105),\n    margin=100,\n    explicit_size=True,\n)\n\n# Layout parameters\nbox_width = 0.25\ncap_width = 0.15\n\n# Pre-compute box plot components and strip points\nstrip_data = []\nbox_data = []\n\nfor i, (category, values) in enumerate(data.items()):\n    center_x = i + 1  # X position for this group (1, 2, 3, 4)\n    values = np.array(values)\n\n    # --- Box Plot Statistics ---\n    median = float(np.median(values))\n    q1 = float(np.percentile(values, 25))\n    q3 = float(np.percentile(values, 75))\n    iqr = q3 - q1\n    whisker_low = float(max(values.min(), q1 - 1.5 * iqr))\n    whisker_high = float(min(values.max(), q3 + 1.5 * iqr))\n    box_data.append((center_x, median, q1, q3, whisker_low, whisker_high))\n\n    # --- Strip Points with Jitter ---\n    np.random.seed(42 + i)\n    jitter = np.random.uniform(-0.12, 0.12, len(values))\n    strip_points = [(center_x + j, float(v)) for j, v in zip(jitter, values, strict=True)]\n    strip_data.append((category, strip_points))\n\n# Draw box plots first (so strip points appear on top)\nfor center_x, median, q1, q3, whisker_low, whisker_high in box_data:\n    # IQR box (filled rectangle)\n    quartile_box = [\n        (center_x - box_width, q1),\n        (center_x - box_width, q3),\n        (center_x + box_width, q3),\n        (center_x + box_width, q1),\n        (center_x - box_width, q1),\n    ]\n    chart.add(\"\", quartile_box, stroke=True, fill=True, show_dots=False, stroke_style={\"width\": 6})\n\n    # Median line (horizontal line within box)\n    median_line = [(center_x - box_width * 1.1, median), (center_x + box_width * 1.1, median)]\n    chart.add(\"\", median_line, stroke=True, fill=False, show_dots=False, stroke_style={\"width\": 10})\n\n    # Whiskers (vertical lines from box to caps)\n    whisker_bottom = [(center_x, q1), (center_x, whisker_low)]\n    whisker_top = [(center_x, q3), (center_x, whisker_high)]\n    chart.add(\"\", whisker_bottom, stroke=True, fill=False, show_dots=False, stroke_style={\"width\": 6})\n    chart.add(\"\", whisker_top, stroke=True, fill=False, show_dots=False, stroke_style={\"width\": 6})\n\n    # Whisker caps (horizontal lines at ends)\n    cap_bottom = [(center_x - cap_width, whisker_low), (center_x + cap_width, whisker_low)]\n    cap_top = [(center_x - cap_width, whisker_high), (center_x + cap_width, whisker_high)]\n    chart.add(\"\", cap_bottom, stroke=True, fill=False, show_dots=False, stroke_style={\"width\": 6})\n    chart.add(\"\", cap_top, stroke=True, fill=False, show_dots=False, stroke_style={\"width\": 6})\n\n# Add strip points on top with transparency\nfor _category, strip_points in strip_data:\n    chart.add(\"\", strip_points, stroke=False, fill=False, dots_size=16)\n\n# X-axis labels for categories\nchart.x_labels = [\n    {\"value\": 0, \"label\": \"\"},\n    {\"value\": 1, \"label\": \"High School\"},\n    {\"value\": 2, \"label\": \"Bachelor's\"},\n    {\"value\": 3, \"label\": \"Master's\"},\n    {\"value\": 4, \"label\": \"PhD\"},\n    {\"value\": 5, \"label\": \"\"},\n]\n\n# Save outputs\nchart.render_to_file(f\"plot-{THEME}.html\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}