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.
Choose an analytical workflow
Section titled “Choose an analytical workflow”| Question | Feature | Read next |
|---|---|---|
| Are missing months being connected? | UTC calendar ticks and explicit gap policy | Calendar axes |
| What is the recent average? | Trailing moving average | Moving averages |
| Is there a linear relationship? | Regression and mean confidence interval | Regression |
| How does this compare with a previous period? | Exact calendar comparison | Period comparisons |
| What would continuation of a linear trend imply? | Forecast and prediction interval | Forecasts |
| What is the mean, median, percentile, or spread? | Calculated references | References |
| How do groups compare on the same scale? | Dimension-driven small multiples | Facets |
| How does actual performance compare with a target? | Bullet ranges and variance | Bullet charts |
| How do distributions differ? | Grouped box plots | Distributions |
| Which categories account for most of the total? | Pareto preset | Pareto |
| Which source records contributed to this point? | Application/Pivot source-record provider | Underlying records |
Try the analytical views
Section titled “Try the analytical views”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…
A first analytical chart
Section titled “A first analytical chart”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.
Calendar axes and missing observations
Section titled “Calendar axes and missing observations”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 policy | Meaning |
|---|---|
Numeric 0 | An observed zero; it remains a real point. |
Explicit null | A 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.
Moving averages
Section titled “Moving averages”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.
Regression and confidence intervals
Section titled “Regression and confidence intervals”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.
Calendar period comparisons
Section titled “Calendar period comparisons”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 afterasOfare 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.
Linear forecasts and prediction intervals
Section titled “Linear forecasts and prediction intervals”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.
Read calculated results without SVG
Section titled “Read calculated results without SVG”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.
Calculated reference lines and bands
Section titled “Calculated reference lines and bands”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.
| Scope | Observations used |
|---|---|
all (default) | The chart’s canonical dataset after upstream queries, filters, and ranking. It does not mean every record in a remote database. |
visible | Categories in the chart’s authored viewport. Display sampling does not redefine the population. |
selected | Observations 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.
Use the same tools in Pivot
Section titled “Use the same tools in Pivot”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.
Compare groups with small multiples
Section titled “Compare groups with small multiples”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.
Compare actuals with targets
Section titled “Compare actuals with targets”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
Section titled “Pareto analysis”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.
Inspect underlying source records
Section titled “Inspect underlying source records”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.