New hierarchical component, used for InstancesDebugger.py
This commit is contained in:
185
src/myfasthtml/controls/HierarchicalCanvasGraph.py
Normal file
185
src/myfasthtml/controls/HierarchicalCanvasGraph.py
Normal file
@@ -0,0 +1,185 @@
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from fasthtml.components import Div
|
||||
from fasthtml.xtend import Script
|
||||
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.instances import MultipleInstance
|
||||
|
||||
logger = logging.getLogger("HierarchicalCanvasGraph")
|
||||
|
||||
|
||||
@dataclass
|
||||
class HierarchicalCanvasGraphConf:
|
||||
"""Configuration for HierarchicalCanvasGraph control.
|
||||
|
||||
Attributes:
|
||||
nodes: List of node dictionaries with keys: id, label, type, kind
|
||||
edges: List of edge dictionaries with keys: from, to
|
||||
events_handlers: Optional dict mapping event names to Command objects
|
||||
Supported events: 'select_node', 'toggle_node'
|
||||
"""
|
||||
nodes: list[dict]
|
||||
edges: list[dict]
|
||||
events_handlers: Optional[dict] = None
|
||||
|
||||
|
||||
class HierarchicalCanvasGraphState(DbObject):
|
||||
"""Persistent state for HierarchicalCanvasGraph.
|
||||
|
||||
Only the collapsed state is persisted. Zoom, pan, and selection are ephemeral.
|
||||
"""
|
||||
|
||||
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 = []
|
||||
|
||||
# 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 HierarchicalCanvasGraph(MultipleInstance):
|
||||
"""A canvas-based hierarchical graph visualization control.
|
||||
|
||||
Displays nodes and edges in a tree layout with expand/collapse functionality.
|
||||
Uses HTML5 Canvas for rendering with stable zoom/pan and search filtering.
|
||||
|
||||
Features:
|
||||
- Reingold-Tilford hierarchical layout
|
||||
- Expand/collapse nodes with children
|
||||
- Zoom and pan with mouse wheel and drag
|
||||
- Search/filter nodes by label or kind
|
||||
- Click to select nodes
|
||||
- Dot grid background
|
||||
- Stable zoom on container resize
|
||||
|
||||
Events:
|
||||
- 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.
|
||||
|
||||
Args:
|
||||
parent: Parent instance
|
||||
conf: Configuration object with nodes, edges, and event handlers
|
||||
_id: Optional custom ID (auto-generated if not provided)
|
||||
"""
|
||||
super().__init__(parent, _id=_id)
|
||||
self.conf = conf
|
||||
self._state = HierarchicalCanvasGraphState(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.
|
||||
|
||||
Returns:
|
||||
HierarchicalCanvasGraphState: The state object
|
||||
"""
|
||||
return self._state
|
||||
|
||||
def get_selected_id(self) -> Optional[str]:
|
||||
"""Get the currently selected node ID.
|
||||
|
||||
Returns:
|
||||
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.
|
||||
|
||||
Args:
|
||||
node_ids: Set of node IDs to mark as collapsed
|
||||
"""
|
||||
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.
|
||||
|
||||
Args:
|
||||
node_id: The ID of the node to toggle
|
||||
|
||||
Returns:
|
||||
self: For chaining
|
||||
"""
|
||||
collapsed_set = set(self._state.collapsed)
|
||||
if node_id in collapsed_set:
|
||||
collapsed_set.remove(node_id)
|
||||
logger.debug(f"toggle_node: expanded {node_id}")
|
||||
else:
|
||||
collapsed_set.add(node_id)
|
||||
logger.debug(f"toggle_node: collapsed {node_id}")
|
||||
|
||||
self._state.collapsed = list(collapsed_set)
|
||||
return self
|
||||
|
||||
def _prepare_options(self) -> dict:
|
||||
"""Prepare JavaScript options object.
|
||||
|
||||
Returns:
|
||||
dict: Options to pass to the JS initialization function
|
||||
"""
|
||||
# Convert event handlers to HTMX options
|
||||
events = {}
|
||||
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
|
||||
}
|
||||
|
||||
def render(self):
|
||||
"""Render the HierarchicalCanvasGraph control.
|
||||
|
||||
Returns:
|
||||
Div: The rendered control with canvas and initialization script
|
||||
"""
|
||||
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() {{
|
||||
if (typeof initHierarchicalCanvasGraph === 'function') {{
|
||||
initHierarchicalCanvasGraph('{self._id}_container', {options_json});
|
||||
}} else {{
|
||||
console.error('initHierarchicalCanvasGraph function not found');
|
||||
}}
|
||||
}})();
|
||||
"""),
|
||||
|
||||
id=self._id,
|
||||
cls="mf-hierarchical-canvas-graph"
|
||||
)
|
||||
|
||||
def __ft__(self):
|
||||
"""FastHTML magic method for rendering.
|
||||
|
||||
Returns:
|
||||
Div: The rendered control
|
||||
"""
|
||||
return self.render()
|
||||
Reference in New Issue
Block a user