SPB Git

spb/cancerindex

Public
6.7 KB · 158 lines tsx
1import Link from 'next/link';2import { Badge, ClaimBadge, MatchBadge } from '@/components/ui/badge';3import { SourceBadge } from '@/components/ui/source-badge';4import { Freshness } from '@/components/ui/freshness';5import type { FreqRow } from '@/lib/queries/genomics';6import type { ProvRow } from '@/lib/queries/provenance';7import { toInfo } from '@/lib/queries/provenance';8import { fmtInt, fmtPct, humanize } from '@/lib/format';910/** Gene alteration frequencies with mandatory denominators (§260-261), one table per cohort. */11export function FrequencyTables({ rows, prov, cohortFilter, showCancer = false }: { rows: FreqRow[]; prov: Map<number, ProvRow>; cohortFilter?: string | null; showCancer?: boolean }) {12  const cohorts = new Map<string, FreqRow[]>();13  for (const r of rows) {14    if (cohortFilter && r.cohort_id !== cohortFilter) continue;15    if (!cohorts.has(r.cohort_id)) cohorts.set(r.cohort_id, []);16    cohorts.get(r.cohort_id)!.push(r);17  }18  return (19    <div className="space-y-8">20      {[...cohorts.entries()].map(([cohortId, rs]) => {21        const c = rs[0]!;22        return (23          <section key={cohortId} aria-label={c.cohort_name}>24            <header className="mb-1.5 flex flex-wrap items-baseline justify-between gap-2">25              <h3 className="font-sans text-[15px] font-medium">26                {c.cohort_name} <span className="ci-mono text-[12px] text-ink-3">{c.study_id}</span>27                {c.program ? <Badge className="ml-2">{c.program}</Badge> : null}28              </h3>29              <p className="text-[12.5px] text-ink-3">30                {c.cases_with_ssm != null ? (31                  <>32                    <span className="ci-num">{fmtInt(c.cases_with_ssm)}</span> cases with simple somatic mutation data33                  </>34                ) : null}35                {c.case_count != null ? (36                  <>37                    {' '}38                    · <span className="ci-num">{fmtInt(c.case_count)}</span> cases total39                  </>40                ) : null}41                {c.data_release ? <span className="ci-mono"> · {c.data_release}</span> : null}42                {showCancer && c.cancer_slug ? (43                  <>44                    {' '}45                    · mapped to{' '}46                    <Link className="ci-link" href={`/cancer/${c.cancer_slug}`}>47                      {c.cancer_name}48                    </Link>{' '}49                    <MatchBadge matchType={c.cancer_match_type} />50                  </>51                ) : null}52              </p>53            </header>54            <div className="ci-table-wrap">55              <table className="ci-table">56                <thead>57                  <tr>58                    <th className="num">#</th>59                    <th>Gene</th>60                    <th>Alteration</th>61                    <th className="num">Affected (n)</th>62                    <th className="num">Profiled (n)</th>63                    <th className="num">Frequency (%)</th>64                    <th>Source</th>65                  </tr>66                </thead>67                <tbody>68                  {rs.map((r, i) => (69                    <tr key={r.id}>70                      <td className="num text-ink-3">{r.rank ?? i + 1}</td>71                      <td>72                        <Link className="ci-mono ci-link font-medium" href={`/gene/${r.gene_symbol}`}>73                          {r.gene_symbol}74                        </Link>75                      </td>76                      <td>77                        <Badge tone="outline">{humanize(r.alteration_type)}</Badge>78                      </td>79                      <td className="num">{fmtInt(r.cases_affected)}</td>80                      <td className="num">{fmtInt(r.cases_profiled)}</td>81                      <td className="num font-medium">{fmtPct(r.frequency, 1)}</td>82                      <td>83                        <span className="inline-flex gap-1">84                          <SourceBadge p={toInfo(prov.get(r.provenance_id)) ?? { sourceSlug: r.source_slug, sourceName: r.source_name }} />85                          <ClaimBadge kind="observed" />86                        </span>87                      </td>88                    </tr>89                  ))}90                </tbody>91              </table>92            </div>93            <Freshness dataUpdatedAt={c.updated_at} sourceVersion={c.data_release} extra="frequency = affected / profiled, as published by the cohort" />94          </section>95        );96      })}97    </div>98  );99}100101/** Gene-centric view: one compact row per cohort (used on /gene/[symbol]); denominators stay mandatory. */102export function GeneFrequencyTable({ rows, prov }: { rows: FreqRow[]; prov: Map<number, ProvRow> }) {103  const sorted = [...rows].sort((a, b) => b.frequency - a.frequency);104  return (105    <div className="ci-table-wrap">106      <table className="ci-table">107        <thead>108          <tr>109            <th>Cohort</th>110            <th>Mapped cancer</th>111            <th>Alteration</th>112            <th className="num">Affected (n)</th>113            <th className="num">Profiled (n)</th>114            <th className="num">Frequency (%)</th>115            <th className="num">Rank in cohort</th>116            <th>Source</th>117          </tr>118        </thead>119        <tbody>120          {sorted.map((r) => (121            <tr key={r.id}>122              <td>123                <span className="font-medium">{r.cohort_name}</span> <span className="ci-mono text-[11.5px] text-ink-3">{r.study_id}</span>124              </td>125              <td>126                {r.cancer_slug ? (127                  <>128                    <Link className="ci-link" href={`/cancer/${r.cancer_slug}`}>129                      {r.cancer_name}130                    </Link>{' '}131                    <MatchBadge matchType={r.cancer_match_type} />132                  </>133                ) : (134                  <span className="text-ink-3">unmapped</span>135                )}136              </td>137              <td>138                <Badge tone="outline">{humanize(r.alteration_type)}</Badge>139              </td>140              <td className="num">{fmtInt(r.cases_affected)}</td>141              <td className="num">{fmtInt(r.cases_profiled)}</td>142              <td className="num font-medium">{fmtPct(r.frequency, 1)}</td>143              <td className="num text-ink-3">{r.rank ?? '—'}</td>144              <td>145                <span className="inline-flex gap-1">146                  <SourceBadge p={toInfo(prov.get(r.provenance_id)) ?? { sourceSlug: r.source_slug, sourceName: r.source_name }} />147                  <ClaimBadge kind="observed" />148                </span>149              </td>150            </tr>151          ))}152        </tbody>153      </table>154      <p className="mt-1 text-[12px] text-ink-3">frequency = affected / profiled, as published by each cohort; cohorts are not pooled.</p>155    </div>156  );157}158
Files · 36d464dcc8d9fe84217ecbfd89a225759f5f0c06
Menu

Theme