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%
8.2 KB · 103 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { PageHeader, Section, KV } from '@/components/ui/section';4import { ClaimBadge } from '@/components/ui/badge';5import { MAP_RAMP } from '@/lib/map-scale';6import { UNMAPPED_COUNTRY_NAMES } from '@cancerindex/ranking';78export const metadata: Metadata = { title: 'Methodology — clinical trial map', description: 'How trial sites per country are counted, aggregated over the cancer hierarchy, mapped to ISO 3166-1 and drawn (Equal Earth, quantile classes).' };9export const revalidate = 3600;1011/** Web rendering of docs/methodology/trial-map.md (kept in sync by hand; the markdown is the reference). */12export default function TrialMapMethodPage() {13  return (14    <div className="ci-prose max-w-4xl">15      <PageHeader kicker="Methodology" title="Clinical trial map" lede="Registered study sites per country, for all oncology trials or one top-level cancer, by phase and recruiting status. Every number is recomputed deterministically from canonical tables; nothing is estimated.">16        <p className="mt-2 flex flex-wrap items-center gap-1.5 text-[13px] text-ink-3">17          <ClaimBadge kind="computed" /> formula <code className="ci-mono">ci-trial-sites-v1</code> · table <code className="ci-mono">trial_site_country_counts</code> · source clinicaltrials ·{' '}18          <Link href="/trials/map" className="ci-link">19            Open the map20          </Link>21        </p>22      </PageHeader>2324      <Section id="definitions" kicker="§11" title="Definitions">25        <KV26          items={[27            { k: 'Site', v: 'One trial_locations row: a facility entered by the registrant for a study. A study listing 40 US facilities contributes 40 sites to the United States. Locations with an empty country are excluded.' },28            { k: 'Trial', v: 'A distinct study with at least one site in the country. A multinational study counts once per country, so the trials column summed over countries exceeds the number of distinct studies; the page headline "Trials" is the distinct count over the whole scope, computed live.' },29            { k: 'Recruiting', v: "The location's own status is RECRUITING; when the registrant gave no location status (about 82 % of rows) the study's overall status RECRUITING is used instead. \"All statuses\" includes completed and withdrawn studies." },30            { k: 'Study type', v: 'Interventional and observational studies are both included; there is no study_type filter.' },31            { k: 'Country name', v: 'The registrant\'s spelling as exported by ClinicalTrials.gov, kept verbatim so the "View trials" link filters the list exactly; ISO 3166-1 alpha-3 is added for drawing.' },32          ]}33        />34      </Section>3536      <Section id="scopes" kicker="§11" title="Scopes and aggregation">37        <p>38          One row per <code className="ci-mono">(cancer, phase, recruiting_only, country)</code>. <strong>Cancer</strong>: all oncology trials, or one of the active top-level cancers; a trial belongs to a top-level cancer when any of its mapped conditions is the cancer <em>or one of its descendants</em> in the NCIt hierarchy (recursive traversal, depth ≤ 12 — the same traversal as the entity counters). A study mapped to several top-level cancers counts in each; the all-trials scope counts it once. <strong>Phase</strong>: any, or Phase 1–4; a PHASE2|PHASE3 study counts under both, EARLY_PHASE1 counts under Phase 1, studies with phase N/A count only under "any". <strong>Recruiting</strong>: as defined above.39        </p>40        <p className="mt-2">41          Counts: <code className="ci-mono">sites = count(*)</code>, <code className="ci-mono">trials = count(DISTINCT trial_id)</code>. Rebuilt in one transaction by <code className="ci-mono">pnpm cix intel</code> (≈ 11 800 rows, 178 country names, about 30 s). The freshness line shows the rebuild time.42        </p>43      </Section>4445      <Section id="iso" kicker="ISO 3166-1" title="Country mapping and unmapped names">46        <p>47          Current short names and legacy long forms ("Korea, Republic of", "Russian Federation", "Viet Nam", "Réunion", "Palestinian Territory, occupied") are mapped to alpha-3, case-, whitespace- and apostrophe-insensitively. Territories keep their own code (Puerto Rico PRI, Hong Kong HKG, Réunion REU, Guam GUM…) because that is how the registrant counted the site; Kosovo uses the user-assigned XKX; "Virgin Islands" is read as the U.S. Virgin Islands.48        </p>49        <p className="mt-2">50          Names without a current ISO code stay unmapped, appear in the table by name and are never painted:{' '}51          {UNMAPPED_COUNTRY_NAMES.filter((n) => n)52            .map((n) => `“${n}”`)53            .join(', ')}54          . A unit test checks that every distinct country name in the database either resolves or is on this explicit list, so a new spelling cannot silently vanish from the map.55        </p>56      </Section>5758      <Section id="drawing" kicker="Cartography" title="Projection, classes and colour">59        <ul>60          <li>61            <strong>Geometry</strong>: Natural Earth 1:110m (world-atlas, public domain); numeric ISO ids converted to alpha-3; Antarctica dropped; Northern Cyprus and Somaliland have no code and render as "no site".62          </li>63          <li>64            <strong>Projection</strong>: Equal Earth (d3-geo), fitted to the sphere in a 960×480 viewBox — equal-area, so high-latitude countries are not visually inflated.65          </li>66          <li>67            <strong>Class breaks</strong>: quantiles (equal number of countries per class), at most 5 classes, computed on the displayed metric over countries with ≥ 1 site for the <em>current filter</em>. Site counts are extremely skewed (the United States hosts about half of all sites; the median country has a few dozen): equal intervals would put every country but one in the first class and a logarithmic scale would hide the difference between 1 and 30 sites. Thresholds are the observed class maxima, so the legend shows the exact range and country count of each class. Colours are comparable within one view only.68          </li>69          <li className="flex flex-wrap items-center gap-2">70            <strong>Colour</strong>: sequential teal ramp71            {MAP_RAMP.map((c) => (72              <span key={c} className="inline-flex items-center gap-1 text-[12px]">73                <span className="inline-block h-3 w-3 border border-rule-strong" style={{ background: c }} aria-hidden /> <code className="ci-mono">{c}</code>74              </span>75            ))}76            ; "no site" is the paper-3 tone. Colour is never the only carrier: each country is a link with a text title, the legend is textual and the full table is always rendered.77          </li>78          <li>79            <strong>City layer</strong>: only when a cancer is selected — live aggregate of locations by (country, city, state) with the mean geocoded position, top 300 by sites, dot area ∝ sites. The registry-wide, all-status city aggregate (≈ 3 s) is not served per request.80          </li>81        </ul>82      </Section>8384      <Section id="limitations" kicker="Caveats" title="Limitations">85        <ul>86          <li>Sites reflect registration practice, not research capacity: US sponsors list every facility, other sponsors often list one coordinating site per country; only ClinicalTrials.gov is ingested.</li>87          <li>Location status is missing for ~82 % of rows; falling back to the study status over-counts recruiting sites at facilities that have closed.</li>88          <li>Basket trials mapped to several top-level cancers count in each cancer scope.</li>89          <li>Coordinates come from the upstream registry; cities without coordinates are absent from the dot layer only.</li>90          <li>Counts are not normalised by population or burden; such views belong to the rankings layer with their own formula version.</li>91        </ul>92        <p className="mt-2 text-[13px] text-ink-3">93          Reference text: <code className="ci-mono">docs/methodology/trial-map.md</code>. General methodology:{' '}94          <Link href="/methodology" className="ci-link">95            How the index is built96          </Link>97          .98        </p>99      </Section>100    </div>101  );102}103