// Rolling temperature-vs-time chart on a 2D canvas (last 300 s of simulated time). const SPAN = 300; // s (sim) const SERIES = [ { key: 'Tj', label: 'MOSFET Tj', color: '#ff8a65' }, { key: 'Tcoil', label: 'Coil', color: '#4fc3f7' }, { key: 'Tglass', label: 'Glass', color: '#c4b5fd' }, ]; const AXIS_COLOR = '#b4b9d6'; const GRID_COLOR = 'rgba(180, 185, 214, 0.15)'; export class TempChart { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); } /** * @param {{t:number,Tj:number,Tcoil:number,Tglass:number}[]} history 1 sample per sim second * @param {{mosfet:number, coil:number, now?:number, current?:{Tj:number,Tcoil:number,Tglass:number}}} limits */ draw(history, limits) { const { canvas, ctx } = this; const cur = limits.current || history[history.length - 1]; if (cur) { canvas.setAttribute('aria-label', `Temperature history, last 5 minutes. Now: MOSFET junction ${cur.Tj.toFixed(0)} °C, ` + `coil ${cur.Tcoil.toFixed(0)} °C, glass ${cur.Tglass.toFixed(0)} °C. ` + `Limits: MOSFET ${limits.mosfet} °C, coil ${limits.coil} °C.`); } // DPR-aware backing store sized to the CSS box. const w = canvas.clientWidth; const h = canvas.clientHeight; if (!w || !h) return; const dpr = Math.min(window.devicePixelRatio || 1, 2); const bw = Math.round(w * dpr); const bh = Math.round(h * dpr); if (canvas.width !== bw || canvas.height !== bh) { canvas.width = bw; canvas.height = bh; } ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, w, h); const m = { l: 30, r: 6, t: 16, b: 16 }; const pw = w - m.l - m.r; const ph = h - m.t - m.b; if (pw <= 0 || ph <= 0) return; // Axes ranges. const tEnd = limits.now ?? (history.length ? history[history.length - 1].t : 0); const t0 = tEnd - SPAN; let maxT = Math.max(limits.mosfet, limits.coil) + 20; for (const s of history) maxT = Math.max(maxT, s.Tj, s.Tcoil, s.Tglass); // Smallest grid step that keeps y labels ≥ 14 px apart. let yStep = 25; let yMax = Math.ceil(maxT / yStep) * yStep; for (const step of [25, 50, 100, 200]) { yStep = step; yMax = Math.ceil(maxT / step) * step; if ((ph * step) / yMax >= 14) break; } const x = (t) => m.l + ((t - t0) / SPAN) * pw; const y = (T) => m.t + ph - (Math.max(0, T) / yMax) * ph; ctx.font = '10px system-ui, sans-serif'; ctx.lineWidth = 1; // Grid + y labels (°C). ctx.fillStyle = AXIS_COLOR; ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; for (let T = 0; T <= yMax; T += yStep) { const yy = Math.round(y(T)) + 0.5; ctx.strokeStyle = GRID_COLOR; ctx.beginPath(); ctx.moveTo(m.l, yy); ctx.lineTo(m.l + pw, yy); ctx.stroke(); ctx.fillText(String(T), m.l - 4, yy); } ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillText('°C', 2, 2); // X labels: seconds before now. ctx.textAlign = 'center'; ctx.textBaseline = 'top'; for (let ago = SPAN; ago >= 0; ago -= 60) { const xx = x(tEnd - ago); ctx.fillText(ago === 0 ? 'now' : `-${ago} s`, Math.min(Math.max(xx, m.l + 12), m.l + pw - 10), m.t + ph + 3); } // Limit lines (dashed). ctx.save(); ctx.setLineDash([5, 4]); for (const [T, color] of [[limits.mosfet, SERIES[0].color], [limits.coil, SERIES[1].color]]) { const yy = Math.round(y(T)) + 0.5; ctx.strokeStyle = color; ctx.globalAlpha = 0.8; ctx.beginPath(); ctx.moveTo(m.l, yy); ctx.lineTo(m.l + pw, yy); ctx.stroke(); } ctx.restore(); // Series. ctx.save(); ctx.beginPath(); ctx.rect(m.l, m.t - 2, pw, ph + 4); ctx.clip(); ctx.lineWidth = 1.75; for (const s of SERIES) { ctx.strokeStyle = s.color; ctx.beginPath(); let started = false; for (const p of history) { if (p.t < t0 - 1) continue; const xx = x(p.t); const yy = y(p[s.key]); if (started) ctx.lineTo(xx, yy); else { ctx.moveTo(xx, yy); started = true; } } if (started && cur) ctx.lineTo(x(tEnd), y(cur[s.key])); ctx.stroke(); } ctx.restore(); // Legend (top row). ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; let lx = m.l + 4; const ly = 7; for (const s of SERIES) { ctx.strokeStyle = s.color; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(lx, ly); ctx.lineTo(lx + 12, ly); ctx.stroke(); ctx.fillStyle = AXIS_COLOR; ctx.fillText(s.label, lx + 15, ly); lx += 15 + ctx.measureText(s.label).width + 10; } ctx.save(); ctx.setLineDash([4, 3]); ctx.strokeStyle = AXIS_COLOR; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(lx, ly); ctx.lineTo(lx + 12, ly); ctx.stroke(); ctx.restore(); ctx.fillText('limits', lx + 15, ly); } }