Skip to content

Commit d70297c

Browse files
committed
update data, fix head and neck
1 parent 1c25d5a commit d70297c

5 files changed

Lines changed: 133 additions & 10 deletions

File tree

.claude/settings.local.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(python3:*)"
5+
]
6+
}
7+
}

CLAUDE.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
TrialMap is a Vue.js 3 SPA that helps researchers optimize clinical trial eligibility criteria using real-world EHR data. It guides users through a 4-step wizard to select a cancer type, trial, endpoint, and eligibility criteria, then visualizes optimized pathway combinations.
8+
9+
Deployed to GitHub Pages at `/TrialMap/` base path. All source code lives in the `web/` subdirectory.
10+
11+
## Commands
12+
13+
All commands must be run from the `web/` directory:
14+
15+
```bash
16+
cd web
17+
npm install # Install dependencies
18+
npm run dev # Start dev server (localhost:5173)
19+
npm run build # Production build → web/dist/
20+
npm run preview # Preview production build locally
21+
```
22+
23+
There are no lint or test scripts configured.
24+
25+
## Architecture
26+
27+
**View switching** is managed entirely in `web/src/Main.vue` via an `activeView` ref (`'home'` | `'step'` | `'result'`). There is no Vue Router — navigation is pure component conditional rendering. `StepView` is force-remounted via `stepViewKey` counter to ensure clean state resets.
28+
29+
**PrimeVue** (v4) is the UI component library with auto-import via `unplugin-vue-components`. Components are resolved automatically — no explicit imports needed for PrimeVue components in templates.
30+
31+
## Data Sources
32+
33+
`web/public/data/meta_data.xlsx` has 3 sheets (parsed via `xlsx` library):
34+
- **Sheet 0 (metadata)**: One row per trial. Columns: `cancer_type`, `trial_name`, `line_of_therapy`, `treatment`, `control`.
35+
- **Sheet 1 (trial_criteria)**: Maps trials to their eligibility criteria. Columns: `trial_name`, `criteria_name`, `must_apply` (truthy = criteria cannot be relaxed).
36+
- **Sheet 2 (criteria)**: Criteria details. Columns: `criteria_name`, `criteria_description`, `type`.
37+
38+
`web/public/data/trail_dataset/[TrialName]_results_web.csv` — Per-trial result files. Each row is one pathway (criteria combination). Columns include: `criteria` (comma-separated criteria names), `hr_os`, `hr_pfs`, `selog_hr_os`, `selog_hr_pfs`, `sucra_os`, `sucra_pfs`, `ae`, `ease`, `g_index`, `number_of_patients`, `path_id`.
39+
40+
## Data Pipeline: StepView (Selection)
41+
42+
In `StepView.vue`, on mount:
43+
1. Fetches `meta_data.xlsx`, parses all 3 sheets into `metadata`, `trialCriteriaRows`, `criteria` refs.
44+
2. **Step 1** — Extracts unique `cancer_type` values from sheet 0 → radio button list.
45+
3. **Step 2** — Filters sheet 0 by selected cancer type → unique `trial_name` list. Displays each trial's `line_of_therapy`, `treatment`, `control` metadata.
46+
4. **Step 3** — User selects endpoint (OS or PFS). PFS is disabled for AHNC cancer type (no progression data in Flatiron DB).
47+
5. **Step 2→3 transition** (`onclickNextTreatment`): Also derives criteria for the selected trial:
48+
- Filters sheet 1 by `trial_name``trialCriteria` (list of criteria names for this trial).
49+
- Computes `mustApplyCriteria` set (criteria with `must_apply` truthy in sheet 1).
50+
- Calls `computeAvailableTokensForTrial()`: fetches the trial CSV, parses all `criteria` column values (comma-split, normalized), builds a set of tokens that actually exist in the data. Falls back to sheet 1 names if CSV fetch fails.
51+
- Builds `filteredCriteria`: joins sheet 2 rows by `criteria_name`, adds `must_apply` and `no_data` flags.
52+
6. **Step 4** — Shows criteria table. `must_apply` criteria are pre-checked and disabled. User selects which criteria to enforce.
53+
7. On "Result" click: emits `{ selectedCancerType, selectedTreatment, selectedEndpoint, selectedCriteria }` to `Main.vue`.
54+
55+
## Data Pipeline: ResultView (Visualization)
56+
57+
In `ResultView.vue`, on mount:
58+
1. Re-fetches `meta_data.xlsx` independently (does not share state with StepView). Extracts trial metadata and criteria rows for the selected trial.
59+
2. Fetches `[TrialName]_results_web.csv`, parses with `xlsx` library.
60+
3. Calls `buildAllPathways(trailResult, selectedCriteria, selectedEndpoint, criteriaNameToIndex)`:
61+
- Normalizes all criteria tokens (lowercase, trimmed).
62+
- Builds `availableTokens` set from all CSV `criteria` column values.
63+
- Computes `effectiveSelected`: selected criteria that actually exist in the CSV (ignores missing ones for filtering).
64+
- Filters CSV rows: keeps rows where `criteria` column contains ALL `effectiveSelected` tokens.
65+
- Maps each row to a UI object with endpoint-specific fields: OS → `hr_os`/`selog_hr_os`/`sucra_os`, PFS → `hr_pfs`/`selog_hr_pfs`/`sucra_pfs`.
66+
- `pathName` is built from criteria indices (1-based) matching the criteria table order.
67+
4. Calls `computeNormalizationParams(allCsvRows, endpoint)`: computes min/max for each metric (HR, selogHR, AE, EASE, G-Index, # patients) across the **entire** CSV (not filtered). Some metrics are reversed (lower=better: HR, selogHR, AE, EASE).
68+
5. **Top 4 paths**: Default sort by `sucra` (descending). `updateDisplayedTop4()` takes first 4 from sorted `AllPathwaysResult`.
69+
6. **Virtual scrolling table**: Full pathway list uses PrimeVue's `virtualScrollerOptions` with lazy loading (`loadPathwaysLazy`). Placeholder objects fill the virtual array; real data is loaded in chunks as user scrolls.
70+
7. **Sorting**: User can sort by any column (HR, AE, # patients, EASE, G-Index, selogHR). `getSortedData()` re-sorts `AllPathwaysResult`. Default (SUCRA) sorts descending; column sorts respect `sortOrder`. Sort changes reset the virtual array and reload.
71+
8. **Origami plots (radar charts)**: Top 4 paths rendered as Chart.js radar charts. Values are normalized to [0.4, 1.0] range via `normalizeRangeValue()` — maps [min, max][0.4, 1.0] with optional reversal. Chart labels alternate between metric names and empty strings (gap spacers for visual separation).
72+
9. **Row selection interaction**: Selecting a row in the top paths table highlights corresponding criteria rows in the left-panel criteria table via `criteriaIndices``selectedCriteriaPath``criteriaRowClass`.
73+
74+
## State Restoration
75+
76+
When user clicks "Return to Selection" in ResultView:
77+
- Emits `returnToSelection` with `{ step: '4' }` to Main.vue.
78+
- Main.vue increments `stepViewKey` to force StepView remount, passes `initialStep='4'` and the previous `resultPayload` as `initialData`.
79+
- StepView's `restoreState()` re-derives all intermediate data (treatment list, criteria, etc.) from the saved selections, then jumps to step 4.
80+
81+
## Deployment
82+
83+
Pushing to `main` triggers the GitHub Actions workflow (`.github/workflows/main.yml`) which builds and deploys to GitHub Pages automatically. The Vite `base` is hardcoded to `/TrialMap/` in `vite.config.js`.

web/public/data/meta_data.xlsx

14 KB
Binary file not shown.

web/src/views/ResultView.vue

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,29 @@ const chartOptions = computed(() => ({
289289
// 缓存图表数据,避免每次渲染都创建新对象
290290
const chartDataList = shallowRef([])
291291
292+
function computeNormParamsFromItems(items) {
293+
const getNums = (key) => items.map(r => Number(r?.[key])).filter(n => Number.isFinite(n))
294+
const makeParams = (arr, reverse) => {
295+
const has = arr.length > 0
296+
const min = has ? Math.min(...arr) : 0
297+
const max = has ? Math.max(...arr) : 0
298+
return { min, max, reverse, min_val: 0.4, max_val: 1 }
299+
}
300+
return {
301+
hr: makeParams(getNums('hr'), true),
302+
selog_hr: makeParams(getNums('selog_hr'), true),
303+
ae: makeParams(getNums('ae'), true),
304+
ease: makeParams(getNums('ease'), true),
305+
g_index: makeParams(getNums('g_index'), false),
306+
number_of_patients: makeParams(getNums('number_of_patients'), false)
307+
}
308+
}
309+
292310
function updateChartDataList() {
293-
const p = normParams.value || {}
294-
chartDataList.value = Top4PathwaysResult.value.map(item => {
311+
const top4 = Top4PathwaysResult.value
312+
if (!top4 || top4.length === 0) { chartDataList.value = []; return }
313+
const p = computeNormParamsFromItems(top4)
314+
chartDataList.value = top4.map(item => {
295315
const gap = 0.25
296316
const values = [
297317
normalizeRangeValue(item.hr, p.hr), gap,
@@ -321,8 +341,8 @@ function updateChartDataList() {
321341
})
322342
}
323343
324-
// 只在 Top4PathwaysResult 或 normParams 变化时更新图表数据
325-
watch([Top4PathwaysResult, normParams], () => {
344+
// 只在 Top4PathwaysResult 变化时更新图表数据
345+
watch([Top4PathwaysResult], () => {
326346
updateChartDataList()
327347
}, {
328348
deep: false, // 不使用深度监听,避免不必要的更新
@@ -480,8 +500,8 @@ function buildAllPathways(trailResult, criteria, endpoint, criteriaNameToIndex)
480500
481501
// Map to UI items, keep all rows
482502
const keyMap = endpoint === 'Overall survival (OS)'
483-
? { srcHr: 'hr_os', srcSelog: 'selog_hr_os', srcSucra: 'sucra_os' }
484-
: { srcHr: 'hr_pfs', srcSelog: 'selog_hr_pfs', srcSucra: 'sucra_pfs' }
503+
? { srcHr: 'hr_os', srcSelog: 'selog_hr_os', srcSucra: 'sucra_os', srcPareto: 'pareto_os' }
504+
: { srcHr: 'hr_pfs', srcSelog: 'selog_hr_pfs', srcSucra: 'sucra_pfs', srcPareto: 'pareto_pfs' }
485505
486506
return filtered.map((row, idx) => {
487507
const rowTokens = String(row.criteria ?? '').split(',').map(normalize)
@@ -506,6 +526,7 @@ function buildAllPathways(trailResult, criteria, endpoint, criteriaNameToIndex)
506526
if (row[keyMap.srcHr] != null) item.hr = row[keyMap.srcHr]
507527
if (row[keyMap.srcSelog] != null) item.selog_hr = row[keyMap.srcSelog]
508528
if (row[keyMap.srcSucra] != null) item.sucra = row[keyMap.srcSucra]
529+
if (row[keyMap.srcPareto] != null) item.pareto = Number(row[keyMap.srcPareto])
509530
return item
510531
})
511532
}
@@ -519,8 +540,20 @@ function getSortedData() {
519540
}
520541
const metric = field || 'sucra'
521542
const isDefault = field == null
522-
// For default (SUCRA): higher is better (desc). Otherwise respect sortOrder (1 asc, -1 desc)
523-
const factor = isDefault ? -1 : (order === -1 ? -1 : 1)
543+
if (isDefault) {
544+
// Default sort: pareto first (descending), then SUCRA descending
545+
const sorted = [...(AllPathwaysResult.value || [])].sort((a, b) => {
546+
const ap = Number(a?.pareto) || 0
547+
const bp = Number(b?.pareto) || 0
548+
if (ap !== bp) return bp - ap // pareto=1 first
549+
const av = asNumber(a?.sucra, -Infinity)
550+
const bv = asNumber(b?.sucra, -Infinity)
551+
return bv - av // higher sucra first
552+
})
553+
return sorted
554+
}
555+
// User-selected column sort: respect sortOrder (1 asc, -1 desc)
556+
const factor = order === -1 ? -1 : 1
524557
const fallbackAsc = factor === 1 ? Infinity : -Infinity
525558
const sorted = [...(AllPathwaysResult.value || [])].sort((a, b) => {
526559
const av = asNumber(a?.[metric], fallbackAsc)

web/src/views/StepView.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ async function restoreState(data) {
235235
if (data.selectedCancerType) {
236236
selectedCancerType.value = data.selectedCancerType
237237
treatment.value = [...new Set(metadata.value.filter(item => item.cancer_type === data.selectedCancerType).map(item => item.trial_name))]
238-
isAHNC.value = String(data.selectedCancerType).trim().toLowerCase() === 'advanced head and neck cancer (ahnc)'
238+
isAHNC.value = String(data.selectedCancerType).trim().toLowerCase().includes('head and neck')
239239
}
240240
241241
if (data.selectedTreatment) {
@@ -299,7 +299,7 @@ function onclickNextCancerType() {
299299
console.log('Selected Cancer Type:', selectedCancerType.value)
300300
treatment.value =[ ...new Set(metadata.value.filter(item => item.cancer_type === selectedCancerType.value).map(item => item.trial_name))]
301301
console.log('Parsed Excel treatment:', treatment.value)
302-
isAHNC.value = String(selectedCancerType.value).trim().toLowerCase() === 'advanced head and neck cancer (ahnc)'
302+
isAHNC.value = String(selectedCancerType.value).trim().toLowerCase().includes('head and neck')
303303
activeStep.value = '2'
304304
}
305305

0 commit comments

Comments
 (0)