{"spec_id":"spc-xbar-r","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nspc-xbar-r: Statistical Process Control Chart (X-bar/R)\nLibrary: altair 6.2.1 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-20\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 — Imprint palette, theme-adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint palette — semantic roles for SPC chart\nBRAND = \"#009E73\"  # in-control data series (first series)\nOOC_COLOR = \"#AE3030\"  # out-of-control — semantic matte-red (bad/error)\nWARN_COLOR = \"#DDCC77\"  # warning limits — semantic amber (caution)\n\n# Data: CNC shaft diameter measurements, subgroups of n=5\nnp.random.seed(42)\nn_samples = 30\nn_per_sample = 5\ntarget_diameter = 25.0  # mm\nprocess_std = 0.05  # mm\n\nmeasurements = np.random.normal(target_diameter, process_std, (n_samples, n_per_sample))\nmeasurements[7] += 0.15\nmeasurements[16] -= 0.18\nmeasurements[23] += 0.20\n\nsample_means = measurements.mean(axis=1)\nsample_ranges = measurements.max(axis=1) - measurements.min(axis=1)\n\n# Control chart constants for n=5 (Shewhart)\nA2, D3, D4 = 0.577, 0.0, 2.114\n\nxbar_bar = sample_means.mean()\nr_bar = sample_ranges.mean()\nxbar_ucl = xbar_bar + A2 * r_bar\nxbar_lcl = xbar_bar - A2 * r_bar\nxbar_uwarn = xbar_bar + (2 / 3) * A2 * r_bar\nxbar_lwarn = xbar_bar - (2 / 3) * A2 * r_bar\n\nr_ucl = D4 * r_bar\nr_lcl = D3 * r_bar\nr_uwarn = r_bar + (2 / 3) * (r_ucl - r_bar)\nr_lwarn = r_bar - (2 / 3) * (r_bar - r_lcl)\n\nsamples = np.arange(1, n_samples + 1)\nx_domain = [0, n_samples + 1]\n\ndf_xbar = pd.DataFrame(\n    {\n        \"sample\": samples,\n        \"value\": sample_means,\n        \"ucl\": xbar_ucl,\n        \"lcl\": xbar_lcl,\n        \"center\": xbar_bar,\n        \"uwarn\": xbar_uwarn,\n        \"lwarn\": xbar_lwarn,\n    }\n)\ndf_xbar[\"ooc\"] = (df_xbar[\"value\"] > xbar_ucl) | (df_xbar[\"value\"] < xbar_lcl)\n\ndf_range = pd.DataFrame(\n    {\n        \"sample\": samples,\n        \"value\": sample_ranges,\n        \"ucl\": r_ucl,\n        \"lcl\": r_lcl,\n        \"center\": r_bar,\n        \"uwarn\": r_uwarn,\n        \"lwarn\": r_lwarn,\n    }\n)\ndf_range[\"ooc\"] = (df_range[\"value\"] > r_ucl) | (df_range[\"value\"] < r_lcl)\n\n# Zone shading bands (±1σ and ±2σ)\nxbar_zone_2s = pd.DataFrame({\"y\": [xbar_lwarn], \"y2\": [xbar_uwarn]})\nxbar_zone_1s = pd.DataFrame({\"y\": [xbar_bar - (1 / 3) * A2 * r_bar], \"y2\": [xbar_bar + (1 / 3) * A2 * r_bar]})\nr_zone_2s = pd.DataFrame({\"y\": [r_lwarn], \"y2\": [r_uwarn]})\nr_zone_1s = pd.DataFrame({\"y\": [r_bar - (1 / 3) * (r_bar - r_lcl)], \"y2\": [r_bar + (1 / 3) * (r_ucl - r_bar)]})\n\n# Inline label data\nxbar_labels_df = pd.DataFrame(\n    {\n        \"sample\": [2] * 5,\n        \"y\": [xbar_ucl, xbar_uwarn, xbar_bar, xbar_lwarn, xbar_lcl],\n        \"label\": [\"UCL\", \"+2σ\", \"CL\", \"−2σ\", \"LCL\"],\n        \"ltype\": [\"limit\", \"warn\", \"center\", \"warn\", \"limit\"],\n    }\n)\nr_labels_df = pd.DataFrame(\n    {\n        \"sample\": [2] * 5,\n        \"y\": [r_ucl, r_uwarn, r_bar, r_lwarn, r_lcl],\n        \"label\": [\"UCL\", \"+2σ\", \"CL\", \"−2σ\", \"LCL\"],\n        \"ltype\": [\"limit\", \"warn\", \"center\", \"warn\", \"limit\"],\n    }\n)\n\nlabel_color_scale = alt.Scale(domain=[\"limit\", \"warn\", \"center\"], range=[OOC_COLOR, WARN_COLOR, INK])\n\n# --- X-bar Chart ---\nxbar_zone2 = alt.Chart(xbar_zone_2s).mark_rect(color=\"#4467A3\", opacity=0.07).encode(y=\"y:Q\", y2=\"y2:Q\")\nxbar_zone1 = alt.Chart(xbar_zone_1s).mark_rect(color=\"#4467A3\", opacity=0.14).encode(y=\"y:Q\", y2=\"y2:Q\")\nxbar_line = (\n    alt.Chart(df_xbar)\n    .mark_line(color=BRAND, strokeWidth=2.5)\n    .encode(\n        x=alt.X(\n            \"sample:Q\", scale=alt.Scale(domain=x_domain, nice=False), axis=alt.Axis(title=\"\", tickMinStep=1, grid=False)\n        ),\n        y=alt.Y(\"value:Q\", scale=alt.Scale(zero=False), axis=alt.Axis(title=\"X̄  (mm)\")),\n    )\n)\nxbar_pts = (\n    alt.Chart(df_xbar[~df_xbar[\"ooc\"]])\n    .mark_point(color=BRAND, size=120, filled=True, stroke=PAGE_BG, strokeWidth=1)\n    .encode(\n        x=\"sample:Q\",\n        y=\"value:Q\",\n        tooltip=[alt.Tooltip(\"sample:Q\", title=\"Sample\"), alt.Tooltip(\"value:Q\", title=\"X̄\", format=\".4f\")],\n    )\n)\nxbar_ooc = (\n    alt.Chart(df_xbar[df_xbar[\"ooc\"]])\n    .mark_point(color=OOC_COLOR, size=240, filled=True, stroke=PAGE_BG, strokeWidth=1.5, shape=\"diamond\")\n    .encode(\n        x=\"sample:Q\",\n        y=\"value:Q\",\n        tooltip=[alt.Tooltip(\"sample:Q\", title=\"Sample\"), alt.Tooltip(\"value:Q\", title=\"X̄ (OOC)\", format=\".4f\")],\n    )\n)\nxbar_ucl_rule = alt.Chart(df_xbar).mark_rule(color=OOC_COLOR, strokeDash=[8, 4], strokeWidth=2).encode(y=\"ucl:Q\")\nxbar_lcl_rule = alt.Chart(df_xbar).mark_rule(color=OOC_COLOR, strokeDash=[8, 4], strokeWidth=2).encode(y=\"lcl:Q\")\nxbar_cl_rule = alt.Chart(df_xbar).mark_rule(color=INK, strokeWidth=2.5).encode(y=\"center:Q\")\nxbar_uwarn_rule = (\n    alt.Chart(df_xbar).mark_rule(color=WARN_COLOR, strokeDash=[4, 4], strokeWidth=1.5, opacity=0.85).encode(y=\"uwarn:Q\")\n)\nxbar_lwarn_rule = (\n    alt.Chart(df_xbar).mark_rule(color=WARN_COLOR, strokeDash=[4, 4], strokeWidth=1.5, opacity=0.85).encode(y=\"lwarn:Q\")\n)\nxbar_labels = (\n    alt.Chart(xbar_labels_df)\n    .mark_text(align=\"left\", dx=5, dy=-13, fontSize=13, fontWeight=\"bold\")\n    .encode(x=\"sample:Q\", y=\"y:Q\", text=\"label:N\", color=alt.Color(\"ltype:N\", scale=label_color_scale, legend=None))\n)\n\nxbar_chart = (\n    xbar_zone2\n    + xbar_zone1\n    + xbar_line\n    + xbar_pts\n    + xbar_ooc\n    + xbar_ucl_rule\n    + xbar_lcl_rule\n    + xbar_cl_rule\n    + xbar_uwarn_rule\n    + xbar_lwarn_rule\n    + xbar_labels\n).properties(width=620, height=160)\n\n# --- R Chart ---\nr_zone2 = alt.Chart(r_zone_2s).mark_rect(color=\"#4467A3\", opacity=0.07).encode(y=\"y:Q\", y2=\"y2:Q\")\nr_zone1 = alt.Chart(r_zone_1s).mark_rect(color=\"#4467A3\", opacity=0.14).encode(y=\"y:Q\", y2=\"y2:Q\")\nr_line = (\n    alt.Chart(df_range)\n    .mark_line(color=BRAND, strokeWidth=2.5)\n    .encode(\n        x=alt.X(\n            \"sample:Q\",\n            scale=alt.Scale(domain=x_domain, nice=False),\n            axis=alt.Axis(title=\"Sample Number\", tickMinStep=1, grid=False),\n        ),\n        y=alt.Y(\"value:Q\", scale=alt.Scale(zero=False), axis=alt.Axis(title=\"Range R (mm)\")),\n    )\n)\nr_pts = (\n    alt.Chart(df_range[~df_range[\"ooc\"]])\n    .mark_point(color=BRAND, size=120, filled=True, stroke=PAGE_BG, strokeWidth=1)\n    .encode(\n        x=\"sample:Q\",\n        y=\"value:Q\",\n        tooltip=[alt.Tooltip(\"sample:Q\", title=\"Sample\"), alt.Tooltip(\"value:Q\", title=\"Range\", format=\".4f\")],\n    )\n)\nr_ooc = (\n    alt.Chart(df_range[df_range[\"ooc\"]])\n    .mark_point(color=OOC_COLOR, size=240, filled=True, stroke=PAGE_BG, strokeWidth=1.5, shape=\"diamond\")\n    .encode(\n        x=\"sample:Q\",\n        y=\"value:Q\",\n        tooltip=[alt.Tooltip(\"sample:Q\", title=\"Sample\"), alt.Tooltip(\"value:Q\", title=\"Range (OOC)\", format=\".4f\")],\n    )\n)\nr_ucl_rule = alt.Chart(df_range).mark_rule(color=OOC_COLOR, strokeDash=[8, 4], strokeWidth=2).encode(y=\"ucl:Q\")\nr_lcl_rule = (\n    alt.Chart(df_range).mark_rule(color=OOC_COLOR, strokeDash=[4, 4], strokeWidth=1.5, opacity=0.5).encode(y=\"lcl:Q\")\n)\nr_cl_rule = alt.Chart(df_range).mark_rule(color=INK, strokeWidth=2.5).encode(y=\"center:Q\")\nr_uwarn_rule = (\n    alt.Chart(df_range)\n    .mark_rule(color=WARN_COLOR, strokeDash=[4, 4], strokeWidth=1.5, opacity=0.85)\n    .encode(y=\"uwarn:Q\")\n)\nr_lwarn_rule = (\n    alt.Chart(df_range)\n    .mark_rule(color=WARN_COLOR, strokeDash=[4, 4], strokeWidth=1.5, opacity=0.85)\n    .encode(y=\"lwarn:Q\")\n)\nr_labels = (\n    alt.Chart(r_labels_df)\n    .mark_text(align=\"left\", dx=5, dy=-13, fontSize=13, fontWeight=\"bold\")\n    .encode(x=\"sample:Q\", y=\"y:Q\", text=\"label:N\", color=alt.Color(\"ltype:N\", scale=label_color_scale, legend=None))\n)\n\nr_chart = (\n    r_zone2\n    + r_zone1\n    + r_line\n    + r_pts\n    + r_ooc\n    + r_ucl_rule\n    + r_lcl_rule\n    + r_cl_rule\n    + r_uwarn_rule\n    + r_lwarn_rule\n    + r_labels\n).properties(width=620, height=160)\n\n# Combined chart — title length-scaled fontSize (baseline 67 chars, default 16px)\ntitle_text = \"CNC Shaft Diameter Monitoring · spc-xbar-r · python · altair · anyplot.ai\"\n_n = len(title_text)\ntitle_fontsize = max(11, round(16 * 67 / _n)) if _n > 67 else 16\n\ncombined = alt.vconcat(xbar_chart, r_chart, spacing=15).properties(\n    background=PAGE_BG,\n    title=alt.Title(title_text, fontSize=title_fontsize, anchor=\"middle\", offset=10, fontWeight=\"bold\", color=INK),\n)\n\nchart = (\n    combined.configure_view(fill=PAGE_BG, strokeWidth=0)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.15,\n        gridDash=[2, 4],\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=12,\n    )\n    .configure_title(color=INK)\n)\n\n# Save PNG then pad to exact 3200×1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\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}×{_h}, exceeds {TW}×{TH}. Shrink chart width/height 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"}