Large Vector Fields Dynamically Downsampled#

The visualization of vector fields from large datasets often presents a challenge. Direct plotting methods can quickly consume excessive memory, leading to crashes or unresponsive applications. To address this issue, we introduce a dynamic downsampling technique that enables interactive exploration of vector fields without sacrificing performance. We first create a sample_data that contains 4,000,000 points.

import holoviews as hv
import hvplot.xarray  # noqa
import numpy as np
import xarray as xr
def sample_data(shape=(20, 30)):
    x = np.linspace(311.9, 391.1, shape[1])
    y = np.linspace(-23.6, 24.8, shape[0])
    x2d, y2d = np.meshgrid(x, y)
    u = 10 * (2 * np.cos(2 * np.deg2rad(x2d) + 3 * np.deg2rad(y2d + 30)) ** 2)
    v = 20 * np.cos(6 * np.deg2rad(x2d))
    return x, y, u, v

xs, ys, U, V = sample_data(shape=(2000, 2000))
mag = np.sqrt(U**2 + V**2)
angle = (np.pi/2.) - np.arctan2(U/mag, V/mag)
ds = xr.Dataset({
    'mag': xr.DataArray(mag, dims=('y', 'x'), coords={'y': ys, 'x': xs}),
    'angle': xr.DataArray(angle, dims=('y', 'x'), coords={'y': ys, 'x': xs})
})
ds
<xarray.Dataset> Size: 64MB
Dimensions:  (y: 2000, x: 2000)
Coordinates:
  * y        (y) float64 16kB -23.6 -23.58 -23.55 -23.53 ... 24.75 24.78 24.8
  * x        (x) float64 16kB 311.9 311.9 312.0 312.0 ... 391.0 391.1 391.1
Data variables:
    mag      (y, x) float64 32MB 6.459 6.383 6.307 6.232 ... 22.04 22.02 22.0
    angle    (y, x) float64 32MB 1.413 1.41 1.406 1.402 ... -1.125 -1.126 -1.127

If we just try to call ds.hvplot.vectorfield this is likely raising a MemoryError. The alternative is to dynamically downsample the data based on the visible range. This helps manage memory consumption when dealing with large datasets, especially when plotting vector fields. We are going to use HoloViews to create a view (hv.DynamicMap) whose content is dynamically updated based on the data range displayed (tracked by the hv.streams.RangeXY stream), find out more about these concepts in HoloViews’ documentation.

def downsample_quiver(x_range=None, y_range=None, nmax=10):
    """
    Creates a HoloViews vector field plot from a dataset, dynamically downsampling 
    data based on the visible range to optimize memory usage.

    Args:
        x_range (tuple, optional): Range of x values to include. Defaults to None (full range).
        y_range (tuple, optional): Range of y values to include. Defaults to None (full range).
        nmax (int, optional): Maximum number of points along each axis after coarsening. 
                              Defaults to 10.

    Returns:
        HoloViews DynamicMap: A dynamic vector field plot that updates based on the visible range.
    """
    if x_range is None or y_range is None:
        # No range provided, downsample the entire dataset for initial display
        xs, ys = ds.x.size, ds.y.size        # Get dataset dimensions
    else:
        # Select data within the specified range
        sub = ds.sel(x=slice(*x_range), y=slice(*y_range))
        # Downsample the selected data
        xs, ys = sub.x.size, sub.y.size

    ix, iy = xs // nmax, ys // nmax     # Calculate downsampling intervals
    ix = max(1, ix)                     # Ensure interval is at least 1
    iy = max(1, iy)
    sub = ds.coarsen(x=ix, y=iy, side="center", boundary="trim").mean()  # Downsample

    # Create the vector field plot
    quiver = sub.hvplot.vectorfield(
        x="x", y="y", mag="mag",
        angle="angle", hover=False,
    ).opts(magnitude="mag")
    return quiver

Zoom in and out to observe the vector field being dynamically downsampled based on the current view.

Note

Run this example locally to see these effects take place.

range_xy = hv.streams.RangeXY()  # Stream to capture range changes
filtered = hv.DynamicMap(downsample_quiver, streams=[range_xy])  # Dynamic plot
filtered
This web page was generated from a Jupyter notebook and not all interactivity will work on this website.