spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { TOP_LEVEL_CANCERS } from '@cancerindex/ontology';4import { PageHeader, Section } from '@/components/ui/section';5import { EmptyState } from '@/components/ui/empty-state';6import { Badge } from '@/components/ui/badge';7import { Tree } from '@/components/data/tree';8import { hierarchyTypes, rootsOf, orphanCount, anatomicalView } from '@/lib/queries/taxonomy';9import { resolveTopLevel } from '@/lib/queries/cancers';10import { fmtInt, humanize } from '@/lib/format';11import { str, type SP } from '@/lib/search-params';1213export const metadata: Metadata = { title: 'Taxonomy', description: 'Browse the cancer taxonomy by NCIt hierarchy, OncoTree hierarchy or anatomical site.' };14export const revalidate = 3600;1516export default async function TaxonomyPage({ searchParams }: { searchParams: Promise<SP> }) {17 const sp = await searchParams;18 const types = await hierarchyTypes();19 const available = types.map((t) => t.hierarchy_type);20 const requested = str(sp, 'view', '');21 const view = requested === 'anatomy' ? 'anatomy' : available.includes(requested) ? requested : available.includes('ncit') ? 'ncit' : (available[0] ?? 'ncit');2223 const topLevel = await resolveTopLevel(TOP_LEVEL_CANCERS.map((t) => t.ncit));24 const [roots, orphans, sites] = await Promise.all([view === 'anatomy' ? Promise.resolve([]) : rootsOf(view), view === 'anatomy' ? Promise.resolve(0) : orphanCount(view), view === 'anatomy' ? anatomicalView() : Promise.resolve([])]);2526 const views = [...available.map((t) => ({ key: t, label: t === 'ncit' ? 'NCIt hierarchy' : t === 'oncotree' ? 'OncoTree hierarchy' : `${humanize(t)} hierarchy`, n: types.find((x) => x.hierarchy_type === t)?.n ?? 0 })), { key: 'anatomy', label: 'Anatomical view', n: null as number | null }];27 if (!available.includes('ncit')) views.unshift({ key: 'ncit', label: 'NCIt hierarchy', n: 0 });2829 return (30 <div>31 <PageHeader kicker="Taxonomy" title="Tree browser" lede="Several hierarchies coexist: the NCIt-derived disease tree, the OncoTree clinical tree and the anatomical grouping. Children load on demand. Non-malignant and precursor entities are shown in grey." />3233 <nav aria-label="Hierarchy" className="flex flex-wrap gap-2 border-y border-rule py-2 text-[13.5px]">34 {views.map((v) => (35 <Link key={v.key} href={`/taxonomy?view=${v.key}`} aria-current={view === v.key ? 'page' : undefined} className={`border px-2.5 py-1 no-underline ${view === v.key ? 'border-accent bg-accent-soft text-accent-2' : 'border-rule text-ink-2 hover:border-accent'}`}>36 {v.label}37 {v.n != null ? <span className="ci-num ml-1.5 text-[11px] text-ink-3">{fmtInt(v.n)} edges</span> : null}38 </Link>39 ))}40 </nav>4142 <div className="mt-6 grid gap-8 lg:grid-cols-[1fr_320px]">43 <div>44 {view === 'anatomy' ? (45 sites.length ? (46 <div className="space-y-6">47 {sites.map((s) => (48 <Section key={s.id} id={`site-${s.slug}`} level={3} title={s.name} description={s.ncit_code ? `NCIt ${s.ncit_code} · ${s.cancers.length} linked ${s.cancers.length === 1 ? 'entity' : 'entities'}` : `${s.cancers.length} linked entities`}>49 {s.cancers.length ? (50 <ul className="columns-1 gap-6 text-[13.5px] sm:columns-2 lg:columns-3">51 {s.cancers.map((c) => (52 <li key={`${c.id}-${c.relation}`} className="flex items-baseline gap-1.5 py-0.5">53 <Link href={`/cancer/${c.slug}`} className={`ci-link ${c.malignant ? '' : 'text-ink-3'}`}>54 {c.canonical_name}55 </Link>56 {c.relation !== 'primary' ? <Badge tone="outline">{c.relation}</Badge> : null}57 </li>58 ))}59 </ul>60 ) : (61 <EmptyState compact>No entity linked to this site yet.</EmptyState>62 )}63 </Section>64 ))}65 </div>66 ) : (67 <EmptyState title="Anatomical sites not yet available">Anatomical sites are populated by the terminology connectors (OncoTree tissues, NCIt anatomy).</EmptyState>68 )69 ) : roots.length ? (70 <>71 <p className="mb-2 text-[12.5px] text-ink-3">72 {fmtInt(roots.length)} root {roots.length === 1 ? 'node' : 'nodes'}73 {orphans > 0 ? (74 <>75 {' '}76 · {fmtInt(orphans)} entities have no edge in this hierarchy and are listed in the{' '}77 <Link className="ci-link" href="/cancers">78 explorer79 </Link>80 </>81 ) : null}82 </p>83 <Tree roots={roots} hierarchyType={view} />84 </>85 ) : (86 <EmptyState title={`${view === 'ncit' ? 'NCIt' : humanize(view)} hierarchy not yet available`} knows={views.filter((v) => v.key !== view && (v.n == null || v.n > 0)).map((v) => ({ label: v.label, href: `/taxonomy?view=${v.key}` }))}>87 {view === 'ncit' ? 'The NCIt connector has not run on this environment yet. NCIt is the primary disease hierarchy; OncoTree edges are available meanwhile.' : 'No edges of this type have been ingested.'}88 </EmptyState>89 )}90 </div>9192 <aside>93 <Section id="top-level" level={3} kicker="Ranking scope" title="Top-level set (36)">94 <p className="mb-2 text-[12.5px] text-ink-3">Mutually exclusive registry site groups used for global rankings, anchored to NCIt concepts. Greyed items are not yet indexed.</p>95 <ul className="text-[13px]">96 {TOP_LEVEL_CANCERS.map((t) => {97 const m = topLevel.get(t.ncit);98 return (99 <li key={t.key} className="flex items-baseline justify-between gap-2 border-b border-rule py-1">100 {m ? (101 <Link href={`/cancer/${m.slug}`} className="ci-link truncate">102 {t.name}103 </Link>104 ) : (105 <span className="truncate text-ink-3">{t.name}</span>106 )}107 <span className="ci-mono shrink-0 text-[10.5px] text-ink-4">{t.icd10.join(', ')}</span>108 </li>109 );110 })}111 </ul>112 </Section>113 </aside>114 </div>115 </div>116 );117}118