// File: score-marks.tsx
// Path: apps/web/components/score-marks.tsx
// Project: AI Risk Index — airiskindex.io
// Author: Simon-Pierre Boucher
// Contact: contact@spboucher.ai
// Copyright © 2026 Simon-Pierre Boucher. All rights reserved.
//
// Description: Chart marks: score bars with CI whiskers, stat tiles, interval strips, meter.
/**
* Chart marks for 0–100 scores with confidence bounds.
* Specs per the dataviz skill: thin bars (≤24px) with a 4px rounded data-end
* and a square baseline, track = lighter step of the same ramp, CI whisker in
* muted ink, values in text tokens (never the series color).
*/
export interface Band {
low: number;
score: number;
high: number;
}
const pct = (value: number): string => `${Math.max(0, Math.min(100, value))}%`;
/** Horizontal score bar with CI whisker on a 0–100 track. */
export function ScoreBar({ band, thick = 10 }: { band: Band; thick?: number }): JSX.Element {
return (
{/* CI whisker: hairline + end ticks, muted ink over the track */}
);
}
/** Compact interval strip for stat tiles: band wash + point marker. */
export function IntervalStrip({ band }: { band: Band }): JSX.Element {
return (
{/* point marker ≥8px with a 2px surface ring */}
);
}
/** Stat tile: label · value · CI, with the interval strip underneath. */
export function ScoreTile({
label,
band,
hint,
}: {
label: string;
band: Band;
hint: string;
}): JSX.Element {
return (
{label}
{band.score.toFixed(0)}
CI {band.low.toFixed(0)}–{band.high.toFixed(0)}
{hint}
);
}
/**
* Dot plot of labeled scores on a shared 0–100 axis: hairline gridlines,
* CI band wash, ≥10px point marker with a 2px surface ring. Rows are
* direct-labeled, single hue — no legend needed.
*/
export function SubScoreDotPlot({
rows,
}: {
rows: Array<{ label: string; band: Band }>;
}): JSX.Element {
return (
{rows.map((row) => (
{row.label}
{[0, 25, 50, 75, 100].map((tick) => (
))}
{row.band.score.toFixed(0)}
))}
);
}
/**
* Compact histogram over the 0–100 scale (bins of equal width). Emphasis
* form: recessive context columns with an optional accent marker line
* ("this occupation"). Columns are thin with rounded caps, square baseline.
*/
export function DistributionChart({
bins,
marker,
markerLabel,
height = 96,
accent = false,
}: {
bins: number[];
marker?: number;
markerLabel?: string;
height?: number;
accent?: boolean;
}): JSX.Element {
const max = Math.max(1, ...bins);
const binWidth = 100 / bins.length;
return (
{/* baseline */}
{bins.map((count, index) => (
0 ? 3 : 0, (count / max) * 100)}%`,
}}
/>
))}
{marker !== undefined && (
<>
{markerLabel && (
{markerLabel}
)}
>
)}
0
25
50
75
100
);
}
/** Meter: fill + same-ramp track (marks-and-anatomy §Figures). */
export function ShareMeter({ share, label }: { share: number; label: string }): JSX.Element {
const value = Math.round(share * 100);
return (
);
}