{"spec_id":"polar-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// polar-basic: Basic Polar Chart\n// Library: muix 7.29.1 | JavaScript 22.23.1\n// Quality: 91/100 | Updated: 2026-07-25\n//# anyplot-orientation: square\n// anyplot.ai\n// polar-basic: Basic Polar Chart\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-07-24\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsTooltip } from \"@mui/x-charts/ChartsTooltip\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\n\n// @mui/x-charts 7.x community has no polar/radial chart component (a PolarProvider\n// exists internally but isn't part of the public export surface), so the polar\n// plot is composed on MUI X's own charting surface: ChartContainer sizes the\n// <svg> + theme, and useDrawingArea() gives the plot rect the custom ring/spoke/\n// polygon geometry is mapped onto. The 24 hourly points are ALSO registered as a\n// genuine `scatter` series (mapped through a matching linear xAxis/yAxis so the\n// pixels line up exactly with the hand-drawn geometry) so ChartsTooltip and MUI's\n// own hover-highlight are real, not decorative — nothing here is faked chrome.\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// Theme-adaptive chrome (ThemeProvider handles MUI text; these are for our SVG).\nconst INK = t.ink;\nconst INK_SOFT = t.inkSoft;\nconst GRID = t.grid;\nconst PAGE_BG = t.pageBg;\nconst BRAND = t.palette[0]; // Imprint palette position 1 — always first series\n\n// --- Data (in-memory, deterministic) -------------------------------------------\n// Smart-home electricity draw across a 24h cycle: a commuter-hours morning peak\n// and a larger evening peak, low overnight — the cyclical pattern polar coords\n// are meant to reveal, invisible as a simple bump in a cartesian line chart.\nconst HOURS = 24;\nconst kwh = [\n  1.2, 1.0, 0.9, 0.8, 0.9, 1.3, 2.1, 3.4, 3.8, 2.9, 2.2, 2.0, 2.3, 2.1, 2.0, 2.2,\n  2.6, 3.5, 4.6, 4.9, 4.2, 3.1, 2.0, 1.5,\n];\nconst MAX_KWH = 5;\nconst RINGS = [1, 2, 3, 4, 5];\nconst LABELED_HOURS = [0, 6, 12, 18];\nconst HOUR_LABELS = { 0: \"12 AM\", 6: \"6 AM\", 12: \"12 PM\", 18: \"6 PM\" };\n// The two daily bulges the circular layout is meant to reveal (morning commute,\n// evening peak) — called out explicitly with an accent ring + leader label below.\nconst PEAKS = [\n  { hour: 8, label: \"Morning peak\" },\n  { hour: 19, label: \"Evening peak\" },\n];\nconst MARKER_R = 6.5;\n\n// Hour 0 points to the top (-90°); angle grows clockwise as the day progresses.\nconst angleOf = (hour) => (-90 + (hour / HOURS) * 360) * (Math.PI / 180);\nconst hourLabel = (hour) => {\n  const period = hour < 12 ? \"AM\" : \"PM\";\n  const h12 = hour % 12 === 0 ? 12 : hour % 12;\n  return `${h12}:00 ${period}`;\n};\n\n// --- Real MUI X scatter series for the hourly points ----------------------------\n// A linear xAxis/yAxis pair whose domain is sized so that value 1 (frac = 1, i.e.\n// MAX_KWH) lands exactly `DATA_R` px from centre — the same radius the hand-drawn\n// rings/polygon below use (both derive from the same MARGIN/size constants) — so\n// the real MUI scatter dots register precisely on top of the custom SVG geometry.\nconst MARGIN = 90;\nconst HALF = Math.min(size.width, size.height) / 2 - MARGIN;\nconst DATA_R = HALF - 60;\nconst DOMAIN = HALF / DATA_R;\nconst SERIES_DATA = kwh.map((v, hour) => {\n  const a = angleOf(hour);\n  const frac = v / MAX_KWH;\n  return { x: frac * Math.cos(a), y: -frac * Math.sin(a), id: hour, hour, kwh: v };\n});\nconst SERIES = [\n  {\n    type: \"scatter\",\n    data: SERIES_DATA,\n    color: BRAND,\n    markerSize: MARKER_R,\n    label: \"Electricity draw\",\n    valueFormatter: (v) => `${hourLabel(v.hour)} · ${v.kwh.toFixed(1)} kWh`,\n  },\n];\n\n// --- Polar layer: rendered as children inside MUI X's ChartsSurface ------------\nfunction PolarLayer() {\n  const area = useDrawingArea();\n  const cx = area.left + area.width / 2;\n  const cy = area.top + area.height / 2;\n  const half = Math.min(area.width, area.height) / 2;\n  const R = half - 60; // data radius; margin between R and half holds labels\n  const labelR = half - 6;\n\n  const point = (frac, hour) => {\n    const a = angleOf(hour);\n    return [cx + frac * R * Math.cos(a), cy + frac * R * Math.sin(a)];\n  };\n\n  const linePoints = kwh\n    .map((v, hour) => point(v / MAX_KWH, hour).join(\",\"))\n    .join(\" \");\n\n  return (\n    <g>\n      {/* Concentric radius gridlines, one per kWh ring — outer ring solid,\n          inner rings lighter so the nested grid stays subtle near the center */}\n      {RINGS.map((level) => (\n        <circle\n          key={`ring-${level}`}\n          cx={cx}\n          cy={cy}\n          r={(level / MAX_KWH) * R}\n          fill=\"none\"\n          stroke={GRID}\n          strokeWidth={level === MAX_KWH ? 2 : 1}\n          strokeOpacity={level === MAX_KWH ? 1 : 0.6}\n        />\n      ))}\n\n      {/* Angular spokes + labels at the four standard clock positions */}\n      {LABELED_HOURS.map((hour) => {\n        const [ox, oy] = point(1, hour);\n        const a = angleOf(hour);\n        const lx = cx + labelR * Math.cos(a);\n        const ly = cy + labelR * Math.sin(a);\n        const cos = Math.cos(a);\n        const anchor = cos > 0.3 ? \"start\" : cos < -0.3 ? \"end\" : \"middle\";\n        const sin = Math.sin(a);\n        const baseline = sin > 0.5 ? \"hanging\" : sin < -0.5 ? \"auto\" : \"central\";\n        return (\n          <g key={`spoke-${hour}`}>\n            <line x1={cx} y1={cy} x2={ox} y2={oy} stroke={GRID} strokeWidth={1.25} />\n            <text\n              x={lx}\n              y={ly}\n              fill={INK}\n              fontSize={18}\n              fontWeight={600}\n              textAnchor={anchor}\n              dominantBaseline={baseline}\n            >\n              {HOUR_LABELS[hour]}\n            </text>\n          </g>\n        );\n      })}\n\n      {/* Closed polygon: translucent fill + solid outline. The hourly markers\n          themselves are a real <ScatterPlot> series (sibling of this layer,\n          registered on a matching xAxis/yAxis) — this halo ring just gives each\n          dot a light separation from the polygon fill/stroke underneath it. */}\n      <polygon\n        points={linePoints}\n        fill={BRAND}\n        fillOpacity={0.22}\n        stroke={BRAND}\n        strokeWidth={3}\n        strokeLinejoin=\"round\"\n      />\n      {kwh.map((v, hour) => {\n        const [px, py] = point(v / MAX_KWH, hour);\n        const isPeak = PEAKS.some((p) => p.hour === hour);\n        return (\n          <circle\n            key={`halo-${hour}`}\n            cx={px}\n            cy={py}\n            r={isPeak ? MARKER_R + 3 : MARKER_R + 1}\n            fill=\"none\"\n            stroke={isPeak ? BRAND : PAGE_BG}\n            strokeWidth={isPeak ? 2 : 1.2}\n          />\n        );\n      })}\n\n      {/* Explicit callouts for the two daily peaks — a dashed leader from the\n          peak point to an italic label, so the story reads immediately instead\n          of only being implicit in the polygon's shape. */}\n      {PEAKS.map(({ hour, label }) => {\n        const a = angleOf(hour);\n        const [peakX, peakY] = point(kwh[hour] / MAX_KWH, hour);\n        const lx = cx + 0.5 * R * Math.cos(a);\n        const ly = cy + 0.5 * R * Math.sin(a);\n        const cos = Math.cos(a);\n        const anchor = cos > 0.15 ? \"start\" : cos < -0.15 ? \"end\" : \"middle\";\n        return (\n          <g key={`peak-${hour}`}>\n            <line\n              x1={peakX}\n              y1={peakY}\n              x2={lx}\n              y2={ly}\n              stroke={INK_SOFT}\n              strokeWidth={1}\n              strokeDasharray=\"2,3\"\n            />\n            <text\n              x={lx}\n              y={ly}\n              fill={INK}\n              fontSize={14}\n              fontWeight={600}\n              fontStyle=\"italic\"\n              textAnchor={anchor}\n              dominantBaseline=\"central\"\n            >\n              {label}\n            </text>\n          </g>\n        );\n      })}\n\n      {/* Radius (kWh) tick labels, offset off the top spoke, drawn LAST so they\n          sit above the data polygon. Theta is continuous here (unlike a radar\n          chart's fixed high-value axis), so the polygon crosses this spoke too\n          at low-value hours — each label gets an opaque backing chip to stay\n          legible regardless of what's underneath. */}\n      {RINGS.map((level) => {\n        const ty = cy - (level / MAX_KWH) * R;\n        return (\n          <g key={`tick-${level}`}>\n            <rect x={cx + 6} y={ty - 11} width={62} height={22} rx={4} fill={PAGE_BG} />\n            <text\n              x={cx + 12}\n              y={ty}\n              fill={INK_SOFT}\n              fontSize={15}\n              textAnchor=\"start\"\n              dominantBaseline=\"central\"\n            >\n              {`${level} kWh`}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Title drawn on the surface, then the polar layer ---------------------------\nfunction Chrome() {\n  return (\n    <text\n      x={size.width / 2}\n      y={52}\n      fill={INK}\n      fontSize={28}\n      fontWeight={700}\n      textAnchor=\"middle\"\n    >\n      polar-basic · javascript · muix · anyplot.ai\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={SERIES}\n      xAxis={[{ scaleType: \"linear\", min: -DOMAIN, max: DOMAIN }]}\n      yAxis={[{ scaleType: \"linear\", min: -DOMAIN, max: DOMAIN }]}\n      margin={{ top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }}\n      skipAnimation\n    >\n      <Chrome />\n      <PolarLayer />\n      <ScatterPlot />\n      <ChartsTooltip trigger=\"item\" />\n    </ChartContainer>\n  );\n}\n"}