MLTimeGraph

Chapter 3: Thresholds, zones and markers

A series on its own says what happened. Only a threshold says whether it was acceptable. That is the difference between a chart you look at and one you act on.

The line

The simplest case: a named limit as a dashed line.

thresholds: [{ name: 'crit', value: 22, color: '#e11d48' }]

// where the label goes: above, below, outside-left, outside-right
thresholds: [{ name: 'crit', value: 22, label: { text: 'CRITICAL', position: 'above' } }]
Named limits, drawn by the Go renderer — each label at the position it asks for.

Limits in view

The automatic value range follows the readings. With readings of 2–7 °C and limits at 0 and 10 °C, both limit lines fall outside the chart — exactly the lines it was meant to show. One switch on the axis widens the range to the thresholds drawn on it:

axes: { left: { includeThresholds: true } }

A threshold with line: 'none' is only a reference and does not count; an explicit domain wins.

Limits in view: the range widened to both limits
Limits in view: the range widened to both limits — open the live demo

The area

A line says where the limit is. An area says how long it was exceeded — and that is usually the real question.

The same threshold with the excursion zone filled.

Colour by value

Instead of one limit, several: the curve takes the colour of the zone it is currently in. Useful for graded assessments — green, amber, red — and dangerous when the grades are arbitrary, because the colour then implies a certainty the data does not carry.

Section-wise colouring by value range.

Marking periods

Sometimes the statement is not in the value but in the period: a maintenance window, an outage, a shift. Annotation bands sit behind the curve.

Shaded periods — maintenance windows, outages, shifts.

Two sensors that should agree

Two probes in one fridge are fine while they see the same thing. More than 2 °C apart, and something is wrong: one is icing up, one sits in the draught, one has drifted. A fill region between the two lines, with a condition, marks exactly that stretch:

series: [
  { ...top, style: { fill: { regions: [{
      from: 'series', to: { series: 'Bottom' },
      where: { exceeds: 2 },            // only where |Bottom − Top| > 2 °C
      fill: { color: '#dc262640' },
  }] } } },
  bottom,
]

The decision is made on the readings — linear between them, held on a step line — not on the drawn curve, whose smoothing may overshoot. That is the right basis for evidence. side: 'above' or 'below' asks for one direction only; maxAgeMs draws nothing where one probe's last reading is too old to say anything. divergenceExcursions() from ml-time-graph/analyze adds which probe ran away, the half you can act on.

Two probes, the stretch where they disagree by more than 2 °C
Two probes, the stretch where they disagree by more than 2 °C — open the live demo

How long outside

The question a GxP report asks is not whether the limit was exceeded, but for how long in total. computeLimitExcursions answers it:

import { computeLimitExcursions } from 'ml-time-graph/analyze';

const { msAboveHigh, msBelowLow, excursions } =
  computeLimitExcursions(data, { low: 2, high: 8 });
// excursions: [{ startTime, endTime, side, extremum, durationMs }]

Strictly outside — exactly 8 °C is inside. A gap ends an episode, and the time between two readings counts to the side where the second one lies. The Go port (analyze.ComputeLimitExcursions) computes the same, so the screen and the PDF report the same duration.

Time outside the limits, with a status strip under the axis
Time outside the limits, with a status strip under the axis — open the live demo

And interactively

The demo shows the same building blocks in the browser: six cards, each extending the previous one by exactly one idea. Code and values sit next to every step, on their own tabs.

Thresholds, step by step
Thresholds, step by step — open the live demo

From the same author

Licence: CC BY-NC 4.0