Skip to content

Seasonality ​

A seasonality view lays the same calendar window (a year, a quarter or a month) of several years on top of each other. Each year is one line, normalised to a common start, so you can see whether a market tends to rise or fall at certain times of year. An optional average path, with the range of the years shaded around it, sums them up.

ts
import { createChart, createSeasonality, dataSource } from '@chart-on/core';

const chart = createChart(container, { plugins: [dataSource()] });
chart.setDataSource(source);

const view = createSeasonality(chart, {
  symbol: 'WHEAT',      // daily bars, loaded through the chart's data source
  years: 5,             // the newest five years with data
  window: 'year',       // or 'quarter' / 'month'
  normalize: 'percent', // or 'indexed' (each year starts at 100) / 'none' (prices)
  average: 'mean',      // or 'median'; leave out for no average
});
await view.ready;

The time axis shows the window's calendar: month names and days, not real dates. The crosshair label shows the date in the window (30 Apr), and the legend shows each year's value at that position.

The view owns the chart's time axis, so give it a chart of its own: one without a loaded symbol (setSymbol) or main series data. createSeasonality throws otherwise.

Data ​

symbol loads daily bars (1d) through the chart's data source, or through source when you pass one. The view pages back until history reaches 1 January of the oldest year it needs, and live bars update the newest year. ready settles once the first load finishes, and rejects with the ChartError when it fails.

Bars you already have go in bars instead, and setBars replaces them later:

ts
const view = createSeasonality(chart, { bars: dailyBars, years: [2019, 2021, 2023] });

years is either a count (the newest years with data in the window) or a list of years. Years without data are left out.

Windows ​

window: 'year' compares whole calendar years. 'quarter' and 'month' compare one quarter or month each year: period picks it (1-4 or 1-12) and defaults to the one holding the newest bar.

ts
// Every July, by trading day.
createSeasonality(chart, { symbol: 'IND50', window: 'month', period: 7, align: 'trading' });

Alignment, leap days and gaps ​

align: 'calendar' (the default) puts the same date at the same position every year. Positions follow a leap-year calendar, so a year window has 366 of them and 1 March is position 60 in every year. 29 February only has a value in leap years.

Days without a bar (weekends, holidays) are handled by gaps:

  • 'carry' (default): repeat the last close over them, from the year's first bar to its last. Common years carry 28 February over the leap day. Every year then has a value at every position it spans, so the average path is not skewed by weekends falling on different dates.
  • 'skip': leave them empty. Lines join across the gap, and the average at that position uses only the years with a bar there.

align: 'trading' lines up the nth bar of the window instead. Positions are trading days, the axis and crosshair show their numbers (Day 42), and there are no gaps. A year with fewer trading days ends earlier.

Neither mode fills before a year's first bar in the window or after its last, so the current year ends at the newest bar.

Average and range ​

average: 'mean' or 'median' adds the average path of the years (dashed), and shades the min/max range of the years behind the lines. At each position the average uses the years with a value there. range: false keeps the average without the shading, and averageColor sets their colour (the theme's line by default).

Showing and hiding years ​

Each year is a line series titled with the year, so the legend's eye button hides it. setYearVisible(year, visible) does the same from code. Hiding a year only changes that line: the other years and the average are not recomputed, and the average still includes the hidden year. A hidden year stays hidden when the view recomputes for new bars or options.

ts
view.setYearVisible(2020, false);
view.isYearVisible(2020); // false
view.yearSeries(2024)?.applyOptions({ lineWidth: 3 });

The newest year is drawn thicker and has the last-value label. colors sets the year colours, oldest year first.

Changing and removing the view ​

applyOptions changes any option except the data and recomputes every path. dispose removes the view's series and restores the chart's time labels.

ts
view.applyOptions({ window: 'quarter', normalize: 'indexed' });
view.dispose();

Without a chart ​

computeSeasonality(bars, options, zone?) is the pure computation behind the view: the normalised path per year as a Float64Array per position (NaN where a year has no value), and the average with its min and max. seasonalPosition(time, window, zone?) gives a date's year, period and calendar position, and seasonalSlots(window, period) the number of calendar positions in a window. Dates are read in zone (UTC by default; the view passes the chart's timezone).