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