Design principles and patterns for medical data visualization including accessibility, chart selection, and clinical context. Use when designing or reviewing health data visualizations.
This skill documents design principles, patterns, and best practices for visualizing medical sleep therapy data in OSCAR Export Analyzer.
Always show clinical thresholds and normal ranges to help users interpret values.
// ❌ Show AHI without context
<Plot data={[{ x: dates, y: ahiValues }]} />
// ✅ Show AHI with severity thresholds
<Plot
data={[
{ x: dates, y: ahiValues, name: 'AHI', type: 'scatter' },
// Normal range
{ x: dates, y: Array(dates.length).fill(5), name: 'Normal threshold', type: 'line', line: { dash: 'dash', color: 'green' } },
// Moderate range
{ x: dates, y: Array(dates.length).fill(15), name: 'Moderate threshold', line: { dash: 'dash', color: 'orange' } },
// Severe range
{ x: dates, y: Array(dates.length).fill(30), name: 'Severe threshold', line: { dash: 'dash', color: 'red' } },
]}
/>
Use rolling averages to show patterns, not just noisy raw data.
// Show both raw and smoothed data
const smoothed7Day = rollingAverage(ahiValues, 7);
const smoothed30Day = rollingAverage(ahiValues, 30);
<Plot
data={[
{
x: dates,
y: ahiValues,
name: 'Daily AHI',
mode: 'markers',
marker: { opacity: 0.3 },
},
{ x: dates, y: smoothed7Day, name: '7-day average', mode: 'lines' },
{
x: dates,
y: smoothed30Day,
name: '30-day average',
mode: 'lines',
line: { width: 3 },
},
]}
/>;
Highlight unusual values that might indicate sensor errors or significant events.
// Detect outliers
const { normal, outliers } = detectOutliers(ahiValues);
<Plot
data={[
{ x: normalDates, y: normal, name: 'Normal readings', type: 'scatter' },
{
x: outlierDates,
y: outliers,
name: 'Outliers',
mode: 'markers',
marker: { color: 'red', size: 10 },
},
]}
/>;
Show same data in different ways to reveal different insights.
// Time-series + distribution + correlation
<div className="analysis-grid">
{/* Time trend */}
<TimeSeries data={sessions} />
{/* Distribution */}
<Histogram data={sessions} />
{/* Correlation */}
<ScatterPlot x={epapValues} y={ahiValues} />
</div>
Design visualizations to answer: "Is my therapy working?"
// Show compliance and effectiveness together
<ComplianceAndEffectiveness
usageHours={usageValues}
ahiValues={ahiValues}
complianceThreshold={4} // 4 hours per night
effectivenessThreshold={5} // AHI < 5
/>
When to use: Daily metrics, therapy progression, change detection
// Line chart for continuous trends
<Plot
data={[
{
x: dates,
y: ahiValues,
type: 'scatter',
mode: 'lines+markers',
name: 'AHI',
},
]}
layout={{
xaxis: { title: 'Date', type: 'date' },
yaxis: { title: 'AHI (events/hour)' },
title: 'AHI Trends Over Time',
}}
/>
When to use: Understanding typical values, identifying patterns
// Histogram for value distribution
<Plot
data={[
{
x: ahiValues,
type: 'histogram',
nbinsx: 20,
name: 'AHI Distribution',
},
]}
layout={{
xaxis: { title: 'AHI (events/hour)' },
yaxis: { title: 'Number of Nights' },
title: 'AHI Distribution',
}}
/>
When to use: Comparing periods, showing variability
// Box plot for comparing before/after
<Plot
data={[
{ y: beforeValues, type: 'box', name: 'Before Adjustment' },
{ y: afterValues, type: 'box', name: 'After Adjustment' },
]}
layout={{
yaxis: { title: 'AHI (events/hour)' },
title: 'AHI Before and After EPAP Adjustment',
}}
/>
When to use: Testing relationships between variables
// Scatter for EPAP vs AHI correlation
<Plot
data={[
{
x: epapValues,
y: ahiValues,
type: 'scatter',
mode: 'markers',
text: dates,
marker: { size: 10 },
},
]}
layout={{
xaxis: { title: 'EPAP Pressure (cmH₂O)' },
yaxis: { title: 'AHI (events/hour)' },
title: 'EPAP vs AHI Correlation',
}}
/>
When to use: Weekly patterns, compliance tracking
// Calendar heatmap (GitHub-style)
<Plot
data={[
{
z: usageByWeek, // 2D array: weeks x 7 days
type: 'heatmap',
colorscale: [
[0, 'lightgray'],
[1, 'green'],
],
},
]}
layout={{
xaxis: { title: 'Day of Week' },
yaxis: { title: 'Week' },
title: 'Usage Compliance Calendar',
}}
/>
// ❌ Poor contrast
const colors = {
line: '#87CEEB', // Light blue on white background
text: '#CCCCCC', // Light gray on white
};
// ✅ WCAG AA compliant (4.5:1 minimum)
const colors = {
line: '#0066CC', // Dark blue
text: '#333333', // Dark gray
background: '#FFFFFF',
};
Test contrast:
// ❌ Red/green (indistinguishable for colorblind users)
const palette = ['#FF0000', '#00FF00', '#0000FF'];
// ✅ Colorblind-safe (use blue/orange/gray)
const palette = ['#0072B2', '#D55E00', '#666666'];
// ✅ Also use patterns, not just color
data={[
{ name: 'AHI', line: { color: '#0072B2', dash: 'solid' } },
{ name: 'Goal', line: { color: '#D55E00', dash: 'dash' } },
]}
// Add accessible names and descriptions
<div
role="img"
aria-label="Chart showing AHI trends from Jan 1 to Jan 31"
aria-describedby="chart-description"
>
<Plot data={data} layout={layout} />
</div>
<p id="chart-description" className="sr-only">
Line chart showing AHI values ranging from 3.2 to 12.5 events per hour. AHI decreases over
time, indicating therapy improvement.
</p>
// Plotly has built-in keyboard support, but ensure controls are keyboard-accessible
<div className="chart-controls">
<button onClick={handleZoomIn} aria-label="Zoom in">
🔍+
</button>
<button onClick={handleZoomOut} aria-label="Zoom out">
🔍-
</button>
<button onClick={handleReset} aria-label="Reset zoom">
↺ Reset
</button>
</div>
// ❌ Technical jargon
const labels = {
xaxis: 'Date (ISO 8601)',
yaxis: 'AHI (apnea-hypopnea index)',
};
// ✅ Clear and approachable
const labels = {
xaxis: 'Date',
yaxis: 'Apnea Events per Hour (AHI)',
};
// ✅ With tooltip explanation
<Tooltip content="AHI measures how many times per hour you stop breathing (apnea) or breathe shallowly (hypopnea) during sleep. Lower is better." />;
// Informative tooltips with context
layout={{
hovermode: 'closest',
hoverlabel: {
bgcolor: 'white',
font: { size: 14 },
},
}}
// Custom hover template