Charts
Horizon wraps all chart rendering in dedicated components. The widget primitives (overview + dashboard) delegate to these wrappers. Per project rule, no view ever instantiates ECharts directly — the wrappers own the lifecycle, handle theming, sync crosshairs, and tear down on unmount.
This page is for developers extending or troubleshooting the chart layer. End-users of the templating system do not need it; reach for Dashboard Widgets instead.
TimeChart
Path: apps/ui/src/components/charts/TimeChart.vue
Used by: line dashboard widget; ad-hoc embeds in feature pages.
Renders: Multi-series line chart via ECharts.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
series |
Series[] |
required | One per line. |
height |
number | 180 | Fixed pixel height. |
unit |
string | — | Optional unit suffix in tooltips. |
accent |
string | var(--sw-accent) |
CSS var or hex for the first series. Subsequent series cycle through the palette. |
format |
int | decimal | compact |
— | Axis and tooltip number formatting. |
Series
interface Series {
label: string;
data: Array<number | null>;
yAxisIndex?: number; // 0 = left (default), 1 = right
unit?: string;
}
Behavior
- Dual y-axis appears when any series has
yAxisIndex: 1. - Legend visible iff
series.length > 1. - Smooth lines with circle point markers.
- Tooltip positioned via callback (appendToBody) so it does not clip near grid edges.
- Synced crosshairs: hover events broadcast to peer
TimeChartinstances on the same page via the shared chart hover bus. - Fingerprinting: data-only updates animate smoothly; structure changes (series count, label set) do a full replace.
Adding a new chart kind
Any new ECharts-backed visualization should land as a sibling component, not as a fork of TimeChart. Share the hover-bus subscription if it should participate in synced crosshairs.
TopList
Path: apps/ui/src/components/charts/TopList.vue
Used by: top dashboard widget.
Renders: Sorted list with optional tab switcher.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
items |
ReadonlyArray<DashboardTopItem> |
— | Single list mode. |
groups |
ReadonlyArray<TopGroup> |
— | Multi-list mode (mutually exclusive with items). |
unit |
string | — | Widget-level unit suffix. |
color |
string | var(--sw-accent) |
Bar color. |
TopGroup
interface TopGroup {
label: string;
expression?: string; // surfaced in tab tooltip
unit?: string; // per-tab unit override
items: DashboardTopItem[];
}
DashboardTopItem
interface DashboardTopItem {
name: string;
value: number | null;
}
Layout
- Rank column (18 px) | name (flex) | value (auto).
- Background fill bar normalized to the maximum value (per tab in multi-list mode).
- Tabs shown when
groups.length > 1.
AlarmsTimeline
Path: apps/ui/src/components/charts/AlarmsTimeline.vue
Used by: Alarms page (full timeline above the alarm table).
Renders: Per-minute stacked bar chart of firing + recovered alarms, with brush selection.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
alarms |
AlarmMessage[] |
required | Alarm messages to bucket. |
startTime |
number | required | Window start (ms). |
endTime |
number | required | Window end (ms). |
height |
number | 110 | Pixel height. |
selectedRange |
{ startTime, endTime } | null |
null | Current brush selection. |
Emits
| Event | Payload | When |
|---|---|---|
select-time-range |
{ startTime, endTime } |
Brush completed or pin flag clicked. |
clear-selection |
— | Empty area click or parent clears selection. |
Behavior
- Two stacked series per minute bucket: firing (red), recovered (green).
- Pin flags on non-zero buckets with count labels.
- Brush (
lineX) for range selection. Snaps to minute boundaries. - Click on non-zero point → selects that single minute. Click on zero → clears selection.
Sparkline
Path: apps/ui/src/components/charts/Sparkline.vue
Used by: Inline tiles, sidebar mini-charts, layer service-list picker (when a column carries a trend).
Renders: Tiny inline SVG. No ECharts, no animation — lightweight enough to render dozens per page.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
values |
Array<number | null> |
required | Data points. null = gap. |
width |
number | 56 | Internal coord width. |
height |
number | 14 | Internal coord height. |
color |
string | var(--sw-accent) |
Line color. |
stroke |
number | 1.25 | Line width (px). |
fluid |
boolean | false | Stretch to container width. |
crosshairBucket |
number | null | null | Shared hover index (for synced sparklines). |
Emits
| Event | Payload | When |
|---|---|---|
bucket-hover |
bucket index | Pointer over the chart. |
bucket-leave |
— | Pointer leaves the chart. |
Behavior
- Fallback single dot when fewer than 2 finite samples.
- Gap bridging on
nullentries (line breaks). - No interactivity beyond hover broadcasting.
D3 components
Where ECharts is wrong (custom interactions, non-cartesian layouts), Horizon uses D3 wrappers under apps/ui/src/components/charts/ named Native*. The same lifecycle rule applies — the composable owns mount, render, and tear-down; no view manipulates the DOM directly.
Theming
All chart colors derive from the design token CSS (apps/ui/src/assets/styles/tokens.css). Per-chart accents take a CSS variable string (var(--sw-accent)) by default — the chart resolves to the token’s current value at render time, which means a theme switch updates colors live without remount.
Hex strings are accepted for one-off cases (e.g. severity colors); prefer tokens for anything that should follow theming.
Adding a new chart wrapper
- Place under
apps/ui/src/components/charts/(shared) orapps/ui/src/features/<feature>/(feature-scoped) per the layering rule. - Own the lifecycle: instantiate in
onMounted, dispose inonBeforeUnmount. Never let the chart outlive its component. - Resolve theme tokens via
getComputedStyle(document.documentElement)if you need numeric values; or pass CSS variable strings through directly when the chart supports them. - If the chart is time-series with hover semantics, subscribe to the shared hover bus so it joins synced crosshairs.
- Add a license header (
.tsand.vuefiles require one — see.licenserc.yaml).