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.
Two premium type modules for clinical and practice dashboards:
west-coast-charts/layers/vitals(about 11 kB gzip): chart typevitalsClock, a 24 h (ward) or 12 h (practice) clock of vital signs, and fourradialColumnlayer types:bands(threshold-band cells),trace(measurements at their true time),span(high / low pairs such as blood pressure) andvitalsNow(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 typeschedule, 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()andflow.fromEvents()(patient flow with median / P90 waiting times for the corerangeBarand the premiumsankey).
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
- vitalsClock
- Layers on a plain radialColumn
- schedule
- Headless helpers
- Patient IDs and privacy
Loading
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 orderWith 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.
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:
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+dashidentify a route, so it is readable without colour),items(appointments withresource,start,end,people– one appointment can need several people –, a pseudonymouspatient,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.
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:
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.