Skip to content

Analysis

Charts can show a trend, explain how it was calculated, and help a reader inspect its source. Analytical settings belong to the shared @revolist/charts engine: standalone charts and the native Pivot renderer use the same calculations.

Start with Installation and Data & Configuration for mounting a chart and mapping rows. This guide explains the analytical methods, their configuration, and what their results mean.

QuestionFeatureRead next
Are missing months being connected?UTC calendar ticks and explicit gap policyCalendar axes
What is the recent average?Trailing moving averageMoving averages
Is there a linear relationship?Regression and mean confidence intervalRegression
How does this compare with a previous period?Exact calendar comparisonPeriod comparisons
What would continuation of a linear trend imply?Forecast and prediction intervalForecasts
What is the mean, median, percentile, or spread?Calculated referencesReferences
How do groups compare on the same scale?Dimension-driven small multiplesFacets
How does actual performance compare with a target?Bullet ranges and varianceBullet charts
How do distributions differ?Grouped box plotsDistributions
Which categories account for most of the total?Pareto presetPareto
Which source records contributed to this point?Application/Pivot source-record providerUnderlying records

Use the selector to compare moving averages, regression, calendar comparisons, forecasts, calculated references, and Pareto analysis. The example uses synthetic data to explain the settings and methods.

A trailing 3-observation mean reduces month-to-month noise. All 3 values must be finite; the missing June 2025 observation is never treated as zero.

Loading analysis example…

Run this function after your container is mounted. Give the container a height, for example height: 440px. The example generates monthly demonstration values; replace them with your own observations in an application.

import { createLineChart } from '@revolist/charts/line';
export async function mountRevenueAnalysis(container: HTMLElement) {
const data = Array.from({ length: 24 }, (_, index) => ({
month: new Date(Date.UTC(2025, index, 1)).toISOString().slice(0, 10),
revenue: 100 + index * 5 + [0, 12, -8][index % 3],
}));
const chart = await createLineChart({
container,
data,
mapping: {
category: { field: 'month' },
series: [{ field: 'revenue', label: 'Revenue' }],
},
axes: {
x: { scale: 'time', interval: 'month', missingPeriods: 'gap' },
},
presentation: {
title: 'Monthly revenue',
yAxisTitle: 'Revenue (€ thousands)',
legendPosition: 'none',
},
analysis: {
movingAverages: [
{ id: 'rolling', seriesId: 'revenue', window: 3, minPeriods: 3 },
],
regressions: [
{
id: 'trend',
seriesId: 'revenue',
showConfidence: true,
confidence: 0.95,
},
],
},
});
return chart;
}

The source line remains the observed data. Moving averages and fitted lines are separate derived overlays. They can be inspected with the keyboard but cannot activate a source-data drill or acquire an invented record identity.

In the remaining standalone examples, chart is the controller returned above. Mapping a numeric field creates its initial series ID from that field. For an existing dataset, check chart.getDataset().series rather than guessing IDs.

await chart.update({
axes: {
...chart.model.axes,
x: { scale: 'time', interval: 'month', missingPeriods: 'gap' },
},
});

Time-axis ticks align to real UTC calendar boundaries. Labels adapt to the available space: a year, quarter, month, date, or intraday timestamp. interval sets the minimum tick cadence and, when gaps are requested, the calendar bucket used to identify an absent period.

Input or policyMeaning
Numeric 0An observed zero; it remains a real point.
Explicit nullA missing observation; it breaks the source path.
An absent March category with interval: 'month', missingPeriods: 'gap'February and April remain separate finite runs. No March value is created.
Omitted missingPeriods, or 'connect'Connect existing finite observations across absent calendar buckets. Explicit nulls still break the path.

Supported intervals are day, week, month, quarter, and year. A gap policy requires a time axis and an explicit interval. Comparisons use UTC calendar buckets, so adjacent month ends such as February 29 and March 31 are not incorrectly treated as a missing month.

The policy does not resample or aggregate transactions. Supply your intended observation grain before charting. Customize labels through localeText.renderer.formatTimeAxisTick(epoch, dateOnly, defaultText); the formatter receives the actual tick instant. See also Axes and Date ranges.

await chart.update({
analysis: {
...chart.model.analysis,
movingAverages: [
{
id: 'rolling',
seriesId: 'revenue',
window: 3,
minPeriods: 2,
label: 'Trailing 3 observations · at least 2 available',
},
],
},
});

A moving average uses the trailing window ordered category positions. Null positions occupy space in that window but do not enter the sum or finite count. minPeriods defaults to window; until enough finite observations are present, the result is null.

For [10, null, 30], a three-position window with minPeriods: 2 produces 20 at the third position. With minPeriods: 3, it produces null. An absent category is not a position: this is an observation-window average, not automatically a three-calendar-month average. To express that distinction, provide explicit missing positions and choose the intended grain before calculating.

window accepts 2–1,000 positions; minPeriods accepts 1 through window. The resolver reports insufficient-data when no point can be calculated.

await chart.update({
analysis: {
...chart.model.analysis,
regressions: [
{
id: 'trend',
seriesId: 'revenue',
label: 'Linear trend · 95% mean confidence interval',
showConfidence: true,
confidence: 0.95,
},
],
},
});

Regression fits ordinary least squares to finite source observations. The X coordinate is elapsed UTC days on a time axis, a category position on a category axis, or the bound numeric X value on scatter, bubble, and quadrant charts. A categorical trend therefore depends on category ordering; it does not infer elapsed time from labels.

Inspection exposes sample count, slope, intercept, R², RMSE, and residual standard error. The diagnostics identify the X unit and xOrigin. For a time fit, the intercept is the fitted value at that origin, not at epoch zero. R² is null when the observed Y values have no variation.

With showConfidence: true, the band estimates uncertainty in the mean response. Its margin uses Student-t with n − 2 degrees of freedom:

margin = t × s × sqrt(1/n + (x − meanX)² / Sxx)

Here s is the residual standard error and Sxx is the sum of squared centered X values. Accepted confidence levels are 0.9, 0.95, and 0.99; the default is 0.95. A two-observation fit can produce a line but cannot estimate a residual interval: its result explicitly reports intervalUnavailable: 'insufficient-degrees-of-freedom'.

At least two finite observations and variation in X are needed for a fit. On a relationship chart, the selected Y series must be bound to the Y channel. If the same Y measure belongs to several X groups, provide xSeriesId to identify which bound X measure to use. See Scatter for numeric bindings.

await chart.update({
analysis: {
...chart.model.analysis,
periodComparisons: [
{
id: 'prior-year',
seriesId: 'revenue',
unit: 'year',
offset: 1,
partialPeriod: 'exclude',
asOf: Date.UTC(2026, 9, 15),
label: 'Previous-year value · completed periods',
},
],
},
});

A comparison matches each observation to the exact UTC timestamp shifted by offset weeks, months, quarters, or years. The default offset is one; month-end shifts clamp to the last valid day of the destination month. Timestamp matches must be unique. Missing matches or null values produce missing results.

The overlay shows the matched previous value, aligned with the current observation. The resolver also returns referenceCategoryIndex, delta, percentChange, and partial for application-owned summaries. Percentage change uses 100 × (current − previous) / abs(previous); it is null when the previous value is zero. Unrepresentable arithmetic is not exposed as Infinity.

asOf is your explicit coverage cutoff in epoch milliseconds. It defaults to the latest source instant, rather than today’s date. A month labeled by its first day does not establish that the month’s data is complete; supply the actual coverage cutoff when bucket labels differ from reporting completeness. The period containing an observation is considered partial until its UTC end has passed that cutoff.

  • partialPeriod: 'include' is the default and retains eligible matches with explicit partial-period metadata.
  • partialPeriod: 'exclude' omits unfinished periods. Observations after asOf are excluded under either policy.

For unit: 'year' with an October 15 cutoff, the current calendar year is unfinished. Choose unit: 'month' to compare completed months instead of excluding that entire year. The library does not infer which records are complete or aggregate daily transactions into monthly totals.

await chart.update({
analysis: {
...chart.model.analysis,
forecasts: [
{
id: 'forecast',
seriesId: 'revenue',
unit: 'month',
horizon: 6,
confidence: 0.95,
label: 'Linear extrapolation · 95% prediction interval',
},
],
},
});

A forecast extends the linear OLS fit at the explicitly chosen UTC calendar cadence. horizon is the number of future predictions and accepts 1–120. Forecasting requires at least three finite observations and variation in their time coordinates. Its confidence levels match regression.

A prediction interval estimates uncertainty for a future individual observation. It includes residual variability in addition to uncertainty in the mean, so its margin has an extra 1 under the square root:

margin = t × s × sqrt(1 + 1/n + (x − meanX)² / Sxx)

This method assumes a linear relationship with independent, constant-variance residuals. It does not estimate seasonality, holidays, structural breaks, or causal effects. Assess those assumptions before interpreting the band as useful forecast uncertainty. A close historical fit does not establish forecasting accuracy.

Future points are separate from historical observations and have no invented source category. Automatic display domains include predictions and intervals; authored bounds and viewports retain precedence. Source CSV exports retain the canonical dataset, rather than manufacturing source rows for the forecast.

import { resolveChartAnalysis } from '@revolist/charts/core';
const result = resolveChartAnalysis(
chart.getDataset(),
chart.model.analysis,
chart.model.axes,
{ chartType: chart.model.chartType, bindings: chart.model.bindings },
);
for (const overlay of result.overlays) {
console.log(overlay.id, overlay.status, overlay.diagnostics);
if (overlay.kind === 'periodComparison') {
console.table(
overlay.points.map(({ x, value, delta, percentChange, partial }) => ({
instant: new Date(x).toISOString(),
previousValue: value,
delta,
percentChange,
partial,
})),
);
}
}

The resolver operates on the supplied canonical dataset before display sampling. It does not query raw transactions or read rendered SVG. Pass the same bindings and chart type when resolving relationship regressions so its numeric X/Y groups match the rendered chart.

await chart.update({
annotations: {
...chart.model.annotations,
calculated: [
{ id: 'average', seriesId: 'revenue', statistic: 'mean', scope: 'all' },
{ id: 'middle', seriesId: 'revenue', statistic: 'median', scope: 'all' },
{
id: 'p90',
seriesId: 'revenue',
statistic: 'percentile',
percentile: 90,
scope: 'visible',
},
{
id: 'spread',
seriesId: 'revenue',
statistic: 'standardDeviation',
deviations: 1,
scope: 'all',
},
],
},
});

Mean and median produce lines. The mean is an unweighted mean of the accepted chart-cell values. Those values may already be Pivot aggregates: averaging regional revenue averages does not compute a record-weighted average across all transactions. Compute the appropriate measure upstream when that is your question. Percentiles use linear interpolation between ordered finite values. A standard-deviation band is mean ± deviations × σ, where σ is the population standard deviation, dividing by n. The default multiplier is one; positive multipliers up to ten are accepted.

ScopeObservations used
all (default)The chart’s canonical dataset after upstream queries, filters, and ranking. It does not mean every record in a remote database.
visibleCategories in the chart’s authored viewport. Display sampling does not redefine the population.
selectedObservations matching the chart’s semantic selection. An empty selection produces no reference.

Nulls, synthetic Others, subtotals, and grand totals are excluded. Empty scopes omit the reference; references recalculate after relevant updates. Unrepresentable calculated coordinates reject rather than silently rendering an invalid band.

Reference IDs must be unique across calculated references, explicit numeric references, notes, and shapes. A reference follows its series’ axis assignment unless axis is explicitly supplied. A secondary reference requires a plotted secondary series. See Reference lines and Reference bands for authored numeric annotations that do not depend on a statistic.

Register Pivot Charts on an active Pivot grid first. In this example, the Pivot is configured with a date row dimension and a revenue measure. The function finds the public plugin, reads the projected dataset, and uses its actual measure ID.

import { PivotChartsPlugin } from '@revolist/pivot';
export async function mountPivotRevenueAnalysis(
grid: HTMLRevoGridElement,
container: HTMLElement,
) {
const charts = (await grid.getPlugins()).find(
(plugin) => plugin instanceof PivotChartsPlugin,
);
if (!charts)
throw new Error('Register PivotChartsPlugin before creating a chart.');
const dataset = await charts.getChartData({
chartType: 'line',
totals: 'none',
});
const revenue = dataset.series.find(
(series) => series.measure?.prop === 'revenue',
);
if (!revenue)
throw new Error('Configure a revenue measure on the Pivot first.');
const chart = await charts.createChart({
container,
chartType: 'line',
totals: 'none',
seriesIds: [revenue.id],
axes: { x: { scale: 'time', interval: 'month', missingPeriods: 'gap' } },
analysis: {
movingAverages: [{ id: 'rolling', seriesId: revenue.id, window: 3 }],
regressions: [
{ id: 'trend', seriesId: revenue.id, showConfidence: true },
],
},
annotations: {
calculated: [{ id: 'average', seriesId: revenue.id, statistic: 'mean' }],
},
underlyingRecords: true,
});
return chart;
}

Pivot supplies data acquisition, query/filter context, and linked refreshes. Analytical calculations still run in shared Charts. Read Pivot datasets through charts.getChartData(...); getDataset() belongs to the standalone controller. For a source-record action using Pivot’s built-in drillthrough, retain a current linked chart without an independent chart query. Details follow below.

Use a real dataset dimension instead of copying a chart manually for each group. Inspect available dimensions first; the IDs belong to analytical-frame fields, not arbitrary display labels.

import { createChart, getChartFacetDimensions } from '@revolist/charts';
import type { ChartDataset } from '@revolist/charts';
export async function mountRegionalPanels(
container: HTMLElement,
dataset: ChartDataset,
) {
const [dimension] = getChartFacetDimensions(dataset);
if (!dimension) throw new Error('Supply a dataset with a usable dimension.');
return createChart({
container,
dataset,
chartType: 'smallMultiples',
facets: {
dimension: dimension.id,
type: 'column',
scaleMode: 'shared',
pageSize: 6,
page: 0,
},
});
}

Line, bar, column, and area panels support shared or independent value scales. Shared scales include panels on other pages. Page capacity also depends on the host’s dimensions, and previous/next controls keep later groups reachable. Without dimension, the existing one-panel-per-series mode remains available. See Small multiples for dataset examples.

Bullet charts combine actual/target bindings with authored qualitative ranges. The range boundaries are strictly increasing upper limits; they do not change source values. Optional variance can be absolute or percentage, and percentage variance is undefined for a zero target.

import { createChart, createChartDataset } from '@revolist/charts';
export async function mountTargetComparison(container: HTMLElement) {
const dataset = createChartDataset(
[{ team: 'North', actual: 88, target: 100 }],
{
category: { field: 'team' },
series: [{ field: 'actual' }, { field: 'target' }],
},
);
return createChart({
container,
dataset,
chartType: 'bullet',
bindings: { actual: ['actual'], target: ['target'] },
bullet: {
minimum: 0,
maximum: 120,
ranges: [
{ to: 50, label: 'Below target', color: '#dedee5' },
{ to: 90, label: 'On track', color: '#b6a9ff' },
{ to: 120, label: 'Strong', color: '#8971f5' },
],
showVariance: true,
varianceFormat: 'percent',
},
});
}

Read Bullet charts for range interpretation and binding requirements.

Compare distributions with grouped box plots

Section titled “Compare distributions with grouped box plots”

A grouped box plot places genuine distributions for several measures side by side. Series colors remain stable; inspection exposes sample size, summary statistics, and outlier information. Supply observations or valid distribution summaries, rather than converting an aggregate total into invented samples.

Box plots documents the standalone distribution schema. Pivot keeps its automatic single eligible measure default; select multiple eligible seriesIds or bindings.value explicitly to compare several distributions, subject to source-sample availability and budgets.

Pareto analysis sorts nonnegative category contributions descending and overlays the cumulative share. Use the preset to construct an explicit derived dataset and a combination chart with an independent percentage axis.

import {
createChart,
createChartDataset,
createParetoPreset,
} from '@revolist/charts';
export async function mountContributionAnalysis(container: HTMLElement) {
const dataset = createChartDataset(
[
{ cause: 'Delivery', cost: 60 },
{ cause: 'Returns', cost: 30 },
{ cause: 'Billing', cost: 10 },
],
{ category: { field: 'cause' }, series: [{ field: 'cost' }] },
);
return createChart({
container,
...createParetoPreset(dataset, {
seriesId: 'cost',
threshold: 80,
limit: 2,
}),
});
}

The example retains Delivery and Returns, then groups Billing into Others. Cumulative percentages are 60%, 90%, and 100%: the denominator includes all eligible contributions, including those grouped into Others. limit limits the individually ranked categories; the disclosed Others group is additional. Equal contributions retain their input order.

Nulls, totals, and existing synthetic categories are excluded. Negative values and zero-total populations reject. Original category IDs and semantic paths remain attached to retained categories, while cumulative and Others marks are inspection-only. The resulting dataset is a flat analytical snapshot, not a live hierarchy.

On an active Pivot chart plugin, use the corresponding snapshot factory:

const dataset = await charts.getChartData({
chartType: 'groupedColumn',
totals: 'none',
});
const [measure] = dataset.series;
if (!measure) throw new Error('Configure a Pivot measure first.');
const paretoChart = await charts.createParetoChart({
container,
pareto: { seriesId: measure.id, threshold: 80, limit: 10 },
});

Here charts is the public PivotChartsPlugin instance obtained in the earlier Pivot example. createParetoChart creates an unlinked saved snapshot; create another snapshot when you want to rerun the ranking against refreshed source results.

View underlying records is distinct from a table of chart values. Chart values can be aggregates; underlying records must come from a source provider. The action becomes available after focusing or selecting a real observation. It supports keyboard entry, paginated rows, cancellation, and focus return.

For standalone charts, supply application-owned access. This example explicitly aggregates raw orders into daily chart values, then queries those same raw orders for the inspected date. It does not infer transactions from an aggregate.

import { createLineChart } from '@revolist/charts/line';
export async function mountOrders(container: HTMLElement) {
const orders = [
{ id: 'A1', date: '2026-01-01', revenue: 30 },
{ id: 'A2', date: '2026-01-01', revenue: 70 },
{ id: 'A3', date: '2026-01-02', revenue: 80 },
];
const data = [...new Set(orders.map((order) => order.date))].map((date) => ({
date,
revenue: orders
.filter((order) => order.date === date)
.reduce((total, order) => total + order.revenue, 0),
}));
return createLineChart({
container,
data,
mapping: {
category: { field: 'date' },
series: [{ field: 'revenue', label: 'Revenue' }],
},
axes: { x: { scale: 'time' } },
underlyingRecords: async ({ target, offset, limit, signal }) => {
if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
const matching = orders.filter(
(order) => order.date === target.category.path[0],
);
return {
records: matching.slice(offset, offset + limit),
totalCount: matching.length,
columns: [
{ key: 'id', label: 'Order' },
{ key: 'date', label: 'Date' },
{ key: 'revenue', label: 'Revenue' },
],
};
},
});
}

For a remote provider, forward signal to your request, preserve offset and limit, and enforce your source-access and column policies on the server. The shared view requests 50 rows per page. Returned columns must have unique nonempty keys and labels; invalid pages or contradictory counts show an error. Closing, updating, or destroying the chart aborts pending work and late responses cannot reopen the view.

Pivot creation and restore accept underlyingRecords: true to reuse PivotPlugin.loadPivotDrilldown with the observation’s semantic cell and current committed query scope. Built-in access requires a current linked chart without an independent chart query. Independent queries and unlinked snapshots need an explicit application provider. Synthetic groups, aggregates, and future predictions cannot request fabricated source rows.

Providers are runtime callbacks and are not saved in a model. Reattach the appropriate provider when restoring a chart.

Supported configurations, persistence, and limits

Section titled “Supported configurations, persistence, and limits”
  • Analytical overlays support ordered, unstacked Cartesian charts. Scatter, bubble, and quadrant charts support numeric regression. Stacked/normalized overlays and arbitrary categorical forecasting reject.
  • Calendar comparisons and forecasts require a time X axis. Comparisons require unique category timestamps. Calculated references also depend on the chart’s annotation and axis capabilities.
  • The shared analysis resolver accepts at most 50,000 ordered categories and 32 overlays across all analysis arrays. The chart/query acquisition limits can be lower; calculations describe the supplied dataset, not records that were never acquired.
  • chart.update({ analysis: {} }) clears analytical overlays. Settings objects are replaced as a whole: spread the previous object when retaining its other fields, as the examples above do.
  • Analysis, calculated references, facets, bullet settings, and explicit axes persist in saved models. Derived points are recalculated; raw source-record providers must be supplied again. See Saved state.
  • Await updates before reading state or exporting. Call chart.destroy() when removing the host so pending work and event handlers are released.

The settings express an analytical method; they do not establish whether the input is representative, complete, or suitable for that method. Keep units, aggregation grain, query scope, and incomplete periods visible to readers.