spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1# Knowledge graph — cancer · gene · variant · drug · trial23The knowledge graph at `/graph` (API: `GET /v1/graph/:type/:id`) shows the **contextual neighbourhood4of one entity**: which cancers, genes, variants, drugs and trials a source connects to it, with the5cancer context, direction, evidence level, claim category and provenance of every link6(CLAUDE.md non-negotiable 7, SPEC §29, §244). It is a reading aid over data CancerIndex already7holds — it never adds knowledge of its own.89## Nodes1011| Type | Public id | Focus reference (`/graph?focus=`) | Entity page |12|---|---|---|---|13| cancer | `CI-CAN-…` | `cancer:<slug>` | `/cancer/<slug>` |14| gene | `CI-GENE-…` | `gene:<HGNC symbol>` (aliases and `HGNC:n` resolve) | `/gene/<symbol>` |15| variant | `CI-VAR-…` | `variant:<slug>` | `/variant/<slug>` |16| drug | `CI-DRUG-…` | `drug:<slug>` | `/drug/<slug>` |17| trial | `CI-TRIAL-…` | `trial:<NCT id>` | `/trial/<nct>` |18| approval | `drug_approvals.id` (display only) | not focusable | `/drug/<slug>#approvals` |1920A bare NCT id, a bare CI id or an upper-case symbol is also accepted as `focus`. Approval nodes21appear only for a **drug focus** when an approval record has no mapped cancer (the indication text22is the label); approvals with a mapped cancer are drawn as `APPROVED_FOR` edges to that cancer.2324The **default focus** is the cancer with the most knowledge edges (source, target or context),25computed at request time. The six "most connected" quick links (two cancers, two genes, two26drugs) come from the same count. Nothing is hardcoded.2728## Edges2930Every edge — drawn or tabulated — carries:3132- `relationshipType` (source-native or derived, see below), `outgoing` (focus → neighbour or33 neighbour → focus), `direction` (supports / sensitivity / resistance / unknown / mixed);34- `evidenceLevel` **as stated by the source** (CIViC A–E, ChEMBL max phase 1–4 or mechanism35 action type, openFDA application type, approval status). Scales are never converted into one36 another; they are only *ordered* for display (A, FDA ≻ B, phase 4 ≻ C, phase 3 ≻ D, phase 2 ≻ E,37 phase 1 ≻ others ≻ unknown);38- `evidenceCategory` → claim badge (observed / published / curated / regulatory / guideline /39 computed), never merged;40- `cancerContext` (`cancer_context_ids` resolved to names — the disease the source stated, which41 may be a descendant of the focus), `supportCount`, `sourceIds`, `provenanceIds`, `derived`.4243### Source-native edges (`knowledge_edges`, `derived: false`, solid spokes)4445Rows written by connectors, one per source record, with `status = 'active'`:4647| Relationship | Direction of the row | Source | Native level |48|---|---|---|---|49| `PREDICTS_RESPONSE_TO` | variant → drug (cancer in context) | CIViC | A–E |50| `ASSOCIATED_WITH` | gene → cancer | CIViC | A–E |51| `PROGNOSTIC_IN` / `DIAGNOSTIC_OF` / `PREDISPOSES_TO` | variant → cancer | CIViC | A–E |52| `INVESTIGATED_FOR` | drug → cancer | ChEMBL (max phase), CIViC | 1–4 / A–E |53| `TARGETS` | drug → gene | ChEMBL mechanisms | action type (e.g. INHIBITOR) |54| `APPROVED_FOR` | drug → cancer | openFDA | application type (FDA ORIG) |5556For display the rows are **aggregated per (neighbour, relationship, direction, level, source)**:57`supportCount` is the sum of the rows' `support_count`, `provenanceIds` the union of their58provenance rows, `cancerContext` the union of their contexts, and the table says59"*n* source records aggregated". Aggregation groups identical claims; it never combines60different directions or levels into one.6162For a **cancer focus**, edges where the cancer is only the *context* (variant → drug **in this63cancer**) are included too: the neighbour is the variant, the drug is shown as "→ predicts response64to <drug>" (`via` in the API). These are the edges the "Paths" panel chains.6566### Derived registry links (`derived: true`, dashed spokes)6768Counts and measurements read from registry tables at query time. They state that two entities69co-occur in a registry, **not** that a source asserted a biological or clinical relationship.7071| Relationship | Focus → neighbour | Computed from | Shown as |72|---|---|---|---|73| `STUDIED_IN` | cancer → trial; drug → trial | `trial_conditions.cancer_id` (cancer + descendants) ; `trial_interventions.drug_id` | top 10 by `last_update_posted_date`, plus total and active count (`RECRUITING`, `NOT_YET_RECRUITING`, `ENROLLING_BY_INVITATION`, `ACTIVE_NOT_RECRUITING`) |74| `INVESTIGATED_IN_TRIALS` | cancer ↔ drug | `trial_interventions.drug_id × trial_conditions.cancer_id` | trial count and active count per pair |75| `ALTERED_IN` | cancer ↔ gene | `cancer_gene_frequencies` (GDC / cBioPortal cohorts) | **the cohort with the largest denominator** for the pair: `cases_affected / cases_profiled (frequency)`, alteration type, study id, number of qualifying cohorts. Thresholds: frequency ≥ 0.05 **and** cases affected ≥ 20 on that cohort row. Genes flagged `is_cancer_gene` are listed first |76| `APPROVED_FOR` (derived) | cancer ↔ drug / drug → approval | `drug_approvals` | authority, jurisdiction, status, date as published, tumour-agnostic flag; rows already present as an openFDA `APPROVED_FOR` knowledge edge are skipped |77| `HAS_VARIANT` | gene → variant | `variants.gene_id` | ordered by accepted CIViC evidence items (curated when > 0, otherwise the bare variant record) |78| `HAS_EVIDENCE_IN` | variant → cancer | `civic_evidence_items` (`ACCEPTED`) | items, levels present, sensitivity / resistance counts, supports / does-not-support counts |79| `PREDICTS_RESPONSE_TO` (derived) | variant → drug | `civic_evidence_items` predictive items **without** a knowledge edge yet | gap filler, flagged derived |80| `CONDITION_OF` / `INTERVENTION_OF` | cancer → trial / drug → trial (trial focus) | `trial_conditions`, `trial_interventions` | registry text and `match_type` of the mapping |8182Registry counts have no `provenance` row of their own (they are not a published number); the83edge names the registry source and the query definition above, and cohort frequencies and84approvals carry the provenance id of the underlying row.8586Cancer foci roll up **descendants** (all hierarchy types, depth ≤ 12, capped at 600 ids) for87trials, cohort frequencies and approvals, and the edge's `cancerContext` names the descendant that88actually matched. Source-native edges are not rolled up: they must name the focus itself (or have it89in context).9091## Caps and truncation9293- Per relationship type: 25 aggregated edges (10 for `STUDIED_IN`), ranked by native level, then94 support, then recency. `?more=<RELATIONSHIP>` raises **one** group to 200. Each group shows95 "*shown* of *total*"; the API returns `groups: { [relationshipType]: total }` and `truncated`.96- Drawn nodes: at most **60**. Selection is a deterministic round-robin over entity types (each type97 contributes its highest-degree node in turn) so every type present stays visible; the figure98 caption states how many neighbours are not drawn. The table lists every fetched edge.99- Spokes: at most 3 relationships per neighbour are drawn as parallel lines; all are in the table.100- Paths: 8 chains (`?limit` ≤ 50 in the API).101102## Layout rules (`apps/web/src/lib/graph-model.ts`, unit-tested)103104- Focus at the centre. Neighbours sit on arcs grouped by entity type in the **fixed** clockwise105 order cancer → gene → variant → drug → trial → approval, starting at the top; sectors never106 reorder by size, so the same focus always draws the same picture.107- Sector width ∝ node count with a minimum share; a fixed gap separates sectors; every sector uses108 one of three alternating ring radii so labels at sector borders do not collide.109- Inside a sector nodes are ordered by degree (edges at the node), then label; a lone node sits at110 the sector's centre. Mark radius = 4 + 2.2·log₂(degree + 1), capped at 13.111- Labels run along the spoke (rotated), flipped on the left half, truncated at 18 characters112 (full text in `<title>`). Entity type is encoded by sector position and caption, mark shape113 (circle / square / triangle / rounded square / hexagon / diamond) and a muted fill from the114 design tokens — the picture is readable without colour.115- Solid spoke = source-native curated / regulatory / published claim; dashed = derived count or116 observed data. Every spoke has a `<title>`: relationship · direction · level · context · source.117- Server-rendered SVG only (no client graph library); the SVG scrolls horizontally below 560 px118 and the edge table is the accessible equivalent.119120## Paths (cancer focus)121122`cancer → gene → variant → drug → approval → trials`, built only from what exists:1231241. variant → drug: a source-native `PREDICTS_RESPONSE_TO` edge with `direction = 'sensitivity'`125 whose context contains the cancer or a descendant (aggregated per variant–drug pair);1262. gene: the variant's gene; its cohort frequency in the cancer is the **largest-denominator**127 cohort row with cases affected ≥ 20 — shown as "not yet available" when no cohort covers it;1283. approval: the earliest `drug_approvals` row for the drug in the cancer (or tumour-agnostic),129 with authority, jurisdiction, status and date as published; `null` when none;1304. trials: registry trials listing the drug with a condition mapped to the cancer (total / active).131132Ranking: native evidence level, then support count, then cohort frequency, then names (stable).133Each hop keeps its own claim category. Chains are descriptive; they are not treatment guidance.134135## What the graph never does136137- **No inferred edges.** Two entities are connected only when a source states the relationship or a138 registry row literally joins them. No transitive closure, no similarity, no co-citation.139- **No LLM edges.** Nothing in the graph is generated or ranked by a language model.140- **No re-scaled evidence.** CIViC letters, ChEMBL phases and FDA application types stay in their141 native form; ordering for display is documented above and never shown as a score.142- **No silent merging.** Different directions, levels or sources are separate rows; derived counts143 are never mixed with curated claims; claim badges are never collapsed.144- **No fabricated hops.** A missing frequency, approval or trial count is shown as missing.145146## Implementation147148- Web: `apps/web/src/lib/graph-model.ts` (types, layout, ranking — pure), `apps/web/src/lib/queries/graph.ts`149 (SQL), `apps/web/src/components/graph/{radial-graph,path-chain,graph-link}.tsx`,150 `apps/web/src/app/graph/page.tsx`, home teaser `apps/web/src/components/home/graph-module.tsx`.151- API: `apps/api/src/routes/graph.ts` — same SQL, duplicated on purpose (the web query module is152 `server-only`); keep thresholds and ordering in step.153- Tests: `apps/web/test/graph-layout.test.ts`.154