{"spec_id":"heatmap-polar","library":"echarts","language":"javascript","code":"// anyplot.ai\n// heatmap-polar: Polar Heatmap for Cyclic Two-Dimensional Data\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 81/100 | Created: 2026-09-05\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Website visits by hour of day (angular, 24 bins, wraps continuously) and day\n// of week (radial, 7 rings — Mon innermost). Native ECharts `heatmap` series\n// only supports cartesian2d/calendar/matrix coordinate systems, so the polar\n// cells are drawn with a `custom` series whose renderItem builds a `sector`\n// shape from the polar coordinate system's own angle/radius mapping — the\n// documented ECharts mechanism for chart types the built-in series don't cover.\nconst days = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\nconst hours = Array.from({ length: 24 }, (_, h) => h);\n\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\nconst cells = [];\nlet maxVisits = 0;\ndays.forEach((day, dayIdx) => {\n  const isWeekend = dayIdx >= 5;\n  hours.forEach((hour, hourIdx) => {\n    const businessBump = !isWeekend && hour >= 9 && hour <= 17 ? 400 : 0;\n    const lunchBump = !isWeekend && (hour === 12 || hour === 13) ? 150 : 0;\n    const eveningBump = isWeekend && hour >= 18 && hour <= 23 ? 350 : 0;\n    const overnightDip = hour <= 5 ? -90 : 0;\n    const base = 150 + businessBump + lunchBump + eveningBump + overnightDip;\n    const visits = Math.max(15, Math.round(base + rand() * 100 - 50));\n    maxVisits = Math.max(maxVisits, visits);\n    // [radiusIndex, angleIndex, value] — matches the polar coord order (radius, angle)\n    // used by the polar custom-series API (see renderItem below).\n    cells.push([dayIdx, hourIdx, visits]);\n  });\n});\n\n// --- Custom-series renderItem: one annular sector per (day, hour) cell ------\nfunction renderItem(params, api) {\n  const radiusIdx = api.value(0);\n  const angleIdx = api.value(1);\n  const point = api.coord([radiusIdx, angleIdx]); // [x, y, radius, angleRad]\n  const size = api.size([1, 1], [radiusIdx, angleIdx]); // [radiusBand, angleBandRad]\n  // zrender's `sector` shape sweeps its angle in the opposite rotational\n  // direction from the polar coordinate system's own angle convention (the\n  // one that places the angleAxis tick labels), so the raw coordinate-system\n  // angle must be negated here or every cell renders mirrored across the\n  // horizontal axis relative to its labeled hour.\n  const angle = -point[3];\n  return {\n    type: \"sector\",\n    shape: {\n      cx: params.coordSys.cx,\n      cy: params.coordSys.cy,\n      r0: point[2] - size[0] / 2,\n      r: point[2] + size[0] / 2,\n      startAngle: angle - size[1] / 2,\n      endAngle: angle + size[1] / 2,\n    },\n    style: api.style({\n      fill: api.visual(\"color\"),\n      stroke: t.pageBg,\n      lineWidth: 1.5,\n    }),\n  };\n}\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nconst title = \"Website Traffic by Hour & Day · heatmap-polar · javascript · echarts · anyplot.ai\";\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: title,\n    left: \"center\",\n    top: 24,\n    textStyle: { color: t.ink, fontSize: 18, fontWeight: 500 },\n  },\n  polar: { center: [\"50%\", \"56%\"], radius: [\"16%\", \"80%\"] },\n  angleAxis: {\n    type: \"category\",\n    data: hours.map(String),\n    polarIndex: 0,\n    startAngle: 90,\n    clockwise: true,\n    z: 10,\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 14,\n      formatter: (value) => {\n        const hour = Number(value);\n        if (hour === 0) return \"12am\";\n        if (hour === 6) return \"6am\";\n        if (hour === 12) return \"12pm\";\n        if (hour === 18) return \"6pm\";\n        return \"\";\n      },\n    },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  // z:10 lifts the axis group above the custom sector series — the radius axis\n  // sits inside the fully-covered ring (its own polar coordinate slot), so\n  // without a higher z its day labels would be painted first and hidden\n  // beneath the opaque cells. The elevated-bg label chip keeps them legible\n  // against whichever cell color ends up underneath.\n  radiusAxis: {\n    type: \"category\",\n    data: days,\n    polarIndex: 0,\n    z: 10,\n    axisLabel: {\n      color: t.ink,\n      fontSize: 15,\n      fontWeight: 500,\n      backgroundColor: t.elevatedBg,\n      padding: [5, 9],\n      borderRadius: 5,\n    },\n    axisLine: { show: false },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  visualMap: {\n    type: \"continuous\",\n    dimension: 2,\n    min: 0,\n    max: maxVisits,\n    right: 60,\n    top: \"middle\",\n    itemHeight: 320,\n    orient: \"vertical\",\n    text: [\"High\", \"Low\"],\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    inRange: { color: t.seq },\n  },\n  series: [\n    {\n      type: \"custom\",\n      coordinateSystem: \"polar\",\n      data: cells,\n      renderItem,\n    },\n  ],\n});\n\nchart.on(\"finished\", () => {\n  window.__anyplotReady = true;\n});\n"}