{"spec_id":"spiral-timeseries","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// spiral-timeseries: Spiral Time Series Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\n// Website page views sampled every 2 hours over 8 weekly cycles. One full\n// spiral revolution = one week, so weekday/weekend and time-of-day patterns\n// line up radially across cycles.\nlet seed = 42;\nfunction random() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst POINTS_PER_DAY = 12; // every 2 hours\nconst DAYS_PER_CYCLE = 7; // one revolution = one week\nconst NUM_CYCLES = 8;\nconst POINTS_PER_CYCLE = POINTS_PER_DAY * DAYS_PER_CYCLE;\nconst TOTAL_POINTS = POINTS_PER_CYCLE * NUM_CYCLES;\nconst DAY_NAMES = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\n\nconst pageViews = [];\nfor (let i = 0; i < TOTAL_POINTS; i++) {\n  const hourOfDay = (i % POINTS_PER_DAY) * (24 / POINTS_PER_DAY);\n  const dayOfWeek = Math.floor(i / POINTS_PER_DAY) % DAYS_PER_CYCLE;\n  const weekIndex = Math.floor(i / POINTS_PER_CYCLE);\n\n  const dailyPattern = 45 * Math.exp(-((hourOfDay - 14) ** 2) / 40); // midday/afternoon peak\n  const weekendDip = dayOfWeek >= 5 ? -22 : 14; // lower traffic on Sat/Sun\n  const growthTrend = weekIndex * 3.5; // gradual week-over-week growth\n  const noise = (random() - 0.5) * 12;\n\n  pageViews.push(Math.max(5, 60 + dailyPattern + weekendDip + growthTrend + noise));\n}\n\nconst minValue = Math.min(...pageViews);\nconst maxValue = Math.max(...pageViews);\n\n// --- Spiral geometry (Archimedean: radius grows linearly with angle) -------\n// Earliest data sits at the center; each full revolution advances one week.\nconst RADIUS_MAX = 10;\nconst AXIS_EXTENT = 11.5;\n\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nfunction mixColor(hexA, hexB, ratio) {\n  const [r1, g1, b1] = hexToRgb(hexA);\n  const [r2, g2, b2] = hexToRgb(hexB);\n  const r = Math.round(r1 + (r2 - r1) * ratio);\n  const g = Math.round(g1 + (g2 - g1) * ratio);\n  const b = Math.round(b1 + (b2 - b1) * ratio);\n  return `rgb(${r},${g},${b})`;\n}\n\nconst spiralPoints = [];\nconst structurePath = [];\nfor (let i = 0; i < TOTAL_POINTS; i++) {\n  const theta = (2 * Math.PI * i) / POINTS_PER_CYCLE;\n  const radius = (RADIUS_MAX * i) / (TOTAL_POINTS - 1);\n  const screenAngle = Math.PI / 2 - theta; // start at 12 o'clock, sweep clockwise\n  const x = radius * Math.cos(screenAngle);\n  const y = radius * Math.sin(screenAngle);\n  const value = pageViews[i];\n  const dayOfWeek = Math.floor(i / POINTS_PER_DAY) % DAYS_PER_CYCLE;\n  const weekIndex = Math.floor(i / POINTS_PER_CYCLE);\n  const hourOfDay = (i % POINTS_PER_DAY) * (24 / POINTS_PER_DAY);\n\n  const point = {\n    x,\n    y,\n    value,\n    dayOfWeek,\n    weekIndex,\n    hourOfDay,\n    marker: { fillColor: mixColor(t.seq[0], t.seq[1], (value - minValue) / (maxValue - minValue)) },\n  };\n  // Label the start of each cycle (top spoke, where every week begins).\n  if (i % POINTS_PER_CYCLE === 0) {\n    point.name = `Week ${weekIndex + 1}`;\n    point.dataLabels = {\n      enabled: true,\n      format: \"{point.name}\",\n      align: \"right\",\n      x: -14,\n      y: 2,\n      style: { color: t.inkSoft, fontSize: \"14px\", fontWeight: \"600\", textOutline: \"none\" },\n    };\n  }\n  spiralPoints.push(point);\n  structurePath.push([x, y]);\n}\n\n// Radial grid lines — one spoke per day-of-week subdivision within a cycle.\nconst spokeSeries = DAY_NAMES.map((_, dayIndex) => {\n  const screenAngle = Math.PI / 2 - (2 * Math.PI * dayIndex) / DAYS_PER_CYCLE;\n  return {\n    type: \"line\",\n    data: [\n      [0, 0],\n      [RADIUS_MAX * Math.cos(screenAngle), RADIUS_MAX * Math.sin(screenAngle)],\n    ],\n    color: t.grid,\n    lineWidth: 1,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n  };\n});\n\n// Concentric rings mark each completed cycle (one full revolution = one week).\nconst ringSeries = Array.from({ length: NUM_CYCLES }, (_, cycleIndex) => {\n  const radius = (RADIUS_MAX * (cycleIndex + 1)) / NUM_CYCLES;\n  const segments = 96;\n  const data = Array.from({ length: segments + 1 }, (_, s) => {\n    const a = (2 * Math.PI * s) / segments;\n    return [radius * Math.cos(a), radius * Math.sin(a)];\n  });\n  return {\n    type: \"line\",\n    data,\n    color: t.grid,\n    lineWidth: 1,\n    dashStyle: \"Dot\",\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n  };\n});\n\n// --- Chart -------------------------------------------------------------------\nconst chart = Highcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    margin: [90, 90, 90, 90],\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"spiral-timeseries · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Website page views · every 2 hours · 8 weekly cycles\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: { min: -AXIS_EXTENT, max: AXIS_EXTENT, visible: false, startOnTick: false, endOnTick: false },\n  yAxis: {\n    min: -AXIS_EXTENT,\n    max: AXIS_EXTENT,\n    visible: false,\n    startOnTick: false,\n    endOnTick: false,\n    title: { text: null },\n  },\n  legend: { enabled: false },\n  tooltip: {\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    style: { color: t.ink, fontSize: \"13px\" },\n    formatter: function () {\n      const p = this.point;\n      return (\n        `Week ${p.weekIndex + 1}, ${DAY_NAMES[p.dayOfWeek]} ${String(p.hourOfDay).padStart(2, \"0\")}:00<br/>` +\n        `<b>${Math.round(p.value)}</b> page views`\n      );\n    },\n  },\n  plotOptions: {\n    series: { animation: false },\n    line: { enableMouseTracking: false },\n  },\n  series: [\n    ...ringSeries,\n    ...spokeSeries,\n    {\n      name: \"Spiral path\",\n      type: \"line\",\n      data: structurePath,\n      color: t.inkSoft,\n      opacity: 0.35,\n      lineWidth: 2,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      name: \"Page views\",\n      type: \"scatter\",\n      data: spiralPoints,\n      marker: { radius: 4, symbol: \"circle\", lineWidth: 0 },\n      showInLegend: false,\n    },\n  ],\n});\n\n// Manual color bar — the core Highcharts bundle has no colorAxis/heatmap\n// module, so the Imprint sequential gradient is drawn directly with the SVG\n// renderer instead of relying on a colorAxis legend.\nconst barWidth = 220;\nconst barHeight = 16;\nconst barX = chart.chartWidth - barWidth - 50;\nconst barY = chart.chartHeight - 68;\n\nchart.renderer\n  .text(\"Page views\", barX, barY - 10)\n  .css({ color: t.inkSoft, fontSize: \"13px\" })\n  .add();\nchart.renderer\n  .rect(barX, barY, barWidth, barHeight, 0)\n  .attr({\n    fill: {\n      linearGradient: { x1: 0, y1: 0, x2: 1, y2: 0 },\n      stops: [\n        [0, t.seq[0]],\n        [1, t.seq[1]],\n      ],\n    },\n    stroke: t.inkSoft,\n    \"stroke-width\": 1,\n  })\n  .add();\nchart.renderer\n  .text(`${Math.round(minValue)}`, barX, barY + barHeight + 20)\n  .css({ color: t.inkSoft, fontSize: \"12px\" })\n  .add();\nchart.renderer\n  .text(`${Math.round(maxValue)}`, barX + barWidth - 16, barY + barHeight + 20)\n  .css({ color: t.inkSoft, fontSize: \"12px\" })\n  .add();\n"}