spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
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 { ratioGapRankings } from '@/lib/queries/research-gap';7import { fmtDate, fmtInt, fmtPct, fmtValue, scopeLabel } from '@/lib/format';89/**10 * Home module "Largest trial and research gaps" (§105, §328, §265-266): the ratio-based gap indexes11 * (Trial Gap Ratio, Research Gap Ratio) from the current ranking snapshots of the latest burden scope12 * of a geography. Each row exposes the two shares that produced the log₂ ratio (lineage stored on the13 * ranking row). The percentile-based indexes remain available under /rankings; the full component14 * table, scatter plots and CSV live at /research-gap.15 */16export async function GapsModule({ geo = 'USA', limit = 8 }: { geo?: string; limit?: number }) {17 const gaps = await ratioGapRankings(geo, limit);18 return (19 <Section20 id="gaps"21 kicker="Unmet need"22 title="Largest trial and research gaps"23 description="log₂ of a cancer's share of deaths divided by its share of active interventional trials (trial gap) or of publications of the last five years (research gap), over the eligible top-level cancers of one burden scope. 0 = proportional; +1 = twice the share of deaths that activity would suggest; −1 = half. A quantitative signal, not an accusation."24 actions={25 <>26 <Link href="/research-gap" className="ci-link">27 Research Gap Index →28 </Link>29 <Link href="/rankings" className="ci-link">30 All rankings31 </Link>32 </>33 }34 >35 {gaps.length === 0 ? (36 <EmptyState title="Gap indexes not yet computed" knows={[{ label: 'Percentile-based Trial Gap Index', href: '/rankings/trial_gap' }, { label: 'Percentile-based Research Gap Index', href: '/rankings/research_gap' }]}>37 Gap ratios require a burden scope (deaths per top-level cancer for one geography, year and source) plus trial and literature counters. Nothing is shown until such a snapshot exists.38 </EmptyState>39 ) : (40 <div className="grid gap-6 lg:grid-cols-2">41 {gaps.map(({ metric, snapshot, rows }) => {42 const isTrial = metric.slug === 'trial_gap_ratio';43 return (44 <div key={metric.slug} className="min-w-0">45 <p className="mb-1 flex flex-wrap items-baseline justify-between gap-x-2">46 <Link href={`/rankings/${metric.slug}?scope=${encodeURIComponent(snapshot.scope_key)}`} className="ci-link font-medium">47 {metric.name}48 </Link>49 <span className="text-[11.5px] text-ink-3">{scopeLabel(snapshot.scope_key)}</span>50 </p>51 <div className="ci-table-wrap">52 <table className="ci-table">53 <thead>54 <tr>55 <th className="num">#</th>56 <th>Cancer</th>57 <th className="num">Ratio (log₂)</th>58 <th className="num">Deaths</th>59 <th className="num">Death share</th>60 <th className="num">{isTrial ? 'Active trials' : 'Publications 5y'}</th>61 <th className="num">{isTrial ? 'Trial share' : 'Pub. share'}</th>62 <th>Confidence</th>63 </tr>64 </thead>65 <tbody>66 {rows.map((r) => (67 <tr key={r.id}>68 <td className="num">{r.rank}</td>69 <td>70 <Link className="ci-link" href={`/cancer/${r.slug}/rankings`}>71 {r.canonical_name}72 </Link>73 </td>74 <td className={`num font-medium ${r.value >= 1 ? 'text-danger' : ''}`}>{fmtValue(r.value, r.unit)}</td>75 <td className="num">{fmtInt(r.inputs.deaths as number | undefined)}</td>76 <td className="num text-ink-3">{fmtPct(r.inputs.deathShare as number | undefined, 1)}</td>77 <td className="num">{fmtInt((isTrial ? r.inputs.activeTrials : r.inputs.publications5y) as number | undefined)}</td>78 <td className="num text-ink-3">{fmtPct((isTrial ? r.inputs.trialShare : r.inputs.publicationShare) as number | undefined, 1)}</td>79 <td>80 <ConfidenceBadge level={r.confidence} />81 </td>82 </tr>83 ))}84 </tbody>85 </table>86 </div>87 <p className="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11.5px] text-ink-3">88 <ClaimBadge kind="computed" />89 <span>90 formula <code className="ci-mono">{metric.formula}</code> · <span className="ci-mono">{snapshot.formula_version}</span>91 </span>92 <span>sources: {snapshot.source_ids.join(', ')}</span>93 <span>{fmtInt(snapshot.eligible_entities)} eligible</span>94 </p>95 <p className="mt-1 text-[11.5px] text-ink-3">96 {isTrial ? '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; a narrow query inflates the ratio.'}{' '}97 Burden is US-only while global estimates are under license review.{' '}98 <Link className="ci-link" href="/methodology#research-gap">99 Read the caveats100 </Link>101 </p>102 <Freshness dataUpdatedAt={snapshot.generated_at} extra={`snapshot generated ${fmtDate(snapshot.generated_at)}`} />103 </div>104 );105 })}106 </div>107 )}108 </Section>109 );110}111