{
const W = 680, H = 260;
const padL = 50, padR = 20, padT = 30, padB = 50;
const chartW = W - padL - padR;
const chartH = H - padT - padB;
const data = [12, 15, 18, 20, 24, 31, 42, 55, 847, 1203];
const mean = data.reduce((a, b) => a + b) / data.length;
const std = Math.sqrt(data.map(v => (v - mean) ** 2).reduce((a, b) => a + b) / data.length);
const zscores = data.map(v => (v - mean) / std);
const xMin = -1, xMax = 5;
const xScale = v => padL + ((v - xMin) / (xMax - xMin)) * chartW;
const mid = padT + chartH / 2;
const threshold = 2;
const svg = d3.create("svg")
.attr("width", W).attr("height", H)
.style("font-family", "monospace")
.style("background", "transparent");
// Normal zone
svg.append("rect")
.attr("x", xScale(-1)).attr("y", padT + 20)
.attr("width", xScale(threshold) - xScale(-1))
.attr("height", chartH - 40)
.attr("fill", "#89b4fa")
.attr("opacity", 0.12);
// Outlier zone
svg.append("rect")
.attr("x", xScale(threshold)).attr("y", padT + 20)
.attr("width", xScale(xMax) - xScale(threshold))
.attr("height", chartH - 40)
.attr("fill", "#f38ba8")
.attr("opacity", 0.12);
// Threshold line
svg.append("line")
.attr("x1", xScale(threshold)).attr("x2", xScale(threshold))
.attr("y1", padT + 10).attr("y2", padT + chartH)
.attr("stroke", "#f38ba8")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,3");
svg.append("text")
.attr("x", xScale(threshold) + 4).attr("y", padT + 20)
.attr("fill", "#f38ba8")
.attr("font-size", 11)
.text("z = 2");
// Zero line
svg.append("line")
.attr("x1", xScale(0)).attr("x2", xScale(0))
.attr("y1", padT + 40).attr("y2", padT + chartH - 20)
.attr("stroke", "#a6e3a1")
.attr("stroke-width", 1.5)
.attr("stroke-dasharray", "3,3");
svg.append("text")
.attr("x", xScale(0)).attr("y", padT + 36)
.attr("text-anchor", "middle")
.attr("fill", "#a6e3a1")
.attr("font-size", 11)
.text("mean");
// Points
const labels = ["Hollow Knight","Celeste","Hades","Dead Cells","Cuphead",
"Shovel Knight","Ori","Axiom Verge","Balatro","StS2"];
zscores.forEach((z, i) => {
const isOut = z > threshold;
svg.append("circle")
.attr("cx", xScale(z)).attr("cy", mid)
.attr("r", 6)
.attr("fill", isOut ? "#f38ba8" : "#89b4fa")
.attr("opacity", 0.9);
if (isOut) {
svg.append("text")
.attr("x", xScale(z)).attr("y", mid - 14)
.attr("text-anchor", "middle")
.attr("fill", "#f38ba8")
.attr("font-size", 11)
.text(labels[i]);
}
});
// X axis
[-1, 0, 1, 2, 3, 4].forEach(v => {
svg.append("text")
.attr("x", xScale(v)).attr("y", H - 10)
.attr("text-anchor", "middle")
.attr("fill", "#6c7086")
.attr("font-size", 10)
.text(v);
});
svg.append("text")
.attr("x", W / 2).attr("y", H - 2)
.attr("text-anchor", "middle")
.attr("fill", "#6c7086")
.attr("font-size", 11)
.text("z-score");
svg.append("line")
.attr("x1", padL).attr("x2", W - padR)
.attr("y1", padT + chartH).attr("y2", padT + chartH)
.attr("stroke", "#585b70").attr("stroke-width", 1.5);
return svg.node();
}