spb/cancerindex
Public
1import Link from 'next/link';2import { Section } from '@/components/ui/section';3import { EmptyState } from '@/components/ui/empty-state';4import { Freshness } from '@/components/ui/freshness';5import { ClaimBadge, ConfidenceBadge } from '@/components/ui/badge';6import { gapRankings } from '@/lib/queries/rankings';7import { fmtDate, fmtInt, fmtValue, scopeLabel } from '@/lib/format';89/**10 * Home module "Largest trial gaps / research gaps" (§105, §328, §265-266): the gap indexes from the current11 * ranking snapshots for a geography. Rendered only when snapshots exist; each row exposes the two percentiles12 * that produced the value (lineage stored on the ranking row).13 */14export async function GapsModule({ geo = 'USA', limit = 8 }: { geo?: string; limit?: number }) {15 const gaps = await gapRankings(geo, limit);16 return (17 <Section18 id="gaps"19 kicker="Unmet need"20 title="Largest trial and research gaps"21 description="Burden percentile minus activity percentile within the same scope. Positive values flag cancers with high mortality burden but comparatively few active trials (trial gap) or little recent literature (research gap). A quantitative signal, not an accusation."22 actions={23 <Link href="/rankings" className="ci-link">24 All rankings →25 </Link>26 }27 >28 {gaps.length === 0 ? (29 <EmptyState title="Gap indexes not yet computed">30 Trial and research gap indexes require a burden snapshot (deaths per top-level cancer for one geography and year) plus trial and literature counters. Nothing is shown until such a snapshot exists.31 </EmptyState>32 ) : (33 <div className="grid gap-6 lg:grid-cols-2">34 {gaps.map(({ metric, snapshot, rows }) => (35 <div key={metric.slug} className="min-w-0">36 <p className="mb-1 flex flex-wrap items-baseline justify-between gap-x-2">37 <Link href={`/rankings/${metric.slug}?scope=${encodeURIComponent(snapshot.scope_key)}`} className="ci-link font-medium">38 {metric.name}39 </Link>40 <span className="text-[11.5px] text-ink-3">{scopeLabel(snapshot.scope_key)}</span>41 </p>42 <div className="ci-table-wrap">43 <table className="ci-table">44 <thead>45 <tr>46 <th className="num">#</th>47 <th>Cancer</th>48 <th className="num">Gap (pct. points)</th>49 <th className="num">Deaths</th>50 <th className="num">{metric.slug === 'trial_gap' ? 'Active trials' : 'Publications 5y'}</th>51 <th>Confidence</th>52 </tr>53 </thead>54 <tbody>55 {rows.map((r) => (56 <tr key={r.id}>57 <td className="num">{r.rank}</td>58 <td>59 <Link className="ci-link" href={`/cancer/${r.slug}/rankings`}>60 {r.canonical_name}61 </Link>62 </td>63 <td className={`num font-medium ${r.value > 0 ? 'text-danger' : ''}`}>{fmtValue(r.value, r.unit)}</td>64 <td className="num">{fmtInt(r.inputs.deaths as number | undefined)}</td>65 <td className="num">{fmtInt(r.inputs.activity as number | undefined)}</td>66 <td>67 <ConfidenceBadge level={r.confidence} />68 </td>69 </tr>70 ))}71 </tbody>72 </table>73 </div>74 <p className="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11.5px] text-ink-3">75 <ClaimBadge kind="computed" />76 <span>77 formula <code className="ci-mono">{metric.formula}</code> · <span className="ci-mono">{snapshot.formula_version}</span>78 </span>79 <span>sources: {snapshot.source_ids.join(', ')}</span>80 <span>{fmtInt(snapshot.eligible_entities)} eligible</span>81 </p>82 <p className="mt-1 text-[11.5px] text-ink-3">83 {metric.slug === 'trial_gap' ? 'Caveat: trial counts aggregate a cancer and its NCIt descendants; trials registered at a broader level (e.g. "colorectal") are attributed to the broader entity and can overstate the gap of narrower sites.' : 'Caveat: literature counts are query-based per entity and not aggregated over descendants.'}{' '}84 <Link className="ci-link" href="/methodology#gap-caveat">85 Read the caveat86 </Link>87 </p>88 <Freshness dataUpdatedAt={snapshot.generated_at} extra={`snapshot generated ${fmtDate(snapshot.generated_at)}`} />89 </div>90 ))}91 </div>92 )}93 </Section>94 );95}96