GrChartArea

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

Overview

When to take it

  • the whole and the contribution of its parts over time — revenue by channel, traffic by source, time by stage: stacked lays every series on the sum of the previous ones;
  • a single series whose volume matters — a fill down to zero reads as “how much in total” rather than “at what level”;
  • shares changing over time — stacked: '100%' normalises every position to one, and the redistribution is visible rather than the growth;
  • series that have to show through — when overlaid without stacking, fill: 'auto' gives a gradient, and the lower series stays visible.

When to take something else

NeedTake
Show the course of a value, the volume does not matterGrChartLine
Compare quantities across categoriesGrChartBar
Show the composition of a whole at one momentGrChartPie
Compare the shape of a profile across several axesGrChartRadar
A trend in a table cellGrSparkline

With more than seven or eight categories a stack stops being readable: the bands become thinner than their labels, and they can no longer be compared by eye. That is a sign that GrChartBar with grouping is needed, or fewer series.

Examples 5 each with a live demo and its code

  • Revenue by channel: the upper edge is the total

    stacked puts every channel on the sum of the previous ones, so the upper edge reads as the total. The plan is a references line, not a series, and include-references-in-domain keeps it on screen. Hiding a channel in the legend (v-model:hidden-series) drops it from the stack and from the figure above. A new period shows loading, an empty one the empty slot, and the tooltip slot names money in euros.

    Revenue in September€399k+4.7%
    Chart data
    XDirect salesPartnersMarketplace
    Apr17110846
    May17511252
    Jun18211555
    Jul19012157
    Aug19612461
    Sep20412966
    Plan €420k: 420
  • Shares of traffic, not its volume

    stacked="100%" answers «what of what»: growth disappears from the picture on purpose, and the tooltip still names absolute sessions. The legend slot gets the series and toggle, so a custom legend with the latest share hides series like the built-in one; legend-toggle reports it. curve="smooth", show-grid="y" and legend-position="top" keep the bands calm.

    Where visitors come fromShare of sessions by source, last 12 weeks
    Chart data
    XSearchDirectSocialNewsletter
    W295,2003,1001,200900
    W305,3503,1501,260880
    W315,2803,2201,380910
    W325,4003,2601,520940
    W335,3203,3001,690920
    W345,1803,3501,840950
    W355,0503,4202,010930
    W364,9803,4802,190960
    W374,9003,5502,350980
    W384,8203,6002,520970
    W394,7603,6802,680990
    W404,7003,7402,8501,000

  • A day of server load with zoom

    1,440 points on a time axis: decimate thins the drawn path to max-points, while the cursor, the keyboard and the hidden table — capped by data-table-max-rows — still know every minute. zoom="both" takes a brush and the wheel, and v-model:x-window lets «Whole day» reset it. The overload zone is a reference band; point-click pins a moment to the incident.

    api-eu-1 · CPU5 October, UTC · drag over the chart or scroll to zoom in
    Chart data
    XCPU
    Oct 5, 2026, 12:00 AM12
    Oct 5, 2026, 12:14 AM20.5
    Oct 5, 2026, 12:36 AM9.7
    Oct 5, 2026, 12:39 AM18.7
    Oct 5, 2026, 12:58 AM9
    Oct 5, 2026, 01:01 AM18.1
    Oct 5, 2026, 01:23 AM7.6
    Oct 5, 2026, 01:26 AM16.7
    Oct 5, 2026, 01:48 AM6.5
    Oct 5, 2026, 01:51 AM15.6
    Oct 5, 2026, 02:13 AM5.6
    Oct 5, 2026, 02:16 AM14.7
    Oct 5, 2026, 02:35 AM5.9
    Oct 5, 2026, 02:41 AM14.2
    Oct 5, 2026, 03:00 AM5.5
    Oct 5, 2026, 03:03 AM14.7
    Oct 5, 2026, 03:25 AM5.5
    Oct 5, 2026, 03:28 AM14.7
    Oct 5, 2026, 03:50 AM5.7
    Oct 5, 2026, 03:53 AM14.9
    Oct 5, 2026, 04:12 AM6.9
    Oct 5, 2026, 04:18 AM15.4
    Oct 5, 2026, 04:37 AM7.6
    Oct 5, 2026, 04:43 AM16.2
    Oct 5, 2026, 05:02 AM8.6
    Oct 5, 2026, 05:05 AM18
    Oct 5, 2026, 05:27 AM9.9
    Oct 5, 2026, 05:30 AM19.2
    Oct 5, 2026, 05:49 AM12
    Oct 5, 2026, 05:55 AM20.7
    Oct 5, 2026, 06:14 AM13.6
    Oct 5, 2026, 06:20 AM22.4
    Oct 5, 2026, 06:39 AM15.5
    Oct 5, 2026, 06:45 AM24.3
    Oct 5, 2026, 07:04 AM17.5
    Oct 5, 2026, 07:07 AM26.9
    Oct 5, 2026, 07:26 AM20.2
    Oct 5, 2026, 07:32 AM29.1
    Oct 5, 2026, 07:51 AM22.4
    Oct 5, 2026, 07:57 AM31.4
    Oct 5, 2026, 08:16 AM24.7
    Oct 5, 2026, 08:22 AM33.7
    Oct 5, 2026, 08:41 AM27.1
    Oct 5, 2026, 08:47 AM36.1
    Oct 5, 2026, 09:03 AM30
    Oct 5, 2026, 09:09 AM39
    Oct 5, 2026, 09:28 AM32.4
    Oct 5, 2026, 09:34 AM41.4
    Oct 5, 2026, 09:53 AM34.8
    Oct 5, 2026, 09:59 AM43.8
    Oct 5, 2026, 10:18 AM37.1
    Oct 5, 2026, 10:24 AM46.1
    Oct 5, 2026, 10:40 AM39.9
    Oct 5, 2026, 10:46 AM48.8
    Oct 5, 2026, 11:05 AM42
    Oct 5, 2026, 11:11 AM50.8
    Oct 5, 2026, 11:30 AM43.9
    Oct 5, 2026, 11:36 AM52.8
    Oct 5, 2026, 11:55 AM45.7
    Oct 5, 2026, 12:01 PM54.5
    Oct 5, 2026, 12:17 PM47.9
    Oct 5, 2026, 12:26 PM56
    Oct 5, 2026, 12:42 PM49.3
    Oct 5, 2026, 12:48 PM58
    Oct 5, 2026, 01:07 PM50.5
    Oct 5, 2026, 01:13 PM59.1
    Oct 5, 2026, 01:32 PM51.4
    Oct 5, 2026, 01:48 PM54.2
    Oct 5, 2026, 02:00 PM91
    Oct 5, 2026, 02:12 PM98
    Oct 5, 2026, 02:22 PM87
    Oct 5, 2026, 02:30 PM53.6
    Oct 5, 2026, 02:39 PM61.2
    Oct 5, 2026, 02:55 PM53.6
    Oct 5, 2026, 03:04 PM61.1
    Oct 5, 2026, 03:20 PM53.3
    Oct 5, 2026, 03:34 PM62.6
    Oct 5, 2026, 03:45 PM52.7
    Oct 5, 2026, 03:51 PM61
    Oct 5, 2026, 04:10 PM51.9
    Oct 5, 2026, 04:16 PM60
    Oct 5, 2026, 04:32 PM51.7
    Oct 5, 2026, 04:41 PM58.9
    Oct 5, 2026, 04:57 PM50.4
    Oct 5, 2026, 05:11 PM59.1
    Oct 5, 2026, 05:22 PM48.7
    Oct 5, 2026, 05:36 PM57.3
    Oct 5, 2026, 05:47 PM46.9
    Oct 5, 2026, 05:53 PM54.8
    Oct 5, 2026, 06:09 PM45.9
    Oct 5, 2026, 06:18 PM52.7
    Oct 5, 2026, 06:34 PM43.7
    Oct 5, 2026, 06:48 PM52
    Oct 5, 2026, 06:59 PM41.3
    Oct 5, 2026, 07:13 PM49.5
    Oct 5, 2026, 07:24 PM38.8
    Oct 5, 2026, 07:30 PM46.5
    Oct 5, 2026, 07:46 PM37.2
    Oct 5, 2026, 07:55 PM43.9
    Oct 5, 2026, 08:11 PM34.5
    Oct 5, 2026, 08:25 PM42.5
    Oct 5, 2026, 08:36 PM31.7
    Oct 5, 2026, 08:50 PM39.7
    Oct 5, 2026, 09:01 PM28.9
    Oct 5, 2026, 09:15 PM36.9
    Oct 5, 2026, 09:23 PM27.2
    Oct 5, 2026, 09:32 PM33.8
    Oct 5, 2026, 09:48 PM24.4
    Oct 5, 2026, 10:02 PM32.5
    Oct 5, 2026, 10:13 PM21.7
    Oct 5, 2026, 10:27 PM29.8
    Oct 5, 2026, 10:38 PM19
    Oct 5, 2026, 10:52 PM27.2
    Oct 5, 2026, 11:00 PM17.6
    Oct 5, 2026, 11:17 PM24.8
    Oct 5, 2026, 11:25 PM15.2
    Oct 5, 2026, 11:39 PM23.6
    Oct 5, 2026, 11:50 PM13
    Oct 5, 2026, 11:58 PM13.5
    Oct 5, 2026, 11:59 PM19.8
    Overload: 80 to 100
    Showing 120 of 1440 points — the ones the chart draws. Arrow keys walk all of them.

    Click a point to add it to the incident.

  • Two charts with one cursor

    v-model:active-index ties the pair: hovering either moves the cursor in both, the tooltips name the same minute, and the readout above sums it up in one line. Requests per minute and milliseconds differ by two orders of magnitude — dual-axis with y-domain-right, y-tick-format-right and value-format-right is a deliberate choice here, not a default. size="sm" keeps the pair compact.

    Checkout API · last hour13:59 · 1,232 requests · p95 140 ms · 1 errors
    Chart data
    XRequests / min (left axis)p95 latency, ms (right axis)
    13:001,100165
    13:011,176167
    13:021,160169
    13:031,235170
    13:041,217170
    13:051,199170
    13:061,271169
    13:071,250167
    13:081,320165
    13:091,295163
    13:101,269160
    13:111,333156
    13:121,303152
    13:131,363148
    13:141,328144
    13:151,291140
    13:161,344136
    13:171,303132
    13:181,352127
    13:191,306124
    13:201,259120
    13:211,302117
    13:221,252115
    13:231,200113
    13:241,238111
    13:251,184110
    13:261,221110
    13:271,165110
    13:281,109111
    13:291,146113
    13:301,090115
    13:311,126117
    13:321,072120
    13:331,018124
    13:341,057128
    13:351,006132
    13:361,048136
    13:371,000140
    13:38953144
    13:391,001149
    13:40958153
    13:411,010156
    13:42972320
    13:43936323
    13:44995325
    13:45963327
    13:46934329
    13:47999330
    13:48974170
    13:491,042170
    13:501,020169
    13:511,000167
    13:521,073165
    13:531,055163
    13:541,130160
    13:551,113156
    13:561,097152
    13:571,173148
    13:581,157144
    13:591,232140
    Chart data
    XErrors / min
    13:000
    13:013
    13:022
    13:031
    13:040
    13:053
    13:062
    13:071
    13:080
    13:093
    13:102
    13:111
    13:120
    13:133
    13:142
    13:151
    13:160
    13:173
    13:182
    13:191
    13:200
    13:213
    13:222
    13:231
    13:240
    13:253
    13:262
    13:271
    13:280
    13:293
    13:302
    13:311
    13:320
    13:333
    13:342
    13:351
    13:360
    13:373
    13:382
    13:391
    13:400
    13:413
    13:4218
    13:4323
    13:4421
    13:4519
    13:4624
    13:4722
    13:480
    13:493
    13:502
    13:511
    13:520
    13:533
    13:542
    13:551
    13:560
    13:573
    13:582
    13:591

API generated from the package · cannot drift

Props

PropTypedefaultDescription
xScaleGrChartScaleKind | undefinedundefinedType of the X axis. Not set — inferred from the data.
stackedboolean | "100%" | undefinedfalseStack the series: each band lies on the sum of the previous ones. `'100%'` additionally normalizes every position to one — the band then shows how the **distribution** changed, not the magnitudes. The own values of the points are left alone: the tooltip and the table say "forty", not "a third".
fill"auto" | "gradient" | "solid" | undefinedundefinedFill. `auto` — a gradient for an overlay, solid for a stack.
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 goes 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, an acceptable corridor. Not a series and never counted as one: it does not get into the legend, does not spend a palette index, does not join the stack and goes into the hidden table as a note, not as 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 so far that the data itself would collapse 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 deliberate: two axes let you fit any two series to a visible correlation, and that must be a decision of the chart author, 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.

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 measurements 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 { GrChartAreaConfigurableProps, GrChartSize, GrChartAreaProps } from '@feugene/granularity-charts/components/GrChartArea'

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

grChartAreaConfigconst
const grChartAreaConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrChartArea">;

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

GrChartAreaConfigurablePropsinterface
interface GrChartAreaConfigurableProps {
    decimate: 'auto' | 'always' | 'never';
    maxPoints: number;
    zoom: GrChartZoom;
    dataTableMaxRows: number | 'auto';
    size: GrChartSize;
    height: number;
    curve: GrChartCurve;
    fill: 'auto' | 'gradient' | 'solid';
    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/GrChartArea/safelist.d.ts

grChartAreaSafelistconst
const grChartAreaSafelist: string[];

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

GrChartAreaPropsinterface
interface GrChartAreaProps {
    series: readonly GrChartSeries[] | readonly number[];
    xScale?: GrChartScaleKind;
    stacked?: boolean | '100%';
    fill?: 'auto' | 'gradient' | 'solid';
    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;
GrChartAreaEmitsinterface
interface GrChartAreaEmits {
    (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

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 fill is chosen by the mode rather than by taste

fill: 'auto' means a gradient for an overlay and a dense fill for a stack. The reason is different in each case: overlaid series need to show through one another, while the bands of a stack stand flush, and a gradient would blur the boundary between two neighbours — that is, the very place where one part ends and another begins.

The value of a point stays its own

In a stack the bounds of the band arrive on a point as separate fields, but its own y is not touched. The tooltip, the hidden table and the live region say “partners — one hundred and ninety” rather than “six hundred and fifty, because retail lies underneath”.

A hidden series drops out of the stack, and the ones above it come down. The value axis is computed from the tops of the bands and always includes zero: a stack from a non-zero base lies about the proportions.

A stack is built from the full series. A gap is the only place where it is not entirely honest: the sum at that position is understated. Carrying the last known value forward would be worse — that would be drawing data that did not exist.

A hundred per cent answers a different question

stacked: true shows the quantities and their sum, and stacked: '100%' only the distribution: the sum at every position equals one, and the band says how the shares changed rather than how much there was in total. One question has to be chosen: the reader does not know whether to compare the heights of the bands or their shares.

What is normalised is the drawing, not the data. The axis moves to percentages, while the tooltip, the hidden table and the live region still name the absolute quantities. A position with a zero sum has nothing to normalise against — there there is a zero rather than a NaN.

A long series is shortened in the drawing rather than in the data

decimate: 'auto' (the default) thins out the vertices of the path when there are more points than the screen can show. In a stack the set of abscissas is shared across all of the series — otherwise the bottom of the upper band would be interpolated over one set of X values and the top of the lower one over another, and the fill would come apart at the seams. The cursor, the keyboard, the tooltip and the hidden table know the full series; the details — ../model.md, the section “Decimation is a projection, not the data”.

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
<GrChartArea 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
<GrChartArea :series="series" />                              <!-- auto: as drawn -->

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

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

<GrChartArea :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”.