import type { Observation } from "fhir/r4"; import { isBloodPressurePanel } from './observationPanelValue'; // One measurement over time, for the staff Record's opened result (patient-chart deck, "Over time"). // A reading joins the series only when it carries the same coded test AND the same unit: a loose match // draws a confident line through somebody else's numbers. export const TREND_WIDTH = 300; export const TREND_HEIGHT = 90; const LEFT = 10; const RIGHT = 290; const TOP = 16; const BOTTOM = 74; /** The deck draws five readings; more than six crowds the labels under the line at 390. */ const MOST = 6; export interface TrendPoint { id: string; on: string; value: number; x: number; y: number; /** "7.2" - the figure printed under its point. */ figure: string; } export interface ResultTrend { points: TrendPoint[]; unit?: string; /** The first point's year, printed under it. */ since: string; /** "Down from 7.2 % in March", against the reading before this one. */ change?: string; summary: string; } const NOT_A_READING = new Set(["entered-in-error", "cancelled"]); const codesOf = (observation: Observation): string[] => (observation.code?.coding ?? []) .filter((coding) => coding.system && coding.code) .map((coding) => `${coding.system}|${coding.code}`); const dayOf = (observation: Observation): string | undefined => { const when = observation.effectiveDateTime ?? observation.effectivePeriod?.start ?? observation.issued; return when && !Number.isNaN(Date.parse(when)) ? when : undefined; }; const figureOf = (value: number): string => value.toLocaleString("en-US", { maximumFractionDigits: 2 }); const monthOf = (when: string, against: string): string => { const at = new Date(when); const sameYear = at.getFullYear() === new Date(against).getFullYear(); return at.toLocaleDateString("en-US", sameYear ? { month: "long" } : { month: "long", year: "numeric" }); }; /** Every earlier reading of this result's test, oldest first, ending at this one. Undefined below two. */ export const resultTrend = ( observation: Observation | undefined, observations: readonly Observation[], ): ResultTrend | undefined => { // A blood pressure is two numbers; a panel-level number stored beside them is not its trend. if (!observation?.id || isBloodPressurePanel(observation)) return undefined; const codes = new Set(codesOf(observation)); const unit = observation.valueQuantity?.unit; const end = dayOf(observation); if (codes.size === 0 || observation.valueQuantity?.value === undefined || !end) return undefined; const byId = new Map(); for (const candidate of observations) { if (!candidate.id || NOT_A_READING.has(candidate.status)) continue; if (!codesOf(candidate).some((code) => codes.has(code))) continue; if (candidate.valueQuantity?.unit !== unit) continue; const value = candidate.valueQuantity?.value; const on = dayOf(candidate); if (value === undefined || !on || Date.parse(on) > Date.parse(end)) continue; byId.set(candidate.id, { id: candidate.id, on, value }); } const readings = [...byId.values()] .sort((a, b) => Date.parse(a.on) - Date.parse(b.on)) .slice(-MOST); if (readings.length < 2 || readings[readings.length - 1].id !== observation.id) return undefined; const values = readings.map((reading) => reading.value); let low = Math.min(...values); let high = Math.max(...values); if (low === high) { low -= 1; high += 1; } const round = (value: number) => Math.round(value * 10) / 10; const points = readings.map((reading, index) => ({ ...reading, x: round(LEFT + (index / (readings.length - 1)) * (RIGHT - LEFT)), y: round(TOP + ((high - reading.value) / (high - low)) * (BOTTOM - TOP)), figure: figureOf(reading.value), })); const latest = readings[readings.length - 1]; const before = readings[readings.length - 2]; const withUnit = (value: number) => `${figureOf(value)}${unit ? ` ${unit}` : ""}`; const change = latest.value === before.value ? `Same as ${monthOf(before.on, latest.on)}` : `${latest.value < before.value ? "Down" : "Up"} from ${withUnit(before.value)} in ${monthOf(before.on, latest.on)}`; return { points, unit, since: String(new Date(readings[0].on).getFullYear()), change, summary: `${readings.length} readings, from ${withUnit(readings[0].value)} to ${withUnit(latest.value)}.`, }; };