Spiral Time Series Chart — Apache ECharts

A time series plotted along a spiral (Archimedean or logarithmic), where each full revolution represents one cycle period (e.g., one year). By wrapping temporal data around a spiral, corresponding periods from different cycles align vertically, making recurring seasonal or periodic patterns immediately visible. This layout provides a compact alternative to long horizontal time series for cyclic data, revealing periodicity and trend simultaneously.

Spiral Time Series Chart rendered with Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// spiral-timeseries: Spiral Time Series Chart
// Library: echarts 6.1.0 | JavaScript 22.23.2
// Quality: 88/100 | Created: 2026-09-09
//# anyplot-orientation: square

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic) ----------------------------------------
// Daily rooftop solar-panel energy output over 3 years. Each full spiral
// revolution is one year, so the same calendar day in different years lines
// up radially.
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const cyclesCount = 3;
const daysPerCycle = 365;
const baseOutput = 28; // kWh/day
const seasonalAmplitude = 16; // summer peak vs. winter trough
const yearlyGrowth = 1.2; // added panel capacity per year
const noiseAmplitude = 3;

// mulberry32: tiny deterministic PRNG (avoids float-precision issues that a
// classic LCG hits in JS once seed * multiplier exceeds 2^53).
let seed = 42;
const nextRandom = () => {
  seed = (seed + 0x6d2b79f5) | 0;
  let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;
  return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
};

const spiralData = [];
for (let i = 0; i < cyclesCount * daysPerCycle; i += 1) {
  const cycleIndex = Math.floor(i / daysPerCycle);
  const dayOfYear = i % daysPerCycle;
  const cycleFraction = dayOfYear / daysPerCycle;
  const angleDeg = cycleFraction * 360; // position within the current cycle
  const radius = cycleIndex + cycleFraction; // grows continuously -> Archimedean spiral
  const seasonal = Math.cos((2 * Math.PI * (dayOfYear - 172)) / daysPerCycle); // peaks ~Jun 21
  const noise = nextRandom() * 2 - 1;
  const output = baseOutput + yearlyGrowth * cycleIndex + seasonalAmplitude * seasonal + noiseAmplitude * noise;
  spiralData.push([radius, angleDeg, output]);
}
const outputValues = spiralData.map((d) => d[2]);
const valueMin = Math.min(...outputValues);
const valueMax = Math.max(...outputValues);

// ECharts' visualMap does not recolor a polar-coordinate line series (it only
// drives itemStyle on symbols/points, which are hidden here). To make the
// color-encodes-value requirement actually render, split the spiral into one
// tiny line segment per consecutive pair of points and color each segment
// explicitly by interpolating the Imprint sequential gradient (t.seq).
const seqStart = hexToRgb(t.seq[0]);
const seqEnd = hexToRgb(t.seq[1]);
function hexToRgb(hex) {
  const clean = hex.replace("#", "");
  return [parseInt(clean.slice(0, 2), 16), parseInt(clean.slice(2, 4), 16), parseInt(clean.slice(4, 6), 16)];
}
function lerpColor(a, b, ratio) {
  const r = Math.round(a[0] + (b[0] - a[0]) * ratio);
  const g = Math.round(a[1] + (b[1] - a[1]) * ratio);
  const bl = Math.round(a[2] + (b[2] - a[2]) * ratio);
  return `rgb(${r}, ${g}, ${bl})`;
}

const spiralSegments = [];
for (let i = 0; i < spiralData.length - 1; i += 1) {
  const [r1, a1, v1] = spiralData[i];
  const [r2, a2, v2] = spiralData[i + 1];
  const ratio = ((v1 + v2) / 2 - valueMin) / (valueMax - valueMin);
  spiralSegments.push({
    type: "line",
    coordinateSystem: "polar",
    data: [
      [r1, a1],
      [r2, a2],
    ],
    encode: { radius: 0, angle: 1 },
    showSymbol: false,
    smooth: false,
    silent: true,
    lineStyle: { width: 3.5, color: lerpColor(seqStart, seqEnd, ratio) },
  });
}

// --- Init --------------------------------------------------------------------
const chart = echarts.init(document.getElementById("container"));

// --- Option --------------------------------------------------------------------
chart.setOption({
  animation: false,
  backgroundColor: "transparent",
  title: {
    text: "spiral-timeseries · javascript · echarts · anyplot.ai",
    subtext: "Daily rooftop solar-panel output (kWh) · each revolution = 1 year",
    left: "center",
    top: 24,
    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },
    subtextStyle: { color: t.inkSoft, fontSize: 15 },
  },
  polar: {
    center: ["50%", "56%"],
    radius: "70%",
  },
  angleAxis: {
    type: "value",
    min: 0,
    max: 360,
    interval: 30,
    startAngle: 90,
    clockwise: true,
    axisLabel: {
      color: t.inkSoft,
      fontSize: 14,
      formatter: (value) => monthNames[Math.round(value / 30) % 12],
    },
    axisLine: { lineStyle: { color: t.inkSoft } },
    axisTick: { show: false },
    splitLine: { show: true, lineStyle: { color: t.grid } },
  },
  radiusAxis: {
    type: "value",
    min: 0,
    max: cyclesCount,
    interval: 1,
    axisLabel: {
      color: t.inkSoft,
      fontSize: 14,
      formatter: (value) => (value < cyclesCount ? `Year ${Math.round(value) + 1}` : ""),
      backgroundColor: t.pageBg,
      padding: [2, 4],
    },
    axisLine: { lineStyle: { color: t.inkSoft } },
    axisTick: { show: false },
    splitLine: { show: true, lineStyle: { color: t.grid } },
  },
  series: spiralSegments,
  visualMap: {
    type: "continuous",
    seriesIndex: [],
    min: valueMin,
    max: valueMax,
    orient: "vertical",
    right: 16,
    top: "middle",
    itemWidth: 20,
    itemHeight: 220,
    text: ["High", "Low"],
    inRange: { color: t.seq },
    textStyle: { color: t.ink, fontSize: 14 },
  },
});

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/spiral-timeseries/echarts/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "spiral-timeseries",
  "language": "javascript",
  "library": "echarts",
  "page": "https://anyplot.ai/spiral-timeseries/javascript/echarts",
  "hub": "https://anyplot.ai/spiral-timeseries",
  "code_json": "https://api.anyplot.ai/specs/spiral-timeseries/echarts/code",
  "spec_json": "https://api.anyplot.ai/specs/spiral-timeseries",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/spiral-timeseries/javascript/echarts/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/spiral-timeseries/javascript/echarts/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/spiral-timeseries/javascript/echarts/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/spiral-timeseries/javascript/echarts/plot-dark.html",
  "quality_score": 88.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Spiral Time Series Chart on anyplot.ai.

Other implementations