Web: compact gene-centric cohort frequency table
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 | ||||