pythonmatplotlibplotgraphing

Appending single plot to multiplot grid to simulate ECG printout


I am trying to plot ECG waveform data to replicate what it looks like as 12-lead ECG printout, with a 3X4 grid of all leads (truncated to 2.5 seconds), and the full 10 second waveform for lead II at the bottom, similar to the attached picture (but with lead II at the bottom instead). ECG printout

I am able to make the multiplot and single lead II plot fine, but am having trouble appending them together without messing up the axes. Here's my code:

import os, numpy as np,pandas as pd,sys,random,matplotlib.pyplot as plt,matplotlib.ticker as ticker,os,pprint
from matplotlib.ticker import AutoLocator, AutoMinorLocator
from scipy.io import loadmat
from tqdm import tqdm 

def plot_individual(ax, x, y,label):
    ax.plot(x, y, color='black', linewidth=0.5)
    ax.minorticks_on()
    #ax.xaxis.set_major_locator(ticker.MultipleLocator(200))
    #ax.yaxis.set_major_locator(ticker.MultipleLocator(10))
    ax.xaxis.set_major_locator(AutoLocator())
    ax.xaxis.set_minor_locator(AutoMinorLocator())
    ax.yaxis.set_major_locator(AutoLocator())
    ax.yaxis.set_minor_locator(AutoMinorLocator())
    ax.annotate(label, xy=(0.02, 0.50), xycoords='axes fraction', fontsize=12, ha='left', va='bottom', fontfamily='serif')
    ax.grid(which='major', linestyle='-', linewidth='0.5', color='red')
    ax.grid(which='minor', linestyle='-', linewidth='0.5', color=(1, 0.7, 0.7))
    ax.tick_params(which='both', left=False, bottom=False, labelleft=False, labelbottom=False)
    ax.spines['top'].set_visible(False)  # Hide the top spine
    ax.spines['right'].set_visible(False)  # Hide the right spine
    ax.spines['bottom'].set_visible(False)  # Hide the bottom spine
    ax.spines['left'].set_visible(False)  # Hide the left spine
    ax.set_facecolor('white')


def plot_12(plot_df):
    x = [i for i in range(1250)]
    df_12 = plot_df.T
    df_short = df_12.iloc[:,0:1250]
    df_short = df_short.T
    df_short = df_short[['I','aVR','V1','V4','II','aVL','V2','V5','III','aVF','V3','V6']].T

    fig, axs = plt.subplots(3,4, figsize=(16,9),sharex=True,sharey=True, gridspec_kw={'wspace': 0,'hspace':0})  # Adjust figsize as desired
    plt.subplots_adjust(
            hspace = 0, 
            wspace = 0.04,
            left   = 0.04,  # the left side of the subplots of the figure
            right  = 0.98,  # the right side of the subplots of the figure
            bottom = 0.06,  # the bottom of the subplots of the figure
            top    = 0.95)
    num_columns = 1250

    for i, (idx, row) in enumerate(df_short.iterrows()):
        x_vals = [i for i in range(1250)]
        y_vals = row.to_list()
        plotvals = pd.DataFrame({'x':x_vals,'y':y_vals})
        x = plotvals['x']
        y=plotvals['y']
        ax = axs[i // 4, i % 4] 
        plot_individual(ax, x, y,label=idx)
    #return fig, axs

    #plt.tight_layout()  # Adjust the spacing between subplots
    plt.show()
    #plt.savefig(save_path, dpi=300)
    #plt.close()
plot_12(plot_df)

def plot_ii(plot_df):
    df_ii = plot_df
    df_ii = df_ii['II']

    fig_ii, ax_ii = plt.subplots(figsize=(16,3))

    x_vals = [i for i in range(5000)]
    y_vals = df_ii.to_list()
    plotvals = pd.DataFrame({'x':x_vals,'y':y_vals})
    x = plotvals['x']
    y=plotvals['y']

    plot_individual(ax_ii, x, y,label='II')
    
    #return fig, ax

    #plt.show()

plot_ii(plot_df)


And here's some code for simulated data:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Define parameters
sampling_rate = 1000    # Samples per second
total_rows = 5000       # Total number of rows
duration = total_rows / sampling_rate  # Duration of the ECG signal in seconds
num_samples = total_rows
time = np.linspace(0, duration, num_samples)

# Create baseline ECG waveform (sinus rhythm)
heart_rate = 75  # Beats per minute
heartbeat_duration = 60 / heart_rate
baseline_ecg = np.sin(2 * np.pi * time / heartbeat_duration)

# Create noise
noise = np.random.normal(0, 0.1, num_samples)

# Simulate 12-lead ECG data
leads = {
    'I': baseline_ecg + noise,
    'II': baseline_ecg + 0.5 * np.roll(baseline_ecg, -sampling_rate) + noise,
    'III': 0.5 * np.roll(baseline_ecg, -sampling_rate) + noise,
    'aVR': -0.5 * baseline_ecg + noise,
    'aVL': baseline_ecg - 0.5 * np.roll(baseline_ecg, -sampling_rate) + noise,
    'aVF': baseline_ecg - 0.5 * baseline_ecg + noise,
    'V1': 0.1 * baseline_ecg + noise,
    'V2': 0.2 * baseline_ecg + noise,
    'V3': 0.3 * baseline_ecg + noise,
    'V4': 0.4 * baseline_ecg + noise,
    'V5': 0.5 * baseline_ecg + noise,
    'V6': 0.6 * baseline_ecg + noise,
}

# Create a DataFrame to store the leads
ecg_dataframe = pd.DataFrame(data=leads, index=time)

Bonus points if you can adjust the code to make the gridlines automatically go closer together like the ECG printout - I have hundreds of thousands of images so will need to be automatic. Thanks!

Tried various versions of changing the axes, making room on the multiplot and changing the gridspace to add leadII


Solution

  • First of all, well prepared sample data, thank you for that!

    For your solution, I followed along a matplotlib example.

    To append your plots, the following worked for me:

    fig = plt.figure(figsize=(16,12))
    subfigs = fig.subfigures(2, 1, wspace=0.07, height_ratios=[3, 1])
    plot_12(plot_df, subfigs[0])
    
    plot_ii(plot_df, subfigs[1])
    plt.show()
    

    Though I had to make some adjustments to your functions:

    def plot_12(plot_df, subfig): # <-- added subfig as an argument
        x = [i for i in range(1250)]
        df_12 = plot_df.T
        df_short = df_12.iloc[:, 0:1250]
        df_short = df_short.T
        df_short = df_short[['I', 'aVR', 'V1', 'V4', 'II', 'aVL', 'V2', 'V5', 'III', 'aVF', 'V3', 'V6']].T
    
        # removed fig, as subfig.subplots only returns one object
        axs = subfig.subplots(3, 4, sharex=True, sharey=True,
                                gridspec_kw={'wspace': 0, 'hspace': 0})  # Adjust figsize as desired
        subfig.subplots_adjust(
            hspace=0,
            wspace=0.04,
            left=0.0,  # the left side of the subplots of the figure <--- changed for your second question
            right=1.,  # the right side of the subplots of the figure <--- changed for your second question
            bottom=0.06,  # the bottom of the subplots of the figure
            top=0.95)
        num_columns = 1250
    
        for i, (idx, row) in enumerate(df_short.iterrows()):
            x_vals = [i for i in range(1250)]
            y_vals = row.to_list()
            plotvals = pd.DataFrame({'x': x_vals, 'y': y_vals})
            x = plotvals['x']
            y = plotvals['y']
            ax = axs[i // 4, i % 4]
            plot_individual(ax, x, y, label=idx)
    
    def plot_ii(plot_df, subfig): # <-- added subfig as an argument
        df_ii = plot_df
        df_ii = df_ii['II']
    
        # removed fig_ii, as subfig.subplots only returns one object
        ax_ii = subfig.subplots()
    
        x_vals = [i for i in range(5000)]
        y_vals = df_ii.to_list()
        plotvals = pd.DataFrame({'x': x_vals, 'y': y_vals})
        x = plotvals['x']
        y = plotvals['y']
    
        plot_individual(ax_ii, x, y, label='II')
    

    About your second issue. I had to change the left and right adjustments in plot_12 (see comments above). Additionally the following needs to be added, else matplotlib seems adjusts the limits itself, whereas you want to have well defined limits:

    def plot_individual(..., limits):
        ...
        ax.set_xlim(limits)
    
    

    In your example, plot_12 needs to call plot_individual() with limits = [0, 1200], else the major ticks do not alight properly.

    Hope that helps.