| 8 | // doesn't draw a stray chord from the implicit origin to the arc start. |
| 9 | |
| 10 | function paint(ctx, w, h) { |
| 11 | ctx.fillStyle = theme.groupbox_background; |
| 12 | ctx.fillRect(0, 0, w, h); |
| 13 | |
| 14 | if (datasets.length === 0) return; |
| 15 | |
| 16 | // Lay rings out on a grid that prefers near-square cells. |
| 17 | const n = datasets.length; |
| 18 | const cols = Math.min(n, Math.max(1, Math.round(Math.sqrt(n * w / h)))); |
| 19 | const rows = Math.ceil(n / cols); |
| 20 | const pad = 12; |
| 21 | const cw = (w - pad * 2) / cols; |
| 22 | const ch = (h - pad * 2) / rows; |
| 23 | |
| 24 | for (let i = 0; i < n; ++i) { |
| 25 | const row = Math.floor(i / cols); |
| 26 | const col = i % cols; |
| 27 | const cx = pad + cw * col + cw * 0.5; |
| 28 | const cy = pad + ch * row + ch * 0.5 - 6; |
| 29 | const rr = Math.max(10, Math.min(cw, ch) * 0.38 - 4); |
| 30 | const ds = datasets[i]; |
| 31 | const v = Number.isFinite(ds.value) ? ds.value : 0; |
| 32 | const span = (ds.max - ds.min) || 1; |
| 33 | const norm = Math.max(0, Math.min(1, (v - ds.min) / span)); |
| 34 | |
| 35 | // Track ring (full circle, stroke). |
| 36 | ctx.strokeStyle = theme.mid; |
| 37 | ctx.lineWidth = 12; |
| 38 | ctx.lineCap = "butt"; |
| 39 | ctx.beginPath(); |
| 40 | ctx.moveTo(cx + rr, cy); |
| 41 | ctx.arc(cx, cy, rr, 0, Math.PI * 2); |
| 42 | ctx.stroke(); |
| 43 | |
| 44 | // Value arc. |
| 45 | if (norm > 0) { |
| 46 | const startA = -Math.PI * 0.5; |
| 47 | const endA = startA + Math.PI * 2 * norm; |
| 48 | ctx.strokeStyle = theme.highlight; |
| 49 | ctx.lineCap = "round"; |
| 50 | ctx.beginPath(); |
| 51 | ctx.moveTo(cx + Math.cos(startA) * rr, cy + Math.sin(startA) * rr); |
| 52 | ctx.arc(cx, cy, rr, startA, endA); |
| 53 | ctx.stroke(); |
| 54 | ctx.lineCap = "butt"; |
| 55 | } |
| 56 | |
| 57 | // Centre value (% of range) and small title underneath. |
| 58 | ctx.fillStyle = theme.text; |
| 59 | ctx.font = "bold 18px sans-serif"; |
| 60 | ctx.textAlign = "center"; |
| 61 | ctx.textBaseline = "middle"; |
| 62 | ctx.fillText((norm * 100).toFixed(0) + "%", cx, cy); |
| 63 | |
| 64 | ctx.fillStyle = theme.pane_section_label || theme.text; |
| 65 | ctx.font = "10px sans-serif"; |
| 66 | ctx.textBaseline = "alphabetic"; |
| 67 | const title = (ds.title || "").substring(0, 16); |