{"spec_id":"box-notched","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nbox-notched: Notched Box Plot\nLibrary: altair 6.2.2 | Python 3.13.15\nQuality: 94/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Background\" + \"Theme-adaptive Chrome\")\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# Imprint categorical palette — canonical order, departments are abstract groups\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data - Employee performance scores across departments\nnp.random.seed(42)\n\ndepartments = [\"Engineering\", \"Marketing\", \"Sales\", \"Operations\"]\ndata = []\n\n# Create varied distributions to showcase notched box plot features\n# Engineering: high scores, tight distribution\nengineering = np.random.normal(78, 8, 80)\nengineering = np.clip(engineering, 50, 100)\ndata.extend([{\"Department\": \"Engineering\", \"Performance Score\": v} for v in engineering])\n\n# Marketing: moderate scores, wider distribution with some outliers\nmarketing = np.concatenate(\n    [\n        np.random.normal(68, 12, 70),\n        np.array([35, 38, 95, 98]),  # outliers\n    ]\n)\ndata.extend([{\"Department\": \"Marketing\", \"Performance Score\": v} for v in marketing])\n\n# Sales: bimodal-ish, high variability\nsales = np.concatenate([np.random.normal(60, 10, 40), np.random.normal(80, 8, 45)])\nsales = np.clip(sales, 25, 100)  # keep within the 0-100 performance-score ceiling\ndata.extend([{\"Department\": \"Sales\", \"Performance Score\": v} for v in sales])\n\n# Operations: lower median, different from Engineering (to show non-overlapping notches)\noperations = np.random.normal(62, 10, 75)\noperations = np.clip(operations, 30, 95)\ndata.extend([{\"Department\": \"Operations\", \"Performance Score\": v} for v in operations])\n\ndf = pd.DataFrame(data)\n\n# Altair does not natively support notched box plots — calculate the notch\n# geometry manually and assemble it from layered marks.\nstats_list = []\nfor dept in departments:\n    values = df[df[\"Department\"] == dept][\"Performance Score\"].values\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: ±1.57 × IQR / √n (95% CI around the median)\n    notch_size = 1.57 * iqr / np.sqrt(n)\n    notch_lower = median - notch_size\n    notch_upper = median + notch_size\n\n    # Whiskers: furthest non-outlier point within 1.5×IQR of the box\n    non_outliers = values[(values >= q1 - 1.5 * iqr) & (values <= q3 + 1.5 * iqr)]\n    whisker_lower = non_outliers.min()\n    whisker_upper = non_outliers.max()\n\n    outliers = values[(values < q1 - 1.5 * iqr) | (values > q3 + 1.5 * iqr)]\n\n    stats_list.append(\n        {\n            \"Department\": dept,\n            \"q1\": q1,\n            \"median\": median,\n            \"q3\": q3,\n            \"mean\": float(np.mean(values)),\n            \"notch_lower\": notch_lower,\n            \"notch_upper\": notch_upper,\n            \"whisker_lower\": whisker_lower,\n            \"whisker_upper\": whisker_upper,\n            \"n\": n,\n            \"n_label\": f\"n = {n}\",\n            \"n_label_y\": 3,  # fixed low baseline, in the near-zero whitespace below every whisker\n            \"outliers\": outliers.tolist(),\n        }\n    )\n\nstats_df = pd.DataFrame(stats_list)\n# Sort by median (descending) so the ranking reads left-to-right — storytelling win over alphabetical order\ndept_order = stats_df.sort_values(\"median\", ascending=False)[\"Department\"].tolist()\n\noutlier_data = []\nfor _, row in stats_df.iterrows():\n    for outlier in row[\"outliers\"]:\n        outlier_data.append({\"Department\": row[\"Department\"], \"Performance Score\": outlier})\noutliers_df = pd.DataFrame(outlier_data) if outlier_data else pd.DataFrame(columns=[\"Department\", \"Performance Score\"])\n\ncolor_scale = alt.Scale(domain=departments, range=IMPRINT_PALETTE)\ntooltip_fields = [\n    alt.Tooltip(\"Department:N\"),\n    alt.Tooltip(\"q1:Q\", title=\"Q1\", format=\".1f\"),\n    alt.Tooltip(\"median:Q\", title=\"Median\", format=\".1f\"),\n    alt.Tooltip(\"q3:Q\", title=\"Q3\", format=\".1f\"),\n    alt.Tooltip(\"mean:Q\", title=\"Mean\", format=\".1f\"),\n    alt.Tooltip(\"notch_lower:Q\", title=\"Notch low (95% CI)\", format=\".1f\"),\n    alt.Tooltip(\"notch_upper:Q\", title=\"Notch high (95% CI)\", format=\".1f\"),\n    alt.Tooltip(\"n:Q\", title=\"Sample size\"),\n]\n\n# Hover highlight — mouseover a department's box to bring it to full opacity\n# and dim the rest, an Altair-native selection_point driving a shared param\n# across every colored layer (only visible in the interactive HTML export;\n# the empty selection matches all rows so the static PNG is unaffected).\nhover = alt.selection_point(fields=[\"Department\"], on=\"mouseover\", empty=True)\nhover_opacity = alt.condition(hover, alt.value(1.0), alt.value(0.55))\n\nx_enc = alt.X(\"Department:N\", title=\"Department\", sort=dept_order, axis=alt.Axis(labelAngle=0, grid=False))\n\n# Whiskers drawn first so the box marks layer cleanly on top\nwhisker_rule = (\n    alt.Chart(stats_df)\n    .mark_rule(strokeWidth=2, color=INK_SOFT)\n    .encode(x=x_enc, y=\"whisker_lower:Q\", y2=\"whisker_upper:Q\")\n)\nlower_cap = (\n    alt.Chart(stats_df)\n    .mark_tick(size=26, thickness=2.5, color=INK_SOFT, opacity=1)\n    .encode(x=x_enc, y=\"whisker_lower:Q\")\n)\nupper_cap = (\n    alt.Chart(stats_df)\n    .mark_tick(size=26, thickness=2.5, color=INK_SOFT, opacity=1)\n    .encode(x=x_enc, y=\"whisker_upper:Q\")\n)\n\n# Notched box: lower box (Q1 -> notch_lower), waist (notch_lower -> notch_upper), upper box (notch_upper -> Q3)\nlower_box = (\n    alt.Chart(stats_df)\n    .mark_bar(size=48, stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        x=x_enc,\n        y=alt.Y(\"q1:Q\", title=\"Performance Score\"),\n        y2=\"notch_lower:Q\",\n        color=alt.Color(\"Department:N\", scale=color_scale, legend=None),\n        opacity=hover_opacity,\n        tooltip=tooltip_fields,\n    )\n    .add_params(hover)\n)\nupper_box = (\n    alt.Chart(stats_df)\n    .mark_bar(size=48, stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        x=x_enc,\n        y=\"notch_upper:Q\",\n        y2=\"q3:Q\",\n        color=alt.Color(\"Department:N\", scale=color_scale, legend=None),\n        opacity=hover_opacity,\n        tooltip=tooltip_fields,\n    )\n)\nnotch_box = (\n    alt.Chart(stats_df)\n    .mark_bar(size=26, stroke=PAGE_BG, strokeWidth=0.75)\n    .encode(\n        x=x_enc,\n        y=\"notch_lower:Q\",\n        y2=\"notch_upper:Q\",\n        color=alt.Color(\"Department:N\", scale=color_scale, legend=None),\n        opacity=hover_opacity,\n        tooltip=tooltip_fields,\n    )\n)\n# Median tick cut in the page background color — reads as a gap through the waist, theme-adaptive by construction\nmedian_line = (\n    alt.Chart(stats_df).mark_tick(color=PAGE_BG, size=26, thickness=2, opacity=1).encode(x=x_enc, y=\"median:Q\")\n)\n\n# Mean diamond — a second, distinct central-tendency marker beside the median notch\nmean_marker = (\n    alt.Chart(stats_df)\n    .mark_point(shape=\"diamond\", size=90, filled=True, color=INK, opacity=0.9, stroke=PAGE_BG, strokeWidth=1)\n    .encode(x=x_enc, y=\"mean:Q\", tooltip=tooltip_fields)\n)\n\n# On-canvas sample-size annotation, sitting in the near-zero whitespace under every box\nn_label = (\n    alt.Chart(stats_df)\n    .mark_text(fontSize=9, color=INK_SOFT, baseline=\"middle\", align=\"center\")\n    .encode(x=x_enc, y=alt.Y(\"n_label_y:Q\"), text=\"n_label:N\")\n)\n\noutliers_chart = (\n    alt.Chart(outliers_df)\n    .mark_point(size=70, filled=True, opacity=0.85, stroke=PAGE_BG, strokeWidth=1.2)\n    .encode(x=x_enc, y=alt.Y(\"Performance Score:Q\"), color=alt.Color(\"Department:N\", scale=color_scale, legend=None))\n    if len(outliers_df) > 0\n    else alt.Chart(pd.DataFrame()).mark_point()\n)\n\nchart = (\n    alt.layer(\n        whisker_rule,\n        lower_cap,\n        upper_cap,\n        lower_box,\n        upper_box,\n        notch_box,\n        median_line,\n        mean_marker,\n        n_label,\n        outliers_chart,\n    )\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"box-notched · python · altair · anyplot.ai\",\n            fontSize=16,\n            anchor=\"middle\",\n            color=INK,\n            subtitle=\"Non-overlapping notches ⇒ medians differ significantly (95% CI) · ◆ = mean\",\n            subtitleFontSize=11,\n            subtitleColor=INK_SOFT,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        labelFontSize=10,\n        titleFontSize=12,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_axisY(grid=True, gridColor=INK, gridOpacity=0.15)\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# PAD-only to the canonical 3200x1800 landscape target — see prompts/library/altair.md \"Canvas\".\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}x{_h}, exceeds target {TW}x{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}