|
|
@ -51,9 +51,11 @@ class TimeSeriesPanel(ViewPanel): |
|
|
|
self._update_lock = threading.RLock() |
|
|
|
self._update_lock = threading.RLock() |
|
|
|
self._results_deque: deque[tuple[str, list, list]] = deque() |
|
|
|
self._results_deque: deque[tuple[str, list, list]] = deque() |
|
|
|
self._new_data = False |
|
|
|
self._new_data = False |
|
|
|
|
|
|
|
self._last_x_limits = (0.0, 0.0) |
|
|
|
|
|
|
|
|
|
|
|
def create_ui(self, parent_tag: str): |
|
|
|
def create_ui(self, parent_tag: str): |
|
|
|
self.data_manager.add_observer(self.on_data_loaded) |
|
|
|
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"): |
|
|
|
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_legend() |
|
|
|
dpg.add_plot_axis(dpg.mvXAxis, no_label=True, tag=self.x_axis_tag) |
|
|
|
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: |
|
|
|
if not self._ui_created: |
|
|
|
return |
|
|
|
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 |
|
|
|
if self._new_data: # handle new data in main thread |
|
|
|
self._new_data = False |
|
|
|
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()): |
|
|
|
for series_path in list(self._series_data.keys()): |
|
|
|
self.add_series(series_path, update=True) |
|
|
|
self.add_series(series_path, update=True) |
|
|
|
|
|
|
|
|
|
|
@ -96,10 +110,50 @@ class TimeSeriesPanel(ViewPanel): |
|
|
|
if dpg.does_item_exist(series_tag): |
|
|
|
if dpg.does_item_exist(series_tag): |
|
|
|
dpg.configure_item(series_tag, label=f"{series_path}: {formatted_value}") |
|
|
|
dpg.configure_item(series_tag, label=f"{series_path}: {formatted_value}") |
|
|
|
|
|
|
|
|
|
|
|
# downsample if plot zoom changed significantly |
|
|
|
def _on_x_axis_sync(self, min_time: float, max_time: float, source_panel): |
|
|
|
plot_duration = dpg.get_axis_limits(self.x_axis_tag)[1] - dpg.get_axis_limits(self.x_axis_tag)[0] |
|
|
|
with self._update_lock: |
|
|
|
if plot_duration > self._last_plot_duration * 2 or plot_duration < self._last_plot_duration * 0.5: |
|
|
|
if source_panel == self or not self._ui_created: |
|
|
|
self._downsample_all_series(plot_duration) |
|
|
|
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): |
|
|
|
def _downsample_all_series(self, plot_duration): |
|
|
|
plot_width = dpg.get_item_rect_size(self.plot_tag)[0] |
|
|
|
plot_width = dpg.get_item_rect_size(self.plot_tag)[0] |
|
|
@ -145,6 +199,7 @@ class TimeSeriesPanel(ViewPanel): |
|
|
|
def destroy_ui(self): |
|
|
|
def destroy_ui(self): |
|
|
|
with self._update_lock: |
|
|
|
with self._update_lock: |
|
|
|
self.data_manager.remove_observer(self.on_data_loaded) |
|
|
|
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): |
|
|
|
if dpg.does_item_exist(self.plot_tag): |
|
|
|
dpg.delete_item(self.plot_tag) |
|
|
|
dpg.delete_item(self.plot_tag) |
|
|
|
self._ui_created = False |
|
|
|
self._ui_created = False |
|
|
|