24 KiB
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:
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:
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:
edges = [
{"from": "parent_id", "to": "child_id"}
]
Step 3: Create configuration
from myfasthtml.controls.HierarchicalCanvasGraph import HierarchicalCanvasGraphConf
conf = HierarchicalCanvasGraphConf(
nodes=nodes,
edges=edges,
events_handlers=None # Optional: dict of event handlers
)
Step 4: Instantiate component
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:
# 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:
# 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:
# 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)
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
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:
# 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):
# Get current transform
transform = graph.get_state().transform
# {"x": 100, "y": 50, "scale": 1.5}
Layout mode:
# Get current layout mode
mode = graph.get_state().layout_mode # "horizontal" or "vertical"
Current selection:
# 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:
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:
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:
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:
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:
{"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:
{
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 identifierlabel(required): Main display textdescription(optional): Secondary text shown below label in smaller gray fonttype(optional): Type badge shown on nodekind(optional): Category affecting border color (root|single|unique|multiple)
Event Flow
Node selection:
- User clicks node (not toggle button)
- JS calls HTMX with
node_idparameter select_nodehandler executes (if defined)- Result updates target element
Node toggle:
- User clicks expand/collapse button
- JS updates local collapsed state
- JS calls
_internal_update_stateHTMX endpoint - State persisted in
HierarchicalCanvasGraphState.collapsed toggle_nodehandler executes (if defined)
Filter changes:
- User types in search / clicks badge / clicks border
- Query component or JS triggers
apply_filter()command _handle_apply_filter()updates state filters- Component re-renders with filtered nodes
- JS dims non-matching nodes
Dependencies
Python:
fasthtml.components.Divfasthtml.xtend.Scriptmyfasthtml.controls.Query- Filter search barmyfasthtml.core.instances.MultipleInstance- Instance managementmyfasthtml.core.dbmanager.DbObject- State persistence
JavaScript:
initHierarchicalCanvasGraph()- Must be loaded via MyFastHtml assets- Canvas API - For rendering
- HTMX - For server communication