medical-data-visualization

star 0

Design principles and patterns for medical data visualization including accessibility, chart selection, and clinical context. Use when designing or reviewing health data visualizations.

kabaka By kabaka schedule Updated 2/7/2026

name: medical-data-visualization description: Design principles and patterns for medical data visualization including accessibility, chart selection, and clinical context. Use when designing or reviewing health data visualizations.

Medical Data Visualization

This skill documents design principles, patterns, and best practices for visualizing medical sleep therapy data in OSCAR Export Analyzer.

Principles for CPAP Data Visualization

1. Context Matters

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' } },
  ]}
/>

2. Trend Visibility

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 },
    },
  ]}
/>;

3. Outlier Clarity

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 },
    },
  ]}
/>;

4. Multiple Perspectives

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>

5. Actionable Insights

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
/>

Chart Type Selection

Time-Series (Trends Over Time)

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',
  }}
/>

Distribution (Value Frequency)

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',
  }}
/>

Box Plot (Statistical Summary)

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',
  }}
/>

Scatter Plot (Correlation)

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',
  }}
/>

Heatmap (Calendar/Pattern Visualization)

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',
  }}
/>

Accessibility Guidelines

Color Contrast (WCAG AA)

// ❌ 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:

  • Use browser DevTools color picker
  • Online tools: WebAIM Contrast Checker
  • Minimum ratio: 4.5:1 for normal text, 3:1 for large text

Colorblind-Safe Palettes

// ❌ 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' } },
]}

ARIA Labels

// 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>

Keyboard Navigation

// 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>

Medical Terminology

Patient-Friendly Labels

// ❌ 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." />;

Hover Tooltips

// Informative tooltips with context
layout={{
  hovermode: 'closest',
  hoverlabel: {
    bgcolor: 'white',
    font: { size: 14 },
  },
}}

// Custom hover template
hovertemplate: '<b>Date:</b> %{x|%Y-%m-%d}<br>' +
               '<b>AHI:</b> %{y:.1f} events/hour<br>' +
               '<b>Severity:</b> %{customdata}<extra></extra>',

customdata: ahiValues.map(ahi =>
  ahi < 5 ? 'Normal' : ahi < 15 ? 'Mild' : ahi < 30 ? 'Moderate' : 'Severe'
),

Plotly Configuration

Responsive Layout

const responsiveLayout = {
  autosize: true,
  margin: { l: 60, r: 20, t: 60, b: 60 },
  font: { size: 14, family: 'system-ui, sans-serif' },
  xaxis: { automargin: true },
  yaxis: { automargin: true },
};

const responsiveConfig = {
  responsive: true,
  displayModeBar: true,
  modeBarButtonsToRemove: ['lasso2d', 'select2d'],
  displaylogo: false,
};

Print-Friendly

// Adjust for print media
const printLayout = {
  ...responsiveLayout,
  paper_bgcolor: 'white',
  plot_bgcolor: 'white',
  font: { color: 'black' },
  xaxis: { ...responsiveLayout.xaxis, gridcolor: '#666666' },
  yaxis: { ...responsiveLayout.yaxis, gridcolor: '#666666' },
};

// Detect print media
const isPrint = window.matchMedia('print').matches;
const layout = isPrint ? printLayout : responsiveLayout;

Dark Mode Support

const darkModeLayout = {
  ...responsiveLayout,
  paper_bgcolor: '#1a1a1a',
  plot_bgcolor: '#2a2a2a',
  font: { color: '#e0e0e0' },
  xaxis: {
    ...responsiveLayout.xaxis,
    gridcolor: '#444444',
    color: '#e0e0e0',
  },
  yaxis: {
    ...responsiveLayout.yaxis,
    gridcolor: '#444444',
    color: '#e0e0e0',
  },
};

// Apply based on theme
const layout = isDarkMode ? darkModeLayout : responsiveLayout;

Statistical Annotations

Change-Points

// Mark significant changes
const changePoint = { date: '2024-01-15', reason: 'EPAP adjusted' };

layout={{
  shapes: [
    {
      type: 'line',
      x0: changePoint.date,
      x1: changePoint.date,
      y0: 0,
      y1: 1,
      yref: 'paper',
      line: { color: 'red', dash: 'dash', width: 2 },
    },
  ],
  annotations: [
    {
      x: changePoint.date,
      y: 1,
      yref: 'paper',
      text: changePoint.reason,
      showarrow: true,
      arrowhead: 2,
    },
  ],
}}

Confidence Intervals

// Show error bands
<Plot
  data={[
    // Mean line
    { x: dates, y: meanValues, name: 'Mean AHI', line: { color: 'blue' } },
    // Upper confidence bound
    {
      x: dates,
      y: upperBound,
      fill: 'tonexty',
      fillcolor: 'rgba(0, 100, 200, 0.2)',
      line: { width: 0 },
      showlegend: false,
    },
    // Lower confidence bound
    {
      x: dates,
      y: lowerBound,
      fill: 'tonexty',
      fillcolor: 'rgba(0, 100, 200, 0.2)',
      line: { width: 0 },
      name: '95% CI',
    },
  ]}
/>

Performance Optimization

Large Datasets

// Downsample for large datasets (> 1000 points)
const downsampled = data.length > 1000 ? downsample(data, 1000) : data;

// Use WebGL for better performance
data={{
  ...chartData,
  type: 'scattergl', // WebGL-accelerated
}}

Lazy Loading

// Load charts only when visible
import { lazy, Suspense } from 'react';

const AhiChart = lazy(() => import('./AhiChart'));

function ChartsSection() {
  return (
    <Suspense fallback={<div>Loading chart...</div>}>
      <AhiChart data={data} />
    </Suspense>
  );
}

Common Patterns

Dual Y-Axis (Two Metrics)

// Show AHI and EPAP on same chart
<Plot
  data={[
    { x: dates, y: ahiValues, name: 'AHI', yaxis: 'y1' },
    { x: dates, y: epapValues, name: 'EPAP', yaxis: 'y2' },
  ]}
  layout={{
    xaxis: { title: 'Date' },
    yaxis: { title: 'AHI (events/hour)', side: 'left' },
    yaxis2: {
      title: 'EPAP (cmH₂O)',
      overlaying: 'y',
      side: 'right',
    },
  }}
/>

Subplots (Multiple Charts)

// Stacked charts sharing x-axis
<Plot
  data={[
    { x: dates, y: ahiValues, xaxis: 'x1', yaxis: 'y1', name: 'AHI' },
    { x: dates, y: usageValues, xaxis: 'x1', yaxis: 'y2', name: 'Usage' },
  ]}
  layout={{
    grid: { rows: 2, columns: 1, pattern: 'independent' },
    xaxis: { title: 'Date' },
    yaxis: { title: 'AHI' },
    yaxis2: { title: 'Usage (hours)' },
  }}
/>

Resources

Install via CLI
npx skills add https://github.com/kabaka/oscar-export-analyzer --skill medical-data-visualization
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator