Skip to content

Commit 3ea9ab4

Browse files
committed
Update cube viewer axis rig and responsive sizing
1 parent d4fadb2 commit 3ea9ab4

File tree

4 files changed

+74
-54
lines changed

4 files changed

+74
-54
lines changed

src/cubedynamics/plotting/axis_rig.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -300,18 +300,22 @@ def axis_rig_css(spec: AxisRigSpec) -> str:
300300
height: var(--cd-cube-size, var(--cube-size));
301301
transform-style: preserve-3d;
302302
pointer-events: none;
303-
color: var(--cd-axis-color, rgba(25, 25, 25, 0.85));
303+
color: var(--cd-axis-color, var(--cube-axis-color, rgba(25, 25, 25, 0.9)));
304304
font-size: var(--cube-axis-font-size, 13px);
305305
letter-spacing: 0.04em;
306306
--cd-axis-out-x: 6px;
307307
--cd-axis-out-y: 8px;
308-
--cd-axis-front-z: calc(var(--cd-cube-size, var(--cube-size)) / 2);
308+
--cd-axis-front-z: calc(0.5 * var(--cd-cube-size, var(--cube-size)));
309309
--cd-axis-out-z: calc(var(--cd-axis-front-z) + 6px);
310310
--cd-axis-line-w: 2px;
311311
--cd-axis-tick-w: 1px;
312312
--cd-axis-tick-l: 8px;
313313
--cd-axis-label-gap: 12px;
314314
--cd-axis-end-nudge: 6px;
315+
--cd-axis-x-name-drop: 18px;
316+
--cd-axis-time-name-drop: 24px;
317+
--cd-axis-time-label-drop: 18px;
318+
--cd-axis-y-extra-left: 22px;
315319
--cd-bb-rot-x: 0rad;
316320
--cd-bb-rot-y: 0rad;
317321
}
@@ -339,7 +343,7 @@ def axis_rig_css(spec: AxisRigSpec) -> str:
339343
340344
.cd-axis-line {
341345
position: absolute;
342-
background: var(--cd-axis-color, rgba(25, 25, 25, 0.85));
346+
background: var(--cd-axis-color, var(--cube-axis-color, rgba(25, 25, 25, 0.9)));
343347
}
344348
345349
.cd-axis-ticks {
@@ -353,7 +357,7 @@ def axis_rig_css(spec: AxisRigSpec) -> str:
353357
354358
.cd-axis-tick-mark {
355359
position: absolute;
356-
background: var(--cd-axis-color, rgba(25, 25, 25, 0.85));
360+
background: var(--cd-axis-color, var(--cube-axis-color, rgba(25, 25, 25, 0.9)));
357361
}
358362
359363
.cd-axis-label {
@@ -368,7 +372,7 @@ def axis_rig_css(spec: AxisRigSpec) -> str:
368372
}
369373
370374
.cd-axis-label-face--time {
371-
transform: rotateY(-90deg) rotateX(var(--cd-bb-rot-x)) rotateY(var(--cd-bb-rot-y));
375+
transform: rotateX(var(--cd-bb-rot-x)) rotateY(var(--cd-bb-rot-y));
372376
}
373377
374378
/* X axis (longitude) - front bottom edge */
@@ -409,19 +413,19 @@ def axis_rig_css(spec: AxisRigSpec) -> str:
409413
410414
.cd-axis-x .cd-axis-end--min {
411415
left: 0;
412-
top: calc(-1 * var(--cd-axis-label-gap));
416+
top: var(--cd-axis-x-name-drop);
413417
transform: translateX(calc(-1 * var(--cd-axis-end-nudge)));
414418
}
415419
416420
.cd-axis-x .cd-axis-end--max {
417421
left: 100%;
418-
top: calc(-1 * var(--cd-axis-label-gap));
422+
top: var(--cd-axis-x-name-drop);
419423
transform: translateX(calc(-100% + var(--cd-axis-end-nudge)));
420424
}
421425
422426
.cd-axis-x .cd-axis-name {
423427
left: 50%;
424-
top: calc(-1 * (var(--cd-axis-label-gap) + 12px));
428+
top: var(--cd-axis-x-name-drop);
425429
transform: translateX(-50%);
426430
}
427431
@@ -456,29 +460,41 @@ def axis_rig_css(spec: AxisRigSpec) -> str:
456460
457461
.cd-axis-y .cd-axis-tick-label {
458462
position: absolute;
459-
left: calc(-1 * (var(--cd-axis-label-gap) + var(--cd-axis-tick-l)));
463+
left: calc(
464+
-1 * (var(--cd-axis-label-gap) + var(--cd-axis-tick-l) + 12px + var(--cd-axis-y-extra-left))
465+
);
460466
top: 50%;
461467
transform: translateY(-50%);
462468
}
463469
464470
.cd-axis-y .cd-axis-end--min {
465-
left: calc(-1 * (var(--cd-axis-label-gap) + var(--cd-axis-tick-l)));
471+
left: calc(
472+
-1 * (var(--cd-axis-label-gap) + var(--cd-axis-tick-l) + 12px + var(--cd-axis-y-extra-left))
473+
);
466474
bottom: 0;
467475
transform: translateY(calc(var(--cd-axis-end-nudge)));
468476
}
469477
470478
.cd-axis-y .cd-axis-end--max {
471-
left: calc(-1 * (var(--cd-axis-label-gap) + var(--cd-axis-tick-l)));
479+
left: calc(
480+
-1 * (var(--cd-axis-label-gap) + var(--cd-axis-tick-l) + 12px + var(--cd-axis-y-extra-left))
481+
);
472482
bottom: 100%;
473483
transform: translateY(calc(-100% - var(--cd-axis-end-nudge)));
474484
}
475485
476486
.cd-axis-y .cd-axis-name {
477-
left: calc(-1 * (var(--cd-axis-label-gap) + var(--cd-axis-tick-l) + 12px));
487+
left: calc(
488+
-1 * (var(--cd-axis-label-gap) + var(--cd-axis-tick-l) + 12px + var(--cd-axis-y-extra-left))
489+
);
478490
bottom: 50%;
479491
transform: translateY(50%);
480492
}
481493
494+
.cd-axis-y .cd-axis-label-face {
495+
transform-origin: 100% 50%;
496+
}
497+
482498
/* Time axis - bottom right edge, rotated into depth */
483499
.cd-axis-time {
484500
left: 100%;
@@ -512,25 +528,25 @@ def axis_rig_css(spec: AxisRigSpec) -> str:
512528
.cd-axis-time .cd-axis-tick-label {
513529
position: absolute;
514530
left: 50%;
515-
top: calc(-1 * var(--cd-axis-label-gap));
531+
top: var(--cd-axis-time-label-drop);
516532
transform: translateX(-50%);
517533
}
518534
519535
.cd-axis-time .cd-axis-end--min {
520536
left: 0;
521-
top: calc(-1 * var(--cd-axis-label-gap));
537+
top: var(--cd-axis-time-label-drop);
522538
transform: translateX(calc(-1 * var(--cd-axis-end-nudge)));
523539
}
524540
525541
.cd-axis-time .cd-axis-end--max {
526542
left: 100%;
527-
top: calc(-1 * var(--cd-axis-label-gap));
543+
top: var(--cd-axis-time-label-drop);
528544
transform: translateX(calc(-100% + var(--cd-axis-end-nudge)));
529545
}
530546
531547
.cd-axis-time .cd-axis-name {
532548
left: 50%;
533-
top: calc(-1 * (var(--cd-axis-label-gap) + 12px));
549+
top: var(--cd-axis-time-name-drop);
534550
transform: translateX(-50%);
535551
}
536552
"""

src/cubedynamics/plotting/cube_plot.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class CubeTheme:
6969
legend_scale: float = 0.6
7070

7171
title_color: str = "cornflowerblue"
72-
axis_color: str = "firebrick"
72+
axis_color: str = "rgba(25, 25, 25, 0.9)"
7373
legend_color: str = "limegreen"
7474

7575
caption_font_scale: float = 0.8
@@ -490,7 +490,7 @@ class CubePlot(metaclass=_CubePlotMeta):
490490
legend_title: Optional[str] = None
491491
theme: CubeTheme = field(default_factory=theme_cube_studio)
492492
caption: Optional[Dict[str, Any]] = None
493-
size_px: int = 260
493+
size_px: int | None = None
494494
cmap: str = "cividis"
495495
fill_scale: Optional[ScaleFillContinuous] = None
496496
alpha_scale: Optional[ScaleAlphaContinuous] = None
@@ -977,10 +977,11 @@ def _repr_html_(self) -> str: # pragma: no cover - exercised in notebooks
977977
logger.info("CubePlot._repr_html_ called for %s", getattr(self.data, "name", None))
978978
html_out = self.to_html()
979979
prefix = Path(self.out_html).stem or "cube_viewer"
980+
frame_base = self.size_px if self.size_px is not None else 760
980981
iframe = show_cube_viewer(
981982
html_out,
982-
width=max(850, self.size_px + 300),
983-
height=max(850, self.size_px + 300),
983+
width=max(850, frame_base + 300),
984+
height=max(850, frame_base + 300),
984985
prefix=prefix,
985986
)
986987
self._last_iframe = iframe # used in tests/notebooks to locate the file

src/cubedynamics/plotting/cube_viewer.py

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -164,38 +164,35 @@ def _build_legend_html(
164164
)
165165

166166

167-
def _interior_plane_transform(axis: str, index: int, meta: Dict[str, int], size_px: int) -> str:
168-
half = size_px / 2
167+
def _interior_plane_transform(axis: str, index: int, meta: Dict[str, int], size_css: str) -> str:
169168
nt = max(meta.get("nt", 1) - 1, 1)
170169
ny = max(meta.get("ny", 1) - 1, 1)
171170
nx = max(meta.get("nx", 1) - 1, 1)
172171
if axis == "time":
173172
frac = index / nt
174-
z = -half + frac * size_px
175-
return f"translate3d(0px, 0px, {z:.2f}px)"
173+
z = f"calc(-0.5 * {size_css} + {frac:.6f} * {size_css})"
174+
return f"translate3d(0px, 0px, {z})"
176175
if axis == "x":
177176
frac = index / nx
178-
x_off = -half + frac * size_px
179-
return f"rotateY(90deg) translateZ({x_off:.2f}px)"
177+
x_off = f"calc(-0.5 * {size_css} + {frac:.6f} * {size_css})"
178+
return f"rotateY(90deg) translateZ({x_off})"
180179
if axis == "y":
181180
frac = index / ny
182-
y_off = -half + frac * size_px
183-
return f"rotateX(90deg) translateZ({y_off:.2f}px)"
181+
y_off = f"calc(-0.5 * {size_css} + {frac:.6f} * {size_css})"
182+
return f"rotateX(90deg) translateZ({y_off})"
184183
return ""
185184

186185

187-
def _vase_panel_transform(panel: VasePanel, size_px: int) -> str:
188-
half = size_px / 2
189-
190-
def _offset(norm: float) -> float:
191-
return -half + float(norm) * size_px
186+
def _vase_panel_transform(panel: VasePanel, size_css: str) -> str:
187+
def _offset(norm: float) -> str:
188+
return f"calc(-0.5 * {size_css} + {float(norm):.6f} * {size_css})"
192189

193190
x_off = _offset(panel.x)
194191
y_off = _offset(panel.y)
195192
z_off = _offset(panel.z)
196193

197194
return (
198-
f"translate3d({x_off:.2f}px, {y_off:.2f}px, {z_off:.2f}px) "
195+
f"translate3d({x_off}, {y_off}, {z_off}) "
199196
f"rotateY({panel.yaw:.2f}deg) rotateX(90deg)"
200197
)
201198

@@ -214,7 +211,7 @@ def _render_cube_html(
214211
coord: "CoordCube" | None,
215212
legend_html: str,
216213
title_html: str,
217-
size_px: int,
214+
size_px: int | None,
218215
axis_meta: Dict[str, Dict[str, str]] | None,
219216
axis_rig_spec: AxisRigSpec | None,
220217
axis_rig_meta: Dict[str, Any] | None,
@@ -227,9 +224,10 @@ def _render_cube_html(
227224
# file export. Keeping the template self contained ensures that v.plot()
228225
# renders the exact same viewer in both contexts.
229226
interior_html_parts = []
227+
size_css = "var(--cd-cube-size)"
230228
if interior_planes:
231229
for axis, idx, b64, meta in interior_planes:
232-
transform = _interior_plane_transform(axis, idx, meta or interior_meta, size_px)
230+
transform = _interior_plane_transform(axis, idx, meta or interior_meta, size_css)
233231
interior_html_parts.append(
234232
"<div class=\"interior-plane\" "
235233
f"style=\"transform: {transform}; background-image: url('data:image/png;base64,{b64}');\"></div>"
@@ -239,12 +237,12 @@ def _render_cube_html(
239237
vase_html_parts = []
240238
if vase_panels:
241239
for panel in vase_panels:
242-
width_px = max(2.0, float(panel.width) * size_px)
243-
height_px = max(2.0, float(panel.height) * size_px)
244-
transform = _vase_panel_transform(panel, size_px)
240+
width_px = f"max(2px, calc({float(panel.width):.6f} * {size_css}))"
241+
height_px = f"max(2px, calc({float(panel.height):.6f} * {size_css}))"
242+
transform = _vase_panel_transform(panel, size_css)
245243
vase_html_parts.append(
246244
"<div class=\"cd-vase-panel\" "
247-
f"style=\"width: {width_px:.2f}px; height: {height_px:.2f}px; transform: {transform};\"></div>"
245+
f"style=\"width: {width_px}; height: {height_px}; transform: {transform};\"></div>"
248246
)
249247
vase_html = "".join(vase_html_parts)
250248

@@ -283,15 +281,18 @@ def _render_cube_html(
283281

284282
axis_rig_data_attr = " data-axis-rig=\"true\"" if axis_rig_spec else ""
285283

284+
cube_size_css = (
285+
f"{size_px}px" if size_px is not None else "clamp(320px, min(70vh, 70vw), 760px)"
286+
)
286287
html = f"""
287288
<!DOCTYPE html>
288289
<html lang=\"en\">
289290
<head>
290291
<meta charset=\"UTF-8\" />
291292
<style>
292293
:root {{
293-
--cube-size: {size_px}px;
294-
--cd-cube-size: var(--cube-size);
294+
--cd-cube-size: {cube_size_css};
295+
--cube-size: var(--cd-cube-size);
295296
{css_vars}
296297
}}
297298
* {{ box-sizing: border-box; }}
@@ -807,7 +808,7 @@ def cube_from_dataarray(
807808
da: xr.DataArray,
808809
out_html: str = "cube_da.html",
809810
cmap: str = "viridis",
810-
size_px: int = 260,
811+
size_px: int | None = None,
811812
thin_time_factor: int = 4,
812813
title: str | None = None,
813814
time_label: str | None = None,
@@ -1065,7 +1066,7 @@ def _face_to_png(arr: np.ndarray, mask_key: str | None) -> str:
10651066
"--cube-panel-color": getattr(theme, "panel_color", "#000"),
10661067
"--cube-shadow-strength": str(getattr(theme, "shadow_strength", 0.2)),
10671068
"--cube-title-color": getattr(theme, "title_color", "#f7f7f7"),
1068-
"--cube-axis-color": getattr(theme, "axis_color", "#f7f7f7"),
1069+
"--cube-axis-color": getattr(theme, "axis_color", "rgba(25, 25, 25, 0.9)"),
10691070
"--cube-legend-color": getattr(theme, "legend_color", "#f7f7f7"),
10701071
"--cube-title-font-size": f"{getattr(theme, 'title_font_size', 18)}px",
10711072
"--cube-axis-font-size": f"{getattr(theme, 'title_font_size', 18) * getattr(theme, 'axis_scale', 0.6)}px",
@@ -1113,11 +1114,12 @@ def _face_to_png(arr: np.ndarray, mask_key: str | None) -> str:
11131114
f.write(full_html)
11141115
if return_html:
11151116
return full_html
1117+
frame_base = size_px if size_px is not None else 760
11161118
prefix = Path(out_html).stem or "cube_viewer"
11171119
return show_cube_viewer(
11181120
full_html,
1119-
width=max(850, size_px + 300),
1120-
height=max(850, size_px + 300),
1121+
width=max(850, frame_base + 300),
1122+
height=max(850, frame_base + 300),
11211123
prefix=prefix,
11221124
)
11231125

@@ -1195,11 +1197,12 @@ def _face_to_png(arr: np.ndarray, mask_key: str | None) -> str:
11951197

11961198
if return_html:
11971199
return full_html
1200+
frame_base = size_px if size_px is not None else 760
11981201
prefix = Path(out_html).stem or "cube_viewer"
11991202
return show_cube_viewer(
12001203
full_html,
1201-
width=max(850, size_px + 300),
1202-
height=max(850, size_px + 300),
1204+
width=max(850, frame_base + 300),
1205+
height=max(850, frame_base + 300),
12031206
prefix=prefix,
12041207
)
12051208

src/cubedynamics/verbs/plot.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
class PlotOptions:
4242
title: str | None = None
4343
cmap: str = "viridis"
44-
size_px: int = 260
44+
size_px: int | None = None
4545
thin_time_factor: int = 4
4646
time_dim: str | None = None
4747
clim: tuple[float, float] | None = None
@@ -58,7 +58,7 @@ def plot(
5858
*,
5959
title: str | None = None,
6060
cmap: str = "viridis",
61-
size_px: int = 260,
61+
size_px: int | None = None,
6262
thin_time_factor: int = 4,
6363
time_dim: str | None = None,
6464
clim: tuple[float, float] | None = None,
@@ -76,7 +76,7 @@ def plot(
7676
*,
7777
title: str | None = None,
7878
cmap: str = "viridis",
79-
size_px: int = 260,
79+
size_px: int | None = None,
8080
thin_time_factor: int = 4,
8181
time_dim: str | None = None,
8282
clim: tuple[float, float] | None = None,
@@ -94,7 +94,7 @@ def plot(
9494
*,
9595
title: str | None = None,
9696
cmap: str = "viridis",
97-
size_px: int = 260,
97+
size_px: int | None = None,
9898
thin_time_factor: int = 4,
9999
time_dim: str | None = None,
100100
clim: tuple[float, float] | None = None,
@@ -122,8 +122,8 @@ def plot(
122122
Override the viewer title. Defaults to ``<name> time × y × x cube``.
123123
cmap : str, default "viridis"
124124
Colormap used for the fill scale.
125-
size_px : int, default 260
126-
Pixel size for each facet tile.
125+
size_px : int, optional
126+
Pixel size for each facet tile. If omitted, the viewer uses responsive sizing.
127127
thin_time_factor : int, default 4
128128
Decimation factor for time frames to keep the viewer responsive.
129129
time_dim : str, optional

0 commit comments

Comments
 (0)