diff --git a/src/myfasthtml/assets/core/hierarchical_canvas_graph.js b/src/myfasthtml/assets/core/hierarchical_canvas_graph.js
index 66d073a..894e455 100644
--- a/src/myfasthtml/assets/core/hierarchical_canvas_graph.js
+++ b/src/myfasthtml/assets/core/hierarchical_canvas_graph.js
@@ -675,7 +675,7 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
function saveViewState() {
postEvent('_internal_update_state', {
- transform: transform,
+ transform: JSON.stringify(transform), // Serialize to JSON string for server
layout_mode: layoutMode
});
}
diff --git a/src/myfasthtml/controls/HierarchicalCanvasGraph.py b/src/myfasthtml/controls/HierarchicalCanvasGraph.py
index 547e418..8ba80bf 100644
--- a/src/myfasthtml/controls/HierarchicalCanvasGraph.py
+++ b/src/myfasthtml/controls/HierarchicalCanvasGraph.py
@@ -69,7 +69,7 @@ class Commands(BaseCommands):
"UpdateViewState",
"Update view transform and layout mode",
self._owner,
- self._owner._handle_update_view_state
+ self._owner.handle_update_view_state
).htmx(target=f"#{self._id}", swap='none')
def apply_filter(self):
@@ -81,9 +81,21 @@ class Commands(BaseCommands):
"ApplyFilter",
"Apply filter to graph",
self._owner,
- self._owner._handle_apply_filter,
+ self._owner.handle_apply_filter,
key="#{id}-apply-filter",
).htmx(target=f"#{self._id}")
+
+ def reset_view(self):
+ """Reset the view transform to default values.
+
+ This command can be used to fix stuck/frozen canvas by resetting zoom/pan state.
+ """
+ return Command(
+ "ResetTransform",
+ "Reset view transform",
+ self._owner,
+ self._owner.handle_reset_view
+ ).htmx(target=f"#{self._id}")
class HierarchicalCanvasGraph(MultipleInstance):
@@ -135,64 +147,81 @@ class HierarchicalCanvasGraph(MultipleInstance):
"""
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 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 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 _handle_update_view_state(self, transform=None, layout_mode=None):
+ def handle_update_view_state(self, transform: Optional[dict] = None, layout_mode: Optional[str] = None):
"""Internal handler to update view state from client.
Args:
- transform: Optional dict with zoom/pan transform state
+ transform: Optional dict with zoom/pan transform state (received as JSON string)
layout_mode: Optional string with layout orientation
Returns:
str: Empty string (no UI update needed)
"""
if transform is not None:
+ # Parse JSON string to dict (sent as JSON.stringify() from JS)
+ if isinstance(transform, str):
+ try:
+ transform = json.loads(transform)
+ except (json.JSONDecodeError, TypeError) as e:
+ logger.error(f"Failed to parse transform JSON: {e}")
+ return ""
+
self._state.transform = transform
- logger.debug(f"Transform updated: {self._state.transform}")
if layout_mode is not None:
self._state.layout_mode = layout_mode
- logger.debug(f"Layout mode updated: {self._state.layout_mode}")
return ""
- def _handle_apply_filter(self, query_param="text", value=None):
+ def handle_reset_view(self):
+ """Internal handler to reset view transform to default values.
+
+ Returns:
+ self: For HTMX to render the updated graph with reset transform
+ """
+ self._state.transform = {"x": 0, "y": 0, "scale": 1}
+ self._state.collapsed = []
+ logger.debug("Transform and collapsed state reset to defaults")
+ return self
+
+ def handle_apply_filter(self, query_param="text", value=None):
"""Internal handler to apply filter and re-render the graph.
Args:
diff --git a/src/myfasthtml/controls/InstancesDebugger.py b/src/myfasthtml/controls/InstancesDebugger.py
index 86161aa..62aaac8 100644
--- a/src/myfasthtml/controls/InstancesDebugger.py
+++ b/src/myfasthtml/controls/InstancesDebugger.py
@@ -1,8 +1,11 @@
from dataclasses import dataclass
+from fasthtml.components import Div, Button
+
from myfasthtml.controls.HierarchicalCanvasGraph import HierarchicalCanvasGraph, HierarchicalCanvasGraphConf
from myfasthtml.controls.Panel import Panel
from myfasthtml.controls.Properties import Properties
+from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
from myfasthtml.core.instances import SingleInstance, UniqueInstance, MultipleInstance, InstancesManager
@@ -24,6 +27,7 @@ class InstancesDebugger(SingleInstance):
super().__init__(parent, _id=_id)
self.conf = conf if conf is not None else InstancesDebuggerConf()
self._panel = Panel(self, _id="-panel")
+ self._canvas_graph = None # Will be created in render
self._select_command = Command("ShowInstance",
"Display selected Instance",
self,
@@ -38,16 +42,32 @@ class InstancesDebugger(SingleInstance):
"select_node": self._select_command
}
)
- canvas_graph = HierarchicalCanvasGraph(self, conf=graph_conf, _id="-canvas-graph")
- return self._panel.set_main(canvas_graph)
+ self._canvas_graph = HierarchicalCanvasGraph(self, conf=graph_conf, _id="-canvas-graph")
+
+ # Debug button to reset transform
+ reset_btn = mk.button(
+ "🔄 Reset View",
+ command=self._canvas_graph.commands.reset_view(),
+ cls="btn btn-sm btn-ghost"
+ )
+
+ main_content = Div(
+ Div(reset_btn, cls="p-2 flex justify-end bg-base-200"),
+ self._canvas_graph,
+ cls="flex flex-col h-full"
+ )
+
+ return self._panel.set_main(main_content)
- def on_select_node(self, event_data: dict):
+ def on_select_node(self, node_id=None, label=None, kind=None, type=None):
"""Handle node selection event from canvas graph.
Args:
- event_data: dict with keys: node_id, label, kind, type
+ node_id: Selected node's full ID (session#instance_id)
+ label: Selected node's label
+ kind: Selected node's kind (root|single|unique|multiple)
+ type: Selected node's type (class name)
"""
- node_id = event_data.get("node_id")
if not node_id:
return None