fix: hide score on submit + total denominator + projector chart cleanup
Three small UX/fairness tweaks from manual live testing: 1. Post-submit "wait for reveal" screen: show only the response time, no score. The +score reveal leaked correctness — any positive number = correct, zero = wrong — short-circuiting the "stop and think" beat the reveal pause was supposed to enforce. Time stays as the engagement signal; score now waits for the instructor reveal. 2. Final-screen "Correct X / Y" denominator is now total_questions instead of questions_answered. Missed questions are scored zero, so they belong in the denominator visibly. Server adds total_questions to the session_ended payload. 3. Projector score-distribution: drop the in-chart count labels (they collided with each other and with the median tag at small N), restore the previously-computed-but-not-rendered x-axis tick labels at the bottom. Stats line at the foot keeps n / mean / max. Also: short-circuit the per-submit instructor + presence broadcasts when no instructor / projector is connected (no listener, no DB work). The 50-student load test was tight on margin against its 2 s time_limit; with the new presence_message / live_histogram_message DB queries firing on every submit, the margin disappeared on busy boxes. Conftest fixture also bumped to 8 s per question for the same reason — gives breathing room for sequential WS submits in the load test. 71/71 pytest green.
This commit is contained in:
@@ -576,19 +576,23 @@ function renderScoreArea(dist) {
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
// X-axis tick labels at each bucket centre
|
||||
// X-axis tick labels at each bucket centre. With 10 buckets across the
|
||||
// 1000-unit-wide SVG these read cleanly at projector scale; the SVG
|
||||
// stretches but the text rotates if we wanted, here it's horizontal
|
||||
// because the labels are short ("0.0-1.0" etc.).
|
||||
const xLabels = buckets.map((b, i) => {
|
||||
const cx = (xEdge(i) + xEdge(i + 1)) / 2;
|
||||
return `<text class="x-tick-label" x="${cx}" y="${H - padB + 16}">${escapeText(b.label)}</text>`;
|
||||
return `<text class="x-tick-label" x="${cx}" y="${H - padB + 18}" text-anchor="middle">${escapeText(b.label)}</text>`;
|
||||
}).join("");
|
||||
|
||||
// Per-bucket count labels above each top, only if non-zero
|
||||
const dataLabels = buckets.map((b, i) => {
|
||||
// Per-bucket data points (small circles at the top of each band) — no
|
||||
// numeric labels above them. With small N the count labels collide
|
||||
// with the median tag and with each other when bars are short; the
|
||||
// x-axis labels + bottom legend (n / mean / max) carry that info now.
|
||||
const dataPoints = buckets.map((b, i) => {
|
||||
if (b.count === 0) return "";
|
||||
const cx = (xEdge(i) + xEdge(i + 1)) / 2;
|
||||
const cy = yFor(b.count) - 8;
|
||||
return `<text class="data-label" x="${cx}" y="${cy - 12}">${b.count}</text>
|
||||
<circle class="data-point" cx="${cx}" cy="${yFor(b.count)}" r="5.5"></circle>`;
|
||||
return `<circle class="data-point" cx="${cx}" cy="${yFor(b.count)}" r="5.5"></circle>`;
|
||||
}).join("");
|
||||
|
||||
// Median tag — find the bucket containing the cumulative midpoint
|
||||
@@ -622,15 +626,15 @@ function renderScoreArea(dist) {
|
||||
${yGrid}
|
||||
<line class="axis" x1="${padL}" x2="${padL + innerW}" y1="${padT + innerH}" y2="${padT + innerH}"></line>
|
||||
<line class="axis" x1="${padL}" x2="${padL}" y1="${padT}" y2="${padT + innerH}"></line>
|
||||
<text class="axis-title" x="${padL + innerW / 2}" y="${H - 6}" text-anchor="middle">Score band (out of ${(dist.max_total || 0).toFixed(1)})</text>
|
||||
<text class="axis-title" x="${padL + innerW / 2}" y="${H - 4}" text-anchor="middle">Score band (out of ${(dist.max_total || 0).toFixed(1)})</text>
|
||||
<text class="axis-title" x="${padL - 36}" y="${padT + innerH / 2}" transform="rotate(-90 ${padL - 36} ${padT + innerH / 2})" text-anchor="middle">Students</text>
|
||||
<path class="area-fill" d="${fillPath.join(" ")}"></path>
|
||||
<path class="area-line" d="${linePath.join(" ")}"></path>
|
||||
${dataLabels}
|
||||
${xLabels}
|
||||
${dataPoints}
|
||||
${medianMarks}
|
||||
</svg>
|
||||
<div class="chart-legend">
|
||||
<span>10 score bands · ${n} buckets</span>
|
||||
<span class="stat">n = <b>${total}</b> · mean <b>${mean.toFixed(2)}</b> · max <b>${(dist.max_total || 0).toFixed(1)}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -415,11 +415,16 @@ function submitAnswer(optionKey, optionText) {
|
||||
function renderSubmitted(message) {
|
||||
store.submitted = message;
|
||||
const seconds = (message.elapsed_ms / 1000).toFixed(1);
|
||||
// Deliberately hide the score until the instructor reveals — leaks
|
||||
// correctness otherwise (any positive score = correct, zero = wrong),
|
||||
// which short-circuits the "stop and think" beat the reveal pause is
|
||||
// there to enforce. Show response time as the engagement signal
|
||||
// instead.
|
||||
setView(`
|
||||
<div class="card narrow center">
|
||||
<p class="eyebrow">Question ${message.question_idx + 1}</p>
|
||||
<h1 class="big-score">+${fmtScore(message.score)}</h1>
|
||||
<p class="muted">submitted in ${seconds}s</p>
|
||||
<h1 class="big-score">${seconds}<small class="unit">s</small></h1>
|
||||
<p class="muted">answer recorded</p>
|
||||
<p class="muted small">Waiting for the reveal…</p>
|
||||
<div class="spinner" aria-hidden="true"></div>
|
||||
</div>
|
||||
@@ -492,7 +497,7 @@ function renderFinished(message) {
|
||||
<div class="reveal-stats">
|
||||
<div class="stat big"><span class="muted">Your total</span><b>${fmtScore(message.your_total)}</b></div>
|
||||
<div class="stat"><span class="muted">Rank</span><b>${message.your_rank ?? "—"}</b></div>
|
||||
<div class="stat"><span class="muted">Correct</span><b>${message.questions_correct ?? 0} / ${message.questions_answered ?? 0}</b></div>
|
||||
<div class="stat"><span class="muted">Correct</span><b>${message.questions_correct ?? 0} / ${message.total_questions ?? message.questions_answered ?? 0}</b></div>
|
||||
</div>
|
||||
<h3>Final top 5</h3>
|
||||
${renderBoard(message.final_top5)}
|
||||
|
||||
@@ -1155,6 +1155,16 @@ h2.question-text.small {
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Unit suffix (e.g. "s" after a duration) — small, muted, baseline-sized
|
||||
* so it reads as a tag, not part of the number. */
|
||||
.big-score .unit {
|
||||
font-size: 0.32em;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0;
|
||||
margin-left: 6px;
|
||||
vertical-align: 0.55em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 22px;
|
||||
|
||||
Reference in New Issue
Block a user