fix(detail): compare each SATs bar to its own national benchmark
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 9s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 9s
The KS2 SATs chart drew a single national-average line spanning the full height of each subject's chart area, positioned at the national *expected* value. But the area stacks two bars — Expected and Exceeding — and the higher-standard/greater-depth national is a very different, much lower figure (e.g. reading higher standard ~29% vs expected ~75%). So the line crossed the Exceeding bar at the wrong place, making every school's exceeding result look far below national when it wasn't. The per-subject higher-standard nationals were already computed in the fact_ks2_national_averages mart; they just weren't serialized. Fix: - backend: add reading_high_pct, writing_gd_pct (writing = greater depth) and maths_high_pct to the national-averages payload. - SchoolDetailView: pass a nationalExceedingPct per subject, mapping writing to the greater-depth figure. - SatsChart: replace the single full-height line with a national marker on each bar's own track (coral tick + "nat X%" in the bar header), so Expected and Exceeding each sit against the correct benchmark. KS2 only; the secondary Attainment 8 chart already uses one line for one measure and is untouched. Verified: tsc --noEmit, next build, and backend pytest (national averages marts, incl. a new test guarding the per-subject nationals). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
@@ -8,6 +8,9 @@ interface SubjectData {
|
||||
expectedPct: number | null;
|
||||
exceedingPct: number | null;
|
||||
nationalExpectedPct: number | null;
|
||||
/** National higher-standard / greater-depth benchmark for the exceeding
|
||||
* bar — a different figure from the expected national. */
|
||||
nationalExceedingPct: number | null;
|
||||
}
|
||||
|
||||
interface SatsChartProps {
|
||||
@@ -17,11 +20,45 @@ interface SatsChartProps {
|
||||
const RULER_TICKS = [0, 25, 50, 75, 100];
|
||||
const GRIDLINE_POSITIONS = [25, 50, 75];
|
||||
|
||||
/** One results bar with a national marker positioned on its own track, so the
|
||||
* expected and exceeding bars each compare against their own benchmark. */
|
||||
function BarRow({
|
||||
label,
|
||||
value,
|
||||
national,
|
||||
barRef,
|
||||
barClass,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
national: number | null;
|
||||
barRef: React.RefObject<HTMLDivElement | null>;
|
||||
barClass: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.barRow}>
|
||||
<div className={styles.barHeader}>
|
||||
<span className={styles.barLabelSuffix}>{label}</span>
|
||||
<span className={styles.barHeaderRight}>
|
||||
{national != null && <span className={styles.natLabel}>nat {national.toFixed(0)}%</span>}
|
||||
<span className={styles.barLabel}>{value.toFixed(0)}%</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.barTrack}>
|
||||
<div ref={barRef} className={`${styles.bar} ${barClass}`} data-width={value} />
|
||||
{national != null && (
|
||||
<div className={styles.natTick} style={{ left: `${national}%` }} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SubjectColumn({ subject }: { subject: SubjectData }) {
|
||||
const expectedRef = useRef<HTMLDivElement>(null);
|
||||
const exceedingRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { name, expectedPct, exceedingPct, nationalExpectedPct } = subject;
|
||||
const { name, expectedPct, exceedingPct, nationalExpectedPct, nationalExceedingPct } = subject;
|
||||
|
||||
// Animate bars on mount
|
||||
useEffect(() => {
|
||||
@@ -51,44 +88,25 @@ function SubjectColumn({ subject }: { subject: SubjectData }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* National average marker */}
|
||||
{nationalExpectedPct != null && (
|
||||
<div className={styles.natLine} style={{ left: `${nationalExpectedPct}%` }}>
|
||||
<div className={styles.natPill}>{nationalExpectedPct.toFixed(0)}%</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bars */}
|
||||
{/* Bars — each carries its own national marker */}
|
||||
<div className={styles.barGroup}>
|
||||
{expectedPct != null && (
|
||||
<div className={styles.barRow}>
|
||||
<div className={styles.barHeader}>
|
||||
<span className={styles.barLabelSuffix}>Expected</span>
|
||||
<span className={styles.barLabel}>{expectedPct.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className={styles.barTrack}>
|
||||
<div
|
||||
ref={expectedRef}
|
||||
className={`${styles.bar} ${styles.barExpected}`}
|
||||
data-width={expectedPct}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BarRow
|
||||
label="Expected"
|
||||
value={expectedPct}
|
||||
national={nationalExpectedPct}
|
||||
barRef={expectedRef}
|
||||
barClass={styles.barExpected}
|
||||
/>
|
||||
)}
|
||||
{exceedingPct != null && (
|
||||
<div className={styles.barRow}>
|
||||
<div className={styles.barHeader}>
|
||||
<span className={styles.barLabelSuffix}>Exceeding</span>
|
||||
<span className={styles.barLabel}>{exceedingPct.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className={styles.barTrack}>
|
||||
<div
|
||||
ref={exceedingRef}
|
||||
className={`${styles.bar} ${styles.barExceeding}`}
|
||||
data-width={exceedingPct}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BarRow
|
||||
label="Exceeding"
|
||||
value={exceedingPct}
|
||||
national={nationalExceedingPct}
|
||||
barRef={exceedingRef}
|
||||
barClass={styles.barExceeding}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -135,7 +153,7 @@ export default function SatsChart({ subjects }: SatsChartProps) {
|
||||
Exceeding / high score
|
||||
</div>
|
||||
<div className={styles.legendItem}>
|
||||
<div className={styles.legendSwatch} style={{ background: 'var(--accent-coral, #e07256)', borderRadius: '50%' }} />
|
||||
<div className={styles.legendSwatch} style={{ background: 'var(--accent-coral, #e07256)', width: '3px', height: '12px', borderRadius: '2px' }} />
|
||||
National average
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user