SPB Git

spb/cancerindex

Public
8.2 KB · 188 lines tsx
1import Link from 'next/link';2import { Badge, ClaimBadge, MatchBadge, isExactMatch } from '@/components/ui/badge';3import { SourceBadge, TableProvenance } 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/**11 * Gene alteration frequencies with mandatory denominators (§260-261), one table per cohort.12 * Every row of a cohort table shares one provenance (one cohort, one release), so the popover is13 * rendered once per cohort in the caption; rows carry the compact badge.14 */15export function FrequencyTables({ rows, prov, cohortFilter, showCancer = false }: { rows: FreqRow[]; prov: Map<number, ProvRow>; cohortFilter?: string | null; showCancer?: boolean }) {16  const cohorts = new Map<string, FreqRow[]>();17  for (const r of rows) {18    if (cohortFilter && r.cohort_id !== cohortFilter) continue;19    if (!cohorts.has(r.cohort_id)) cohorts.set(r.cohort_id, []);20    cohorts.get(r.cohort_id)!.push(r);21  }22  return (23    <div className="space-y-8">24      {[...cohorts.entries()].map(([cohortId, rs]) => {25        const c = rs[0]!;26        const info = toInfo(prov.get(c.provenance_id)) ?? { sourceSlug: c.source_slug, sourceName: c.source_name, datasetVersion: c.data_release };27        return (28          <section key={cohortId} aria-label={c.cohort_name}>29            <header className="mb-1.5 flex flex-wrap items-baseline justify-between gap-2">30              <h3 className="font-sans text-[15px] font-medium">31                {c.cohort_name} <span className="ci-mono text-[12px] text-ink-3">{c.study_id}</span>32                {c.program ? <Badge className="ml-2">{c.program}</Badge> : null}33              </h3>34              <p className="text-[12.5px] text-ink-3">35                {c.cases_with_ssm != null ? (36                  <>37                    <span className="ci-num">{fmtInt(c.cases_with_ssm)}</span> cases with simple somatic mutation data38                  </>39                ) : null}40                {c.case_count != null ? (41                  <>42                    {' '}43                    · <span className="ci-num">{fmtInt(c.case_count)}</span> cases total44                  </>45                ) : null}46                {c.data_release ? <span className="ci-mono"> · {c.data_release}</span> : null}47                {showCancer && c.cancer_slug ? (48                  <>49                    {' '}50                    · mapped to{' '}51                    <Link className="ci-link" href={`/cancer/${c.cancer_slug}`}>52                      {c.cancer_name}53                    </Link>{' '}54                    <MatchBadge matchType={c.cancer_match_type} />55                  </>56                ) : null}57              </p>58            </header>59            <TableProvenance p={info} claim={<ClaimBadge kind="observed" />}>60              {rs.length} gene{rs.length === 1 ? '' : 's'} · frequency = affected / profiled, as published by the cohort.61            </TableProvenance>62            <div className="ci-table-wrap">63              <table className="ci-table">64                <thead>65                  <tr>66                    <th scope="col" className="num">67                      #68                    </th>69                    <th scope="col">Gene</th>70                    <th scope="col">Alteration</th>71                    <th scope="col" className="num">72                      Affected (n)73                    </th>74                    <th scope="col" className="num">75                      Profiled (n)76                    </th>77                    <th scope="col" className="num">78                      Frequency (%)79                    </th>80                    <th scope="col">Source</th>81                  </tr>82                </thead>83                <tbody>84                  {rs.map((r, i) => (85                    <tr key={r.id}>86                      <td className="num text-ink-3">{r.rank ?? i + 1}</td>87                      <td>88                        <Link className="ci-mono ci-link font-medium" href={`/gene/${r.gene_symbol}`}>89                          {r.gene_symbol}90                        </Link>91                      </td>92                      <td>93                        <Badge tone="outline">{humanize(r.alteration_type)}</Badge>94                      </td>95                      <td className="num">{fmtInt(r.cases_affected)}</td>96                      <td className="num">{fmtInt(r.cases_profiled)}</td>97                      <td className="num font-medium">{fmtPct(r.frequency, 1)}</td>98                      <td>99                        <SourceBadge compact title={null} p={info} />100                      </td>101                    </tr>102                  ))}103                </tbody>104              </table>105            </div>106            <Freshness dataUpdatedAt={c.updated_at} sourceVersion={c.data_release} />107          </section>108        );109      })}110    </div>111  );112}113114/**115 * Gene-centric view: one compact row per cohort (used on /gene/[symbol]); denominators stay mandatory.116 * Rows come from different cohorts (different releases), so each compact badge carries a `title`117 * with dataset · version · retrieved; the caption shows one popover for the source.118 */119export function GeneFrequencyTable({ rows, prov }: { rows: FreqRow[]; prov: Map<number, ProvRow> }) {120  const sorted = [...rows].sort((a, b) => b.frequency - a.frequency);121  const first = sorted[0];122  const caption = first ? (toInfo(prov.get(first.provenance_id)) ?? { sourceSlug: first.source_slug, sourceName: first.source_name }) : null;123  return (124    <div>125      {caption ? (126        <TableProvenance p={caption} claim={<ClaimBadge kind="observed" />}>127          {sorted.length} cohort row{sorted.length === 1 ? '' : 's'} · frequency = affected / profiled, as published by each cohort; cohorts are not pooled. Hover a row badge for its release.128        </TableProvenance>129      ) : null}130      <div className="ci-table-wrap">131        <table className="ci-table">132          <thead>133            <tr>134              <th scope="col">Cohort</th>135              <th scope="col">Mapped cancer</th>136              <th scope="col">Alteration</th>137              <th scope="col" className="num">138                Affected (n)139              </th>140              <th scope="col" className="num">141                Profiled (n)142              </th>143              <th scope="col" className="num">144                Frequency (%)145              </th>146              <th scope="col" className="num">147                Rank in cohort148              </th>149              <th scope="col">Source</th>150            </tr>151          </thead>152          <tbody>153            {sorted.map((r) => (154              <tr key={r.id}>155                <td>156                  <span className="font-medium">{r.cohort_name}</span> <span className="ci-mono text-[11.5px] text-ink-3">{r.study_id}</span>157                </td>158                <td>159                  {r.cancer_slug ? (160                    <>161                      <Link className="ci-link" href={`/cancer/${r.cancer_slug}`}>162                        {r.cancer_name}163                      </Link>164                      {!isExactMatch(r.cancer_match_type) ? <MatchBadge matchType={r.cancer_match_type} className="ml-1" /> : null}165                    </>166                  ) : (167                    <span className="text-ink-3">unmapped</span>168                  )}169                </td>170                <td>171                  <Badge tone="outline">{humanize(r.alteration_type)}</Badge>172                </td>173                <td className="num">{fmtInt(r.cases_affected)}</td>174                <td className="num">{fmtInt(r.cases_profiled)}</td>175                <td className="num font-medium">{fmtPct(r.frequency, 1)}</td>176                <td className="num text-ink-3">{r.rank ?? '—'}</td>177                <td>178                  <SourceBadge compact p={toInfo(prov.get(r.provenance_id)) ?? { sourceSlug: r.source_slug, sourceName: r.source_name, dataset: r.cohort_name, datasetVersion: r.data_release }} />179                </td>180              </tr>181            ))}182          </tbody>183        </table>184      </div>185    </div>186  );187}188
Files · 602dc111f3dd7d7dec082e01832909f07dca7bd3
Menu

Theme