// File: insights-charts.tsx // Path: apps/web/components/insights-charts.tsx // Project: AI Risk Index — airiskindex.io // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // // Description: Server-rendered SVG charts for the insights page: scatter (substitution × augmentation), wage-decile columns. export interface ScatterPoint { x: number; // substitution 0–100 y: number; // augmentation 0–100 size?: number | null; // employment label: string; } /** * Single-series scatter on two 0–100 axes with quadrant annotations. * One hue (identity is not encoded), dot area ~ employment, native * tooltips, hairline gridlines, text in ink tokens. */ export function ScatterPlot({ points }: { points: ScatterPoint[] }): JSX.Element { const W = 640; const H = 440; const m = { t: 20, r: 18, b: 46, l: 46 }; const sx = (v: number): number => m.l + (v / 100) * (W - m.l - m.r); const sy = (v: number): number => H - m.b - (v / 100) * (H - m.t - m.b); const maxSize = Math.max(1, ...points.map((p) => p.size ?? 0)); const radius = (size?: number | null): number => 2.2 + 7 * Math.sqrt((size ?? 0) / maxSize); return ( <svg viewBox={`0 0 ${W} ${H}`} className="h-auto w-full" role="img" aria-label="Scatter plot of substitution versus augmentation for every scored occupation; dot size reflects employment."> {[0, 25, 50, 75, 100].map((tick) => ( <g key={tick}> <line x1={sx(tick)} y1={sy(0)} x2={sx(tick)} y2={sy(100)} stroke="var(--grid)" strokeWidth="1" /> <line x1={sx(0)} y1={sy(tick)} x2={sx(100)} y2={sy(tick)} stroke="var(--grid)" strokeWidth="1" /> <text x={sx(tick)} y={H - m.b + 16} textAnchor="middle" fontSize="10" fill="var(--muted)"> {tick} </text> <text x={m.l - 8} y={sy(tick) + 3} textAnchor="end" fontSize="10" fill="var(--muted)"> {tick} </text> </g> ))} {/* midlines */} <line x1={sx(50)} y1={sy(0)} x2={sx(50)} y2={sy(100)} stroke="var(--baseline)" strokeWidth="1" /> <line x1={sx(0)} y1={sy(50)} x2={sx(100)} y2={sy(50)} stroke="var(--baseline)" strokeWidth="1" /> {/* quadrant annotations */} <text x={sx(25)} y={sy(94)} textAnchor="middle" fontSize="11" fontWeight="600" fill="var(--muted)"> AI assists, rarely replaces </text> <text x={sx(75)} y={sy(94)} textAnchor="middle" fontSize="11" fontWeight="600" fill="var(--muted)"> deep task transformation </text> <text x={sx(25)} y={sy(4)} textAnchor="middle" fontSize="11" fontWeight="600" fill="var(--muted)"> little AI contact today </text> <text x={sx(75)} y={sy(4)} textAnchor="middle" fontSize="11" fontWeight="600" fill="var(--muted)"> replacement pressure </text> {points.map((point, index) => ( <circle key={index} cx={sx(point.x)} cy={sy(point.y)} r={radius(point.size)} fill="var(--seq)" fillOpacity="0.42" stroke="var(--surface-1)" strokeWidth="0.6" > <title> {point.label} — substitution {point.x.toFixed(0)}, augmentation {point.y.toFixed(0)} {point.size ? `, ${Math.round(point.size).toLocaleString("en-US")} employed` : ""} ))} Substitution → Augmentation → ); } export interface DecileColumn { label: string; // wage range value: number; // employment-weighted mean substitution } /** Column chart: one value per wage decile, value labels on caps. */ export function DecileColumns({ columns }: { columns: DecileColumn[] }): JSX.Element { const max = Math.max(1, ...columns.map((c) => c.value)); return (
); }