{"spec_id":"gauge-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// gauge-basic: Basic Gauge Chart\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 93/100 | Created: 2026-06-30\n//# anyplot-orientation: square\n\n// anyplot.ai\n// gauge-basic: Basic Gauge Chart\n// Library: d3 7.9.0 | JavaScript 22\n// Quality: pending | Created: 2026-06-30\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// Project completion: 68% — just entered the \"Ahead\" zone\nconst value = 68;\nconst minValue = 0;\nconst maxValue = 100;\nconst thresholds = [33, 67];\n\n// Geometry\nconst cx = width / 2;\nconst cy = height * 0.60;\nconst outerR = Math.min(width, height) * 0.33;\nconst innerR = outerR * 0.60;\n\n// D3 arc angles: 0 = 12 o'clock, clockwise\n// Semi-circle: left (9 o'clock, -π/2) → top → right (3 o'clock, +π/2)\nconst startAngle = -Math.PI / 2;\nconst endAngle = Math.PI / 2;\n\nconst angleScale = d3.scaleLinear()\n  .domain([minValue, maxValue])\n  .range([startAngle, endAngle]);\n\n// Zones use Imprint semantic tokens: matte red / amber / brand green\nconst zones = [\n  { start: minValue, end: thresholds[0], color: t.palette[4] },\n  { start: thresholds[0], end: thresholds[1], color: t.amber },\n  { start: thresholds[1], end: maxValue, color: t.palette[0] },\n];\n\nconst arcGen = d3.arc().innerRadius(innerR).outerRadius(outerR);\n\n// SVG\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\n\nsvg.style(\"font-family\", \"'Inter', 'Helvetica Neue', Arial, sans-serif\");\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${cx},${cy})`);\n\n// Zone arcs\nzones.forEach(zone => {\n  g.append(\"path\")\n    .attr(\"d\", arcGen({\n      startAngle: angleScale(zone.start),\n      endAngle: angleScale(zone.end),\n    }))\n    .attr(\"fill\", zone.color)\n    .attr(\"opacity\", 0.9);\n});\n\n// Outer ring guide\nconst outerRingArc = d3.arc().innerRadius(outerR + 5).outerRadius(outerR + 11);\ng.append(\"path\")\n  .attr(\"d\", outerRingArc({ startAngle, endAngle }))\n  .attr(\"fill\", t.inkSoft)\n  .attr(\"opacity\", 0.25);\n\n// Inner background fill — clean donut center behind the needle\nconst innerFillArc = d3.arc().innerRadius(0).outerRadius(innerR);\ng.append(\"path\")\n  .attr(\"d\", innerFillArc({ startAngle, endAngle }))\n  .attr(\"fill\", t.pageBg);\n\n// Tick marks and percentage labels\n[0, 25, 50, 75, 100].forEach(v => {\n  const a = angleScale(v);\n  const sa = Math.sin(a);\n  const ca = Math.cos(a);\n\n  g.append(\"line\")\n    .attr(\"x1\", sa * (outerR + 16)).attr(\"y1\", -ca * (outerR + 16))\n    .attr(\"x2\", sa * (outerR + 32)).attr(\"y2\", -ca * (outerR + 32))\n    .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 3).attr(\"opacity\", 0.6);\n\n  const lx = sa * (outerR + 58);\n  const ly = -ca * (outerR + 58);\n  g.append(\"text\")\n    .attr(\"x\", lx).attr(\"y\", ly)\n    .attr(\"text-anchor\", sa < -0.3 ? \"end\" : sa > 0.3 ? \"start\" : \"middle\")\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"22px\")\n    .text(`${v}%`);\n});\n\n// Needle — triangle pointing from center toward the arc at the current value\nconst needleAngle = angleScale(value);\nconst nsa = Math.sin(needleAngle);\nconst nca = Math.cos(needleAngle);\nconst needleLen = (innerR + outerR) * 0.52;\nconst halfBase = 14;\n// Direction vector: (nsa, -nca); perpendicular: (nca, nsa)\nconst tipX = nsa * needleLen;\nconst tipY = -nca * needleLen;\nconst bx = nca * halfBase;\nconst by = nsa * halfBase;\n\ng.append(\"path\")\n  .attr(\"d\", `M ${bx},${by} L ${tipX},${tipY} L ${-bx},${-by} Z`)\n  .attr(\"fill\", t.ink);\n\n// Pivot circles\ng.append(\"circle\").attr(\"r\", 22).attr(\"fill\", t.ink);\ng.append(\"circle\").attr(\"r\", 9).attr(\"fill\", t.pageBg);\n\n// Value display\nsvg.append(\"text\")\n  .attr(\"x\", cx).attr(\"y\", cy + 68)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"hanging\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"88px\")\n  .style(\"font-weight\", \"700\")\n  .text(`${value}%`);\n\n// Metric label\nsvg.append(\"text\")\n  .attr(\"x\", cx).attr(\"y\", cy + 192)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"hanging\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"30px\")\n  .style(\"font-weight\", \"500\")\n  .text(\"Project Completion\");\n\n// Zone legend\nconst legendDefs = [\n  { color: t.palette[4], label: \"Behind (<33%)\" },\n  { color: t.amber, label: \"On Track (33–67%)\" },\n  { color: t.palette[0], label: \"Ahead (>67%)\" },\n];\nconst lItemW = 230;\nconst lStartX = cx - (lItemW * (legendDefs.length - 1)) / 2;\nconst lY = cy + 312;\n\nlegendDefs.forEach((item, i) => {\n  const lx = lStartX + i * lItemW;\n  svg.append(\"circle\")\n    .attr(\"cx\", lx).attr(\"cy\", lY)\n    .attr(\"r\", 11).attr(\"fill\", item.color);\n  svg.append(\"text\")\n    .attr(\"x\", lx + 20).attr(\"y\", lY)\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"22px\")\n    .text(item.label);\n});\n\n// Title\nsvg.append(\"text\")\n  .attr(\"x\", cx).attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"gauge-basic · javascript · d3 · anyplot.ai\");\n"}