Englische Referenzdokumentation. Der technische Originaltext und die Codebeispiele bleiben unverändert auf Englisch; die Website-Navigation ist deutsch.

Vitals Clock and Practice Schedule

Two separate Premium type modules: vital-sign time slots, practice resources and controlled schedule changes. Explicit imports, caller-defined bands and alarms, missing measurements, schedule helpers and redaction. Examples use invented demo data.

View .md Agent setup

Two premium type modules for clinical and practice dashboards:

  • west-coast-charts/layers/vitals (about 11 kB gzip): chart type vitalsClock, a 24 h (ward) or 12 h (practice) clock of vital signs, and four radialColumn layer types: bands (threshold-band cells), trace (measurements at their true time), span (high / low pairs such as blood pressure) and vitalsNow (the now hand, caller alarm wedges and a strip with the last value per parameter).
  • west-coast-charts/layers/schedule (about 14 kB gzip): chart type schedule, a practice day or week with rooms, devices or people as lanes, a route per doctor, conflicts and a vertical board, plus headless helpers: conflicts(), utilization(), utilizationSeries() and flow.fromEvents() (patient flow with median / P90 waiting times for the core rangeBar and the premium sankey).

A chart library, not a medical device. Neither module computes a score (no NEWS2, no MEWS), decides an alarm or ships a clinical threshold. Bands, levels, alarms and scores always come from your software; the chart only draws them. The sample pages use invented demo thresholds and data and say so on the chart.

Loading

js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
import vitals from 'west-coast-charts/layers/vitals'
import schedule from 'west-coast-charts/layers/schedule'
WestCoastCharts.use([premium, vitals, schedule]) // any order

With script tags, load dist/layers/vitals.min.js and / or dist/layers/schedule.min.js after the core and the premium script; they install themselves. With script tags the schedule helpers are on the global WestCoastChartsLayer_schedule.

Unlike quint, these two modules are not autoloaded: premium does not know the names vitalsClock and schedule (that keeps the premium bundle small). Without the module the core says the type is unknown; load it with use() or a script tag.

vitalsClock

The data lives in plotOptions.vitalsClock (the series stay empty). Each parameter is a list of measurements [time, value] (or [time, high, low] for kind: 'range'), time as ms or ISO string. The chart puts them on time slots (time.step, default 60 min) and draws one ring per parameter:

kind ring
columns the column ring: one bar per slot (median by default, aggregate changes it)
cells threshold-band cells (colour and pattern per level, so it works without colour)
trace a line at the true measurement times, bands as background zones
range high / low pairs (e.g. systolic / diastolic)

A slot without a measurement is not measured (hatched), never 0. Slots after now are "no data yet". The last value of each parameter and its age appear under the ring; staleAfter (minutes) flags an old value. now is a shared cutoff: measurements later than it do not enter the dial, aggregates, description, table or last-value strip until now advances.

For kind: 'range', bands classifies the high value and bandsLow classifies the low value. Use separate bands for different measures such as systolic and diastolic pressure. If bandsLow is absent, the vitals clock defaults to bandsLowPolicy: 'high-only': it classifies only the high value and emits one warning per parameter unless the policy is explicit. Set bandsLowPolicy: 'shared' only when both values should use the same thresholds. Explicit bandsLow always takes precedence. These options only select caller-supplied bands; the library does not infer any medical thresholds.

js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
import vitals from 'west-coast-charts/layers/vitals'
WestCoastCharts.use([premium, vitals])

const t0 = Date.parse('2026-10-26T07:00:00+01:00')
const h = (n) => t0 + n * 3600000
// invented demo bands, not clinical thresholds
const demo = [{ to: 90, level: 2 }, { to: 94, level: 1 }, { level: 0 }]
const el = document.createElement('div')
document.body.appendChild(el)
const chart = new WestCoastCharts(el, {
  chart: { type: 'vitalsClock', height: 420 },
  series: [],
  plotOptions: {
    vitalsClock: {
      time: { start: t0, span: '12h', step: '60min' },
      now: h(5.3),
      timeZone: 'Europe/Berlin',
      params: [
        { id: 'hr', title: 'Heart rate', short: 'HR', unit: '/min', kind: 'columns', data: [[h(0.2), 72], [h(2.1), 80], [h(4.6), 77]] },
        { id: 'spo2', title: 'SpO₂', short: 'SpO₂', unit: '%', kind: 'cells', aggregate: 'min', bands: demo, data: [[h(0.2), 97], [h(2.1), 93], [h(4.6), 96]] },
      ],
      demo: 'Demo data',
    },
  },
})
await chart.render()

describe() says what is shown, per parameter range, median, the slots per level and the not-measured slots, e.g. "SpO₂ (%, lowest per slot): 93–97 …; 9 slots not measured". api(chart).lastValues() returns the strip's values; api(chart).append(id, [[t, v], ...]) adds measurements to that parameter's internal data and redraws at most once a second (a light live feed for demos). It returns false for an unknown parameter or a non-array input. It copies the appended tuples and does not mutate the caller's original options object. A retained API object continues to return the current last values after redraws.

By default followNow: false, appending future measurements does not advance now; those measurements stay excluded from every output channel until the caller advances the time with updateOptions(). For a live feed, set plotOptions.vitalsClock.followNow: true: append() advances the internal now to the newest valid appended timestamp, never backwards. The status strip, dial, table and description then update together on the throttled redraw. This does not slide the configured time.start/span window; the caller controls the window and persists any history it needs. Supplying replacement data in updateOptions() replaces the corresponding internal data, so keep the authoritative dataset in the application.

The centre (core) takes any radialColumn layer of yours, e.g. a matrix of your own scores. events (e.g. medication) are marked on the dial; alarms are drawn as wedges and listed, never computed. Orient (features/orient) turns the clock like any radialColumn.

Layers on a plain radialColumn

The four layer types also work in plotOptions.radialColumn.layers of a normal radialColumn:

js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
import vitals from 'west-coast-charts/layers/vitals'
WestCoastCharts.use([premium, vitals])

const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + ':00')
const el = document.createElement('div')
document.body.appendChild(el)
const chart = new WestCoastCharts(el, {
  chart: { type: 'radialColumn', height: 420 },
  series: [{ name: 'Measurements', data: hours.map((_, i) => 2 + (i % 3)) }],
  xaxis: { categories: hours },
  plotOptions: {
    radialColumn: {
      layers: [{
        type: 'bands',
        rows: [{
          name: 'SpO₂', unit: '%', aggregate: 'min',
          bands: [{ to: 90, level: 2 }, { to: 94, level: 1 }, { level: 0 }],
          data: hours.map((_, i) => (i % 5 === 0 ? null : 95 - (i % 4))),
        }],
      }],
    },
  },
})
await chart.render()

A plain span layer retains the shared-band fallback for compatibility: without bandsLow or bandsLowPolicy it classifies both values with bands and warns once per layer. State the policy explicitly: bandsLowPolicy: 'shared' for a common scale, bandsLowPolicy: 'high-only' to grade only the high value, or separate bandsLow for the low value. This differs from the safer high-only default of vitalsClock range parameters.

schedule

plotOptions.schedule holds the practice model and the view options:

  • resources (rooms, devices, staff), people (doctors and staff; color + dash identify a route, so it is readable without colour), items (appointments with resource, start, end, people – one appointment can need several people –, a pseudonymous patient, label, status),
  • hours (open, close, days, breaks; closed time is hatched), rules.minTransfer (default 5 min),
  • date, range: 'day' | 'week', timeZone, now.

Times are ms, ISO strings with offset, or 'HH:MM' on date (wall clock in timeZone).

view: 'lanes' draws one horizontal lane per room (lanes: 'people': per person), view: 'board' a vertical day or week board (columns = days × rooms); 'auto' switches to the board below 560 px.

Conflicts are drawn and listed in the header: a person in two rooms at once and a double-booked room are errors; a changeover between two rooms below minTransfer and an appointment outside the opening hours are warnings. The chart is controlled: a drag, resize or keyboard move (Enter, arrows, Enter) never changes your data, it emits scheduleChange with { id, from, to, conflicts, added, resolved } and shows a ghost; your software decides and redraws.

api(chart).propose(id, { start, end, resource, people }) accepts the same time formats as items, including 'HH:MM' on the model's date in timeZone. Omitting end when changing start preserves the appointment's duration. Invalid times or an end before the start produce a warning and return null, without changing the current proposal or the source appointments.

Proposal conflict sets describe the whole schedule, independently of a display filter: conflicts contains all conflicts after the proposed move, added contains new conflicts and resolved contains removed conflicts. This includes indirect changes, such as a new transfer conflict between two appointments after the appointment between them moves away. Conflict IDs include the conflict type and its resource/person where relevant; pair IDs remain stable if the appointments change chronological order. Treat them as opaque identifiers. A change from a transfer warning to a person-overlap error resolves the former and adds the latter.

js
import WestCoastCharts from 'west-coast-charts'
import premium from 'west-coast-charts/premium'
import schedule from 'west-coast-charts/layers/schedule'
WestCoastCharts.use([premium, schedule])

const el = document.createElement('div')
document.body.appendChild(el)
const chart = new WestCoastCharts(el, {
  chart: {
    type: 'schedule', height: 320, width: 900,
    events: { scheduleChange: (e) => console.log('proposal', e.id, e.conflicts.length) },
  },
  series: [],
  plotOptions: {
    schedule: {
      date: '2026-10-26',
      timeZone: 'Europe/Berlin',
      hours: { open: '08:00', close: '12:00' },
      resources: [{ id: 'R1', name: 'Room 1' }, { id: 'R2', name: 'Room 2' }],
      people: [{ id: 'D1', name: 'Dr. A' }],
      items: [
        { id: 'a', resource: 'R1', start: '08:00', end: '08:30', people: ['D1'], patient: 'P-1042' },
        { id: 'b', resource: 'R2', start: '08:20', end: '08:40', people: ['D1'], patient: 'P-2210' },
      ],
    },
  },
})
await chart.render()

Events (in chart.events): scheduleChange (a proposal), scheduleSelect, slotClick ({ time, lane, by, day }), scheduleConflict (once per render with conflicts). api(chart): conflicts(), utilization(), propose(), proposal(), clearProposal(), select(), highlightPerson(), selection(), view().

Headless helpers

The schedule module object carries helpers that need no chart:

js
import schedule from 'west-coast-charts/layers/schedule'

const day = {
  date: '2026-10-26', timeZone: 'Europe/Berlin', hours: { open: '08:00', close: '12:00' },
  resources: [{ id: 'R1', name: 'Room 1' }],
  items: [{ id: 'a', resource: 'R1', start: '08:00', end: '09:00' }],
}
const list = schedule.conflicts(day)            // [] here
const u = schedule.utilization(day, { bucket: '60min' })
// u.value = 25 (%); closed buckets are null, never 0 %
const series = schedule.utilizationSeries(u)    // series for a core heatmap
if (list.length || u.value !== 25 || series.length !== 1) throw new Error('unexpected')

Closed buckets should read as closed, not as the lowest colour: with the chart types bundle (west-coast-charts/types) loaded, plotOptions.heatmap.empty: { label: 'closed' } draws null cells of a heatmap or calendar with a neutral hatch, keeps them out of the colour scale, adds a legend entry next to the colorScale.ranges (legend: false drops it; color sets the hatch ink) and puts the label into the tooltip, the data table and describe().

schedule.flow.fromEvents(visits) takes per patient the stage times (checkin, room, lab, exam, checkout) and returns the segments, the transitions with median and P90 waiting times (never a sum or mean of waits), a rangeBar series, sankey links and a sankey tooltip plugin that adds patients / median / P90 to each flow.

Patient IDs and privacy

Use pseudonymous IDs only. With the default redact: true the patient ID appears nowhere in library-generated descriptions, tooltips, keyboard announcements, focus labels, appointment sublabels or the data table; exported drawings inherit that redaction. redact: false allows these patient fields to be shown (for staff screens). A display hint such as showPatient: true does not override redact: true.

Redaction controls generated output, not the source data stored in memory. Do not put patient identifiers into appointment id/label, resource names or custom callback output: arbitrary caller text is not rewritten by this option. The charts send nothing anywhere.

WestCoast Charts 1.1.5 · generated from the library guide at commit 95f093ba. Code examples in the guide are executed by the library's docs check.