# ControlChart

> Package `@umriss-ui/charts`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/charts/controlchart/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/charts/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/charts/llms-full.txt).

Centre line, control limits and the rule violations of a process (an SPC or Shewhart chart). Reach for it to see a process drift before it leaves its specification.

```ts
import { ControlChart, controlLimits, violations } from "@umriss-ui/charts";
```

Demo page: https://romanhaendler.github.io/umriss-ui/charts/controlchart/

Control limits are computed from a named reference window, never from what happens to be visible; specification limits are chosen and drawn with `LimitLine` ([ADR-0008](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0008-control-limits-are-not-specification-limits.md)).

The rules - outlier, run, trend, two of three - are exported as plain functions, so the violations can stand beside the chart as text.

## Examples

### Chart a process against its control limits

In the plant: `origin` names the reference window the limits come from; the burner's overshoot breaks them while every tile stays in specification.

```tsx
import { ControlChart, LimitLine, Tooltip, useChart } from "@umriss-ui/charts";

/* Data from the plant world, written out here so the example runs on its own. */

/** A small LCG - reproducible across runs and platforms. */
function random(seed: number): () => number {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

/** The early shift, 06:00 to 14:00. */
const SHIFT_MINUTES = 480;

/** The kiln's zone 3, in °C: the numbers every part reads it against, once.
    Between the two warnings lies the tolerance - a tile fired outside it is
    scrap; `returnTo` is the alarm's dead band. */
const KILN = { target: 1200, tolerance: [1185, 1215], alarm: 1230, returnTo: 1222 } as const;

/** The kiln fires one tile every quarter of a minute when nothing holds it up -
    the ideal cycle time of the OEE's performance. */
const IDEAL_CYCLE_MINUTES = 0.25;

/** One minute of the line. */
interface Reading {
  /** Minutes since the start of the shift. */
  minute: number;
  /** Zone 3 of the kiln, in °C. */
  kiln: number;
  /** The exit pyrometer, in °C - `null` while it sends nothing. */
  exit: number | null;
  /** Whether the belt runs. */
  running: boolean;
  /** Tiles out of the kiln in this minute. */
  fired: number;
  /** Of them, the ones fired inside the tolerance. */
  good: number;
}

/** A tile taken off the belt and measured - one every ten minutes while it runs. */
interface Sample {
  minute: number;
  /** The tile's length after firing, in mm (nominal 600). */
  length: number;
}

/** A batch of tiles through the three stations. */
interface Batch {
  id: string;
  name: string;
  /** How many tiles the batch is planned for. */
  tiles: number;
  /** Minutes since the start of the shift, per station. */
  press: readonly [number, number];
  dryer: readonly [number, number];
  kiln: readonly [number, number];
}

interface Plant {
  readings: readonly Reading[];
  samples: readonly Sample[];
  batches: readonly Batch[];
}

/* When the exit pyrometer goes silent, and for how long. Fixed rather than
   seeded, so that a running control room shows its reading turn stale, then
   lost, and come back. */
const SILENT_FROM = 288;

const SILENT_FOR = 50;

/** The shift, from a seed. */
function plant(seed: number): Plant {
  const r = random(seed);

  /* The one excursion: a burner overshoots somewhere between 02:30 and 03:30
     into the shift, for half an hour, peaking near 1242 °C. The operator stops
     the feed twenty-two minutes in, for eighteen minutes. The noise around it
     is too small to reach a warning on its own, so this is the only crossing -
     which is what the tests hold. */
  const surgeFrom = 150 + Math.floor(r() * 60);
  const surgeFor = 30;
  const stopFrom = surgeFrom + 22;
  const stopFor = 18;

  /* The plan: batches through the kiln one after another, each pressed an
     hour and dried half an hour before - the first ones during the night
     shift. Tiles come out of the kiln only while a batch is in it, so the
     OEE's count and the batches' counts are the same tiles. The plan is a
     plan: a stop of the belt does not move it. */
  const batches: Batch[] = [];
  for (let at = 0, i = 0; at < SHIFT_MINUTES; i++) {
    const length = 70 + Math.floor(r() * 25);
    batches.push({
      id: `b-${4121 + i}`,
      name: `B-${4121 + i}`,
      tiles: Math.round(length / IDEAL_CYCLE_MINUTES / 10) * 10,
      press: [at - 60, at - 60 + length],
      dryer: [at - 30, at - 30 + length],
      kiln: [at, at + length],
    });
    at += length + 5;
  }

  const readings: Reading[] = [];
  const samples: Sample[] = [];
  let drift = 0;
  for (let minute = 0; minute < SHIFT_MINUTES; minute++) {
    drift = 0.9 * drift + (r() - 0.5) * 3;
    const into = minute - surgeFrom;
    const surge = into >= 0 && into <= surgeFor ? 42 * Math.sin((Math.PI * into) / surgeFor) : 0;
    const kiln = Math.round((KILN.target + drift + surge) * 10) / 10;

    const running = !(minute >= stopFrom && minute < stopFrom + stopFor);
    const loaded = batches.some((batch) => batch.kiln[0] <= minute && minute < batch.kiln[1]);
    const jam = r() < 0.12;
    const fired = running && loaded ? (jam ? 3 : 4) : 0;
    const inTolerance = kiln >= KILN.tolerance[0] && kiln <= KILN.tolerance[1];
    const silent = minute >= SILENT_FROM && minute < SILENT_FROM + SILENT_FOR;
    const exit = silent ? null : Math.round((kiln - 380 + (r() - 0.5) * 4) * 10) / 10;

    readings.push({ minute, kiln, exit, running, fired, good: inTolerance ? fired : 0 });

    if (running && minute % 10 === 0) {
      /* Hotter shrinks more: the length follows the kiln, so the control
         chart sees the excursion the trend sees. */
      const length = 600 + (KILN.target - kiln) * 0.05 + (r() - 0.5) * 0.6;
      samples.push({ minute, length: Math.round(length * 100) / 100 });
    }
  }

  return { readings, samples, batches };
}

/* A tile measured off the belt every ten minutes of the early shift. */
const SAMPLES = plant(7).samples;

/* The first fifteen samples, 06:00 to 08:20: the kiln demonstrably ran in
   control. Never the samples that happen to be visible (ADR-0008). */
const REFERENCE_WINDOW = { kind: "referenceWindow", from: 0, to: 15 } as const;

export default function ControlChartExample() {
  const { Chart, XAxis, YAxis } = useChart(SAMPLES);
  return (
    <Chart height={300} ariaLabel="Control chart of the tile length after firing">
      <XAxis value="minute" label="Minute of the shift" />
      <YAxis label="mm" />
      {/* Specification limits are chosen, control limits are computed. */}
      <LimitLine value={602.5} severity="alarm" label="USL" />
      <LimitLine value={597.5} severity="alarm" label="LSL" />
      <ControlChart
        value="length"
        data={SAMPLES}
        origin={REFERENCE_WINDOW}
        name="Tile length"
        labelUpper="UCL"
        labelLower="LCL"
        violationName="Tile length - rule violation"
      />
      <Tooltip mode="x" />
    </Chart>
  );
}
```

### List the rule violations

In the plant: `onViolations` hands over the verdict the chart marks, so the same finds can stand beside it as text.

```tsx
import { useState } from "react";
import { ControlChart, Tooltip, useChart } from "@umriss-ui/charts";
import type { RuleName, Violation } from "@umriss-ui/charts";

/* Data from the plant world, written out here so the example runs on its own. */

/** A small LCG - reproducible across runs and platforms. */
function random(seed: number): () => number {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

/** The early shift, 06:00 to 14:00. */
const SHIFT_MINUTES = 480;

/** The kiln's zone 3, in °C: the numbers every part reads it against, once.
    Between the two warnings lies the tolerance - a tile fired outside it is
    scrap; `returnTo` is the alarm's dead band. */
const KILN = { target: 1200, tolerance: [1185, 1215], alarm: 1230, returnTo: 1222 } as const;

/** The kiln fires one tile every quarter of a minute when nothing holds it up -
    the ideal cycle time of the OEE's performance. */
const IDEAL_CYCLE_MINUTES = 0.25;

/** One minute of the line. */
interface Reading {
  /** Minutes since the start of the shift. */
  minute: number;
  /** Zone 3 of the kiln, in °C. */
  kiln: number;
  /** The exit pyrometer, in °C - `null` while it sends nothing. */
  exit: number | null;
  /** Whether the belt runs. */
  running: boolean;
  /** Tiles out of the kiln in this minute. */
  fired: number;
  /** Of them, the ones fired inside the tolerance. */
  good: number;
}

/** A tile taken off the belt and measured - one every ten minutes while it runs. */
interface Sample {
  minute: number;
  /** The tile's length after firing, in mm (nominal 600). */
  length: number;
}

/** A batch of tiles through the three stations. */
interface Batch {
  id: string;
  name: string;
  /** How many tiles the batch is planned for. */
  tiles: number;
  /** Minutes since the start of the shift, per station. */
  press: readonly [number, number];
  dryer: readonly [number, number];
  kiln: readonly [number, number];
}

interface Plant {
  readings: readonly Reading[];
  samples: readonly Sample[];
  batches: readonly Batch[];
}

/* When the exit pyrometer goes silent, and for how long. Fixed rather than
   seeded, so that a running control room shows its reading turn stale, then
   lost, and come back. */
const SILENT_FROM = 288;

const SILENT_FOR = 50;

/** The shift, from a seed. */
function plant(seed: number): Plant {
  const r = random(seed);

  /* The one excursion: a burner overshoots somewhere between 02:30 and 03:30
     into the shift, for half an hour, peaking near 1242 °C. The operator stops
     the feed twenty-two minutes in, for eighteen minutes. The noise around it
     is too small to reach a warning on its own, so this is the only crossing -
     which is what the tests hold. */
  const surgeFrom = 150 + Math.floor(r() * 60);
  const surgeFor = 30;
  const stopFrom = surgeFrom + 22;
  const stopFor = 18;

  /* The plan: batches through the kiln one after another, each pressed an
     hour and dried half an hour before - the first ones during the night
     shift. Tiles come out of the kiln only while a batch is in it, so the
     OEE's count and the batches' counts are the same tiles. The plan is a
     plan: a stop of the belt does not move it. */
  const batches: Batch[] = [];
  for (let at = 0, i = 0; at < SHIFT_MINUTES; i++) {
    const length = 70 + Math.floor(r() * 25);
    batches.push({
      id: `b-${4121 + i}`,
      name: `B-${4121 + i}`,
      tiles: Math.round(length / IDEAL_CYCLE_MINUTES / 10) * 10,
      press: [at - 60, at - 60 + length],
      dryer: [at - 30, at - 30 + length],
      kiln: [at, at + length],
    });
    at += length + 5;
  }

  const readings: Reading[] = [];
  const samples: Sample[] = [];
  let drift = 0;
  for (let minute = 0; minute < SHIFT_MINUTES; minute++) {
    drift = 0.9 * drift + (r() - 0.5) * 3;
    const into = minute - surgeFrom;
    const surge = into >= 0 && into <= surgeFor ? 42 * Math.sin((Math.PI * into) / surgeFor) : 0;
    const kiln = Math.round((KILN.target + drift + surge) * 10) / 10;

    const running = !(minute >= stopFrom && minute < stopFrom + stopFor);
    const loaded = batches.some((batch) => batch.kiln[0] <= minute && minute < batch.kiln[1]);
    const jam = r() < 0.12;
    const fired = running && loaded ? (jam ? 3 : 4) : 0;
    const inTolerance = kiln >= KILN.tolerance[0] && kiln <= KILN.tolerance[1];
    const silent = minute >= SILENT_FROM && minute < SILENT_FROM + SILENT_FOR;
    const exit = silent ? null : Math.round((kiln - 380 + (r() - 0.5) * 4) * 10) / 10;

    readings.push({ minute, kiln, exit, running, fired, good: inTolerance ? fired : 0 });

    if (running && minute % 10 === 0) {
      /* Hotter shrinks more: the length follows the kiln, so the control
         chart sees the excursion the trend sees. */
      const length = 600 + (KILN.target - kiln) * 0.05 + (r() - 0.5) * 0.6;
      samples.push({ minute, length: Math.round(length * 100) / 100 });
    }
  }

  return { readings, samples, batches };
}

const SAMPLES = plant(7).samples;
const REFERENCE_WINDOW = { kind: "referenceWindow", from: 0, to: 15 } as const;

const RULES: Record<RuleName, string> = {
  outlier: "Beyond a control limit",
  run: "A run on one side of the centre",
  trend: "A steady climb or fall",
  twoOfThree: "Two of three near a limit",
};

const clock = (minute: number) => {
  const total = 6 * 60 + minute;
  return `${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`;
};

export default function ListTheViolations() {
  const [found, setFound] = useState<readonly Violation[]>([]);
  const { Chart, XAxis, YAxis } = useChart(SAMPLES);
  return (
    <div className="side-by-side">
      <div style={{ flex: "1 1 320px" }}>
        <Chart height={240} ariaLabel="Control chart of the tile length, its violations listed beside it">
          <XAxis value="minute" label="Minute of the shift" />
          <YAxis label="mm" />
          <ControlChart value="length" data={SAMPLES} origin={REFERENCE_WINDOW} name="Tile length" onViolations={setFound} />
          <Tooltip mode="x" />
        </Chart>
      </div>
      <ul className="side-note">
        {found.length === 0 ? (
          <li>No rule violated.</li>
        ) : (
          found.map((v) => (
            <li key={v.rule}>
              <strong>{RULES[v.rule]}</strong>: {v.indices.map((i) => clock(SAMPLES[i]!.minute)).join(", ")}
            </li>
          ))
        )}
      </ul>
    </div>
  );
}
```

### Choose the rules

In the plant: `rules` switches rules off and sets their run lengths - here rule 4 is off and a run counts from eight - and without `zoneLines` the sigma zones stay unmarked.

```tsx
import { useState } from "react";
import { ControlChart, Tooltip, useChart } from "@umriss-ui/charts";
import type { RuleName, Violation } from "@umriss-ui/charts";

/* Data from the plant world, written out here so the example runs on its own. */

/** A small LCG - reproducible across runs and platforms. */
function random(seed: number): () => number {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

/** The early shift, 06:00 to 14:00. */
const SHIFT_MINUTES = 480;

/** The kiln's zone 3, in °C: the numbers every part reads it against, once.
    Between the two warnings lies the tolerance - a tile fired outside it is
    scrap; `returnTo` is the alarm's dead band. */
const KILN = { target: 1200, tolerance: [1185, 1215], alarm: 1230, returnTo: 1222 } as const;

/** The kiln fires one tile every quarter of a minute when nothing holds it up -
    the ideal cycle time of the OEE's performance. */
const IDEAL_CYCLE_MINUTES = 0.25;

/** One minute of the line. */
interface Reading {
  /** Minutes since the start of the shift. */
  minute: number;
  /** Zone 3 of the kiln, in °C. */
  kiln: number;
  /** The exit pyrometer, in °C - `null` while it sends nothing. */
  exit: number | null;
  /** Whether the belt runs. */
  running: boolean;
  /** Tiles out of the kiln in this minute. */
  fired: number;
  /** Of them, the ones fired inside the tolerance. */
  good: number;
}

/** A tile taken off the belt and measured - one every ten minutes while it runs. */
interface Sample {
  minute: number;
  /** The tile's length after firing, in mm (nominal 600). */
  length: number;
}

/** A batch of tiles through the three stations. */
interface Batch {
  id: string;
  name: string;
  /** How many tiles the batch is planned for. */
  tiles: number;
  /** Minutes since the start of the shift, per station. */
  press: readonly [number, number];
  dryer: readonly [number, number];
  kiln: readonly [number, number];
}

interface Plant {
  readings: readonly Reading[];
  samples: readonly Sample[];
  batches: readonly Batch[];
}

/* When the exit pyrometer goes silent, and for how long. Fixed rather than
   seeded, so that a running control room shows its reading turn stale, then
   lost, and come back. */
const SILENT_FROM = 288;

const SILENT_FOR = 50;

/** The shift, from a seed. */
function plant(seed: number): Plant {
  const r = random(seed);

  /* The one excursion: a burner overshoots somewhere between 02:30 and 03:30
     into the shift, for half an hour, peaking near 1242 °C. The operator stops
     the feed twenty-two minutes in, for eighteen minutes. The noise around it
     is too small to reach a warning on its own, so this is the only crossing -
     which is what the tests hold. */
  const surgeFrom = 150 + Math.floor(r() * 60);
  const surgeFor = 30;
  const stopFrom = surgeFrom + 22;
  const stopFor = 18;

  /* The plan: batches through the kiln one after another, each pressed an
     hour and dried half an hour before - the first ones during the night
     shift. Tiles come out of the kiln only while a batch is in it, so the
     OEE's count and the batches' counts are the same tiles. The plan is a
     plan: a stop of the belt does not move it. */
  const batches: Batch[] = [];
  for (let at = 0, i = 0; at < SHIFT_MINUTES; i++) {
    const length = 70 + Math.floor(r() * 25);
    batches.push({
      id: `b-${4121 + i}`,
      name: `B-${4121 + i}`,
      tiles: Math.round(length / IDEAL_CYCLE_MINUTES / 10) * 10,
      press: [at - 60, at - 60 + length],
      dryer: [at - 30, at - 30 + length],
      kiln: [at, at + length],
    });
    at += length + 5;
  }

  const readings: Reading[] = [];
  const samples: Sample[] = [];
  let drift = 0;
  for (let minute = 0; minute < SHIFT_MINUTES; minute++) {
    drift = 0.9 * drift + (r() - 0.5) * 3;
    const into = minute - surgeFrom;
    const surge = into >= 0 && into <= surgeFor ? 42 * Math.sin((Math.PI * into) / surgeFor) : 0;
    const kiln = Math.round((KILN.target + drift + surge) * 10) / 10;

    const running = !(minute >= stopFrom && minute < stopFrom + stopFor);
    const loaded = batches.some((batch) => batch.kiln[0] <= minute && minute < batch.kiln[1]);
    const jam = r() < 0.12;
    const fired = running && loaded ? (jam ? 3 : 4) : 0;
    const inTolerance = kiln >= KILN.tolerance[0] && kiln <= KILN.tolerance[1];
    const silent = minute >= SILENT_FROM && minute < SILENT_FROM + SILENT_FOR;
    const exit = silent ? null : Math.round((kiln - 380 + (r() - 0.5) * 4) * 10) / 10;

    readings.push({ minute, kiln, exit, running, fired, good: inTolerance ? fired : 0 });

    if (running && minute % 10 === 0) {
      /* Hotter shrinks more: the length follows the kiln, so the control
         chart sees the excursion the trend sees. */
      const length = 600 + (KILN.target - kiln) * 0.05 + (r() - 0.5) * 0.6;
      samples.push({ minute, length: Math.round(length * 100) / 100 });
    }
  }

  return { readings, samples, batches };
}

const SAMPLES = plant(7).samples;
const REFERENCE_WINDOW = { kind: "referenceWindow", from: 0, to: 15 } as const;

/* Western Electric's run of eight in place of Nelson's nine; "two of three
   near a limit" is not watched on this line, so its zones need no lines. */
const CHOSEN = { twoOfThree: false, runLength: 8 } as const;

const RULES: Record<RuleName, string> = {
  outlier: "Beyond a control limit",
  run: "Eight on one side of the centre",
  trend: "A steady climb or fall",
  twoOfThree: "Two of three near a limit",
};

const clock = (minute: number) => {
  const total = 6 * 60 + minute;
  return `${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`;
};

export default function ChooseTheRules() {
  const [found, setFound] = useState<readonly Violation[]>([]);
  const { Chart, XAxis, YAxis } = useChart(SAMPLES);
  return (
    <div className="side-by-side">
      <div style={{ flex: "1 1 320px" }}>
        <Chart height={240} ariaLabel="Control chart of the tile length under three of the four rules">
          <XAxis value="minute" label="Minute of the shift" />
          <YAxis label="mm" />
          <ControlChart
            value="length"
            data={SAMPLES}
            origin={REFERENCE_WINDOW}
            name="Tile length"
            rules={CHOSEN}
            zoneLines={false}
            onViolations={setFound}
          />
          <Tooltip mode="x" />
        </Chart>
      </div>
      <ul className="side-note">
        {found.length === 0 ? (
          <li>No rule violated.</li>
        ) : (
          found.map((v) => (
            <li key={v.rule}>
              <strong>{RULES[v.rule]}</strong>: {v.indices.map((i) => clock(SAMPLES[i]!.minute)).join(", ")}
            </li>
          ))
        )}
      </ul>
    </div>
  );
}
```

## When to use something else

- Limits you set yourself → LimitLine

## Keyboard

The keys of [Chart](https://romanhaendler.github.io/umriss-ui/charts/chart/#keyboard-chart) apply here.

## API

### `ControlChartProps<T>`

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#type-Value)`<T>` | — | The values of the chart - a number field of its `data`'s rows or a function of them.<br>Shown in: [Chart a process against its control limits](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#control-chart), [List the rule violations](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#list-the-violations), [Choose the rules](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#choose-the-rules) and 1 more |
| `data` *required* | `readonly T[]` | — | Required: the limits arise out of these values, not out of what happens to be visible.<br>Shown in: [Chart a process against its control limits](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#control-chart), [List the rule violations](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#list-the-violations), [Choose the rules](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#choose-the-rules) and 1 more |
| `origin` *required* | [`ControlLimitOrigin`](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#type-ControlLimitOrigin) | — | Given, or from a named reference window - never tacitly from everything on the screen ([ADR-0008](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0008-control-limits-are-not-specification-limits.md)).<br>Shown in: [Chart a process against its control limits](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#control-chart), [List the rule violations](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#list-the-violations), [Choose the rules](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#choose-the-rules) and 1 more |
| `xAxisId` | `string` | `"x"` | Binding to an x axis. |
| `yAxisId` | `string` | `"y"` | Binding to a y axis. |
| `name` | `string` | "Series n", after its place in the chart | The name in legend and tooltip. Without one a warning stands in DEV - an unnamed series is a colour nobody can look up.<br>Shown in: [Chart a process against its control limits](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#control-chart), [List the rule violations](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#list-the-violations), [Choose the rules](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#choose-the-rules) and 1 more |
| `format` | `(value: number) => string` | the y axis' `tickFormat`, else the built-in number format | The value as the tooltip writes it - on the line and on the violations. |
| `color` | `string` | the palette's `--uc-series-N` | Any CSS colour value. |
| `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value for the line; the theme resolves it. The violations are always "alarm". |
| `rules` | `Partial<`[`RuleOptions`](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#type-RuleOptions)`>` | — | Switchable one by one; run lengths are parameters.<br>Shown in: [Choose the rules](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#choose-the-rules) |
| `zoneLines` | `boolean` | `true` | The faint zone lines at one and two sigma. Rule 4 is about the two-sigma zone; without them a reader cannot check it.<br>Shown in: [Choose the rules](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#choose-the-rules) |
| `labelUpper` | `string` | no label | Label of the upper control limit in the axis band. The package brings no text along, not even "UCL" and "LCL".<br>Shown in: [Chart a process against its control limits](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#control-chart), [Hold the tile length in control (Scenarios)](https://romanhaendler.github.io/umriss-ui/charts/#hold-the-tile-length-in-control) |
| `labelLower` | `string` | no label | The same for the lower control limit. Named separately because a chart may well want one of the two labelled and not the other.<br>Shown in: [Chart a process against its control limits](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#control-chart), [Hold the tile length in control (Scenarios)](https://romanhaendler.github.io/umriss-ui/charts/#hold-the-tile-length-in-control) |
| `violationName` | `string` | "Series n", after its place in the chart | Name of the scatter of violations in the legend and the tooltip. Without one a legend warns once in DEV. Before this, a German suffix stood on the name of the chart.<br>Shown in: [Chart a process against its control limits](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#control-chart), [Hold the tile length in control (Scenarios)](https://romanhaendler.github.io/umriss-ui/charts/#hold-the-tile-length-in-control) |

#### Events

| Prop | Type | Default | Description |
|---|---|---|---|
| `onViolations` | `(found: readonly `[`Violation`](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#type-Violation)`[]) => void` | — | The violations as data - they can be listed beside the chart as well as marked on it.<br>Shown in: [List the rule violations](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#list-the-violations), [Choose the rules](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#choose-the-rules) |

### Types on this page

#### `Value<T>`

A value read from the row ([ADR-0048](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0048-charts-are-bound-to-their-rows-by-the-hook.md)): the name of a number field, compared
by its name, or a function, compared by its source text (`ValueFunction`).

```ts
type Value<T> = NumberField<T> | ValueFunction<T>;
```

#### `ControlLimitOrigin`

Where the limits come from. There are exactly these two possibilities.

```ts
type ControlLimitOrigin =
  /** From a baseline the caller has fixed. Taken as it is - even where the
      series would yield something else out of itself. */
  | { readonly kind: "given"; readonly center: number; readonly sigma: number }
  /** From exactly this index range of the series, half-open [from, to). */
  | { readonly kind: "referenceWindow"; readonly from: number; readonly to: number };
```

#### `RuleOptions`

Which rules run, and with which run lengths.

| Prop | Type | Default | Description |
|---|---|---|---|
| `outlier` *required* | `boolean` | — |  |
| `run` *required* | `boolean` | — |  |
| `trend` *required* | `boolean` | — |  |
| `twoOfThree` *required* | `boolean` | — |  |
| `runLength` *required* | `number` | — | Points on one side of the centre line from which rule 2 fires. |
| `trendLength` *required* | `number` | — | Points in succession from which rule 3 fires. |

#### `Violation`

A rule and the indices it violates.

| Prop | Type | Default | Description |
|---|---|---|---|
| `rule` *required* | [`RuleName`](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#type-RuleName)<br>`"outlier" \| "run" \| "trend" \| "twoOfThree"` | — | <br>Shown in: [List the rule violations](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#list-the-violations), [Choose the rules](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#choose-the-rules), [Hold the tile length in control (Scenarios)](https://romanhaendler.github.io/umriss-ui/charts/#hold-the-tile-length-in-control) |
| `indices` *required* | `readonly number[]` | — | <br>Shown in: [List the rule violations](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#list-the-violations), [Choose the rules](https://romanhaendler.github.io/umriss-ui/charts/controlchart/#choose-the-rules), [Hold the tile length in control (Scenarios)](https://romanhaendler.github.io/umriss-ui/charts/#hold-the-tile-length-in-control) |

#### `NumberField<T>`

The fields of a row whose value is a number, or absent.

```ts
type NumberField<T> = NoInfer<
  {
    [K in keyof T & string]-?: T[K] extends number | null | undefined ? K : never;
  }[keyof T & string]
>;
```

#### `ValueFunction<T>`

A **value** computed from the row - the function form of `Value`.
Compared by its source text, not its identity - an inline function is new
on every render. Known limit: one that reads a
changed closure variable under the same text is not run again; pass a new
data reference then. A native or bound function is compared by identity.

```ts
type ValueFunction<T> = (d: T, index: number) => number | null | undefined;
```

#### `RuleName`

Name of a rule, the way it is switched on and off in `violations`.

```ts
type RuleName = "outlier" | "run" | "trend" | "twoOfThree";
```
