FIT File Visualizer Script

July 9, 2025 Wietse Venema
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "fitparse==1.2.0",
#   "matplotlib==3.8.2",
#   "numpy==1.26.4"
# ]
# ///

import fitparse
import os
import sys
import matplotlib.pyplot as plt
import numpy as np

def plot_hr_vs_power_over_time(filepath):
    """
    Reads a .fit file and creates a plot of heart rate vs. power
    with color indicating time.

    Args:
        filepath (str): The path to the .fit file.
    """
    if not os.path.exists(filepath):
        print(f"Error: File not found at '{filepath}'")
        return

    try:
        fitfile = fitparse.FitFile(filepath)

        # Data extraction
        timestamps, heart_rates, powers = [], [], []

        for record in fitfile.get_messages("record"):
            timestamps.append(record.get_value("timestamp"))
            heart_rates.append(record.get_value("heart_rate"))
            powers.append(record.get_value("power"))

        # Filter out None values
        valid_records = [
            (t, hr, p)
            for t, hr, p in zip(timestamps, heart_rates, powers)
            if all(x is not None for x in [t, hr, p])
        ]
        if not valid_records:
            print("No complete records found to generate the plot.")
            return
        timestamps, heart_rates, powers = zip(*valid_records)

        start_time = timestamps[0]
        # time in minutes
        time_deltas = [
            (t - start_time).total_seconds() / 60 for t in timestamps
        ]

        plt.figure(figsize=(10, 6))
        scatter = plt.scatter(
            powers, heart_rates, c=time_deltas, cmap="viridis", alpha=0.7
        )
        plt.title("Heart Rate vs. Power Over Time")
        plt.xlabel("Power (watts)")
        plt.ylabel("Heart Rate (bpm)")
        plt.grid(True)

        # Add colorbar
        cbar = plt.colorbar(scatter)
        cbar.set_label("Time (minutes)")

        plt.savefig("hr_vs_power_over_time.png")
        plt.close()
        print("Plot saved as hr_vs_power_over_time.png")

    except fitparse.FitParseError as e:
        print(f"Error parsing FIT file: {e}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")


if __name__ == "__main__":
    if len(sys.argv) > 1:
        fit_file_path = sys.argv[1]
        plot_hr_vs_power_over_time(fit_file_path)
    else:
        print(
            "Please provide the path to the .fit file as an argument."
        )

* * *