{"spec_id":"gauge-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// gauge-basic: Basic Gauge Chart\n// Library: muix 7.29.1 | JavaScript 22.23.0\n// Quality: 90/100 | Created: 2026-06-30\n//# anyplot-orientation: square\n// anyplot.ai\n// gauge-basic: Basic Gauge 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-06-30\n\nimport {\n  GaugeContainer,\n  useGaugeState,\n} from \"@mui/x-charts/Gauge\";\nimport { Box, Typography } from \"@mui/material\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Customer satisfaction score — 0–100 scale, zones at 30 and 70\nconst score = 72;\nconst VALUE_MIN = 0;\nconst VALUE_MAX = 100;\nconst LOW_THRESHOLD = 30;\nconst HIGH_THRESHOLD = 70;\n\n// Imprint semantic colors: red=poor, amber=fair, green=good\nconst COLOR_POOR = t.palette[4]; // #AE3030\nconst COLOR_FAIR = t.amber;      // #DDCC77\nconst COLOR_GOOD = t.palette[0]; // #009E73 — brand green\n\nconst scoreColor =\n  score < LOW_THRESHOLD\n    ? COLOR_POOR\n    : score < HIGH_THRESHOLD\n    ? COLOR_FAIR\n    : COLOR_GOOD;\n\n// Gauge arc geometry (degrees — MUI X convention)\nconst START_DEG = -120;\nconst END_DEG = 120;\nconst START_RAD = (START_DEG * Math.PI) / 180;\nconst TOTAL_RAD = ((END_DEG - START_DEG) * Math.PI) / 180;\n\n// SVG annular sector path (angles in radians, MUI X convention: 0=top, CW)\nfunction sectorPath(cx, cy, innerR, outerR, s, e) {\n  const la = e - s > Math.PI ? 1 : 0;\n  const x1 = cx + outerR * Math.sin(s);\n  const y1 = cy - outerR * Math.cos(s);\n  const x2 = cx + outerR * Math.sin(e);\n  const y2 = cy - outerR * Math.cos(e);\n  const x3 = cx + innerR * Math.sin(e);\n  const y3 = cy - innerR * Math.cos(e);\n  const x4 = cx + innerR * Math.sin(s);\n  const y4 = cy - innerR * Math.cos(s);\n  return `M${x1},${y1} A${outerR},${outerR} 0 ${la},1 ${x2},${y2} L${x3},${y3} A${innerR},${innerR} 0 ${la},0 ${x4},${y4} Z`;\n}\n\n// Zone arcs drawn inside GaugeContainer — has access to gauge geometry via hook\nfunction ZoneArcs() {\n  const { cx, cy, outerRadius, innerRadius } = useGaugeState();\n  const lowFrac = LOW_THRESHOLD / VALUE_MAX;\n  const highFrac = HIGH_THRESHOLD / VALUE_MAX;\n  const gapRad = TOTAL_RAD * 0.007; // thin gap between zones\n\n  const z1S = START_RAD;\n  const z1E = START_RAD + TOTAL_RAD * lowFrac - gapRad;\n  const z2S = START_RAD + TOTAL_RAD * lowFrac + gapRad;\n  const z2E = START_RAD + TOTAL_RAD * highFrac - gapRad;\n  const z3S = START_RAD + TOTAL_RAD * highFrac + gapRad;\n  const z3E = START_RAD + TOTAL_RAD;\n\n  return (\n    <g>\n      <path d={sectorPath(cx, cy, innerRadius, outerRadius, z1S, z1E)} fill={COLOR_POOR} />\n      <path d={sectorPath(cx, cy, innerRadius, outerRadius, z2S, z2E)} fill={COLOR_FAIR} />\n      <path d={sectorPath(cx, cy, innerRadius, outerRadius, z3S, z3E)} fill={COLOR_GOOD} />\n    </g>\n  );\n}\n\n// Custom needle + score label rendered inside the gauge SVG\nfunction NeedleAndLabel() {\n  const { cx, cy, outerRadius, innerRadius, value } = useGaugeState();\n  const frac = (value - VALUE_MIN) / (VALUE_MAX - VALUE_MIN);\n  const angleRad = START_RAD + TOTAL_RAD * frac;\n  const needleLen = outerRadius * 0.82;\n  const tipX = cx + needleLen * Math.sin(angleRad);\n  const tipY = cy - needleLen * Math.cos(angleRad);\n  const perp = angleRad + Math.PI / 2;\n  const baseW = Math.max(4, outerRadius * 0.025);\n  const baseR = innerRadius * 0.35;\n  const b1x = cx + baseR * Math.sin(angleRad) + baseW * Math.sin(perp);\n  const b1y = cy - baseR * Math.cos(angleRad) - baseW * Math.cos(perp);\n  const b2x = cx + baseR * Math.sin(angleRad) - baseW * Math.sin(perp);\n  const b2y = cy - baseR * Math.cos(angleRad) + baseW * Math.cos(perp);\n\n  return (\n    <g>\n      {/* Needle */}\n      <polygon\n        points={`${tipX},${tipY} ${b1x},${b1y} ${b2x},${b2y}`}\n        fill={t.ink}\n        opacity={0.88}\n      />\n      {/* Center cap */}\n      <circle cx={cx} cy={cy} r={outerRadius * 0.055} fill={t.ink} opacity={0.88} />\n      {/* Score */}\n      <text\n        x={cx}\n        y={cy + outerRadius * 0.24}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        style={{\n          fontSize: outerRadius * 0.38,\n          fontWeight: 700,\n          fill: scoreColor,\n          fontFamily: \"Roboto, sans-serif\",\n        }}\n      >\n        {value}\n      </text>\n      <text\n        x={cx}\n        y={cy + outerRadius * 0.50}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        style={{\n          fontSize: outerRadius * 0.13,\n          fill: t.inkSoft,\n          fontFamily: \"Roboto, sans-serif\",\n        }}\n      >\n        out of {VALUE_MAX}\n      </text>\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const gaugeW = width * 0.8;\n  const gaugeH = height * 0.62;\n\n  // Title: \"Customer Satisfaction Score · gauge-basic · javascript · muix · anyplot.ai\" = 80 chars\n  // Scale: round(22 × 67/80) = round(18.43) = 18 px\n  const titleFontSize = Math.round(22 * (67 / 80));\n\n  const zones = [\n    { label: \"Needs Work\", range: \"0–30\", color: COLOR_POOR },\n    { label: \"Developing\", range: \"30–70\", color: COLOR_FAIR },\n    { label: \"Excellent\",  range: \"70–100\", color: COLOR_GOOD },\n  ];\n\n  return (\n    <Box\n      sx={{\n        width,\n        height,\n        bgcolor: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      {/* Mandatory title */}\n      <Typography\n        sx={{\n          color: t.ink,\n          fontSize: titleFontSize,\n          fontWeight: 600,\n          letterSpacing: \"0.01em\",\n          mb: 1,\n          textAlign: \"center\",\n          px: 2,\n        }}\n      >\n        Customer Satisfaction Score · gauge-basic · javascript · muix · anyplot.ai\n      </Typography>\n\n      {/* Gauge */}\n      <GaugeContainer\n        width={gaugeW}\n        height={gaugeH}\n        value={score}\n        valueMin={VALUE_MIN}\n        valueMax={VALUE_MAX}\n        startAngle={START_DEG}\n        endAngle={END_DEG}\n        innerRadius=\"58%\"\n        outerRadius=\"90%\"\n        skipAnimation\n      >\n        <ZoneArcs />\n        <NeedleAndLabel />\n      </GaugeContainer>\n\n      {/* Zone legend */}\n      <Box sx={{ display: \"flex\", gap: 4, mt: 1 }}>\n        {zones.map(({ label, range, color }) => (\n          <Box key={label} sx={{ display: \"flex\", alignItems: \"center\", gap: 1 }}>\n            <Box\n              sx={{\n                width: 14,\n                height: 14,\n                borderRadius: \"3px\",\n                bgcolor: color,\n                flexShrink: 0,\n              }}\n            />\n            <Typography sx={{ color: t.inkSoft, fontSize: 15 }}>\n              {label} ({range})\n            </Typography>\n          </Box>\n        ))}\n      </Box>\n    </Box>\n  );\n}\n"}