DL
Back to Blog
TechFebruary 2, 2026·5 min read

Vitals Charting with Recharts

How FhirHub uses Recharts to render interactive vital signs charts with reference range overlays, multi-vital selection, and time filtering so clinicians can spot trends at a glance.

D

David Le

Vitals Charting with Recharts

By David Le -- Part 6 of the FhirHub Series

Clinical data is only useful if clinicians can spot trends at a glance. FhirHub uses Recharts to render interactive line charts for vital signs, with reference range overlays, multi-vital selection, and time range filtering.

The Charting Challenge

Vital signs charting in a healthcare context has specific requirements beyond a typical dashboard:

  1. Multiple data series -- Blood pressure alone needs two lines (systolic + diastolic)
  2. Reference ranges -- Clinicians need to see normal vs. abnormal at a glance
  3. Time filtering -- Compare trends over days, months, or years
  4. Responsive design -- Must work on desktop monitors and tablets at the bedside

Component Architecture

The VitalsChart component handles all of this in a single file:

// frontend/src/components/patients/vitals-chart.tsx
interface VitalReading {
  date: string;
  systolic?: number;
  diastolic?: number;
  heartRate?: number;
  temperature?: number;
  weight?: number;
  respiratoryRate?: number;
  oxygenSaturation?: number;
}

interface VitalsChartProps {
  data: VitalReading[];
  className?: string;
}

The data shape is flat -- each reading is a single object with optional fields for each vital type. This works well with Recharts, which expects uniform data point objects.

Vital Type Selection

Users select which vitals to display using filter pills:

const vitalTypes = [
  { id: "bloodPressure", label: "Blood Pressure" },
  { id: "heartRate", label: "Heart Rate" },
  { id: "temperature", label: "Temperature" },
  { id: "weight", label: "Weight" },
  { id: "respiratoryRate", label: "Respiratory Rate" },
  { id: "oxygenSaturation", label: "O2 Saturation" },
];

State management is straightforward React:

const [selectedVitals, setSelectedVitals] = useState<string[]>([
  "bloodPressure",
]);
const [timeRange, setTimeRange] = useState("30d");

Blood pressure is selected by default since it's the most commonly monitored vital sign.

Reference Range Lines

Reference lines help clinicians identify abnormal values without looking at exact numbers:

{showBloodPressure && (
  <>
    <ReferenceLine
      y={referenceRanges.systolic.high}
      stroke="#EF4444"
      strokeDasharray="3 3"
      opacity={0.5}
    />
    <ReferenceLine
      y={referenceRanges.diastolic.high}
      stroke="#F97316"
      strokeDasharray="3 3"
      opacity={0.5}
    />
  </>
)}

Dashed reference lines at the normal range boundaries provide visual context. A systolic reading above the red dashed line immediately signals elevated blood pressure.

The reference ranges used in the chart:

const referenceRanges = {
  systolic: { low: 90, high: 120, critical: 140 },
  diastolic: { low: 60, high: 80, critical: 90 },
  heartRate: { low: 60, high: 100 },
  temperature: { low: 97, high: 99 },
  oxygenSaturation: { low: 95, high: 100 },
  respiratoryRate: { low: 12, high: 20 },
};

Color Coding

Each vital type has a distinct color for clarity:

const vitalColors = {
  systolic: "#EF4444",       // Red
  diastolic: "#F97316",      // Orange
  heartRate: "#3B82F6",      // Blue
  temperature: "#10B981",    // Green
  weight: "#8B5CF6",         // Purple
  respiratoryRate: "#EC4899", // Pink
  oxygenSaturation: "#06B6D4", // Cyan
};

Red and orange for blood pressure is a deliberate choice -- it draws attention to the most clinically significant vital.

Responsive Container

The chart uses Recharts' ResponsiveContainer to fill available space:

<div className="h-80 w-full">
  <ResponsiveContainer width="100%" height="100%">
    <LineChart
      data={data}
      margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
    >
      <CartesianGrid
        strokeDasharray="3 3"
        stroke="#374151"
        opacity={0.3}
      />
      <XAxis
        dataKey="date"
        stroke="#9CA3AF"
        fontSize={12}
        tickFormatter={(value) => {
          const date = new Date(value);
          return date.toLocaleDateString("en-US", {
            month: "short",
            day: "numeric",
          });
        }}
      />
      <YAxis stroke="#9CA3AF" fontSize={12} />
      <Tooltip
        contentStyle={{
          backgroundColor: "#1F2937",
          border: "1px solid #374151",
          borderRadius: "8px",
        }}
        labelStyle={{ color: "#F9FAFB" }}
      />
      <Legend />
      {/* Data lines rendered conditionally */}
    </LineChart>
  </ResponsiveContainer>
</div>

The dark-themed tooltip styling matches the DaisyUI dark theme used throughout FhirHub.

Data Lines

Each vital type is conditionally rendered based on the user's selection:

{showBloodPressure && (
  <>
    <Line
      type="monotone"
      dataKey="systolic"
      name="Systolic"
      stroke={vitalColors.systolic}
      strokeWidth={2}
      dot={{ r: 4 }}
      activeDot={{ r: 6 }}
    />
    <Line
      type="monotone"
      dataKey="diastolic"
      name="Diastolic"
      stroke={vitalColors.diastolic}
      strokeWidth={2}
      dot={{ r: 4 }}
      activeDot={{ r: 6 }}
    />
  </>
)}

The monotone interpolation type creates smooth curves between data points, which is appropriate for vital signs trends. The active dot enlarges on hover for easier interaction.

Reference Ranges Legend

Below the chart, a contextual legend shows normal ranges for the selected vital types:

function VitalsReferenceRanges({ selectedVitals }: { selectedVitals: string[] }) {
  const ranges = [
    { id: "bloodPressure", label: "Blood Pressure", normal: "90-120 / 60-80 mmHg" },
    { id: "heartRate", label: "Heart Rate", normal: "60-100 bpm" },
    { id: "temperature", label: "Temperature", normal: "97-99°F" },
    { id: "oxygenSaturation", label: "O2 Saturation", normal: "95-100%" },
  ];

  const activeRanges = ranges.filter((r) => selectedVitals.includes(r.id));
  // ...render active ranges
}

This updates dynamically -- if you deselect blood pressure and select heart rate, only the heart rate range is shown.

Data Flow

The full data flow from FHIR server to chart:

HAPI FHIR (Observation resources)
  → .NET API (HapiFhirPatientRepository)
    → PatientService.GetVitalsChartAsync()
      → VitalsChartDTO (date + vital values)
        → Frontend API client
          → VitalsChart component
            → Recharts LineChart

The API's /api/patients/{id}/vitals/chart endpoint is specifically designed for the chart -- it returns pre-formatted data with dates as keys and vital values pre-extracted from the FHIR Observation resources.

Vitals Chart

What's Next

In Part 7, we'll look at the clinical reference ranges system in depth -- covering AHA blood pressure guidelines, 21+ lab test ranges, and how FhirHub computes FHIR interpretation codes.


Find the source code on GitHub Connect on LinkedIn

Related Projects

Featured

FhirHub

A healthcare data management platform built on the HL7 FHIR R4 standard, providing a comprehensive web interface for managing patient clinical data including vitals, conditions, medications, lab orders, and bulk data exports with role-based access control and full audit logging.

Next.js 16
React 19
Typescript
Tailwind CSS 4
+8