Documentation

Migrating from highcharts-regression

How to move an existing project to @statili/highcharts.

@statili/highcharts reads regression: true and regressionSettings exactly as highcharts-regression does, so an existing chart config moves across without edits.

It fixes several defects in the incumbent — which has been unchanged since October 2020 and ships no test suite — and adds accessible chart descriptions for free.

The change

npm install @statili/highcharts

The incumbent installs itself by patching Highcharts.Chart.prototype. This one is a function you call on your options:

- import "highcharts-regression";
+ import { withRegression } from "@statili/highcharts";

- Highcharts.chart("container", options);
+ Highcharts.chart("container", withRegression(options).options);

Your series array is untouched:

const options = {
  series: [{
    name: "Sales",
    data: [[1, 12], [2, 19], [3, 29]],
    regression: true,
    regressionSettings: {
      type: "polynomial",
      order: 3,
      name: "Fit: %eq (R²=%r2)",
    },
  }],
};

Every documented option is supported: type, order, name, decimalPlaces, lineType, lineWidth, dashStyle, color, visible, hideInLegend, index, legendIndex, tooltip, dataLabels, extrapolate, loessSmooth, useAllSeries and regressionSeriesOptions. All four name placeholders (%eq, %r, %r2, %se) work.

Deliberate differences

Each fixes a defect. Two are visible on a migrated chart, so check those first.

%r carries its sign — visible change

The incumbent computes Math.sqrt(rSquared), which is always positive. A perfectly inverse relationship reports r = 1, indistinguishable from a perfectly positive one.

Any chart with a negative correlation will now show a different number — r = -1 where it previously showed 1. That is the correct value.

No plot lines are added — no visible change, by design

The native API can mark a polynomial’s vertex or a classifier’s decision boundary. The compat layer suppresses that, so a migrated chart gains nothing unexpected. Use the native fit API if you want it.

Your data is no longer mutated

The incumbent writes array indices onto live Highcharts point objects (data[n][0] = data[n].x) and calls .sort() on the array you passed it, permanently reordering your series. Neither is documented.

@statili/highcharts never writes to your input. If your code relied on the series being sorted afterwards, sort it yourself.

Degenerate data draws no line

Given points that all share an x, the incumbent returns a horizontal line through their mean with r² = 0 and no error signal — a confident trend line through data that has no trend.

Here, no series is added and the reason is reported:

const { options, fits } = withRegression(chartOptions);

if (fits[0].error) {
  console.warn(fits[0].error.message);
  // "Unable to determine a trend: all X values are identical."
}

useAllSeries works

In the incumbent, processSerie loops for (di = 0; di < series.length; di++) where di is undeclared and series is not in scope at that point — it exists only in the init wrapper. The documented option cannot run as written.

Here it pools every series’ data into the fit, as documented.

What you gain

Accessible chart descriptions

Charts get a text alternative built from the same numbers they draw, so the prose and the plot cannot drift apart:

options.accessibility.description;
// "Sales. Each additional unit of week is associated with an increase of 7.85
//  in sales. At week = 0 the fitted value of sales is 4.55. The slope of 7.85
//  is statistically significant (p = 8.7e-9, 6 degrees of freedom)…"

Axis names come from your chart’s own xAxis.title.text and yAxis.title.text — no extra configuration.

Structured facts

Each fitted series returns machine-readable claims alongside the chart options:

const { fits } = withRegression(options);

fits[0].facts.map((f) => f.kind);
// ["trend.linear", "fit.significance", "fit.quality", "fit.error", "caveat.small-sample"]

fits[0].facts[0].claim;
// { direction: "rising", slope: 7.85, intercept: 4.55 }

Facts are JSON-serialisable, carry the provenance of every judgement, and render at two verbosities. See @statili/forge.

loess and exponential are real fits

Both are supported. loess has caveats worth knowing — see below.

loess caveats

LOESS fits no global model, so some placeholders have nothing to answer with:

Placeholder Under loess
%eq "LOESS smoothing" — there is no equation
%r "n/a" — no slope, so no sign to report
%r2 real
%se real

extrapolate is also inert under loess. A smoothed value at an unobserved x would require refitting against the whole dataset, so the curve genuinely cannot be projected past the data.

In exchange, LOESS reports shape — where the curve turns:

fits[0].facts.find((f) => f.kind === "trend.smoothed").claim;
// { bandwidth: 0.4, turningPoints: [{ x: 5, kind: "peak" }], netDirection: "rising" }

Moving beyond the compat layer

withRegression exists to make switching cheap. It is a thin adapter over a native API that the incumbent’s option shape cannot express:

import { analyse } from "@statili/highcharts";

const { options, fits } = analyse({
  xAxis: { title: { text: "week" } },
  yAxis: { title: { text: "signups" } },
  series: [{
    type: "scatter",
    name: "Signups",
    data,
    fit: {
      method: "linear",
      showBand: true,          // confidence band as an arearange series
      confidenceLevel: 0.95,
      extrapolate: 4,          // project past the data
      units: { y: "users" },   // units in the rendered prose
    },
  }],
});

showBand needs Highcharts’ highcharts-more module for the arearange series type. Import both from highcharts/esm/ — mixing the root UMD build with an ESM module creates two separate Highcharts instances, and the series type registers against the wrong one:

import Highcharts from "highcharts/esm/highcharts";
import "highcharts/esm/highcharts-more";

The compat layer will not gain new options. Anything new lands on the native API, which keeps regressionSettings behaving exactly as it does today.