'use client'; import Link from 'next/link'; import { useMemo, useState } from 'react'; import { t } from '@/i18n'; import { cn } from '@/lib/cn'; import { fixed, formatValue, grouped } from '@/lib/format'; import { routes } from '@/lib/site'; import { useUrlState } from '@/lib/url-state'; import type { FormatSpec } from '@/lib/types'; import type { PeerPoint, PeersResponse } from '@/lib/types-analytics'; import { BubbleChart } from '@/components/charts/bubble-chart'; import { Segmented } from '@/components/controls/indicator-select'; /** * Above / below expected: pair selector (URL-synced), bubble chart with the robust fitted line, the ten largest * positive and negative residuals. Wording stays descriptive: "above / below the fitted line". */ export function PeersView({ data }: { data: PeersResponse }) { const { set } = useUrlState(); const [selected, setSelected] = useState(null); const xs: FormatSpec = { format: data.x.format, unit: data.x.unit, unit_short: data.x.unit_short, precision: data.x.precision, name: data.x.short_name ?? data.x.name }; const ys: FormatSpec = { format: data.y.format, unit: data.y.unit, unit_short: data.y.unit_short, precision: data.y.precision, name: data.y.short_name ?? data.y.name }; const points = useMemo(() => data.points.filter((p) => p.x != null && p.y != null).map((p) => ({ id: p.id, label: p.name ?? p.id, flag: p.flag, x: p.x, y: p.y, region: p.region })), [data.points]); const highlight = useMemo(() => [...data.above.slice(0, 5), ...data.below.slice(0, 5)].map((p) => p.id).concat(selected ? [selected] : []), [data.above, data.below, selected]); const pairKey = `${data.x.slug}|${data.y.slug}`; const pairs = data.pairs.map((p) => ({ value: `${p.x}|${p.y}`, label: p.label })); const current = pairs.find((p) => p.value === pairKey) ? pairKey : pairs[0]?.value ?? pairKey; return (
set({ method: v }, 0)} label={t('peers.method')} size="sm" options={[{ value: 'theil-sen', label: t('peers.method.theilSen') }, { value: 'ols', label: t('peers.method.ols') }]} /> {t('peers.stats', { n: grouped(data.n), year: data.year_used ?? '', r2: data.fit?.r2 != null ? fixed(data.fit.r2, 2) : t('common.na') })}

{data.note}

{t('peers.methodology')}

{data.methodology}

{t('peers.disclaimer')}

); } function ResidualList({ title, rows, spec, tone, onPick, selected }: { title: string; rows: PeerPoint[]; spec: FormatSpec; tone: 'inc' | 'dec'; onPick: (id: string) => void; selected: string | null }) { const max = Math.max(1, ...rows.map((r) => Math.abs(r.residual_z ?? 0))); return (

{title}

    {rows.map((r, i) => (
  1. {i + 1} {r.residual != null ? `${r.residual > 0 ? '+' : '−'}${formatValue(Math.abs(r.residual), spec)}` : t('common.na')}
  2. ))}
); }