|  |  |  | @ -2,6 +2,9 @@ import dearpygui.dearpygui as dpg | 
			
		
	
		
			
				
					|  |  |  |  | from openpilot.tools.jotpluggler.data import DataManager | 
			
		
	
		
			
				
					|  |  |  |  | from openpilot.tools.jotpluggler.views import TimeSeriesPanel | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | GRIP_SIZE = 4 | 
			
		
	
		
			
				
					|  |  |  |  | MIN_PANE_SIZE = 60 | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | class PlotLayoutManager: | 
			
		
	
		
			
				
					|  |  |  |  |   def __init__(self, data_manager: DataManager, playback_manager, scale: float = 1.0): | 
			
		
	
	
		
			
				
					|  |  |  | @ -9,10 +12,9 @@ class PlotLayoutManager: | 
			
		
	
		
			
				
					|  |  |  |  |     self.playback_manager = playback_manager | 
			
		
	
		
			
				
					|  |  |  |  |     self.scale = scale | 
			
		
	
		
			
				
					|  |  |  |  |     self.container_tag = "plot_layout_container" | 
			
		
	
		
			
				
					|  |  |  |  |     self.active_panels = [] | 
			
		
	
		
			
				
					|  |  |  |  |     self.active_panels: list = [] | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     initial_panel = TimeSeriesPanel(data_manager, playback_manager) | 
			
		
	
		
			
				
					|  |  |  |  |     self.active_panels.append(initial_panel) | 
			
		
	
		
			
				
					|  |  |  |  |     self.layout = {"type": "panel", "panel": initial_panel} | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def create_ui(self, parent_tag: str): | 
			
		
	
	
		
			
				
					|  |  |  | @ -23,33 +25,57 @@ class PlotLayoutManager: | 
			
		
	
		
			
				
					|  |  |  |  |       container_width, container_height = dpg.get_item_rect_size(self.container_tag) | 
			
		
	
		
			
				
					|  |  |  |  |       self._create_ui_recursive(self.layout, self.container_tag, [], container_width, container_height) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def on_viewport_resize(self): | 
			
		
	
		
			
				
					|  |  |  |  |     self._resize_splits_recursive(self.layout, []) | 
			
		
	
		
			
				
					|  |  |  |  |   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) | 
			
		
	
		
			
				
					|  |  |  |  |     else: | 
			
		
	
		
			
				
					|  |  |  |  |       self._create_split_ui(layout, parent_tag, path, width, height) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def split_panel(self, panel_path: list[int], orientation: str): | 
			
		
	
		
			
				
					|  |  |  |  |     current_layout = self._get_layout_at_path(panel_path) | 
			
		
	
		
			
				
					|  |  |  |  |     existing_panel = current_layout["panel"] | 
			
		
	
		
			
				
					|  |  |  |  |     new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager) | 
			
		
	
		
			
				
					|  |  |  |  |     self.active_panels.append(new_panel) | 
			
		
	
		
			
				
					|  |  |  |  |   def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int]): | 
			
		
	
		
			
				
					|  |  |  |  |     panel_tag = self._path_to_tag(path, "panel") | 
			
		
	
		
			
				
					|  |  |  |  |     panel = layout["panel"] | 
			
		
	
		
			
				
					|  |  |  |  |     self.active_panels.append(panel) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     parent, child_index = self._get_parent_and_index(panel_path) | 
			
		
	
		
			
				
					|  |  |  |  |     with dpg.child_window(tag=panel_tag, parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True): | 
			
		
	
		
			
				
					|  |  |  |  |       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)) | 
			
		
	
		
			
				
					|  |  |  |  |         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_button(label="Delete", callback=lambda: self.delete_panel(path), width=int(40 * self.scale)) | 
			
		
	
		
			
				
					|  |  |  |  |         dpg.add_button(label="Split H", callback=lambda: self.split_panel(path, 0), width=int(40 * self.scale)) | 
			
		
	
		
			
				
					|  |  |  |  |         dpg.add_button(label="Split V", callback=lambda: self.split_panel(path, 1), width=int(40 * self.scale)) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     if parent is None:  # Root split | 
			
		
	
		
			
				
					|  |  |  |  |       self.layout = { | 
			
		
	
		
			
				
					|  |  |  |  |         "type": "split", | 
			
		
	
		
			
				
					|  |  |  |  |         "orientation": orientation, | 
			
		
	
		
			
				
					|  |  |  |  |         "children": [{"type": "panel", "panel": existing_panel}, {"type": "panel", "panel": new_panel}], | 
			
		
	
		
			
				
					|  |  |  |  |         "proportions": [0.5, 0.5], | 
			
		
	
		
			
				
					|  |  |  |  |       } | 
			
		
	
		
			
				
					|  |  |  |  |       self._rebuild_ui_at_path([]) | 
			
		
	
		
			
				
					|  |  |  |  |     elif parent["type"] == "split" and parent["orientation"] == orientation: | 
			
		
	
		
			
				
					|  |  |  |  |       parent["children"].insert(child_index + 1, {"type": "panel", "panel": new_panel}) | 
			
		
	
		
			
				
					|  |  |  |  |       parent["proportions"] = [1.0 / len(parent["children"])] * len(parent["children"]) | 
			
		
	
		
			
				
					|  |  |  |  |       self._rebuild_ui_at_path(panel_path[:-1]) | 
			
		
	
		
			
				
					|  |  |  |  |     else: | 
			
		
	
		
			
				
					|  |  |  |  |       new_split = {"type": "split", "orientation": orientation, "children": [current_layout, {"type": "panel", "panel": new_panel}], "proportions": [0.5, 0.5]} | 
			
		
	
		
			
				
					|  |  |  |  |       self._replace_layout_at_path(panel_path, new_split) | 
			
		
	
		
			
				
					|  |  |  |  |       self._rebuild_ui_at_path(panel_path) | 
			
		
	
		
			
				
					|  |  |  |  |       dpg.add_separator() | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |       content_tag = self._path_to_tag(path, "content") | 
			
		
	
		
			
				
					|  |  |  |  |       with dpg.child_window(tag=content_tag, border=False, height=-1, width=-1, no_scrollbar=True): | 
			
		
	
		
			
				
					|  |  |  |  |         panel.create_ui(content_tag) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def _create_split_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int): | 
			
		
	
		
			
				
					|  |  |  |  |     split_tag = self._path_to_tag(path, "split") | 
			
		
	
		
			
				
					|  |  |  |  |     orientation = layout["orientation"] | 
			
		
	
		
			
				
					|  |  |  |  |     min_pane_size = int(MIN_PANE_SIZE * self.scale) | 
			
		
	
		
			
				
					|  |  |  |  |     grip_size = int(GRIP_SIZE * self.scale) | 
			
		
	
		
			
				
					|  |  |  |  |     num_grips = len(layout["children"]) - 1 | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     with dpg.group(tag=split_tag, parent=parent_tag, horizontal=orientation == 0): | 
			
		
	
		
			
				
					|  |  |  |  |       for i, (child_layout, proportion) in enumerate(zip(layout["children"], layout["proportions"], strict=True)): | 
			
		
	
		
			
				
					|  |  |  |  |         child_path = path + [i] | 
			
		
	
		
			
				
					|  |  |  |  |         container_tag = self._path_to_tag(child_path, "container") | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         size = [width, height]  # pass through since get_item_rect_size is unavailble until rendered | 
			
		
	
		
			
				
					|  |  |  |  |         fill_size = [-1, -1]  # fill up to the border upon resize | 
			
		
	
		
			
				
					|  |  |  |  |         calculated_size = max(min_pane_size, int((size[orientation] - (num_grips * grip_size)) * proportion)) | 
			
		
	
		
			
				
					|  |  |  |  |         size[orientation] = fill_size[orientation] = calculated_size | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         with dpg.child_window(tag=container_tag, width=fill_size[0], height=fill_size[1], border=False, no_scrollbar=True): | 
			
		
	
		
			
				
					|  |  |  |  |           self._create_ui_recursive(child_layout, container_tag, child_path, size[0], size[1]) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         if i < len(layout["children"]) - 1:  # Add grip between panes (except after the last pane) | 
			
		
	
		
			
				
					|  |  |  |  |           self._create_grip(split_tag, path, i, orientation) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def clear_panel(self, panel): | 
			
		
	
		
			
				
					|  |  |  |  |     panel.clear() | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def delete_panel(self, panel_path: list[int]): | 
			
		
	
		
			
				
					|  |  |  |  |     if not panel_path:  # Root deletion | 
			
		
	
	
		
			
				
					|  |  |  | @ -57,37 +83,113 @@ class PlotLayoutManager: | 
			
		
	
		
			
				
					|  |  |  |  |       old_panel.destroy_ui() | 
			
		
	
		
			
				
					|  |  |  |  |       self.active_panels.remove(old_panel) | 
			
		
	
		
			
				
					|  |  |  |  |       new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager) | 
			
		
	
		
			
				
					|  |  |  |  |       self.active_panels.append(new_panel) | 
			
		
	
		
			
				
					|  |  |  |  |       self.layout = {"type": "panel", "panel": new_panel} | 
			
		
	
		
			
				
					|  |  |  |  |       self._rebuild_ui_at_path([]) | 
			
		
	
		
			
				
					|  |  |  |  |       return | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     parent, child_index = self._get_parent_and_index(panel_path) | 
			
		
	
		
			
				
					|  |  |  |  |     layout_to_delete = parent["children"][child_index] | 
			
		
	
		
			
				
					|  |  |  |  |     self._cleanup_ui_recursive(layout_to_delete) | 
			
		
	
		
			
				
					|  |  |  |  |     self._cleanup_ui_recursive(layout_to_delete, panel_path) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     parent["children"].pop(child_index) | 
			
		
	
		
			
				
					|  |  |  |  |     parent["proportions"].pop(child_index) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     if len(parent["children"]) == 1: # remove parent and collapse | 
			
		
	
		
			
				
					|  |  |  |  |     if len(parent["children"]) == 1:  # remove parent and collapse | 
			
		
	
		
			
				
					|  |  |  |  |       remaining_child = parent["children"][0] | 
			
		
	
		
			
				
					|  |  |  |  |       if len(panel_path) == 1: # parent is at root level - promote remaining child to root | 
			
		
	
		
			
				
					|  |  |  |  |       if len(panel_path) == 1:  # parent is at root level - promote remaining child to root | 
			
		
	
		
			
				
					|  |  |  |  |         self.layout = remaining_child | 
			
		
	
		
			
				
					|  |  |  |  |         self._rebuild_ui_at_path([]) | 
			
		
	
		
			
				
					|  |  |  |  |       else: # replace parent with remaining child in grandparent | 
			
		
	
		
			
				
					|  |  |  |  |       else:  # replace parent with remaining child in grandparent | 
			
		
	
		
			
				
					|  |  |  |  |         grandparent_path = panel_path[:-2] | 
			
		
	
		
			
				
					|  |  |  |  |         parent_index = panel_path[-2] | 
			
		
	
		
			
				
					|  |  |  |  |         self._replace_layout_at_path(grandparent_path + [parent_index], remaining_child) | 
			
		
	
		
			
				
					|  |  |  |  |         self._rebuild_ui_at_path(grandparent_path + [parent_index]) | 
			
		
	
		
			
				
					|  |  |  |  |     else: # redistribute proportions | 
			
		
	
		
			
				
					|  |  |  |  |     else:  # redistribute proportions | 
			
		
	
		
			
				
					|  |  |  |  |       equal_prop = 1.0 / len(parent["children"]) | 
			
		
	
		
			
				
					|  |  |  |  |       parent["proportions"] = [equal_prop] * len(parent["children"]) | 
			
		
	
		
			
				
					|  |  |  |  |       self._rebuild_ui_at_path(panel_path[:-1]) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def split_panel(self, panel_path: list[int], orientation: int): | 
			
		
	
		
			
				
					|  |  |  |  |     current_layout = self._get_layout_at_path(panel_path) | 
			
		
	
		
			
				
					|  |  |  |  |     existing_panel = current_layout["panel"] | 
			
		
	
		
			
				
					|  |  |  |  |     new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager) | 
			
		
	
		
			
				
					|  |  |  |  |     parent, child_index = self._get_parent_and_index(panel_path) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     if parent is None:  # Root split | 
			
		
	
		
			
				
					|  |  |  |  |       self.layout = { | 
			
		
	
		
			
				
					|  |  |  |  |         "type": "split", | 
			
		
	
		
			
				
					|  |  |  |  |         "orientation": orientation, | 
			
		
	
		
			
				
					|  |  |  |  |         "children": [{"type": "panel", "panel": existing_panel}, {"type": "panel", "panel": new_panel}], | 
			
		
	
		
			
				
					|  |  |  |  |         "proportions": [0.5, 0.5], | 
			
		
	
		
			
				
					|  |  |  |  |       } | 
			
		
	
		
			
				
					|  |  |  |  |       self._rebuild_ui_at_path([]) | 
			
		
	
		
			
				
					|  |  |  |  |     elif parent["type"] == "split" and parent["orientation"] == orientation:  # Same orientation - insert into existing split | 
			
		
	
		
			
				
					|  |  |  |  |       parent["children"].insert(child_index + 1, {"type": "panel", "panel": new_panel}) | 
			
		
	
		
			
				
					|  |  |  |  |       parent["proportions"] = [1.0 / len(parent["children"])] * len(parent["children"]) | 
			
		
	
		
			
				
					|  |  |  |  |       self._rebuild_ui_at_path(panel_path[:-1]) | 
			
		
	
		
			
				
					|  |  |  |  |     else:  # Different orientation - create new split level | 
			
		
	
		
			
				
					|  |  |  |  |       new_split = {"type": "split", "orientation": orientation, "children": [current_layout, {"type": "panel", "panel": new_panel}], "proportions": [0.5, 0.5]} | 
			
		
	
		
			
				
					|  |  |  |  |       self._replace_layout_at_path(panel_path, new_split) | 
			
		
	
		
			
				
					|  |  |  |  |       self._rebuild_ui_at_path(panel_path) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def _rebuild_ui_at_path(self, path: list[int]): | 
			
		
	
		
			
				
					|  |  |  |  |     layout = self._get_layout_at_path(path) | 
			
		
	
		
			
				
					|  |  |  |  |     if path: | 
			
		
	
		
			
				
					|  |  |  |  |       container_tag = self._path_to_tag(path, "container") | 
			
		
	
		
			
				
					|  |  |  |  |     else:  # Root update | 
			
		
	
		
			
				
					|  |  |  |  |       container_tag = self.container_tag | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     self._cleanup_ui_recursive(layout, path) | 
			
		
	
		
			
				
					|  |  |  |  |     dpg.delete_item(container_tag, children_only=True) | 
			
		
	
		
			
				
					|  |  |  |  |     width, height = dpg.get_item_rect_size(container_tag) | 
			
		
	
		
			
				
					|  |  |  |  |     self._create_ui_recursive(layout, container_tag, path, width, height) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def _cleanup_ui_recursive(self, layout: dict, path: list[int]): | 
			
		
	
		
			
				
					|  |  |  |  |     if layout["type"] == "panel": | 
			
		
	
		
			
				
					|  |  |  |  |       panel = layout["panel"] | 
			
		
	
		
			
				
					|  |  |  |  |       panel.destroy_ui() | 
			
		
	
		
			
				
					|  |  |  |  |       if panel in self.active_panels: | 
			
		
	
		
			
				
					|  |  |  |  |         self.active_panels.remove(panel) | 
			
		
	
		
			
				
					|  |  |  |  |     else: | 
			
		
	
		
			
				
					|  |  |  |  |       # Clean up grip handler registries for splits BEFORE recursing | 
			
		
	
		
			
				
					|  |  |  |  |       for i in range(len(layout["children"]) - 1): | 
			
		
	
		
			
				
					|  |  |  |  |         grip_tag = self._path_to_tag(path, f"grip_{i}") | 
			
		
	
		
			
				
					|  |  |  |  |         handler_tag = f"{grip_tag}_handler" | 
			
		
	
		
			
				
					|  |  |  |  |         if dpg.does_item_exist(handler_tag): | 
			
		
	
		
			
				
					|  |  |  |  |           dpg.delete_item(handler_tag) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |       # Recursively cleanup children | 
			
		
	
		
			
				
					|  |  |  |  |       for i, child in enumerate(layout["children"]): | 
			
		
	
		
			
				
					|  |  |  |  |         child_path = path + [i] | 
			
		
	
		
			
				
					|  |  |  |  |         self._cleanup_ui_recursive(child, child_path) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def update_all_panels(self): | 
			
		
	
		
			
				
					|  |  |  |  |     for panel in self.active_panels: | 
			
		
	
		
			
				
					|  |  |  |  |       panel.update() | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def on_viewport_resize(self): | 
			
		
	
		
			
				
					|  |  |  |  |     self._resize_splits_recursive(self.layout, []) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def _resize_splits_recursive(self, layout: dict, path: list[int]): | 
			
		
	
		
			
				
					|  |  |  |  |     if layout["type"] == "split": | 
			
		
	
		
			
				
					|  |  |  |  |       split_tag = self._path_to_tag(path, "split") | 
			
		
	
		
			
				
					|  |  |  |  |       if dpg.does_item_exist(split_tag): | 
			
		
	
		
			
				
					|  |  |  |  |         parent_tag = dpg.get_item_parent(split_tag) | 
			
		
	
		
			
				
					|  |  |  |  |         grip_size = int(GRIP_SIZE * self.scale) | 
			
		
	
		
			
				
					|  |  |  |  |         min_pane_size = int(MIN_PANE_SIZE * self.scale) | 
			
		
	
		
			
				
					|  |  |  |  |         num_grips = len(layout["children"]) - 1 | 
			
		
	
		
			
				
					|  |  |  |  |         orientation = layout["orientation"] | 
			
		
	
		
			
				
					|  |  |  |  |         available_sizes = dpg.get_item_rect_size(parent_tag) | 
			
		
	
		
			
				
					|  |  |  |  |         size_properties = ("width", "height") | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         for i, proportion in enumerate(layout["proportions"]): | 
			
		
	
		
			
				
					|  |  |  |  |           child_path = path + [i] | 
			
		
	
		
			
				
					|  |  |  |  |           container_tag = self._path_to_tag(child_path, "container") | 
			
		
	
		
			
				
					|  |  |  |  |           if dpg.does_item_exist(container_tag): | 
			
		
	
		
			
				
					|  |  |  |  |             new_size = max(min_pane_size, int((available_sizes[orientation] - (num_grips * grip_size)) * proportion)) | 
			
		
	
		
			
				
					|  |  |  |  |             dpg.configure_item(container_tag, **{size_properties[orientation]: new_size}) | 
			
		
	
		
			
				
					|  |  |  |  |           self._resize_splits_recursive(layout["children"][i], child_path) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def _get_layout_at_path(self, path: list[int]) -> dict: | 
			
		
	
		
			
				
					|  |  |  |  |     current = self.layout | 
			
		
	
		
			
				
					|  |  |  |  |     for index in path: | 
			
		
	
	
		
			
				
					|  |  |  | @ -108,91 +210,64 @@ class PlotLayoutManager: | 
			
		
	
		
			
				
					|  |  |  |  |     path_str = "_".join(map(str, path)) if path else "root" | 
			
		
	
		
			
				
					|  |  |  |  |     return f"{prefix}_{path_str}" if prefix else path_str | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   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) | 
			
		
	
		
			
				
					|  |  |  |  |     else: | 
			
		
	
		
			
				
					|  |  |  |  |       self._create_split_ui(layout, parent_tag, path, width, height) | 
			
		
	
		
			
				
					|  |  |  |  |   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}") | 
			
		
	
		
			
				
					|  |  |  |  |     grip_size = int(GRIP_SIZE * self.scale) | 
			
		
	
		
			
				
					|  |  |  |  |     width = grip_size if orientation == 0 else -1 | 
			
		
	
		
			
				
					|  |  |  |  |     height = grip_size if orientation == 1 else -1 | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int]): | 
			
		
	
		
			
				
					|  |  |  |  |     panel_tag = self._path_to_tag(path, "panel") | 
			
		
	
		
			
				
					|  |  |  |  |     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.child_window(tag=panel_tag, parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True): | 
			
		
	
		
			
				
					|  |  |  |  |       with dpg.group(horizontal=True): | 
			
		
	
		
			
				
					|  |  |  |  |         dpg.add_input_text(default_value=layout["panel"].title, width=int(100 * self.scale), callback=lambda s, v: setattr(layout["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(layout["panel"]), width=int(50 * self.scale)) | 
			
		
	
		
			
				
					|  |  |  |  |         dpg.add_button(label="Delete", callback=lambda: self.delete_panel(path), width=int(50 * self.scale)) | 
			
		
	
		
			
				
					|  |  |  |  |         dpg.add_button(label="Split H", callback=lambda: self.split_panel(path, "horizontal"), width=int(50 * self.scale)) | 
			
		
	
		
			
				
					|  |  |  |  |         dpg.add_button(label="Split V", callback=lambda: self.split_panel(path, "vertical"), width=int(50 * self.scale)) | 
			
		
	
		
			
				
					|  |  |  |  |     with dpg.item_handler_registry(tag=f"{grip_tag}_handler"): | 
			
		
	
		
			
				
					|  |  |  |  |       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.add_separator() | 
			
		
	
		
			
				
					|  |  |  |  |   def _on_grip_drag(self, sender, app_data, user_data): | 
			
		
	
		
			
				
					|  |  |  |  |     path, grip_index, orientation = user_data | 
			
		
	
		
			
				
					|  |  |  |  |     layout = self._get_layout_at_path(path) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |       content_tag = self._path_to_tag(path, "content") | 
			
		
	
		
			
				
					|  |  |  |  |       with dpg.child_window(tag=content_tag, border=False, height=-1, width=-1, no_scrollbar=True): | 
			
		
	
		
			
				
					|  |  |  |  |         layout["panel"].create_ui(content_tag) | 
			
		
	
		
			
				
					|  |  |  |  |     if "_drag_data" not in layout: | 
			
		
	
		
			
				
					|  |  |  |  |       layout["_drag_data"] = {"initial_proportions": layout["proportions"][:], "start_mouse": dpg.get_mouse_pos(local=False)[orientation]} | 
			
		
	
		
			
				
					|  |  |  |  |       return | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def _create_split_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int): | 
			
		
	
		
			
				
					|  |  |  |  |     split_tag = self._path_to_tag(path, "split") | 
			
		
	
		
			
				
					|  |  |  |  |     is_horizontal = layout["orientation"] == "horizontal" | 
			
		
	
		
			
				
					|  |  |  |  |     drag_data = layout["_drag_data"] | 
			
		
	
		
			
				
					|  |  |  |  |     current_coord = dpg.get_mouse_pos(local=False)[orientation] | 
			
		
	
		
			
				
					|  |  |  |  |     delta = current_coord - drag_data["start_mouse"] | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     with dpg.group(tag=split_tag, parent=parent_tag, horizontal=is_horizontal): | 
			
		
	
		
			
				
					|  |  |  |  |       for i, (child_layout, proportion) in enumerate(zip(layout["children"], layout["proportions"], strict=True)): | 
			
		
	
		
			
				
					|  |  |  |  |         child_path = path + [i] | 
			
		
	
		
			
				
					|  |  |  |  |         container_tag = self._path_to_tag(child_path, "container") | 
			
		
	
		
			
				
					|  |  |  |  |     split_tag = self._path_to_tag(path, "split") | 
			
		
	
		
			
				
					|  |  |  |  |     if not dpg.does_item_exist(split_tag): | 
			
		
	
		
			
				
					|  |  |  |  |       return | 
			
		
	
		
			
				
					|  |  |  |  |     total_size = dpg.get_item_rect_size(split_tag)[orientation] | 
			
		
	
		
			
				
					|  |  |  |  |     num_grips = len(layout["children"]) - 1 | 
			
		
	
		
			
				
					|  |  |  |  |     usable_size = max(100, total_size - (num_grips * int(GRIP_SIZE * self.scale))) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         if is_horizontal: | 
			
		
	
		
			
				
					|  |  |  |  |           child_width = max(100, int(width * proportion)) | 
			
		
	
		
			
				
					|  |  |  |  |           with dpg.child_window(tag=container_tag, width=child_width, height=-1, border=False, no_scrollbar=True): | 
			
		
	
		
			
				
					|  |  |  |  |             self._create_ui_recursive(child_layout, container_tag, child_path, child_width, height) | 
			
		
	
		
			
				
					|  |  |  |  |         else: | 
			
		
	
		
			
				
					|  |  |  |  |           child_height = max(100, int(height * proportion)) | 
			
		
	
		
			
				
					|  |  |  |  |           with dpg.child_window(tag=container_tag, width=-1, height=child_height, border=False, no_scrollbar=True): | 
			
		
	
		
			
				
					|  |  |  |  |             self._create_ui_recursive(child_layout, container_tag, child_path, width, child_height) | 
			
		
	
		
			
				
					|  |  |  |  |     delta_prop = delta / usable_size | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def _rebuild_ui_at_path(self, path: list[int]): | 
			
		
	
		
			
				
					|  |  |  |  |     layout = self._get_layout_at_path(path) | 
			
		
	
		
			
				
					|  |  |  |  |     left_idx = grip_index | 
			
		
	
		
			
				
					|  |  |  |  |     right_idx = left_idx + 1 | 
			
		
	
		
			
				
					|  |  |  |  |     initial = drag_data["initial_proportions"] | 
			
		
	
		
			
				
					|  |  |  |  |     min_prop = int(MIN_PANE_SIZE * self.scale) / usable_size | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     if not path:  # Root update | 
			
		
	
		
			
				
					|  |  |  |  |       dpg.delete_item(self.container_tag, children_only=True) | 
			
		
	
		
			
				
					|  |  |  |  |       container_width, container_height = dpg.get_item_rect_size(self.container_tag) | 
			
		
	
		
			
				
					|  |  |  |  |       self._create_ui_recursive(layout, self.container_tag, path, container_width, container_height) | 
			
		
	
		
			
				
					|  |  |  |  |     else: | 
			
		
	
		
			
				
					|  |  |  |  |       container_tag = self._path_to_tag(path, "container") | 
			
		
	
		
			
				
					|  |  |  |  |       if dpg.does_item_exist(container_tag): | 
			
		
	
		
			
				
					|  |  |  |  |         self._cleanup_ui_recursive(layout) | 
			
		
	
		
			
				
					|  |  |  |  |         dpg.delete_item(container_tag, children_only=True) | 
			
		
	
		
			
				
					|  |  |  |  |         width, height = dpg.get_item_rect_size(container_tag) | 
			
		
	
		
			
				
					|  |  |  |  |         self._create_ui_recursive(layout, container_tag, path, width, height) | 
			
		
	
		
			
				
					|  |  |  |  |     new_left = max(min_prop, initial[left_idx] + delta_prop) | 
			
		
	
		
			
				
					|  |  |  |  |     new_right = max(min_prop, initial[right_idx] - delta_prop) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def _cleanup_ui_recursive(self, layout: dict): | 
			
		
	
		
			
				
					|  |  |  |  |     if layout["type"] == "panel": | 
			
		
	
		
			
				
					|  |  |  |  |       panel = layout["panel"] | 
			
		
	
		
			
				
					|  |  |  |  |       panel.destroy_ui() | 
			
		
	
		
			
				
					|  |  |  |  |       if panel in self.active_panels: | 
			
		
	
		
			
				
					|  |  |  |  |         self.active_panels.remove(panel) | 
			
		
	
		
			
				
					|  |  |  |  |     else: | 
			
		
	
		
			
				
					|  |  |  |  |       for child in layout["children"]: | 
			
		
	
		
			
				
					|  |  |  |  |         self._cleanup_ui_recursive(child) | 
			
		
	
		
			
				
					|  |  |  |  |     total_available = initial[left_idx] + initial[right_idx] | 
			
		
	
		
			
				
					|  |  |  |  |     if new_left + new_right > total_available: | 
			
		
	
		
			
				
					|  |  |  |  |       if new_left > new_right: | 
			
		
	
		
			
				
					|  |  |  |  |         new_left = total_available - new_right | 
			
		
	
		
			
				
					|  |  |  |  |       else: | 
			
		
	
		
			
				
					|  |  |  |  |         new_right = total_available - new_left | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def _resize_splits_recursive(self, layout: dict, path: list[int]): | 
			
		
	
		
			
				
					|  |  |  |  |     if layout["type"] == "split": | 
			
		
	
		
			
				
					|  |  |  |  |       split_tag = self._path_to_tag(path, "split") | 
			
		
	
		
			
				
					|  |  |  |  |       if dpg.does_item_exist(split_tag): | 
			
		
	
		
			
				
					|  |  |  |  |         parent_tag = dpg.get_item_parent(split_tag) | 
			
		
	
		
			
				
					|  |  |  |  |         available_width, available_height = dpg.get_item_rect_size(parent_tag) | 
			
		
	
		
			
				
					|  |  |  |  |     layout["proportions"] = initial[:] | 
			
		
	
		
			
				
					|  |  |  |  |     layout["proportions"][left_idx] = new_left | 
			
		
	
		
			
				
					|  |  |  |  |     layout["proportions"][right_idx] = new_right | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         for i, proportion in enumerate(layout["proportions"]): | 
			
		
	
		
			
				
					|  |  |  |  |           child_path = path + [i] | 
			
		
	
		
			
				
					|  |  |  |  |           container_tag = self._path_to_tag(child_path, "container") | 
			
		
	
		
			
				
					|  |  |  |  |           if dpg.does_item_exist(container_tag): | 
			
		
	
		
			
				
					|  |  |  |  |             if layout["orientation"] == "horizontal": | 
			
		
	
		
			
				
					|  |  |  |  |               dpg.configure_item(container_tag, width=max(100, int(available_width * proportion))) | 
			
		
	
		
			
				
					|  |  |  |  |             else: | 
			
		
	
		
			
				
					|  |  |  |  |               dpg.configure_item(container_tag, height=max(100, int(available_height * proportion))) | 
			
		
	
		
			
				
					|  |  |  |  |     self._resize_splits_recursive(layout, path) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |           self._resize_splits_recursive(layout["children"][i], child_path) | 
			
		
	
		
			
				
					|  |  |  |  |   def _on_grip_end(self, sender, app_data, user_data): | 
			
		
	
		
			
				
					|  |  |  |  |     path, _, _ = user_data | 
			
		
	
		
			
				
					|  |  |  |  |     self._get_layout_at_path(path).pop("_drag_data", None) | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |   def _clear_panel(self, panel): | 
			
		
	
		
			
				
					|  |  |  |  |     if hasattr(panel, 'clear_all_series'): | 
			
		
	
		
			
				
					|  |  |  |  |       panel.clear_all_series() | 
			
		
	
	
		
			
				
					|  |  |  | 
 |