Spaces:
Runtime error
Runtime error
| /* Small canvas charts. One job: draw the density trajectory each candidate | |
| * strategy produced, so the comparison table has a shape as well as numbers. */ | |
| const SERIES_COLOURS = [ | |
| '#ff3222', '#35c8f5', '#12d38a', '#ffa415', | |
| '#a78bfa', '#f472b6', '#94a3b8', '#facc15', | |
| ]; | |
| export function drawStrategyChart(canvas, result, legendEl, highlightId) { | |
| const ctx = canvas.getContext('2d'); | |
| const dpr = Math.min(window.devicePixelRatio || 1, 2); | |
| const rect = canvas.getBoundingClientRect(); | |
| if (!rect.width) return; | |
| canvas.width = Math.round(rect.width * dpr); | |
| canvas.height = Math.round(rect.height * dpr); | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| const W = rect.width, H = rect.height; | |
| ctx.clearRect(0, 0, W, H); | |
| const strategies = (result.strategies || []).filter(s => s.series?.density?.length); | |
| if (!strategies.length) return; | |
| const pad = { l: 30, r: 8, t: 10, b: 20 }; | |
| const maxT = Math.max(...strategies.map(s => s.series.t.at(-1) || 1)); | |
| const critical = result.bottleneck_critical_density | |
| ?? Math.max(...strategies.flatMap(s => s.series.density)) * 0.85; | |
| const maxD = Math.max( | |
| critical * 1.12, | |
| ...strategies.flatMap(s => s.series.density)) * 1.06; | |
| const X = t => pad.l + (t / maxT) * (W - pad.l - pad.r); | |
| const Y = d => H - pad.b - (d / maxD) * (H - pad.t - pad.b); | |
| // grid | |
| ctx.strokeStyle = 'rgba(255,255,255,.055)'; | |
| ctx.lineWidth = 1; | |
| ctx.font = '9px ui-monospace, monospace'; | |
| ctx.fillStyle = '#64708a'; | |
| ctx.textAlign = 'right'; | |
| ctx.textBaseline = 'middle'; | |
| const steps = 4; | |
| for (let i = 0; i <= steps; i++) { | |
| const d = (maxD / steps) * i; | |
| const y = Y(d); | |
| ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(W - pad.r, y); ctx.stroke(); | |
| ctx.fillText(d.toFixed(1), pad.l - 6, y); | |
| } | |
| // critical threshold | |
| if (result.bottleneck_critical_density) { | |
| ctx.save(); | |
| ctx.setLineDash([4, 4]); | |
| ctx.strokeStyle = 'rgba(255,50,34,.55)'; | |
| ctx.beginPath(); | |
| ctx.moveTo(pad.l, Y(result.bottleneck_critical_density)); | |
| ctx.lineTo(W - pad.r, Y(result.bottleneck_critical_density)); | |
| ctx.stroke(); | |
| ctx.restore(); | |
| ctx.textAlign = 'left'; | |
| ctx.fillStyle = 'rgba(255,80,66,.8)'; | |
| ctx.fillText('critical', pad.l + 4, Y(result.bottleneck_critical_density) - 7); | |
| } | |
| // x axis labels | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'top'; | |
| ctx.fillStyle = '#64708a'; | |
| for (let i = 0; i <= 4; i++) { | |
| const t = (maxT / 4) * i; | |
| ctx.fillText(`+${Math.round(t)}s`, X(t), H - pad.b + 5); | |
| } | |
| strategies.forEach((s, i) => { | |
| const colour = s.id === 'no_action' ? '#ff3222' : SERIES_COLOURS[(i + 1) % SERIES_COLOURS.length]; | |
| const emphasised = highlightId ? s.id === highlightId : (s.recommended || s.id === 'no_action'); | |
| ctx.beginPath(); | |
| s.series.t.forEach((t, k) => { | |
| const x = X(t), y = Y(s.series.density[k]); | |
| k ? ctx.lineTo(x, y) : ctx.moveTo(x, y); | |
| }); | |
| ctx.strokeStyle = colour; | |
| ctx.globalAlpha = emphasised ? 1 : 0.3; | |
| ctx.lineWidth = emphasised ? 2.1 : 1.1; | |
| ctx.lineJoin = 'round'; | |
| ctx.stroke(); | |
| ctx.globalAlpha = 1; | |
| s._colour = colour; | |
| }); | |
| if (legendEl) { | |
| legendEl.innerHTML = strategies.map(s => | |
| `<span><i style="background:${s._colour}"></i>${s.label}</span>`).join(''); | |
| } | |
| } | |