{"spec_id":"gauge-activity-rings","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// gauge-activity-rings: Activity Rings Progress Chart\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-14\n//# anyplot-orientation: square\n// anyplot.ai\n// gauge-activity-rings: Activity Rings Progress Chart\n// Library: Highcharts 12.6.0 | Node 22\n// License: Highcharts — commercial license, free for non-commercial use (highcharts.com/license)\n// Quality: pending | Created: 2026-06-14\n\nconst t = window.ANYPLOT_TOKENS;\nconst isDark = window.ANYPLOT_THEME === \"dark\";\n\n// --- Data: Daily fitness summary -------------------------------------------\n// Outer ring first (Move), then Exercise, then Stand (innermost)\nconst rings = [\n  { metric: \"Move\",     value: 420, goal: 600, unit: \"kcal\", color: t.palette[0] },\n  { metric: \"Exercise\", value: 25,  goal: 30,  unit: \"min\",  color: t.palette[1] },\n  { metric: \"Stand\",    value: 9,   goal: 12,  unit: \"hr\",   color: t.palette[2] },\n];\n\n// Dark mode: low-opacity hue over near-black background disappears at 18% opacity;\n// 25% keeps the track visible without overshadowing the progress arc.\nconst trackOpacity = isDark ? 0.25 : 0.18;\n\nlet rendererEls = [];\n\nfunction clearEls() {\n  rendererEls.forEach(el => { try { el.destroy(); } catch (_) {} });\n  rendererEls = [];\n}\n\nfunction drawRings(chart) {\n  clearEls();\n  const ren = chart.renderer;\n  const W = chart.chartWidth;\n  const H = chart.chartHeight;\n  const cx = W / 2;\n  const plotTop = chart.plotTop;\n\n  // Ring geometry — radius limited by canvas width\n  const maxR = Math.round(Math.min(W * 0.265, (H - plotTop - 100) * 0.45));\n  const strokeW = Math.round(maxR * 0.138);\n  const ringGap = Math.round(strokeW * 0.30);\n\n  // Center the rings+legend block vertically between plotTop and the canvas bottom\n  // so equal whitespace appears above and below the visual content.\n  const ringVisH = 2 * (maxR + Math.round(strokeW * 0.5));\n  const totalVisH = ringVisH + 28 + 36; // 28px gap + ~36px for two legend lines\n  const topOffset = Math.max(0, (H - plotTop - totalVisH) / 2);\n  const cy = plotTop + topOffset + maxR + Math.round(strokeW * 0.5);\n\n  // Legend sits directly below the outermost ring\n  const legY = cy + maxR + Math.round(strokeW * 0.5) + 28;\n\n  rings.forEach((ring, i) => {\n    const r = maxR - i * (strokeW + ringGap);\n    // Clamp fraction to avoid degenerate full-circle arc edge case\n    const frac = Math.min(ring.value / ring.goal, 0.9999);\n\n    const startA = -Math.PI / 2; // 12 o'clock\n    const endA = startA + frac * 2 * Math.PI;\n    const x1 = cx + r * Math.cos(startA);\n    const y1 = cy + r * Math.sin(startA);\n    const x2 = cx + r * Math.cos(endA);\n    const y2 = cy + r * Math.sin(endA);\n    const largeArc = frac > 0.5 ? 1 : 0;\n\n    // Faint background track (full circle)\n    rendererEls.push(\n      ren.circle(cx, cy, r).attr({\n        fill: \"none\",\n        stroke: ring.color,\n        \"stroke-width\": strokeW,\n        \"stroke-opacity\": trackOpacity,\n        zIndex: 3,\n      }).add()\n    );\n\n    // Progress arc with rounded end caps — the iconic activity-ring look\n    rendererEls.push(\n      ren.path([\"M\", x1, y1, \"A\", r, r, 0, largeArc, 1, x2, y2]).attr({\n        fill: \"none\",\n        stroke: ring.color,\n        \"stroke-width\": strokeW,\n        \"stroke-linecap\": \"round\",\n        zIndex: 4,\n      }).add()\n    );\n  });\n\n  // Center label: primary metric (Move) percentage\n  const movePct = Math.round((rings[0].value / rings[0].goal) * 100);\n  rendererEls.push(\n    ren.text(movePct + \"%\", cx, cy + 14)\n      .attr({ align: \"center\", zIndex: 5 })\n      .css({ color: t.ink, fontSize: \"46px\", fontWeight: \"700\" })\n      .add()\n  );\n  rendererEls.push(\n    ren.text(\"Move goal\", cx, cy + 38)\n      .attr({ align: \"center\", zIndex: 5 })\n      .css({ color: t.inkSoft, fontSize: \"14px\" })\n      .add()\n  );\n\n  // Legend: three columns, centered horizontally\n  const ITEM_W = 158;\n  const legStartX = (W - rings.length * ITEM_W) / 2;\n\n  rings.forEach((ring, i) => {\n    const lx = legStartX + i * ITEM_W;\n    const pct = Math.round((ring.value / ring.goal) * 100);\n\n    rendererEls.push(\n      ren.circle(lx + 8, legY + 7, 7).attr({ fill: ring.color, zIndex: 5 }).add()\n    );\n    rendererEls.push(\n      ren.text(ring.metric, lx + 22, legY)\n        .attr({ align: \"left\", zIndex: 5 })\n        .css({ color: t.inkSoft, fontSize: \"14px\", fontWeight: \"600\" })\n        .add()\n    );\n    rendererEls.push(\n      ren.text(ring.value + \"/\" + ring.goal + \" \" + ring.unit + \"  ·  \" + pct + \"%\", lx + 22, legY + 18)\n        .attr({ align: \"left\", zIndex: 5 })\n        .css({ color: t.inkSoft, fontSize: \"13px\" })\n        .add()\n    );\n  });\n}\n\n// --- Chart -----------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      render: function () { drawRings(this); },\n    },\n  },\n  credits:  { enabled: false },\n  colors:   t.palette,\n  title: {\n    text: \"gauge-activity-rings · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Daily fitness goal completion\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: { visible: false },\n  yAxis: { visible: false },\n  legend: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  series: [],\n});\n"}