GrChartLine

  • charts
  • charts
  • API · 41 props
  • ⌨ keys
import { GrChartLine } from '@feugene/granularity-charts/components/GrChartLine'

Overview

When to take it

  • how a quantity changed — revenue by month, load by hour, the balance by day: a line shows the course rather than individual measurements;
  • several series side by side — the legend toggles the series, and the tooltip shows all of the values at one abscissa;
  • there are gaps in the series — null breaks the line rather than substituting a zero; gaps draws a bridge across a gap when the readability of the trend matters more;
  • there are thousands of points — decimate: 'auto' (the default) reduces the drawing to two vertices per pixel, while the cursor, the keyboard and the hidden table keep knowing the full series. The details — ../model.md, the section “Decimation is a projection, not the data”.

When to take something else

NeedTake
Show the whole and the contribution of its partsGrChartArea
Compare quantities across categoriesGrChartBar
Show what one whole consists ofGrChartPie
Compare the shape of a profile across several axesGrChartRadar
A trend in a table cell, without axes and a frameGrSparkline
One number large, with a trendGrStatistic

Examples 4 each with a live demo and its code

  • A month of latency, and the hour that broke it

    720 points a series on a time scale: the path is thinned to the width, while the cursor and the hidden table still know every hour. v-model:x-window is the visible window — the presets set it, zoom="brush" lets the reader drag their own, and the axis switches from days to hours when the window gets short. The SLO is a references line, not a series.

    Checkout API · latencyLast 30 days · drag over the chart to zoom in
    Chart data
    Xp95p50
    Sep 7, 2026, 12:00 AM15358
    Sep 7, 2026, 07:00 AM13166
    Sep 7, 2026, 01:00 PM23280
    Sep 7, 2026, 10:00 PM14766
    Sep 8, 2026, 06:00 AM14666
    Sep 8, 2026, 11:00 AM22880
    Sep 8, 2026, 05:00 PM22981
    Sep 8, 2026, 07:00 PM16977
    Sep 9, 2026, 06:00 AM13864
    Sep 9, 2026, 11:00 AM21878
    Sep 9, 2026, 02:00 PM25482
    Sep 9, 2026, 10:00 PM14564
    Sep 10, 2026, 07:00 AM15259
    Sep 10, 2026, 01:00 PM23991
    Sep 10, 2026, 03:00 PM24484
    Sep 10, 2026, 09:00 PM16557
    Sep 11, 2026, 07:00 AM13968
    Sep 11, 2026, 12:00 PM23979
    Sep 11, 2026, 05:00 PM23481
    Sep 11, 2026, 09:00 PM13862
    Sep 12, 2026, 07:00 AM12866
    Sep 12, 2026, 10:00 AM17675
    Sep 12, 2026, 02:00 PM20670
    Sep 12, 2026, 10:00 PM12758
    Sep 13, 2026, 06:00 AM12859
    Sep 13, 2026, 01:00 PM20770
    Sep 13, 2026, 03:00 PM16576
    Sep 14, 2026, 12:00 AM13963
    Sep 14, 2026, 06:00 AM13561
    Sep 14, 2026, 01:00 PM24980
    Sep 14, 2026, 07:00 PM19966
    Sep 14, 2026, 10:00 PM12866
    Sep 15, 2026, 07:00 AM13461
    Sep 15, 2026, 01:00 PM24986
    Sep 15, 2026, 04:00 PM24480
    Sep 16, 2026, 12:00 AM13361
    Sep 16, 2026, 07:00 AM13764
    Sep 16, 2026, 12:00 PM24384
    Sep 16, 2026, 09:00 PM13868
    Sep 16, 2026, 11:00 PM17260
    Sep 17, 2026, 05:00 AM13365
    Sep 17, 2026, 11:00 AM23180
    Sep 17, 2026, 09:00 PM14856
    Sep 18, 2026, 02:00 AM13868
    Sep 18, 2026, 08:00 AM15167
    Sep 18, 2026, 02:00 PM25388
    Sep 18, 2026, 09:00 PM15058
    Sep 19, 2026, 01:00 AM17064
    Sep 19, 2026, 05:00 AM13668
    Sep 19, 2026, 11:00 AM20073
    Sep 19, 2026, 07:00 PM15473
    Sep 20, 2026, 04:00 AM12965
    Sep 20, 2026, 05:00 AM16462
    Sep 20, 2026, 02:00 PM20472
    Sep 20, 2026, 10:00 PM13767
    Sep 21, 2026, 04:00 AM12864
    Sep 21, 2026, 10:00 AM21877
    Sep 21, 2026, 04:00 PM24084
    Sep 21, 2026, 10:00 PM12860
    Sep 22, 2026, 12:00 AM16463
    Sep 22, 2026, 06:00 AM15067
    Sep 22, 2026, 01:00 PM25081
    Sep 22, 2026, 09:00 PM14866
    Sep 23, 2026, 12:00 AM13869
    Sep 23, 2026, 07:00 AM13568
    Sep 23, 2026, 03:00 PM25288
    Sep 23, 2026, 09:00 PM14263
    Sep 24, 2026, 04:00 AM13561
    Sep 24, 2026, 07:00 AM629177
    Sep 24, 2026, 12:00 PM23977
    Sep 24, 2026, 09:00 PM12859
    Sep 25, 2026, 05:00 AM12959
    Sep 25, 2026, 12:00 PM23879
    Sep 25, 2026, 04:00 PM24887
    Sep 25, 2026, 11:00 PM13367
    Sep 26, 2026, 04:00 AM16959
    Sep 26, 2026, 07:00 AM13568
    Sep 26, 2026, 01:00 PM20971
    Sep 26, 2026, 09:00 PM13964
    Sep 27, 2026, 06:00 AM13762
    Sep 27, 2026, 12:00 PM20267
    Sep 27, 2026, 02:00 PM17171
    Sep 27, 2026, 10:00 PM13562
    Sep 28, 2026, 07:00 AM13561
    Sep 28, 2026, 01:00 PM24091
    Sep 28, 2026, 03:00 PM24780
    Sep 28, 2026, 08:00 PM14866
    Sep 29, 2026, 07:00 AM14867
    Sep 29, 2026, 11:00 AM23177
    Sep 29, 2026, 02:00 PM25789
    Sep 29, 2026, 10:00 PM13967
    Sep 30, 2026, 07:00 AM14565
    Sep 30, 2026, 11:00 AM23076
    Sep 30, 2026, 03:00 PM24781
    Sep 30, 2026, 10:00 PM14168
    Oct 1, 2026, 06:00 AM13466
    Oct 1, 2026, 01:00 PM23581
    Oct 1, 2026, 08:00 PM14972
    Oct 1, 2026, 09:00 PM17264
    Oct 2, 2026, 07:00 AM13562
    Oct 2, 2026, 11:00 AM23081
    Oct 2, 2026, 03:00 PM25082
    Oct 2, 2026, 09:00 PM12865
    Oct 3, 2026, 08:00 AM18066
    Oct 3, 2026, 10:00 AM15672
    Oct 3, 2026, 05:00 PM18970
    Oct 3, 2026, 09:00 PM14156
    Oct 4, 2026, 07:00 AM13366
    Oct 4, 2026, 10:00 AM19269
    Oct 4, 2026, 07:00 PM17765
    Oct 4, 2026, 10:00 PM12960
    Oct 5, 2026, 06:00 AM14064
    Oct 5, 2026, 02:00 PM24681
    Oct 5, 2026, 09:00 PM13061
    Oct 6, 2026, 02:00 AM17164
    Oct 6, 2026, 07:00 AM14356
    Oct 6, 2026, 03:00 PM25388
    Oct 6, 2026, 06:00 PM18781
    Oct 6, 2026, 10:00 PM16857
    Oct 6, 2026, 11:00 PM14657
    SLO 300 ms: 300
    Showing 120 of 720 points — the ones the chart draws. Arrow keys walk all of them.
  • A logger that went silent

    null breaks the line instead of dropping it to zero: a silent logger did not measure 0 °C. gaps="dashed" bridges the hole so the trend reads on, and the dash says the bridge is not a measurement. The allowed corridor is a reference band, and curve="smooth" never overshoots the readings.

    Fridge 3 · temperatureNo readings 03:00–05:00 — logger offline
    Chart data
    XFridge 3
    Oct 6, 2026, 12:00 AM4.6
    Oct 6, 2026, 12:30 AM4.8
    Oct 6, 2026, 01:00 AM5.1
    Oct 6, 2026, 01:30 AM5.0
    Oct 6, 2026, 02:00 AM4.9
    Oct 6, 2026, 02:30 AM5.3
    Oct 6, 2026, 03:00 AMno value
    Oct 6, 2026, 03:30 AMno value
    Oct 6, 2026, 04:00 AMno value
    Oct 6, 2026, 04:30 AMno value
    Oct 6, 2026, 05:00 AM7.6
    Oct 6, 2026, 05:30 AM6.9
    Oct 6, 2026, 06:00 AM6.1
    Oct 6, 2026, 06:30 AM5.4
    Oct 6, 2026, 07:00 AM5.0
    Oct 6, 2026, 07:30 AM4.8
    Oct 6, 2026, 08:00 AM4.7
    Oct 6, 2026, 08:30 AM4.9
    Oct 6, 2026, 09:00 AM5.2
    Oct 6, 2026, 09:30 AM5.1
    Oct 6, 2026, 10:00 AM4.8
    Oct 6, 2026, 10:30 AM4.6
    Oct 6, 2026, 11:00 AM4.5
    Oct 6, 2026, 11:30 AM4.7
    Oct 6, 2026, 12:00 PM4.8
    2–8 °C: 2.0 to 8.0
  • Seats that change on a day, not gradually

    curve="step" draws a value that jumps and then holds — a sloped line would invent 18.5 seats in March. show-points="always" marks the billed months, and point-click opens the invoice line of the month.

    Northwind Traders · seats billedTeam plan, €12 a seat · click a month
    Chart data
    XSeats
    Nov 1, 2025, 12:00 AM12
    Dec 1, 2025, 12:00 AM12
    Jan 1, 2026, 12:00 AM15
    Feb 1, 2026, 12:00 AM15
    Mar 1, 2026, 12:00 AM15
    Apr 1, 2026, 12:00 AM22
    May 1, 2026, 12:00 AM22
    Jun 1, 2026, 12:00 AM22
    Jul 1, 2026, 12:00 AM20
    Aug 1, 2026, 12:00 AM28
    Sep 1, 2026, 12:00 AM28
    Oct 1, 2026, 12:00 AM34

    Click a month to see its invoice line.

  • Two units, two scales

    dual-axis gives the conversion its own scale on the right — on the left it would lie flat under 1,700 sign-ups. Each axis has its own tick format, an x reference marks the price change, and v-model:hidden-series keeps the legend state with the page: the chart only reports the intent.

    Growth after the price changeMore people sign up since W32, fewer of them pay
    Chart data
    XSign-ups (left axis)Paid within 14 days (right axis)
    W288206.1
    W298606.0
    W309106.3
    W318706.4
    W321,0405.8
    W331,1805.2
    W341,2105.0
    W351,1605.1
    W361,3904.6
    W371,5204.3
    W381,4804.4
    W391,6104.1
    W401,7003.9
    New prices: W32

API generated from the package · cannot drift

Props

PropTypedefaultDescription
xScaleGrChartScaleKind | undefinedundefinedType of the X axis. Not set — inferred from the data.
heightnumber | undefinedundefinedThe canvas height in pixels; 256 by default. The data, the loading state and the empty state all take up the same height.
widthnumber | undefined640Declared width: the first render starts from it, after that the width is measured.
yDomainreadonly [number | null, number | null] | undefinedundefinedThe bounds of the value axis `[min, max]`; `null` in place of a bound — it is taken from the data.
includeZeroboolean | undefinedfalseExtend the value axis to zero; off by default — the axis fits tightly around the data.
referencesreadonly GrChartReference[] | undefinedundefinedReference lines and bands: a threshold, a plan, a tolerance corridor. Not a series and never treated as one: it stays out of the legend, spends no palette index, joins no stack, and goes into the hidden table as a note rather than a data row.
includeReferencesInDomainboolean | undefinedfalseInclude the references in the axis domain. Off by default, and deliberately so: a threshold of `1.0` with data around `0.03` would stretch the axis until the data itself collapsed into a line.
dualAxisboolean | undefinedfalseShow a second value axis on the right. Without it, series with `axis: 'right'` land on the left one. Turning it on is a deliberate act: two axes let you fit any two series to a visible correlation, and that has to be the chart author's decision, not a side effect of a field in the data.
yDomainRightreadonly [number | null, number | null] | undefinedundefinedThe bounds of the right value axis with `dualAxis`; `null` in place of a bound — it is taken from the data.
yTickFormatRight((value: number) => string) | undefinedundefinedThe tick label of the right axis. Unset — a number formatted by the locale.
valueFormatRightGrChartNumberFormat | undefinedundefinedThe value format of the right-axis series in the tooltip, the hidden table and announcements.
xTickCountnumber | undefinedundefinedThe desired number of ticks on the X axis; 6 by default. For categories — the cap on the number of labels (unset — 12). Labels that do not fit the width are thinned out.
yTickCountnumber | undefined5The desired number of ticks on the value axis; 5 by default. The step stays "round", so there may be one tick fewer.
xTickFormatChartTickFormat | undefinedundefinedThe tick label of the X axis: `(value, kind) => string`. Unset — the category, or a date or number formatted by the locale.

Slots

SlotTypeDescription
tooltip{ active: GrChartActivePoint; formatValue: (value: number | null) => string; }Custom content for the tooltip panel instead of the standard one; the data of the active position comes in the slot props.
legend{ series: readonly NormalizedSeries[]; toggle: (id: string) => void; }A custom legend instead of the standard one. It is not drawn for an empty chart.
emptyanyA custom placeholder for an empty chart instead of "No data"; it takes up the same `height`.
headeranyA header above the chart, inside its root — above a legend placed on top.

Events

EventSignatureDescription
update:hiddenSeries(event: "update:hiddenSeries", value: string[]): void—
update:activeIndex(event: "update:activeIndex", value: number | null): void—
update:xWindow(event: "update:xWindow", value: GrChartXWindow | null): void—
pointClick(event: "pointClick", value: GrChartActivePoint): void—
pointHover(event: "pointHover", value: GrChartActivePoint | null): void—
legendToggle(event: "legendToggle", value: { seriesId: string; hidden: boolean; }): void—

Methods

MethodsSignatureDescription
elementHTMLElement | nullThe root element — for measuring and scrolling in the consumer.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrChartLineConfigurableProps, GrChartSize, GrChartLineProps } from '@feugene/granularity-charts/components/GrChartLine'

Filedist/types/components/GrChartLine/config.d.ts

grChartLineConfigconst
const grChartLineConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrChartLine">;

Filedist/types/components/GrChartLine/defaults.d.ts

GrChartLineConfigurablePropsinterface
interface GrChartLineConfigurableProps {
    decimate: 'auto' | 'always' | 'never';
    maxPoints: number;
    zoom: GrChartZoom;
    dataTableMaxRows: number | 'auto';
    size: GrChartSize;
    height: number;
    curve: GrChartCurve;
    gaps: 'hidden' | 'shadow' | 'dashed';
    showGrid: 'both' | 'x' | 'y' | 'none';
    showLegend: boolean | 'auto';
    legendPosition: 'top' | 'bottom';
    tooltip: boolean;
    dataTable: 'hidden' | 'visible' | 'off';
}

Filedist/types/components/GrChartFrame/chartFrameStyles.d.ts

GrChartSizetype
type GrChartSize = GrComponentSize;

Filedist/types/components/GrChartLine/safelist.d.ts

grChartLineSafelistconst
const grChartLineSafelist: string[];

Filedist/types/components/GrChartLine/GrChartLine.vue.d.ts

GrChartLinePropsinterface
interface GrChartLineProps {
    series: readonly GrChartSeries[] | readonly number[];
    xScale?: GrChartScaleKind;
    height?: number;
    width?: number;
    yDomain?: readonly [number | null, number | null];
    includeZero?: boolean;
    references?: readonly GrChartReference[];
    includeReferencesInDomain?: boolean;
    dualAxis?: boolean;
    yDomainRight?: readonly [number | null, number | null];
    yTickFormatRight?: (value: number) => string;
GrChartLineEmitsinterface
interface GrChartLineEmits {
    (e: 'update:hiddenSeries', value: string[]): void;
    (e: 'update:activeIndex', value: number | null): void;
    (e: 'update:xWindow', value: GrChartXWindow | null): void;
    (e: 'pointClick', value: GrChartActivePoint): void;
    (e: 'pointHover', value: GrChartActivePoint | null): void;
    (e: 'legendToggle', value: {
        seriesId: string;
        hidden: boolean;
    }): void;
}

Accessibility

APG pattern

При нескольких сериях ↑/↓ переключают читаемую серию

Usage from the library prose

Smoothing does not invent values

curve: 'smooth' is a monotone cubic: it does not throw the curve beyond the range of the neighbouring values. An ordinary cubic spline on a sharp drop gives a “hump” below the minimum or above the maximum of a pair — that is, it draws a value that is not in the data. step is appropriate where a quantity really did change in steps: a tariff, a version, a status.

The gaps bridge is always straight, even with smooth. A curve across a gap would have a shape, that is, it would show the course of a value where it was not measured.

The marks appear by themselves

showPoints: 'auto' draws the marks while the series is short (up to sixty points). On a long series they merge into a solid band and get in the way of reading the line, so they disappear. 'always' on fifty thousand points is a deliberate choice of the consumer, and it costs exactly what it costs.

Above the threshold it is drawn by a canvas

The body of the chart can be drawn in two ways. Below the threshold it is SVG, above it a <canvas>; the component chooses itself, and there is no “which renderer” prop.

The threshold is counted in drawn vertices rather than in points — canvasThreshold, 24,000 by default. The difference is substantial: decimation cuts every series down to the limit of the screen (about two vertices per pixel), so one series of a hundred thousand points is drawn as 2,400 vertices and costs milliseconds, while twenty series of 2,400 — the same 48,000 points — cost sixteen, that is, a whole frame.

The measured numbers at a width of 1200px, 2,400 vertices per series:

SeriesSVGCanvas
11.1 ms0.4 ms
86.7 ms0.6 ms
2016.3 ms1.7 ms

canvasThreshold: 0 switches the canvas off entirely — for the case where the drawing has to stay vector: printing, an SVG export, CSS of your own over the marks.

Accessibility does not change at all with the change of renderer. The cursor, the keyboard, the tooltip and the hidden table work with the overlay and with the full series rather than with the marks: the canvas does not exist for them, it is aria-hidden and does not catch the pointer. That was the condition on which a second renderer was allowed at all.

What the canvas draws differently. The grid moves into it as well: the canvas lies under the <svg> so that the axes and the active point stay on top — and the grid has to stay under the series. In an area chart the gradient fill becomes solid: the canvas does not understand url(#…), and across twenty areas a gradient reads as a mess anyway.

The cursor and the keyboard work with one state

activeIndex is a v-model: a pair of charts is synchronised with it, so that the cursor in one highlights the same abscissa in the other. hiddenSeries is a v-model too, but the component does not apply it itself: the legend emits the intent, and the state belongs to the consumer.

tooltip: false hides only the panel. The cursor under the pointer and from the keyboard, the active marks, update:activeIndex and pointHover stay: on the second chart of a pair the panel is often redundant, but the cursor is needed — it shows the same abscissa.

interactive: false turns the chart into a picture: a role="img" with a name, with no focus, no tooltip and no keyboard. That is the mode for printing and for a tile that is clicked as a whole.

The hidden table is not a fallback

dataTable: 'hidden' (the default) keeps the full data as a table in the accessibility tree but not on the screen. A screen reader reads the values rather than “a chart”. 'visible' shows the same table under the canvas, and 'off' removes it entirely — and that is a deliberate refusal of the accessibility of the data rather than an optimisation.

Zooming along the abscissa

zoom switches the window on: 'brush' is a drag across the canvas, 'wheel' is the wheel, and 'both' is both. It is off by default.

vue
<GrChartLine v-model:x-window="window" :series="series" zoom="both" />

<GrButton :disabled="window === null" @click="window = null">
The whole series
</GrButton>

The window selects the data rather than cropping the drawing: the positions, the cursor, the keyboard, the hidden table and the span of the value axis are computed from it. The practical consequence is that zooming reveals the fine structure that on the full series lies as a solid hatching: the decimation budget is computed from the width of the area, and there are fewer points in the window, so more vertices fall to each of them.

The keyboard works whenever zoom is on: +/- zoom towards the active point, Shift+arrows shift the window, and 0 returns the whole series. The union of the prop enumerates only pointer gestures — zooming has no switchable keyboard by design (../a11y.md, the section on zooming).

v-model:x-window is not mandatory — without a binding the chart zooms by itself. It is bound for something else: a synchronised pair of charts, a reset button next to the canvas, keeping the zoom in the address bar.

The bounds are accepted in the same form as the abscissas of the points (Date, an ISO string, a number) and go out as numbers. With a window set, activeIndex addresses it rather than the whole series.

The hidden table and its ceiling

The full data of the chart as rows — what a screen reader reads instead of the picture. By default dataTable: 'hidden': in the accessibility tree, invisible to the eye.

A row per point is readable while there are not many rows. The table therefore has a ceiling — dataTableMaxRows, 'auto' by default, that is, the budget of the drawing:

vue
<GrChartLine :series="series" />                              <!-- auto: as drawn -->

<GrChartLine :series="series" :data-table-max-rows="200" />   <!-- a ceiling of your own -->

<GrChartLine :series="series" :data-table-max-rows="Infinity" /> <!-- always full -->

<GrChartLine :series="series" data-table="off" />             <!-- there is no table at all -->

Above the ceiling the table prints the same points that are drawn and says so with a note in the footer. The point-by-point completeness is not lost in the process: the arrows walk the whole series and pronounce every point. The table is responsible for the overview and the keyboard for the exact value; the details — ../model.md.

'auto' means “as many rows as can be read”. The budget of the drawing is taken when there is one; with decimate: 'never' there is none, and a fixed ceiling with a uniform sample remains.

Which of those is needed is decided by the application: keeping ten thousand rows in the accessibility tree is its right, but so is the cost of rebuilding such a table.

A threshold is drawn as a reference, not as a series

A plan, a norm and the limit of the acceptable are the references prop rather than a series made of a constant: a constant series would enter the legend, would stretch the axis and would travel into the table as data. The details — ../model.md, the section “A reference is not a series”.

Two axes are a deliberate decision

Series of different orders (money and units) are readable on one chart only with two axes, and two axes let any pair be fitted to a visible correlation. That is why the axis: 'right' of a series does not work until dualAxis is switched on. The invariants — ../model.md, the section “The second value axis”.