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 { 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