Files
MyFastHtml/docs/HierarchicalCanvasGraph.md

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 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