Mean ± SD
Median
Minimum
Maximum
Simulated rolls
Distribution of totals
Plot.plot({
style: {
backgroundColor: "transparent",
color: "#fff",
fontSize: "13px"
},
x: {
label: "Total rolled",
labelAnchor: "center",
tickFormat: "d",
domain: [0, 150]
},
y: {
label: "Share of rolls",
tickFormat: ".0%",
grid: true,
ticks: 5
},
color: {
domain: ["Below target", "Meets target"],
range: ["#898781", "#3987e5"],
legend: true
},
marks: [
Plot.rectY(subset, Plot.binX({y: "sum"}, {
x: "value",
y: (d) => d.n / stats.n,
thresholds: bins.thresholds,
fill: "transparent",
color: "#888"
})),
Plot.areaY(subset, Plot.binX({y: "sum"}, {
x: "value",
y: (d) => d.n / stats.n,
thresholds: bins.thresholds,
fill: (d) => d.value >= target ? "Meets target" : "Below target",
fillOpacity: 0.75
})),
Plot.lineY(subset, Plot.binX({y: "sum"}, {
x: "value",
y: (d) => d.n / stats.n,
thresholds: bins.thresholds,
color: "#000"
})),
Plot.ruleX([target], {stroke: "currentColor", strokeOpacity: 0.55}),
Plot.text([`Target ${target}`], {
x: target,
frameAnchor: "top",
textAnchor: "start",
dx: 5,
dy: 4,
fill: "currentColor",
fillOpacity: 0.72
}),
Plot.ruleY([0], {stroke: "currentColor", strokeOpacity: 0.3})
]
})stats = {
const rows = subset.slice().sort((a, b) => a.value - b.value);
const n = d3.sum(rows, (d) => d.n);
const hits = d3.sum(rows.filter((d) => d.value >= target), (d) => d.n);
// Weighted median: walk the tallies until half the rolls are behind us.
let seen = 0;
let median = NaN;
for (const row of rows) {
seen += row.n;
if (seen >= n / 2) {
median = row.value;
break;
}
}
// Each row is a tally, so mean and SD must be weighted by n. d3.deviation
// would treat every distinct total as a single roll and overweight the tails.
const mean = n ? d3.sum(rows, (d) => d.value * d.n) / n : NaN;
const sd = n > 1
? Math.sqrt(d3.sum(rows, (d) => d.n * (d.value - mean) ** 2) / (n - 1))
: NaN;
return {
n: n,
hits: hits,
p: n ? hits / n : 0,
mean: mean,
sd: sd,
median: median,
min: rows.length ? rows[0].value : NaN,
max: rows.length ? rows[rows.length - 1].value : NaN
};
}// Bin edges are anchored on the target so no bar straddles it: every bar is
// wholly below the target or wholly at/above it, which is what the fill colour
// claims.
bins = {
const width = Math.max(1, Math.ceil((stats.max - stats.min + 1) / 36));
const start = target - Math.ceil((target - stats.min) / width) * width;
return {
width: width,
thresholds: d3.range(start, stats.max + width + 1, width)
};
}// Follows Quarto's light/dark toggle so the hover tip matches the card.
darkMode = Generators.observe((notify) => {
const read = () => notify(document.body.classList.contains("quarto-dark"));
const observer = new MutationObserver(read);
read();
observer.observe(document.body, {attributes: true, attributeFilter: ["class"]});
return () => observer.disconnect();
})
Average Dice Rolls
Improving Rolls vs. Keeps