Added documentation for HierarchicalCanvasGraph.py
This commit is contained in:
706
docs/HierarchicalCanvasGraph.md
Normal file
706
docs/HierarchicalCanvasGraph.md
Normal 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
|
||||||
Reference in New Issue
Block a user