diff --git a/oceanarray/config/parameters.py b/oceanarray/config/parameters.py index 1641d09..179bec0 100644 --- a/oceanarray/config/parameters.py +++ b/oceanarray/config/parameters.py @@ -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 # --------------------------------------------------------------------------- diff --git a/oceanarray/oceanarray.mplstyle b/oceanarray/oceanarray.mplstyle index 68cdfda..3c8530d 100644 --- a/oceanarray/oceanarray.mplstyle +++ b/oceanarray/oceanarray.mplstyle @@ -1,5 +1,5 @@ -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 @@ -7,12 +7,18 @@ 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 diff --git a/oceanarray/plotters/__init__.py b/oceanarray/plotters/__init__.py index f47f874..a5fe180 100644 --- a/oceanarray/plotters/__init__.py +++ b/oceanarray/plotters/__init__.py @@ -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): @@ -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, @@ -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", diff --git a/oceanarray/plotters/current.py b/oceanarray/plotters/current.py index 015e2e0..3f1ab9e 100644 --- a/oceanarray/plotters/current.py +++ b/oceanarray/plotters/current.py @@ -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, @@ -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]) @@ -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 @@ -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: @@ -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)") @@ -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( @@ -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) @@ -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, ) @@ -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, ) @@ -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, ) @@ -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) @@ -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( @@ -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, ) @@ -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( @@ -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})"), @@ -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( diff --git a/oceanarray/plotters/diagnostic.py b/oceanarray/plotters/diagnostic.py index 121fe0b..66b6e9b 100644 --- a/oceanarray/plotters/diagnostic.py +++ b/oceanarray/plotters/diagnostic.py @@ -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 ---------- @@ -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] @@ -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 @@ -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, ) @@ -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. @@ -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``. @@ -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, @@ -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]}, ) diff --git a/oceanarray/plotters/hydrography.py b/oceanarray/plotters/hydrography.py index a3ecd18..feaa287 100644 --- a/oceanarray/plotters/hydrography.py +++ b/oceanarray/plotters/hydrography.py @@ -1,7 +1,6 @@ """Tier-2 domain wrappers for hydrographic section and isopycnal plots. hydrography.py contains: - - ``draw_isopycnal_fig``: time × pressure with iso-sigma contour lines. - ``draw_isopycnal_ts_fig``: isopycnal height-above-seabed time series. - ``draw_isopycnal_coverage``: three-panel isopycnal diagnostic. - ``draw_overflow_temperature_fig``: temperature time series at ~100 m above seabed. @@ -19,79 +18,10 @@ import matplotlib.pyplot as plt import xarray as xr -from .primitives import colorbar_norm, date_axis, pressure_axis -from ..analysis.temporal import filter_sigma_tukey +from .primitives import colorbar_norm, date_axis from .. import parameters as params -def draw_isopycnal_fig( - da: "xr.DataArray", - levels: list, - filter_samples: int = 0, - zoom_center_idx: Optional[int] = None, - zoom_n: int = 0, -) -> "plt.Figure": - """Render time × pressure with iso-sigma contour lines; return a Figure. - - Parameters - ---------- - da : xr.DataArray - DataArray with ``pressure`` and ``time`` dimensions. - levels : list - Sigma-0 contour levels (kg m⁻³). - filter_samples : int - If > 1, apply a Tukey moving-average filter over this many samples. - zoom_center_idx : int, optional - Centre index for a time-axis zoom window. - zoom_n : int - Half-width (in samples) of the zoom window. - - Returns - ------- - plt.Figure - - """ - import matplotlib.pyplot as plt - - da_tp = da.transpose("pressure", "time") - time_vals = da_tp["time"].values - pressure_vals = da_tp["pressure"].values - data = da_tp.values - - if zoom_center_idx is not None and zoom_n > 0: - t0 = max(0, zoom_center_idx - zoom_n // 2) - t1 = min(data.shape[1], t0 + zoom_n) - time_vals = time_vals[t0:t1] - data = data[:, t0:t1] - - if filter_samples > 1 and data.shape[1] > filter_samples: - data = filter_sigma_tukey(data, filter_samples) - - level_colors = ["#808080"] + ["black"] * (len(levels) - 1) - - fig, ax = plt.subplots(figsize=(13, 4)) - for lev, col in zip(levels, level_colors): - try: - ax.contour( - time_vals, - pressure_vals, - data, - levels=[lev], - colors=[col], - linewidths=1.2, - ) - except Exception: # noqa: BLE001 — individual contour level may fail; skip and continue - pass - ax.plot([], [], color=col, lw=1.2, label=f"σ₀ = {lev} kg m⁻³") - - pressure_axis(ax) - date_axis(ax) - ax.set_xlabel("Time") - if levels: - ax.legend(loc="upper right", framealpha=0.8) - return fig - - def draw_isopycnal_ts_fig(ds_iso: "xr.Dataset") -> "Optional[plt.Figure]": """Isopycnal height-above-seabed time series; return a Figure. @@ -138,7 +68,7 @@ def draw_isopycnal_ts_fig(ds_iso: "xr.Dataset") -> "Optional[plt.Figure]": color_norms = np.linspace(0.25, 0.95, max(n_levels, 1)) colors = [cmap(v) for v in color_norms] - fig, ax = plt.subplots(figsize=(13, 4)) + fig, ax = plt.subplots(figsize=(params.W_FULL, 4)) for i, (sval, col) in enumerate(zip(sigma_vals, colors)): h = height[i, :] @@ -309,7 +239,7 @@ def _bar_color(p: float) -> str: fig, (ax0, ax1, ax2) = plt.subplots( 1, 3, - figsize=(14, fig_h), + figsize=(params.W_FULL, fig_h), sharey=True, gridspec_kw={"width_ratios": [0.8, 1.0, 1.2]}, ) @@ -450,7 +380,7 @@ def draw_overflow_temperature_fig(ds: "xr.Dataset") -> "Optional[plt.Figure]": .values ) - fig, ax = plt.subplots(figsize=(13, 3)) + fig, ax = plt.subplots(figsize=(params.W_FULL, 3)) ax.plot(time_vals, temp_med, color="#1a3a5c", lw=1.0) ax.set_ylabel(params.vlabel("temperature")) hab = waterdepth - actual_p diff --git a/oceanarray/plotters/primitives.py b/oceanarray/plotters/primitives.py index 24e3cb4..8af48fe 100644 --- a/oceanarray/plotters/primitives.py +++ b/oceanarray/plotters/primitives.py @@ -142,7 +142,7 @@ def plot_trajectory( matplotlib.figure.Figure """ - fig, ax = plt.subplots(figsize=(7, 6)) + fig, ax = plt.subplots(figsize=(params.W_HALF, 6), constrained_layout=True) if color_data is not None: points = np.array([x, y]).T.reshape(-1, 1, 2) @@ -168,7 +168,7 @@ def plot_trajectory( # Start/end markers ax.plot(x[0], y[0], "o", color="green", markersize=8, label="Start", zorder=5) ax.plot(x[-1], y[-1], "s", color="red", markersize=8, label="End", zorder=5) - ax.legend(fontsize=8) + ax.legend() ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) @@ -178,7 +178,6 @@ def plot_trajectory( ax.axvline(0, color="k", linewidth=0.5, linestyle="--", alpha=0.4) ax.set_aspect("equal", adjustable="box") ax.grid(True, linestyle="--", linewidth=0.5, alpha=0.4) - fig.tight_layout() return fig @@ -227,9 +226,9 @@ def hodograph_panel( cb = ax.figure.colorbar( sm, ax=ax, shrink=0.75, pad=0.03, aspect=20, ticks=bounds_lc ) - cb.set_label("Time ->", size=8) + cb.set_label("Time →") cb.ax.set_yticks([0.0, 1.0]) - cb.ax.set_yticklabels(["start", "end"], size=7) + cb.ax.set_yticklabels(["start", "end"]) ax.scatter( e_v[0], @@ -258,7 +257,7 @@ def hodograph_panel( ax.axvline(0, color="#888", lw=0.7) ax.set_xlabel(f"East ({units})") ax.set_ylabel(f"North ({units})") - ax.set_title(title, fontsize=9) + ax.set_title(title) ax.grid(True, linestyle="--", linewidth=0.4, alpha=0.3) diff --git a/oceanarray/plotters/spectrum.py b/oceanarray/plotters/spectrum.py index 146a0cb..dee5b32 100644 --- a/oceanarray/plotters/spectrum.py +++ b/oceanarray/plotters/spectrum.py @@ -12,12 +12,7 @@ Pairs with :mod:`oceanarray.analysis.spectral` for spectral computations. Post-OdB remaining migrations from report/_plots.py: - plot_grid_fig (was _make_grid_fig_b64), - plot_isopycnal (was _make_isopycnal_fig_b64), plot_grid_n2 (was _make_grid_n2_b64). - -Note: _filter_sigma_tukey belongs in tools/ (data pre-treatment), not here. -Callers pass pre-filtered data; high-level wrappers like plot_isopycnal may -apply the filter internally but expose it as a parameter. + plot_grid_fig (was _make_grid_fig_b64), plot_grid_n2 (was _make_grid_n2_b64). See .claude/plotters_update-20260718.md for migration checklist. """ @@ -32,6 +27,7 @@ from oceanarray.utilities import _nice_colorbar_bounds, period_axis_ticks from ..analysis.spectral import gonella_rotary_spectrum +from .. import parameters as params if TYPE_CHECKING: import matplotlib.axes @@ -357,7 +353,7 @@ def draw_spectrum( from matplotlib.ticker import NullLocator - fig, (ax_lf, ax_hf) = plt.subplots(1, 2, figsize=(14, 5)) + fig, (ax_lf, ax_hf) = plt.subplots(1, 2, figsize=(params.W_FULL, 5)) x_min_lf = max(nyq_period, 10.0 / 1440.0) # right edge: Nyquist or 10 min # Left edge = longest period Welch can estimate = 1/min_freq = window length @@ -651,7 +647,7 @@ def draw_wavelet( n_panels = len(results) # height ratios: 1 part time series, 3 parts wavelet, per level hr = [1, 3] * n_panels - fig = plt.figure(figsize=(14, 4.5 * n_panels)) + fig = plt.figure(figsize=(params.W_FULL, 4.5 * n_panels)) gs = GridSpec(2 * n_panels, 1, figure=fig, height_ratios=hr, hspace=0.08) tax: list = [] # time series axes (top of each pair) @@ -873,7 +869,7 @@ def draw_grid_rotary_spectrum( cmap_cw = plt.get_cmap("Reds") cmap_ccw = plt.get_cmap("Blues") - fig, (ax_spec, ax_rot) = plt.subplots(1, 2, figsize=(13, 5)) + fig, (ax_spec, ax_rot) = plt.subplots(1, 2, figsize=(params.W_FULL, 5)) # Panel 1: CW (solid, reds) + CCW (dashed, blues) for s_cw, s_ccw, p in zip(s_cw_list, s_ccw_list, press_plotted): diff --git a/oceanarray/plotters/ts.py b/oceanarray/plotters/ts.py index bbe3c71..b566e14 100644 --- a/oceanarray/plotters/ts.py +++ b/oceanarray/plotters/ts.py @@ -153,7 +153,7 @@ def draw_ts_diagram(nc_path: Path) -> "Optional[plt.Figure]": ncols = 3 if has_sat else 2 fig, axes = plt.subplots( - 1, ncols, figsize=(5.5 * ncols, 4.5), constrained_layout=True + 1, ncols, figsize=(params.W_FULL, 4.5), constrained_layout=True ) ax_l, ax_r = axes[0], axes[1] ax_sat = axes[2] if has_sat else None @@ -292,8 +292,9 @@ def draw_stack_ts_diagram(ds: "xr.Dataset") -> "Optional[plt.Figure]": has_sat = SAT_flat is not None and np.isfinite(SAT_flat).any() ncols = 3 if has_sat else 2 - fig_w = 5.5 * ncols # ~5.5 in per panel keeps them compact in a row - fig, axes = plt.subplots(1, ncols, figsize=(fig_w, 4.5), constrained_layout=True) + fig, axes = plt.subplots( + 1, ncols, figsize=(params.W_FULL, 4.5), constrained_layout=True + ) ax_scatter, ax_heat = axes[0], axes[1] ax_sat = axes[2] if has_sat else None @@ -423,7 +424,7 @@ def draw_grid_ts_diagram( has_o2 = has_o2 and o2_valid.any() ncols = 2 if has_o2 else 1 - fig, axes = plt.subplots(1, ncols, figsize=(6 * ncols, 5)) + fig, axes = plt.subplots(1, ncols, figsize=(params.W_HALF, 5)) if ncols == 1: axes = [axes] diff --git a/oceanarray/report/_grid.py b/oceanarray/report/_grid.py index ea4d620..d2128f1 100644 --- a/oceanarray/report/_grid.py +++ b/oceanarray/report/_grid.py @@ -191,7 +191,7 @@
Median (solid line) and interquartile range (shaded, 25–75 %) across the full deployment at each pressure level. 2.5–97.5 % outer envelope for speed. Right panel: count of non-NaN values per depth.
No plottable variables found.
{% endif %} @@ -276,7 +278,9 @@ stage 1 record — the suggested recovery time equals the last raw sample. Check the timing table in the mooring summary report for the suggested UTC time. -Insufficient data for start/end windows.
{% endif %} diff --git a/oceanarray/report/_mooring.py b/oceanarray/report/_mooring.py index f6195c7..edacb51 100644 --- a/oceanarray/report/_mooring.py +++ b/oceanarray/report/_mooring.py @@ -1405,19 +1405,28 @@ def _build_context( any_clock = any(i["clock"]["has_correction"] for i in instruments) # Build {serial: nc_path} for the clock-offset comparison figure. - # Prefer stage3 over stage2 (clock correction was applied in stage2 and - # is preserved in stage3, so either works for comparing alignment). + # Use stage1 (raw, UNtrimmed): stage2/3 are trimmed to the deployment + # window, which removes exactly the pre-deploy / post-recover data the + # ±window check needs to show the deployment/recovery temperature + # transient (the shared timing feature). Raw clocks also expose the real + # inter-instrument offsets before correction. (Note: stage1 times are in + # the raw instrument clock, so a very large offset could shift the + # transient out of the ±window; offsets are normally seconds-to-minutes.) + # Fall back to stage2/3 only if stage1 is absent. _clock_nc_paths: Dict[str, Path] = {} for _instr in instruments: _s = _instr["serial"] _itype = _instr["instr_type"] _base = proc_dir / _itype / f"{mooring_name}_{_s}" + _s1 = Path(str(_base) + "_stage1.nc") _s3 = Path(str(_base) + "_stage3.nc") _s2 = Path(str(_base) + "_stage2.nc") - if _s3.exists(): - _clock_nc_paths[_s] = _s3 + if _s1.exists(): + _clock_nc_paths[_s] = _s1 elif _s2.exists(): _clock_nc_paths[_s] = _s2 + elif _s3.exists(): + _clock_nc_paths[_s] = _s3 fig_clock_check_b64 = _make_clock_check_b64( _clock_nc_paths, deploy_dt, recover_dt ) diff --git a/oceanarray/report/_pdf.py b/oceanarray/report/_pdf.py index 2910eb3..5cbec30 100644 --- a/oceanarray/report/_pdf.py +++ b/oceanarray/report/_pdf.py @@ -42,6 +42,14 @@ height: auto; break-inside: avoid; } +/* A single