Skip to content

Commit a8f7666

Browse files
authored
Merge pull request #203 from CU-ESIIL/codex/fix-prism-loader-return-type-and-viewer-interactivity
Fix PRISM single-variable return and restore cube viewer rotation interactivity
2 parents 774a1aa + a347a11 commit a8f7666

File tree

7 files changed

+88
-18
lines changed

7 files changed

+88
-18
lines changed

docs/changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
- Documented the `Pipe` helper and new operations reference structure.
88
- `fire_plot` now requests daily gridMET/PRISM data by default and propagates provenance metadata on returned cubes.
99
- Added an `allow_synthetic` safety switch to gridMET/PRISM loaders with clearer empty-time/all-NaN error messages.
10+
- `load_prism_cube` now returns a DataArray when a single variable is requested, matching docs examples.
11+
- Fixed cube viewer rotation so drag/zoom updates the cube as well as axis labels.
1012

1113
## Earlier work
1214

src/cubedynamics/data/prism.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def load_prism_cube(
3636
prefer_streaming: bool = True,
3737
show_progress: bool = True,
3838
allow_synthetic: bool = False,
39-
) -> xr.Dataset:
39+
) -> xr.Dataset | xr.DataArray:
4040
"""Load a PRISM-like cube.
4141
4242
Parameters
@@ -54,7 +54,8 @@ def load_prism_cube(
5454
Temporal extent for the request.
5555
variable : str or sequence of str, default "ppt"
5656
PRISM variable(s) to request. ``variables`` may also be used for
57-
clarity when passing multiple entries.
57+
clarity when passing multiple entries. When a single variable is
58+
requested through ``variable``, the loader returns an ``xarray.DataArray``.
5859
time_res, freq : str, default "ME"
5960
Temporal frequency code. ``freq`` overrides ``time_res`` when set.
6061
chunks : mapping, optional
@@ -114,7 +115,7 @@ def load_prism_cube(
114115

115116
aoi = _coerce_aoi(lat=lat, lon=lon, bbox=bbox, aoi_geojson=aoi_geojson)
116117

117-
return _load_prism_cube_impl(
118+
ds = _load_prism_cube_impl(
118119
normalized_variables,
119120
start_ts.isoformat(),
120121
end_ts.isoformat(),
@@ -125,6 +126,12 @@ def load_prism_cube(
125126
show_progress,
126127
allow_synthetic,
127128
)
129+
if variables is None and len(normalized_variables) == 1:
130+
da = ds[normalized_variables[0]]
131+
da = da.copy()
132+
da.attrs.update(ds.attrs)
133+
return da
134+
return ds
128135

129136

130137
def _load_prism_cube_impl(

src/cubedynamics/fire_time_hull.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -992,9 +992,12 @@ def load_climate_cube_for_event(
992992
allow_synthetic=allow_synth,
993993
)
994994

995-
target_var = variable if variable in ds.data_vars else next(iter(ds.data_vars))
996-
cube_da = ds[target_var]
997-
cube_da.attrs.update(ds.attrs)
995+
if isinstance(ds, xr.DataArray):
996+
cube_da = ds
997+
else:
998+
target_var = variable if variable in ds.data_vars else next(iter(ds.data_vars))
999+
cube_da = ds[target_var]
1000+
cube_da.attrs.update(ds.attrs)
9981001
log(verbose, f"{source.upper()} source: {cube_da.attrs.get('source')}")
9991002
return ClimateCube(da=cube_da)
10001003

src/cubedynamics/plotting/cube_viewer.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,7 @@ def _render_cube_html(
702702
document.getElementById("cube-drag-" + viewerId)
703703
|| sceneDragSurface
704704
|| cubeWrapper;
705-
const rotationTarget = scene || cubeWrapper;
705+
const rotationTarget = cubeWrapper || scene;
706706
const jsWarning = document.getElementById("cube-js-warning-" + viewerId);
707707
const jsWarningText = jsWarning ? jsWarning.querySelector(".cube-warning-text") : null;
708708
let updateAxisRigBillboard = null;
@@ -734,9 +734,21 @@ def _render_cube_html(
734734
// Maintain a universal reference frame via CSS variables so scenes with
735735
// multiple cubes can share rotation/zoom state.
736736
function applyCubeRotation() {{
737-
rotationTarget.style.setProperty("--rot-x", rotationX + "rad");
738-
rotationTarget.style.setProperty("--rot-y", rotationY + "rad");
739-
rotationTarget.style.setProperty("--zoom", zoom);
737+
if (cubeWrapper) {{
738+
cubeWrapper.style.setProperty("--rot-x", rotationX + "rad");
739+
cubeWrapper.style.setProperty("--rot-y", rotationY + "rad");
740+
cubeWrapper.style.setProperty("--zoom", zoom);
741+
}}
742+
if (scene && scene !== cubeWrapper) {{
743+
scene.style.setProperty("--rot-x", rotationX + "rad");
744+
scene.style.setProperty("--rot-y", rotationY + "rad");
745+
scene.style.setProperty("--zoom", zoom);
746+
}}
747+
if (!cubeWrapper && rotationTarget) {{
748+
rotationTarget.style.setProperty("--rot-x", rotationX + "rad");
749+
rotationTarget.style.setProperty("--rot-y", rotationY + "rad");
750+
rotationTarget.style.setProperty("--zoom", zoom);
751+
}}
740752
if (updateAxisRigBillboard) {{
741753
updateAxisRigBillboard();
742754
}}

src/cubedynamics/variables.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,11 @@ def _load_temperature(
128128
**kwargs,
129129
)
130130

131-
da = ds[var_name]
132-
da = da.copy()
131+
if isinstance(ds, xr.DataArray):
132+
da = ds
133+
else:
134+
da = ds[var_name]
135+
da = da.copy()
133136
da.attrs.setdefault("variable", var_name)
134137
da.attrs.setdefault("source", source)
135138
return da

tests/test_cube_viewer_interactivity.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,33 @@ def test_cube_viewer_emits_interactive_markup(tmp_path):
4545
assert (tmp_path / "viewer.html").exists()
4646

4747

48+
def test_cube_viewer_rotation_writes_to_wrapper(tmp_path):
49+
data = xr.DataArray(
50+
np.arange(2 * 3 * 4, dtype=float).reshape(2, 3, 4),
51+
dims=("time", "y", "x"),
52+
coords={
53+
"time": pd.date_range("2023-01-01", periods=2, freq="D"),
54+
"y": np.arange(3),
55+
"x": np.arange(4),
56+
},
57+
name="demo",
58+
)
59+
60+
html = cube_from_dataarray(
61+
data,
62+
out_html=str(tmp_path / "viewer.html"),
63+
return_html=True,
64+
show_progress=False,
65+
thin_time_factor=1,
66+
)
67+
68+
assert "const rotationTarget = cubeWrapper || scene" in html
69+
assert "const rotationTarget = scene || cubeWrapper" not in html
70+
assert 'cubeWrapper.style.setProperty("--rot-x"' in html
71+
assert 'cubeWrapper.style.setProperty("--rot-y"' in html
72+
assert 'cubeWrapper.style.setProperty("--zoom"' in html
73+
74+
4875
def test_cube_viewer_wraps_html_in_iframe(tmp_path):
4976
data = xr.DataArray(
5077
np.arange(4 * 4 * 4, dtype=float).reshape(4, 4, 4),

tests/test_prism_loader.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,29 +52,31 @@ def fake_download(*args, **kwargs): # pragma: no cover - fallback not used
5252

5353

5454
def test_load_prism_cube_with_point_aoi(stub_prism_backends):
55-
ds = prism.load_prism_cube(
55+
da = prism.load_prism_cube(
5656
lat=40.0,
5757
lon=-105.25,
5858
start="2000-01-01",
5959
end="2000-12-31",
6060
variable="ppt",
6161
)
6262

63-
assert "ppt" in ds.data_vars
63+
assert isinstance(da, xr.DataArray)
64+
assert da.name == "ppt"
6465
aoi = stub_prism_backends["stream"]["aoi"]
6566
assert aoi["min_lat"] < 40.0 < aoi["max_lat"]
6667
assert aoi["min_lon"] < -105.25 < aoi["max_lon"]
6768

6869

6970
def test_load_prism_cube_with_bbox(stub_prism_backends):
70-
ds = prism.load_prism_cube(
71+
da = prism.load_prism_cube(
7172
bbox=[-105.4, 40.0, -105.2, 40.2],
7273
start="2000-01-01",
7374
end="2000-12-31",
7475
variable="ppt",
7576
)
7677

77-
assert "ppt" in ds.data_vars
78+
assert isinstance(da, xr.DataArray)
79+
assert da.name == "ppt"
7880
assert stub_prism_backends["stream"]["aoi"] == {
7981
"min_lon": -105.4,
8082
"min_lat": 40.0,
@@ -99,14 +101,15 @@ def test_load_prism_cube_with_geojson(stub_prism_backends):
99101
"properties": {"name": "Boulder"},
100102
}
101103

102-
ds = prism.load_prism_cube(
104+
da = prism.load_prism_cube(
103105
aoi_geojson=boulder,
104106
start="2000-01-01",
105107
end="2000-12-31",
106108
variable="ppt",
107109
)
108110

109-
assert "ppt" in ds.data_vars
111+
assert isinstance(da, xr.DataArray)
112+
assert da.name == "ppt"
110113
aoi = stub_prism_backends["stream"]["aoi"]
111114
assert aoi["min_lon"] == pytest.approx(-105.35)
112115
assert aoi["max_lon"] == pytest.approx(-105.20)
@@ -145,3 +148,16 @@ def test_load_prism_cube_legacy_positional(stub_prism_backends):
145148

146149
assert "ppt" in ds.data_vars
147150
assert stub_prism_backends["stream"]["aoi"] == aoi
151+
152+
153+
def test_load_prism_cube_variable_returns_dataarray(stub_prism_backends):
154+
da = prism.load_prism_cube(
155+
lat=40.0,
156+
lon=-105.25,
157+
start="2000-01-01",
158+
end="2000-12-31",
159+
variable="ppt",
160+
)
161+
162+
assert isinstance(da, xr.DataArray)
163+
assert da.name == "ppt"

0 commit comments

Comments
 (0)