98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
/**
|
|||
|
|
* DotStrip — the compare screen's signature element: one measure per strip,
|
||
|
|
* every school's dot on a shared track, anchored by a grey England-average
|
||
|
|
* tick so "right of the tick = above average" needs no domain knowledge.
|
||
|
|
*/
|
||
|
|
|
||
|
|
'use client';
|
||
|
|
|
||
|
|
import { stripPositions } from '@/lib/compareLogic';
|
||
|
|
import { CHART_COLORS, CHART_TEXT_COLORS } from '@/lib/utils';
|
||
|
|
import styles from './DotStrip.module.css';
|
||
|
|
|
||
|
|
export interface DotStripProps {
|
||
|
|
label: string;
|
||
|
|
/** One value per school; index = the school's chart-colour index. */
|
||
|
|
values: Array<number | null>;
|
||
|
|
schoolNames: string[];
|
||
|
|
/** Anchor tick, e.g. { value: 62, label: 'England 62%' }. Omit when the
|
||
|
|
* benchmark isn't available — the caller should say why in `headNote`. */
|
||
|
|
anchor?: { value: number; label: string } | null;
|
||
|
|
min?: number;
|
||
|
|
max?: number;
|
||
|
|
unit?: string;
|
||
|
|
/** Tooltip on the measure label (plain-English definition). */
|
||
|
|
tip?: string;
|
||
|
|
/** Small note on the right of the header row (e.g. the tick legend). */
|
||
|
|
headNote?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function DotStrip({
|
||
|
|
label,
|
||
|
|
values,
|
||
|
|
schoolNames,
|
||
|
|
anchor = null,
|
||
|
|
min = 0,
|
||
|
|
max = 100,
|
||
|
|
unit = '%',
|
||
|
|
tip,
|
||
|
|
headNote,
|
||
|
|
}: DotStripProps) {
|
||
|
|
const points = stripPositions(values, min, max);
|
||
|
|
const span = max - min;
|
||
|
|
const anchorPos =
|
||
|
|
anchor != null
|
||
|
|
? Math.min(100, Math.max(0, ((anchor.value - min) / span) * 100))
|
||
|
|
: null;
|
||
|
|
|
||
|
|
const ariaParts = [
|
||
|
|
anchor ? `${anchor.label}` : null,
|
||
|
|
...points.map(
|
||
|
|
(p) => `${schoolNames[p.schoolIndex] ?? `School ${p.schoolIndex + 1}`} ${p.value}${unit}`,
|
||
|
|
),
|
||
|
|
].filter(Boolean);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={styles.row}>
|
||
|
|
<div className={styles.head}>
|
||
|
|
<span className={styles.title} title={tip}>
|
||
|
|
{label}
|
||
|
|
</span>
|
||
|
|
{headNote && <span className={styles.headNote}>{headNote}</span>}
|
||
|
|
</div>
|
||
|
|
<div className={styles.strip} role="img" aria-label={`${label}: ${ariaParts.join(', ')}`}>
|
||
|
|
<div className={styles.track} />
|
||
|
|
{anchorPos != null && anchor && (
|
||
|
|
<>
|
||
|
|
<span className={styles.anchorTick} style={{ left: `${anchorPos}%` }} />
|
||
|
|
<span className={styles.anchorLabel} style={{ left: `${anchorPos}%` }}>
|
||
|
|
{anchor.label}
|
||
|
|
</span>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
{points.map((p) => (
|
||
|
|
<span key={p.schoolIndex}>
|
||
|
|
<span
|
||
|
|
className={styles.point}
|
||
|
|
style={{
|
||
|
|
left: `${p.pos}%`,
|
||
|
|
background: CHART_COLORS[p.schoolIndex % CHART_COLORS.length],
|
||
|
|
}}
|
||
|
|
title={`${schoolNames[p.schoolIndex] ?? ''}: ${p.value}${unit}`}
|
||
|
|
/>
|
||
|
|
<span
|
||
|
|
className={`${styles.pointLabel} ${p.labelAbove ? styles.pointLabelAbove : ''}`}
|
||
|
|
style={{
|
||
|
|
left: `${p.pos}%`,
|
||
|
|
color: CHART_TEXT_COLORS[p.schoolIndex % CHART_TEXT_COLORS.length],
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{p.value}
|
||
|
|
</span>
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|