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%
6.5 KB · 118 lines tsx
Raw Blame History
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