Added documentation for HierarchicalCanvasGraph.py

This commit is contained in:
2026-02-22 18:12:19 +01:00
parent 0686103a8f
commit 3715954222

View File

@@ -0,0 +1,706 @@
# 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