{"spec_id":"spiral-timeseries","library":"echarts","language":"javascript","code":"// anyplot.ai\n// spiral-timeseries: Spiral Time Series Chart\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-09\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Daily rooftop solar-panel energy output over 3 years. Each full spiral\n// revolution is one year, so the same calendar day in different years lines\n// up radially.\nconst monthNames = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\nconst cyclesCount = 3;\nconst daysPerCycle = 365;\nconst baseOutput = 28; // kWh/day\nconst seasonalAmplitude = 16; // summer peak vs. winter trough\nconst yearlyGrowth = 1.2; // added panel capacity per year\nconst noiseAmplitude = 3;\n\n// mulberry32: tiny deterministic PRNG (avoids float-precision issues that a\n// classic LCG hits in JS once seed * multiplier exceeds 2^53).\nlet seed = 42;\nconst nextRandom = () => {\n  seed = (seed + 0x6d2b79f5) | 0;\n  let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n  x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;\n  return ((x ^ (x >>> 14)) >>> 0) / 4294967296;\n};\n\nconst spiralData = [];\nfor (let i = 0; i < cyclesCount * daysPerCycle; i += 1) {\n  const cycleIndex = Math.floor(i / daysPerCycle);\n  const dayOfYear = i % daysPerCycle;\n  const cycleFraction = dayOfYear / daysPerCycle;\n  const angleDeg = cycleFraction * 360; // position within the current cycle\n  const radius = cycleIndex + cycleFraction; // grows continuously -> Archimedean spiral\n  const seasonal = Math.cos((2 * Math.PI * (dayOfYear - 172)) / daysPerCycle); // peaks ~Jun 21\n  const noise = nextRandom() * 2 - 1;\n  const output = baseOutput + yearlyGrowth * cycleIndex + seasonalAmplitude * seasonal + noiseAmplitude * noise;\n  spiralData.push([radius, angleDeg, output]);\n}\nconst outputValues = spiralData.map((d) => d[2]);\nconst valueMin = Math.min(...outputValues);\nconst valueMax = Math.max(...outputValues);\n\n// ECharts' visualMap does not recolor a polar-coordinate line series (it only\n// drives itemStyle on symbols/points, which are hidden here). To make the\n// color-encodes-value requirement actually render, split the spiral into one\n// tiny line segment per consecutive pair of points and color each segment\n// explicitly by interpolating the Imprint sequential gradient (t.seq).\nconst seqStart = hexToRgb(t.seq[0]);\nconst seqEnd = hexToRgb(t.seq[1]);\nfunction hexToRgb(hex) {\n  const clean = hex.replace(\"#\", \"\");\n  return [parseInt(clean.slice(0, 2), 16), parseInt(clean.slice(2, 4), 16), parseInt(clean.slice(4, 6), 16)];\n}\nfunction lerpColor(a, b, ratio) {\n  const r = Math.round(a[0] + (b[0] - a[0]) * ratio);\n  const g = Math.round(a[1] + (b[1] - a[1]) * ratio);\n  const bl = Math.round(a[2] + (b[2] - a[2]) * ratio);\n  return `rgb(${r}, ${g}, ${bl})`;\n}\n\nconst spiralSegments = [];\nfor (let i = 0; i < spiralData.length - 1; i += 1) {\n  const [r1, a1, v1] = spiralData[i];\n  const [r2, a2, v2] = spiralData[i + 1];\n  const ratio = ((v1 + v2) / 2 - valueMin) / (valueMax - valueMin);\n  spiralSegments.push({\n    type: \"line\",\n    coordinateSystem: \"polar\",\n    data: [\n      [r1, a1],\n      [r2, a2],\n    ],\n    encode: { radius: 0, angle: 1 },\n    showSymbol: false,\n    smooth: false,\n    silent: true,\n    lineStyle: { width: 3.5, color: lerpColor(seqStart, seqEnd, ratio) },\n  });\n}\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"spiral-timeseries · javascript · echarts · anyplot.ai\",\n    subtext: \"Daily rooftop solar-panel output (kWh) · each revolution = 1 year\",\n    left: \"center\",\n    top: 24,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n    subtextStyle: { color: t.inkSoft, fontSize: 15 },\n  },\n  polar: {\n    center: [\"50%\", \"56%\"],\n    radius: \"70%\",\n  },\n  angleAxis: {\n    type: \"value\",\n    min: 0,\n    max: 360,\n    interval: 30,\n    startAngle: 90,\n    clockwise: true,\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 14,\n      formatter: (value) => monthNames[Math.round(value / 30) % 12],\n    },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: true, lineStyle: { color: t.grid } },\n  },\n  radiusAxis: {\n    type: \"value\",\n    min: 0,\n    max: cyclesCount,\n    interval: 1,\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 14,\n      formatter: (value) => (value < cyclesCount ? `Year ${Math.round(value) + 1}` : \"\"),\n      backgroundColor: t.pageBg,\n      padding: [2, 4],\n    },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: true, lineStyle: { color: t.grid } },\n  },\n  series: spiralSegments,\n  visualMap: {\n    type: \"continuous\",\n    seriesIndex: [],\n    min: valueMin,\n    max: valueMax,\n    orient: \"vertical\",\n    right: 16,\n    top: \"middle\",\n    itemWidth: 20,\n    itemHeight: 220,\n    text: [\"High\", \"Low\"],\n    inRange: { color: t.seq },\n    textStyle: { color: t.ink, fontSize: 14 },\n  },\n});\n"}