{"spec_id":"gauge-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// gauge-basic: Basic Gauge Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.0\n// Quality: 90/100 | Created: 2026-06-30\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data -------------------------------------------------------------------\nconst value = 72;\nconst minVal = 0;\nconst maxVal = 100;\nconst thresholds = [30, 70];\nconst metricLabel = \"Customer Satisfaction Score\";\n\n// Zone sizes (proportion of range)\nconst zone1Size = thresholds[0] - minVal;         // 30: Poor\nconst zone2Size = thresholds[1] - thresholds[0];  // 40: Average\nconst zone3Size = maxVal - thresholds[1];         // 30: Good\n\n// Semantic color exception: red/amber/green = bad/caution/good\nconst COLOR_POOR = \"#AE3030\";\nconst COLOR_AVG  = \"#DDCC77\";\nconst COLOR_GOOD = \"#009E73\";\n\n// Arc math: rotation:180 shifts default start angle by +180°.\n// Default start = -90° (top). After shift: -90°+180° = +90° (bottom).\n// Arc goes clockwise from 90° (bottom, min) → 180° (left) → 270° (top, max).\n// Angle for any fraction = PI/2 + fraction * PI (in radians, canvas convention).\nfunction arcAngle(frac) {\n  return Math.PI / 2 + frac * Math.PI;\n}\n\n// --- Needle Plugin ----------------------------------------------------------\nconst needlePlugin = {\n  id: \"needlePlugin\",\n  afterDatasetsDraw(chart) {\n    const { ctx } = chart;\n    const meta = chart.getDatasetMeta(0);\n    const arc0 = meta.data[0];\n    const { x, y, innerRadius, outerRadius } = arc0.getProps(\n      [\"x\", \"y\", \"innerRadius\", \"outerRadius\"],\n      true\n    );\n\n    const bandWidth = outerRadius - innerRadius;\n    const fraction  = (value - minVal) / (maxVal - minVal);\n    const needleAngle = arcAngle(fraction);\n    const needleLen = outerRadius * 0.80;\n    const hubR      = bandWidth * 0.36;\n    const baseW     = hubR * 0.80;\n    const perp      = needleAngle + Math.PI / 2;\n\n    ctx.save();\n\n    // --- Needle body ---\n    ctx.beginPath();\n    ctx.moveTo(x + baseW * Math.cos(perp),  y + baseW * Math.sin(perp));\n    ctx.lineTo(x + needleLen * Math.cos(needleAngle), y + needleLen * Math.sin(needleAngle));\n    ctx.lineTo(x - baseW * Math.cos(perp),  y - baseW * Math.sin(perp));\n    ctx.closePath();\n    ctx.fillStyle = t.ink;\n    ctx.fill();\n\n    // --- Hub circle ---\n    ctx.beginPath();\n    ctx.arc(x, y, hubR, 0, 2 * Math.PI);\n    ctx.fillStyle = t.ink;\n    ctx.fill();\n\n    // --- Threshold tick marks ---\n    for (const thresh of thresholds) {\n      const tf = (thresh - minVal) / (maxVal - minVal);\n      const ta = arcAngle(tf);\n      const innerR = innerRadius - bandWidth * 0.08;\n      const outerR = outerRadius + bandWidth * 0.08;\n      ctx.beginPath();\n      ctx.moveTo(x + innerR * Math.cos(ta), y + innerR * Math.sin(ta));\n      ctx.lineTo(x + outerR * Math.cos(ta), y + outerR * Math.sin(ta));\n      ctx.strokeStyle = t.pageBg;\n      ctx.lineWidth = Math.max(2, bandWidth * 0.04);\n      ctx.stroke();\n\n      // Threshold label just outside the outer arc edge\n      const labelR = outerRadius + bandWidth * 0.60;\n      const lx = x + labelR * Math.cos(ta);\n      const ly = y + labelR * Math.sin(ta);\n      ctx.font = `${Math.round(outerRadius * 0.075)}px system-ui, sans-serif`;\n      ctx.fillStyle = t.inkSoft;\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"middle\";\n      ctx.fillText(`${thresh}`, lx, ly);\n    }\n\n    // --- Prominent value display (inside opening, below hub) ---\n    const valSize = Math.round(outerRadius * 0.22);\n    ctx.font = `bold ${valSize}px system-ui, sans-serif`;\n    ctx.fillStyle = t.ink;\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"alphabetic\";\n    ctx.fillText(`${value}`, x, y + innerRadius * 0.55);\n\n    // --- Metric label below value ---\n    const lblSize = Math.round(outerRadius * 0.072);\n    ctx.font = `${lblSize}px system-ui, sans-serif`;\n    ctx.fillStyle = t.inkSoft;\n    ctx.textBaseline = \"top\";\n    ctx.fillText(metricLabel, x, y + innerRadius * 0.78);\n\n    ctx.restore();\n  },\n};\n\n// --- Mount ------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"doughnut\",\n  data: {\n    labels: [\"Poor (0–30)\", \"Average (30–70)\", \"Good (70–100)\"],\n    datasets: [{\n      data: [zone1Size, zone2Size, zone3Size],\n      backgroundColor: [COLOR_POOR, COLOR_AVG, COLOR_GOOD],\n      borderWidth: 0,\n    }],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    rotation: 180,\n    circumference: 180,\n    cutout: \"62%\",\n    layout: {\n      padding: { top: 20, bottom: 100, left: 120, right: 120 },\n    },\n    plugins: {\n      title: {\n        display: true,\n        text: \"gauge-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { top: 24, bottom: 20 },\n      },\n      legend: {\n        display: true,\n        position: \"bottom\",\n        labels: {\n          color: t.inkSoft,\n          font: { size: 16 },\n          padding: 24,\n          boxWidth: 18,\n          boxHeight: 18,\n        },\n      },\n      tooltip: { enabled: false },\n    },\n  },\n  plugins: [needlePlugin],\n});\n"}