SPB Git

spb/cancerindex

Public

Web: compact gene-centric cohort frequency table

Simon-Pierre Boucher committed (Sep 8, 2026) parent 0440a9b Browse files

2 changed files +60 −2

modified apps/web/src/app/gene/[symbol]/page.tsx +2 −2
@@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge';
7 import { EmptyState } from '@/components/ui/empty-state'; 7 import { EmptyState } from '@/components/ui/empty-state';
8 import { Freshness } from '@/components/ui/freshness'; 8 import { Freshness } from '@/components/ui/freshness';
9 import { EvidenceTable } from '@/components/data/evidence-table'; 9 import { EvidenceTable } from '@/components/data/evidence-table';
10−import { FrequencyTables } from '@/components/data/frequency-table'; 10+import { GeneFrequencyTable } from '@/components/data/frequency-table';
11 import { getGeneBySymbol, variantsForGene, frequenciesForGene } from '@/lib/queries/genomics'; 11 import { getGeneBySymbol, variantsForGene, frequenciesForGene } from '@/lib/queries/genomics';
12 import { evidenceForGene } from '@/lib/queries/evidence'; 12 import { evidenceForGene } from '@/lib/queries/evidence';
13 import { loadProvenance } from '@/lib/queries/provenance'; 13 import { loadProvenance } from '@/lib/queries/provenance';
@@ -93,7 +93,7 @@ export default async function GenePage({ params }: { params: Promise<{ symbol: s
93 </Section> 93 </Section>
94 94
95 <Section id="frequencies" kicker="Cohorts" title={`Alteration frequency by cohort (${fmtInt(freqs.length)})`} description="Frequency = cases affected / cases profiled within one cohort. Cohorts are never pooled."> 95 <Section id="frequencies" kicker="Cohorts" title={`Alteration frequency by cohort (${fmtInt(freqs.length)})`} description="Frequency = cases affected / cases profiled within one cohort. Cohorts are never pooled.">
96− {freqs.length ? <FrequencyTables rows={freqs} prov={prov} showCancer /> : <EmptyState compact>No cohort frequency recorded for this gene.</EmptyState>} 96+ {freqs.length ? <GeneFrequencyTable rows={freqs} prov={prov} /> : <EmptyState compact>No cohort frequency recorded for this gene.</EmptyState>}
97 </Section> 97 </Section>
98 98
99 <Section id="publications" kicker="Literature" title="Linked publications"> 99 <Section id="publications" kicker="Literature" title="Linked publications">
modified apps/web/src/components/data/frequency-table.tsx +58 −0
@@ -97,3 +97,61 @@ export function FrequencyTables({ rows, prov, cohortFilter, showCancer = false }
97 </div> 97 </div>
98 ); 98 );
99 } 99 }
100+
101+/** Gene-centric view: one compact row per cohort (used on /gene/[symbol]); denominators stay mandatory. */
102+export 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+}
100 158
Menu

Theme