CwResponsiveLineChart

A Canvas2D, touch-first multi-series time chart for farm sensor telemetry. Drag horizontally to pan, pinch (two fingers) or Ctrl + scroll to zoom, tap to lock the crosshair, double-tap to reset. Built-in support for dual Y-axes, a value-mapped temperature gradient, named threshold lines, anomaly markers, explicit data-gap bands, night-time shading, and a fully responsive shell that picks a layout from its container width.

Full demo โ€” 5 series, dual axes, gradient temperature

The reference dataset: ~7 days of two-minute samples for air temperature, air humidity, soil moisture, soil EC, and COโ‚‚. Air temperature uses the value-mapped gradient (cold โ†’ blue, hot โ†’ red) and carries a Frost threshold at 0ย ยฐC. Every visible series gets its own colored Y axis โ€” stacked on the left and right as you toggle chips in the legend below the chart.

North Greenhouse ยท Bay 3
Live sensor telemetry ยท one Y-axis per series
Sensors ยท 7 of 7 on
Full configuration Copy code
<script lang="ts">
	import { CwResponsiveLineChart } from '@cropwatchdevelopment/cwui';
	import type { CwResponsiveLineSeries } from '@cropwatchdevelopment/cwui';

	const series: CwResponsiveLineSeries[] = [
		{
			id: 'airTemp',
			label: 'Air temperature',
			unit: 'ยฐC',
			color: '#ef4444',
			gradient: true,           // value-mapped temperature gradient
			data: airTempPoints,      // [{ t: epochMs, v: number | null }]
			decimals: 1,
			gapMs: 8 * 60 * 1000,     // mark > 8-min jumps as "no signal"
			thresholds: [{ value: 0, label: 'Frost', color: 'rgba(59,130,246,0.6)' }]
		},
		{
			id: 'airHumidity',
			label: 'Air humidity',
			unit: '%RH',
			color: '#3b82f6',
			data: humidityPoints,
			decimals: 0
		}
	];
</script>

<CwResponsiveLineChart
	{series}
	title="North Greenhouse ยท Bay 3"
	subtitle="Live sensor telemetry"
	initialRange="24h"
	themeAuto
/>

Soil profile โ€” moisture + EC, color-matched axes

Pass only the series you need. Each gets its own axis colored to match the line; toggle a chip to hide both line and axis. With two series, one axis lands on each side.

Soil profile ยท Bay 3
Moisture and electrical conductivity
Sensors ยท 2 of 2 on
Soil-focused example Copy code
<CwResponsiveLineChart
	series={soilSeries}
	title="Soil profile"
	subtitle="Last 24 hours"
	initialRange="24h"
	height={420}
/>

Compact / bare โ€” dashboard tile

For dense cards, drop the chrome: bare, showLegend=false, showThemeToggle=false, and an empty ranges array leave just the canvas (and tooltip on hover/touch).

Compact tile Copy code
<CwResponsiveLineChart
	series={miniSeries}
	bare
	height={220}
	showLegend={false}
	showThemeToggle={false}
	ranges={[]}
/>

Forced dark theme

Pass theme="dark" (or bind it) for surfaces that always render dark. Pair with themeAuto to follow the OS color-scheme preference instead.

North Greenhouse ยท Bay 3
Dark surface ยท 7-day window
Sensors ยท 7 of 7 on
Documentation Upgrade

Start here

CwResponsiveLineChart is a Canvas2D, touch-first multi-series time chart for farm sensor telemetry. Every visible series gets its own Y-axis colored to match its line; axes auto-distribute (or use the per-series `axisSide` opt-in) and stack on each side. The legend lives below the chart so the canvas stays as wide as possible. Built-in features: value-mapped temperature gradient, named threshold lines, anomaly markers, explicit data-gap bands, night-time shading, light/dark theming (manual or auto), and a responsive shell.

How to think about it

  1. Shape your data as { t, v } points Each series carries a `data` array of `{ t: epochMs, v: number | null }` points sorted ascending by `t`. Use `v: null` for a missing reading โ€” combined with `gapMs`, this draws an explicit "no signal" band.
  2. Pick an id, label, unit, and color per series `id` is referenced by `initialHidden`, legend chips, and onchange events. `color` drives both the line and its dedicated Y-axis ticks/unit label. Set `gradient: true` to use the value-mapped temperature gradient instead (the axis still falls back to `color`).
  3. Each visible series gets its own axis Axes auto-distribute: series index 0 โ†’ left, 1 โ†’ right, 2 โ†’ left, etc. Override per series with `axisSide: 'left' | 'right'` when you want to force a side. Hiding a series via the legend removes both its line and its axis, which also reclaims the horizontal space.
  4. Toggle visibility with the legend below the chart The legend chips live under the canvas as a horizontal wrap to maximize chart width. Click a chip to hide/show its series; the chip dims, the line disappears, and its Y-axis collapses. `L`/`R` badges indicate which side each visible series is on.
  5. Tune the chrome to the surface Toggle `showLegend`, `showLegendStats`, `showThemeToggle`, `showAnomalies`, `showThresholds`, `showDataGaps`, and `showNightBands` to strip or restore individual layers. For dense dashboard tiles, set `bare`, drop the legend, and pass `ranges={[]}` to hide the time-range pills.
  6. Pick a theme Pass `theme="light"` or `theme="dark"` for a fixed surface. Pass `themeAuto` to follow `prefers-color-scheme` (the manual toggle button hides automatically in auto mode). You can bind `theme` to react to user actions outside the component.
  7. Use the built-in gestures Drag horizontally (one finger / mouse) pans the X view; a one-finger vertical swipe scrolls the page so the chart never traps the gesture. Two-finger pinch zooms with the midpoint as anchor. On desktop you must hold Ctrl (or โŒ˜) while scrolling to zoom โ€” Ctrl+Shift+scroll pans โ€” so a plain wheel scrolls past the chart. A single tap/click locks the crosshair; double-tap/double-click resets the zoom to the full data range.
  8. Listen for state changes Provide `onchange` to be notified when the user pans, zooms, toggles a series, or picks a range. The handler receives `{ viewStart, viewEnd, hidden, legendStats }` โ€” use it to drive deep-linkable URLs or persist user preferences.

Props and data shape

All controls (range pills, theme toggle, legend chips) are built in. The component renders to a single internal canvas; no SVG fallback. Bindable props: `theme`.

APITypeDetails
series Required
CwResponsiveLineSeries[]Series rendered on the chart. Each series carries its own `data: { t, v }[]`, color, unit, and optional thresholds.
series[].id
stringStable identifier referenced by axis props, legend toggles, and onchange events.
series[].label
stringHuman-readable label shown in legend chips, tooltip rows, and axis selectors.
series[].unit
stringUnit suffix shown beside last-value readouts and axis crowns (e.g. "ยฐC", "%RH", "ppm").
series[].color
stringSolid line color. Ignored when `gradient` is true.
series[].gradient

Default: false

booleanWhen true, color each segment by mean value using the temperature gradient (indigo โ†’ blue โ†’ cyan โ†’ green โ†’ amber โ†’ red), legible on light and dark backgrounds.
series[].data
{ t: number; v: number | null }[]Samples sorted ascending by `t` (epoch ms). Use `v: null` for missing readings.
series[].decimals

Default: 1

numberDecimals used in tooltips and legend readouts.
series[].gapMs
numberWhen set, gaps between consecutive samples greater than this many ms render as an explicit "no signal" band.
series[].thresholds
CwResponsiveLineThreshold[]Optional named horizontal reference lines drawn on the series' axis. Each entry is `{ value, label, color? }`.
series[].axisSide
'left' | 'right'Force this series' Y axis to a specific side. When omitted, axes auto-distribute (index 0 โ†’ left, 1 โ†’ right, 2 โ†’ left, โ€ฆ).
dataStart
numberEarliest selectable timestamp (epoch ms). Computed from `series` when omitted.
dataEnd
numberLatest selectable timestamp (epoch ms). Computed from `series` when omitted.
title

Default: ''

stringHeading shown above the chart.
subtitle

Default: ''

stringSub-heading shown beneath the title.
theme

Default: 'light'

'light' | 'dark'Theme palette. Bindable.
themeAuto

Default: false

booleanWhen true, follows `prefers-color-scheme` and hides the manual toggle.
showThemeToggle

Default: true

booleanShow the sun/moon theme toggle in the header.
ranges

Default: [ '1H', '24H', '7D' ]

CwResponsiveLineRangePreset[]Range pill presets shown in the header. Each entry is `{ id, label, ms }`. Pass `[]` to hide the row.
initialRange

Default: '24h'

stringRange preset id used on mount when its `ms` is shorter than the full dataset span.
initialHidden

Default: []

string[]Series ids hidden at mount time. Mutable thereafter via legend chips.
showLegend

Default: true

booleanShow the legend panel at all (chips, axis picker, stats).
showLegendStats

Default: true

booleanShow min / avg / max under each legend chip.
showAnomalies

Default: true

booleanRender ringed dots for z-score > 2.5ฯƒ within the current viewport.
showThresholds

Default: true

booleanRender named threshold lines from each series' `thresholds` array.
showDataGaps

Default: true

booleanRender explicit "no signal" bands when a series has `gapMs` set and a gap occurs.
showNightBands

Default: true

booleanShade 18:00โ€“06:00 local time when the view spans less than 14 days.
layout

Default: 'auto'

'auto' | 'desktop' | 'tablet-land' | 'tablet' | 'phone-land' | 'phone'Forced layout variant. `auto` (default) picks one from the container width.
height

Default: 480

number | stringComponent height. Number is treated as pixels; strings are passed through verbatim (e.g. "60vh").
bare

Default: false

booleanStrip the card chrome (background, border, padding) for embedding inside other cards.
noData
string | booleanWhen present, blurs the chart and shows an overlay. Pass localized text, or use the bare `noData` attribute for "No Data Available".
class

Default: ''

stringOptional class forwarded to the root element.
onchange
(e: CwResponsiveLineChangeEvent) => voidCalled whenever the view or visibility changes. Receives `{ viewStart, viewEnd, hidden, legendStats }`.

Copy-paste examples

These snippets intentionally show the full public API surface the live demo relies on.

Minimal example โ€” one series

The simplest possible usage. Pass an array with a single series and let the chart size to its container.

Minimal example โ€” one series Copy code
<script lang="ts">
	import { CwResponsiveLineChart } from '@cropwatchdevelopment/cwui';
	import type { CwResponsiveLineSeries } from '@cropwatchdevelopment/cwui';

	const series: CwResponsiveLineSeries[] = [{
		id: 'temp',
		label: 'Temperature',
		unit: 'ยฐC',
		color: '#ef4444',
		data: readings.map(r => ({ t: r.epochMs, v: r.celsius })),
		decimals: 1
	}];
</script>

<CwResponsiveLineChart {series} title="Greenhouse temperature" />
Two color-matched axes + gradient temperature + frost threshold

Two series โ†’ axes auto-distribute (temp on the left in red, humidity on the right in blue). Temperature uses the value-mapped gradient and a Frost line at 0 ยฐC. Auto-follow the OS color scheme.

Two color-matched axes + gradient temperature + frost threshold Copy code
const series: CwResponsiveLineSeries[] = [
	{
		id: 'airTemp', label: 'Air temperature', unit: 'ยฐC',
		color: '#ef4444', gradient: true,
		data: tempPoints, decimals: 1,
		gapMs: 8 * 60 * 1000,
		thresholds: [{ value: 0, label: 'Frost', color: 'rgba(59,130,246,0.6)' }]
	},
	{
		id: 'airHumidity', label: 'Air humidity', unit: '%RH',
		color: '#3b82f6', data: humidityPoints, decimals: 0
	}
];

<CwResponsiveLineChart
	{series}
	title="North Greenhouse ยท Bay 3"
	subtitle="Live sensor telemetry"
	initialRange="24h"
	themeAuto
/>
Compact dashboard tile (no chrome)

Inside a card, hide everything but the canvas. The tooltip still appears on hover/touch.

Compact dashboard tile (no chrome) Copy code
<CwResponsiveLineChart
	{series}
	bare
	height={220}
	showLegend={false}
	showThemeToggle={false}
	ranges={[]}
/>
React to view changes

Persist the viewport and visible series whenever the user interacts. The handler fires on pan, zoom, range pill, axis change, and legend toggle.

React to view changes Copy code
function handleChange(e: CwResponsiveLineChangeEvent) {
	url.searchParams.set('from', String(e.viewStart));
	url.searchParams.set('to', String(e.viewEnd));
	url.searchParams.set('hidden', e.hidden.join(','));
	history.replaceState(null, '', url);
}

<CwResponsiveLineChart {series} onchange={handleChange} />