Climate cubes with a grammar-of-graphics framework and a streaming-first renderer for scientific-quality 3D visuals.
CubeDynamics turns climate and NDVI rasters into tidy cubes and lets you explore them with ggplot-inspired pipes:
from cubedynamics import pipe, verbs as v
pipe(cube) | v.plot()The cube viewer is streaming-first (designed for PRISM, gridMET, Sentinel-2, and other huge archives) and grammar-based so you can build layered figures, legends, captions, and facets.
Install from PyPI or GitHub:
pip install cubedynamics
# or latest main branch
pip install "git+https://github.com/CU-ESIIL/climate_cube_math.git@main"Render a cube immediately:
import cubedynamics as cd
from cubedynamics import pipe, verbs as v
ndvi = cd.ndvi(lat=40.0, lon=-105.25, start="2018-01-01", end="2019-12-31")
(pipe(ndvi)
| v.plot(title="NDVI cube", fig_id=1, fig_title="Front range greenness")
)import cubedynamics as cd
from cubedynamics import pipe, verbs as v
# Example: load an NDVI cube (time, y, x)
ndvi = cd.load_example_ndvi_cube() # or use an existing helper
pipe(ndvi) | v.plot(title="NDVI")v.plot() is a high-level, streaming-first 3-D viewer. Under the hood it builds a
CubePlot (grammar-of-graphics) and routes frames through the streaming renderer so
you can keep working with in-memory arrays, dask-backed cubes, or VirtualCube
streams.
Climate cubes are small stacks of maps through time (typically (time, y, x) and optional band). A grammar keeps the visualization expressive while staying readable:
- Data: VirtualCube or xarray DataArray
- Aesthetics (
aes):fill,alpha,slice - Stats (
stat_*):stat_time_mean,stat_time_anomaly,stat_space_mean - Geoms (
geom_*):geom_cube,geom_slice,geom_outline,geom_path3d(stub) - Scales:
ScaleFillContinuous,ScaleAlphaContinuous - Coordinates:
coord_cube - Themes:
CubeTheme,theme_cube_studio - Facets:
facet_wrap,facet_grid - Annotations:
annot_plane,annot_text
from cubedynamics.plotting.cube_plot import CubePlot
p = (CubePlot(ndvi)
.aes(fill="ndvi")
.stat_time_anomaly(time_dim="time")
.geom_cube()
.scale_fill_continuous(center=0, palette="diverging")
.coord_cube(elev=35, azim=45)
.theme_cube_studio()
.annot_plane(axis="time", value="2018-07-01", text="Fire event")
)
p.save("ndvi_anomaly.html")VirtualCube streams large datasets lazily without ever calling .values on the full cube:
- Iterates over time slices and encodes only 2D frames
- Shows an inline progress bar in notebooks
- Facets subset cubes one slice at a time with shared scales
- Works for multi-year NDVI, NEON towers, PRISM and gridMET pulls
Performance tips:
- Use chunked data and keep cubes lazy (
daskarrays) - Avoid forcing
.compute(); let the viewer stream - Lower
thin_time_factorfor quick previews
Captions support figure numbers, markdown, and math; legends inherit palettes from ScaleFillContinuous; CSS variables expose theme colors for polished exports. The studio theme ships with balanced background, axis, and legend colors suited for reports.
from cubedynamics import verbs as v, pipe
from cubedynamics.plotting.cube_plot import CubePlot
ndvi = cd.ndvi(lat=40.0, lon=-105.25, start="2017-01-01", end="2020-12-31")
figure = (CubePlot(ndvi, title="NDVI anomaly")
.aes(fill="ndvi")
.stat_time_anomaly(time_dim="time")
.geom_cube()
.scale_fill_continuous(center=0, palette="diverging")
.coord_cube(elev=30, azim=60)
.facet_wrap(by="year")
.annot_plane(axis="time", value="2018-06-10", text="Smoke plume")
.theme_cube_studio()
)
figure.save("ndvi_anomaly_facets.html")See the CubePlot Grammar of Graphics and the Streaming-First Renderer pages for the full narrative and gallery examples.
A vase volume is a time-varying polygon cross-section over (x, y) that traces out a 3-D hull as it moves through (time, y, x).
Use VaseDefinition to describe the polygons, v.vase_extract(...) to mask the cube, and let v.plot()
auto-detect and overlay the vase outline.
import numpy as np
import shapely.geometry as geom
from cubedynamics import pipe, verbs as v
from cubedynamics.plotting import CubePlot
from cubedynamics.vase import VaseSection, VaseDefinition
cube = ndvi # DataArray (time, y, x)
t0, t1 = cube.time.values[[0, -1]]
y_center = float(cube.y.mean())
x_center = float(cube.x.mean())
radius = 0.2 * min(float(cube.x.max()-cube.x.min()),
float(cube.y.max()-cube.y.min()))
poly_t0 = geom.Point(x_center, y_center).buffer(radius)
poly_t1 = geom.Point(x_center, y_center).buffer(1.5 * radius)
sections = [VaseSection(time=t0, polygon=poly_t0),
VaseSection(time=t1, polygon=poly_t1)]
vase = VaseDefinition(sections=sections, interp="nearest")
vase_cube = v.vase_extract(cube, vase)
# High-level pilot viewer
pipe(vase_cube) | v.plot(title="NDVI inside vase")
# Grammar-of-graphics control
p = (CubePlot(cube)
.stat_vase(vase)
.geom_cube()
.geom_vase_outline(color="limegreen", alpha=0.6))
p- Vases are analytic volumes in
(time, y, x)defined by polygons through time. v.vase_extractmasks the cube and attaches the vase definition inattrs["vase"].v.plot()auto-detectsattrs["vase"]and adds the outline overlay.