# HierarchicalCanvasGraph ## Introduction The HierarchicalCanvasGraph component provides a canvas-based hierarchical graph visualization with interactive features. It displays nodes and edges in a tree layout using the Reingold-Tilford algorithm, with built-in support for expand/collapse, zoom/pan, and filtering. **Key features:** - Canvas-based rendering for smooth performance with large graphs - Expand/collapse nodes with children - Zoom and pan with mouse wheel and drag - Search/filter nodes by text, type, or kind - Click to select nodes - Stable zoom maintained on container resize - Persistent state (collapsed nodes, view transform, filters) - Event handlers for node interactions **Common use cases:** - Visualizing class hierarchies and inheritance trees - Displaying dependency graphs - Exploring file/folder structures - Showing organizational charts - Debugging object instance relationships ## Quick Start Here's a minimal example showing a simple hierarchy: ```python from fasthtml.common import * from myfasthtml.controls.HierarchicalCanvasGraph import ( HierarchicalCanvasGraph, HierarchicalCanvasGraphConf ) from myfasthtml.core.instances import RootInstance from myfasthtml.myfastapp import create_app app, rt = create_app() @rt("/") def index(session): root = RootInstance(session) # Define nodes and edges nodes = [ {"id": "root", "label": "Root", "description": "Base class", "type": "Class", "kind": "base"}, {"id": "child1", "label": "Child 1", "description": "First derived class", "type": "Class", "kind": "derived"}, {"id": "child2", "label": "Child 2", "description": "Second derived class", "type": "Class", "kind": "derived"}, ] edges = [ {"from": "root", "to": "child1"}, {"from": "root", "to": "child2"}, ] # Create graph conf = HierarchicalCanvasGraphConf(nodes=nodes, edges=edges) graph = HierarchicalCanvasGraph(root, conf) return Titled("Graph Example", graph) serve() ``` This creates a complete hierarchical graph with: - Three nodes in a simple parent-child relationship - Automatic tree layout with expand/collapse controls - Built-in zoom/pan navigation - Search filter bar **Note:** The graph automatically saves collapsed state and zoom/pan position across sessions. ## Basic Usage ### Visual Structure ``` ┌─────────────────────────────────────────┐ │ Filter instances... [x] │ ← Query filter bar ├─────────────────────────────────────────┤ │ │ │ ┌──────────────┐ │ │ │ Root │ [±] │ ← Node label (bold) │ │ Description │ │ ← Description (gray, smaller) │ └──────────────┘ │ │ │ │ │ ┌─────┴─────┐ │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ │ ← Child nodes │ │Child 1 │ │Child 2 │ │ │ │Details │ │Details │ │ │ └─────────┘ └─────────┘ │ │ │ │ • Dot grid background │ ← Canvas area │ • Mouse wheel to zoom │ │ • Drag to pan │ │ │ └─────────────────────────────────────────┘ ``` ### Creating the Graph **Step 1: Define nodes** Each node is a dictionary with: ```python nodes = [ { "id": "unique_id", # Required: unique identifier "label": "Display Name", # Required: shown in the node (main line) "description": "Details...", # Optional: shown below label (smaller, gray) "type": "ClassName", # Optional: shown as badge "kind": "category", # Optional: affects border color } ] ``` **Note:** Nodes with `description` are taller (54px vs 36px) to accommodate the second line of text. **Step 2: Define edges** Each edge connects two nodes: ```python edges = [ {"from": "parent_id", "to": "child_id"} ] ``` **Step 3: Create configuration** ```python from myfasthtml.controls.HierarchicalCanvasGraph import HierarchicalCanvasGraphConf conf = HierarchicalCanvasGraphConf( nodes=nodes, edges=edges, events_handlers=None # Optional: dict of event handlers ) ``` **Step 4: Instantiate component** ```python graph = HierarchicalCanvasGraph(parent, conf, _id="custom-id") ``` ### Configuration | Parameter | Type | Description | Default | |-------------------|-----------------------|--------------------------------------------------|---------| | `nodes` | `list[dict]` | List of node dictionaries | - | | `edges` | `list[dict]` | List of edge dictionaries | - | | `events_handlers` | `Optional[dict]` | Event name to Command object mapping | `None` | ### Node Properties | Property | Type | Required | Description | |---------------|-------|----------|------------------------------------------------| | `id` | `str` | Yes | Unique node identifier | | `label` | `str` | Yes | Display text shown in the node (main line) | | `description` | `str` | No | Secondary text shown below label (smaller, gray) | | `type` | `str` | No | Type badge shown on node (clickable filter) | | `kind` | `str` | No | Category determining border color/style | ### Edge Properties | Property | Type | Required | Description | |----------|-------|----------|--------------------------------| | `from` | `str` | Yes | Source node ID | | `to` | `str` | Yes | Target node ID | ## Advanced Features ### Filtering The component provides three types of filtering: **1. Text search** (via filter bar) Searches in node `id`, `label`, `type`, and `kind` fields: ```python # User types in filter bar # Automatically filters matching nodes # Non-matching nodes are dimmed ``` **2. Type filter** (click node badge) Click a node's type badge to filter by that type: ```python # Clicking "Class" badge shows only nodes with type="Class" # Clicking again clears the filter ``` **3. Kind filter** (click node border) Click a node's border to filter by kind: ```python # Clicking border of kind="base" shows only base nodes # Clicking again clears the filter ``` **Filter behavior:** - Only one filter type active at a time - Filters dim non-matching nodes (opacity reduced) - Matching nodes and their ancestors remain fully visible - Clear filter by clicking same badge/border or clearing search text ### Events The component supports two event types via `events_handlers`: **select_node**: Fired when clicking a node (not the toggle button) ```python from myfasthtml.core.commands import Command def on_select(node_id=None): print(f"Selected node: {node_id}") return f"Selected: {node_id}" conf = HierarchicalCanvasGraphConf( nodes=nodes, edges=edges, events_handlers={ "select_node": Command( "SelectNode", "Handle node selection", on_select ) } ) ``` **toggle_node**: Fired when clicking expand/collapse button ```python def on_toggle(node_id=None): print(f"Toggled node: {node_id}") return "Toggled" conf = HierarchicalCanvasGraphConf( nodes=nodes, edges=edges, events_handlers={ "toggle_node": Command( "ToggleNode", "Handle node toggle", on_toggle ) } ) ``` ### State Persistence The graph automatically persists: **Collapsed nodes:** ```python # Get current collapsed state collapsed = graph.get_state().collapsed # List of node IDs # Programmatically set collapsed nodes graph.set_collapsed({"node1", "node2"}) # Toggle a specific node graph.toggle_node("node1") ``` **View transform (zoom/pan):** ```python # Get current transform transform = graph.get_state().transform # {"x": 100, "y": 50, "scale": 1.5} ``` **Layout mode:** ```python # Get current layout mode mode = graph.get_state().layout_mode # "horizontal" or "vertical" ``` **Current selection:** ```python # Get selected node ID selected = graph.get_selected_id() # Returns node ID or None ``` ### Layout Modes The graph supports two layout orientations: - **horizontal**: Nodes flow left-to-right (default) - **vertical**: Nodes flow top-to-bottom Users can toggle between modes using the UI controls. The mode is persisted in state. ## Examples ### Example 1: Simple Class Hierarchy Basic inheritance tree visualization: ```python from fasthtml.common import * from myfasthtml.controls.HierarchicalCanvasGraph import ( HierarchicalCanvasGraph, HierarchicalCanvasGraphConf ) from myfasthtml.core.instances import RootInstance from myfasthtml.myfastapp import create_app app, rt = create_app() @rt("/") def index(session): root = RootInstance(session) nodes = [ {"id": "object", "label": "Object", "description": "Base class for all objects", "type": "BaseClass", "kind": "builtin"}, {"id": "animal", "label": "Animal", "description": "Abstract animal class", "type": "Class", "kind": "abstract"}, {"id": "dog", "label": "Dog", "description": "Canine implementation", "type": "Class", "kind": "concrete"}, {"id": "cat", "label": "Cat", "description": "Feline implementation", "type": "Class", "kind": "concrete"}, ] edges = [ {"from": "object", "to": "animal"}, {"from": "animal", "to": "dog"}, {"from": "animal", "to": "cat"}, ] conf = HierarchicalCanvasGraphConf(nodes=nodes, edges=edges) graph = HierarchicalCanvasGraph(root, conf) return Titled("Class Hierarchy", graph) serve() ``` ### Example 2: Graph with Event Handlers Handling node selection and expansion: ```python from fasthtml.common import * from myfasthtml.controls.HierarchicalCanvasGraph import ( HierarchicalCanvasGraph, HierarchicalCanvasGraphConf ) from myfasthtml.core.instances import RootInstance from myfasthtml.core.commands import Command from myfasthtml.myfastapp import create_app app, rt = create_app() @rt("/") def index(session): root = RootInstance(session) # Event handlers def on_select(node_id=None): return Div( f"Selected: {node_id}", id="selection-info", cls="alert alert-info" ) def on_toggle(node_id=None): return Div( f"Toggled: {node_id}", id="toggle-info", cls="alert alert-success" ) # Create commands select_cmd = Command("SelectNode", "Handle selection", on_select) toggle_cmd = Command("ToggleNode", "Handle toggle", on_toggle) nodes = [ {"id": "root", "label": "Root Module", "description": "Main package entry point", "type": "Module", "kind": "package"}, {"id": "sub1", "label": "Submodule A", "description": "Feature A implementation", "type": "Module", "kind": "module"}, {"id": "sub2", "label": "Submodule B", "description": "Feature B implementation", "type": "Module", "kind": "module"}, {"id": "class1", "label": "ClassA", "description": "Handler for feature A", "type": "Class", "kind": "class"}, {"id": "class2", "label": "ClassB", "description": "Handler for feature B", "type": "Class", "kind": "class"}, ] edges = [ {"from": "root", "to": "sub1"}, {"from": "root", "to": "sub2"}, {"from": "sub1", "to": "class1"}, {"from": "sub2", "to": "class2"}, ] conf = HierarchicalCanvasGraphConf( nodes=nodes, edges=edges, events_handlers={ "select_node": select_cmd, "toggle_node": toggle_cmd } ) graph = HierarchicalCanvasGraph(root, conf) return Titled("Interactive Graph", graph, Div(id="selection-info"), Div(id="toggle-info") ) serve() ``` ### Example 3: Filtered Graph with Type/Kind Badges Graph with multiple node types for filtering: ```python from fasthtml.common import * from myfasthtml.controls.HierarchicalCanvasGraph import ( HierarchicalCanvasGraph, HierarchicalCanvasGraphConf ) from myfasthtml.core.instances import RootInstance from myfasthtml.myfastapp import create_app app, rt = create_app() @rt("/") def index(session): root = RootInstance(session) nodes = [ # Controllers {"id": "ctrl1", "label": "UserController", "description": "Handles user requests", "type": "Controller", "kind": "web"}, {"id": "ctrl2", "label": "AdminController", "description": "Admin panel endpoints", "type": "Controller", "kind": "web"}, # Services {"id": "svc1", "label": "AuthService", "description": "Authentication logic", "type": "Service", "kind": "business"}, {"id": "svc2", "label": "EmailService", "description": "Email notifications", "type": "Service", "kind": "infrastructure"}, # Repositories {"id": "repo1", "label": "UserRepo", "description": "User data access", "type": "Repository", "kind": "data"}, {"id": "repo2", "label": "LogRepo", "description": "Logging data access", "type": "Repository", "kind": "data"}, ] edges = [ {"from": "ctrl1", "to": "svc1"}, {"from": "ctrl2", "to": "svc1"}, {"from": "svc1", "to": "repo1"}, {"from": "svc2", "to": "repo2"}, ] conf = HierarchicalCanvasGraphConf(nodes=nodes, edges=edges) graph = HierarchicalCanvasGraph(root, conf) return Titled("Dependency Graph", Div( P("Click on type badges (Controller, Service, Repository) to filter by type"), P("Click on node borders to filter by kind (web, business, infrastructure, data)"), P("Use search bar to filter by text"), cls="mb-4" ), graph ) serve() ``` ### Example 4: Programmatic State Control Controlling collapsed state and getting selection: ```python from fasthtml.common import * from myfasthtml.controls.HierarchicalCanvasGraph import ( HierarchicalCanvasGraph, HierarchicalCanvasGraphConf ) from myfasthtml.controls.helpers import mk from myfasthtml.core.instances import RootInstance from myfasthtml.core.commands import Command from myfasthtml.myfastapp import create_app app, rt = create_app() @rt("/") def index(session): root = RootInstance(session) nodes = [ {"id": "root", "label": "Project", "description": "Root directory", "type": "Folder", "kind": "root"}, {"id": "src", "label": "src/", "description": "Source code", "type": "Folder", "kind": "folder"}, {"id": "tests", "label": "tests/", "description": "Test suite", "type": "Folder", "kind": "folder"}, {"id": "main", "label": "main.py", "description": "Application entry point", "type": "File", "kind": "python"}, {"id": "utils", "label": "utils.py", "description": "Utility functions", "type": "File", "kind": "python"}, {"id": "test1", "label": "test_main.py", "description": "Main module tests", "type": "File", "kind": "test"}, ] edges = [ {"from": "root", "to": "src"}, {"from": "root", "to": "tests"}, {"from": "src", "to": "main"}, {"from": "src", "to": "utils"}, {"from": "tests", "to": "test1"}, ] conf = HierarchicalCanvasGraphConf(nodes=nodes, edges=edges) graph = HierarchicalCanvasGraph(root, conf, _id="file-tree") # Commands to control graph def collapse_all(): graph.set_collapsed({"root", "src", "tests"}) return graph def expand_all(): graph.set_collapsed(set()) return graph def show_selection(): selected = graph.get_selected_id() if selected: return Div(f"Selected: {selected}", id="info", cls="alert alert-info") return Div("No selection", id="info", cls="alert alert-warning") collapse_cmd = Command("CollapseAll", "Collapse all nodes", collapse_all) expand_cmd = Command("ExpandAll", "Expand all nodes", expand_all) selection_cmd = Command("ShowSelection", "Show selection", show_selection) return Titled("File Tree", Div( mk.button("Collapse All", command=collapse_cmd.htmx(target="#file-tree")), mk.button("Expand All", command=expand_cmd.htmx(target="#file-tree")), mk.button("Show Selection", command=selection_cmd.htmx(target="#info")), cls="flex gap-2 mb-4" ), Div(id="info"), graph ) serve() ``` --- ## Developer Reference This section contains technical details for developers working on the HierarchicalCanvasGraph component itself. ### State The component uses `HierarchicalCanvasGraphState` (inherits from `DbObject`) for persistence. | Name | Type | Description | Default | Persisted | |-----------------|-------------------|--------------------------------------------------|--------------|-----------| | `collapsed` | `list[str]` | List of collapsed node IDs | `[]` | Yes | | `transform` | `dict` | Zoom/pan transform: `{x, y, scale}` | See below | Yes | | `layout_mode` | `str` | Layout orientation: "horizontal" or "vertical" | `"horizontal"` | Yes | | `filter_text` | `Optional[str]` | Text search filter | `None` | Yes | | `filter_type` | `Optional[str]` | Type filter (from badge click) | `None` | Yes | | `filter_kind` | `Optional[str]` | Kind filter (from border click) | `None` | Yes | | `ns_selected_id` | `Optional[str]` | Currently selected node ID (ephemeral) | `None` | No | **Default transform:** ```python {"x": 0, "y": 0, "scale": 1} ``` ### Commands Internal commands (managed by `Commands` class inheriting from `BaseCommands`): | Name | Description | Internal | |----------------------|--------------------------------------------------|----------| | `update_view_state()` | Update view transform and layout mode | Yes | | `apply_filter()` | Apply current filter and update graph display | Yes | ### Public Methods | Method | Description | Returns | |--------------------------------|------------------------------------------|----------------------------| | `get_state()` | Get the persistent state object | `HierarchicalCanvasGraphState` | | `get_selected_id()` | Get currently selected node ID | `Optional[str]` | | `set_collapsed(node_ids: set)` | Set collapsed state of nodes | `None` | | `toggle_node(node_id: str)` | Toggle collapsed state of a node | `self` | | `render()` | Render the complete component | `Div` | ### Constructor Parameters | Parameter | Type | Description | Default | |------------|-----------------------------------|------------------------------------|---------| | `parent` | Instance | Parent instance (required) | - | | `conf` | `HierarchicalCanvasGraphConf` | Configuration object | - | | `_id` | `Optional[str]` | Custom ID (auto-generated if None) | `None` | ### High Level Hierarchical Structure ``` Div(id="{id}", cls="mf-hierarchical-canvas-graph") ├── Query(id="{id}-query") │ └── [Filter input and controls] ├── Div(id="{id}_container", cls="mf-hcg-container") │ └── [Canvas element - created by JS] └── Script └── initHierarchicalCanvasGraph('{id}_container', options) ``` ### Element IDs | Name | Description | |----------------------|------------------------------------------| | `{id}` | Root container div | | `{id}-query` | Query filter component | | `{id}_container` | Canvas container (sized by JS) | **Note:** `{id}` is the instance ID (auto-generated or custom `_id`). ### CSS Classes | Class | Element | |------------------------------------|-----------------------------------| | `mf-hierarchical-canvas-graph` | Root container | | `mf-hcg-container` | Canvas container div | ### Internal Methods | Method | Description | Returns | |-------------------------------------|--------------------------------------------------|-------------------| | `_handle_update_view_state()` | Update view state from client | `str` (empty) | | `_handle_apply_filter()` | Apply filter and re-render graph | `self` | | `_calculate_filtered_nodes()` | Calculate which nodes match current filter | `Optional[list[str]]` | | `_prepare_options()` | Prepare JavaScript options object | `dict` | ### JavaScript Interface The component calls `initHierarchicalCanvasGraph(containerId, options)` where `options` is: ```javascript { nodes: [...], // Array of node objects {id, label, description?, type?, kind?} edges: [...], // Array of edge objects {from, to} collapsed: [...], // Array of collapsed node IDs transform: {x, y, scale}, // Current view transform layout_mode: "horizontal", // Layout orientation filtered_nodes: [...], // Array of visible node IDs (null if no filter) events: { _internal_update_state: {...}, // HTMX options for state updates _internal_filter_by_type: {...}, // HTMX options for type filter _internal_filter_by_kind: {...}, // HTMX options for kind filter select_node: {...}, // User event handler (optional) toggle_node: {...} // User event handler (optional) } } ``` **Node object structure:** - `id` (required): Unique identifier - `label` (required): Main display text - `description` (optional): Secondary text shown below label in smaller gray font - `type` (optional): Type badge shown on node - `kind` (optional): Category affecting border color (root|single|unique|multiple) ### Event Flow **Node selection:** 1. User clicks node (not toggle button) 2. JS calls HTMX with `node_id` parameter 3. `select_node` handler executes (if defined) 4. Result updates target element **Node toggle:** 1. User clicks expand/collapse button 2. JS updates local collapsed state 3. JS calls `_internal_update_state` HTMX endpoint 4. State persisted in `HierarchicalCanvasGraphState.collapsed` 5. `toggle_node` handler executes (if defined) **Filter changes:** 1. User types in search / clicks badge / clicks border 2. Query component or JS triggers `apply_filter()` command 3. `_handle_apply_filter()` updates state filters 4. Component re-renders with filtered nodes 5. JS dims non-matching nodes ### Dependencies **Python:** - `fasthtml.components.Div` - `fasthtml.xtend.Script` - `myfasthtml.controls.Query` - Filter search bar - `myfasthtml.core.instances.MultipleInstance` - Instance management - `myfasthtml.core.dbmanager.DbObject` - State persistence **JavaScript:** - `initHierarchicalCanvasGraph()` - Must be loaded via MyFastHtml assets - Canvas API - For rendering - HTMX - For server communication