mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-06-28 01:52:06 +08:00
jotpluggler: store and load layouts (#36148)
* store and load layouts * torque controller layout * ignore missing yaml stubs for mypy
This commit is contained in:
+105
-25
@@ -20,17 +20,35 @@ class LayoutManager:
|
||||
self.tabs: dict = {0: {"name": "Tab 1", "panel_layout": initial_panel_layout}}
|
||||
self._next_tab_id = self.active_tab + 1
|
||||
|
||||
self._create_tab_themes()
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"tabs": {
|
||||
str(tab_id): {
|
||||
"name": tab_data["name"],
|
||||
"panel_layout": tab_data["panel_layout"].to_dict()
|
||||
}
|
||||
for tab_id, tab_data in self.tabs.items()
|
||||
}
|
||||
}
|
||||
|
||||
def _create_tab_themes(self):
|
||||
for tag, color in (("active_tab_theme", (37, 37, 38, 255)), ("inactive_tab_theme", (70, 70, 75, 255))):
|
||||
with dpg.theme(tag=tag):
|
||||
for cmp, target in ((dpg.mvChildWindow, dpg.mvThemeCol_ChildBg), (dpg.mvInputText, dpg.mvThemeCol_FrameBg), (dpg.mvImageButton, dpg.mvThemeCol_Button)):
|
||||
with dpg.theme_component(cmp):
|
||||
dpg.add_theme_color(target, color)
|
||||
with dpg.theme(tag="tab_bar_theme"):
|
||||
with dpg.theme_component(dpg.mvChildWindow):
|
||||
dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (51, 51, 55, 255))
|
||||
def clear_and_load_from_dict(self, data: dict):
|
||||
tab_ids_to_close = list(self.tabs.keys())
|
||||
for tab_id in tab_ids_to_close:
|
||||
self.close_tab(tab_id, force=True)
|
||||
|
||||
for tab_id_str, tab_data in data["tabs"].items():
|
||||
tab_id = int(tab_id_str)
|
||||
panel_layout = PanelLayoutManager.load_from_dict(
|
||||
tab_data["panel_layout"], self.data_manager, self.playback_manager,
|
||||
self.worker_manager, self.scale
|
||||
)
|
||||
self.tabs[tab_id] = {
|
||||
"name": tab_data["name"],
|
||||
"panel_layout": panel_layout
|
||||
}
|
||||
|
||||
self.active_tab = min(self.tabs.keys()) if self.tabs else 0
|
||||
self._next_tab_id = max(self.tabs.keys()) + 1 if self.tabs else 1
|
||||
|
||||
def create_ui(self, parent_tag: str):
|
||||
if dpg.does_item_exist(self.container_tag):
|
||||
@@ -52,7 +70,7 @@ class LayoutManager:
|
||||
|
||||
def _create_tab_ui(self, tab_id: int, tab_name: str):
|
||||
text_size = int(13 * self.scale)
|
||||
tab_width = int(120 * self.scale)
|
||||
tab_width = int(140 * self.scale)
|
||||
with dpg.child_window(width=tab_width, height=-1, border=False, no_scrollbar=True, tag=f"tab_window_{tab_id}", parent="tab_bar_group"):
|
||||
with dpg.group(horizontal=True, tag=f"tab_group_{tab_id}"):
|
||||
dpg.add_input_text(
|
||||
@@ -70,20 +88,21 @@ class LayoutManager:
|
||||
|
||||
def _create_tab_content(self):
|
||||
with dpg.child_window(tag=self.tab_content_tag, parent=self.container_tag, border=False, width=-1, height=-1, no_scrollbar=True, no_scroll_with_mouse=True):
|
||||
active_panel_layout = self.tabs[self.active_tab]["panel_layout"]
|
||||
active_panel_layout.create_ui()
|
||||
if self.active_tab in self.tabs:
|
||||
active_panel_layout = self.tabs[self.active_tab]["panel_layout"]
|
||||
active_panel_layout.create_ui()
|
||||
|
||||
def add_tab(self):
|
||||
new_panel_layout = PanelLayoutManager(self.data_manager, self.playback_manager, self.worker_manager, self.scale)
|
||||
new_tab = {"name": f"Tab {self._next_tab_id + 1}", "panel_layout": new_panel_layout}
|
||||
self.tabs[ self._next_tab_id] = new_tab
|
||||
self._create_tab_ui( self._next_tab_id, new_tab["name"])
|
||||
self.tabs[self._next_tab_id] = new_tab
|
||||
self._create_tab_ui(self._next_tab_id, new_tab["name"])
|
||||
dpg.move_item("add_tab_button", parent="tab_bar_group") # move plus button to end
|
||||
self.switch_tab( self._next_tab_id)
|
||||
self.switch_tab(self._next_tab_id)
|
||||
self._next_tab_id += 1
|
||||
|
||||
def close_tab(self, tab_id: int):
|
||||
if len(self.tabs) <= 1:
|
||||
def close_tab(self, tab_id: int, force = False):
|
||||
if len(self.tabs) <= 1 and not force:
|
||||
return # don't allow closing the last tab
|
||||
|
||||
tab_to_close = self.tabs[tab_id]
|
||||
@@ -94,7 +113,7 @@ class LayoutManager:
|
||||
dpg.delete_item(tag)
|
||||
del self.tabs[tab_id]
|
||||
|
||||
if self.active_tab == tab_id: # switch to another tab if we closed the active one
|
||||
if self.active_tab == tab_id and self.tabs: # switch to another tab if we closed the active one
|
||||
self.active_tab = next(iter(self.tabs.keys()))
|
||||
self._switch_tab_content()
|
||||
dpg.bind_item_theme(f"tab_window_{self.active_tab}", "active_tab_theme")
|
||||
@@ -134,6 +153,8 @@ class PanelLayoutManager:
|
||||
self.scale = scale
|
||||
self.active_panels: list = []
|
||||
self.parent_tag = "tab_content_area"
|
||||
self._queue_resize = False
|
||||
self._created_handler_tags: set[str] = set()
|
||||
|
||||
self.grip_size = int(GRIP_SIZE * self.scale)
|
||||
self.min_pane_size = int(MIN_PANE_SIZE * self.scale)
|
||||
@@ -141,19 +162,70 @@ class PanelLayoutManager:
|
||||
initial_panel = TimeSeriesPanel(data_manager, playback_manager, worker_manager)
|
||||
self.layout: dict = {"type": "panel", "panel": initial_panel}
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return self._layout_to_dict(self.layout)
|
||||
|
||||
def _layout_to_dict(self, layout: dict) -> dict:
|
||||
if layout["type"] == "panel":
|
||||
return {
|
||||
"type": "panel",
|
||||
"panel": layout["panel"].to_dict()
|
||||
}
|
||||
else: # split
|
||||
return {
|
||||
"type": "split",
|
||||
"orientation": layout["orientation"],
|
||||
"proportions": layout["proportions"],
|
||||
"children": [self._layout_to_dict(child) for child in layout["children"]]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def load_from_dict(cls, data: dict, data_manager, playback_manager, worker_manager, scale: float = 1.0):
|
||||
manager = cls(data_manager, playback_manager, worker_manager, scale)
|
||||
manager.layout = manager._dict_to_layout(data)
|
||||
return manager
|
||||
|
||||
def _dict_to_layout(self, data: dict) -> dict:
|
||||
if data["type"] == "panel":
|
||||
panel_data = data["panel"]
|
||||
if panel_data["type"] == "timeseries":
|
||||
panel = TimeSeriesPanel.load_from_dict(
|
||||
panel_data, self.data_manager, self.playback_manager, self.worker_manager
|
||||
)
|
||||
return {"type": "panel", "panel": panel}
|
||||
else:
|
||||
# Handle future panel types here or make a general mapping
|
||||
raise ValueError(f"Unknown panel type: {panel_data['type']}")
|
||||
else: # split
|
||||
return {
|
||||
"type": "split",
|
||||
"orientation": data["orientation"],
|
||||
"proportions": data["proportions"],
|
||||
"children": [self._dict_to_layout(child) for child in data["children"]]
|
||||
}
|
||||
|
||||
def create_ui(self):
|
||||
self.active_panels.clear()
|
||||
|
||||
if dpg.does_item_exist(self.parent_tag):
|
||||
dpg.delete_item(self.parent_tag, children_only=True)
|
||||
self._cleanup_all_handlers()
|
||||
|
||||
container_width, container_height = dpg.get_item_rect_size(self.parent_tag)
|
||||
if container_width == 0 and container_height == 0:
|
||||
self._queue_resize = True
|
||||
self._create_ui_recursive(self.layout, self.parent_tag, [], container_width, container_height)
|
||||
|
||||
def destroy_ui(self):
|
||||
self._cleanup_ui_recursive(self.layout, [])
|
||||
self._cleanup_all_handlers()
|
||||
self.active_panels.clear()
|
||||
|
||||
def _cleanup_all_handlers(self):
|
||||
for handler_tag in list(self._created_handler_tags):
|
||||
if dpg.does_item_exist(handler_tag):
|
||||
dpg.delete_item(handler_tag)
|
||||
self._created_handler_tags.clear()
|
||||
|
||||
def _create_ui_recursive(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int):
|
||||
if layout["type"] == "panel":
|
||||
self._create_panel_ui(layout, parent_tag, path, width, height)
|
||||
@@ -165,13 +237,14 @@ class PanelLayoutManager:
|
||||
panel = layout["panel"]
|
||||
self.active_panels.append(panel)
|
||||
text_size = int(13 * self.scale)
|
||||
bar_height = (text_size + 24) if width < int(279 * self.scale + 64) else (text_size + 8) # adjust height to allow for scrollbar
|
||||
bar_height = (text_size + 24) if width < int(329 * self.scale + 64) else (text_size + 8) # adjust height to allow for scrollbar
|
||||
|
||||
with dpg.child_window(parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True):
|
||||
with dpg.group(horizontal=True):
|
||||
with dpg.child_window(tag=panel_tag, width=-(text_size + 16), height=bar_height, horizontal_scrollbar=True, no_scroll_with_mouse=True, border=False):
|
||||
with dpg.group(horizontal=True):
|
||||
dpg.add_input_text(default_value=panel.title, width=int(100 * self.scale), callback=lambda s, v: setattr(panel, "title", v))
|
||||
# if you change the widths make sure to change the sum of widths (currently 329 * scale)
|
||||
dpg.add_input_text(default_value=panel.title, width=int(150 * self.scale), callback=lambda s, v: setattr(panel, "title", v))
|
||||
dpg.add_combo(items=["Time Series"], default_value="Time Series", width=int(100 * self.scale))
|
||||
dpg.add_button(label="Clear", callback=lambda: self.clear_panel(panel), width=int(40 * self.scale))
|
||||
dpg.add_image_button(texture_tag="split_h_texture", callback=lambda: self.split_panel(path, 0), width=text_size, height=text_size)
|
||||
@@ -280,11 +353,16 @@ class PanelLayoutManager:
|
||||
handler_tag = f"{self._path_to_tag(path, f'grip_{i}')}_handler"
|
||||
if dpg.does_item_exist(handler_tag):
|
||||
dpg.delete_item(handler_tag)
|
||||
self._created_handler_tags.discard(handler_tag)
|
||||
|
||||
for i, child in enumerate(layout["children"]):
|
||||
self._cleanup_ui_recursive(child, path + [i])
|
||||
|
||||
def update_all_panels(self):
|
||||
if self._queue_resize:
|
||||
if (size := dpg.get_item_rect_size(self.parent_tag)) != [0, 0]:
|
||||
self._queue_resize = False
|
||||
self._resize_splits_recursive(self.layout, [], *size)
|
||||
for panel in self.active_panels:
|
||||
panel.update()
|
||||
|
||||
@@ -308,7 +386,7 @@ class PanelLayoutManager:
|
||||
self._resize_splits_recursive(child_layout, child_path, child_width, child_height)
|
||||
else: # leaf node/panel - adjust bar height to allow for scrollbar
|
||||
panel_tag = self._path_to_tag(path, "panel")
|
||||
if width is not None and width < int(279 * self.scale + 64): # scaled widths of the elements in top bar + fixed 8 padding on left and right of each item
|
||||
if width is not None and width < int(329 * self.scale + 64): # scaled widths of the elements in top bar + fixed 8 padding on left and right of each item
|
||||
dpg.configure_item(panel_tag, height=(int(13 * self.scale) + 24))
|
||||
else:
|
||||
dpg.configure_item(panel_tag, height=(int(13 * self.scale) + 8))
|
||||
@@ -342,16 +420,18 @@ class PanelLayoutManager:
|
||||
|
||||
def _create_grip(self, parent_tag: str, path: list[int], grip_index: int, orientation: int):
|
||||
grip_tag = self._path_to_tag(path, f"grip_{grip_index}")
|
||||
handler_tag = f"{grip_tag}_handler"
|
||||
width, height = [(self.grip_size, -1), (-1, self.grip_size)][orientation]
|
||||
|
||||
with dpg.child_window(tag=grip_tag, parent=parent_tag, width=width, height=height, no_scrollbar=True, border=False):
|
||||
button_tag = dpg.add_button(label="", width=-1, height=-1)
|
||||
|
||||
with dpg.item_handler_registry(tag=f"{grip_tag}_handler"):
|
||||
with dpg.item_handler_registry(tag=handler_tag):
|
||||
user_data = (path, grip_index, orientation)
|
||||
dpg.add_item_active_handler(callback=self._on_grip_drag, user_data=user_data)
|
||||
dpg.add_item_deactivated_handler(callback=self._on_grip_end, user_data=user_data)
|
||||
dpg.bind_item_handler_registry(button_tag, f"{grip_tag}_handler")
|
||||
dpg.bind_item_handler_registry(button_tag, handler_tag)
|
||||
self._created_handler_tags.add(handler_tag)
|
||||
|
||||
def _on_grip_drag(self, sender, app_data, user_data):
|
||||
path, grip_index, orientation = user_data
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
tabs:
|
||||
'0':
|
||||
name: Lateral Plan Conformance
|
||||
panel_layout:
|
||||
type: split
|
||||
orientation: 1
|
||||
proportions:
|
||||
- 0.3333333333333333
|
||||
- 0.3333333333333333
|
||||
- 0.3333333333333333
|
||||
children:
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: desired vs actual
|
||||
series_paths:
|
||||
- controlsState/lateralControlState/torqueState/desiredLateralAccel
|
||||
- controlsState/lateralControlState/torqueState/actualLateralAccel
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: ff vs output
|
||||
series_paths:
|
||||
- controlsState/lateralControlState/torqueState/f
|
||||
- carState/steeringPressed
|
||||
- carControl/actuators/torque
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: vehicle speed
|
||||
series_paths:
|
||||
- carState/vEgo
|
||||
'1':
|
||||
name: Actuator Performance
|
||||
panel_layout:
|
||||
type: split
|
||||
orientation: 1
|
||||
proportions:
|
||||
- 0.3333333333333333
|
||||
- 0.3333333333333333
|
||||
- 0.3333333333333333
|
||||
children:
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: calc vs learned latAccelFactor
|
||||
series_paths:
|
||||
- liveTorqueParameters/latAccelFactorFiltered
|
||||
- liveTorqueParameters/latAccelFactorRaw
|
||||
- carParams/lateralTuning/torque/latAccelFactor
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: learned latAccelOffset
|
||||
series_paths:
|
||||
- liveTorqueParameters/latAccelOffsetRaw
|
||||
- liveTorqueParameters/latAccelOffsetFiltered
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: calc vs learned friction
|
||||
series_paths:
|
||||
- liveTorqueParameters/frictionCoefficientFiltered
|
||||
- liveTorqueParameters/frictionCoefficientRaw
|
||||
- carParams/lateralTuning/torque/friction
|
||||
'2':
|
||||
name: Vehicle Dynamics
|
||||
panel_layout:
|
||||
type: split
|
||||
orientation: 1
|
||||
proportions:
|
||||
- 0.3333333333333333
|
||||
- 0.3333333333333333
|
||||
- 0.3333333333333333
|
||||
children:
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: initial vs learned steerRatio
|
||||
series_paths:
|
||||
- carParams/steerRatio
|
||||
- liveParameters/steerRatio
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: initial vs learned tireStiffnessFactor
|
||||
series_paths:
|
||||
- carParams/tireStiffnessFactor
|
||||
- liveParameters/stiffnessFactor
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: live steering angle offsets
|
||||
series_paths:
|
||||
- liveParameters/angleOffsetDeg
|
||||
- liveParameters/angleOffsetAverageDeg
|
||||
'3':
|
||||
name: Controller PIF Terms
|
||||
panel_layout:
|
||||
type: split
|
||||
orientation: 1
|
||||
proportions:
|
||||
- 0.3333333333333333
|
||||
- 0.3333333333333333
|
||||
- 0.3333333333333333
|
||||
children:
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: ff vs output
|
||||
series_paths:
|
||||
- carControl/actuators/torque
|
||||
- controlsState/lateralControlState/torqueState/f
|
||||
- carState/steeringPressed
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: PIF terms
|
||||
series_paths:
|
||||
- controlsState/lateralControlState/torqueState/f
|
||||
- controlsState/lateralControlState/torqueState/p
|
||||
- controlsState/lateralControlState/torqueState/i
|
||||
- type: panel
|
||||
panel:
|
||||
type: timeseries
|
||||
title: road roll angle
|
||||
series_paths:
|
||||
- liveParameters/roll
|
||||
@@ -7,6 +7,8 @@ import dearpygui.dearpygui as dpg
|
||||
import multiprocessing
|
||||
import uuid
|
||||
import signal
|
||||
import yaml # type: ignore
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.tools.jotpluggler.data import DataManager
|
||||
from openpilot.tools.jotpluggler.datatree import DataTree
|
||||
@@ -131,17 +133,27 @@ class MainController:
|
||||
self.data_manager.add_observer(self.on_data_loaded)
|
||||
|
||||
def _create_global_themes(self):
|
||||
with dpg.theme(tag="global_line_theme"):
|
||||
with dpg.theme(tag="line_theme"):
|
||||
with dpg.theme_component(dpg.mvLineSeries):
|
||||
scaled_thickness = max(1.0, self.scale)
|
||||
dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, scaled_thickness, category=dpg.mvThemeCat_Plots)
|
||||
|
||||
with dpg.theme(tag="global_timeline_theme"):
|
||||
with dpg.theme(tag="timeline_theme"):
|
||||
with dpg.theme_component(dpg.mvInfLineSeries):
|
||||
scaled_thickness = max(1.0, self.scale)
|
||||
dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, scaled_thickness, category=dpg.mvThemeCat_Plots)
|
||||
dpg.add_theme_color(dpg.mvPlotCol_Line, (255, 0, 0, 128), category=dpg.mvThemeCat_Plots)
|
||||
|
||||
for tag, color in (("active_tab_theme", (37, 37, 38, 255)), ("inactive_tab_theme", (70, 70, 75, 255))):
|
||||
with dpg.theme(tag=tag):
|
||||
for cmp, target in ((dpg.mvChildWindow, dpg.mvThemeCol_ChildBg), (dpg.mvInputText, dpg.mvThemeCol_FrameBg), (dpg.mvImageButton, dpg.mvThemeCol_Button)):
|
||||
with dpg.theme_component(cmp):
|
||||
dpg.add_theme_color(target, color)
|
||||
|
||||
with dpg.theme(tag="tab_bar_theme"):
|
||||
with dpg.theme_component(dpg.mvChildWindow):
|
||||
dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (51, 51, 55, 255))
|
||||
|
||||
def on_data_loaded(self, data: dict):
|
||||
duration = data.get('duration', 0.0)
|
||||
self.playback_manager.set_route_duration(duration)
|
||||
@@ -165,6 +177,56 @@ class MainController:
|
||||
|
||||
dpg.configure_item("timeline_slider", max_value=duration)
|
||||
|
||||
def save_layout_to_yaml(self, filepath: str):
|
||||
layout_dict = self.layout_manager.to_dict()
|
||||
with open(filepath, 'w') as f:
|
||||
yaml.dump(layout_dict, f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
def load_layout_from_yaml(self, filepath: str):
|
||||
with open(filepath) as f:
|
||||
layout_dict = yaml.safe_load(f)
|
||||
self.layout_manager.clear_and_load_from_dict(layout_dict)
|
||||
self.layout_manager.create_ui("main_plot_area")
|
||||
|
||||
def save_layout_dialog(self):
|
||||
if dpg.does_item_exist("save_layout_dialog"):
|
||||
dpg.delete_item("save_layout_dialog")
|
||||
with dpg.file_dialog(
|
||||
directory_selector=False, show=True, callback=self._save_layout_callback,
|
||||
tag="save_layout_dialog", width=int(700 * self.scale), height=int(400 * self.scale),
|
||||
default_filename="layout", default_path="layouts"
|
||||
):
|
||||
dpg.add_file_extension(".yaml")
|
||||
|
||||
def load_layout_dialog(self):
|
||||
if dpg.does_item_exist("load_layout_dialog"):
|
||||
dpg.delete_item("load_layout_dialog")
|
||||
with dpg.file_dialog(
|
||||
directory_selector=False, show=True, callback=self._load_layout_callback,
|
||||
tag="load_layout_dialog", width=int(700 * self.scale), height=int(400 * self.scale), default_path="layouts"
|
||||
):
|
||||
dpg.add_file_extension(".yaml")
|
||||
|
||||
def _save_layout_callback(self, sender, app_data):
|
||||
filepath = app_data['file_path_name']
|
||||
try:
|
||||
self.save_layout_to_yaml(filepath)
|
||||
dpg.set_value("load_status", f"Layout saved to {os.path.basename(filepath)}")
|
||||
except Exception:
|
||||
dpg.set_value("load_status", "Error saving layout")
|
||||
cloudlog.exception(f"Error saving layout to {filepath}")
|
||||
dpg.delete_item("save_layout_dialog")
|
||||
|
||||
def _load_layout_callback(self, sender, app_data):
|
||||
filepath = app_data['file_path_name']
|
||||
try:
|
||||
self.load_layout_from_yaml(filepath)
|
||||
dpg.set_value("load_status", f"Layout loaded from {os.path.basename(filepath)}")
|
||||
except Exception:
|
||||
dpg.set_value("load_status", "Error loading layout")
|
||||
cloudlog.exception(f"Error loading layout from {filepath}:")
|
||||
dpg.delete_item("load_layout_dialog")
|
||||
|
||||
def setup_ui(self):
|
||||
with dpg.texture_registry():
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
@@ -175,21 +237,30 @@ class MainController:
|
||||
with dpg.window(tag="Primary Window"):
|
||||
with dpg.group(horizontal=True):
|
||||
# Left panel - Data tree
|
||||
with dpg.child_window(label="Sidebar", width=300 * self.scale, tag="sidebar_window", border=True, resizable_x=True):
|
||||
with dpg.child_window(label="Sidebar", width=int(300 * self.scale), tag="sidebar_window", border=True, resizable_x=True):
|
||||
with dpg.group(horizontal=True):
|
||||
dpg.add_input_text(tag="route_input", width=-75 * self.scale, hint="Enter route name...")
|
||||
dpg.add_input_text(tag="route_input", width=int(-75 * self.scale), hint="Enter route name...")
|
||||
dpg.add_button(label="Load", callback=self.load_route, tag="load_button", width=-1)
|
||||
dpg.add_text("Ready to load route", tag="load_status")
|
||||
dpg.add_separator()
|
||||
|
||||
with dpg.table(header_row=False, policy=dpg.mvTable_SizingStretchProp):
|
||||
dpg.add_table_column(init_width_or_weight=0.5)
|
||||
dpg.add_table_column(init_width_or_weight=0.5)
|
||||
with dpg.table_row():
|
||||
dpg.add_button(label="Save Layout", callback=self.save_layout_dialog, width=-1)
|
||||
dpg.add_button(label="Load Layout", callback=self.load_layout_dialog, width=-1)
|
||||
dpg.add_separator()
|
||||
|
||||
self.data_tree.create_ui("sidebar_window")
|
||||
|
||||
# Right panel - Plots and timeline
|
||||
with dpg.group(tag="right_panel"):
|
||||
with dpg.child_window(label="Plot Window", border=True, height=-(32 + 13 * self.scale), tag="main_plot_area"):
|
||||
with dpg.child_window(label="Plot Window", border=True, height=int(-(32 + 13 * self.scale)), tag="main_plot_area"):
|
||||
self.layout_manager.create_ui("main_plot_area")
|
||||
|
||||
with dpg.child_window(label="Timeline", border=True):
|
||||
with dpg.table(header_row=False, borders_innerH=False, borders_innerV=False, borders_outerH=False, borders_outerV=False):
|
||||
with dpg.table(header_row=False):
|
||||
btn_size = int(13 * self.scale)
|
||||
dpg.add_table_column(width_fixed=True, init_width_or_weight=(btn_size + 8)) # Play button
|
||||
dpg.add_table_column(width_stretch=True) # Timeline slider
|
||||
|
||||
@@ -33,6 +33,15 @@ class ViewPanel(ABC):
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def to_dict(self) -> dict:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def load_from_dict(cls, data: dict, data_manager, playback_manager, worker_manager):
|
||||
pass
|
||||
|
||||
|
||||
class TimeSeriesPanel(ViewPanel):
|
||||
def __init__(self, data_manager, playback_manager, worker_manager, panel_id: str | None = None):
|
||||
@@ -55,6 +64,20 @@ class TimeSeriesPanel(ViewPanel):
|
||||
self._queued_x_sync: tuple | None = None
|
||||
self._queued_reallow_x_zoom = False
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"type": "timeseries",
|
||||
"title": self.title,
|
||||
"series_paths": list(self._series_data.keys())
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def load_from_dict(cls, data: dict, data_manager, playback_manager, worker_manager):
|
||||
panel = cls(data_manager, playback_manager, worker_manager)
|
||||
panel.title = data.get("title", "Time Series Plot")
|
||||
panel._series_data = {path: (np.array([]), np.array([])) for path in data.get("series_paths", [])}
|
||||
return panel
|
||||
|
||||
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)
|
||||
@@ -63,7 +86,7 @@ class TimeSeriesPanel(ViewPanel):
|
||||
dpg.add_plot_axis(dpg.mvXAxis, no_label=True, tag=self.x_axis_tag)
|
||||
dpg.add_plot_axis(dpg.mvYAxis, no_label=True, tag=self.y_axis_tag)
|
||||
timeline_series_tag = dpg.add_inf_line_series(x=[0], label="Timeline", parent=self.y_axis_tag, tag=self.timeline_indicator_tag)
|
||||
dpg.bind_item_theme(timeline_series_tag, "global_timeline_theme")
|
||||
dpg.bind_item_theme(timeline_series_tag, "timeline_theme")
|
||||
|
||||
self._new_data = True
|
||||
self._ui_created = True
|
||||
@@ -199,7 +222,7 @@ class TimeSeriesPanel(ViewPanel):
|
||||
dpg.set_value(series_tag, (time_array, value_array.astype(float)))
|
||||
else:
|
||||
line_series_tag = dpg.add_line_series(x=time_array, y=value_array.astype(float), label=series_path, parent=self.y_axis_tag, tag=series_tag)
|
||||
dpg.bind_item_theme(line_series_tag, "global_line_theme")
|
||||
dpg.bind_item_theme(line_series_tag, "line_theme")
|
||||
dpg.fit_axis_data(self.x_axis_tag)
|
||||
dpg.fit_axis_data(self.y_axis_tag)
|
||||
plot_duration = dpg.get_axis_limits(self.x_axis_tag)[1] - dpg.get_axis_limits(self.x_axis_tag)[0]
|
||||
|
||||
Reference in New Issue
Block a user