{"spec_id":"polar-bar","library":"muix","language":"javascript","code":"// anyplot.ai\n// polar-bar: Polar Bar Chart (Wind Rose)\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n// anyplot.ai\n// polar-bar: Polar Bar Chart (Wind Rose)\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\";\n\n// @mui/x-charts 7.x community has no polar-bar / wind-rose component (BarChart\n// only offers `layout: 'vertical' | 'horizontal'`, and PieChart encodes value as\n// angle, not radius) — so the rose is composed on MUI X's own charting surface:\n// ChartContainer sizes the theme-aware <svg>, and useDrawingArea() gives the plot\n// rect the wedge/ring/spoke geometry is mapped onto, exactly like the polar-basic\n// and radar-basic muix implementations in this catalog.\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;\n\n// --- Data (in-memory, deterministic) — monthly wind observations by direction --\nconst DIRECTIONS = [\n  \"N\", \"NNE\", \"NE\", \"ENE\", \"E\", \"ESE\", \"SE\", \"SSE\",\n  \"S\", \"SSW\", \"SW\", \"WSW\", \"W\", \"WNW\", \"NW\", \"NNW\",\n];\nconst SPEED_BINS = [\n  { label: \"Light (0–10 mph)\", color: t.palette[0],\n    values: [18, 15, 12, 10, 14, 20, 25, 30, 35, 40, 45, 42, 38, 30, 24, 20] },\n  { label: \"Moderate (10–20 mph)\", color: t.palette[1],\n    values: [10, 8, 6, 5, 8, 14, 18, 24, 30, 38, 46, 40, 32, 22, 16, 12] },\n  { label: \"Strong (20+ mph)\", color: t.palette[2],\n    values: [2, 1, 1, 1, 2, 4, 6, 10, 14, 20, 28, 22, 15, 8, 4, 3] },\n];\nconst N = DIRECTIONS.length;\nconst RINGS = [30, 60, 90, 120];\nconst MAX_VALUE = 120;\n// Only the 8 primary compass points get a spoke + label — a full 16-spoke grid\n// would compete visually with the wedges themselves.\nconst MAJOR_INDICES = [0, 2, 4, 6, 8, 10, 12, 14];\n\n// Direction 0 (N) points to the top (-90°); angle grows clockwise around the compass.\nconst angleOf = (i) => (-90 + (i * 360) / N) * (Math.PI / 180);\nconst SECTOR_HALF_WIDTH = Math.PI / N;\nconst WEDGE_GAP = 0.035; // radians of padding on each side of a wedge\n\nfunction sectorPath(cx, cy, innerR, outerR, startAngle, endAngle) {\n  const at = (r, a) => [cx + r * Math.cos(a), cy + r * Math.sin(a)];\n  const [ox0, oy0] = at(outerR, startAngle);\n  const [ox1, oy1] = at(outerR, endAngle);\n  if (innerR < 0.5) {\n    return `M ${cx},${cy} L ${ox0},${oy0} A ${outerR} ${outerR} 0 0 1 ${ox1},${oy1} Z`;\n  }\n  const [ix0, iy0] = at(innerR, startAngle);\n  const [ix1, iy1] = at(innerR, endAngle);\n  return (\n    `M ${ix0},${iy0} L ${ox0},${oy0} ` +\n    `A ${outerR} ${outerR} 0 0 1 ${ox1},${oy1} L ${ix1},${iy1} ` +\n    `A ${innerR} ${innerR} 0 0 0 ${ix0},${iy0} Z`\n  );\n}\n\n// --- Wind rose layer: rendered as children inside MUI X's ChartsSurface --------\nfunction WindRose() {\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 - 70; // data radius; margin between R and half holds labels\n  const labelR = half - 8;\n  const pxPerUnit = R / MAX_VALUE;\n\n  return (\n    <g>\n      {/* Concentric frequency rings — outer ring solid, inner rings lighter so\n          the 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_VALUE) * R}\n          fill=\"none\"\n          stroke={GRID}\n          strokeWidth={level === MAX_VALUE ? 2 : 1}\n          strokeOpacity={level === MAX_VALUE ? 1 : 0.6}\n        />\n      ))}\n\n      {/* Spokes + labels at the 8 primary compass points */}\n      {MAJOR_INDICES.map((i) => {\n        const a = angleOf(i);\n        const ox = cx + R * Math.cos(a);\n        const oy = cy + R * Math.sin(a);\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-${DIRECTIONS[i]}`}>\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              {DIRECTIONS[i]}\n            </text>\n          </g>\n        );\n      })}\n\n      {/* Stacked wedges: each direction's speed bins radiate outward from the\n          center, radius encoding cumulative observation count */}\n      {DIRECTIONS.map((label, i) => {\n        const a0 = angleOf(i) - SECTOR_HALF_WIDTH + WEDGE_GAP;\n        const a1 = angleOf(i) + SECTOR_HALF_WIDTH - WEDGE_GAP;\n        let cum = 0;\n        return SPEED_BINS.map((bin) => {\n          const r0 = cum * pxPerUnit;\n          cum += bin.values[i];\n          const r1 = cum * pxPerUnit;\n          return (\n            <path\n              key={`wedge-${label}-${bin.label}`}\n              d={sectorPath(cx, cy, r0, r1, a0, a1)}\n              fill={bin.color}\n              stroke={PAGE_BG}\n              strokeWidth={2}\n            />\n          );\n        });\n      })}\n\n      {/* Frequency tick labels along the top spoke, with a backing chip so they\n          stay legible over whatever wedge color sits underneath */}\n      {RINGS.map((level) => {\n        const ty = cy - (level / MAX_VALUE) * R;\n        return (\n          <g key={`tick-${level}`}>\n            <rect x={cx + 6} y={ty - 11} width={66} 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} obs`}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Title + legend drawn on the surface, then the wind-rose layer ------------\nfunction Chrome() {\n  const swatch = 26;\n  const swatchGap = 12;\n  const itemGap = 44;\n  const fontSize = 17;\n  const charWidth = fontSize * 0.56;\n  const itemWidths = SPEED_BINS.map((bin) => swatch + swatchGap + bin.label.length * charWidth);\n  const totalWidth = itemWidths.reduce((sum, w) => sum + w, 0) + itemGap * (SPEED_BINS.length - 1);\n  const legendY = size.height - 44;\n  let x = size.width / 2 - totalWidth / 2;\n\n  return (\n    <g>\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-bar · javascript · muix · anyplot.ai\n      </text>\n      {SPEED_BINS.map((bin, i) => {\n        const itemX = x;\n        x += itemWidths[i] + itemGap;\n        return (\n          <g key={`legend-${bin.label}`}>\n            <rect x={itemX} y={legendY - 13} width={swatch} height={swatch} rx={5} fill={bin.color} />\n            <text\n              x={itemX + swatch + swatchGap}\n              y={legendY}\n              fill={INK}\n              fontSize={fontSize}\n              textAnchor=\"start\"\n              dominantBaseline=\"central\"\n            >\n              {bin.label}\n            </text>\n          </g>\n        );\n      })}\n    </g>\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      margin={{ top: 90, bottom: 90, left: 90, right: 90 }}\n      skipAnimation\n    >\n      <Chrome />\n      <WindRose />\n    </ChartContainer>\n  );\n}\n"}