Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions oceanarray/config/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@
FIGURE_SIZE_WIDE = (14, 6) # multi-instrument overview (scatter / line)
FIGURE_SIZE_TALL = (12, 8) # stacked single-instrument panels

# ---------------------------------------------------------------------------
# Report figure slot widths (inches). The HTML report body is ~1150-1200px
# wide; figures display at full/two-thirds/half/third of that slot via the
# `.fig` CSS max-width caps. Rendering a figure at the matching slot WIDTH
# makes displayed font size depend only on figsize (font_px = pt/72 x
# display_px/fig_in) — independent of dpi — so mplstyle fonts appear at a
# consistent, readable size instead of the double-shrink that oversized (12-16
# in) figsizes caused at half/third slots. Set figure *width* to one of these;
# height is content-driven. (dpi is a separate size knob, set once in the
# mplstyle's savefig.dpi; report figure bytes are cut mainly by palette
# quantization in _fig_to_base64, not by dpi.)
# ---------------------------------------------------------------------------
W_FULL = 9.0 # full-slot figure (CSS max-width 100%)
W_TWOTHIRDS = 6.0 # two-thirds slot (~66%)
W_HALF = 4.5 # half slot (~50%)
W_THIRD = 3.0 # one-third slot (~33%)

# ---------------------------------------------------------------------------
# Resampling
# ---------------------------------------------------------------------------
Expand Down
18 changes: 12 additions & 6 deletions oceanarray/oceanarray.mplstyle
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
axes.titlesize : 15
axes.labelsize : 12
axes.titlesize : 12
axes.labelsize : 10
date.autoformatter.day: %Y-%m-%d
date.converter: auto
figure.figsize: 8, 4
figure.dpi: 100
savefig.dpi: 150
font.family: sans-serif
font.style: normal
font.size: 12
legend.fontsize: 12
font.size: 10
legend.fontsize: 9
lines.linewidth : 1
lines.linestyle: -
lines.markersize : 10
xtick.labelsize : 12
xtick.labelsize : 10
xtick.alignment: center
ytick.labelsize : 12
ytick.labelsize : 10
axes.grid : False
axes.linewidth : 0.6
grid.alpha : 0.5
grid.color : 0.5
grid.linestyle : :
grid.linewidth : 0.6
contour.linewidth : 0.8
4 changes: 1 addition & 3 deletions oceanarray/plotters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
draw_ts_diagram, draw_stack_ts_diagram, draw_grid_ts_diagram

Hydrography (hydrography.py):
draw_isopycnal_fig, draw_isopycnal_ts_fig, draw_isopycnal_coverage,
draw_isopycnal_ts_fig, draw_isopycnal_coverage,
draw_overflow_temperature_fig

Legacy functions (from plotter.py — available via backward-compat shim):
Expand Down Expand Up @@ -131,7 +131,6 @@
# draw_* functions — isopycnal
# ---------------------------------------------------------------------------
from oceanarray.plotters.hydrography import ( # noqa: F401
draw_isopycnal_fig,
draw_isopycnal_ts_fig,
draw_isopycnal_coverage,
draw_overflow_temperature_fig,
Expand Down Expand Up @@ -190,7 +189,6 @@
"draw_stack_ts_diagram",
"draw_grid_ts_diagram",
# draw_* — isopycnal
"draw_isopycnal_fig",
"draw_isopycnal_ts_fig",
"draw_isopycnal_coverage",
"draw_overflow_temperature_fig",
Expand Down
44 changes: 24 additions & 20 deletions oceanarray/plotters/current.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def plot_speed_boxplot(
val = np.percentile(speed_clean, p)
print(f" {p:2d}th percentile: {val:.4f} {units}")

fig, ax = plt.subplots(figsize=(4, 6))
fig, ax = plt.subplots(figsize=(params.W_THIRD, 6))
bp = ax.boxplot(
speed_clean,
vert=True,
Expand Down Expand Up @@ -249,7 +249,7 @@ def plot_multi_aquadopp_trajectories(
_bounds = _nice_colorbar_bounds(0.0, 1.0, n=20)
norm: mcolors.BoundaryNorm = mcolors.BoundaryNorm(_bounds, ncolors=256)

fig, ax = plt.subplots(figsize=(6, 5))
fig, ax = plt.subplots(figsize=(params.W_FULL, 5), constrained_layout=True)

for instr_i, x, y, temp in trajs:
serial = str(serials[instr_i])
Expand Down Expand Up @@ -322,7 +322,6 @@ def plot_multi_aquadopp_trajectories(
title = ds.attrs.get("id", "")
if title:
ax.set_title(title)
fig.tight_layout()
return fig


Expand Down Expand Up @@ -368,10 +367,14 @@ def plot_hodograph(
import pandas as pd

instr_id = ds.attrs.get("id", "")
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
fig.subplots_adjust(top=0.90)
# constrained_layout places the shared colorbar correctly against the
# equal-aspect ("box"-adjustable) panels; without it the colorbar overlaps
# the panel whitespace and appears to sit inside the axes.
fig, axes = plt.subplots(
1, 2, figsize=(params.W_FULL, 4.5), constrained_layout=True
)
if instr_id:
fig.suptitle(instr_id, fontsize=10, y=0.995)
fig.suptitle(instr_id)

if u_var not in ds.data_vars or v_var not in ds.data_vars:
for ax in axes:
Expand Down Expand Up @@ -466,13 +469,13 @@ def _panel(ax: plt.Axes, e: np.ndarray, n: np.ndarray, title: str) -> None:
linewidths=0.5,
label="End",
)
ax.legend(fontsize=7, loc="upper right", framealpha=0.7)
ax.legend(loc="upper right", framealpha=0.7)
ax.axhline(0, color="#bbb", lw=0.7, zorder=0)
ax.axvline(0, color="#bbb", lw=0.7, zorder=0)
ax.set_aspect("equal", adjustable="box")
ax.set_xlabel(f"East ({units})")
ax.set_ylabel(f"North ({units})")
ax.set_title(title, fontsize=10)
ax.set_title(title)
ax.grid(True, linestyle="--", linewidth=0.4, alpha=0.4)

_panel(axes[0], east, north, f"Raw ({smooth_hours:.0f}-h smoothed)")
Expand Down Expand Up @@ -564,7 +567,7 @@ def plot_aquadopp_speed_profile(
hab_range = max(hab_vals) - min(hab_vals) if len(hab_vals) > 1 else 10.0
box_width = max(2.0, hab_range * 0.06)

fig, ax = plt.subplots(figsize=(5, max(3, len(records) * 0.7 + 1)))
fig, ax = plt.subplots(figsize=(params.W_HALF, max(3, len(records) * 0.7 + 1)))

for hab, serial, spd_clean in records:
bp = ax.boxplot(
Expand Down Expand Up @@ -701,7 +704,7 @@ def plot_adcp_trajectories(
_bounds = _nice_colorbar_bounds(hab_min, hab_max, n=_n_hab)
norm: mcolors.BoundaryNorm = mcolors.BoundaryNorm(_bounds, ncolors=256)

fig, ax = plt.subplots(figsize=(6, 5))
fig, ax = plt.subplots(figsize=(params.W_FULL, 5))

for hab, x, y in trajs:
points = np.array([x, y]).T.reshape(-1, 1, 2)
Expand Down Expand Up @@ -803,7 +806,7 @@ def _masked(flag_mask: "np.ndarray") -> "tuple[np.ndarray, np.ndarray]":
fig, axs = plt.subplots(
1,
ncols,
figsize=(ncols * 3.0, 3.2),
figsize=(params.W_TWOTHIRDS, 3.2),
subplot_kw={"projection": "polar"},
squeeze=False,
)
Expand Down Expand Up @@ -911,7 +914,7 @@ def draw_rose_grid(
fig, axs = plt.subplots(
nrows,
ncols,
figsize=(ncols * 3.0, nrows * 3.2),
figsize=(params.W_FULL, nrows * 3.2),
subplot_kw={"projection": "polar"},
squeeze=False,
)
Expand Down Expand Up @@ -988,7 +991,7 @@ def draw_grid_rose(ds: "xr.Dataset", max_roses: int = 4) -> "Optional[plt.Figure
fig, axs = plt.subplots(
nrows,
ncols,
figsize=(ncols * 3.0, nrows * 3.2),
figsize=(params.W_FULL, nrows * 3.2),
subplot_kw={"projection": "polar"},
squeeze=False,
)
Expand Down Expand Up @@ -1055,7 +1058,7 @@ def draw_grid_trajectory(ds: "xr.Dataset") -> "Optional[plt.Figure]":
_bounds, norm = colorbar_norm(vmin=min(p_vals), vmax=max(p_vals))
cmap = plt.get_cmap("viridis_r") # shallow (low p) → light; deep → dark

fig, ax = plt.subplots(figsize=(6, 5))
fig, ax = plt.subplots(figsize=(params.W_HALF, 5))

for p_val, x, y in trajs:
points = np.array([x, y]).T.reshape(-1, 1, 2)
Expand Down Expand Up @@ -1223,7 +1226,7 @@ def draw_adcp_velocity(nc_path: str) -> "Optional[plt.Figure]":

n = len(present)
fig, axes = plt.subplots(
n, 1, figsize=(13, 3.5 * n), sharex=True, squeeze=False
n, 1, figsize=(params.W_FULL, 3.5 * n), sharex=True, squeeze=False
)

orientation = ds.attrs.get("orientation_yaml") or ds.attrs.get(
Expand Down Expand Up @@ -1374,7 +1377,7 @@ def draw_adcp_rose(nc_path: str) -> "Optional[plt.Figure]":
fig, axs = plt.subplots(
1,
ncols,
figsize=(ncols * 3.2, 4.0),
figsize=(params.W_FULL, 4.0),
subplot_kw={"projection": "polar"},
squeeze=False,
)
Expand Down Expand Up @@ -1505,7 +1508,7 @@ def draw_adcp_hodograph(

from oceanarray.report._plots import _draw_hodograph_pair

fig, axes = plt.subplots(2, 2, figsize=(13, 9))
fig, axes = plt.subplots(2, 2, figsize=(params.W_FULL, 9))
fig.subplots_adjust(hspace=0.55, wspace=0.45)

_draw_hodograph_pair(
Expand Down Expand Up @@ -1612,8 +1615,9 @@ def draw_grid_hodograph(

smooth_n = max(3, int(round(smooth_hours * 3600.0 / dt_s)))

fig, (ax_shallow, ax_deep) = plt.subplots(1, 2, figsize=(13, 6))
fig.subplots_adjust(wspace=0.45)
fig, (ax_shallow, ax_deep) = plt.subplots(
1, 2, figsize=(params.W_FULL, 4.5), constrained_layout=True
)

for ax, i_lev, label in [
(ax_shallow, i_shallow, f"Shallow ({label_shallow})"),
Expand All @@ -1626,7 +1630,7 @@ def draw_grid_hodograph(
ax.text(
0.5, 0.5, "No data", transform=ax.transAxes, ha="center", va="center"
)
ax.set_title(label, fontsize=9)
ax.set_title(label)
continue
t_frac = np.linspace(0.0, 1.0, len(east_2d[:, i_lev]))[mask]
hodograph_panel(
Expand Down
62 changes: 40 additions & 22 deletions oceanarray/plotters/diagnostic.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,24 +677,30 @@ def plot_clock_offset_check(
nc_paths: "Dict[str, Path]",
deploy_dt: "Optional[datetime]",
recover_dt: "Optional[datetime]",
window_minutes: int = 10,
window_minutes: int = 30,
) -> "Optional[matplotlib.figure.Figure]":
"""Overlaid temperature time series zoomed to deployment start and end.
"""Overlaid, per-instrument normalised temperature around deploy and recover.

Plots the first and last *window_minutes* of the deployment for every
instrument that has a temperature variable, so that clock alignment
between instruments can be assessed visually. If an instrument's clock
is offset the temperature signal will appear shifted in time relative to
the other instruments.
Plots a ``±window_minutes`` window centred on deployment and on recovery for
every instrument with a temperature variable, so clock alignment between
instruments can be assessed visually. If an instrument's clock is offset the
temperature signal appears shifted in time relative to the others.

Each instrument's trace is **standardised over the plotted window**
(subtract the window mean, divide by the window standard deviation) so
instruments with different absolute temperatures and amplitudes overlay on a
common ``std`` y-axis and their *timing* can be compared directly.

Two sub-panels are produced side by side:

- **Left**: first ``window_minutes`` minutes after ``deploy_dt``
- **Right**: last ``window_minutes`` minutes before ``recover_dt``
- **Left**: ``deploy_dt ± window_minutes``
- **Right**: ``recover_dt ± window_minutes``

When ``deploy_dt`` or ``recover_dt`` is ``None``, only the available
window is produced. A shared legend below both panels lists all
instruments.
instruments. An instrument with zero variance in a window (flat/constant)
is skipped for that window (no timing information, and normalisation is
undefined).

Parameters
----------
Expand Down Expand Up @@ -756,16 +762,18 @@ def plot_clock_offset_check(
windows: list = []
if deploy_dt is not None:
t0 = np.datetime64(deploy_dt.replace(tzinfo=None).isoformat())
windows.append((t0, t0 + _td, f"Start +{window_minutes} min"))
windows.append((t0 - _td, t0 + _td, f"Deployment ±{window_minutes} min"))
if recover_dt is not None:
t1 = np.datetime64(recover_dt.replace(tzinfo=None).isoformat())
windows.append((t1 - _td, t1, f"End −{window_minutes} min"))
windows.append((t1 - _td, t1 + _td, f"Recovery ±{window_minutes} min"))
if not windows:
return None

n_panels = len(windows)
with plt.style.context(str(params.MPLSTYLE)):
fig, axes = plt.subplots(1, n_panels, figsize=(5 * n_panels, 3.5), sharey=False)
fig, axes = plt.subplots(
1, n_panels, figsize=(params.W_FULL, 3.5), sharey=False
)
if n_panels == 1:
axes = [axes]

Expand All @@ -778,16 +786,21 @@ def plot_clock_offset_check(
mask = (t >= t_lo) & (t <= t_hi) & np.isfinite(temp)
if not np.any(mask):
continue
ax.plot(t[mask], temp[mask], color=colors[serial], lw=1.0)
tw = temp[mask]
sd = np.nanstd(tw)
if not np.isfinite(sd) or sd == 0:
continue # flat window: no timing info, normalisation undefined
tw_norm = (tw - np.nanmean(tw)) / sd
ax.plot(t[mask], tw_norm, color=colors[serial], lw=1.0)
plotted_serials.add(serial)

ax.set_title(title, fontsize=8)
ax.set_title(title)
ax.set_xlabel("Time (UTC)")
ax.set_ylabel("Temperature (°C)")
ax.set_ylabel("Normalised temperature (std)")
ax.grid(True, linestyle="--", linewidth=0.4, alpha=0.3)
locator = mdates.AutoDateLocator()
ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(locator))
ax.tick_params(axis="x", labelsize=7)

if plotted_serials:
from matplotlib.lines import Line2D
Expand All @@ -802,7 +815,6 @@ def plot_clock_offset_check(
loc="lower center",
ncol=min(len(handles), 6),
bbox_to_anchor=(0.5, -0.05),
fontsize=7,
frameon=True,
)

Expand All @@ -823,6 +835,7 @@ def draw_windows(
show_qc: bool = True,
vlines: Optional[list] = None,
stage1_nc: Optional[Path] = None,
panels: Optional[list] = None,
) -> "Optional[plt.Figure]":
"""Combined start + end window figure: (nrows × 2) — left = first N h, right = last N h.

Expand All @@ -836,6 +849,10 @@ def draw_windows(
Width of each window in hours (default 6).
show_qc : bool
Overlay QC flag markers on the data.
panels : list, optional
Subset of ``_instrument_panels`` tuples to draw. When given, only these
rows are rendered (used to paginate a tall window figure across several
images); otherwise every panel for the instrument is drawn on one figure.
vlines : list of (time_val, color, label), optional
Vertical marker lines to draw on both panels. *time_val* may be a
``numpy.datetime64``, an ISO-8601 string, or a ``pandas.Timestamp``.
Expand Down Expand Up @@ -890,16 +907,17 @@ def draw_windows(
# One sample interval used to expand x-axis limits (stage2/3 fallback).
_dt_one = (time[1] - time[0]) if len(time) > 1 else np.timedelta64(300, "s") # noqa: F841

panels = _instrument_panels(ds, combine_pitch_roll=True)
if panels is None:
panels = _instrument_panels(ds, combine_pitch_roll=True)
if not panels:
return None

height_ratios = [
_COMPACT_PANEL_HEIGHT if vname in _COMPACT_PANEL_VARS else 3.0
_COMPACT_PANEL_HEIGHT if vname in _COMPACT_PANEL_VARS else 2.0
for vname, *_ in panels
]
nrows = len(panels)
fig = plt.figure(figsize=(13, sum(height_ratios)))
fig = plt.figure(figsize=(params.W_FULL, sum(height_ratios)))
gs = GridSpec(
nrows,
2,
Expand Down Expand Up @@ -1423,7 +1441,7 @@ def draw_velocity_iqr_profile(ds: "xr.Dataset") -> "Optional[plt.Figure]":
fig, axs = plt.subplots(
1,
n_panels,
figsize=(n_panels * 3.5, 6),
figsize=(params.W_FULL, 4.5),
sharey=True,
gridspec_kw={"width_ratios": [2] * (n_panels - 1) + [1]},
)
Expand Down
Loading
Loading