Skip to content

Commit e97e41d

Browse files
committed
fix(batch): echograms use time-proportional x-axis — gaps shown as blank regions
1 parent 40e198f commit e97e41d

1 file changed

Lines changed: 44 additions & 47 deletions

File tree

scripts/batch_processing/run_combine_daily.py

Lines changed: 44 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -606,66 +606,56 @@ def combine_one_day(
606606
# ---------------------------------------------------------------------------
607607

608608
def _build_hourly_ticks(
609-
ping_time: np.ndarray,
610-
n_pings: int,
611-
) -> tuple[list[int], list[str], list[int], list[str]]:
612-
"""Build hourly tick positions for a single-day echogram."""
613-
major_ticks: list[int] = []
614-
major_labels: list[str] = []
615-
616-
pt_hours = (
617-
(ping_time - ping_time[0]).astype("timedelta64[s]").astype(float) / 3600
618-
)
619-
620-
for h in range(0, 25):
621-
after = pt_hours >= h
622-
if after.any():
623-
idx = int(np.argmax(after))
624-
if idx < n_pings:
625-
major_ticks.append(idx)
626-
# Label as HH:00
627-
ts = ping_time[idx]
628-
hh = int((ts - ts.astype("datetime64[D]")).astype("timedelta64[h]").astype(int))
629-
major_labels.append(f"{hh:02d}:00")
630-
631-
return major_ticks, major_labels, [], []
609+
x_hours: np.ndarray,
610+
) -> tuple[list[float], list[str]]:
611+
"""Build hourly tick positions for a time-proportional x-axis (hours since midnight)."""
612+
h_min = int(np.floor(x_hours[0]))
613+
h_max = int(np.ceil(x_hours[-1]))
614+
ticks = [float(h) for h in range(h_min, h_max + 1)]
615+
labels = [f"{int(h) % 24:02d}:00" for h in ticks]
616+
return ticks, labels
632617

633618

634619
def _draw_pulse_axis(
635620
ax_pulse: plt.Axes,
636621
pulse_mode: np.ndarray,
637-
n_pings: int,
622+
x_hours: np.ndarray,
638623
) -> None:
639-
"""Draw pulse-mode colour bar (blue=Long, orange=Short)."""
624+
"""Draw pulse-mode colour bar (blue=Long, orange=Short) using time-proportional x."""
640625
from matplotlib.patches import Rectangle
641626

642627
colors = {0: "#2196F3", 1: "#FF9800"}
643628
labels_map = {0: "Long pulse", 1: "Short pulse"}
644629

630+
x_min, x_max = x_hours[0], x_hours[-1]
631+
total_span = x_max - x_min
632+
645633
changes = np.where(np.diff(pulse_mode))[0] + 1
646634
starts = np.concatenate([[0], changes])
647-
ends = np.concatenate([changes, [n_pings]])
635+
ends = np.concatenate([changes, [len(pulse_mode)]])
648636

649637
drawn_labels: set[int] = set()
650638
for s, e in zip(starts, ends):
651639
mode = int(pulse_mode[s])
640+
x0 = x_hours[s]
641+
x1 = x_hours[min(e, len(x_hours)) - 1]
642+
w = x1 - x0
652643
lbl = labels_map[mode] if mode not in drawn_labels else None
653644
rect = Rectangle(
654-
(s, 0), e - s, 1,
645+
(x0, 0), w, 1,
655646
facecolor=colors[mode], alpha=0.85, edgecolor="none",
656647
label=lbl,
657648
)
658649
ax_pulse.add_patch(rect)
659-
seg_width = e - s
660-
if seg_width > n_pings * 0.008:
650+
if w > total_span * 0.008:
661651
ax_pulse.text(
662-
s + seg_width / 2, 0.5, labels_map[mode],
652+
x0 + w / 2, 0.5, labels_map[mode],
663653
ha="center", va="center", fontsize=7,
664654
fontweight="bold", color="white",
665655
)
666656
drawn_labels.add(mode)
667657

668-
ax_pulse.set_xlim(0, n_pings)
658+
ax_pulse.set_xlim(x_min, x_max)
669659
ax_pulse.set_ylim(0, 1)
670660
ax_pulse.set_yticks([])
671661
ax_pulse.set_ylabel("Pulse", fontsize=9, rotation=0, labelpad=30, va="center")
@@ -684,7 +674,11 @@ def render_echogram(
684674
cmap,
685675
output_dir: Path,
686676
) -> Path | None:
687-
"""Render one echogram from a combined daily dataset."""
677+
"""Render one echogram from a combined daily dataset.
678+
679+
Uses time-proportional x-axis so gaps appear as blank regions
680+
instead of being collapsed.
681+
"""
688682
# Select frequency channel
689683
if "channel" in ds.coords:
690684
chans = [str(c) for c in ds.channel.values]
@@ -730,23 +724,27 @@ def render_echogram(
730724
depth_plot = depth_vals[depth_mask]
731725
sv_data = sv_raw[:, :len(depth_plot)]
732726

733-
# Remove fully-NaN pings
727+
# Count valid (non-NaN) pings for the title
734728
valid_pings = ~np.isnan(sv_data).all(axis=1)
735-
sv_data = sv_data[valid_pings]
736-
ping_time = ping_time[valid_pings]
729+
n_valid = int(valid_pings.sum())
730+
if n_valid == 0:
731+
return None
737732

738733
n_pings = len(ping_time)
739-
if n_pings == 0:
740-
return None
734+
735+
# Time-proportional x-axis: hours since midnight UTC
736+
day_start = np.datetime64(day, "D")
737+
x_hours = (ping_time - day_start).astype("timedelta64[s]").astype(float) / 3600.0
741738

742739
pulse_mode = None
743740
if "pulse_mode" in ds:
744-
pulse_mode = ds["pulse_mode"].values[valid_pings]
741+
pulse_mode = ds["pulse_mode"].values
745742

746743
has_pulse = pulse_mode is not None
747744

748-
# Figure sizing
749-
width = min(30, max(12, n_pings * 0.003))
745+
# Figure sizing — proportional to time span (hours)
746+
time_span = x_hours[-1] - x_hours[0]
747+
width = min(30, max(12, time_span * 1.2))
750748

751749
if has_pulse:
752750
from matplotlib.gridspec import GridSpec
@@ -765,10 +763,9 @@ def render_echogram(
765763
fig, ax = plt.subplots(figsize=(width, 6))
766764
cax = None
767765

768-
x = np.arange(n_pings)
769766
vmin, vmax = (SV_VMIN, SV_VMAX) if data_var == "Sv" else (0, None)
770767
im = ax.pcolormesh(
771-
x, depth_plot, sv_data.T,
768+
x_hours, depth_plot, sv_data.T,
772769
shading="auto", cmap=cmap, vmin=vmin, vmax=vmax, rasterized=True,
773770
)
774771
ax.invert_yaxis()
@@ -777,21 +774,21 @@ def render_echogram(
777774
product_label = {"sv": "Sv", "denoised": "Denoised Sv", "mvbs": "MVBS", "nasc": "NASC"}
778775
ax.set_title(
779776
f"{day}{product_label.get(product, product)} {freq_label} (combined)\n"
780-
f"{n_pings} pings | {cmap_name}",
777+
f"{n_valid} pings | {cmap_name}",
781778
fontsize=12, fontweight="bold",
782779
)
783780

784-
# Time ticks
785-
major_ticks, major_labels, _, _ = _build_hourly_ticks(ping_time, n_pings)
781+
# Time ticks — proportional hourly
782+
major_ticks, major_labels = _build_hourly_ticks(x_hours)
786783
tick_ax = ax_pulse if has_pulse else ax
787784
tick_ax.set_xticks(major_ticks)
788785
tick_ax.set_xticklabels(major_labels, rotation=45, ha="right", fontsize=9)
789786
tick_ax.set_xlabel("Time (UTC)", fontsize=11)
790-
ax.set_xlim(0, n_pings)
787+
ax.set_xlim(x_hours[0] - 0.1, x_hours[-1] + 0.1)
791788

792789
if has_pulse:
793790
ax.tick_params(axis="x", labelbottom=False, which="both")
794-
_draw_pulse_axis(ax_pulse, pulse_mode, n_pings)
791+
_draw_pulse_axis(ax_pulse, pulse_mode, x_hours)
795792
cbar = fig.colorbar(im, cax=cax)
796793
else:
797794
cbar = fig.colorbar(im, ax=ax, fraction=0.015, pad=0.01)

0 commit comments

Comments
 (0)