{
const w = 660, h = 260;
const svg = d3.create("svg")
.attr("width", w).attr("height", h)
.style("font-family", "inherit");
const pad = { l: 55, r: 30, t: 20, b: 40 };
const pw = w - pad.l - pad.r;
const ph = h - pad.t - pad.b;
// fake data: stream duration vs retention rate
const data = [
[30,92],[45,88],[60,84],[75,80],[90,74],[105,70],
[120,68],[135,62],[150,58],[165,55],[180,50],
[50,95],[80,78],[100,72],[130,65],[160,52],[40,90],[110,69]
];
const xMin = 25, xMax = 190, yMin = 44, yMax = 100;
const xS = v => pad.l + (v - xMin) / (xMax - xMin) * pw;
const yS = v => pad.t + ph - (v - yMin) / (yMax - yMin) * ph;
// axes
svg.append("line").attr("x1", pad.l).attr("y1", pad.t + ph)
.attr("x2", pad.l + pw).attr("y2", pad.t + ph)
.attr("stroke", "#ccc").attr("stroke-width", 1.5);
svg.append("line").attr("x1", pad.l).attr("y1", pad.t)
.attr("x2", pad.l).attr("y2", pad.t + ph)
.attr("stroke", "#ccc").attr("stroke-width", 1.5);
// axis labels
svg.append("text").attr("x", pad.l + pw / 2).attr("y", h - 5)
.attr("text-anchor", "middle").attr("font-size", 12).attr("fill", "#888")
.text("Stream Duration (minutes)");
svg.append("text").attr("x", 14).attr("y", pad.t + ph / 2)
.attr("text-anchor", "middle").attr("font-size", 12).attr("fill", "#888")
.attr("transform", `rotate(-90, 14, ${pad.t + ph / 2})`)
.text("Viewer Retention (%)");
// regression line y = 100 - 0.28x (approx)
const lineX1 = xMin, lineX2 = xMax;
const lineY1 = 100 - 0.28 * lineX1;
const lineY2 = 100 - 0.28 * lineX2;
// residuals
data.forEach(([x, y]) => {
const yHat = 100 - 0.28 * x;
svg.append("line")
.attr("x1", xS(x)).attr("y1", yS(y))
.attr("x2", xS(x)).attr("y2", yS(yHat))
.attr("stroke", "#f97316").attr("stroke-width", 1.2)
.attr("stroke-dasharray", "3,2");
});
// regression line
svg.append("line")
.attr("x1", xS(lineX1)).attr("y1", yS(lineY1))
.attr("x2", xS(lineX2)).attr("y2", yS(lineY2))
.attr("stroke", "#3b82f6").attr("stroke-width", 2.5);
// data points
data.forEach(([x, y]) => {
svg.append("circle")
.attr("cx", xS(x)).attr("cy", yS(y))
.attr("r", 5).attr("fill", "#6366f1")
.attr("opacity", 0.85);
});
// legend
svg.append("circle").attr("cx", pad.l + 10).attr("cy", pad.t + 12).attr("r", 5).attr("fill", "#6366f1");
svg.append("text").attr("x", pad.l + 20).attr("y", pad.t + 16).attr("font-size", 12).attr("fill", "#555").text("Observed data");
svg.append("line").attr("x1", pad.l + 110).attr("y1", pad.t + 12)
.attr("x2", pad.l + 140).attr("y2", pad.t + 12)
.attr("stroke", "#3b82f6").attr("stroke-width", 2.5);
svg.append("text").attr("x", pad.l + 145).attr("y", pad.t + 16).attr("font-size", 12).attr("fill", "#555").text("Model (regression line)");
svg.append("line").attr("x1", pad.l + 295).attr("y1", pad.t + 8)
.attr("x2", pad.l + 295).attr("y2", pad.t + 18)
.attr("stroke", "#f97316").attr("stroke-width", 1.5).attr("stroke-dasharray", "3,2");
svg.append("text").attr("x", pad.l + 300).attr("y", pad.t + 16).attr("font-size", 12).attr("fill", "#555").text("Residual (error)");
return svg.node();
}