{"spec_id":"heatmap-polar","library":"muix","language":"javascript","code":"// anyplot.ai\n// heatmap-polar: Polar Heatmap for Cyclic Two-Dimensional Data\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n// anyplot.ai\n// heatmap-polar: Polar Heatmap for Cyclic Two-Dimensional Data\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-05\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\nimport { ContinuousColorLegend } from \"@mui/x-charts/ChartsLegend\";\n\n// @mui/x-charts 7.x community has neither a polar coordinate system nor a\n// Heatmap component (Heatmap ships only in the paid @mui/x-charts-pro), so the\n// polar grid is composed directly on MUI X's own surface: ChartContainer draws\n// the sized <svg> + drawing area, useDrawingArea() gives the plot rect the\n// wedges are mapped onto, and a zAxis continuous colorMap feeds the same\n// ContinuousColorLegend component ScatterChart heatmaps use elsewhere in this\n// catalog — a real gradient legend, not a drawn stand-in.\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) — hourly website visits by day of week ---\nconst DAYS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\nconst HOURS = 24;\n\nfunction gauss(x, mu, sigma) {\n  const d = x - mu;\n  return Math.exp(-(d * d) / (2 * sigma * sigma));\n}\n\n// Distance around the 24h clock face, so a peak near midnight (hour 23 vs.\n// hour 0) is treated as adjacent rather than 23 hours apart.\nfunction cyclicDist(hour, center) {\n  const d = Math.abs(hour - center);\n  return Math.min(d, HOURS - d);\n}\n\nfunction visits(dayIdx, hour) {\n  const isWeekend = dayIdx >= 5; // Sat, Sun\n  const isNightlifeDay = dayIdx === 4 || dayIdx === 5; // Fri, Sat late-night crowd\n  // Baseline traffic climbs through the work week toward the weekend, then\n  // eases off Sunday -- a realistic trend that also sharpens the ring-to-ring\n  // (radial) contrast at a fixed hour, since each day now carries a distinct\n  // floor on top of its hourly shape.\n  const base = [14, 18, 22, 26, 31, 27, 19][dayIdx];\n  const morning = isWeekend ? 55 * gauss(hour, 10, 3.2) : 92 * gauss(hour, 8.5, 2.1);\n  const evening = isWeekend ? 85 * gauss(hour, 15.5, 4.2) : 68 * gauss(hour, 19, 2.6);\n  // Peaks exactly at midnight — the case a rectangular heatmap would split\n  // across its first/last column instead of showing as one continuous band.\n  const nightlife = isNightlifeDay ? 48 * gauss(cyclicDist(hour, 0), 0, 2.4) : 0;\n  return base + morning + evening + nightlife;\n}\n\nconst cells = [];\nlet minValue = Infinity;\nlet maxValue = -Infinity;\nfor (let day = 0; day < DAYS.length; day += 1) {\n  for (let hour = 0; hour < HOURS; hour += 1) {\n    const value = visits(day, hour);\n    cells.push({ day, hour, value });\n    minValue = Math.min(minValue, value);\n    maxValue = Math.max(maxValue, value);\n  }\n}\nconst MIN_LABEL = `${Math.round(minValue)} visits/hr`;\nconst MAX_LABEL = `${Math.round(maxValue)} visits/hr`;\n\n// --- Sequential Imprint color scale (imprint_seq: brand green -> blue) --------\nfunction mixHex(hexA, hexB, ratio) {\n  const a = parseInt(hexA.slice(1), 16);\n  const b = parseInt(hexB.slice(1), 16);\n  const channel = (shift) => {\n    const av = (a >> shift) & 255;\n    const bv = (b >> shift) & 255;\n    return Math.round(av + (bv - av) * ratio);\n  };\n  return `#${[16, 8, 0].map((shift) => channel(shift).toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\nfunction seqColor(frac) {\n  return mixHex(t.seq[0], t.seq[1], Math.max(0, Math.min(1, frac)));\n}\n\n// --- Title (fontsize scaled to the 67-char mandated-title baseline) ----------\nconst TITLE = \"Website Traffic by Hour & Day · heatmap-polar · javascript · muix · anyplot.ai\";\nconst TITLE_FONT_SIZE = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length)));\n\n// Angular axis tiles the full 360 degrees with no gap -- hour 23 and hour 0\n// are adjacent wedges, preserving the cyclic-continuity guarantee the spec\n// calls out as the reason to use a polar layout over a rectangular one.\nconst START_ANGLE = 0;\nconst ANGLE_PER_HOUR = 360 / HOURS;\nconst RING_HOLE_RATIO = 0.16;\nconst LABEL_PAD = 40;\n// Day-of-week labels run along a single radial spoke at the bottom (the\n// noon boundary) rather than inside an angular gap, so they never interrupt\n// the continuous wrap around midnight. A text halo (paint-order: stroke)\n// keeps them legible over whichever wedge color they land on.\nconst DAY_LABEL_ANGLE = 180;\n\n// --- Polar heatmap layer: rendered as children inside MUI X's ChartsSurface ---\nfunction PolarHeatmapLayer() {\n  const area = useDrawingArea();\n  const cx = area.left + area.width / 2;\n  const cy = area.top + area.height / 2;\n  const outerR = Math.min(area.width, area.height) / 2 - LABEL_PAD;\n  const innerR = outerR * RING_HOLE_RATIO;\n  const ringThickness = (outerR - innerR) / DAYS.length;\n\n  // angle 0 = straight up (12am), increasing clockwise — matches a clock face.\n  const polarPoint = (angleDeg, r) => {\n    const rad = (angleDeg * Math.PI) / 180;\n    return [cx + r * Math.sin(rad), cy - r * Math.cos(rad)];\n  };\n\n  const wedgePath = (r0, r1, a0, a1) => {\n    const [x0, y0] = polarPoint(a0, r1);\n    const [x1, y1] = polarPoint(a1, r1);\n    const [x2, y2] = polarPoint(a1, r0);\n    const [x3, y3] = polarPoint(a0, r0);\n    return `M ${x0} ${y0} A ${r1} ${r1} 0 0 1 ${x1} ${y1} L ${x2} ${y2} A ${r0} ${r0} 0 0 0 ${x3} ${y3} Z`;\n  };\n\n  const hourMarks = [\n    { hour: 0, label: \"12am\" },\n    { hour: 6, label: \"6am\" },\n    { hour: 12, label: \"12pm\" },\n    { hour: 18, label: \"6pm\" },\n  ];\n\n  return (\n    <g>\n      {cells.map((cell) => {\n        const r0 = innerR + cell.day * ringThickness;\n        const r1 = r0 + ringThickness;\n        const a0 = START_ANGLE + cell.hour * ANGLE_PER_HOUR;\n        const a1 = a0 + ANGLE_PER_HOUR;\n        const frac = (cell.value - minValue) / (maxValue - minValue);\n        return (\n          <path\n            key={`${cell.day}-${cell.hour}`}\n            d={wedgePath(r0, r1, a0, a1)}\n            fill={seqColor(frac)}\n            stroke={t.pageBg}\n            strokeWidth={2.5}\n          />\n        );\n      })}\n\n      {hourMarks.map(({ hour, label }) => {\n        const angle = START_ANGLE + (hour + 0.5) * ANGLE_PER_HOUR;\n        const [lx, ly] = polarPoint(angle, outerR + 24);\n        const dx = lx - cx;\n        const dy = ly - cy;\n        const anchor = dx > 8 ? \"start\" : dx < -8 ? \"end\" : \"middle\";\n        const baseline = dy > 8 ? \"hanging\" : dy < -8 ? \"auto\" : \"central\";\n        return (\n          <text\n            key={`hour-${hour}`}\n            x={lx}\n            y={ly}\n            fill={t.inkSoft}\n            fontSize={16}\n            textAnchor={anchor}\n            dominantBaseline={baseline}\n          >\n            {label}\n          </text>\n        );\n      })}\n\n      <line\n        x1={polarPoint(DAY_LABEL_ANGLE, innerR)[0]}\n        y1={polarPoint(DAY_LABEL_ANGLE, innerR)[1]}\n        x2={polarPoint(DAY_LABEL_ANGLE, outerR)[0]}\n        y2={polarPoint(DAY_LABEL_ANGLE, outerR)[1]}\n        stroke={t.ink}\n        strokeWidth={1}\n        strokeOpacity={0.35}\n      />\n      {DAYS.map((day, i) => {\n        const r = innerR + (i + 0.5) * ringThickness;\n        const [lx, ly] = polarPoint(DAY_LABEL_ANGLE, r);\n        return (\n          <text\n            key={`day-${day}`}\n            x={lx}\n            y={ly}\n            fill={t.ink}\n            fontSize={14}\n            fontWeight={700}\n            textAnchor=\"middle\"\n            dominantBaseline=\"central\"\n            stroke={t.pageBg}\n            strokeWidth={4}\n            strokeLinejoin=\"round\"\n            paintOrder=\"stroke\"\n          >\n            {day}\n          </text>\n        );\n      })}\n    </g>\n  );\n}\n\nfunction Title() {\n  return (\n    <text x={size.width / 2} y={64} fill={t.ink} fontSize={TITLE_FONT_SIZE} fontWeight={500} textAnchor=\"middle\">\n      {TITLE}\n    </text>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) --------------\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={size.width}\n      height={size.height}\n      series={[]}\n      zAxis={[\n        {\n          id: \"visits\",\n          min: minValue,\n          max: maxValue,\n          colorMap: { type: \"continuous\", min: minValue, max: maxValue, color: seqColor },\n        },\n      ]}\n      margin={{ top: 130, bottom: 185, left: 70, right: 70 }}\n      skipAnimation\n    >\n      <Title />\n      <PolarHeatmapLayer />\n      <ContinuousColorLegend\n        axisId=\"visits\"\n        axisDirection=\"z\"\n        position={{ horizontal: \"middle\", vertical: \"bottom\" }}\n        length=\"45%\"\n        thickness={16}\n        minLabel={MIN_LABEL}\n        maxLabel={MAX_LABEL}\n        labelStyle={{ fontSize: 14, fill: t.inkSoft, fontFamily: \"inherit\" }}\n      />\n    </ChartContainer>\n  );\n}\n"}