{
const div = document.createElement("div");
div.style.width = "100%";
div.style.height = "480px";
div.style.touchAction = "none";
div.setAttribute("data-prevent-swipe", "");
const traces = [{
x: data3D.x, y: data3D.y, z: data3D.z,
mode: "markers",
type: "scatter3d",
marker: { size: 2.5, color: "steelblue", opacity: 0.6 },
name: "Observations"
}];
// Center mark (always shown)
traces.push({
x: [data3D.means[0]], y: [data3D.means[1]], z: [data3D.means[2]],
mode: "markers",
type: "scatter3d",
marker: { size: 6, color: "red", symbol: "cross", opacity: 1 },
name: "Center",
showlegend: true
});
if (show3DEigen) {
const colors = ["#D55E00", "#0072B2", "#009E73"];
const labels = ["PC1", "PC2", "PC3"];
const widths = [6, 4, 3];
const vectors = data3D.eig.vectors; // already column-extracted & sorted ↓ λ
const values = data3D.eig.values;
const m = data3D.means;
const n = data3D.x.length;
const dot = (a, b) => a.reduce((s, ai, i) => s + ai * b[i], 0);
// Center the raw data once
const centered = Array.from({ length: n }, (_, i) => [
data3D.x[i] - m[0],
data3D.y[i] - m[1],
data3D.z[i] - m[2]
]);
const total = values.reduce((a, b) => a + b, 0);
for (let k = 0; k < 3; k++) {
const v = vectors[k];
// PC score for each observation: how far along eigenvector k
const scores = centered.map(p => dot(p, v));
// Sort by score so Plotly draws a continuous line (not a zigzag)
const idx = scores.map((s, i) => ({ s, i })).sort((a, b) => a.s - b.s).map(d => d.i);
// Project back into original 3D space (R: PC1_DRESS = center + score * loading)
traces.push({
x: idx.map(i => m[0] + scores[i] * v[0]),
y: idx.map(i => m[1] + scores[i] * v[1]),
z: idx.map(i => m[2] + scores[i] * v[2]),
mode: "lines",
type: "scatter3d",
line: { color: colors[k], width: widths[k] },
name: `${labels[k]} (λ=${values[k].toFixed(1)}, ${(100 * values[k] / total).toFixed(0)}%)`
});
}
}
const layout = {
autosize: true,
margin: { l: 0, r: 0, t: 10, b: 0 },
scene: {
aspectmode: "manual",
aspectratio: { x: 1.2, y: 1.0, z: 0.8 },
xaxis: { title: "TV ($k)" },
yaxis: { title: "radio ($k)" },
zaxis: { title: "sales" },
camera: { eye: { x: 1.5, y: 1.5, z: 1.0 } }
},
legend: { x: 0, y: 1, font: { size: 11 } },
paper_bgcolor: "rgba(0,0,0,0)",
plot_bgcolor: "rgba(0,0,0,0)"
};
Plotly.newPlot(div, traces, layout, { responsive: true, displayModeBar: false, scrollZoom: true });
return div;
}