{
const c = pcCanvas, fg = exFg(), W = 760, H = 580, cols = ["#1f77b4", "#ff7f0e", "#2ca02c"];
const {X, values, vectors, scores, total, species} = pca, feats = irisRaw.features;
c.clearRect(0, 0, W, H); c.font = exFont;
const get = (key, i) => key.startsWith("pc") ? scores[i][+key[2]] : X[i][+key[1]];
const label = key => key.startsWith("pc") ? `PC ${+key[2] + 1}` : feats[+key[1]] + (pcStd ? " (z-scored)" : " (cm, centred)");
const xs = X.map((_, i) => get(pcX, i)), ys = X.map((_, i) => get(pcY, i));
const lim = Math.max(...xs.map(Math.abs), ...ys.map(Math.abs)) * 1.1;
const sP = {x0: 90, y0: 12, s: 360};
const px = v => sP.x0 + sP.s * (v + lim) / (2 * lim), py = v => sP.y0 + sP.s * (1 - (v + lim) / (2 * lim));
axisBox(c, sP.x0, sP.y0, sP.s, sP.s);
c.strokeStyle = "#ccc"; c.lineWidth = 1; c.beginPath(); c.moveTo(px(0), sP.y0); c.lineTo(px(0), sP.y0 + sP.s); c.moveTo(sP.x0, py(0)); c.lineTo(sP.x0 + sP.s, py(0)); c.stroke();
// principal axes drawn in the plane of two raw variables
if (!pcX.startsWith("pc") && !pcY.startsWith("pc")) {
const i = +pcX[1], j = +pcY[1];
for (const [k, col] of [[0, "#d62728"], [1, "#9467bd"]]) {
const e = vectors[k], L = 2 * Math.sqrt(values[k]);
c.strokeStyle = col; c.lineWidth = 3; c.beginPath(); c.moveTo(px(-L * e[i]), py(-L * e[j])); c.lineTo(px(L * e[i]), py(L * e[j])); c.stroke();
c.fillStyle = col; c.textAlign = "left"; c.fillText(`PC ${k + 1}`, px(L * e[i]) + 6, py(L * e[j]) + 5);
}
}
X.forEach((_, i) => { c.fillStyle = pcColour ? cols[species[i]] : "#555"; c.globalAlpha = 0.6; c.beginPath(); c.arc(px(xs[i]), py(ys[i]), 4.5, 0, 2 * Math.PI); c.fill(); });
c.globalAlpha = 1; c.fillStyle = fg; c.textAlign = "center";
c.fillText(label(pcX), sP.x0 + sP.s / 2, sP.y0 + sP.s + 24);
c.fillText((-lim).toFixed(1), sP.x0, sP.y0 + sP.s + 44); c.fillText(lim.toFixed(1), sP.x0 + sP.s, sP.y0 + sP.s + 44);
c.save(); c.translate(sP.x0 - 60, sP.y0 + sP.s / 2); c.rotate(-Math.PI / 2); c.fillText(label(pcY), 0, 0); c.restore();
c.textAlign = "right"; c.fillText(lim.toFixed(1), sP.x0 - 8, sP.y0 + 6); c.fillText((-lim).toFixed(1), sP.x0 - 8, sP.y0 + sP.s + 2);
if (pcColour) irisRaw.species.forEach((s, k) => { c.fillStyle = cols[k]; c.textAlign = "left"; c.fillText(s, sP.x0 + 10, sP.y0 + 22 + 22 * k); });
// explained variance bars, right
const bP = {x0: 520, y0: 12, w: 220, h: 150};
axisBox(c, bP.x0, bP.y0, bP.w, bP.h);
values.forEach((l, k) => {
const frac = l / total, bw = bP.w / 4 * 0.7, bx = bP.x0 + bP.w * (k + 0.15) / 4;
c.fillStyle = "#1f77b4"; c.fillRect(bx, bP.y0 + bP.h * (1 - frac), bw, bP.h * frac);
c.fillStyle = fg; c.textAlign = "center"; c.fillText(`${(100 * frac).toFixed(0)}%`, bx + bw / 2, bP.y0 + bP.h * (1 - frac) - 6);
c.fillText(`PC ${k + 1}`, bx + bw / 2, bP.y0 + bP.h + 22);
});
c.fillStyle = fg; c.textAlign = "center"; c.fillText("explained variance", bP.x0 + bP.w / 2, bP.y0 + bP.h + 44);
// loadings table, full width below the scatter
const tY = 470, colX = j => 250 + 125 * j;
c.fillStyle = fg; c.textAlign = "left"; c.fillText("loadings: components of each eₖ", 90, tY);
c.font = "14px " + getComputedStyle(document.body).fontFamily;
feats.forEach((f, j) => { c.textAlign = "center"; c.fillText(f, colX(j), tY + 24); });
values.forEach((_, k) => {
c.textAlign = "left"; c.fillText(`PC ${k + 1}`, 90, tY + 46 + 20 * k);
vectors[k].forEach((e, j) => { c.textAlign = "center"; c.fillText(e.toFixed(2), colX(j), tY + 46 + 20 * k); });
});
return c.canvas;
}