{"spec_id":"heatmap-polar","library":"d3","language":"javascript","code":"// anyplot.ai\n// heatmap-polar: Polar Heatmap for Cyclic Two-Dimensional Data\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Hourly website visits by day of week: hour-of-day is the angular axis\n// (0-23, cyclic), day of week is the radial axis (Mon innermost, Sun outermost).\nconst days = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\nconst hours = d3.range(24);\n\nconst formatHour = (h) => {\n  const displayHour = h % 12 === 0 ? 12 : h % 12;\n  return `${displayHour}${h < 12 ? \"am\" : \"pm\"}`;\n};\n\n// Tiny fixed-seed LCG for reproducible jitter — Math.random() is not seedable.\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nfunction trafficShape(dayIndex, hour) {\n  const isWeekend = dayIndex >= 5;\n  const morningCommute = Math.exp(-((hour - 9) ** 2) / (2 * 2.5 ** 2));\n  const eveningPeak = Math.exp(-((hour - 20) ** 2) / (2 * 3 ** 2));\n  const lazyMidday = Math.exp(-((hour - 14) ** 2) / (2 * 5 ** 2));\n  return isWeekend ? lazyMidday : morningCommute * 0.9 + eveningPeak;\n}\n\nconst data = [];\nfor (let dayIndex = 0; dayIndex < days.length; dayIndex++) {\n  const dayLevel = dayIndex >= 5 ? 0.55 : 1;\n  for (const hour of hours) {\n    const visits = Math.round(trafficShape(dayIndex, hour) * dayLevel * 820 + lcg() * 60 + 20);\n    data.push({ dayIndex, hour, visits });\n  }\n}\n\nconst [minVisits, maxVisits] = d3.extent(data, (d) => d.visits);\n\n// --- Scales ------------------------------------------------------------------\nconst angle = d3.scaleLinear().domain([0, 24]).range([0, 2 * Math.PI]);\nconst color = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([minVisits, maxVisits]);\n\nconst cx = width / 2;\nconst cy = height / 2 - 40;\nconst outerRadius = Math.min(width, height) / 2 - 190;\nconst innerRadius0 = 90;\nconst ringWidth = (outerRadius - innerRadius0) / days.length;\nconst ringInner = (dayIndex) => innerRadius0 + dayIndex * ringWidth;\nconst ringOuter = (dayIndex) => innerRadius0 + (dayIndex + 1) * ringWidth;\n\n// --- SVG mount -----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${cx},${cy})`);\n\n// --- Cells (arc wedges) --------------------------------------------------------\nconst arc = d3\n  .arc()\n  .startAngle((d) => angle(d.hour))\n  .endAngle((d) => angle(d.hour + 1))\n  .innerRadius((d) => ringInner(d.dayIndex))\n  .outerRadius((d) => ringOuter(d.dayIndex));\n\ng.selectAll(\"path.cell\")\n  .data(data)\n  .join(\"path\")\n  .attr(\"class\", \"cell\")\n  .attr(\"d\", arc)\n  .attr(\"fill\", (d) => color(d.visits))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Angular axis: labels + spokes at readable hour intervals -----------------\nconst majorHours = [0, 6, 12, 18];\nconst hourLabels = { 0: \"12am\", 6: \"6am\", 12: \"12pm\", 18: \"6pm\" };\n\ng.selectAll(\"line.spoke\")\n  .data(majorHours)\n  .join(\"line\")\n  .attr(\"class\", \"spoke\")\n  .attr(\"x1\", (h) => Math.sin(angle(h)) * innerRadius0)\n  .attr(\"y1\", (h) => -Math.cos(angle(h)) * innerRadius0)\n  .attr(\"x2\", (h) => Math.sin(angle(h)) * outerRadius)\n  .attr(\"y2\", (h) => -Math.cos(angle(h)) * outerRadius)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\ng.selectAll(\"text.hour-label\")\n  .data(majorHours)\n  .join(\"text\")\n  .attr(\"class\", \"hour-label\")\n  .attr(\"x\", (h) => Math.sin(angle(h)) * (outerRadius + 34))\n  .attr(\"y\", (h) => -Math.cos(angle(h)) * (outerRadius + 34))\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"18px\")\n  .text((h) => hourLabels[h]);\n\n// --- Radial axis: day labels along an empty 45-degree spoke -------------------\nconst labelAngle = angle(3);\ng.selectAll(\"text.day-label\")\n  .data(days)\n  .join(\"text\")\n  .attr(\"class\", \"day-label\")\n  .attr(\"x\", (d, i) => Math.sin(labelAngle) * (ringInner(i) + ringWidth / 2))\n  .attr(\"y\", (d, i) => -Math.cos(labelAngle) * (ringInner(i) + ringWidth / 2))\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .style(\"paint-order\", \"stroke\")\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 4)\n  .attr(\"stroke-linejoin\", \"round\")\n  .attr(\"fill\", t.ink)\n  .text((d) => d);\n\n// --- Peak-cell highlight & story annotation ------------------------------------\n// Calls out the single highest-traffic cell so the chart states its insight\n// (morning-commute/evening-peak pattern) instead of relying on color alone.\nconst peak = data.reduce((best, d) => (d.visits > best.visits ? d : best), data[0]);\nconst peakMidAngle = angle(peak.hour + 0.5);\nconst peakOuterR = ringOuter(peak.dayIndex);\nconst peakPoint = {\n  x: Math.sin(peakMidAngle) * peakOuterR,\n  y: -Math.cos(peakMidAngle) * peakOuterR,\n};\nconst peakLabelX = -outerRadius - 40;\nconst peakLabelY = -outerRadius * 0.65;\n\ng.append(\"path\")\n  .attr(\"d\", arc({ hour: peak.hour, dayIndex: peak.dayIndex }))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 3);\n\ng.append(\"line\")\n  .attr(\"x1\", peakPoint.x)\n  .attr(\"y1\", peakPoint.y)\n  .attr(\"x2\", peakLabelX + 120)\n  .attr(\"y2\", peakLabelY + 34)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.25);\n\ng.append(\"circle\")\n  .attr(\"cx\", peakPoint.x)\n  .attr(\"cy\", peakPoint.y)\n  .attr(\"r\", 4.5)\n  .attr(\"fill\", t.ink);\n\nconst peakLabel = g.append(\"text\").attr(\"text-anchor\", \"start\");\npeakLabel\n  .append(\"tspan\")\n  .attr(\"x\", peakLabelX)\n  .attr(\"y\", peakLabelY)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"700\")\n  .attr(\"fill\", t.ink)\n  .text(\"Peak traffic\");\npeakLabel\n  .append(\"tspan\")\n  .attr(\"x\", peakLabelX)\n  .attr(\"dy\", \"1.3em\")\n  .style(\"font-size\", \"14px\")\n  .attr(\"fill\", t.inkSoft)\n  .text(`${days[peak.dayIndex]} ${formatHour(peak.hour)} · ${peak.visits} visits`);\n\n// --- Title ---------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"heatmap-polar · javascript · d3 · anyplot.ai\");\n\n// --- Legend: sequential colorbar -------------------------------------------\nconst legendWidth = 420;\nconst legendHeight = 18;\nconst legendX = width / 2 - legendWidth / 2;\nconst legendY = height - 110;\n\nconst gradient = svg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", \"imprint-seq-gradient\")\n  .attr(\"x1\", \"0%\")\n  .attr(\"x2\", \"100%\")\n  .attr(\"y1\", \"0%\")\n  .attr(\"y2\", \"0%\");\n\nd3.range(0, 1.01, 0.1).forEach((stop) => {\n  gradient\n    .append(\"stop\")\n    .attr(\"offset\", `${stop * 100}%`)\n    .attr(\"stop-color\", color(minVisits + stop * (maxVisits - minVisits)));\n});\n\nsvg\n  .append(\"rect\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY)\n  .attr(\"width\", legendWidth)\n  .attr(\"height\", legendHeight)\n  .attr(\"rx\", legendHeight / 2)\n  .attr(\"ry\", legendHeight / 2)\n  .attr(\"fill\", \"url(#imprint-seq-gradient)\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\n// Intermediate tick marks give the colorbar a graduated feel beyond bare min/max\nsvg\n  .selectAll(\"line.legend-tick\")\n  .data([0.25, 0.5, 0.75])\n  .join(\"line\")\n  .attr(\"class\", \"legend-tick\")\n  .attr(\"x1\", (frac) => legendX + frac * legendWidth)\n  .attr(\"x2\", (frac) => legendX + frac * legendWidth)\n  .attr(\"y1\", legendY + legendHeight)\n  .attr(\"y2\", legendY + legendHeight + 6)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY - 12)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(`${Math.round(minVisits)} visits`);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendWidth)\n  .attr(\"y\", legendY - 12)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(`${Math.round(maxVisits)} visits`);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", legendY + legendHeight + 26)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Hourly website visits by day of week\");\n"}