mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-10 11:52:10 +08:00
jotpluggler: sync x axes and autofit y axis (#36143)
* sync x axes of all timeseries plots * always autofit y-axis * fix typing
This commit is contained in:
@@ -65,6 +65,10 @@ class PlaybackManager:
|
||||
self.current_time_s = 0.0
|
||||
self.duration_s = 0.0
|
||||
|
||||
self.x_axis_bounds = (0.0, 0.0) # (min_time, max_time)
|
||||
self.x_axis_observers = [] # callbacks for x-axis changes
|
||||
self._updating_x_axis = False
|
||||
|
||||
def set_route_duration(self, duration: float):
|
||||
self.duration_s = duration
|
||||
self.seek(min(self.current_time_s, duration))
|
||||
@@ -87,6 +91,33 @@ class PlaybackManager:
|
||||
dpg.configure_item("play_pause_button", texture_tag="play_texture")
|
||||
return self.current_time_s
|
||||
|
||||
def set_x_axis_bounds(self, min_time: float, max_time: float, source_panel=None):
|
||||
if self._updating_x_axis:
|
||||
return
|
||||
|
||||
new_bounds = (min_time, max_time)
|
||||
if new_bounds == self.x_axis_bounds:
|
||||
return
|
||||
|
||||
self.x_axis_bounds = new_bounds
|
||||
self._updating_x_axis = True # prevent recursive updates
|
||||
|
||||
try:
|
||||
for callback in self.x_axis_observers:
|
||||
try:
|
||||
callback(min_time, max_time, source_panel)
|
||||
except Exception as e:
|
||||
print(f"Error in x-axis sync callback: {e}")
|
||||
finally:
|
||||
self._updating_x_axis = False
|
||||
|
||||
def add_x_axis_observer(self, callback):
|
||||
if callback not in self.x_axis_observers:
|
||||
self.x_axis_observers.append(callback)
|
||||
|
||||
def remove_x_axis_observer(self, callback):
|
||||
if callback in self.x_axis_observers:
|
||||
self.x_axis_observers.remove(callback)
|
||||
|
||||
class MainController:
|
||||
def __init__(self, scale: float = 1.0):
|
||||
|
||||
@@ -51,9 +51,11 @@ class TimeSeriesPanel(ViewPanel):
|
||||
self._update_lock = threading.RLock()
|
||||
self._results_deque: deque[tuple[str, list, list]] = deque()
|
||||
self._new_data = False
|
||||
self._last_x_limits = (0.0, 0.0)
|
||||
|
||||
def create_ui(self, parent_tag: str):
|
||||
self.data_manager.add_observer(self.on_data_loaded)
|
||||
self.playback_manager.add_x_axis_observer(self._on_x_axis_sync)
|
||||
with dpg.plot(height=-1, width=-1, tag=self.plot_tag, parent=parent_tag, drop_callback=self._on_series_drop, payload_type="TIMESERIES_PAYLOAD"):
|
||||
dpg.add_plot_legend()
|
||||
dpg.add_plot_axis(dpg.mvXAxis, no_label=True, tag=self.x_axis_tag)
|
||||
@@ -70,8 +72,20 @@ class TimeSeriesPanel(ViewPanel):
|
||||
if not self._ui_created:
|
||||
return
|
||||
|
||||
current_limits = dpg.get_axis_limits(self.x_axis_tag)
|
||||
# downsample if plot zoom changed significantly
|
||||
plot_duration = current_limits[1] - current_limits[0]
|
||||
if plot_duration > self._last_plot_duration * 2 or plot_duration < self._last_plot_duration * 0.5:
|
||||
self._downsample_all_series(plot_duration)
|
||||
# sync x-axis if changed by user
|
||||
if self._last_x_limits != current_limits:
|
||||
self.playback_manager.set_x_axis_bounds(current_limits[0], current_limits[1], source_panel=self)
|
||||
self._last_x_limits = current_limits
|
||||
self._fit_y_axis(current_limits[0], current_limits[1])
|
||||
|
||||
if self._new_data: # handle new data in main thread
|
||||
self._new_data = False
|
||||
dpg.set_axis_limits_constraints(self.x_axis_tag, -10, (self.playback_manager.duration_s + 10))
|
||||
for series_path in list(self._series_data.keys()):
|
||||
self.add_series(series_path, update=True)
|
||||
|
||||
@@ -96,10 +110,50 @@ class TimeSeriesPanel(ViewPanel):
|
||||
if dpg.does_item_exist(series_tag):
|
||||
dpg.configure_item(series_tag, label=f"{series_path}: {formatted_value}")
|
||||
|
||||
# downsample if plot zoom changed significantly
|
||||
plot_duration = dpg.get_axis_limits(self.x_axis_tag)[1] - dpg.get_axis_limits(self.x_axis_tag)[0]
|
||||
if plot_duration > self._last_plot_duration * 2 or plot_duration < self._last_plot_duration * 0.5:
|
||||
self._downsample_all_series(plot_duration)
|
||||
def _on_x_axis_sync(self, min_time: float, max_time: float, source_panel):
|
||||
with self._update_lock:
|
||||
if source_panel == self or not self._ui_created:
|
||||
return
|
||||
dpg.set_axis_limits(self.x_axis_tag, min_time, max_time)
|
||||
dpg.render_dearpygui_frame()
|
||||
dpg.set_axis_limits_auto(self.x_axis_tag)
|
||||
self._last_x_limits = (min_time, max_time)
|
||||
self._fit_y_axis(min_time, max_time)
|
||||
|
||||
def _fit_y_axis(self, x_min: float, x_max: float):
|
||||
if not self._series_data:
|
||||
dpg.set_axis_limits(self.y_axis_tag, -1, 1)
|
||||
return
|
||||
|
||||
global_min = float('inf')
|
||||
global_max = float('-inf')
|
||||
found_data = False
|
||||
|
||||
for time_array, value_array in self._series_data.values():
|
||||
if len(time_array) == 0:
|
||||
continue
|
||||
start_idx, end_idx = np.searchsorted(time_array, [x_min, x_max])
|
||||
end_idx = min(end_idx, len(time_array) - 1)
|
||||
if start_idx <= end_idx:
|
||||
y_slice = value_array[start_idx:end_idx + 1]
|
||||
series_min, series_max = np.min(y_slice), np.max(y_slice)
|
||||
global_min = min(global_min, series_min)
|
||||
global_max = max(global_max, series_max)
|
||||
found_data = True
|
||||
|
||||
if not found_data:
|
||||
dpg.set_axis_limits(self.y_axis_tag, -1, 1)
|
||||
return
|
||||
|
||||
if global_min == global_max:
|
||||
padding = max(abs(global_min) * 0.1, 1.0)
|
||||
y_min, y_max = global_min - padding, global_max + padding
|
||||
else:
|
||||
range_size = global_max - global_min
|
||||
padding = range_size * 0.1
|
||||
y_min, y_max = global_min - padding, global_max + padding
|
||||
|
||||
dpg.set_axis_limits(self.y_axis_tag, y_min, y_max)
|
||||
|
||||
def _downsample_all_series(self, plot_duration):
|
||||
plot_width = dpg.get_item_rect_size(self.plot_tag)[0]
|
||||
@@ -145,6 +199,7 @@ class TimeSeriesPanel(ViewPanel):
|
||||
def destroy_ui(self):
|
||||
with self._update_lock:
|
||||
self.data_manager.remove_observer(self.on_data_loaded)
|
||||
self.playback_manager.remove_x_axis_observer(self._on_x_axis_sync)
|
||||
if dpg.does_item_exist(self.plot_tag):
|
||||
dpg.delete_item(self.plot_tag)
|
||||
self._ui_created = False
|
||||
|
||||
Reference in New Issue
Block a user