updated css. Added orientation. Saving orientation, position and scale in state

This commit is contained in:
2026-02-22 10:28:13 +01:00
parent 44691be30f
commit 8b8172231a
4 changed files with 345 additions and 97 deletions

View File

@@ -6,6 +6,8 @@ from typing import Optional
from fasthtml.components import Div
from fasthtml.xtend import Script
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance
@@ -30,20 +32,39 @@ class HierarchicalCanvasGraphConf:
class HierarchicalCanvasGraphState(DbObject):
"""Persistent state for HierarchicalCanvasGraph.
Only the collapsed state is persisted. Zoom, pan, and selection are ephemeral.
Persists collapsed nodes, view transform (zoom/pan), and layout orientation.
"""
def __init__(self, owner, save_state=True):
super().__init__(owner, save_state=save_state)
with self.initializing():
# Persisted: set of collapsed node IDs (stored as list for JSON serialization)
self.collapsed: list = []
# Persisted: zoom/pan transform
self.transform: dict = {"x": 0, "y": 0, "scale": 1}
# Persisted: layout orientation ('horizontal' or 'vertical')
self.layout_mode: str = 'horizontal'
# Not persisted: current selection (ephemeral)
self.ns_selected_id: Optional[str] = None
# Not persisted: zoom/pan transform (ephemeral)
self.ns_transform: dict = {"x": 0, "y": 0, "scale": 1}
class Commands(BaseCommands):
"""Commands for HierarchicalCanvasGraph internal state management."""
def update_view_state(self):
"""Update view transform and layout mode.
This command is called internally by the JS to persist view state changes.
"""
return Command(
"UpdateViewState",
"Update view transform and layout mode",
self._owner,
self._owner._handle_update_view_state
).htmx(target=f"#{self._id}", swap='none')
class HierarchicalCanvasGraph(MultipleInstance):
@@ -65,7 +86,7 @@ class HierarchicalCanvasGraph(MultipleInstance):
- select_node: Fired when a node is clicked (not on toggle button)
- toggle_node: Fired when a node's expand/collapse button is clicked
"""
def __init__(self, parent, conf: HierarchicalCanvasGraphConf, _id=None):
"""Initialize the HierarchicalCanvasGraph control.
@@ -77,10 +98,11 @@ class HierarchicalCanvasGraph(MultipleInstance):
super().__init__(parent, _id=_id)
self.conf = conf
self._state = HierarchicalCanvasGraphState(self)
self.commands = Commands(self)
logger.debug(f"HierarchicalCanvasGraph created with id={self._id}, "
f"nodes={len(conf.nodes)}, edges={len(conf.edges)}")
def get_state(self):
"""Get the control's persistent state.
@@ -88,7 +110,7 @@ class HierarchicalCanvasGraph(MultipleInstance):
HierarchicalCanvasGraphState: The state object
"""
return self._state
def get_selected_id(self) -> Optional[str]:
"""Get the currently selected node ID.
@@ -96,7 +118,7 @@ class HierarchicalCanvasGraph(MultipleInstance):
str or None: The selected node ID, or None if no selection
"""
return self._state.ns_selected_id
def set_collapsed(self, node_ids: set):
"""Set the collapsed state of nodes.
@@ -105,7 +127,7 @@ class HierarchicalCanvasGraph(MultipleInstance):
"""
self._state.collapsed = list(node_ids)
logger.debug(f"set_collapsed: {len(node_ids)} nodes collapsed")
def toggle_node(self, node_id: str):
"""Toggle the collapsed state of a node.
@@ -122,10 +144,29 @@ class HierarchicalCanvasGraph(MultipleInstance):
else:
collapsed_set.add(node_id)
logger.debug(f"toggle_node: collapsed {node_id}")
self._state.collapsed = list(collapsed_set)
return self
def _handle_update_view_state(self, event_data: dict):
"""Internal handler to update view state from client.
Args:
event_data: Dictionary with 'transform' and/or 'layout_mode' keys
Returns:
str: Empty string (no UI update needed)
"""
if 'transform' in event_data:
self._state.transform = event_data['transform']
logger.debug(f"Transform updated: {self._state.transform}")
if 'layout_mode' in event_data:
self._state.layout_mode = event_data['layout_mode']
logger.debug(f"Layout mode updated: {self._state.layout_mode}")
return ""
def _prepare_options(self) -> dict:
"""Prepare JavaScript options object.
@@ -134,17 +175,24 @@ class HierarchicalCanvasGraph(MultipleInstance):
"""
# Convert event handlers to HTMX options
events = {}
# Add internal handler for view state persistence
events['_internal_update_state'] = self.commands.update_view_state().ajax_htmx_options()
# Add user-provided event handlers
if self.conf.events_handlers:
for event_name, command in self.conf.events_handlers.items():
events[event_name] = command.ajax_htmx_options()
return {
"nodes": self.conf.nodes,
"edges": self.conf.edges,
"collapsed": self._state.collapsed,
"events": events
"nodes": self.conf.nodes,
"edges": self.conf.edges,
"collapsed": self._state.collapsed,
"transform": self._state.transform,
"layout_mode": self._state.layout_mode,
"events": events
}
def render(self):
"""Render the HierarchicalCanvasGraph control.
@@ -153,14 +201,14 @@ class HierarchicalCanvasGraph(MultipleInstance):
"""
options = self._prepare_options()
options_json = json.dumps(options, indent=2)
return Div(
# Canvas element (sized by JS to fill container)
Div(
id=f"{self._id}_container",
cls="mf-hcg-container"
),
# Initialization script
Script(f"""
(function() {{
@@ -171,11 +219,11 @@ class HierarchicalCanvasGraph(MultipleInstance):
}}
}})();
"""),
id=self._id,
cls="mf-hierarchical-canvas-graph"
)
def __ft__(self):
"""FastHTML magic method for rendering.