Skip to content

NGI Plot Library

C pynasonde.vipir.ngi.plotlibIonogram class for rendering VIPIR NGI ionograms.

pynasonde.vipir.ngi.plotlib

Plotting helpers for visualizing VIPIR NGI ionograms and time-series data.

Ionogram

Bases: object

Convenience wrapper around a Matplotlib figure for VIPIR ionograms.

Attributes:

Name Type Description
dates

Optional list of timestamps associated with the plotted data.

ncols

Number of subplot columns requested at construction.

nrows

Number of subplot rows requested at construction.

fig

Matplotlib figure hosting the subplots.

axes

Flattened array of Matplotlib axes corresponding to each slot.

fig_title

Title drawn on the first subplot.

font_size

Base font size used throughout the figure.

Source code in pynasonde/vipir/ngi/plotlib.py
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
class Ionogram(object):
    """Convenience wrapper around a Matplotlib figure for VIPIR ionograms.

    Attributes:
        dates: Optional list of timestamps associated with the plotted data.
        ncols: Number of subplot columns requested at construction.
        nrows: Number of subplot rows requested at construction.
        fig: Matplotlib figure hosting the subplots.
        axes: Flattened array of Matplotlib axes corresponding to each slot.
        fig_title: Title drawn on the first subplot.
        font_size: Base font size used throughout the figure.
    """

    def __init__(
        self,
        dates: dt.datetime = None,
        fig_title: str = "",
        nrows: int = 2,
        ncols: int = 3,
        font_size: float = 10,
        figsize: tuple = (6, 3),
    ):
        """Initialize the figure canvas and subplot grid.

        Args:
            dates: Optional timestamp(s) used when labeling plots.
            fig_title: Title added above the first subplot.
            nrows: Number of subplot rows.
            ncols: Number of subplot columns.
            font_size: Base font size applied through `utils.setsize`.
            figsize: Base width/height (in inches) of a single subplot.
        """
        self.dates = dates
        self.ncols = ncols
        self.nrows = nrows
        self.fig, self.axes = plt.subplots(
            figsize=(figsize[0] * ncols, figsize[1] * nrows),
            dpi=300,
            nrows=nrows,
            ncols=ncols,
        )  # Size for website
        if type(self.axes) == list or type(self.axes) == np.ndarray:
            self.axes = self.axes.ravel()
        self.fig_title = fig_title
        self.font_size = font_size
        utils.setsize(font_size)
        self._num_subplots_created = 0
        return

    def add_ionogram(
        self,
        frequency: np.array,
        height: np.array,
        value: np.array,
        mode: str = "O",
        xlabel: str = "Frequency, MHz",
        ylabel: str = "Virtual Height, km",
        ylim: List[float] = [50, 800],
        xlim: List[float] = [1, 22],
        add_cbar: bool = False,
        cbar_label: str = "{}-mode Power, dB",
        cmap: Union[str, LinearSegmentedColormap] = COLOR_MAPS.Inferno,
        prange: List[float] = [5, 70],
        xticks: List[float] = [1.5, 2.0, 3.0, 5.0, 7.0, 10.0, 15.0, 20.0],
        text: str = None,
        del_ticks: bool = True,
        txt_color: str = "w",
    ) -> None:
        """Render a single ionogram heatmap into the next subplot slot.

        Args:
            frequency: Plasma frequency axis (MHz).
            height: Virtual height axis (km).
            value: 2-D power array aligned with `frequency` × `height`.
            mode: Descriptor used in the colorbar label (e.g., O or X).
            xlabel: X-axis label text.
            ylabel: Y-axis label text.
            ylim: Y-axis limits (km).
            xlim: Frequency limits (MHz).
            add_cbar: Whether to append a colorbar for this axes.
            cbar_label: Format string used for the colorbar title.
            cmap: Matplotlib colormap or name.
            prange: Min/max bounds (dB) applied to the power field.
            xticks: Explicit tick positions shown when `del_ticks` is False.
            text: Optional annotation placed inside the axes.
            del_ticks: Remove axis ticks for a cleaner grid layout.

        Returns:
            Matplotlib axes instance that received the plot.
        """
        ax = self._add_axis(del_ticks)
        ax.set_xlim(np.log10(xlim))
        ax.set_xlabel(xlabel, fontdict={"size": self.font_size})
        ax.set_ylim(ylim)
        ax.set_ylabel(
            ylabel,
            fontdict={"size": self.font_size},
        )
        im = ax.pcolormesh(
            np.log10(frequency),
            height,
            value.T,
            lw=0.01,
            edgecolors="None",
            cmap=cmap,
            vmax=prange[1],
            vmin=prange[0],
            zorder=3,
        )
        if np.logical_not(del_ticks):
            ax.set_xticks(np.log10(xticks))
            ax.set_xticklabels(xticks)
        if text:
            ax.text(
                0.05,
                0.9,
                text,
                ha="left",
                va="center",
                transform=ax.transAxes,
                fontdict={"size": self.font_size, "color": txt_color},
            )
        if add_cbar:
            self._add_colorbar(im, self.fig, ax, label=cbar_label.format(mode))
        return ax

    def add_ionogram_traces(
        self,
        frequency: np.array,
        height: np.array,
        mode: str = "O",
        xlabel: str = "Frequency, MHz",
        ylabel: str = "Virtual Height, km",
        ylim: List[float] = [50, 400],
        xlim: List[float] = [1, 22],
        xticks: List[float] = [1.5, 2.0, 3.0, 5.0, 7.0, 10.0, 15.0, 20.0],
        text: str = None,
        del_ticks: bool = True,
        alpha: float = 0.8,
        ms: float = 0.7,
        color: str = "r",
        ax=None,
    ) -> None:
        """Overlay traced ionogram echoes as point markers.

        Args:
            frequency: Frequency coordinates (MHz) of the traced points.
            height: Virtual heights (km) for each trace sample.
            mode: Descriptor used in on-plot text (e.g., O or X).
            xlabel: X-axis label text.
            ylabel: Y-axis label text.
            ylim: Y-axis limits (km).
            xlim: Frequency limits (MHz).
            xticks: Tick positions when `del_ticks` is False.
            text: Optional annotation inside the axes; defaults to timestamp.
            del_ticks: Remove ticks for cleaner multipanel layouts.
            alpha: Marker transparency.
            ms: Marker size.
            color: Base color (combined with "." marker).
            ax: Optional axes to draw on (defaults to the next subplot).

        Returns:
            Matplotlib axes instance that received the plot.
        """
        ax = ax if ax else self._add_axis(del_ticks)
        ax.set_xlim(np.log10(xlim))
        ax.set_xlabel(xlabel, fontdict={"size": self.font_size})
        ax.set_ylim(ylim)
        ax.set_ylabel(
            ylabel,
            fontdict={"size": self.font_size},
        )
        ax.plot(np.log10(frequency), height, color + ".", ms=ms, alpha=alpha)
        if np.logical_not(del_ticks):
            ax.set_xticks(np.log10(xticks))
            ax.set_xticklabels(xticks)
        text = (
            text if text else f"{mode}-mode/{self.dates[0].strftime('%Y%m%d %H%M')} UT"
        )
        ax.text(
            0.05,
            0.9,
            text,
            ha="left",
            va="center",
            transform=ax.transAxes,
            fontdict={"size": self.font_size},
        )
        return ax

    def _add_axis(self, del_ticks=True):
        """Return the next available subplot axis, optionally stripping ticks."""
        ax = (
            self.axes[self._num_subplots_created]
            if type(self.axes) == np.ndarray or type(self.axes) == list
            else self.axes
        )
        if del_ticks:
            ax.set_xticks([])
            ax.set_yticks([])
        if self._num_subplots_created == 0:
            ax.text(
                0.01,
                1.05,
                self.fig_title,
                ha="left",
                va="center",
                transform=ax.transAxes,
                fontdict={"size": self.font_size},
            )
        self._num_subplots_created += 1
        return ax

    def add_interval_plots(
        self,
        df: pd.DataFrame,
        mode: str = "O",
        xlabel: str = "Time, UT",
        ylabel: str = "Virtual Height, km",
        ylim: List[float] = [50, 800],
        xlim: List[dt.datetime] = None,
        add_cbar: bool = True,
        cbar_label: str = "{}-mode Power, dB",
        cmap: str = "Spectral",
        prange: List[float] = [5, 70],
        noise_scale: float = 1.2,
        date_format: str = r"$%H^{%M}$",
        del_ticks: bool = False,
        xtick_locator: mdates.HourLocator = mdates.HourLocator(interval=4),
        xdate_lims: List[dt.datetime] = None,
        kind: str = "pcolormesh",
    ):
        """Plot mode-specific interval statistics on a time/height grid.

        Args:
            df: DataFrame containing time, range, and power columns.
            mode: Mode prefix (O/X/etc.) used to select DataFrame columns.
            xlabel: X-axis label text.
            ylabel: Y-axis label text.
            ylim: Limits for the height axis.
            xlim: Optional datetime bounds passed to `set_xlim`.
            add_cbar: Whether to include a colorbar for the filled contour.
            cbar_label: Format string applied to the colorbar label.
            cmap: Matplotlib colormap name.
            prange: Minimum/maximum dB values shown in the contour.
            noise_scale: Multiplier applied to the noise floor when masking.
            date_format: Matplotlib datetime formatter string.
            del_ticks: Remove ticks before plotting, if desired.
            xtick_locator: Locator used for primary x-axis ticks.
            xdate_lims: Optional override for the x-axis limits.

        Returns:
            Matplotlib axes instance containing the interval plot.
        """
        xlim = xlim if xlim is not None else [df.time.min(), df.time.max()]
        ax = self._add_axis(del_ticks=del_ticks)
        ax.set_xlim(xlim)
        ax.set_xlabel(xlabel, fontdict={"size": self.font_size})
        ax.set_ylim(ylim)
        ax.set_ylabel(ylabel, fontdict={"size": self.font_size})
        ax.xaxis.set_major_locator(xtick_locator)
        ax.xaxis.set_major_formatter(DateFormatter(date_format))
        Zval, lims = (
            np.array(df[f"{mode}_mode_power"]),
            np.array(df[f"{mode}_mode_noise"]),
        )
        Zval[Zval < lims * noise_scale] = np.nan
        df[f"{mode}_mode_power"] = Zval
        X, Y, Z = utils.get_gridded_parameters(
            df,
            xparam="time",
            yparam="range",
            zparam=f"{mode}_mode_power",
            rounding=False,
        )
        Z[Z < prange[0]] = prange[0]
        if kind == "pcolormesh":
            x_minutes = (
                pd.to_datetime(X[0, :]) - pd.to_datetime(X[0, 0])
            ).total_seconds() / 60.0
            y_km = np.asarray(Y[:, 0])
            nx_fine = 4 * len(x_minutes)  # 4x upsample in time
            ny_fine = 4 * len(y_km)  # 4x upsample in height
            x_fine = np.linspace(x_minutes.min(), x_minutes.max(), nx_fine)
            y_fine = np.linspace(y_km.min(), y_km.max(), ny_fine)

            from scipy.interpolate import RegularGridInterpolator

            interp = RegularGridInterpolator(
                (x_minutes, y_km),
                Z,
                method="linear",  # or "nearest"
                bounds_error=False,
                fill_value=np.nan,
            )
            xx, yy = np.meshgrid(x_fine, y_fine, indexing="ij")
            points = np.column_stack([xx.ravel(), yy.ravel()])
            Z_fine = interp(points).reshape(xx.shape)
            xx_dt = (
                (pd.to_datetime(X[0, 0]) + pd.to_timedelta(xx.ravel(), unit="min"))
                .to_numpy()
                .reshape(xx.shape)
            )

            im = ax.pcolormesh(
                xx_dt,
                yy,
                Z_fine,
                cmap=cmap,
                vmax=prange[1],
                vmin=prange[0],
                zorder=3,
            )
        elif kind == "contourf":
            levels = np.linspace(prange[0], prange[1], 5)
            # Overlay filled contours for the same data
            im = ax.contourf(
                X,
                Y,
                Z.T,
                levels=levels,
                cmap=cmap,
                alpha=0.4,
                zorder=4,
            )

            # Overlay contour lines
            cs = ax.contour(
                X,
                Y,
                Z.T,
                levels=levels,
                colors="k",
                linewidths=0.5,
                zorder=5,
            )
            # Optionally label the contour lines
            ax.clabel(cs, inline=True, fontsize=self.font_size * 0.5)
        ax.text(
            0.01, 1.05, self.fig_title, ha="left", va="center", transform=ax.transAxes
        )
        if xdate_lims is not None:
            ax.set_xlim(xdate_lims)
        if add_cbar:
            self._add_colorbar(im, self.fig, ax, label=cbar_label.format(mode))
        return ax

    def add_TS(
        self,
        time: List,
        ys: np.array,
        ms=0.6,
        alpha=0.7,
        ylim: List = None,
        xlim: List[dt.datetime] = None,
        ylabel: str = r"$foF_2$, MHz",
        xlabel: str = "Time, UT",
        color: str = "r",
        marker: str = ".",
        major_locator: mdates.RRuleLocator = mdates.HourLocator(byhour=range(0, 24, 4)),
        minor_locator: mdates.RRuleLocator = mdates.MinuteLocator(
            byminute=range(0, 60, 30)
        ),
    ):
        """Plot a time-series curve (e.g., foF2) in the next subplot slot.

        Args:
            time: Sequence of timestamps corresponding to `ys`.
            ys: Values to plot.
            ms: Marker size.
            alpha: Marker transparency.
            ylim: Optional y-axis limits; defaults to data min/max.
            xlim: Optional x-axis limits.
            ylabel: Y-axis label text.
            xlabel: X-axis label text.
            color: Matplotlib color specification.
            marker: Marker style for the scatter plot.
            major_locator: Locator for major ticks on the time axis.
            minor_locator: Locator for minor ticks on the time axis.

        Returns:
            Matplotlib axes instance that received the plot.
        """
        ylim = ylim if ylim else [np.min(ys), np.max(ys)]
        ax = self._add_axis(del_ticks=False)
        ax.set_xlim(xlim)
        ax.set_xlabel(xlabel)
        ax.set_ylim(ylim)
        ax.set_ylabel(ylabel)
        ax.xaxis.set_major_locator(major_locator)
        ax.xaxis.set_major_locator(minor_locator)
        ax.xaxis.set_major_formatter(DateFormatter(r"%H^{%M}"))
        ax.plot(time, ys, marker=marker, color=color, ms=ms, alpha=alpha, ls="None")
        return ax

    def _add_colorbar(self, im, fig, ax, label=""):
        """Attach a vertical colorbar to the right of the supplied axis.

        Args:
            im: Mappable returned by a Matplotlib plotting call.
            fig: Parent figure.
            ax: Axis the colorbar aligns with.
            label: Text label applied to the colorbar.
        """
        pos = ax.get_position()
        cpos = [
            pos.x1 + 0.025,
            pos.y0 + 0.0125,
            0.015,
            pos.height * 0.9,
        ]  # this list defines (left, bottom, width, height
        cax = fig.add_axes(cpos)
        cb = fig.colorbar(im, ax=ax, cax=cax)
        cb.set_label(label)
        return

    def save(self, filepath):
        """Persist the assembled figure to disk."""
        self.fig.savefig(filepath, bbox_inches="tight")
        return

    def close(self):
        """Release Matplotlib resources associated with the figure."""
        self.fig.clf()
        plt.close()
        return

__init__(dates=None, fig_title='', nrows=2, ncols=3, font_size=10, figsize=(6, 3))

Initialize the figure canvas and subplot grid.

Parameters:

Name Type Description Default
dates dt.datetime

Optional timestamp(s) used when labeling plots.

None
fig_title str

Title added above the first subplot.

''
nrows int

Number of subplot rows.

2
ncols int

Number of subplot columns.

3
font_size float

Base font size applied through utils.setsize.

10
figsize tuple

Base width/height (in inches) of a single subplot.

(6, 3)
Source code in pynasonde/vipir/ngi/plotlib.py
def __init__(
    self,
    dates: dt.datetime = None,
    fig_title: str = "",
    nrows: int = 2,
    ncols: int = 3,
    font_size: float = 10,
    figsize: tuple = (6, 3),
):
    """Initialize the figure canvas and subplot grid.

    Args:
        dates: Optional timestamp(s) used when labeling plots.
        fig_title: Title added above the first subplot.
        nrows: Number of subplot rows.
        ncols: Number of subplot columns.
        font_size: Base font size applied through `utils.setsize`.
        figsize: Base width/height (in inches) of a single subplot.
    """
    self.dates = dates
    self.ncols = ncols
    self.nrows = nrows
    self.fig, self.axes = plt.subplots(
        figsize=(figsize[0] * ncols, figsize[1] * nrows),
        dpi=300,
        nrows=nrows,
        ncols=ncols,
    )  # Size for website
    if type(self.axes) == list or type(self.axes) == np.ndarray:
        self.axes = self.axes.ravel()
    self.fig_title = fig_title
    self.font_size = font_size
    utils.setsize(font_size)
    self._num_subplots_created = 0
    return

add_ionogram(frequency, height, value, mode='O', xlabel='Frequency, MHz', ylabel='Virtual Height, km', ylim=[50, 800], xlim=[1, 22], add_cbar=False, cbar_label='{}-mode Power, dB', cmap=COLOR_MAPS.Inferno, prange=[5, 70], xticks=[1.5, 2.0, 3.0, 5.0, 7.0, 10.0, 15.0, 20.0], text=None, del_ticks=True, txt_color='w')

Render a single ionogram heatmap into the next subplot slot.

Parameters:

Name Type Description Default
frequency np.array

Plasma frequency axis (MHz).

required
height np.array

Virtual height axis (km).

required
value np.array

2-D power array aligned with frequency × height.

required
mode str

Descriptor used in the colorbar label (e.g., O or X).

'O'
xlabel str

X-axis label text.

'Frequency, MHz'
ylabel str

Y-axis label text.

'Virtual Height, km'
ylim List[float]

Y-axis limits (km).

[50, 800]
xlim List[float]

Frequency limits (MHz).

[1, 22]
add_cbar bool

Whether to append a colorbar for this axes.

False
cbar_label str

Format string used for the colorbar title.

'{}-mode Power, dB'
cmap Union[str, LinearSegmentedColormap]

Matplotlib colormap or name.

COLOR_MAPS.Inferno
prange List[float]

Min/max bounds (dB) applied to the power field.

[5, 70]
xticks List[float]

Explicit tick positions shown when del_ticks is False.

[1.5, 2.0, 3.0, 5.0, 7.0, 10.0, 15.0, 20.0]
text str

Optional annotation placed inside the axes.

None
del_ticks bool

Remove axis ticks for a cleaner grid layout.

True

Returns:

Type Description
None

Matplotlib axes instance that received the plot.

Source code in pynasonde/vipir/ngi/plotlib.py
def add_ionogram(
    self,
    frequency: np.array,
    height: np.array,
    value: np.array,
    mode: str = "O",
    xlabel: str = "Frequency, MHz",
    ylabel: str = "Virtual Height, km",
    ylim: List[float] = [50, 800],
    xlim: List[float] = [1, 22],
    add_cbar: bool = False,
    cbar_label: str = "{}-mode Power, dB",
    cmap: Union[str, LinearSegmentedColormap] = COLOR_MAPS.Inferno,
    prange: List[float] = [5, 70],
    xticks: List[float] = [1.5, 2.0, 3.0, 5.0, 7.0, 10.0, 15.0, 20.0],
    text: str = None,
    del_ticks: bool = True,
    txt_color: str = "w",
) -> None:
    """Render a single ionogram heatmap into the next subplot slot.

    Args:
        frequency: Plasma frequency axis (MHz).
        height: Virtual height axis (km).
        value: 2-D power array aligned with `frequency` × `height`.
        mode: Descriptor used in the colorbar label (e.g., O or X).
        xlabel: X-axis label text.
        ylabel: Y-axis label text.
        ylim: Y-axis limits (km).
        xlim: Frequency limits (MHz).
        add_cbar: Whether to append a colorbar for this axes.
        cbar_label: Format string used for the colorbar title.
        cmap: Matplotlib colormap or name.
        prange: Min/max bounds (dB) applied to the power field.
        xticks: Explicit tick positions shown when `del_ticks` is False.
        text: Optional annotation placed inside the axes.
        del_ticks: Remove axis ticks for a cleaner grid layout.

    Returns:
        Matplotlib axes instance that received the plot.
    """
    ax = self._add_axis(del_ticks)
    ax.set_xlim(np.log10(xlim))
    ax.set_xlabel(xlabel, fontdict={"size": self.font_size})
    ax.set_ylim(ylim)
    ax.set_ylabel(
        ylabel,
        fontdict={"size": self.font_size},
    )
    im = ax.pcolormesh(
        np.log10(frequency),
        height,
        value.T,
        lw=0.01,
        edgecolors="None",
        cmap=cmap,
        vmax=prange[1],
        vmin=prange[0],
        zorder=3,
    )
    if np.logical_not(del_ticks):
        ax.set_xticks(np.log10(xticks))
        ax.set_xticklabels(xticks)
    if text:
        ax.text(
            0.05,
            0.9,
            text,
            ha="left",
            va="center",
            transform=ax.transAxes,
            fontdict={"size": self.font_size, "color": txt_color},
        )
    if add_cbar:
        self._add_colorbar(im, self.fig, ax, label=cbar_label.format(mode))
    return ax

add_ionogram_traces(frequency, height, mode='O', xlabel='Frequency, MHz', ylabel='Virtual Height, km', ylim=[50, 400], xlim=[1, 22], xticks=[1.5, 2.0, 3.0, 5.0, 7.0, 10.0, 15.0, 20.0], text=None, del_ticks=True, alpha=0.8, ms=0.7, color='r', ax=None)

Overlay traced ionogram echoes as point markers.

Parameters:

Name Type Description Default
frequency np.array

Frequency coordinates (MHz) of the traced points.

required
height np.array

Virtual heights (km) for each trace sample.

required
mode str

Descriptor used in on-plot text (e.g., O or X).

'O'
xlabel str

X-axis label text.

'Frequency, MHz'
ylabel str

Y-axis label text.

'Virtual Height, km'
ylim List[float]

Y-axis limits (km).

[50, 400]
xlim List[float]

Frequency limits (MHz).

[1, 22]
xticks List[float]

Tick positions when del_ticks is False.

[1.5, 2.0, 3.0, 5.0, 7.0, 10.0, 15.0, 20.0]
text str

Optional annotation inside the axes; defaults to timestamp.

None
del_ticks bool

Remove ticks for cleaner multipanel layouts.

True
alpha float

Marker transparency.

0.8
ms float

Marker size.

0.7
color str

Base color (combined with "." marker).

'r'
ax

Optional axes to draw on (defaults to the next subplot).

None

Returns:

Type Description
None

Matplotlib axes instance that received the plot.

Source code in pynasonde/vipir/ngi/plotlib.py
def add_ionogram_traces(
    self,
    frequency: np.array,
    height: np.array,
    mode: str = "O",
    xlabel: str = "Frequency, MHz",
    ylabel: str = "Virtual Height, km",
    ylim: List[float] = [50, 400],
    xlim: List[float] = [1, 22],
    xticks: List[float] = [1.5, 2.0, 3.0, 5.0, 7.0, 10.0, 15.0, 20.0],
    text: str = None,
    del_ticks: bool = True,
    alpha: float = 0.8,
    ms: float = 0.7,
    color: str = "r",
    ax=None,
) -> None:
    """Overlay traced ionogram echoes as point markers.

    Args:
        frequency: Frequency coordinates (MHz) of the traced points.
        height: Virtual heights (km) for each trace sample.
        mode: Descriptor used in on-plot text (e.g., O or X).
        xlabel: X-axis label text.
        ylabel: Y-axis label text.
        ylim: Y-axis limits (km).
        xlim: Frequency limits (MHz).
        xticks: Tick positions when `del_ticks` is False.
        text: Optional annotation inside the axes; defaults to timestamp.
        del_ticks: Remove ticks for cleaner multipanel layouts.
        alpha: Marker transparency.
        ms: Marker size.
        color: Base color (combined with "." marker).
        ax: Optional axes to draw on (defaults to the next subplot).

    Returns:
        Matplotlib axes instance that received the plot.
    """
    ax = ax if ax else self._add_axis(del_ticks)
    ax.set_xlim(np.log10(xlim))
    ax.set_xlabel(xlabel, fontdict={"size": self.font_size})
    ax.set_ylim(ylim)
    ax.set_ylabel(
        ylabel,
        fontdict={"size": self.font_size},
    )
    ax.plot(np.log10(frequency), height, color + ".", ms=ms, alpha=alpha)
    if np.logical_not(del_ticks):
        ax.set_xticks(np.log10(xticks))
        ax.set_xticklabels(xticks)
    text = (
        text if text else f"{mode}-mode/{self.dates[0].strftime('%Y%m%d %H%M')} UT"
    )
    ax.text(
        0.05,
        0.9,
        text,
        ha="left",
        va="center",
        transform=ax.transAxes,
        fontdict={"size": self.font_size},
    )
    return ax

add_interval_plots(df, mode='O', xlabel='Time, UT', ylabel='Virtual Height, km', ylim=[50, 800], xlim=None, add_cbar=True, cbar_label='{}-mode Power, dB', cmap='Spectral', prange=[5, 70], noise_scale=1.2, date_format='$%H^{%M}$', del_ticks=False, xtick_locator=mdates.HourLocator(interval=4), xdate_lims=None, kind='pcolormesh')

Plot mode-specific interval statistics on a time/height grid.

Parameters:

Name Type Description Default
df pd.DataFrame

DataFrame containing time, range, and power columns.

required
mode str

Mode prefix (O/X/etc.) used to select DataFrame columns.

'O'
xlabel str

X-axis label text.

'Time, UT'
ylabel str

Y-axis label text.

'Virtual Height, km'
ylim List[float]

Limits for the height axis.

[50, 800]
xlim List[dt.datetime]

Optional datetime bounds passed to set_xlim.

None
add_cbar bool

Whether to include a colorbar for the filled contour.

True
cbar_label str

Format string applied to the colorbar label.

'{}-mode Power, dB'
cmap str

Matplotlib colormap name.

'Spectral'
prange List[float]

Minimum/maximum dB values shown in the contour.

[5, 70]
noise_scale float

Multiplier applied to the noise floor when masking.

1.2
date_format str

Matplotlib datetime formatter string.

'$%H^{%M}$'
del_ticks bool

Remove ticks before plotting, if desired.

False
xtick_locator mdates.HourLocator

Locator used for primary x-axis ticks.

mdates.HourLocator(interval=4)
xdate_lims List[dt.datetime]

Optional override for the x-axis limits.

None

Returns:

Type Description

Matplotlib axes instance containing the interval plot.

Source code in pynasonde/vipir/ngi/plotlib.py
def add_interval_plots(
    self,
    df: pd.DataFrame,
    mode: str = "O",
    xlabel: str = "Time, UT",
    ylabel: str = "Virtual Height, km",
    ylim: List[float] = [50, 800],
    xlim: List[dt.datetime] = None,
    add_cbar: bool = True,
    cbar_label: str = "{}-mode Power, dB",
    cmap: str = "Spectral",
    prange: List[float] = [5, 70],
    noise_scale: float = 1.2,
    date_format: str = r"$%H^{%M}$",
    del_ticks: bool = False,
    xtick_locator: mdates.HourLocator = mdates.HourLocator(interval=4),
    xdate_lims: List[dt.datetime] = None,
    kind: str = "pcolormesh",
):
    """Plot mode-specific interval statistics on a time/height grid.

    Args:
        df: DataFrame containing time, range, and power columns.
        mode: Mode prefix (O/X/etc.) used to select DataFrame columns.
        xlabel: X-axis label text.
        ylabel: Y-axis label text.
        ylim: Limits for the height axis.
        xlim: Optional datetime bounds passed to `set_xlim`.
        add_cbar: Whether to include a colorbar for the filled contour.
        cbar_label: Format string applied to the colorbar label.
        cmap: Matplotlib colormap name.
        prange: Minimum/maximum dB values shown in the contour.
        noise_scale: Multiplier applied to the noise floor when masking.
        date_format: Matplotlib datetime formatter string.
        del_ticks: Remove ticks before plotting, if desired.
        xtick_locator: Locator used for primary x-axis ticks.
        xdate_lims: Optional override for the x-axis limits.

    Returns:
        Matplotlib axes instance containing the interval plot.
    """
    xlim = xlim if xlim is not None else [df.time.min(), df.time.max()]
    ax = self._add_axis(del_ticks=del_ticks)
    ax.set_xlim(xlim)
    ax.set_xlabel(xlabel, fontdict={"size": self.font_size})
    ax.set_ylim(ylim)
    ax.set_ylabel(ylabel, fontdict={"size": self.font_size})
    ax.xaxis.set_major_locator(xtick_locator)
    ax.xaxis.set_major_formatter(DateFormatter(date_format))
    Zval, lims = (
        np.array(df[f"{mode}_mode_power"]),
        np.array(df[f"{mode}_mode_noise"]),
    )
    Zval[Zval < lims * noise_scale] = np.nan
    df[f"{mode}_mode_power"] = Zval
    X, Y, Z = utils.get_gridded_parameters(
        df,
        xparam="time",
        yparam="range",
        zparam=f"{mode}_mode_power",
        rounding=False,
    )
    Z[Z < prange[0]] = prange[0]
    if kind == "pcolormesh":
        x_minutes = (
            pd.to_datetime(X[0, :]) - pd.to_datetime(X[0, 0])
        ).total_seconds() / 60.0
        y_km = np.asarray(Y[:, 0])
        nx_fine = 4 * len(x_minutes)  # 4x upsample in time
        ny_fine = 4 * len(y_km)  # 4x upsample in height
        x_fine = np.linspace(x_minutes.min(), x_minutes.max(), nx_fine)
        y_fine = np.linspace(y_km.min(), y_km.max(), ny_fine)

        from scipy.interpolate import RegularGridInterpolator

        interp = RegularGridInterpolator(
            (x_minutes, y_km),
            Z,
            method="linear",  # or "nearest"
            bounds_error=False,
            fill_value=np.nan,
        )
        xx, yy = np.meshgrid(x_fine, y_fine, indexing="ij")
        points = np.column_stack([xx.ravel(), yy.ravel()])
        Z_fine = interp(points).reshape(xx.shape)
        xx_dt = (
            (pd.to_datetime(X[0, 0]) + pd.to_timedelta(xx.ravel(), unit="min"))
            .to_numpy()
            .reshape(xx.shape)
        )

        im = ax.pcolormesh(
            xx_dt,
            yy,
            Z_fine,
            cmap=cmap,
            vmax=prange[1],
            vmin=prange[0],
            zorder=3,
        )
    elif kind == "contourf":
        levels = np.linspace(prange[0], prange[1], 5)
        # Overlay filled contours for the same data
        im = ax.contourf(
            X,
            Y,
            Z.T,
            levels=levels,
            cmap=cmap,
            alpha=0.4,
            zorder=4,
        )

        # Overlay contour lines
        cs = ax.contour(
            X,
            Y,
            Z.T,
            levels=levels,
            colors="k",
            linewidths=0.5,
            zorder=5,
        )
        # Optionally label the contour lines
        ax.clabel(cs, inline=True, fontsize=self.font_size * 0.5)
    ax.text(
        0.01, 1.05, self.fig_title, ha="left", va="center", transform=ax.transAxes
    )
    if xdate_lims is not None:
        ax.set_xlim(xdate_lims)
    if add_cbar:
        self._add_colorbar(im, self.fig, ax, label=cbar_label.format(mode))
    return ax

add_TS(time, ys, ms=0.6, alpha=0.7, ylim=None, xlim=None, ylabel='$foF_2$, MHz', xlabel='Time, UT', color='r', marker='.', major_locator=mdates.HourLocator(byhour=range(0, 24, 4)), minor_locator=mdates.MinuteLocator(byminute=range(0, 60, 30)))

Plot a time-series curve (e.g., foF2) in the next subplot slot.

Parameters:

Name Type Description Default
time List

Sequence of timestamps corresponding to ys.

required
ys np.array

Values to plot.

required
ms

Marker size.

0.6
alpha

Marker transparency.

0.7
ylim List

Optional y-axis limits; defaults to data min/max.

None
xlim List[dt.datetime]

Optional x-axis limits.

None
ylabel str

Y-axis label text.

'$foF_2$, MHz'
xlabel str

X-axis label text.

'Time, UT'
color str

Matplotlib color specification.

'r'
marker str

Marker style for the scatter plot.

'.'
major_locator mdates.RRuleLocator

Locator for major ticks on the time axis.

mdates.HourLocator(byhour=range(0, 24, 4))
minor_locator mdates.RRuleLocator

Locator for minor ticks on the time axis.

mdates.MinuteLocator(byminute=range(0, 60, 30))

Returns:

Type Description

Matplotlib axes instance that received the plot.

Source code in pynasonde/vipir/ngi/plotlib.py
def add_TS(
    self,
    time: List,
    ys: np.array,
    ms=0.6,
    alpha=0.7,
    ylim: List = None,
    xlim: List[dt.datetime] = None,
    ylabel: str = r"$foF_2$, MHz",
    xlabel: str = "Time, UT",
    color: str = "r",
    marker: str = ".",
    major_locator: mdates.RRuleLocator = mdates.HourLocator(byhour=range(0, 24, 4)),
    minor_locator: mdates.RRuleLocator = mdates.MinuteLocator(
        byminute=range(0, 60, 30)
    ),
):
    """Plot a time-series curve (e.g., foF2) in the next subplot slot.

    Args:
        time: Sequence of timestamps corresponding to `ys`.
        ys: Values to plot.
        ms: Marker size.
        alpha: Marker transparency.
        ylim: Optional y-axis limits; defaults to data min/max.
        xlim: Optional x-axis limits.
        ylabel: Y-axis label text.
        xlabel: X-axis label text.
        color: Matplotlib color specification.
        marker: Marker style for the scatter plot.
        major_locator: Locator for major ticks on the time axis.
        minor_locator: Locator for minor ticks on the time axis.

    Returns:
        Matplotlib axes instance that received the plot.
    """
    ylim = ylim if ylim else [np.min(ys), np.max(ys)]
    ax = self._add_axis(del_ticks=False)
    ax.set_xlim(xlim)
    ax.set_xlabel(xlabel)
    ax.set_ylim(ylim)
    ax.set_ylabel(ylabel)
    ax.xaxis.set_major_locator(major_locator)
    ax.xaxis.set_major_locator(minor_locator)
    ax.xaxis.set_major_formatter(DateFormatter(r"%H^{%M}"))
    ax.plot(time, ys, marker=marker, color=color, ms=ms, alpha=alpha, ls="None")
    return ax

save(filepath)

Persist the assembled figure to disk.

Source code in pynasonde/vipir/ngi/plotlib.py
def save(self, filepath):
    """Persist the assembled figure to disk."""
    self.fig.savefig(filepath, bbox_inches="tight")
    return

close()

Release Matplotlib resources associated with the figure.

Source code in pynasonde/vipir/ngi/plotlib.py
def close(self):
    """Release Matplotlib resources associated with the figure."""
    self.fig.clf()
    plt.close()
    return