Skip to content

Commit 0cd0486

Browse files
author
Benjamin Moody
committed
plot_items: add sampling_freq and ann_freq parameters.
An annotation file (represented by a wfdb.Annotation object) may have a "sampling frequency" that differs from the sampling frequency or frequencies of the signals themselves. This will be the case, for example, for annotations generated by programs like sqrs that operate on an upsampled or downsampled copy of the input signals. In any event, when plotting annotations, we need to translate the annotation time into a sample number in order to display it in the correct location. Furthermore, in the future, we want to permit plotting multiple synchronized signals that are sampled at different frequencies. Therefore, to disambiguate between the many possible "sampling frequencies" invvolved, add parameters 'sampling_freq' and 'ann_freq'. Both parameters are optional and default to the value of 'fs'. Either may be a list (one entry per channel), allowing the API to accomodate multi-frequency data. Currently, if 'sampling_freq' is a list, all of its elements must be equal.
1 parent dac438f commit 0cd0486

File tree

1 file changed

+155
-20
lines changed

1 file changed

+155
-20
lines changed

wfdb/plot/plot.py

Lines changed: 155 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,79 @@
88
from wfdb.io.annotation import Annotation
99

1010

11+
def _get_sampling_freq(sampling_freq, n_sig, frame_freq):
12+
"""
13+
Convert application-specified sampling frequency to a list.
14+
15+
Parameters
16+
----------
17+
sampling_freq : number or sequence or None
18+
The sampling frequency or frequencies of the signals. If this is a
19+
list, its length must equal `n_sig`. If unset, defaults to
20+
`frame_freq`.
21+
n_sig : int
22+
Number of channels.
23+
frame_freq : number or None
24+
Default sampling frequency (record frame frequency).
25+
26+
Returns
27+
-------
28+
sampling_freq : list
29+
The sampling frequency for each channel (a list of length `n_sig`.)
30+
31+
"""
32+
if sampling_freq is None:
33+
return [frame_freq] * n_sig
34+
elif hasattr(sampling_freq, '__len__'):
35+
if len(sampling_freq) != n_sig:
36+
raise ValueError('length mismatch: n_sig = {}, '
37+
'len(sampling_freq) = {}'.format(
38+
n_sig, len(sampling_freq)))
39+
return list(sampling_freq)
40+
else:
41+
return [sampling_freq] * n_sig
42+
43+
44+
def _get_ann_freq(ann_freq, n_annot, frame_freq):
45+
"""
46+
Convert application-specified annotation frequency to a list.
47+
48+
Parameters
49+
----------
50+
ann_freq : number or sequence or None
51+
The sampling frequency or frequencies of the annotations. If this
52+
is a list, its length must equal `n_annot`. If unset, defaults to
53+
`frame_freq`.
54+
n_annot : int
55+
Number of channels.
56+
frame_freq : number or None
57+
Default sampling frequency (record frame frequency).
58+
59+
Returns
60+
-------
61+
ann_freq : list
62+
The sampling frequency for each channel (a list of length `n_annot`).
63+
64+
"""
65+
if ann_freq is None:
66+
return [frame_freq] * n_annot
67+
elif hasattr(ann_freq, '__len__'):
68+
if len(ann_freq) != n_annot:
69+
raise ValueError('length mismatch: n_annot = {}, '
70+
'len(ann_freq) = {}'.format(
71+
n_annot, len(ann_freq)))
72+
return list(ann_freq)
73+
else:
74+
return [ann_freq] * n_annot
75+
76+
1177
def plot_items(signal=None, ann_samp=None, ann_sym=None, fs=None,
1278
time_units='samples', sig_name=None, sig_units=None,
1379
xlabel=None, ylabel=None, title=None, sig_style=[''],
1480
ann_style=['r*'], ecg_grids=[], figsize=None,
1581
sharex=False, sharey=False, return_fig=False,
16-
return_fig_axes=False):
82+
return_fig_axes=False, sampling_freq=None,
83+
ann_freq=None):
1784
"""
1885
Subplot individual channels of signals and/or annotations.
1986
@@ -87,6 +154,14 @@ def plot_items(signal=None, ann_samp=None, ann_sym=None, fs=None,
87154
'figsize' argument passed into matplotlib.pyplot's `figure` function.
88155
return_fig : bool, optional
89156
Whether the figure is to be returned as an output argument.
157+
sampling_freq : number or sequence, optional
158+
The sampling frequency or frequencies of the signals. If this is a
159+
list, it must have the same length as the number of channels. If
160+
unspecified, defaults to `fs`.
161+
ann_freq : number or sequence, optional
162+
The sampling frequency or frequencies of the annotations. If this
163+
is a list, it must have the same length as `ann_samp`. If
164+
unspecified, defaults to `fs`.
90165
91166
Returns
92167
-------
@@ -113,18 +188,25 @@ def plot_items(signal=None, ann_samp=None, ann_sym=None, fs=None,
113188
# Figure out number of subplots required
114189
sig_len, n_sig, n_annot, n_subplots = get_plot_dims(signal, ann_samp)
115190

191+
# Convert sampling_freq and ann_freq to lists if needed
192+
sampling_freq = _get_sampling_freq(sampling_freq, n_sig, fs)
193+
ann_freq = _get_ann_freq(ann_freq, n_annot, fs)
194+
116195
# Create figure
117196
fig, axes = create_figure(n_subplots, sharex, sharey, figsize)
118197

119198
if signal is not None:
120-
plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes)
199+
plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes,
200+
sampling_freq=sampling_freq)
121201

122202
if ann_samp is not None:
123203
plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs,
124-
time_units, ann_style, axes)
204+
time_units, ann_style, axes,
205+
sampling_freq=sampling_freq, ann_freq=ann_freq)
125206

126207
if ecg_grids:
127-
plot_ecg_grids(ecg_grids, fs, sig_units, time_units, axes)
208+
plot_ecg_grids(ecg_grids, fs, sig_units, time_units, axes,
209+
sampling_freq=sampling_freq)
128210

129211
# Add title and axis labels.
130212
# First, make sure that xlabel and ylabel inputs are valid
@@ -238,7 +320,8 @@ def create_figure(n_subplots, sharex, sharey, figsize):
238320
return fig, axes
239321

240322

241-
def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes):
323+
def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes,
324+
sampling_freq=None):
242325
"""
243326
Plot signal channels.
244327
@@ -262,22 +345,39 @@ def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes):
262345
will be used for all channels.
263346
axes : list
264347
The information needed for each subplot.
348+
sampling_freq : number or sequence, optional
349+
The sampling frequency or frequencies of the signals. If this is a
350+
list, it must have the same length as the number of channels. If
351+
unspecified, defaults to `fs`.
265352
266353
Returns
267354
-------
268355
N/A
269356
270357
"""
358+
if n_sig == 0:
359+
return
360+
271361
# Extend signal style if necessary
272362
if len(sig_style) == 1:
273363
sig_style = n_sig * sig_style
274364

365+
# Convert sampling_freq to a list if needed
366+
sampling_freq = _get_sampling_freq(sampling_freq, n_sig, fs)
367+
368+
if any(f != sampling_freq[0] for f in sampling_freq):
369+
raise NotImplementedError(
370+
'multiple sampling frequencies are not supported')
371+
275372
# Figure out time indices
276373
if time_units == 'samples':
277374
t = np.linspace(0, sig_len-1, sig_len)
278375
else:
279-
downsample_factor = {'seconds':fs, 'minutes':fs * 60,
280-
'hours':fs * 3600}
376+
downsample_factor = {
377+
'seconds': sampling_freq[0],
378+
'minutes': sampling_freq[0] * 60,
379+
'hours': sampling_freq[0] * 3600
380+
}
281381
t = np.linspace(0, sig_len-1, sig_len) / downsample_factor[time_units]
282382

283383
# Plot the signals
@@ -289,7 +389,7 @@ def plot_signal(signal, sig_len, n_sig, fs, time_units, sig_style, axes):
289389

290390

291391
def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units,
292-
ann_style, axes):
392+
ann_style, axes, sampling_freq=None, ann_freq=None):
293393
"""
294394
Plot annotations, possibly overlaid on signals.
295395
ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, ann_style, axes
@@ -320,6 +420,14 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units,
320420
will be used for all channels.
321421
axes : list
322422
The information needed for each subplot.
423+
sampling_freq : number or sequence, optional
424+
The sampling frequency or frequencies of the signals. If this is a
425+
list, it must have the same length as the number of channels. If
426+
unspecified, defaults to `fs`.
427+
ann_freq : number or sequence, optional
428+
The sampling frequency or frequencies of the annotations. If this
429+
is a list, it must have the same length as `ann_samp`. If
430+
unspecified, defaults to `fs`.
323431
324432
Returns
325433
-------
@@ -330,25 +438,45 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units,
330438
if len(ann_style) == 1:
331439
ann_style = n_annot * ann_style
332440

333-
# Figure out downsample factor for time indices
334-
if time_units == 'samples':
335-
downsample_factor = 1
336-
else:
337-
downsample_factor = {'seconds':float(fs), 'minutes':float(fs)*60,
338-
'hours':float(fs)*3600}[time_units]
441+
# Convert sampling_freq and ann_freq to lists if needed
442+
sampling_freq = _get_sampling_freq(sampling_freq, n_sig, fs)
443+
ann_freq = _get_ann_freq(ann_freq, n_annot, fs)
339444

340445
# Plot the annotations
341446
for ch in range(n_annot):
447+
afreq = ann_freq[ch]
448+
if ch < n_sig:
449+
sfreq = sampling_freq[ch]
450+
else:
451+
sfreq = afreq
452+
453+
# Figure out downsample factor for time indices
454+
if time_units == 'samples':
455+
if afreq is None and sfreq is None:
456+
downsample_factor = 1
457+
else:
458+
downsample_factor = afreq / sfreq
459+
else:
460+
downsample_factor = {
461+
'seconds': float(afreq),
462+
'minutes': float(afreq) * 60,
463+
'hours': float(afreq) * 3600
464+
}[time_units]
465+
342466
if ann_samp[ch] is not None and len(ann_samp[ch]):
343467
# Figure out the y values to plot on a channel basis
344468

345469
# 1 dimensional signals
346470
try:
347471
if n_sig > ch:
472+
if sfreq == afreq:
473+
index = ann_samp[ch]
474+
else:
475+
index = (sfreq / afreq * ann_samp[ch]).astype('int')
348476
if signal.ndim == 1:
349-
y = signal[ann_samp[ch]]
477+
y = signal[index]
350478
else:
351-
y = signal[ann_samp[ch], ch]
479+
y = signal[index, ch]
352480
else:
353481
y = np.zeros(len(ann_samp[ch]))
354482
except IndexError:
@@ -364,7 +492,7 @@ def plot_annotation(ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units,
364492
y[i]))
365493

366494

367-
def plot_ecg_grids(ecg_grids, fs, units, time_units, axes):
495+
def plot_ecg_grids(ecg_grids, fs, units, time_units, axes, sampling_freq=None):
368496
"""
369497
Add ECG grids to the axes.
370498
@@ -381,6 +509,10 @@ def plot_ecg_grids(ecg_grids, fs, units, time_units, axes):
381509
and 'hours'.
382510
axes : list
383511
The information needed for each subplot.
512+
sampling_freq : number or sequence, optional
513+
The sampling frequency or frequencies of the signals. If this is a
514+
list, it must have the same length as the number of channels. If
515+
unspecified, defaults to `fs`.
384516
385517
Returns
386518
-------
@@ -390,15 +522,18 @@ def plot_ecg_grids(ecg_grids, fs, units, time_units, axes):
390522
if ecg_grids == 'all':
391523
ecg_grids = range(0, len(axes))
392524

525+
# Convert sampling_freq to a list if needed
526+
sampling_freq = _get_sampling_freq(sampling_freq, len(axes), fs)
527+
393528
for ch in ecg_grids:
394529
# Get the initial plot limits
395530
auto_xlims = axes[ch].get_xlim()
396531
auto_ylims= axes[ch].get_ylim()
397532

398533
(major_ticks_x, minor_ticks_x, major_ticks_y,
399534
minor_ticks_y) = calc_ecg_grids(auto_ylims[0], auto_ylims[1],
400-
units[ch], fs, auto_xlims[1],
401-
time_units)
535+
units[ch], sampling_freq[ch],
536+
auto_xlims[1], time_units)
402537

403538
min_x, max_x = np.min(minor_ticks_x), np.max(minor_ticks_x)
404539
min_y, max_y = np.min(minor_ticks_y), np.max(minor_ticks_y)
@@ -439,7 +574,7 @@ def calc_ecg_grids(minsig, maxsig, sig_units, fs, maxt, time_units):
439574
sig_units : list
440575
The units used for plotting each signal.
441576
fs : float
442-
The sampling frequency of the record.
577+
The sampling frequency of the signal.
443578
maxt : float
444579
The max time of the signal.
445580
time_units : str

0 commit comments

Comments
 (0)