Skip to content

visualdynamics.plot.scalogram

scalogram

The scalogram, drawn: time across, frequency up, magnitude as colour.

One channel at a time, because the picture is dense — a second one beside it at this size reads as noise rather than as a second answer.

Two things here are not decoration and are the reason this is a module rather than four lines beside the coherence map.

The frequency axis is logarithmic, because a wavelet's bandwidth is a constant fraction of its frequency: the rows are evenly spaced in log frequency, so drawing them evenly spaced in linear frequency would put the image's own rows where they do not belong. The image is drawn in log space and the axis is labelled back in Hz.

The cone of influence is drawn over the picture, shaded, because inside it a scalogram is an artefact of where the record was cut rather than a reading of the record. It looks exactly like data — that is the whole problem — and a reading that cannot be told from an artefact is worse than no reading.

Functions:

Name Description
scalogram_image

Draw one channel's scalogram onto plot, and return the image.

Functions:

scalogram_image

scalogram_image(
    plot: Any,
    magnitude: ndarray,
    times: ndarray,
    frequencies: ndarray,
    colors: Mapping[str, str],
    *,
    label: str = "",
    units: str = "",
    omega0: float = OMEGA0,
    time_label: str = "time [s]",
) -> Any

Draw one channel's scalogram onto plot, and return the image.

Parameters:

Name Type Description Default
plot PlotItem

Where to draw. Cleared of nothing — the caller owns the layout.

required
magnitude ndarray

(frequencies, times), already the magnitude: this draws, it does not transform.

required
times ndarray

The record's clock, in seconds, one per column.

required
frequencies ndarray

One per row, ascending, spaced evenly by octave.

required
colors mapping

The theme.

required
label str

What the colour bar is of — the record's own quantity and unit, which the amplitude normalisation is what makes meaningful.

''
units str

What the colour bar is of — the record's own quantity and unit, which the amplitude normalisation is what makes meaningful.

''
omega0 float

The wavelet's width, for working out the cone.

OMEGA0
time_label str

The bottom axis's label, units included.

'time [s]'

Returns:

Type Description
ImageItem

The image, so a caller can adjust its levels.

Source code in src/visualdynamics/plot/scalogram.py
def scalogram_image(plot: Any, magnitude: np.ndarray, times: np.ndarray,
                    frequencies: np.ndarray, colors: Mapping[str, str],
                    *, label: str = '', units: str = '',
                    omega0: float = wavelet.OMEGA0,
                    time_label: str = 'time [s]') -> Any:
    """Draw one channel's scalogram onto `plot`, and return the image.

    Parameters
    ----------
    plot : pyqtgraph.PlotItem
        Where to draw. Cleared of nothing — the caller owns the layout.
    magnitude : numpy.ndarray
        ``(frequencies, times)``, already the magnitude: this draws, it
        does not transform.
    times : numpy.ndarray
        The record's clock, in seconds, one per column.
    frequencies : numpy.ndarray
        One per row, ascending, spaced evenly by octave.
    colors : mapping
        The theme.
    label, units : str
        What the colour bar is of — the record's own quantity and unit,
        which the amplitude normalisation is what makes meaningful.
    omega0 : float
        The wavelet's width, for working out the cone.
    time_label : str
        The bottom axis's label, units included.

    Returns
    -------
    pyqtgraph.ImageItem
        The image, so a caller can adjust its levels.
    """
    import pyqtgraph as pg

    foreground = pg.mkPen(colors['plot_foreground'])
    for edge in ('left', 'bottom', 'top', 'right'):
        axis = plot.getAxis(edge)
        axis.setPen(foreground)
        axis.setTextPen(foreground)

    image = pg.ImageItem(np.asarray(magnitude).T)
    image.setColorMap(pg.colormap.get('viridis'))
    finite = np.isfinite(magnitude)
    top = float(np.max(magnitude[finite])) if finite.any() else 1.0
    image.setLevels((0.0, top if top > 0.0 else 1.0))

    left, right = float(times[0]), float(times[-1])
    # drawn in log frequency, which is where the rows are actually
    # evenly spaced; the axis is labelled back in Hz below
    low, high = np.log10(frequencies[0]), np.log10(frequencies[-1])
    image.setRect(left, float(low), right - left, float(high - low))
    plot.addItem(image)

    plot.setXRange(left, right, padding=0)
    plot.setYRange(float(low), float(high), padding=0)
    plot.getViewBox().setLimits(xMin=left, xMax=right,
                                yMin=float(low), yMax=float(high))
    plot.setLabel('bottom', time_label)
    plot.setLabel('left', 'frequency [Hz]')
    plot.getAxis('left').setTicks([_decade_ticks(frequencies)])
    plot.showGrid(x=True, y=True, alpha=0.2)

    _draw_cone(plot, times, frequencies, colors, omega0)

    bar = pg.ColorBarItem(
        values=(0.0, top if top > 0.0 else 1.0),
        colorMap=pg.colormap.get('viridis'),
        label=f'{label} [{units}]' if units else label, interactive=False)
    bar.setImageItem(image, insert_in=plot)
    return image