@@ -606,66 +606,56 @@ def combine_one_day(
606606# ---------------------------------------------------------------------------
607607
608608def _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
634619def _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