html`<div style="display: flex; gap: 8px;">
${kpi("Cards shown", filtered.length, `of ${n_cards} fitted`)}
${kpi("p < 0.05", nSigRaw, "unadjusted")}
${kpi("FDR significant", nSigFdr, "Benjamini-Hochberg")}
${kpi("Median lift", `${medianLift > 0 ? "+" : ""}${medianLift.toFixed(1)}%`,
"win rate, current selection")}
${kpi("Separated fits", nSeparated, hideSeparated ? "excluded" : "included")}
</div>`
Effect Size: Top 25 Cards by |coefficient| (odds ratio, 95% CI)
forestData.length === 0
? html`<p class="text-muted">No estimable cards match the current filters.</p>`
: Plot.plot({
height: Math.max(200, forestData.length * 22 + 60),
marginLeft: 190,
marginRight: 20,
style: {fontSize: "12px"},
x: {
type: "log",
label: "Odds ratio (log scale)",
domain: [CI_FLOOR, CI_CEIL],
grid: true,
ticks: [0.1, 0.25, 0.5, 1, 2, 4, 10],
tickFormat: x => String(x)
},
y: {domain: forestData.map(d => d.card_name), label: null},
color: {
domain: ["FDR significant", "p < 0.05", "Not significant"],
range: ["var(--bs-success)", "var(--bs-warning)", "var(--bs-secondary)"],
legend: true,
label: null
},
marks: [
Plot.ruleX([1], {stroke: "gray", strokeDasharray: "4,4", strokeOpacity: 0.6}),
Plot.link(forestData, {
y: "card_name",
x1: "lo",
x2: "hi",
stroke: d => d.sig_fdr ? "FDR significant"
: d.sig_raw ? "p < 0.05" : "Not significant",
strokeWidth: 2,
strokeOpacity: 0.7
}),
Plot.dot(forestData, {
y: "card_name",
x: "odds_ratio",
fill: d => d.sig_fdr ? "FDR significant"
: d.sig_raw ? "p < 0.05" : "Not significant",
r: 5,
stroke: "var(--bs-body-bg)",
strokeWidth: 1,
channels: {
Card: "card_name",
Games: "n_games",
OR: d => d.odds_ratio.toFixed(2),
CI: d => `${d.ci_lower.toFixed(2)} – ${d.ci_upper.toFixed(2)}`,
p: d => d.p.toExponential(1),
"CI clipped": d => d.clipped ? "yes" : "no"
},
tip: {format: {x: false, y: false, fill: false, r: false}}
})
]
})
Volcano: Effect vs Evidence
estimable.length === 0
? html`<p class="text-muted">No estimable cards match the current filters.</p>`
: Plot.plot({
marginLeft: 60,
marginBottom: 45,
style: {fontSize: "12px"},
x: {label: "Coefficient (log odds)", grid: true, nice: true},
y: {label: "−log₁₀(p)", grid: true, nice: true},
r: {range: [2, 12]},
color: {
domain: ["FDR significant", "p < 0.05", "Not significant"],
range: ["var(--bs-success)", "var(--bs-warning)", "var(--bs-secondary)"],
legend: true,
label: null
},
marks: [
Plot.ruleX([0], {stroke: "gray", strokeDasharray: "4,4", strokeOpacity: 0.6}),
Plot.ruleY([-Math.log10(0.05)], {
stroke: "gray", strokeDasharray: "2,3", strokeOpacity: 0.5
}),
Plot.dot(estimable, {
x: "coef",
y: "neg_log10_p",
r: "n_games",
fill: d => d.sig_fdr ? "FDR significant"
: d.sig_raw ? "p < 0.05" : "Not significant",
fillOpacity: 0.75,
stroke: "var(--bs-body-bg)",
strokeWidth: 0.5,
channels: {
Card: "card_name",
Games: "n_games",
Lift: d => d.lift_pct == null ? "—" : `${d.lift_pct.toFixed(1)}%`
},
tip: {format: {x: ".3f", y: false, r: false, fill: false}}
})
]
})
Detailed Estimates (pooled across all seasons)
Inputs.table(filtered, {
columns: [
"card_name", "n_games", "coef", "se", "odds_ratio",
"ci_lower", "ci_upper", "p", "p_adj", "lift_pct", "confidence", "best_model"
],
header: {
card_name: "Card Name",
n_games: "Games",
coef: "Coef",
se: "SE",
odds_ratio: "OR",
ci_lower: "CI low",
ci_upper: "CI high",
p: "p",
p_adj: "p (FDR)",
lift_pct: "Win rate lift",
confidence: "Confidence",
best_model: "Best AIC"
},
format: {
coef: x => x == null ? "—" : x.toFixed(3),
se: x => x == null ? "—" : x.toFixed(3),
odds_ratio: orCell,
ci_lower: orCell,
ci_upper: orCell,
p: pCell,
p_adj: pCell,
lift_pct: liftCell
},
align: {
n_games: "right", coef: "right", se: "right", odds_ratio: "right",
ci_lower: "right", ci_upper: "right", p: "right", p_adj: "right",
lift_pct: "right"
},
sort: "abs_coef",
reverse: true,
rows: 20,
layout: "auto"
})