{"spec_id":"box-notched","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nbox-notched: Notched Box Plot\nLibrary: pygal 3.1.3 | Python 3.13.15\nQuality: 90/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\nimport re\nimport xml.etree.ElementTree as ET\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\nSVG_NS = \"http://www.w3.org/2000/svg\"\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\"\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\")\n\n# Data - Generate response times for different server configurations\nnp.random.seed(42)\ncategories = [\"Baseline\", \"Config A\", \"Config B\", \"Config C\", \"Config D\"]\ndata = {\n    \"Baseline\": np.random.normal(120, 25, 80),\n    \"Config A\": np.random.normal(95, 20, 80),\n    \"Config B\": np.random.normal(115, 22, 80),\n    \"Config C\": np.random.normal(85, 18, 80),\n    \"Config D\": np.random.normal(110, 30, 80),\n}\ndata[\"Baseline\"] = np.append(data[\"Baseline\"], [200, 210, 45])\ndata[\"Config D\"] = np.append(data[\"Config D\"], [190, 35])\n\n# Calculate notched box plot statistics (inlined)\nstats = {}\nfor cat in categories:\n    values = data[cat]\n    q1 = np.percentile(values, 25)\n    median = np.percentile(values, 50)\n    q3 = np.percentile(values, 75)\n    iqr = q3 - q1\n    n = len(values)\n\n    notch_width = 1.57 * iqr / np.sqrt(n)\n    notch_low = median - notch_width\n    notch_high = median + notch_width\n\n    whisker_low = max(q1 - 1.5 * iqr, np.min(values))\n    whisker_high = min(q3 + 1.5 * iqr, np.max(values))\n\n    outliers = values[(values < q1 - 1.5 * iqr) | (values > q3 + 1.5 * iqr)]\n\n    stats[cat] = {\n        \"q1\": q1,\n        \"median\": median,\n        \"q3\": q3,\n        \"mean\": float(np.mean(values)),\n        \"notch_low\": notch_low,\n        \"notch_high\": notch_high,\n        \"whisker_low\": whisker_low,\n        \"whisker_high\": whisker_high,\n        \"outliers\": outliers.tolist(),\n    }\n\n# Custom style (Imprint palette + theme-adaptive chrome)\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=IMPRINT,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n)\n\nall_values = np.concatenate([data[cat] for cat in categories])\ny_min = np.floor(np.min(all_values) / 10) * 10 - 10\n# Extra headroom above the tallest whisker so the significance brackets have room to breathe.\ny_max = np.ceil(np.max(all_values) / 10) * 10 + 20\n\n# Base chart only supplies axis geometry (ticks, legend, titles) - the notched\n# boxes themselves are drawn as an SVG overlay below, aligned to pygal's own\n# rendered coordinates. Bar (not Line/XY) is used so each category gets a full\n# equal-width slot with generous edge margin - Line/XY reserve almost none,\n# which clips box overlays and axis labels near the first/last category.\nchart = pygal.Bar(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"box-notched · python · pygal · anyplot.ai\",\n    x_title=\"Server Configuration\",\n    y_title=\"Response Time (ms)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_box_size=40,\n    show_y_guides=True,\n    show_x_guides=False,\n    margin=50,\n    range=(y_min, y_max),\n    no_data_text=\"\",\n)\n\nchart.x_labels = categories\nfor category in categories:\n    chart.add(category, [{\"value\": y_min, \"label\": \"\"}])\n\nsvg_string = chart.render()\nif isinstance(svg_string, bytes):\n    svg_string = svg_string.decode(\"utf-8\")\n\n# Introspect pygal's own rendered coordinate system (plot origin, x-axis tick\n# centers, y-axis value<->pixel mapping) so the overlay aligns exactly with the\n# axes regardless of font size / margin choices - no hardcoded pixel guesses.\nplot_origin = re.search(r'<g transform=\"translate\\(([\\d.]+),\\s*([\\d.]+)\\)\" class=\"plot\">', svg_string)\nplot_dx, plot_dy = (float(v) for v in plot_origin.groups())\n\nx_block = re.search(r'<g class=\"axis x\">(.*?)</g></g>', svg_string, re.S).group(1)\nx_centers = {\n    label: plot_dx + float(x_pos)\n    for x_pos, label in re.findall(\n        r'<path d=\"M([\\d.]+) [\\d.]+ v[\\d.]+\" class=\"[^\"]*\" ?/><text x=\"-?[\\d.]+\" y=\"-?[\\d.]+\" class=\"[^\"]*\">([^<]*)</text>',\n        x_block,\n    )\n}\n\ny_block = re.search(r'<g class=\"axis y[^\"]*\">(.*?)</g><g class=\"axis x\">', svg_string, re.S).group(1)\ny_ticks = [\n    (float(value), float(y_pos))\n    for y_pos, value in re.findall(\n        r'<path d=\"M[\\d.]+ ([\\d.]+) h[\\d.]+\" class=\"[^\"]*\" ?/><text x=\"-?[\\d.]+\" y=\"-?[\\d.]+\" class=\"[^\"]*\">([^<]*)</text>',\n        y_block,\n    )\n]\ny_scale, y_intercept = np.polyfit([v for v, _ in y_ticks], [p for _, p in y_ticks], 1)\n\n\ndef y_px(value):\n    return plot_dy + y_scale * value + y_intercept\n\n\ncenters_sorted = [x_centers[c] for c in categories]\nbox_spacing = float(np.mean(np.diff(centers_sorted))) if len(centers_sorted) > 1 else 400.0\nbox_width = box_spacing * 0.6\nnotch_indent = box_width * 0.15\ncap_width = box_width * 0.3\n\n# Parse and augment the rendered SVG\nET.register_namespace(\"\", SVG_NS)\nET.register_namespace(\"xlink\", \"http://www.w3.org/1999/xlink\")\nroot = ET.fromstring(svg_string)\n\n# Drop the anchor bars pygal drew for the invisible series - only their tick\n# geometry (already extracted above) was needed; the boxes below replace them.\nparent_map = {child: parent for parent in root.iter() for child in parent}\nfor g in list(root.iter(f\"{{{SVG_NS}}}g\")):\n    if g.get(\"class\", \"\").startswith(\"series\"):\n        parent = parent_map.get(g)\n        if parent is not None:\n            parent.remove(g)\n\ndefs = ET.SubElement(root, f\"{{{SVG_NS}}}defs\")\nboxes_group = ET.Element(f\"{{{SVG_NS}}}g\", attrib={\"class\": \"notched-boxes\"})\n\nfor i, category in enumerate(categories):\n    s = stats[category]\n    color = IMPRINT[i % len(IMPRINT)]\n    x_center = x_centers[category]\n    x_left = x_center - box_width / 2\n    x_right = x_center + box_width / 2\n\n    y_q1 = y_px(s[\"q1\"])\n    y_q3 = y_px(s[\"q3\"])\n    y_med = y_px(s[\"median\"])\n    y_mean = y_px(s[\"mean\"])\n    y_notch_low = y_px(s[\"notch_low\"])\n    y_notch_high = y_px(s[\"notch_high\"])\n    y_whisker_low = y_px(s[\"whisker_low\"])\n    y_whisker_high = y_px(s[\"whisker_high\"])\n\n    notch_x_left = x_left + notch_indent\n    notch_x_right = x_right - notch_indent\n\n    # Subtle top-to-bottom gradient gives each box a touch of depth beyond flat fill-opacity.\n    gradient = ET.SubElement(\n        defs, f\"{{{SVG_NS}}}linearGradient\", attrib={\"id\": f\"box-grad-{i}\", \"x1\": \"0\", \"y1\": \"0\", \"x2\": \"0\", \"y2\": \"1\"}\n    )\n    ET.SubElement(gradient, f\"{{{SVG_NS}}}stop\", attrib={\"offset\": \"0%\", \"stop-color\": color, \"stop-opacity\": \"0.55\"})\n    ET.SubElement(gradient, f\"{{{SVG_NS}}}stop\", attrib={\"offset\": \"100%\", \"stop-color\": color, \"stop-opacity\": \"0.22\"})\n\n    path_d = (\n        f\"M {x_left} {y_q3} \"\n        f\"L {x_right} {y_q3} \"\n        f\"L {x_right} {y_notch_high} \"\n        f\"L {notch_x_right} {y_med} \"\n        f\"L {x_right} {y_notch_low} \"\n        f\"L {x_right} {y_q1} \"\n        f\"L {x_left} {y_q1} \"\n        f\"L {x_left} {y_notch_low} \"\n        f\"L {notch_x_left} {y_med} \"\n        f\"L {x_left} {y_notch_high} \"\n        f\"Z\"\n    )\n\n    ET.SubElement(\n        boxes_group,\n        f\"{{{SVG_NS}}}path\",\n        attrib={\"d\": path_d, \"fill\": f\"url(#box-grad-{i})\", \"stroke\": color, \"stroke-width\": \"3\"},\n    )\n\n    ET.SubElement(\n        boxes_group,\n        f\"{{{SVG_NS}}}line\",\n        attrib={\n            \"x1\": str(notch_x_left),\n            \"y1\": str(y_med),\n            \"x2\": str(notch_x_right),\n            \"y2\": str(y_med),\n            \"stroke\": color,\n            \"stroke-width\": \"4\",\n        },\n    )\n\n    ET.SubElement(\n        boxes_group,\n        f\"{{{SVG_NS}}}line\",\n        attrib={\n            \"x1\": str(x_center),\n            \"y1\": str(y_q3),\n            \"x2\": str(x_center),\n            \"y2\": str(y_whisker_high),\n            \"stroke\": color,\n            \"stroke-width\": \"2.5\",\n        },\n    )\n    ET.SubElement(\n        boxes_group,\n        f\"{{{SVG_NS}}}line\",\n        attrib={\n            \"x1\": str(x_center - cap_width / 2),\n            \"y1\": str(y_whisker_high),\n            \"x2\": str(x_center + cap_width / 2),\n            \"y2\": str(y_whisker_high),\n            \"stroke\": color,\n            \"stroke-width\": \"2.5\",\n        },\n    )\n    ET.SubElement(\n        boxes_group,\n        f\"{{{SVG_NS}}}line\",\n        attrib={\n            \"x1\": str(x_center),\n            \"y1\": str(y_q1),\n            \"x2\": str(x_center),\n            \"y2\": str(y_whisker_low),\n            \"stroke\": color,\n            \"stroke-width\": \"2.5\",\n        },\n    )\n    ET.SubElement(\n        boxes_group,\n        f\"{{{SVG_NS}}}line\",\n        attrib={\n            \"x1\": str(x_center - cap_width / 2),\n            \"y1\": str(y_whisker_low),\n            \"x2\": str(x_center + cap_width / 2),\n            \"y2\": str(y_whisker_low),\n            \"stroke\": color,\n            \"stroke-width\": \"2.5\",\n        },\n    )\n\n    # Mean marker (diamond) alongside the median line - the notch already tests the\n    # median's confidence interval, the diamond gives the mean for comparison at a glance.\n    diamond_r = 11\n    ET.SubElement(\n        boxes_group,\n        f\"{{{SVG_NS}}}rect\",\n        attrib={\n            \"x\": str(x_center - diamond_r),\n            \"y\": str(y_mean - diamond_r),\n            \"width\": str(diamond_r * 2),\n            \"height\": str(diamond_r * 2),\n            \"fill\": PAGE_BG,\n            \"stroke\": color,\n            \"stroke-width\": \"2.5\",\n            \"transform\": f\"rotate(45 {x_center} {y_mean})\",\n        },\n    )\n\n    for outlier in s[\"outliers\"]:\n        ET.SubElement(\n            boxes_group,\n            f\"{{{SVG_NS}}}circle\",\n            attrib={\n                \"cx\": str(x_center),\n                \"cy\": str(y_px(outlier)),\n                \"r\": \"9\",\n                \"fill\": PAGE_BG,\n                \"stroke\": color,\n                \"stroke-width\": \"2.5\",\n            },\n        )\n\n# Significance brackets: a shared row in the headroom above the tallest whisker\n# marks adjacent category pairs whose notches do not overlap - the visual\n# \"quick hypothesis test\" the notched box plot exists for (see specification.md).\n# Placed a third of the way down from the range ceiling so it clears both the\n# title and the y=y_max gridline instead of crowding the nearest gridline.\ntop_of_range_px = y_px(y_max)\nmin_whisker_px = min(y_px(stats[c][\"whisker_high\"]) for c in categories)\nbracket_y = top_of_range_px + (min_whisker_px - top_of_range_px) * 0.35\ntick_len = 18\nfor cat_a, cat_b in zip(categories, categories[1:], strict=False):\n    stats_a, stats_b = stats[cat_a], stats[cat_b]\n    significant = stats_a[\"notch_high\"] < stats_b[\"notch_low\"] or stats_b[\"notch_high\"] < stats_a[\"notch_low\"]\n    if not significant:\n        continue\n    # Inset from the tick centers so consecutive significant pairs read as separate\n    # brackets instead of fusing into one continuous line across the whole row.\n    inset = box_width * 0.2\n    xa, xb = x_centers[cat_a] + inset, x_centers[cat_b] - inset\n    ET.SubElement(\n        boxes_group,\n        f\"{{{SVG_NS}}}path\",\n        attrib={\n            \"d\": f\"M {xa} {bracket_y + tick_len} L {xa} {bracket_y} L {xb} {bracket_y} L {xb} {bracket_y + tick_len}\",\n            \"fill\": \"none\",\n            \"stroke\": INK,\n            \"stroke-width\": \"3\",\n        },\n    )\n    ET.SubElement(\n        boxes_group,\n        f\"{{{SVG_NS}}}circle\",\n        attrib={\"cx\": str((xa + xb) / 2), \"cy\": str(bracket_y - 16), \"r\": \"7\", \"fill\": INK},\n    )\n\nroot.append(boxes_group)\nmodified_svg = ET.tostring(root, encoding=\"unicode\")\n\n# Save as PNG and HTML\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(modified_svg)\n\ncairosvg.svg2png(\n    bytestring=modified_svg.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\", output_width=3200, output_height=1800\n)\n"}