Cycle Plot (Seasonal Subseries) — Highcharts

A cycle plot, also known as a seasonal subseries plot (introduced by William Cleveland), separates the seasonal pattern of a time series from the trend within each season. The series is split by seasonal period (e.g. one group per month or weekday); within each group the values are drawn in chronological order as a small line, and a horizontal reference line marks that group's mean. Comparing the mean lines across groups reveals the seasonal effect, while the slope of each subseries reveals the trend within that season — two patterns that are entangled in a standard time series plot.

Cycle Plot (Seasonal Subseries) rendered with Highcharts

JavaScript source (Highcharts)

// anyplot.ai
// line-cycle-seasonal: Cycle Plot (Seasonal Subseries)
// Library: highcharts 12.6.0 | JavaScript 22.22.3
// Quality: 90/100 | Created: 2026-06-15

//# anyplot-orientation: landscape
const t = window.ANYPLOT_TOKENS;

// --- Data (deterministic) ---
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const N_YEARS = 20;
const START_YEAR = 2004;

// Base monthly mean temperatures (°C) for a temperate Northern European city
const BASE_TEMPS = [-1.5, -0.5, 3.5, 8.5, 14.0, 18.5, 20.5, 19.5, 14.5, 8.5, 3.0, 0.0];
const WARMING_RATE = 0.065; // °C per year warming trend

// Deterministic pseudo-noise via sin-based hash (±1.4 °C amplitude)
function fakeNoise(seed) {
    const x = Math.sin(seed * 127.1 + 311.7) * 43758.5453;
    return (x - Math.floor(x) - 0.5) * 2.8;
}

// temps[m][y] = temperature for month m, year y offset from START_YEAR
const temps = BASE_TEMPS.map((base, m) =>
    Array.from({ length: N_YEARS }, (_, y) =>
        parseFloat((base + WARMING_RATE * y + fakeNoise(m * 23 + y)).toFixed(1))
    )
);

// Monthly means across all years
const means = temps.map(ys =>
    parseFloat((ys.reduce((a, b) => a + b, 0) / ys.length).toFixed(2))
);

// --- Layout constants ---
const GAP = 1;                  // gap units between month groups on x-axis
const GROUP_W = N_YEARS + GAP;  // 21 x-units per month group

// --- Colors ---
const TREND_COLOR = t.palette[0]; // #009E73 — brand green, first categorical series
const MEAN_COLOR  = t.palette[2]; // #4467A3 — blue for mean reference lines

// Theme-adaptive band color for alternating group separators
const BAND_COLOR = window.ANYPLOT_THEME === 'dark'
    ? 'rgba(255,255,255,0.05)'
    : 'rgba(0,0,0,0.04)';

// --- Series ---
// 12 subseries: one connected trend line per month, plotted chronologically
const trendSeries = MONTHS.map((name, m) => ({
    type: 'line',
    name: 'Year-to-year trend',
    color: TREND_COLOR,
    lineWidth: 1.5,
    opacity: 0.85,
    marker: { radius: 4, symbol: 'circle', enabled: true },
    data: temps[m].map((val, y) => ({ x: m * GROUP_W + y, y: val })),
    showInLegend: m === 0,
    zIndex: 1,
}));

// 12 horizontal mean reference lines: one per month, spanning the group's x-range
const meanSeries = MONTHS.map((name, m) => ({
    type: 'line',
    name: 'Monthly mean',
    color: MEAN_COLOR,
    lineWidth: 3,
    marker: { enabled: false },
    data: [
        { x: m * GROUP_W,               y: means[m] },
        { x: m * GROUP_W + N_YEARS - 1, y: means[m] },
    ],
    showInLegend: m === 0,
    enableMouseTracking: false,
    zIndex: 2,
}));

// x-axis tick positions at the midpoint of each month group
const tickPositions = MONTHS.map((_, m) => m * GROUP_W + (N_YEARS - 1) / 2);

// Alternating background bands to visually separate month groups
const plotBands = MONTHS.map((_, m) => ({
    from:  m * GROUP_W - 0.5,
    to:    m * GROUP_W + N_YEARS - 0.5,
    color: m % 2 === 0 ? BAND_COLOR : 'transparent',
}));

// Total warming to annotate
const totalWarming = (WARMING_RATE * (N_YEARS - 1)).toFixed(1);

// --- Chart ---
const chart = Highcharts.chart('container', {
    chart: {
        type: 'line',
        backgroundColor: 'transparent',
        animation: false,
        style: { fontFamily: 'inherit' },
    },
    credits: { enabled: false },
    colors: t.palette,

    title: {
        text: 'line-cycle-seasonal · javascript · highcharts · anyplot.ai',
        style: { color: t.ink, fontSize: '22px', fontWeight: '600' },
    },
    subtitle: {
        text: 'Average monthly temperature by season, ' + START_YEAR + '–' + (START_YEAR + N_YEARS - 1) + ' — Northern European city (°C)',
        style: { color: t.inkSoft, fontSize: '14px' },
    },

    xAxis: {
        tickPositions,
        labels: {
            formatter: function () {
                const m = Math.round(this.value / GROUP_W);
                return MONTHS[Math.min(m, 11)];
            },
            style: { color: t.inkSoft, fontSize: '14px' },
        },
        lineColor: t.inkSoft,
        tickColor: 'transparent',
        gridLineColor: 'transparent',
        plotBands,
    },

    yAxis: {
        title: {
            text: 'Temperature (°C)',
            style: { color: t.inkSoft, fontSize: '16px' },
        },
        gridLineColor: t.grid,
        labels: { style: { color: t.inkSoft, fontSize: '14px' } },
        lineColor: t.inkSoft,
        tickColor: t.inkSoft,
    },

    legend: {
        enabled: true,
        itemStyle: { color: t.inkSoft, fontSize: '14px' },
        itemHoverStyle: { color: t.ink },
    },

    tooltip: { enabled: false },

    plotOptions: {
        series: { animation: false },
        line: { connectNulls: false },
    },

    series: [...trendSeries, ...meanSeries],
});

// Annotation: callout on July group (hottest month) highlighting the warming trend
const JUL = 6; // July index (0-based)
const annDataX = JUL * GROUP_W + (N_YEARS - 1) / 2; // midpoint of July group
const annPx  = chart.xAxis[0].toPixels(annDataX, false);
const annPy  = chart.plotTop + 18; // near the top of the plot area

chart.renderer.text(
    `↑ +${totalWarming} °C warming (Jul, 20 yrs)`,
    annPx,
    annPy
).css({
    color: TREND_COLOR,
    fontSize: '11px',
    fontWeight: '700',
}).attr({
    align: 'center',
    zIndex: 6,
}).add();

Part of Cycle Plot (Seasonal Subseries) on anyplot.ai.

Other implementations