Vue 3 graph card for the time series served by the kal-rrd-graph
HTTP API (api/graph, api/watch). One card fetches and draws one graph,
keeps its data while a new window loads, follows file changes with
small tail requests, and reports user pan/zoom so linked charts move
together. A chart’s legend is as wide as its longest label (up to 240
pixels, longer labels are ellipsized), and the charts of a page share
the widest legend so their plots line up. Several cards on a page share one scheduler: visible cards
load first, then the next four below are prefetched.
The package knows nothing about how the host organizes its graphs, routing or period selection: the host gives each card a graph descriptor and a time window, and decides what a pan/zoom does.
npm install @0k/rrd-graph vuevue (^3.5.12) is a peer dependency. Load the card stylesheet
after the host stylesheet (see Styling):
import './styles.css'
import '@0k/rrd-graph/style.css'A card without an injected scheduler schedules itself: it loads once it is on screen.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { GraphCard, useGraphWatch, type GraphDescriptor, type TimeWindow } from '@0k/rrd-graph'
const graph: GraphDescriptor = { id: 'load-average', title: 'Load average', type: 'line', stack_toggle: false, unit: null }
const now = ref(Math.floor(Date.now() / 1000))
// The last day, live, until the user pans or zooms the chart.
const pinned = ref<TimeWindow | null>(null)
const timeWindow = computed<TimeWindow>(() => pinned.value ?? { start: now.value - 86400, end: now.value, live: true })
// Optional: tail the graph when its RRD files change.
const changes = useGraphWatch(computed(() => [graph.id]), timeWindow, now)
function onWindow(start: number, end: number): void {
pinned.value = { start, end, live: false }
}
</script>
<template>
<aside class="host-panel">…</aside>
<GraphCard :graph="graph" :window="timeWindow" :change="changes[graph.id] ?? 0" @window="onWindow" />
</template>A live window is computed from now: useGraphWatch advances now
when a change arrives, and a pure live slide only pans the data already
held.
Provide one scheduler for the displayed cards, in display order:
import { computed, provide, type Ref } from 'vue'
import { GRAPH_SCHEDULER, useGraphScheduler, useGraphWatch, type GraphDescriptor, type TimeWindow } from '@0k/rrd-graph'
// The host's displayed graphs, window and clock, as in the example above.
declare const graphs: Ref<GraphDescriptor[]>, timeWindow: Ref<TimeWindow>, now: Ref<number>
const ids = computed(() => graphs.value.map(graph => graph.id))
provide(GRAPH_SCHEDULER, useGraphScheduler(ids))
const changes = useGraphWatch(ids, timeWindow, now)| Prop | Type | Default | Meaning |
|---|---|---|---|
graph | GraphDescriptor | required | id, title, type (line / stacked), stack_toggle, unit |
window | TimeWindow | required | { start, end, live } in Unix seconds |
revision | number | 0 | Host configuration revision; a change reloads the window |
change | number | 0 | Change counter from useGraphWatch; a change fetches a tail |
tall | boolean | false | Taller chart, for a page showing a single graph |
apiBase | string | api/ | URL prefix of the kal-rrd-graph API |
Event window(start, end): the user panned or zoomed the chart (Unix
seconds).
A failed request, or data the chart refuses to draw (for instance a
point whose min exceeds its max), shows its message in the card’s
error banner with a Retry button.
apiBase is relative by default, so the host works at / and behind
a reverse-proxy prefix. Never pass an absolute /api/.
TimeSeriesChart: the chart alone (data,window,tall,stackedprops;window,readyanderror(message)events:errorreplacesreadywhen@0k/time-chartrejects the data).useGraphScheduler(order),GRAPH_SCHEDULER,admitted,PRELOAD_COUNT: visible-first admission.useGraphWatch(ids, window, now, { apiBase }): oneapi/watchsubscription for the displayed set; returns per-graph change counters.fetchGraph,parseGraph,parseGraphDescriptor,parseWatchSnapshot,parseGraphChange,requestJson,apiUrland theexpect*validators. Also available without Vue from@0k/rrd-graph/api.tailRange,mergeTail,toTimeSeries,toYAxis,samplePoints,isStale,formatValue,formatDate.
The graph YAML’s drawing keys (documented in kal-rrd-graph’s README,
“Series entries” and “Y axis”) reach the card as optional api/graph
fields, which parseGraph validates and the chart passes to
@0k/time-chart unchanged in meaning:
api/graph field | @0k/time-chart option |
|---|---|
series color | TimeSeries.color |
series dash | TimeSeries.dash: solid, dashed or dotted |
series width | TimeSeries.width |
series fill | TimeSeries.fill (area to the axis bottom) |
series band: {pattern, opacity} | TimeSeries.band over min / max |
series line: false | TimeSeries.line: false (no line, no tooltip) |
graph y_axis.min / max | yAxis.min / yAxis.max |
graph y_axis.interval | yAxis.interval |
graph y_axis.minor_interval | yAxis.minorInterval |
Absent fields set nothing, so unstyled graphs draw as before. Series
are drawn in list order. A tail merge takes the drawing keys of the
newest response. The y axis is fixed when a chart is created; a
changed y-axis applies the next time the card’s chart is created
(e.g. when it becomes visible again).
style.css lays out the card and the chart. Colours are custom
properties with light defaults; map them to the host theme:
:root {
--rrd-graph-placeholder-background: var(--c-bg);
--rrd-graph-muted: var(--c-text-muted);
--rrd-graph-accent: var(--c-accent);
--rrd-graph-accent-soft: var(--c-accent-soft);
--rrd-graph-danger: var(--c-danger);
--rrd-graph-danger-soft: var(--c-danger-soft);
--rrd-graph-radius: var(--radius-sm);
--rrd-graph-small-font-size: var(--fs-small);
}The card frame uses host classes: section, section__head,
section__note, error-banner, muted, and the host’s button
style. .graph-card overrides the .section padding, hence the load
order above.
npm install
npm test # vitest run, source modules
npm run typecheck
npm run build # dist/: ES modules, style.css, declarations
npm run test:package # Vitest built-artifact checks (after build)
npm run check # all of the aboveMIT