{"spec_id":"spc-xbar-r","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nspc-xbar-r: Statistical Process Control Chart (X-bar/R)\nLibrary: letsplot 4.10.1 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-06-20\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\n# Theme tokens (Imprint palette — 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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — data colors (theme-independent)\nDATA_COLOR = \"#009E73\"  # Imprint position 1 — always first series\nOOC_COLOR = \"#AE3030\"  # Imprint position 5 — semantic red for out-of-control\n\n# Data - CNC shaft diameter measurements (subgroups of n=5)\nnp.random.seed(42)\nn_samples = 30\ntarget_diameter = 25.0  # mm\nprocess_std = 0.05  # mm\n\n# Generate subgroup measurements\nmeasurements = np.random.normal(target_diameter, process_std, (n_samples, 5))\n\n# Inject mean shifts for X-bar out-of-control points\nmeasurements[7] += 0.15  # sudden upward shift\nmeasurements[18] -= 0.18  # sudden downward shift\nmeasurements[24] += 0.12  # upward drift\n\n# Inject extreme spread for R chart OOC at sample 12 (preserves mean, inflates range)\nmeasurements[11, 0] -= 0.16\nmeasurements[11, 4] += 0.16\n\nsample_ids = np.arange(1, n_samples + 1)\nsample_means = measurements.mean(axis=1)\nsample_ranges = measurements.max(axis=1) - measurements.min(axis=1)\n\n# Control chart constants for n=5 subgroups\nA2 = 0.577\nD3 = 0.0\nD4 = 2.114\n\n# X-bar chart limits\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_uwl = xbar_bar + (2 / 3) * A2 * r_bar\nxbar_lwl = xbar_bar - (2 / 3) * A2 * r_bar\n\n# R chart limits\nr_ucl = D4 * r_bar\nr_lcl = D3 * r_bar\nr_uwl = r_bar + (2 / 3) * (r_ucl - r_bar)\n\n# Classify in-control vs out-of-control\nxbar_ooc = (sample_means > xbar_ucl) | (sample_means < xbar_lcl)\nr_ooc = sample_ranges > r_ucl\n\n# DataFrames\ndf_xbar = pd.DataFrame(\n    {\"sample\": sample_ids, \"mean\": sample_means, \"status\": np.where(xbar_ooc, \"Out of Control\", \"In Control\")}\n)\n\ndf_r = pd.DataFrame(\n    {\"sample\": sample_ids, \"range\": sample_ranges, \"status\": np.where(r_ooc, \"Out of Control\", \"In Control\")}\n)\n\n# Limit line dataframes (each line = 2 rows: start and end sample)\nxbar_limits = pd.DataFrame(\n    {\n        \"sample\": np.tile([1, n_samples], 5),\n        \"y\": ([xbar_ucl] * 2 + [xbar_lcl] * 2 + [xbar_bar] * 2 + [xbar_uwl] * 2 + [xbar_lwl] * 2),\n        \"line\": [\"UCL\"] * 2 + [\"LCL\"] * 2 + [\"CL\"] * 2 + [\"UWL\"] * 2 + [\"LWL\"] * 2,\n    }\n)\n\nr_limits = pd.DataFrame(\n    {\n        \"sample\": np.tile([1, n_samples], 4),\n        \"y\": [r_ucl] * 2 + [r_lcl] * 2 + [r_bar] * 2 + [r_uwl] * 2,\n        \"line\": [\"UCL\"] * 2 + [\"LCL\"] * 2 + [\"CL\"] * 2 + [\"UWL\"] * 2,\n    }\n)\n\n# Label annotations placed just past last data point (within x-axis range)\nxbar_labels = pd.DataFrame(\n    {\"sample\": [n_samples + 0.5] * 3, \"y\": [xbar_ucl, xbar_bar, xbar_lcl], \"label\": [\"UCL\", \"CL\", \"LCL\"]}\n)\n\nr_labels = pd.DataFrame({\"sample\": [n_samples + 0.5] * 3, \"y\": [r_ucl, r_bar, r_lcl], \"label\": [\"UCL\", \"R̄\", \"LCL\"]})\n\n# Theme for top panel (X-bar chart — title here, no x-axis labels)\ntheme_top = theme(\n    plot_title=element_text(size=16, color=INK, face=\"bold\"),\n    axis_title_y=element_text(size=12, color=INK),\n    axis_text_y=element_text(size=10, color=INK_SOFT),\n    axis_text_x=element_blank(),\n    axis_title_x=element_blank(),\n    panel_grid_major_x=element_blank(),\n    panel_grid_minor=element_blank(),\n    panel_grid_major_y=element_line(color=INK_SOFT, size=0.25),\n    panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    axis_line_x=element_line(color=INK_SOFT, size=0.6),\n    axis_line_y=element_line(color=INK_SOFT, size=0.6),\n    axis_ticks=element_line(color=INK_SOFT, size=0.3),\n    legend_position=\"none\",\n    plot_margin=[20, 50, 5, 15],\n)\n\n# Theme for bottom panel (R chart — x-axis labels here)\ntheme_bottom = theme(\n    axis_title=element_text(size=12, color=INK),\n    axis_text=element_text(size=10, color=INK_SOFT),\n    panel_grid_major_x=element_blank(),\n    panel_grid_minor=element_blank(),\n    panel_grid_major_y=element_line(color=INK_SOFT, size=0.25),\n    panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    axis_line_x=element_line(color=INK_SOFT, size=0.6),\n    axis_line_y=element_line(color=INK_SOFT, size=0.6),\n    axis_ticks=element_line(color=INK_SOFT, size=0.3),\n    legend_position=\"none\",\n    plot_margin=[5, 50, 20, 15],\n)\n\n# X-bar chart\nxbar_plot = (\n    ggplot()\n    + geom_line(\n        data=xbar_limits[xbar_limits[\"line\"].isin([\"UWL\", \"LWL\"])],\n        mapping=aes(x=\"sample\", y=\"y\", group=\"line\"),\n        linetype=\"dotted\",\n        color=INK_MUTED,\n        size=0.8,\n    )\n    + geom_line(\n        data=xbar_limits[xbar_limits[\"line\"].isin([\"UCL\", \"LCL\"])],\n        mapping=aes(x=\"sample\", y=\"y\", group=\"line\"),\n        linetype=\"dashed\",\n        color=INK_SOFT,\n        size=1.0,\n    )\n    + geom_line(\n        data=xbar_limits[xbar_limits[\"line\"] == \"CL\"],\n        mapping=aes(x=\"sample\", y=\"y\"),\n        linetype=\"solid\",\n        color=INK,\n        size=0.9,\n    )\n    + geom_line(data=df_xbar, mapping=aes(x=\"sample\", y=\"mean\"), color=DATA_COLOR, size=1.5)\n    + geom_point(\n        data=df_xbar[df_xbar[\"status\"] == \"In Control\"],\n        mapping=aes(x=\"sample\", y=\"mean\"),\n        color=DATA_COLOR,\n        fill=PAGE_BG,\n        size=5,\n        shape=21,\n        stroke=1.5,\n    )\n    + geom_point(\n        data=df_xbar[df_xbar[\"status\"] == \"Out of Control\"],\n        mapping=aes(x=\"sample\", y=\"mean\"),\n        color=OOC_COLOR,\n        fill=OOC_COLOR,\n        size=6,\n        shape=21,\n        stroke=1.5,\n    )\n    + geom_text(\n        data=xbar_labels,\n        mapping=aes(x=\"sample\", y=\"y\", label=\"label\"),\n        size=4,\n        color=INK_SOFT,\n        fontface=\"bold\",\n        hjust=0,\n    )\n    + scale_x_continuous(breaks=list(range(1, n_samples + 1, 5)) + [n_samples], limits=[0.5, n_samples + 4.5])\n    + scale_y_continuous(expand=[0.12, 0])\n    + labs(title=\"spc-xbar-r · python · letsplot · anyplot.ai\", y=\"X̄ (Sample Mean, mm)\", x=\"\")\n    + theme_top\n    + ggsize(800, 260)\n)\n\n# R chart\nr_plot = (\n    ggplot()\n    + geom_line(\n        data=r_limits[r_limits[\"line\"] == \"UWL\"],\n        mapping=aes(x=\"sample\", y=\"y\"),\n        linetype=\"dotted\",\n        color=INK_MUTED,\n        size=0.8,\n    )\n    + geom_line(\n        data=r_limits[r_limits[\"line\"].isin([\"UCL\", \"LCL\"])],\n        mapping=aes(x=\"sample\", y=\"y\", group=\"line\"),\n        linetype=\"dashed\",\n        color=INK_SOFT,\n        size=1.0,\n    )\n    + geom_line(\n        data=r_limits[r_limits[\"line\"] == \"CL\"], mapping=aes(x=\"sample\", y=\"y\"), linetype=\"solid\", color=INK, size=0.9\n    )\n    + geom_line(data=df_r, mapping=aes(x=\"sample\", y=\"range\"), color=DATA_COLOR, size=1.5)\n    + geom_point(\n        data=df_r[df_r[\"status\"] == \"In Control\"],\n        mapping=aes(x=\"sample\", y=\"range\"),\n        color=DATA_COLOR,\n        fill=PAGE_BG,\n        size=5,\n        shape=21,\n        stroke=1.5,\n    )\n    + geom_point(\n        data=df_r[df_r[\"status\"] == \"Out of Control\"],\n        mapping=aes(x=\"sample\", y=\"range\"),\n        color=OOC_COLOR,\n        fill=OOC_COLOR,\n        size=6,\n        shape=21,\n        stroke=1.5,\n    )\n    + geom_text(\n        data=r_labels, mapping=aes(x=\"sample\", y=\"y\", label=\"label\"), size=4, color=INK_SOFT, fontface=\"bold\", hjust=0\n    )\n    + scale_x_continuous(breaks=list(range(1, n_samples + 1, 5)) + [n_samples], limits=[0.5, n_samples + 4.5])\n    + scale_y_continuous(expand=[0.15, 0])\n    + labs(x=\"Sample Number\", y=\"R (Sample Range, mm)\")\n    + theme_bottom\n    + ggsize(800, 190)\n)\n\n# Add tooltip layers for HTML interactivity (lets-plot distinctive feature)\nxbar_interactive = xbar_plot + geom_point(\n    data=df_xbar,\n    mapping=aes(x=\"sample\", y=\"mean\"),\n    size=5,\n    alpha=0.01,\n    tooltips=layer_tooltips().line(\"Sample|@sample\").line(\"X̄|@mean\").line(\"Status|@status\").format(\"mean\", \".4f\"),\n)\n\nr_interactive = r_plot + geom_point(\n    data=df_r,\n    mapping=aes(x=\"sample\", y=\"range\"),\n    size=5,\n    alpha=0.01,\n    tooltips=layer_tooltips().line(\"Sample|@sample\").line(\"Range|@range\").line(\"Status|@status\").format(\"range\", \".4f\"),\n)\n\n# Combine top (X-bar, 260) + bottom (R, 190) = 800×450 logical → 3200×1800 at scale=4\ncombined = gggrid([xbar_plot, r_plot], ncol=1)\ncombined_interactive = gggrid([xbar_interactive, r_interactive], ncol=1)\n\n# Save\nggsave(combined, f\"plot-{THEME}.png\", scale=4, path=\".\")\nggsave(combined_interactive, f\"plot-{THEME}.html\", path=\".\")\n"}