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%

Web: compact gene-centric cohort frequency table

Simon-Pierre Boucher committed 16 days ago (Sep 8, 2026) parent 0440a9b

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 7 import { EmptyState } from '@/components/ui/empty-state';
8 8 import { Freshness } from '@/components/ui/freshness';
9 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 11 import { getGeneBySymbol, variantsForGene, frequenciesForGene } from '@/lib/queries/genomics';
12 12 import { evidenceForGene } from '@/lib/queries/evidence';
13 13 import { loadProvenance } from '@/lib/queries/provenance';
@@ -93,7 +93,7 @@ export default async function GenePage({ params }: { params: Promise<{ symbol: s
93 93 </Section>
94 94
95 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 97 </Section>
98 98
99 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 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