/** GMRES relative residual against iteration, on a log scale. */ export function drawConvergence(canvas: HTMLCanvasElement, series: { label: string; res: number[]; color: string }[], eps: number) { const dpr = window.devicePixelRatio || 1; const W = canvas.clientWidth || 300; const H = canvas.clientHeight || 140; canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr); const ctx = canvas.getContext('2d')!; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, W, H); const style = getComputedStyle(canvas); const ink = style.getPropertyValue('--ink-2').trim() || '#888'; const line = style.getPropertyValue('--line').trim() || '#ccc'; const L = 44; const R = 8; const T = 8; const B = 22; const nmax = Math.max(10, ...series.map((s) => s.res.length)); const lo = Math.floor(Math.log10(Math.min(eps, ...series.flatMap((s) => s.res)) * 0.5)); const hi = 0; const X = (i: number) => L + ((W - L - R) * i) / nmax; const Y = (r: number) => T + ((H - T - B) * (hi - Math.log10(r))) / (hi - lo); ctx.font = '11px system-ui, sans-serif'; ctx.fillStyle = ink; ctx.strokeStyle = line; ctx.lineWidth = 1; for (let e = lo; e <= hi; e++) { if ((e - lo) % Math.max(1, Math.ceil((hi - lo) / 5)) !== 0) continue; const y = Y(10 ** e); ctx.beginPath(); ctx.moveTo(L, y); ctx.lineTo(W - R, y); ctx.stroke(); ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; ctx.fillText(`1e${e}`, L - 4, y); } ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText('GMRES iteration', (L + W - R) / 2, H - B + 6); // tolerance ctx.setLineDash([4, 3]); ctx.beginPath(); ctx.moveTo(L, Y(eps)); ctx.lineTo(W - R, Y(eps)); ctx.stroke(); ctx.setLineDash([]); for (const s of series) { if (!s.res.length) continue; ctx.strokeStyle = s.color; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(X(0), Y(1)); s.res.forEach((r, i) => ctx.lineTo(X(i + 1), Y(Math.max(r, 10 ** lo)))); ctx.stroke(); } let ly = T + 4; for (const s of series) { if (!s.res.length) continue; ctx.fillStyle = s.color; ctx.fillRect(W - R - 110, ly + 3, 10, 3); ctx.fillStyle = ink; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillText(s.label, W - R - 96, ly - 2); ly += 14; } }