Building a Heart Rate Zone Visualizer in JavaScript

Building a Heart Rate Zone Visualizer in JavaScript


Disclosure: This article may contain affiliate links. We only recommend products we believe in.

Heart rate training is a cornerstone of modern athletic conditioning. By targeting specific heart rate zones, athletes can optimize aerobic endurance, lactate threshold, and VO2 max.

For developers building fitness tracking dashboards or coaching apps, displaying heart rate data visually helps users instantly understand their training intensity. In this guide, we will implement the formulas for calculating heart rate zones in JavaScript and show how to render a clean, CSS-based visual representation of these zones.

The Mathematical Formulas

To calculate heart rate zones, we must first determine the user’s Maximum Heart Rate (HR_max). There are two primary methods used in fitness apps:

1. Maximum Heart Rate (HR_max) Estimation

  • Haskell & Fox Formula (Standard): HR_max = 220 - age
  • Tanaka Formula (More accurate for active adults): HR_max = 208 - (0.7 * age)

2. Zone Calculation Methods

  • Percentage of Max HR (%MHR): A simple percentage of HR_max. Target HR = HR_max * percentage
  • Karvonen Method (Heart Rate Reserve - %HRR): Incorporates resting heart rate (HR_rest) for a more personalized baseline. HRR = HR_max - HR_rest Target HR = (HRR * percentage) + HR_rest

JavaScript Zone Calculator Logic

Let’s write a clean JavaScript module to calculate the boundaries of all five zones. We will support both the Tanaka and Haskell & Fox max HR formulas, and both the %MHR and Karvonen zone methods.

/**
 * Calculates training heart rate zones.
 * 
 * @param {Object} config
 * @param {number} config.age - User age in years
 * @param {number} [config.restingHR] - Resting heart rate (required for Karvonen)
 * @param {string} [config.maxHRFormula='tanaka'] - 'tanaka' or 'haskell'
 * @param {string} [config.zoneMethod='karvonen'] - 'karvonen' or 'mhr'
 * @returns {Array<Object>} List of zones with boundaries and colors
 */
function calculateHeartRateZones(config) {
  const { age, restingHR, maxHRFormula = 'tanaka', zoneMethod = 'karvonen' } = config;

  if (!age || age <= 0) {
    throw new Error("Age must be a positive number.");
  }
  if (zoneMethod === 'karvonen' && (!restingHR || restingHR <= 0)) {
    throw new Error("Resting Heart Rate is required for the Karvonen method.");
  }

  // 1. Calculate Max HR
  let maxHR;
  if (maxHRFormula === 'tanaka') {
    maxHR = 208 - (0.7 * age);
  } else {
    maxHR = 220 - age;
  }
  maxHR = Math.round(maxHR);

  // 2. Define standard zone percentages
  const zoneDefinitions = [
    { name: 'Zone 1: Recovery', min: 0.50, max: 0.60, color: '#3b82f6' }, // Blue
    { name: 'Zone 2: Aerobic Base', min: 0.60, max: 0.70, color: '#10b981' }, // Green
    { name: 'Zone 3: Tempo', min: 0.70, max: 0.80, color: '#f59e0b' }, // Yellow
    { name: 'Zone 4: Threshold', min: 0.80, max: 0.90, color: '#f97316' }, // Orange
    { name: 'Zone 5: Max Effort', min: 0.90, max: 1.00, color: '#ef4444' } // Red
  ];

  // 3. Calculate target range for each zone
  return zoneDefinitions.map(zone => {
    let lowerLimit, upperLimit;

    if (zoneMethod === 'karvonen') {
      const hrr = maxHR - restingHR;
      lowerLimit = (hrr * zone.min) + restingHR;
      upperLimit = (hrr * zone.max) + restingHR;
    } else {
      lowerLimit = maxHR * zone.min;
      upperLimit = maxHR * zone.max;
    }

    return {
      name: zone.name,
      minBpm: Math.round(lowerLimit),
      maxBpm: Math.round(upperLimit),
      color: zone.color
    };
  });
}

Visualizing the Zones in HTML/CSS

Once you have the zone boundaries, you can easily render them as a visual graph or progress bar on the client side. Below is an example of how you can dynamically build the DOM elements for a zone visualizer using standard vanilla CSS.

1. HTML Markup Structure

<div class="hr-visualizer" id="visualizer">
  <!-- Zones will be rendered here dynamically -->
</div>

2. CSS Stylesheet (Vanilla CSS)

.hr-visualizer {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 500px;
  margin: 20px auto;
  font-family: system-ui, -apple-system, sans-serif;
}

.zone-bar-wrapper {
  display: flex;
  align-items: center;
  gap: 16px;
  background: #f3f4f6;
  border-radius: 8px;
  padding: 8px 16px;
}

.zone-label {
  width: 140px;
  font-size: 14px;
  font-weight: 600;
  color: #374151;
}

.zone-track {
  flex-grow: 1;
  height: 16px;
  background: #e5e7eb;
  border-radius: 9999px;
  overflow: hidden;
  position: relative;
}

.zone-fill {
  height: 100%;
  border-radius: 9999px;
  transition: width 0.3s ease;
}

.zone-range {
  font-size: 12px;
  font-weight: 500;
  color: #6b7280;
  width: 80px;
  text-align: right;
}

3. DOM Rendering Script

function renderVisualizer(zones) {
  const container = document.getElementById('visualizer');
  container.innerHTML = ''; // Clear previous content

  zones.forEach(zone => {
    const wrapper = document.createElement('div');
    wrapper.className = 'zone-bar-wrapper';

    wrapper.innerHTML = `
      <div class="zone-label">${zone.name}</div>
      <div class="zone-track">
        <div class="zone-fill" style="width: 100%; background-color: ${zone.color};"></div>
      </div>
      <div class="zone-range">${zone.minBpm} - ${zone.maxBpm} bpm</div>
    `;

    container.appendChild(wrapper);
  });
}

// Render example for a 30-year-old with a resting heart rate of 60 bpm
const calculatedZones = calculateHeartRateZones({
  age: 30,
  restingHR: 60,
  zoneMethod: 'karvonen'
});
renderVisualizer(calculatedZones);

Architectural Guidelines for Cardio Apps

If you are developing a fitness platform that logs workouts (e.g. from smartwatch Bluetooth feeds), follow these best practices:

  1. Filter Outlier Spikes: Optical wrist sensors are prone to “cadence locking,” where the sensor mistakes step frequency for heart rate. Implement a rolling average filter to discard single-second spikes (e.g. sudden jumps from 130 to 185 bpm).
  2. Adapt to Cardiac Drift: For long workouts (over 1 hour), hydration loss will cause the heart rate to drift upward. Warn users that their zones may shift slightly during endurance sessions.
  3. Encourage Field Testing: Formula-based estimations have a standard deviation of 10-12 bpm. Encourage advanced users to override estimated max heart rates with field test results (e.g., maximum HR reached during a 5k sprint finish).

For information on exercise hydration equations, see our Coding an AI-Powered Hydration Widget guide.