PlasmaToroid / src /chart.js
AK51's picture
Upload 195 files
e0276cc verified
Raw History Blame Contribute Delete
5.02 kB
// 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);
}
}