SPB Git

spb/cancerindex

Public
4.9 KB · 96 lines tsx
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
Files · 602dc111f3dd7d7dec082e01832909f07dca7bd3
Menu

Theme