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