Instrument history
Keep raw observations, filter output and forecast uncertainty distinct while loading bounded time windows.
On this page
Raw and filtered
Illustrative accepted observations and separately supplied filter output
- Accepted raw
- Filter output
Code
import { LineChart } from "@bosunkit/components/line-chart";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { ChartPanel } from "@bosunkit/components/chart-panel";
import { Switch } from "@bosunkit/components/switch";
import type { ChartConfig, ChartValueKey } from "@bosunkit/components/chart-container";
import { useId, useState, type Dispatch, type SetStateAction, type ReactElement } from "react";
export const description: string =
"A controlled switch shows accepted raw observations behind supplied filter output; the domain stays fixed when raw is hidden.";
interface Sample {
readonly day: string;
readonly raw: number | null;
readonly filtered: number | null;
}
const data: readonly Sample[] = [
{ day: "Mon", raw: 3, filtered: 3 },
{ day: "Tue", raw: 8, filtered: 5 },
{ day: "Wed", raw: 4, filtered: 5 },
{ day: "Thu", raw: 9, filtered: 6 },
{ day: "Fri", raw: 5, filtered: 6 },
{ day: "Sat", raw: null, filtered: null },
{ day: "Sun", raw: 4, filtered: 5 },
];
const values: Readonly<Record<number, string>> = {
0: "0 units",
1: "1 unit",
2: "2 units",
3: "3 units",
4: "4 units",
5: "5 units",
6: "6 units",
7: "7 units",
8: "8 units",
9: "9 units",
10: "10 units",
};
const write: (value: number) => string = (value: number): string => values[value] ?? "";
const config: ChartConfig<Sample> = {
raw: { label: "Accepted raw", formatValue: write, color: "01", pattern: "dotted", emphasis: "context" },
filtered: { label: "Filter output", formatValue: write, color: "01", pattern: "solid" },
};
export default function RawAndFiltered(): ReactElement {
const id: string = useId();
const [showRaw, setShowRaw]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);
const series: readonly ChartValueKey<Sample>[] = showRaw ? ["raw", "filtered"] : ["filtered"];
return (
<ChartPanel
title="Raw and filtered"
description="Illustrative accepted observations and separately supplied filter output"
controls={
<label htmlFor={id} style={{ display: "flex", alignItems: "center", gap: "var(--space-md)" }}>
<Switch id={id} checked={showRaw} onCheckedChange={setShowRaw} />
Show accepted raw observations
</label>
}
legend={<ChartLegend config={config} series={series} label="Raw and filtered series" />}
footer="Same timestamps, unit and scale · supplied values, no browser smoothing"
>
<LineChart
data={data}
categoryKey="day"
config={config}
series={series}
valueDomain={[0, 10]}
formatCategory={(value: string | number): string => (typeof value === "string" ? value : "")}
formatAxisValue={write}
label="Illustrative raw and filtered observations, Monday through Sunday"
categoryLabel="Day"
dataTableLabel="View raw and filtered observations"
missingValueText="Not observed"
/>
</ChartPanel>
);
}
Historical charts draw caller-supplied observations. The vessel owns filtering, forecasting, storage and source selection. Chart components take props and contain no network requests, instrument subscriptions or model inference.
Select the source deliberately
instrument.History.QuerySeries returns the complete half-open interval [From, To) as equal-width buckets. Its storage default budget is 240 points and its maximum is 10,000. The protected HTTP adapter defaults to 500 points and caps requests at 2,000 points over at most 366 days. Every bucket has a timestamp and either sample-count-weighted aggregates or an explicit gap. The response includes the actual step, contributing sources per bucket, processing stage, summary validity and aggregation meaning. Arithmetic series include a fitted trend; circular series do not use a linear fit.
| Selection | Meaning | Suitable use |
|---|---|---|
StreamRaw | Accepted non-filter sources | Inspect accepted observations before smoothing |
Source="filter" | Exact filter output, with no raw fallback | Compare smoothing against accepted raw data |
StreamCurrent | Filter output where present; raw fallback in other buckets | Inspect the operational stream, with fallback provenance made explicit |
StreamAll | Raw and filtered samples aggregated together | Combined-source diagnostics, not a raw/filtered comparison |
| An exact raw source | One producer's observations | Compare sensors without merging their identity |
Raw here means accepted, persisted observations, not rejected packets or every sensor transmission. Equipment adapters can hold or smooth engine readings before they reach the bus; accepted history does not imply untouched hardware output. Bucket processing is accepted, filter-pipeline or mixed; the pipeline can include estimation. Older history uses retained rollups, so a plotted raw-stream bucket is not necessarily one unaggregated observation. Display its bucket width, aggregation and source. Values.Min and Values.Max preserve observed extrema; Values.Avg is the weighted mean and Values.Last is the last observation by event time. A missing bucket becomes null, never zero.
For a raw/filtered comparison, request the same kind, field, From, To and MaxPoints twice: once for the desired raw source or StreamRaw, once with Source="filter". Check that the returned timestamps and steps match before joining them. Do not use StreamCurrent as if it were an exact filtered stream. Source="filter" identifies the combined pipeline output; it does not identify the originating raw sensor, so a multi-sensor comparison needs separate provenance from the server.
Draw raw and filtered values together
Make the filter output the primary trace. Draw accepted raw values with contextual emphasis and a distinct pattern, preserve a shared unit and scale, and expose both values in inspection. A controlled switch can hide the raw trace while the full registry keeps its appearance and domain stable. The fixture below contains separately supplied values; the browser does no smoothing.
Raw and filtered
Illustrative accepted observations and separately supplied filter output
- Accepted raw
- Filter output
Code
import { LineChart } from "@bosunkit/components/line-chart";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { ChartPanel } from "@bosunkit/components/chart-panel";
import { Switch } from "@bosunkit/components/switch";
import type { ChartConfig, ChartValueKey } from "@bosunkit/components/chart-container";
import { useId, useState, type Dispatch, type SetStateAction, type ReactElement } from "react";
export const description: string =
"A controlled switch shows accepted raw observations behind supplied filter output; the domain stays fixed when raw is hidden.";
interface Sample {
readonly day: string;
readonly raw: number | null;
readonly filtered: number | null;
}
const data: readonly Sample[] = [
{ day: "Mon", raw: 3, filtered: 3 },
{ day: "Tue", raw: 8, filtered: 5 },
{ day: "Wed", raw: 4, filtered: 5 },
{ day: "Thu", raw: 9, filtered: 6 },
{ day: "Fri", raw: 5, filtered: 6 },
{ day: "Sat", raw: null, filtered: null },
{ day: "Sun", raw: 4, filtered: 5 },
];
const values: Readonly<Record<number, string>> = {
0: "0 units",
1: "1 unit",
2: "2 units",
3: "3 units",
4: "4 units",
5: "5 units",
6: "6 units",
7: "7 units",
8: "8 units",
9: "9 units",
10: "10 units",
};
const write: (value: number) => string = (value: number): string => values[value] ?? "";
const config: ChartConfig<Sample> = {
raw: { label: "Accepted raw", formatValue: write, color: "01", pattern: "dotted", emphasis: "context" },
filtered: { label: "Filter output", formatValue: write, color: "01", pattern: "solid" },
};
export default function RawAndFiltered(): ReactElement {
const id: string = useId();
const [showRaw, setShowRaw]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);
const series: readonly ChartValueKey<Sample>[] = showRaw ? ["raw", "filtered"] : ["filtered"];
return (
<ChartPanel
title="Raw and filtered"
description="Illustrative accepted observations and separately supplied filter output"
controls={
<label htmlFor={id} style={{ display: "flex", alignItems: "center", gap: "var(--space-md)" }}>
<Switch id={id} checked={showRaw} onCheckedChange={setShowRaw} />
Show accepted raw observations
</label>
}
legend={<ChartLegend config={config} series={series} label="Raw and filtered series" />}
footer="Same timestamps, unit and scale · supplied values, no browser smoothing"
>
<LineChart
data={data}
categoryKey="day"
config={config}
series={series}
valueDomain={[0, 10]}
formatCategory={(value: string | number): string => (typeof value === "string" ? value : "")}
formatAxisValue={write}
label="Illustrative raw and filtered observations, Monday through Sunday"
categoryLabel="Day"
dataTableLabel="View raw and filtered observations"
missingValueText="Not observed"
/>
</ChartPanel>
);
}
Observable's window-transform examples pair a subdued raw trace with a rolling summary. This is the presentation used here; the vessel's actual filter output remains authoritative. Name the algorithm and parameters when known. A centered moving window uses future samples; use trailing filters for a causal live view and distinguish a retrospective smoother from operational output.
Draw forecasts as forecasts
instrument.Forecast supplies Mean, Start, StepMillis, Model and a map of quantile arrays. The first forecast bucket starts at the history's To. Each timestamp is Start plus its index times StepMillis. Look quantiles up by level: implementations can supply a subset, and the Holt fallback commonly supplies the 0.1, 0.5 and 0.9 levels. Missing levels do not become zero bounds. The HTTP response names the central estimate point_forecast, identifies point_kind as expected or median, and encodes quantiles as sorted {level, values} pairs. It validates finite values, matching lengths and a positive step. TimesFM supplies its median through the Go field named Mean; a caller must not label that estimate as a mean.
The consumer maps observed buckets and forecast buckets to separate series on a numeric time axis. A dashed forecast and a start marker distinguish future values. A typed ChartBand names its lower and upper fields and formats the interval in the tooltip and source table. State the model, horizon, forecast creation time and selected quantile levels in the surrounding panel. The interval is a forecast distribution, not an observed min/max envelope or a guarantee.
Observed and forecast
Illustrative amounts · observations end at 14:20 UTC
- Observed
- Forecast
Code
import { AreaChart } from "@bosunkit/components/area-chart";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { ChartPanel } from "@bosunkit/components/chart-panel";
import type { ChartConfig, ChartBand } from "@bosunkit/components/chart-container";
import * as Recharts from "recharts";
import type { ReactElement } from "react";
export const description: string =
"Illustrative observed and forecast amounts have separate series, a dashed forecast, a cutoff and explicit 10th–90th quantile bounds.";
interface Sample {
readonly time: number;
readonly observed: number | null;
readonly forecast: number | null;
readonly q10: number | null;
readonly q90: number | null;
}
const start: number = 1791986400000;
const step: number = 600000;
const data: readonly Sample[] = [
{ time: start, observed: 6, forecast: null, q10: null, q90: null },
{ time: start + step, observed: 5, forecast: null, q10: null, q90: null },
{ time: start + step * 2, observed: 6, forecast: null, q10: null, q90: null },
{ time: start + step * 3, observed: null, forecast: 7, q10: 6, q90: 8 },
{ time: start + step * 4, observed: null, forecast: 8, q10: 6, q90: 10 },
{ time: start + step * 5, observed: null, forecast: 9, q10: 5, q90: 10 },
];
const values: Readonly<Record<number, string>> = {
0: "0 items",
1: "1 item",
2: "2 items",
3: "3 items",
4: "4 items",
5: "5 items",
6: "6 items",
7: "7 items",
8: "8 items",
9: "9 items",
10: "10 items",
};
const times: Readonly<Record<number, string>> = {
[start]: "14:00 UTC",
[start + step]: "14:10 UTC",
[start + step * 2]: "14:20 UTC",
[start + step * 3]: "14:30 UTC",
[start + step * 4]: "14:40 UTC",
[start + step * 5]: "14:50 UTC",
};
const write: (value: number) => string = (value: number): string => values[value] ?? "";
const writeTime: (value: string | number) => string = (value: string | number): string =>
typeof value === "number" ? (times[value] ?? "") : "";
const config: ChartConfig<Sample> = {
observed: { label: "Observed", formatValue: write, pattern: "solid" },
forecast: { label: "Forecast", formatValue: write, pattern: "dashed" },
};
const ranges: Readonly<Record<string, string>> = { "6:8": "6–8 items", "6:10": "6–10 items", "5:10": "5–10 items" };
const bands: readonly ChartBand<Sample>[] = [
{
series: "forecast",
low: "q10",
high: "q90",
label: "10th–90th forecast quantiles",
formatRange: (low: number, high: number): string => ranges[`${String(low)}:${String(high)}`] ?? "",
},
];
export default function ForecastWindow(): ReactElement {
return (
<ChartPanel
title="Observed and forecast"
description="Illustrative amounts · observations end at 14:20 UTC"
legend={<ChartLegend config={config} label="Observation and forecast series" />}
footer="Illustrative only · band shows supplied 10th–90th quantiles, not observed extrema"
>
<AreaChart
data={data}
categoryKey="time"
categoryScale="time"
config={config}
series={["observed", "forecast"]}
bands={bands}
formatCategory={writeTime}
formatAxisValue={write}
valueDomain={[0, 10]}
label="Illustrative observations and forecast, 14:00 to 14:50 UTC"
categoryLabel="Time, UTC"
dataTableLabel="View observations and quantiles"
missingValueText="Not supplied"
>
<Recharts.ReferenceLine x={start + step * 3} label={{ value: "Forecast starts", position: "insideTopRight" }} />
</AreaChart>
</ChartPanel>
);
}
Sparkline also accepts rawData, filterLabel and a SparklineForecast with supplied mean, lower and upper bounds. TrendChart accepts already prepared points and a time marker; it does not have a structured quantile-band contract. LineChart and overlapping AreaChart accept typed bands. Forecast inference remains on the vessel.
Directional and state history
Reference-specific heading_true, heading_magnetic, cog_true and cog_magnetic fields retain separate bases. Wind angle, relative-wind angle_boat and angle_water, and current set also retain unit-vector components through rollups. Their circular mean follows the north crossing; opposing directions have no defined mean and produce a gap. Check summary.avg_valid before displaying a summary mean. Directional extrema are numeric ranges, not shortest arcs. Unqualified heading and COG fields use their arithmetic contract; select the reference-specific fields for directional analysis.
LineChart's circularPeriod preserves the shortest angular increments and original source values. For time windows, categoryDomain fixes the viewport, onCategoryDomainChange receives drag-to-zoom or Shift-drag pan requests, and maxGap bounds elapsed continuity. Shared cursors match values or nearest timestamps within syncTolerance.
State history records charger, inverter and converter modes, switch-bank states, engine fault snapshots and autopilot modes. One exact source supplies each state query. Repeated identical heartbeats extend a continuous interval; missing heartbeats bound validity at two minutes. The query includes the preceding state at its left edge and clips it to its actual validity. StateTimeline fills the remaining periods as unknown and never extends an expired state. Fault snapshots describe accepted observations; this history is not the incident audit log.
Load history as the window changes
The session-protected Go routes expose the retained data:
| Route | Contract |
|---|---|
GET /history/catalog | Recorded kind, field, exact source, representation, aggregation and retained coverage; display names belong to the consumer |
GET /history/series | kind, optional field, source or stream, RFC3339 from and to, optional points |
GET /history/states | Exact kind and source over a bounded interval; expired or absent state remains unknown |
GET /history/forecast | The series query plus a positive horizon of at most 24 hours; at most 2,000 forecast steps |
Storage and forecast work receive cancellation and timeouts. An unavailable dependency returns 503, invalid budgets return 400, an unsupported forecast window returns 422, and concurrent forecast requests receive 429. Quantile inference has one admission slot per mounted handler. A history-query failure is distinct from a successful all-gap window.
The scalar forecasters do not support circular directions. A circular history request returns 422 before inference; an angular forecast requires a model and interval representation that preserve its reference and wrap behavior.
The application adapter in ui/web/src/data/instrumentHistory.ts validates identity, range and observations, keeps a bounded cache with a freshness deadline and discards superseded responses even when a transport ignores cancellation. useInstrumentHistory connects a controlled range to that adapter. counterDeltas preserves resets and source changes as gaps; intervalConsumption integrates bucket-average rates per hour over their actual elapsed span, including a partial final bucket.
The consumer flow is:
- Keep the visible UTC interval and point budget outside the chart. Offer named earlier/later, range and zoom controls as well as pan gestures.
- Request the committed visible interval, with a budget related to the plot width. The server calls QuerySeries and returns the actual grid and aggregation metadata.
- Keep raw and exact filtered queries on the same grid. Coalesce rapid viewport changes, abort superseded requests and ignore late responses using request identity.
- Cache by kind, field, exact source or stream, interval and resolution. Prefetch adjacent intervals when useful. Reuse compatible coverage; do not splice differently anchored or differently sized buckets into one apparent grid.
- Keep the viewport and source identity stable while data arrives. Show a caller-translated loading or error state through ChartPanel. Do not place earlier data under a later time label, move the viewport when older data is prepended, or interpolate loaded values.
- Treat following the latest bucket as an explicit mode. Panning into history stops follow mode; returning to live data is a named control. Request the forecast for the selected historical basis separately and preserve its generation time.
Highcharts' asynchronous history example requests server data when the visible axis interval changes. That interaction pattern informs range loading; the kit continues to use Recharts and the instrument store's existing query contract. A Recharts Brush selects within data already loaded; loading older history requires the consumer's range controller and server request.
Raw and filtered
Illustrative accepted observations and separately supplied filter output
- Accepted raw
- Filter output
Query the exact filter source and retain its gaps. A mixed current-stream trace needs an explicit fallback description.
Related
- Line chart: numeric time spacing, context traces and typed intervals.
- Area chart: zero-based amounts and forecast quantile bands.
- Sparkline: compact operational readings with raw samples and forecast bounds.