{"spec_id":"gauge-activity-rings","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// gauge-activity-rings: Activity Rings Progress Chart\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 92/100 | Created: 2026-06-14\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst isDark = window.ANYPLOT_THEME === \"dark\";\n\n// --- Helpers ---\nfunction hexToRgba(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// --- Data (daily fitness summary) ---\nconst rings = [\n  { label: \"Move\",     value: 420, goal: 600, unit: \"kcal\" },\n  { label: \"Exercise\", value: 38,  goal: 30,  unit: \"min\"  },\n  { label: \"Stand\",    value: 9,   goal: 12,  unit: \"hr\"   },\n];\n\n// Theme-adaptive track opacity: dark bg absorbs translucent colours; increase alpha\nconst trackAlpha = isDark ? 0.28 : 0.15;\n\nconst datasets = rings.map((ring, i) => {\n  const fraction = Math.min(ring.value / ring.goal, 1.0);\n  const color = t.palette[i];\n  return {\n    label: ring.label,\n    data: [fraction, 1 - fraction],\n    backgroundColor: [color, hexToRgba(color, trackAlpha)],\n    borderWidth: 0,\n    borderRadius: (ctx) => (ctx.dataIndex === 0 ? 32 : 0),\n  };\n});\n\nconst avgPct = Math.round(\n  rings.reduce((sum, r) => sum + Math.min(r.value / r.goal, 1), 0) / rings.length * 100\n);\n\n// --- Mount ---\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Plugin: center summary + glowing progress-tip dots ---\nconst centerTextPlugin = {\n  id: \"centerText\",\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const cx = (chartArea.left + chartArea.right) / 2;\n    const cy = (chartArea.top + chartArea.bottom) / 2;\n\n    // Soft radial glow behind the percentage — lifts the focal point\n    ctx.save();\n    const glowR = 90;\n    const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, glowR);\n    grad.addColorStop(0, hexToRgba(t.palette[0], isDark ? 0.12 : 0.06));\n    grad.addColorStop(1, \"rgba(0,0,0,0)\");\n    ctx.fillStyle = grad;\n    ctx.beginPath();\n    ctx.arc(cx, cy, glowR, 0, Math.PI * 2);\n    ctx.fill();\n    ctx.restore();\n\n    // Center percentage and sub-label\n    ctx.save();\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    ctx.font = `bold 62px system-ui, sans-serif`;\n    ctx.fillStyle = t.ink;\n    ctx.fillText(`${avgPct}%`, cx, cy - 28);\n    ctx.font = `20px system-ui, sans-serif`;\n    ctx.fillStyle = t.inkSoft;\n    ctx.fillText(\"avg. complete\", cx, cy + 30);\n    ctx.restore();\n\n    // Glowing dot at each ring's progress endpoint (skips full/over-100% rings)\n    rings.forEach((ring, i) => {\n      if (ring.value / ring.goal >= 1.0) return;\n      const meta = chart.getDatasetMeta(i);\n      const arc = meta.data[0];\n      if (!arc) return;\n      const dotR = (arc.outerRadius - arc.innerRadius) / 2;\n      const midR = (arc.outerRadius + arc.innerRadius) / 2;\n      const tipX = arc.x + midR * Math.cos(arc.endAngle);\n      const tipY = arc.y + midR * Math.sin(arc.endAngle);\n      ctx.save();\n      ctx.beginPath();\n      ctx.arc(tipX, tipY, dotR, 0, Math.PI * 2);\n      ctx.fillStyle = t.palette[i];\n      ctx.shadowColor = hexToRgba(t.palette[i], 0.55);\n      ctx.shadowBlur = 14;\n      ctx.fill();\n      ctx.restore();\n    });\n  },\n};\n\n// --- Chart ---\nnew Chart(canvas, {\n  type: \"doughnut\",\n  plugins: [centerTextPlugin],\n  data: {\n    labels: [\"Progress\", \"Remaining\"],\n    datasets,\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    rotation: -90,\n    circumference: 360,\n    cutout: \"42%\",\n    plugins: {\n      title: {\n        display: true,\n        text: \"gauge-activity-rings · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { top: 16, bottom: 16 },\n      },\n      legend: {\n        display: true,\n        position: \"bottom\",\n        labels: {\n          color: t.ink,\n          font: { size: 17 },\n          padding: 28,\n          usePointStyle: true,\n          pointStyle: \"circle\",\n          generateLabels() {\n            return rings.map((ring, i) => {\n              const fraction = ring.value / ring.goal;\n              const pct = Math.round(fraction * 100);\n              const prefix = fraction > 1 ? \">\" : \"\";\n              return {\n                text: `${ring.label}  ·  ${ring.value} / ${ring.goal} ${ring.unit}  ·  ${prefix}${pct}%`,\n                fillStyle: t.palette[i],\n                strokeStyle: \"transparent\",\n                lineWidth: 0,\n                hidden: false,\n                datasetIndex: i,\n                pointStyle: \"circle\",\n              };\n            });\n          },\n        },\n      },\n      tooltip: { enabled: false },\n    },\n  },\n});\n"}