Control Chart (XmR)
A control chart (XmR, also called a process behaviour chart) answers "did something actually change, or is this just noise?" for any metric you track over time: weekly signups, defect counts, delivery times, support volume. It draws the metric with its natural process limits, plus a companion Moving Range panel showing point-to-point volatility.
Following the XmR method, limits come from the average moving range, not the standard deviation, and points are flagged by three rules: outside the process limits (strong signal, red), three of four successive points closer to a limit than the center line (moderate, orange), and eight or more consecutive points on one side of the center line (weak, yellow).
CustomChartDef control_chart {
label: 'Control Chart (XmR)'
description: 'Use this chart to separate routine variation from real change, with XmR process limits and color-coded signal detection rules.'
fields {
field period {
label: 'Period'
type: 'dimension'
sort {
apply_order: 1
direction: 'asc'
}
}
field value {
label: 'Value'
type: 'measure'
data_type: 'number'
sort {
apply_order: 2
direction: 'asc'
}
}
}
options {
option line_color {
label: 'Line Color'
type: 'color-picker'
default_value: '#255dd4'
}
option show_mr_chart {
label: 'Show Moving Range chart'
type: 'toggle'
default_value: true
}
option show_tooltip {
label: 'Show tooltip'
type: 'toggle'
default_value: true
}
}
template: @vg {
"$schema": "https://vega.github.io/schema/vega/v5.json",
"description": "XmR process behaviour chart: X panel with natural process limits, Moving Range panel, and color-coded signal rules.",
"autosize": "none",
"padding": {"left": 44, "top": 6, "right": 8, "bottom": 4},
"signals": [
{
"name": "width",
"init": "containerSize()[0] - 52",
"on": [{ "events": "window:resize", "update": "containerSize()[0] - 52" }]
},
{
"name": "height",
"init": "containerSize()[1] - 64",
"on": [{ "events": "window:resize", "update": "containerSize()[1] - 64" }]
},
{"name": "showMr", "update": "@{options.show_mr_chart.value}"},
{"name": "plotW", "update": "width - 110"},
{"name": "xH", "update": "showMr ? (height - 48) * 0.62 : height - 18"},
{"name": "mrTop", "update": "18 + xH + 30"},
{"name": "mrH", "update": "showMr ? height - mrTop : 1"},
{
"name": "hoveredPeriod",
"value": null,
"on": [
{"events": "symbol:mouseover", "update": "datum.period"},
{"events": "symbol:mouseout", "update": "null"}
]
}
],
"data": [
{
"name": "source",
"values": @{values},
"transform": [
{"type": "formula", "expr": "datum['@{fields.period.name}']", "as": "period"},
{"type": "formula", "expr": "datum['@{fields.value.name}']", "as": "amount"},
{"type": "filter", "expr": "datum.amount != null"},
{"type": "collect", "sort": {"field": "period"}},
{"type": "window", "ops": ["lag"], "fields": ["amount"], "as": ["prev_amount"]},
{
"type": "formula",
"expr": "datum.prev_amount == null ? null : abs(datum.amount - datum.prev_amount)",
"as": "mr"
},
{"type": "joinaggregate", "fields": ["amount", "mr"], "ops": ["mean", "mean"], "as": ["xbar", "mrbar"]},
{"type": "formula", "expr": "datum.xbar + 2.66 * datum.mrbar", "as": "unpl"},
{"type": "formula", "expr": "datum.xbar - 2.66 * datum.mrbar", "as": "lnpl"},
{"type": "formula", "expr": "datum.xbar + 1.33 * datum.mrbar", "as": "uql"},
{"type": "formula", "expr": "datum.xbar - 1.33 * datum.mrbar", "as": "lql"},
{"type": "formula", "expr": "3.268 * datum.mrbar", "as": "mr_url"},
{"type": "formula", "expr": "datum.amount > datum.xbar ? 1 : 0", "as": "above"},
{"type": "formula", "expr": "datum.amount < datum.xbar ? 1 : 0", "as": "below"},
{"type": "formula", "expr": "datum.amount > datum.uql ? 1 : 0", "as": "qabove"},
{"type": "formula", "expr": "datum.amount < datum.lql ? 1 : 0", "as": "qbelow"},
{
"type": "window",
"ops": ["sum", "sum"],
"fields": ["qabove", "qbelow"],
"as": ["qabove4", "qbelow4"],
"frame": [-3, 0]
},
{
"type": "window",
"ops": ["sum", "sum"],
"fields": ["above", "below"],
"as": ["above8", "below8"],
"frame": [-7, 0]
},
{
"type": "formula",
"expr": "datum.amount > datum.unpl || datum.amount < datum.lnpl",
"as": "rule1"
},
{
"type": "formula",
"expr": "(datum.qabove === 1 && datum.qabove4 >= 3) || (datum.qbelow === 1 && datum.qbelow4 >= 3)",
"as": "rule2"
},
{
"type": "formula",
"expr": "(datum.above === 1 && datum.above8 >= 8) || (datum.below === 1 && datum.below8 >= 8)",
"as": "rule3"
},
{
"type": "formula",
"expr": "datum.rule1 ? 'strong' : (datum.rule2 ? 'moderate' : (datum.rule3 ? 'weak' : 'none'))",
"as": "severity"
},
{
"type": "formula",
"expr": "datum.rule1 ? 'Outside process limits' : (datum.rule2 ? '3 of 4 points near a limit' : (datum.rule3 ? 'Long run on one side of center' : 'Routine variation'))",
"as": "signal_desc"
}
]
},
{
"name": "stats",
"source": "source",
"transform": [
{"type": "aggregate", "fields": ["amount", "mr"], "ops": ["mean", "mean"], "as": ["xbar", "mrbar"]},
{"type": "formula", "expr": "datum.xbar + 2.66 * datum.mrbar", "as": "unpl"},
{"type": "formula", "expr": "datum.xbar - 2.66 * datum.mrbar", "as": "lnpl"},
{"type": "formula", "expr": "3.268 * datum.mrbar", "as": "mr_url"}
]
}
],
"scales": [
{
"name": "x",
"type": "point",
"domain": {"data": "source", "field": "period", "sort": true},
"range": [0, {"signal": "plotW"}],
"padding": 0.5
},
{
"name": "severityColor",
"type": "ordinal",
"domain": ["none", "weak", "moderate", "strong"],
"range": [@{options.line_color.value}, "#eab308", "#f97316", "#e5484d"]
}
],
"axes": [
{
"orient": "bottom",
"scale": "x",
"domain": false,
"ticks": false,
"labelPadding": 8,
"labelFontSize": 12,
"labelOverlap": true
}
],
"marks": [
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"value": 0},
"y": {"value": 10},
"text": {"value": "Individual Values (X)"},
"fontSize": {"value": 11},
"fontWeight": {"value": 600},
"fill": {"value": "#6b7280"}
}
}
},
{
"type": "symbol",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "plotW - 330"},
"y": {"value": 8},
"fill": {"value": "#e5484d"},
"size": {"value": 50}
}
}
},
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "plotW - 322"},
"y": {"value": 8},
"baseline": {"value": "middle"},
"text": {"value": "Outside limits"},
"fontSize": {"value": 10},
"fill": {"value": "#6b7280"}
}
}
},
{
"type": "symbol",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "plotW - 225"},
"y": {"value": 8},
"fill": {"value": "#f97316"},
"size": {"value": 50}
}
}
},
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "plotW - 217"},
"y": {"value": 8},
"baseline": {"value": "middle"},
"text": {"value": "Near limit (3 of 4)"},
"fontSize": {"value": 10},
"fill": {"value": "#6b7280"}
}
}
},
{
"type": "symbol",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "plotW - 95"},
"y": {"value": 8},
"fill": {"value": "#eab308"},
"size": {"value": 50}
}
}
},
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "plotW - 87"},
"y": {"value": 8},
"baseline": {"value": "middle"},
"text": {"value": "One-side run"},
"fontSize": {"value": 10},
"fill": {"value": "#6b7280"}
}
}
},
{
"type": "group",
"name": "xChart",
"encode": {
"update": {"x": {"value": 0}, "y": {"value": 18}, "width": {"signal": "plotW"}, "height": {"signal": "xH"}}
},
"scales": [
{
"name": "yx",
"type": "linear",
"nice": true,
"zero": false,
"domain": {"data": "source", "fields": ["amount", "unpl", "lnpl"]},
"range": [{"signal": "xH"}, 10]
}
],
"axes": [
{
"orient": "left",
"scale": "yx",
"domain": false,
"ticks": false,
"labelFontSize": 12,
"labelPadding": 8,
"grid": true,
"gridColor": "#F3F4F6"
}
],
"marks": [
{
"type": "rule",
"from": {"data": "stats"},
"encode": {
"update": {
"x": {"value": 0},
"x2": {"signal": "plotW"},
"y": {"scale": "yx", "field": "unpl"},
"stroke": {"value": "#9ba1a6"},
"strokeDash": {"value": [4, 4]}
}
}
},
{
"type": "rule",
"from": {"data": "stats"},
"encode": {
"update": {
"x": {"value": 0},
"x2": {"signal": "plotW"},
"y": {"scale": "yx", "field": "lnpl"},
"stroke": {"value": "#9ba1a6"},
"strokeDash": {"value": [4, 4]}
}
}
},
{
"type": "rule",
"from": {"data": "stats"},
"encode": {
"update": {
"x": {"value": 0},
"x2": {"signal": "plotW"},
"y": {"scale": "yx", "field": "xbar"},
"stroke": {"value": "#e5484d"},
"strokeDash": {"value": [6, 4]}
}
}
},
{
"type": "text",
"interactive": false,
"from": {"data": "stats"},
"encode": {
"update": {
"x": {"signal": "plotW + 8"},
"y": {"scale": "yx", "field": "unpl"},
"baseline": {"value": "middle"},
"text": {"signal": "'Upper limit ' + format(datum.unpl, ',.4')"},
"fontSize": {"value": 10},
"fill": {"value": "#9ba1a6"}
}
}
},
{
"type": "text",
"interactive": false,
"from": {"data": "stats"},
"encode": {
"update": {
"x": {"signal": "plotW + 8"},
"y": {"scale": "yx", "field": "lnpl"},
"baseline": {"value": "middle"},
"text": {"signal": "'Lower limit ' + format(datum.lnpl, ',.4')"},
"fontSize": {"value": 10},
"fill": {"value": "#9ba1a6"}
}
}
},
{
"type": "text",
"interactive": false,
"from": {"data": "stats"},
"encode": {
"update": {
"x": {"signal": "plotW + 8"},
"y": {"scale": "yx", "field": "xbar"},
"baseline": {"value": "middle"},
"text": {"signal": "'Average ' + format(datum.xbar, ',.4')"},
"fontSize": {"value": 10},
"fill": {"value": "#e5484d"}
}
}
},
{
"type": "rule",
"interactive": false,
"encode": {
"update": {
"x": {"scale": "x", "signal": "hoveredPeriod"},
"y": {"value": 4},
"y2": {"signal": "xH"},
"stroke": {"value": "#9ba1a6"},
"strokeDash": {"value": [3, 3]},
"opacity": {"signal": "hoveredPeriod === null ? 0 : 0.7"}
}
}
},
{
"type": "line",
"from": {"data": "source"},
"encode": {
"update": {
"x": {"scale": "x", "field": "period"},
"y": {"scale": "yx", "field": "amount"},
"stroke": {"value": @{options.line_color.value}},
"strokeWidth": {"value": 1.5}
}
}
},
{
"type": "symbol",
"from": {"data": "source"},
"encode": {
"update": {
"x": {"scale": "x", "field": "period"},
"y": {"scale": "yx", "field": "amount"},
"fill": {"scale": "severityColor", "field": "severity"},
"size": {"signal": "datum.period === hoveredPeriod ? 130 : (datum.severity === 'none' ? 40 : 90)"},
"stroke": {"value": "white"},
"strokeWidth": {"value": 1},
"tooltip": {
"signal": "@{options.show_tooltip.value} ? {'Period': datum.period, 'Value': format(datum.amount, ','), 'Signal': datum.signal_desc, 'Avg': format(datum.xbar, ',.4'), 'Limits': format(datum.lnpl, ',.4') + ' to ' + format(datum.unpl, ',.4')} : null"
}
}
}
}
]
},
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"value": 0},
"y": {"signal": "mrTop - 8"},
"text": {"value": "Moving Range"},
"fontSize": {"value": 11},
"fontWeight": {"value": 600},
"fill": {"value": "#6b7280"},
"opacity": {"signal": "showMr ? 1 : 0"}
}
}
},
{
"type": "group",
"name": "mrChart",
"encode": {
"update": {
"x": {"value": 0},
"y": {"signal": "mrTop"},
"width": {"signal": "plotW"},
"height": {"signal": "mrH"},
"opacity": {"signal": "showMr ? 1 : 0"}
}
},
"scales": [
{
"name": "ymr",
"type": "linear",
"nice": true,
"zero": true,
"domain": {"data": "source", "fields": ["mr", "mr_url"]},
"range": [{"signal": "mrH"}, 6]
}
],
"axes": [
{
"orient": "left",
"scale": "ymr",
"domain": false,
"ticks": false,
"labelFontSize": 12,
"labelPadding": 8,
"grid": true,
"gridColor": "#F3F4F6",
"tickCount": 3
}
],
"marks": [
{
"type": "rule",
"from": {"data": "stats"},
"encode": {
"update": {
"x": {"value": 0},
"x2": {"signal": "plotW"},
"y": {"scale": "ymr", "field": "mr_url"},
"stroke": {"value": "#9ba1a6"},
"strokeDash": {"value": [4, 4]},
"opacity": {"signal": "showMr ? 1 : 0"}
}
}
},
{
"type": "rule",
"from": {"data": "stats"},
"encode": {
"update": {
"x": {"value": 0},
"x2": {"signal": "plotW"},
"y": {"scale": "ymr", "field": "mrbar"},
"stroke": {"value": "#e5484d"},
"strokeDash": {"value": [6, 4]},
"opacity": {"signal": "showMr ? 1 : 0"}
}
}
},
{
"type": "text",
"interactive": false,
"from": {"data": "stats"},
"encode": {
"update": {
"x": {"signal": "plotW + 8"},
"y": {"scale": "ymr", "field": "mr_url"},
"baseline": {"value": "middle"},
"text": {"signal": "'Range limit ' + format(datum.mr_url, ',.4')"},
"fontSize": {"value": 10},
"fill": {"value": "#9ba1a6"},
"opacity": {"signal": "showMr ? 1 : 0"}
}
}
},
{
"type": "rule",
"interactive": false,
"encode": {
"update": {
"x": {"scale": "x", "signal": "hoveredPeriod"},
"y": {"value": 0},
"y2": {"signal": "mrH"},
"stroke": {"value": "#9ba1a6"},
"strokeDash": {"value": [3, 3]},
"opacity": {"signal": "showMr && hoveredPeriod !== null ? 0.7 : 0"}
}
}
},
{
"type": "line",
"from": {"data": "source"},
"encode": {
"update": {
"x": {"scale": "x", "field": "period"},
"y": {"scale": "ymr", "field": "mr"},
"stroke": {"value": @{options.line_color.value}},
"strokeWidth": {"value": 1.2},
"defined": {"signal": "datum.mr != null"},
"opacity": {"signal": "showMr ? 1 : 0"}
}
}
},
{
"type": "symbol",
"from": {"data": "source"},
"encode": {
"update": {
"x": {"scale": "x", "field": "period"},
"y": {"scale": "ymr", "field": "mr"},
"fill": {"signal": "datum.mr != null && datum.mr > datum.mr_url ? '#e5484d' : '@{options.line_color.value}'"},
"size": {"signal": "datum.period === hoveredPeriod ? 120 : (datum.mr != null && datum.mr > datum.mr_url ? 80 : 30)"},
"opacity": {"signal": "showMr && datum.mr != null ? 1 : 0"},
"tooltip": {
"signal": "@{options.show_tooltip.value} && datum.mr != null ? {'Period': datum.period, 'Moving Range': format(datum.mr, ',.4'), 'Avg MR': format(datum.mrbar, ',.4'), 'Upper Range Limit': format(datum.mr_url, ',.4')} : null"
}
}
}
}
]
}
]
};;
}