{"spec_id":"polar-scatter","library":"muix","language":"javascript","code":"// anyplot.ai\n// polar-scatter: Polar Scatter Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 85/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n// anyplot.ai\n// polar-scatter: Polar Scatter Plot\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\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsTooltip } from \"@mui/x-charts/ChartsTooltip\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG — the browser has no seeded RNG) ----\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\n// Wind observations grouped by time of day. Coastal sites see a diurnal wind\n// rotation (land breeze overnight/morning, sea breeze in the afternoon, with\n// evening/night transitional flow), so each group's prevailing bearing is\n// spaced ~90° apart around the full compass rather than clustered on one side.\nconst GROUPS = [\n  { category: \"Morning\", bearing: 15, speed: 7, spread: 85, markerSize: 8 },\n  { category: \"Afternoon\", bearing: 105, speed: 13, spread: 90, markerSize: 8 },\n  { category: \"Evening\", bearing: 195, speed: 10, spread: 80, markerSize: 8 },\n  { category: \"Night\", bearing: 285, speed: 4, spread: 75, markerSize: 6 },\n];\nconst POINTS_PER_GROUP = 30;\nconst MAX_RADIUS = 20; // m/s\nconst FILL_OPACITY = 0.7; // keeps overlapping points distinguishable\n\nfunction polarToXY(bearingDeg, radius) {\n  const rad = (bearingDeg * Math.PI) / 180;\n  return { x: radius * Math.sin(rad), y: radius * Math.cos(rad) };\n}\n\n// `ScatterSeriesType` has no `fillOpacity` prop — the renderer paints markers\n// with `fill: series.color` directly, so translucency is baked into the color.\nfunction withAlpha(hex, alpha) {\n  const h = hex.replace(\"#\", \"\");\n  const r = parseInt(h.substring(0, 2), 16);\n  const g = parseInt(h.substring(2, 4), 16);\n  const b = parseInt(h.substring(4, 6), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\nconst series = GROUPS.map((group, i) => ({\n  type: \"scatter\",\n  id: group.category,\n  label: group.category,\n  color: withAlpha(t.palette[i], FILL_OPACITY),\n  markerSize: group.markerSize,\n  data: Array.from({ length: POINTS_PER_GROUP }, (_, idx) => {\n    const bearing = (((group.bearing + (rand() - 0.5) * group.spread) % 360) + 360) % 360;\n    const radius = Math.min(MAX_RADIUS, Math.max(0.5, group.speed + (rand() - 0.5) * group.speed));\n    const { x, y } = polarToXY(bearing, radius);\n    return { id: `${group.category}-${idx}`, x, y };\n  }),\n}));\n\n// --- Polar grid overlay — community `@mui/x-charts` has no native polar\n// chart, so the radial/angular grid is drawn as an SVG layer that shares the\n// same linear x/y scales as the scatter points (via useXScale/useYScale),\n// keeping rings, spokes and the data perfectly aligned at any canvas size. --\nconst RINGS = [5, 10, 15, 20];\nconst SPOKE_DEGREES = [0, 45, 90, 135, 180, 225, 270, 315];\nconst LABEL_RADIUS = MAX_RADIUS * 1.14;\n\nfunction PolarGrid() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const cx = xScale(0);\n  const cy = yScale(0);\n\n  return (\n    <g>\n      {RINGS.map((r) => (\n        <circle\n          key={`ring-${r}`}\n          cx={cx}\n          cy={cy}\n          r={Math.abs(xScale(r) - xScale(0))}\n          fill=\"none\"\n          stroke={t.grid}\n          strokeWidth={1}\n        />\n      ))}\n      {SPOKE_DEGREES.map((deg) => {\n        const { x, y } = polarToXY(deg, MAX_RADIUS);\n        return (\n          <line\n            key={`spoke-${deg}`}\n            x1={cx}\n            y1={cy}\n            x2={xScale(x)}\n            y2={yScale(y)}\n            stroke={t.grid}\n            strokeWidth={1}\n          />\n        );\n      })}\n      {SPOKE_DEGREES.map((deg) => {\n        const { x, y } = polarToXY(deg, LABEL_RADIUS);\n        return (\n          <text\n            key={`spoke-label-${deg}`}\n            x={xScale(x)}\n            y={yScale(y)}\n            fill={t.inkSoft}\n            fontSize={14}\n            textAnchor=\"middle\"\n            dominantBaseline=\"middle\"\n          >\n            {`${deg}°`}\n          </text>\n        );\n      })}\n      {RINGS.map((r) => (\n        <text\n          key={`ring-label-${r}`}\n          x={xScale(0) + 8}\n          y={yScale(r) - 6}\n          fill={t.inkSoft}\n          fontSize={12}\n          textAnchor=\"start\"\n        >\n          {`${r} m/s`}\n        </text>\n      ))}\n    </g>\n  );\n}\n\nconst TITLE = \"polar-scatter · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 70;\nconst LEGEND_HEIGHT = 50;\nconst MARGIN = { top: 40, bottom: 40, left: 40, right: 40 };\nconst DOMAIN = MAX_RADIUS * 1.3;\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chartSize = height - TITLE_HEIGHT - LEGEND_HEIGHT;\n\n  return (\n    <div style={{ width, height, display: \"flex\", flexDirection: \"column\", alignItems: \"center\" }}>\n      <div\n        style={{\n          height: TITLE_HEIGHT,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontSize: 22,\n          fontWeight: 500,\n          color: t.ink,\n        }}\n      >\n        {TITLE}\n      </div>\n      <ChartContainer\n        width={chartSize}\n        height={chartSize}\n        margin={MARGIN}\n        skipAnimation\n        xAxis={[{ scaleType: \"linear\", min: -DOMAIN, max: DOMAIN }]}\n        yAxis={[{ scaleType: \"linear\", min: -DOMAIN, max: DOMAIN }]}\n        series={series}\n      >\n        <PolarGrid />\n        <ScatterPlot />\n        <ChartsTooltip trigger=\"item\" />\n      </ChartContainer>\n      <div\n        style={{\n          height: LEGEND_HEIGHT,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          gap: 24,\n        }}\n      >\n        {GROUPS.map((group, i) => (\n          <div key={group.category} style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n            <span\n              style={{\n                width: 12,\n                height: 12,\n                borderRadius: \"50%\",\n                backgroundColor: t.palette[i],\n                display: \"inline-block\",\n              }}\n            />\n            <span style={{ fontSize: 14, color: t.inkSoft }}>{group.category}</span>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n"}