Chartist

Search

Find a guide, an API entry or an example.

Try

Trend lines

Fit a straight line through each series with trendLine(), read how well it fits, and draw exponential trends on a log axis.

Values go up and down. Is the series growing anyway? trendLine() answers with one straight line through it, dashed and in the series’ color. It works on the presets and in composed charts, on bars, lines and points. For a fixed goal or limit, see target lines.

It comes from @chartist-js/essentials, with its stylesheet after Chartist’s:

import 'chartist/styles.css';
import '@chartist-js/essentials/styles.css';

A trend through bars

Presets take extra layers in their layers option: above draws them on top of the bars or lines. On bar charts, align: 'center' lines the trend up with the middle of the bars:

src/main.ts
import 'chartist/styles.css';
import '@chartist-js/essentials/styles.css';
import { barChart } from 'chartist';
import { trendLine } from '@chartist-js/essentials';

// Monthly sign-ups with the trend they follow.
barChart(
  '#chart',
  {
    labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'],
    series: [[42, 38, 51, 47, 58, 54, 66, 63]]
  },
  {
    low: 0,
    // Centered, so the line runs through the middle of the bars.
    layers: { above: [trendLine({ align: 'center' })] }
  }
);
Live

The line runs from the series’ first to its last value. Where it ends below or above the values, the Y axis grows to keep it in view. Holes are skipped, and a series with fewer than two values gets no line. Without options, every series gets a line; series picks some by name or index.

Option What it does
series The series to fit, by name or index; by default all of them
x, y The ids of the axes to draw on, in a composed chart
align 'start' (default) or 'center' of each step, to line up with bars
classNames { series, line } replace the classes ct-series and ct-trend-line

Points on a scale

The fit is least squares: on a step axis it uses the values’ order, on a scale axis their x. So a scatter plot gets its regression line:

src/main.ts
import 'chartist/styles.css';
import '@chartist-js/essentials/styles.css';
import { autoScaleAxis, lineChart } from 'chartist';
import { trendLine } from '@chartist-js/essentials';

// Daily ice cream sales against the day's high, in °C.
const days = [
  [14, 215],
  [16, 325],
  [11, 185],
  [15, 332],
  [18, 406],
  [22, 522],
  [19, 412],
  [25, 614],
  [23, 544],
  [18, 421],
  [22, 445],
  [17, 408],
  [24, 590],
  [27, 621],
  [21, 489],
  [13, 230],
  [20, 470],
  [26, 632]
];

lineChart(
  '#chart',
  { series: [days.map(([x, y]) => ({ x, y }))] },
  {
    // Points only: a scatter plot.
    showLine: false,
    axisX: { type: autoScaleAxis(), onlyInteger: true },
    low: 0,
    // The least-squares line through the points.
    layers: { above: [trendLine()] }
  }
);
Live

How well it fits

A rising line says little when the values jump around it. Each trend line’s node has the line’s slope and intercept, and its correlation: Pearson’s r, from -1 for values that all lie on a falling line, through 0 for no straight trend, to 1 for a rising one. Its square is the R² that spreadsheets show next to a trendline, the share of the ups and downs the line explains. A listener can read it from the scene:

src/main.ts
import 'chartist/styles.css';
import '@chartist-js/essentials/styles.css';
import './style.css';
import { alphaNumerate, findAll, lineChart } from 'chartist';
import { trendLine } from '@chartist-js/essentials';

const fit = document.querySelector('#fit')!;
const shops = ['Basel', 'Bern'];

// Two shops over twelve weeks: one grows steadily, one jumps around. Both
// trend lines rise, but only one fits its values well.
lineChart(
  '#chart',
  {
    labels: Array.from({ length: 12 }, (_, week) => `${week + 1}`),
    series: [
      {
        name: shops[0],
        data: [20, 22, 25, 24, 28, 30, 31, 34, 33, 37, 39, 41]
      },
      { name: shops[1], data: [18, 34, 15, 29, 40, 22, 38, 19, 44, 27, 35, 46] }
    ]
  },
  {
    showPoint: false,
    lineSmooth: false,
    layers: { above: [trendLine()] },
    on: {
      // Each trend line's node knows its slope and its correlation r; r²
      // is the share of the ups and downs the line explains.
      scene: ({ scene }) => {
        fit.replaceChildren(
          ...findAll(scene, { role: 'trendLine' }).map(({ ctx }) => {
            const row = document.createElement('span');
            // The series class gives the name its series' color.
            row.className = `ct-series-${alphaNumerate(ctx.seriesIndex)}`;
            const r2 = (ctx.correlation ?? 0) ** 2;
            row.textContent = `${shops[ctx.seriesIndex]}: +${ctx.slope.toFixed(1)} a week, R² ${r2.toFixed(2)}`;
            return row;
          })
        );
      }
    }
  }
);
Live

Both shops grow, but only Basel’s line describes its weeks. With two values r is always 1 or -1, so a fit needs a few values before it means much. A tooltip can show the same numbers on hover.

Exponential growth

On a log axis, the line is fitted to the logarithms of the values, so it’s straight there: an exponential trend on a log Y axis, a power law on two log axes. Here an animal’s metabolic rate grows with its mass to the power of about ¾:

src/main.ts
import 'chartist/styles.css';
import '@chartist-js/essentials/styles.css';
import { axisGrid, axisLabels, cartesianAxes, chart, line } from 'chartist';
import { logAxis, trendLine } from '@chartist-js/essentials';

// Body mass (kg) and resting metabolic rate (W) from a mouse to an
// elephant. The rate grows with the mass to the power of about 3/4
// (Kleiber's law): on two log axes that's a straight line, and the trend
// line fitted to the logarithms finds it.
const animals = [
  { x: 0.02, y: 0.17, meta: 'Mouse' },
  { x: 0.3, y: 1.45, meta: 'Rat' },
  { x: 4, y: 9.1, meta: 'Cat' },
  { x: 30, y: 46, meta: 'Dog' },
  { x: 70, y: 80, meta: 'Human' },
  { x: 600, y: 395, meta: 'Horse' },
  { x: 4000, y: 1800, meta: 'Elephant' }
];

chart('#chart', {
  data: { series: [animals] },
  axes: cartesianAxes([
    { id: 'mass', side: 'bottom', type: logAxis({ minorTicks: true }) },
    { id: 'rate', side: 'left', type: logAxis({ minorTicks: true }) }
  ]),
  layers: [axisGrid(), trendLine(), line({ showLine: false }), axisLabels()],
  // Room for the last X label, which starts at its tick.
  padding: { right: 32 }
});
Live

The slope is then in powers of the base per step, here about 0.75: the rate grows by a factor of 10 to the 0.75 for every tenfold mass.

Styling

A trend line takes its series’ color. Its width and dash pattern are custom properties:

.ct-chart {
  --ct-trend-line-width: 1px;
  --ct-trend-line-dasharray: 2px 2px;
}

The markup: a line.ct-trend-line in a series group (g.ct-series.ct-series-a) per line. In listeners and plugins, find them by the role trendLine, as in events and plugins. Importing @chartist-js/essentials declares the role, so TypeScript knows its context.