Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
"port": 3000,
"autoPort": true
},
{
"name": "viz2-mock-api",
"runtimeExecutable": "node",
"runtimeArgs": [
"/Volumes/Samsung-Ext/dev/deep/DeepPhe-Visualizer-v2/mock-api/server.js"
],
"port": 3333
},
{
"name": "viz2-docs",
"runtimeExecutable": "npm",
Expand Down
45 changes: 42 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Event Timeline** panel on the patient view: temporal relations packed into
overlap-free lanes by Finding, Disease, Stage/Grade, and Treatment, with
relation-specific end caps (On / Overlaps / After / Before), scroll-to-zoom
and a draggable overview band, a per-lane density strip when a lane is
collapsed, and a Patient Age axis derived from the patient's date of birth
relation-specific end caps (On / Overlaps / After / Before), a per-lane
density strip when a lane is collapsed, and a Patient Age axis derived from
the patient's date of birth
- **Overview + detail date range** on both the **Patient Document Timeline** and
the **Event Timeline**, from one shared implementation. An overview strip under
each chart shows every document or event across the full date range with fixed
end dates, and its two handles select the range the chart shows. Each handle
labels its date while zoomed, handles can't cross, and range changes are
announced to screen readers. Both timelines share the same controls: zoom %,
zoom in and out, pan earlier and later, reset, and the `+` / `-` / `0` /
`←` / `→` keys
- The Patient Document Timeline and the Event Timeline are **linked**: they
share one date range covering every document and event, a slider, button or
key in either moves both, and their strips, handles and date axes line up
vertically on screens 620px and wider. A handle released within a few pixels
of either end snaps to it
- Selecting a mark in the Event Timeline highlights its concepts in the
Document Viewer, and clearing the concept there clears the mark
- Patient demographics (gender, race, birth date) are filled from a bundled
Expand All @@ -26,8 +39,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
collapsible concept column inside the Document Viewer
- Clickable **Patient Summary** findings that open their source note in the
Document Viewer
- A **View** control on the patient view switches between the **Improved**
presentation (default) and the **Alpha** one, and remembers the choice. The
improved view compares the cancers in one matrix with tumors nested and
undocumented fields shown as a dash, drops a level of nested borders, uses
four text sizes, bands alternate event lanes, marks negated events with a
dashed line as well as red, names the visible date range against the whole
range, and replaces the two magnifier icons with + and −
- The improved Cancer and Tumor Detail keeps its height down: TNM reads as one
row ("T1 M0") rather than three, rows with nothing to compare (nothing
documented, or a tumor only repeating its cancer) fold behind a count, and in
the patient drawer the panel opens folded to its header, which reports how
many attributes differ. Nothing is dropped: the completeness count still
counts every field, and one click shows the folded rows

### Changed
- The Patient Document Timeline's date range slider was drawn against the full
date range but sat under the zoomed axis, so its handle positions were easy to
misread by months. Its handles now sit on the overview strip's own fixed axis
- The Event Timeline no longer zooms on mouse-wheel scroll (the page scrolls,
as on the document timeline), zooms up to 1600% rather than without limit, and
labels its axes with evenly spaced dates. Event and birth dates are now built
at local midnight, like document dates, so linked timelines print the same
calendar date for the same position in every time zone. Its pan buttons
move a fifth of the visible range; they previously moved a fifth of the whole
range, which skipped past events at high zoom
- `d3-zoom`, `d3-brush`, `d3-selection`, and `d3-axis` are no longer
dependencies. `d3-scale` and `d3-interpolate`, which the Event Timeline
imports directly, are now declared rather than resolved through `@mui/x-charts`
- **Patient Details** and **Cancer and Tumor Detail** now flow across the full
panel width instead of stacking in a single column, reclaiming roughly 300px
of vertical space above the fold on a wide screen
Expand Down
41 changes: 41 additions & 0 deletions docs/explore-patient/document-timeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,47 @@ The **Patient Document Timeline** plots a patient's notes over time so you can p
- **Episode colors** group documents that belong to the same care episode.
- A **document count** near the heading shows how many notes the patient has.

## Zoom into a date range

The chart has two parts:

- The **detail view** on top shows the documents in the selected date range. Its date axis always runs from the start to the end of that range.
- The **overview strip** underneath always shows the patient's full date range. A small tick marks every document, in its report type's row and episode color. The dates at its left and right ends never change.

Two handles on the overview strip select the range the detail view shows.

- **Drag a handle** to move that end of the range. Each handle shows its date underneath while you are zoomed in. The handles cannot cross.
- **Drag the shaded window** between the handles to move the range without changing its length.
- **Press the strip outside the window** to center the range there.
- Use the buttons beside the heading to **zoom in**, **zoom out**, **pan earlier**, **pan later**, or **reset** to the full range. The percentage shows the current zoom, up to 1600%.
- When zoomed in, you can also **drag the detail view** sideways to pan.

Scrolling the mouse wheel over the chart scrolls the page; it does not zoom.

With the keyboard:

| Key | Where | Action |
| --- | --- | --- |
| `+` / `-` | Anywhere in the chart | Zoom in / out |
| `0` | Anywhere in the chart | Reset to the full range |
| `←` / `→` | A document point | Pan earlier / later |
| `←` / `→` | A handle or the window | Move it by 5% of the full range (**Shift**: 20%) |
| `Home` / `End` | A handle or the window | Move to the start or end of the full range |

Screen readers announce the selected range, for example "Showing Nov 22, 2010 to Mar 4, 2011", shortly after you stop changing it.

### Linked to the Event Timeline

When the patient also has an [Event Timeline](event-timeline.md), the two timelines are linked:

- They share one date range, wide enough for every document and every event. If events go back further than the first document, the document timeline starts earlier too, with empty space before its first document.
- Moving a handle, a button, or a key in either timeline moves both.
- Their overview strips, handles, and date axes line up, so a date sits directly above the same date in the other timeline.

The strips line up on screens at least 620 pixels wide. On narrower screens the timelines stay linked but do not line up.

A handle released within a few pixels of either end snaps to that end.

## Open a document

- **Click a point** to open that document in the [Document Viewer](document-viewer.md).
Expand Down
23 changes: 17 additions & 6 deletions docs/explore-patient/event-timeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Events are grouped into four lanes, labelled on the left with the number of mark
- **Stage, Grade**; and
- **Treatment**.

The Improved view bands alternate lanes, so a mark reads against its own lane rather than blending into the next.

Within a lane, marks are packed onto as few rows as possible — two events share a row when their date ranges do not overlap. A row is not a single concept, so hover a mark to read which concepts it covers.

Where several concepts in the same lane share exactly the same start and end date, they are drawn as **one mark**. The tooltip lists every concept it stands for, with a `(xN)` count when a name repeats.
Expand All @@ -35,7 +37,7 @@ The shape of each end tells you how precisely the date is known:

The legend above the chart shows each of these.

Marks are **green**. A **red** mark means the concept is **negated** — the note records its absence, not its presence. Colour is not the only cue: the tooltip and the screen-reader label both state the negation, and the relation and dates are given in words.
Marks are **green**. A **red** mark means the concept is **negated** — the note records its absence, not its presence. In the **Improved** view (see [Choose how the patient view reads](overview.md#choose-how-the-patient-view-reads)) a negated mark is also **dashed**, so the distinction does not depend on telling red from green, and the legend includes a key for it. In either view the tooltip and the screen-reader label state the negation, and the relation and dates are given in words.

Dashed vertical guidelines mark every date that carries an event, so you can line marks up across lanes.

Expand All @@ -51,12 +53,21 @@ The age axis only appears when the patient's date of birth is known. When it is

## Change the date range

The chart opens showing the full range of the patient's events, padded slightly at both ends.
The chart opens showing the full range of the patient's events, padded by 50 days at both ends. It works the same way as the [Patient Document Timeline](document-timeline.md#zoom-into-a-date-range).

The **Date** strip beneath the chart is an overview of the full range. It never moves: its left and right dates stay fixed, and it draws a small copy of every mark, one row per lane. Two handles on it select the range the chart above shows.

- **Drag a handle** to move that end of the range. While zoomed in, each handle shows its date underneath. The handles cannot cross.
- **Drag the shaded window** to move the range, or **press the strip outside the window** to center the range there.
- Use the buttons above the chart to **zoom in**, **zoom out**, **pan earlier**, **pan later**, or **reset**. Each pan moves the range by a fifth of its length. Zoom goes up to 1600%.
- When zoomed in, **drag the chart** sideways to pan.
- With a mark focused, press `+` or `-` to zoom, `0` to reset, and `←` or `→` to pan. On a focused handle, `←` and `→` move it, and `Home` and `End` jump to either end.

Scrolling the mouse wheel over the chart scrolls the page; it does not zoom. Collapsing a lane, or switching **Showing**, keeps the current range.

- **Scroll** over the chart to zoom the date axis in and out.
- **Drag** on the **Date** band beneath the chart to select a narrower window.
The Event Timeline is **linked to the [Patient Document Timeline](document-timeline.md#linked-to-the-event-timeline)**. Both show the same date range, changing it in either one changes both, and their strips and date axes line up vertically, so an event sits directly below the documents written at the same time.

The two stay in step: zooming moves the band, and dragging the band rescales the chart.
All dates on this chart are calendar dates, shown the same in every time zone.

## Collapse a lane

Expand All @@ -82,7 +93,7 @@ The **Showing** control above the chart filters the timeline:
- **All Patient Events** — every temporal relation for the patient.
- **Filtered Patient Events** — only relations whose concepts appear in the report currently open in the Document Viewer.

The count beside the panel title tells you how many relations are in view.
The count beside the panel title tells you how many relations are in view. Below the chart, the Improved view spells out the difference between the two numbers: relations that share a lane and both dates are drawn as one span.
The current report name appears above the chart. Filtering preserves the date
range so that the remaining events stay in the same positions. If a report has
no matching events, the Showing control remains available to return to all events.
Expand Down
16 changes: 16 additions & 0 deletions docs/explore-patient/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ The patient opens as a tab in the drawer.

**Standalone Patient View (from Home).** A separate page where you look a patient up by ID. It shows demographics, cancer and tumor detail, the document timeline, the Event Timeline, and the Document Viewer, but **not** the structured Patient Summary. See [Standalone Patient View](standalone-patient-view.md).

## Choose how the patient view reads

The **View** control at the top of the patient view switches between two presentations. Your choice is remembered, and it applies to both the embedded and the standalone view.

**Improved** (the default) is laid out for scanning:

- The cancers are compared in **one matrix** — attributes down the side, one column per cancer, tumors nested underneath — so differences like a grade of 3 against 1 sit in a single column. Values a tumor shares with its cancer read "Same as cancer" rather than repeating.
- **Undocumented values are shown as a dash**, never hidden, and each cancer says how many of its fields are documented.
- Panels use one level of containment, and one set of text sizes for panel titles, field labels, values and axis ticks.
- The timelines say which range they are showing, band alternate event lanes, and mark negated events with a **dashed** line as well as red.
- Zoom in and out read as **+** and **−** rather than two similar magnifiers.

**Alpha** keeps the original reading, faithful to the DeepPhe-Viz-v2-alpha code the Event Timeline was ported from: per-cancer cards, solid marks that carry negation in color alone, and the alpha's wording and controls.

Both show the same data. Only the presentation changes.

## What the embedded view contains

Depending on the available data, the embedded view can include:
Expand Down
7 changes: 7 additions & 0 deletions mock-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Serves the DeepPhe data API from the fixtures vendored in `mock-api/fixtures/`
`public/data/demographics/patient_demographics.json`. No sibling checkout is
required; set `FIXTURE_DIR` to serve from elsewhere.

`FIXTURE_DIR` can point at either layout:

- flat patient files, such as `mock-api/fixtures/fake_patient1.json`
- directory-shaped DeepPhe JSON output, such as
`dphe_output_500/json/fake_patient500/fake_patient500.json` plus that
patient's `*_Doc.json`, `*_Cancers.json`, and `*_Concepts.json` files

```bash
npm run mock-api # http://localhost:3333
npm start # the app expects the API on 3333
Expand Down
82 changes: 75 additions & 7 deletions mock-api/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,85 @@ const demographicsRecords = readJson(DEMOGRAPHICS_FILE, []);
// anything, so serve the real API's spec verbatim.
const openApiSpec = readJson(path.join(__dirname, "openapi.json"), { openapi: "3.0.0", paths: {} });

const patientIds = fs
.readdirSync(FIXTURE_DIR)
.filter((name) => /^fake_patient\d+\.json$/.test(name))
.map((name) => name.replace(/\.json$/, ""))
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
function sortPatientIds(left, right) {
return left.localeCompare(right, undefined, { numeric: true });
}

function sortFileNames(left, right) {
return left.localeCompare(right, undefined, { numeric: true });
}

function discoverPatientSources(fixtureDir) {
const sources = new Map();
const entries = fs.readdirSync(fixtureDir, { withFileTypes: true });

entries.forEach((entry) => {
if (entry.isFile() && /^fake_patient\d+\.json$/.test(entry.name)) {
const patientId = entry.name.replace(/\.json$/, "");
sources.set(patientId, {
kind: "flat",
path: path.join(fixtureDir, entry.name),
});
return;
}

if (entry.isDirectory() && /^fake_patient\d+$/.test(entry.name)) {
const patientId = entry.name;
const patientDir = path.join(fixtureDir, patientId);
const patientFile = path.join(patientDir, `${patientId}.json`);
if (fs.existsSync(patientFile)) {
sources.set(patientId, {
kind: "directory",
path: patientDir,
});
}
}
});

return sources;
}

function loadDirectoryPatient(patientId, patientDir) {
const patient = readJson(path.join(patientDir, `${patientId}.json`), {
id: patientId,
name: patientId,
});
const fileNames = fs.readdirSync(patientDir).sort(sortFileNames);
const documents = fileNames
.filter((fileName) => /_Doc\.json$/.test(fileName))
.map((fileName) => readJson(path.join(patientDir, fileName), null))
.filter(Boolean);
const cancers = readJson(path.join(patientDir, `${patientId}_Cancers.json`), []);
const conceptsPayload = readJson(path.join(patientDir, `${patientId}_Concepts.json`), {
concepts: [],
conceptRelations: [],
});

return {
...patient,
documents,
cancers: Array.isArray(cancers) ? cancers : [],
concepts: Array.isArray(conceptsPayload?.concepts) ? conceptsPayload.concepts : [],
conceptRelations: Array.isArray(conceptsPayload?.conceptRelations)
? conceptsPayload.conceptRelations
: [],
};
}

const patientSources = discoverPatientSources(FIXTURE_DIR);
const patientIds = [...patientSources.keys()].sort(sortPatientIds);

const fixtureCache = new Map();
function loadPatient(patientId) {
if (!patientIds.includes(patientId)) return null;
const source = patientSources.get(patientId);
if (!source) return null;
if (!fixtureCache.has(patientId)) {
fixtureCache.set(patientId, readJson(path.join(FIXTURE_DIR, `${patientId}.json`), null));
fixtureCache.set(
patientId,
source.kind === "directory"
? loadDirectoryPatient(patientId, source.path)
: readJson(source.path, null)
);
}
return fixtureCache.get(patientId);
}
Expand Down
Loading
Loading