Spaces:
Running
Running
Download src/chart.js from AK51/PlasmaToroid: direct link, hf CLI and curl.
- Browser
- Download file 5.02 kB
-
https://huggingface.co/spaces/AK51/PlasmaToroid/resolve/main/src/chart.js
- Command line
-
hf download hf://spaces/AK51/PlasmaToroid/src/chart.js
-
curl -L -o chart.js https://huggingface.co/spaces/AK51/PlasmaToroid/resolve/main/src/chart.js
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); | |
| } | |
| } | |