SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
6.2 KB · 111 lines tsx
Raw Blame History
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