Files
school_compare/nextjs-app/components/DotStrip.tsx

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>
);
}