{
const sorted = [...screenData].sort((a,b) => a-b);
const n = sorted.length;
const q1 = sorted[Math.floor(n * 0.25)];
const median = (sorted[14] + sorted[15]) / 2;
const q3 = sorted[Math.floor(n * 0.75)];
const iqr = q3 - q1;
const whiskerLow = sorted.find(d => d >= q1 - 1.5 * iqr);
const whiskerHigh = [...sorted].reverse().find(d => d <= q3 + 1.5 * iqr);
const outliers = sorted.filter(d => d < whiskerLow || d > whiskerHigh);
const W = 260, H = 420;
const left = 50, right = 120, top = 30, bottom = 40;
const plotH = H - top - bottom;
const yScale = (v) => H - bottom - (v - 0) / (16 - 0) * plotH;
const cx = left + 40;
const boxW = 50;
const svg = d3.create("svg").attr("width", W).attr("height", H);
// Y-axis
svg.append("line")
.attr("x1", left).attr("x2", left)
.attr("y1", top).attr("y2", H - bottom)
.attr("stroke", "#ccc").attr("stroke-width", 1);
[0, 2, 4, 6, 8, 10, 12, 14, 16].forEach(v => {
svg.append("text")
.attr("x", left - 8).attr("y", yScale(v) + 4)
.attr("text-anchor", "end").attr("font-size", "11px").attr("fill", "#666")
.text(v);
svg.append("line")
.attr("x1", left - 3).attr("x2", left)
.attr("y1", yScale(v)).attr("y2", yScale(v))
.attr("stroke", "#ccc");
});
svg.append("text")
.attr("transform", `rotate(-90)`)
.attr("x", -(top + plotH / 2)).attr("y", 14)
.attr("text-anchor", "middle").attr("font-size", "12px").attr("fill", "#666")
.text("Screen time (hours)");
// Lower whisker
svg.append("line")
.attr("x1", cx).attr("x2", cx)
.attr("y1", yScale(whiskerLow)).attr("y2", yScale(q1))
.attr("stroke", "#4a90d9").attr("stroke-width", 1.5);
// Upper whisker
svg.append("line")
.attr("x1", cx).attr("x2", cx)
.attr("y1", yScale(q3)).attr("y2", yScale(whiskerHigh))
.attr("stroke", "#4a90d9").attr("stroke-width", 1.5);
// Whisker caps
[whiskerLow, whiskerHigh].forEach(v => {
svg.append("line")
.attr("x1", cx - boxW * 0.3).attr("x2", cx + boxW * 0.3)
.attr("y1", yScale(v)).attr("y2", yScale(v))
.attr("stroke", "#4a90d9").attr("stroke-width", 1.5);
});
// Box
svg.append("rect")
.attr("x", cx - boxW / 2).attr("y", yScale(q3))
.attr("width", boxW).attr("height", yScale(q1) - yScale(q3))
.attr("fill", "#4a90d9").attr("opacity", 0.15)
.attr("stroke", "#4a90d9").attr("stroke-width", 2)
.attr("rx", 4);
// Median line
svg.append("line")
.attr("x1", cx - boxW / 2).attr("x2", cx + boxW / 2)
.attr("y1", yScale(median)).attr("y2", yScale(median))
.attr("stroke", "#2ecc71").attr("stroke-width", 3);
// Outliers
outliers.forEach(v => {
svg.append("circle")
.attr("cx", cx).attr("cy", yScale(v))
.attr("r", 5).attr("fill", "#e06c4c").attr("opacity", 0.8);
});
// Labels — right side, connected with lines
const labelX = cx + boxW / 2 + 12;
const textX = labelX + 4;
// Q1
svg.append("line").attr("x1", cx + boxW/2).attr("x2", labelX).attr("y1", yScale(q1)).attr("y2", yScale(q1)).attr("stroke", "#4a90d9").attr("stroke-width", 0.8);
svg.append("text").attr("x", textX).attr("y", yScale(q1) + 4).attr("font-size", "11px").attr("fill", "#4a90d9").text(`Q1 = ${q1}`);
// Median
svg.append("line").attr("x1", cx + boxW/2).attr("x2", labelX).attr("y1", yScale(median)).attr("y2", yScale(median)).attr("stroke", "#2ecc71").attr("stroke-width", 0.8);
svg.append("text").attr("x", textX).attr("y", yScale(median) + 4).attr("font-size", "11px").attr("fill", "#2ecc71").attr("font-weight", "bold").text(`Median = ${median}`);
// Q3
svg.append("line").attr("x1", cx + boxW/2).attr("x2", labelX).attr("y1", yScale(q3)).attr("y2", yScale(q3)).attr("stroke", "#4a90d9").attr("stroke-width", 0.8);
svg.append("text").attr("x", textX).attr("y", yScale(q3) + 4).attr("font-size", "11px").attr("fill", "#4a90d9").text(`Q3 = ${q3}`);
// Whisker low
svg.append("line").attr("x1", cx + boxW*0.3).attr("x2", labelX).attr("y1", yScale(whiskerLow)).attr("y2", yScale(whiskerLow)).attr("stroke", "#999").attr("stroke-width", 0.8);
svg.append("text").attr("x", textX).attr("y", yScale(whiskerLow) + 4).attr("font-size", "10px").attr("fill", "#999").text(`Min = ${whiskerLow}`);
// Whisker high
svg.append("line").attr("x1", cx + boxW*0.3).attr("x2", labelX).attr("y1", yScale(whiskerHigh)).attr("y2", yScale(whiskerHigh)).attr("stroke", "#999").attr("stroke-width", 0.8);
svg.append("text").attr("x", textX).attr("y", yScale(whiskerHigh) + 4).attr("font-size", "10px").attr("fill", "#999").text(`Max = ${whiskerHigh}`);
// Outlier labels
outliers.forEach(v => {
svg.append("line").attr("x1", cx + 6).attr("x2", labelX).attr("y1", yScale(v)).attr("y2", yScale(v)).attr("stroke", "#e06c4c").attr("stroke-width", 0.8).attr("stroke-dasharray", "3 2");
svg.append("text").attr("x", textX).attr("y", yScale(v) + 4).attr("font-size", "10px").attr("fill", "#e06c4c").text(`${v}`);
});
return svg.node();
}