{"spec_id":"heatmap-polar","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// heatmap-polar: Polar Heatmap for Cyclic Two-Dimensional Data\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: simulated hourly website visits, angular = hour of day, radial = --\n// day of week. Deterministic LCG so light/dark renders match exactly.\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];\nconst N_RADIAL = DAYS.length;\nconst N_ANGULAR = 24;\n\n// Weekdays show the commute bimodal (~9am, ~7pm); weekends shift to a single\n// broad, later, generally busier afternoon/evening peak.\nfunction baseline(day, hour) {\n  const isWeekend = day >= 5;\n  if (!isWeekend) {\n    const morning = 85 * Math.exp(-((hour - 9) ** 2) / (2 * 2.2 ** 2));\n    const evening = 100 * Math.exp(-((hour - 19) ** 2) / (2 * 2.6 ** 2));\n    return 18 + morning + evening;\n  }\n  const afternoon = 120 * Math.exp(-((hour - 15.5) ** 2) / (2 * 4 ** 2));\n  return 30 + afternoon;\n}\n\nconst matrix = [];\nfor (let day = 0; day < N_RADIAL; day++) {\n  const row = [];\n  for (let hour = 0; hour < N_ANGULAR; hour++) {\n    const noise = (rand() - 0.5) * 14;\n    row.push(Math.max(4, Math.round(baseline(day, hour) + noise)));\n  }\n  matrix.push(row);\n}\n\nlet VAL_MIN = Infinity;\nlet VAL_MAX = -Infinity;\nmatrix.forEach((row) =>\n  row.forEach((v) => {\n    if (v < VAL_MIN) VAL_MIN = v;\n    if (v > VAL_MAX) VAL_MAX = v;\n  })\n);\n\n// --- Color: imprint_seq — visit counts are single-polarity ------------------\nfunction hexToRgb(hex) {\n  return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];\n}\nfunction lerp(a, b, f) {\n  return a + (b - a) * f;\n}\nfunction rgbToCss([r, g, b]) {\n  return `rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)})`;\n}\nconst SEQ_LO = hexToRgb(t.seq[0]);\nconst SEQ_HI = hexToRgb(t.seq[1]);\nfunction valueFill(v) {\n  const frac = (v - VAL_MIN) / (VAL_MAX - VAL_MIN);\n  return rgbToCss([lerp(SEQ_LO[0], SEQ_HI[0], frac), lerp(SEQ_LO[1], SEQ_HI[1], frac), lerp(SEQ_LO[2], SEQ_HI[2], frac)]);\n}\n\n// --- Title (scaled off the 67-char baseline; this title sits well under it) -\nconst TITLE_TEXT = 'heatmap-polar · javascript · highcharts · anyplot.ai';\nconst TITLE_FS = Math.max(Math.round(22 * Math.min(1, 67 / TITLE_TEXT.length)), 14);\n\n// --- Fixed chart geometry (square canvas, harness-guaranteed 1200x1200) -----\n// Right margin holds the colorbar; the wheel itself sits inside plotLeft/Top\n// + plotWidth/Height, which we compute directly rather than trusting an\n// auto-margin (title/subtitle height is already folded into the top value).\nconst SIZE = window.ANYPLOT_SIZE;\nconst CHART_MARGIN = [120, 210, 70, 40]; // [top, right, bottom, left]\nconst plotLeft = CHART_MARGIN[3];\nconst plotTop = CHART_MARGIN[0];\nconst plotWidth = SIZE.width - CHART_MARGIN[1] - CHART_MARGIN[3];\nconst plotHeight = SIZE.height - CHART_MARGIN[0] - CHART_MARGIN[2];\nconst cx = plotLeft + plotWidth / 2;\nconst cy = plotTop + plotHeight / 2;\nconst OUTER_R = Math.min(plotWidth, plotHeight) / 2 - 46; // clearance for hour labels\nconst INNER_R = OUTER_R * 0.16; // small hub hole avoids degenerate center wedges\nconst RING_THICKNESS = (OUTER_R - INNER_R) / N_RADIAL;\nconst ANGLE_STEP = (2 * Math.PI) / N_ANGULAR;\nconst ANGLE0 = -Math.PI / 2; // hour 0 (12am) at the top; angle grows clockwise\nfunction hourAngle(hour) {\n  return ANGLE0 + hour * ANGLE_STEP;\n}\nconst CELL_MARKER_R = Math.max(Math.min(RING_THICKNESS, OUTER_R * ANGLE_STEP) / 2 - 1, 3);\n\n// --- Draw: wedge cells + hour/day axis labels + colorbar, all via the core --\n// renderer — the loaded bundle has neither the polar-chart module\n// (highcharts-more) nor the heatmap module, so the wheel itself is hand-drawn\n// while a matched invisible scatter layer (below) recovers native tooltips.\nconst drawn = [];\nfunction clearDrawn() {\n  drawn.forEach((el) => {\n    try {\n      el.destroy();\n    } catch (_err) {\n      // already removed\n    }\n  });\n  drawn.length = 0;\n}\n\nfunction drawAll() {\n  const chart = this;\n  clearDrawn();\n  const r = chart.renderer;\n\n  // Heatmap wedges: rings outward = day of week, sectors clockwise = hour.\n  for (let day = 0; day < N_RADIAL; day++) {\n    const r0 = INNER_R + day * RING_THICKNESS;\n    const r1 = r0 + RING_THICKNESS;\n    for (let hour = 0; hour < N_ANGULAR; hour++) {\n      drawn.push(\n        r\n          .arc(cx, cy, r1 - 0.5, r0 + 0.5, hourAngle(hour), hourAngle(hour + 1))\n          .attr({ fill: valueFill(matrix[day][hour]), stroke: t.pageBg, 'stroke-width': 1 })\n          .add()\n      );\n    }\n  }\n\n  // Angular axis: hour-of-day labels at readable intervals, outside the wheel.\n  [\n    [0, '12am'],\n    [6, '6am'],\n    [12, '12pm'],\n    [18, '6pm'],\n  ].forEach(([hour, label]) => {\n    const a = hourAngle(hour);\n    drawn.push(\n      r\n        .text(label, cx + (OUTER_R + 22) * Math.cos(a), cy + (OUTER_R + 22) * Math.sin(a) + 5)\n        .attr({ align: 'center' })\n        .css({ color: t.inkSoft, fontSize: '14px' })\n        .add()\n    );\n  });\n\n  // Radial axis: day-of-week ring labels along the 12am spoke, each on a\n  // soft halo so they stay legible over the wedge color beneath them.\n  DAYS.forEach((day, i) => {\n    const ly = cy - (INNER_R + (i + 0.5) * RING_THICKNESS);\n    drawn.push(r.rect(cx - 16, ly - 9, 32, 18, 4).attr({ fill: t.elevatedBg, opacity: 0.88 }).add());\n    drawn.push(\n      r\n        .text(day, cx, ly + 4)\n        .attr({ align: 'center' })\n        .css({ color: t.ink, fontSize: '12px', fontWeight: '600' })\n        .add()\n    );\n  });\n\n  // Sequential colorbar.\n  const barLeft = plotLeft + plotWidth + 46;\n  const barTop = cy - OUTER_R;\n  const barWidth = 24;\n  const barHeight = OUTER_R * 2;\n  const segments = 60;\n  const segH = barHeight / segments;\n  for (let i = 0; i < segments; i++) {\n    const value = VAL_MIN + ((segments - 1 - i) / (segments - 1)) * (VAL_MAX - VAL_MIN);\n    drawn.push(r.rect(barLeft, barTop + i * segH, barWidth, segH + 0.5).attr({ fill: valueFill(value) }).add());\n  }\n  drawn.push(r.rect(barLeft, barTop, barWidth, barHeight).attr({ fill: 'none', stroke: t.inkSoft, 'stroke-width': 1 }).add());\n  [\n    [VAL_MAX, 0],\n    [VAL_MIN, 1],\n  ].forEach(([value, frac]) => {\n    drawn.push(\n      r\n        .text(Math.round(value).toString(), barLeft + barWidth + 10, barTop + frac * barHeight + 5)\n        .attr({ align: 'left' })\n        .css({ color: t.inkSoft, fontSize: '13px' })\n        .add()\n    );\n  });\n  drawn.push(\n    r\n      .text('Visits / hr', barLeft, barTop - 14)\n      .attr({ align: 'left' })\n      .css({ color: t.inkSoft, fontSize: '14px', fontWeight: '500' })\n      .add()\n  );\n}\n\n// Invisible scatter layer aligned to each wedge centroid so hovering exposes\n// a native Highcharts tooltip over the hand-drawn wheel.\nconst cellPoints = [];\nfor (let day = 0; day < N_RADIAL; day++) {\n  const rMid = INNER_R + (day + 0.5) * RING_THICKNESS;\n  for (let hour = 0; hour < N_ANGULAR; hour++) {\n    const aMid = hourAngle(hour + 0.5);\n    cellPoints.push({\n      x: rMid * Math.cos(aMid),\n      y: rMid * Math.sin(aMid),\n      value: matrix[day][hour],\n      day: DAYS[day],\n      hour,\n    });\n  }\n}\n\nHighcharts.chart('container', {\n  chart: {\n    backgroundColor: 'transparent',\n    animation: false,\n    style: { fontFamily: 'inherit' },\n    margin: CHART_MARGIN,\n    events: { load: drawAll, redraw: drawAll },\n  },\n  credits: { enabled: false },\n  title: { text: TITLE_TEXT, style: { color: t.ink, fontSize: TITLE_FS + 'px', fontWeight: '600' } },\n  subtitle: {\n    text: 'Simulated hourly visits · Mon (inner ring) to Sun (outer ring)',\n    style: { color: t.inkSoft, fontSize: '14px' },\n  },\n  xAxis: { visible: false, min: -plotWidth / 2, max: plotWidth / 2 },\n  yAxis: { visible: false, min: -plotHeight / 2, max: plotHeight / 2, reversed: true },\n  legend: { enabled: false },\n  tooltip: {\n    enabled: true,\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    borderRadius: 6,\n    style: { color: t.ink, fontSize: '13px' },\n    formatter: function () {\n      const p = this.point;\n      const hourLabel = `${p.hour % 12 === 0 ? 12 : p.hour % 12}${p.hour < 12 ? 'am' : 'pm'}`;\n      return `<b>${p.day}</b>, ${hourLabel}<br/>${p.value} visits`;\n    },\n  },\n  plotOptions: {\n    series: { animation: false },\n    scatter: {\n      enableMouseTracking: true,\n      stickyTracking: false,\n      marker: {\n        enabled: true,\n        symbol: 'circle',\n        radius: CELL_MARKER_R,\n        fillColor: 'rgba(0,0,0,0.001)',\n        lineWidth: 0,\n        states: { hover: { enabled: false } },\n      },\n    },\n  },\n  series: [{ type: 'scatter', name: 'Visits', data: cellPoints }],\n});\n"}