codenzia/filament-diagrammer

A ReactFlow-inspired diagramming plugin for Filament 4+ with nodes, connections, and canvas

Maintainers

Package info

github.com/Codenzia/filament-diagrammer

Homepage

pkg:composer/codenzia/filament-diagrammer

Statistics

Installs: 0

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-04-16 04:43 UTC

This package is auto-updated.

Last update: 2026-05-20 07:38:01 UTC


README

A powerful, interactive node-based diagramming plugin for Filament 4+ — built on jsPlumb v6 Community Edition with schema-driven node rendering, zoom/pan, snap-to-grid, full dark mode support, and deep Filament action integration.

Requirements

  • PHP 8.3+
  • Laravel 12+
  • Filament 4.0+
  • Livewire 3.0+

Installation

composer require codenzia/filament-diagrammer

Publish the config (optional):

php artisan vendor:publish --tag=filament-diagrammer-config

Publish the assets:

php artisan vendor:publish --tag=filament-diagrammer-assets

Tailwind v4 Custom Theme

If your Filament panel uses a custom theme (Tailwind CSS v4), add the package's source paths so that utility classes used in the package's blade templates are compiled:

/* resources/css/filament/{panel}/theme.css */
@source '../../../../vendor/codenzia/*/src/**/*.php';
@source '../../../../vendor/codenzia/*/resources/views/**/*.blade.php';

This wildcard pattern covers all Codenzia packages at once. Alternatively, use package-specific paths if preferred.

Then rebuild your assets (npm run build).

Quick Start

1. Create a Diagram Page

Extend the base Diagram page or use the HasDiagram trait on any Filament page:

<?php

declare(strict_types=1);

namespace App\Filament\Pages;

use Codenzia\FilamentDiagrammer\Components\DiagramCanvas;
use Codenzia\FilamentDiagrammer\Components\DiagramConnection;
use Codenzia\FilamentDiagrammer\Components\DiagramNode;
use Codenzia\FilamentDiagrammer\Enums\ConnectionEndpoint;
use Codenzia\FilamentDiagrammer\Enums\ConnectorType;
use Codenzia\FilamentDiagrammer\Pages\Diagram;

class MyDiagram extends Diagram
{
    protected static ?string $title = 'My Diagram';

    protected function getDiagramCanvas(): DiagramCanvas
    {
        return DiagramCanvas::make()
            ->grid(enabled: true, size: 20, type: 'dots')
            ->zoom(enabled: true, min: 0.1, max: 3.0, step: 0.05)
            ->snapToGrid()
            ->defaultConnector(ConnectorType::BEZIER)
            ->nodes($this->getNodes())
            ->connections($this->getConnections());
    }

    protected function getNodes(): array
    {
        return [
            DiagramNode::make('start')
                ->label('Start')
                ->type('Process')
                ->position(100, 100)
                ->color('#22c55e')
                ->sourceEndpoints([ConnectionEndpoint::BOTTOM]),

            DiagramNode::make('end')
                ->label('End')
                ->type('Process')
                ->position(400, 300)
                ->color('#3b82f6')
                ->targetEndpoints([ConnectionEndpoint::TOP]),
        ];
    }

    protected function getConnections(): array
    {
        return [
            DiagramConnection::make('start', 'end')
                ->label('Flow')
                ->connector(ConnectorType::BEZIER)
                ->animated(),
        ];
    }
}

2. Include Assets in Your View

If using a custom Blade view instead of the built-in page:

@push('scripts')
    <script src="{{ asset('js/filament-diagrammer.js') }}"></script>
@endpush
@push('styles')
    <link rel="stylesheet" href="{{ asset('css/filament-diagrammer.css') }}">
@endpush

Node Types (Schema-Driven Nodes)

Node types define reusable schemas for node sections. Instead of hardcoding node content, define a NodeType class that provides Filament schema components for each section.

Creating a Node Type

<?php

declare(strict_types=1);

namespace App\Diagram\NodeTypes;

use Codenzia\FilamentDiagrammer\Enums\NodeShape;
use Codenzia\FilamentDiagrammer\NodeTypes\BaseNodeType;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;

class GoalNode extends BaseNodeType
{
    public static function label(): string
    {
        return 'Goal';
    }

    public static function icon(): ?string
    {
        return 'heroicon-o-flag';
    }

    public static function defaultColor(): ?string
    {
        return '#22c55e';
    }

    public static function defaultShape(): NodeShape
    {
        return NodeShape::RECTANGLE;
    }

    // Filament schema components for the body section
    public static function bodySchema(): array
    {
        return [
            TextEntry::make('description')
                ->hiddenLabel()
                ->size('xs')
                ->color('gray'),
        ];
    }

    // Filament form components for the edit slide-over
    public static function editFormSchema(): array
    {
        return [
            TextInput::make('label')->label('Title')->required(),
            Textarea::make('description')->label('Description'),
            TagsInput::make('tags')->label('Tags'),
            TextInput::make('progress')->label('Progress (%)')->numeric(),
        ];
    }
}

Using a Node Type

use App\Diagram\NodeTypes\GoalNode;

DiagramNode::make('goal-1')
    ->label('Customer Experience')
    ->type(GoalNode::class)        // Schemas resolved from class
    ->position(100, 200)
    ->data([
        'description' => 'Improve NPS score by 20 points',
        'tags' => ['Customer', 'CX'],
        'progress' => 60.0,
    ]);

When type() receives a class implementing NodeType:

  • The display label is set from NodeType::label() (shown as a badge)
  • Default color, shape, and icon are applied (unless explicitly overridden)
  • Schema getters (getBodySchema(), etc.) fall back to the NodeType's defaults
  • The edit slide-over uses NodeType::editFormSchema() if available

You can still use plain strings for simple labels: ->type('Process').

Database-Backed Nodes (Eloquent Records)

Bind Eloquent models directly to nodes using ->record(). Schema components resolve their state from the model's attributes — just like Filament infolists and tables do natively:

use App\Models\Kpi;
use App\Diagram\NodeTypes\KpiNode;

protected function getNodes(): array
{
    return Kpi::all()->map(fn (Kpi $kpi) =>
        DiagramNode::make("kpi-{$kpi->id}")
            ->label($kpi->title)
            ->type(KpiNode::class)
            ->record($kpi)                    // Components resolve from model attributes
    )->toArray();
}

With a KpiNode that defines:

public static function bodySchema(): array
{
    return [
        TextEntry::make('description'),       // Reads $kpi->description
        TagEntry::make('tags'),               // Reads $kpi->tags (relationship or cast)
    ];
}

No ->data() is needed — the record provides all the state. If you do pass ->data(), those values override the corresponding record attributes:

DiagramNode::make("kpi-{$kpi->id}")
    ->record($kpi)
    ->data(['progress' => 100.0]);            // Overrides $kpi->progress

Available Schema Sections

Each node has five schema sections — all accept arrays of Filament schema/infolist components:

Section Method Purpose
Header headerSchema([...]) Content next to the type badge and action buttons
Body bodySchema([...]) Main content area below the label
Footer footerSchema([...]) Bottom section (progress bars, metrics)
Left Sidebar leftSidebarSchema([...]) Narrow left panel
Right Sidebar rightSidebarSchema([...]) Narrow right panel

Each section also supports Filament actions: headerActions([...]), bodyActions([...]), etc.

The data array (or bound record) serves as the backing state for schema components — TextEntry::make('description') reads from $data['description'] or $record->description.

Inline Schemas (No Node Type)

All node schemas — including the edit slide-over form — can be defined directly on the node using the fluent API. No NodeType class is required:

DiagramNode::make('custom-1')
    ->label('Custom Node')
    ->type('Task')
    ->bodySchema([
        TextEntry::make('description')->hiddenLabel(),
    ])
    ->footerSchema([
        ProgressBarEntry::make('progress')->hiddenLabel(),
    ])
    ->editFormSchema([
        TextInput::make('label')->label('Title')->required(),
        Textarea::make('description')->label('Description'),
        TextInput::make('progress')->label('Progress (%)')->numeric(),
    ])
    ->data([
        'description' => 'Some task description',
        'progress' => 45.0,
    ]);

This is equivalent to defining a NodeType class — use whichever approach fits your project. The NodeType class approach is better when the same schema is reused across many nodes; the inline approach is ideal for one-off or highly varied nodes.

Class Diagram Example

A complete UML-style class diagram built entirely with the inline fluent API:

use Codenzia\FilamentDiagrammer\Components\DiagramCanvas;
use Codenzia\FilamentDiagrammer\Components\DiagramConnection;
use Codenzia\FilamentDiagrammer\Components\DiagramNode;
use Codenzia\FilamentDiagrammer\Enums\ConnectionEndpoint;
use Codenzia\FilamentDiagrammer\Enums\ConnectorType;
use Codenzia\FilamentDiagrammer\Enums\HeaderStyle;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;

protected function getDiagramCanvas(): DiagramCanvas
{
    return DiagramCanvas::make()
        ->snapToGrid()
        ->defaultConnector(ConnectorType::STRAIGHT)
        ->nodes($this->getClassNodes())
        ->connections($this->getClassConnections());
}

protected function getClassNodes(): array
{
    // Pre-compute display HTML to avoid Filament TextEntry array-state limitations
    $fmtProps = fn (array $props): string => collect($props)
        ->map(fn ($p) => '<div class="font-mono py-0.5 text-gray-700 dark:text-gray-300">'.e($p).'</div>')
        ->join('');

    $fmtMethods = fn (array $methods): string => collect($methods)
        ->map(fn ($m) => '<div class="font-mono py-0.5 text-violet-600 dark:text-violet-400">'.e($m).'</div>')
        ->join('');

    $makeNode = fn (string $id, string $label, array $properties, array $methods, float $x, float $y) =>
        DiagramNode::make($id)
            ->label($label)
            ->type('Class')
            ->color('#3b82f6')
            ->icon('heroicon-o-code-bracket')
            ->headerStyle(HeaderStyle::MAC)
            ->position($x, $y)
            ->size(260)
            ->sourceEndpoints([ConnectionEndpoint::RIGHT, ConnectionEndpoint::BOTTOM])
            ->targetEndpoints([ConnectionEndpoint::LEFT, ConnectionEndpoint::TOP])
            ->bodySchema([
                TextEntry::make('properties_html')->hiddenLabel()->html()->size('xs'),
            ])
            ->footerSchema([
                TextEntry::make('methods_html')->hiddenLabel()->html()->size('xs'),
            ])
            ->editFormSchema([
                TextInput::make('label')->label('Class Name')->required(),
                TagsInput::make('properties')->label('Properties (e.g. +id: int)'),
                TagsInput::make('methods')->label('Methods (e.g. +save(): void)'),
            ])
            ->data([
                'properties_html' => $fmtProps($properties),
                'methods_html'    => $fmtMethods($methods),
                'properties'      => $properties,  // Used by the edit form
                'methods'         => $methods,      // Used by the edit form
            ]);

    return [
        $makeNode('user',      'User',      ['+id: int', '+name: string', '+email: string'], ['+orders(): HasMany', '+save(): void'],  80,  60),
        $makeNode('order',     'Order',     ['+id: int', '+user_id: int FK', '+total: decimal'], ['+items(): HasMany', '+user(): BelongsTo'], 420,  60),
        $makeNode('product',   'Product',   ['+id: int', '+name: string', '+price: decimal'], ['+orderItems(): HasMany', '+save(): void'],  80, 460),
        $makeNode('orderitem', 'OrderItem', ['+id: int', '+order_id: int FK', '+product_id: int FK'], ['+order(): BelongsTo', '+product(): BelongsTo'], 420, 460),
    ];
}

protected function getClassConnections(): array
{
    return [
        DiagramConnection::make('user',    'order')     ->label('1 : N')->connector(ConnectorType::STRAIGHT)->color('#6b7280')->width(1),
        DiagramConnection::make('order',   'orderitem') ->label('1 : N')->connector(ConnectorType::STRAIGHT)->color('#6b7280')->width(1),
        DiagramConnection::make('product', 'orderitem') ->label('1 : N')->connector(ConnectorType::STRAIGHT)->color('#6b7280')->width(1),
    ];
}

Note on pre-computed HTML: Filament's TextEntry converts array state to a string ("Array") before passing it to formatStateUsing. For array data that should render as HTML, pre-compute the display HTML at node-creation time and store it in a dedicated field (e.g., properties_html). Use TextEntry::make('properties_html')->html() to render it. Keep the raw arrays alongside for use by the edit form (TagsInput, Repeater, etc.).

API Reference

DiagramCanvas

The canvas is the main container for the diagram surface, controlling layout, theming, interactivity, and all global settings.

DiagramCanvas::make()
    // ─── Layout ───────────────────────────────────────────────
    ->height('70vh')
    ->background('#f8fafc')                   // Light mode background (null = auto)
    ->canvasDarkBackground('#1a1b1e')         // Dark mode background (null = auto)

    // ─── Grid ─────────────────────────────────────────────────
    ->grid(enabled: true, size: 20, color: null, type: 'dots')
    ->gridDarkColor('#2a2b2e')                // Grid color for dark mode
    ->snapToGrid(true)

    // ─── Zoom & Pan ──────────────────────────────────────────
    ->zoom(enabled: true, min: 0.1, max: 3.0, step: 0.05, default: 1.0)
    ->panEnabled(true)

    // ─── Toolbar ─────────────────────────────────────────────
    ->toolbar(enabled: true, position: 'top')
    ->undoRedo(true)                          // Undo/redo buttons (Ctrl+Z / Ctrl+Y)
    ->save(true)                              // Save button (Ctrl+S) with dirty indicator
    ->export(true)                            // Export dropdown (PNG/PDF with theme choice)
    ->zoomControls(true)
    ->zoomControlsPosition(ZoomControlsPosition::BOTTOM_RIGHT)  // TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, FLOAT, DOCKED
    ->fitView(true)
    ->fitViewPadding(60)                      // Padding around nodes when fitting

    // ─── Interactivity ───────────────────────────────────────
    ->readOnly(false)
    ->keyboardShortcuts(true)
    ->multiSelect(true)
    ->interactionModeToggle(true)             // Show pan/select mode toggle in toolbar
    ->defaultInteractionMode('pan')           // 'pan' or 'select' — default left-click drag behavior
    ->dragToConnect()                          // Enable drag port-to-port connections
    ->newConnectionDefaults([...])            // Default props for new connections
    ->autoOpenConnectionEditor()              // Open editor after new connection
    ->defaultConnector(ConnectorType::BEZIER)

    // ─── Context Menu ────────────────────────────────────────
    ->contextMenu(true)                       // Right-click context menu
    ->contextMenuItems([                      // Custom menu items
        ContextMenuItem::make('export-node')
            ->label('Export as JSON')
            ->icon('heroicon-o-arrow-down-tray')
            ->group('node')
            ->wireMethod('exportNodeAsJson'),
    ])

    // ─── Alignment Guides ────────────────────────────────────
    ->alignmentGuides(true)                   // Snap-to-alignment guides during drag
    ->alignmentThreshold(5)                   // Snap distance in pixels

    // ─── Minimap ─────────────────────────────────────────────
    ->minimap(true)                           // Show minimap overview
    ->minimapPosition('bottom-right')         // bottom-right, bottom-left, top-right, top-left

    // ─── Connection Styling ──────────────────────────────────
    ->connectionColor('#9ca3af')              // Light mode stroke
    ->connectionDarkColor('#6b7280')          // Dark mode stroke
    ->connectionHoverColor('#f59e0b')         // Hover highlight
    ->connectionHoverWidth(3)                 // Stroke width on hover
    ->connectionArrowSize(10)                 // Arrow head size
    ->connectionArrowFoldback(0.8)            // 0.0 = flat, 1.0 = sharp point

    // ─── Endpoints ───────────────────────────────────────────
    ->endpointType('Dot')                     // 'Dot', 'Rectangle', or 'Blank'
    ->endpointRadius(2)

    // ─── Selection ───────────────────────────────────────────
    ->selectionColor('#f59e0b')               // Border/glow color for selected nodes

    // ─── Node Defaults ───────────────────────────────────────
    ->nodeDefaultWidth(240)
    ->nodeDefaultHeight(200)                   // null = auto (content-driven)
    ->contentSizing('auto')                    // 'auto', 'fit', or 'clip'

    // ─── Auto Layout ─────────────────────────────────────────
    ->autoLayoutHGap(60)                      // Horizontal gap between node edges (px)
    ->autoLayoutVGap(60)                      // Vertical gap between levels (px)

    // ─── Data ────────────────────────────────────────────────
    ->nodes([...])
    ->connections([...])

    // ─── Actions (Filament Actions) ──────────────────────────
    ->headerActions([...])
    ->toolbarActions([...])
    ->footerActions([...])

    // ─── Lifecycle Callbacks ─────────────────────────────────
    ->onNodeCreate(fn ($nodeId) => ...)
    ->onNodeDelete(fn ($nodeId) => ...)
    ->onNodeMove(fn ($nodeId, $x, $y) => ...)
    ->onConnectionCreate(fn ($sourceId, $targetId) => ...)
    ->onConnectionDelete(fn ($sourceId, $targetId) => ...)
    ->onSave(fn ($nodes, $connections) => ...);

DiagramNode

Nodes represent items on the canvas. Each node supports structured sections (header, body, footer, sidebars) with Filament schema-based content and actions.

DiagramNode::make('node-1')
    // ─── Basic ───────────────────────────────────────────────
    ->label('My Node')
    ->type(GoalNode::class)                   // NodeType class or plain string
    ->position(100, 200)                      // Optional — auto-layout if omitted
    ->size(width: 240, height: null)          // Height auto-sizes by default
    ->autoFit()                               // Width adapts to content
    ->shape(NodeShape::RECTANGLE)
    ->color('#3b82f6')                        // Accent color (header tint)
    ->borderColor('#e5e7eb')
    ->icon('heroicon-o-bolt')

    // ─── Header Styling ──────────────────────────────────────
    ->headerBackgroundColor('#1e40af')        // Custom header background
    ->headerStyle(HeaderStyle::MAC)           // none, windows, mac, custom
    ->footerBackgroundColor('#f1f5f9')        // Custom footer background

    // ─── Behavior ────────────────────────────────────────────
    ->draggable(true)                         // Can user drag this node?
    ->selectable(true)                        // Can user click to select?
    ->deletable(true)                         // Show X button & allow Delete key?
    ->connectable(true)                       // Show connection endpoints?
    ->resizable(false)                        // Allow resize handles?

    // ─── Connection Endpoints ────────────────────────────────
    ->sourceEndpoints([ConnectionEndpoint::BOTTOM, ConnectionEndpoint::RIGHT])
    ->targetEndpoints([ConnectionEndpoint::TOP, ConnectionEndpoint::LEFT])

    // ─── Sections (Filament Schemas) ─────────────────────────
    ->headerSchema([...])                     // Or resolved from NodeType
    ->bodySchema([...])
    ->footerSchema([...])
    ->leftSidebarSchema([...])
    ->rightSidebarSchema([...])
    ->editFormSchema([...])                   // Edit slide-over form (inline, no NodeType needed)

    // ─── Section Actions ─────────────────────────────────────
    ->headerActions([...])
    ->footerActions([...])

    // ─── Data (backing state for schemas) ────────────────────
    ->record($eloquentModel)                  // Bind an Eloquent model directly
    ->data([                                  // Or provide data manually (overrides record)
        'description' => 'Some description',
        'tags' => ['Tag1', 'Tag2'],
        'progress' => 75.0,
    ])

    // ─── Callbacks ───────────────────────────────────────────
    ->onClick(fn ($nodeId) => ...)
    ->onDoubleClick(fn ($nodeId) => ...)

    // ─── Extra HTML Attributes ───────────────────────────────
    ->extraAttributes(['data-priority' => 'high']);

Optional Position (Auto Layout)

When position() is not called, the canvas auto-layout manager positions the node using a top-down tree layout based on connections. Unconnected nodes are placed left-to-right. The layout uses autoLayoutHGap and autoLayoutVGap from the canvas config as edge-to-edge gaps — i.e., the space between node borders, not between node centers.

// These nodes will be auto-positioned
DiagramNode::make('a')->label('A')->type(GoalNode::class),
DiagramNode::make('b')->label('B')->type(GoalNode::class),
DiagramNode::make('c')->label('C')->type(GoalNode::class),

Auto-Fit Sizing

When autoFit() is enabled, the node width adapts to its content instead of using a fixed width:

DiagramNode::make('flexible')
    ->label('Auto-sized')
    ->autoFit();

Header Styles

Control the decorative chrome on node headers:

Style Description
HeaderStyle::NONE No decoration (default)
HeaderStyle::MAC macOS traffic light dots (red/yellow/green)
HeaderStyle::WINDOWS Windows-style minimize/maximize/close buttons
HeaderStyle::CUSTOM Only user's headerSchema renders
DiagramNode::make('styled')
    ->headerStyle(HeaderStyle::MAC)
    ->headerBackgroundColor('#1e293b');

Available Shapes

Shape Enum Value
Rectangle NodeShape::RECTANGLE
Rounded Rectangle NodeShape::ROUNDED_RECTANGLE
Circle NodeShape::CIRCLE
Ellipse NodeShape::ELLIPSE
Diamond NodeShape::DIAMOND
Triangle NodeShape::TRIANGLE
Hexagon NodeShape::HEXAGON
Parallelogram NodeShape::PARALLELOGRAM

Content Sizing for Non-Rectangular Shapes

When using non-rectangular shapes (circle, diamond, hexagon, etc.), content may overflow the visible shape boundary. The contentSizing canvas setting controls how this is handled:

Mode Description
auto Natural flow — content renders at full size, may overflow non-rectangular shapes (default)
fit Shape grows to encompass the content — the node width/height define the content rectangle, and the shape is calculated to fully inscribe it. Requires nodeDefaultHeight to be set.
clip Content is clipped at the shape boundary via overflow: hidden
// "Fit" mode: width × height define content area, circle grows to contain it
DiagramCanvas::make()
    ->nodeDefaultWidth(240)
    ->nodeDefaultHeight(200)
    ->contentSizing('fit')    // Circle diameter = √(240² + 200²) ≈ 313px
    ->nodes([
        DiagramNode::make('process')
            ->label('My Process')
            ->shape(NodeShape::CIRCLE),
    ]);

The fit mode uses NodeShape::outerDimensions() to calculate the exact shape dimensions needed. For a circle, the diameter equals the diagonal of the content rectangle (√(w² + h²)). For a diamond, the outer dimensions are 2× content width and 2× content height.

DiagramConnection

Connections are edges between two nodes. Each connection supports per-connection styling for color, width, arrows, and hover behavior.

DiagramConnection::make('source-node-id', 'target-node-id')
    // ─── Basic ───────────────────────────────────────────────
    ->label('100%')
    ->value(100)                              // Arbitrary value for callbacks
    ->connector(ConnectorType::BEZIER)

    // ─── Styling ─────────────────────────────────────────────
    ->color('#f59e0b')                        // Stroke color
    ->width(2)                                // Stroke width
    ->animated(true)                          // Dashed flowing animation
    ->hoverColor('#ef4444')                   // Per-connection hover color

    // ─── Arrows ──────────────────────────────────────────────
    ->arrowSize(10)                           // Arrow head size
    ->arrowFoldback(0.8)                      // 0.0 = flat, 1.0 = sharp point

    // ─── Label ───────────────────────────────────────────────
    ->labelPosition(0.5)                      // 0.0 = at source, 1.0 = at target

    // ─── Endpoints ───────────────────────────────────────────
    ->sourceEndpoint(ConnectionEndpoint::BOTTOM)
    ->targetEndpoint(ConnectionEndpoint::TOP)

    // ─── Behavior ────────────────────────────────────────────
    ->deletable(true)
    ->editable(true)                          // Allow label editing via double-click

    // ─── Custom Data ─────────────────────────────────────────
    ->data(['weight' => 0.5])
    ->extraAttributes(['data-type' => 'dependency'])

    // ─── Callbacks ───────────────────────────────────────────
    ->onConnect(fn ($sourceNode, $targetNode) => ...)
    ->onDisconnect(fn ($sourceNode, $targetNode) => ...)
    ->onClick(fn ($connection) => ...);

Connector Types

Type Description
ConnectorType::BEZIER Smooth bezier curve
ConnectorType::STRAIGHT Direct straight line
ConnectorType::STEP Right-angle step path
ConnectorType::SMOOTHSTEP Rounded right-angle path
ConnectorType::FLOWCHART Vertical flowchart routing (default)

Connection Endpoints

Endpoint Description
ConnectionEndpoint::TOP Top center of the node
ConnectionEndpoint::RIGHT Right center of the node
ConnectionEndpoint::BOTTOM Bottom center of the node
ConnectionEndpoint::LEFT Left center of the node
ConnectionEndpoint::AUTO jsPlumb auto-selects best anchor

NodeType Interface

Create custom node types by implementing NodeType or extending BaseNodeType:

use Codenzia\FilamentDiagrammer\Contracts\NodeType;
use Codenzia\FilamentDiagrammer\Enums\NodeShape;

interface NodeType
{
    public static function label(): string;
    public static function description(): ?string;  // Shown in palette help tooltip
    public static function icon(): ?string;
    public static function defaultColor(): ?string;
    public static function defaultShape(): NodeShape;
    public static function defaultWidth(): ?float;
    public static function headerSchema(): array;
    public static function bodySchema(): array;
    public static function footerSchema(): array;
    public static function leftSidebarSchema(): array;
    public static function rightSidebarSchema(): array;
    public static function editFormSchema(): array;
}

BaseNodeType provides empty defaults for all methods — only override what you need.

Palette Help Tooltips

When description() returns a non-null string, the palette sidebar shows a ? icon on hover. Hovering or clicking it displays a tooltip with the node type's description. This helps users understand what each node type does before dragging it onto the canvas.

HasDiagram Trait

Add diagram functionality to any Filament page:

use Codenzia\FilamentDiagrammer\Concerns\HasDiagram;

class MyPage extends Page implements HasActions, HasForms
{
    use HasDiagram;
    use InteractsWithActions;
    use InteractsWithForms;

    // Required: define the canvas configuration
    protected function getDiagramCanvas(): DiagramCanvas { ... }

    // Optional: customize the node edit form (opens in slide-over)
    // Note: if a node has a NodeType with editFormSchema(), that is used instead
    protected function getNodeFormSchema(): array
    {
        return [
            TextInput::make('title')->required(),
            Textarea::make('description'),
            Select::make('priority')->options([...]),
        ];
    }

    // Optional: customize the connection edit form
    protected function getConnectionFormSchema(): array
    {
        return [
            TextInput::make('label'),
            TextInput::make('value')->numeric(),
        ];
    }

    // Optional: persist node changes to database
    protected function handleNodeSave(array $data, ?string $nodeId = null): void
    {
        // Save to your model
    }

    // Optional: persist connection changes
    protected function handleConnectionSave(array $data, ?string $connectionId = null): void
    {
        // Save to your model
    }

    // Optional: handle node deletion
    protected function handleNodeDelete(string $nodeId): void
    {
        // Delete from your model
    }
}

Available Trait Methods

Method Description
initializeDiagram() Initialize diagram from canvas config
refreshDiagram() Rebuild diagram with current config (dispatches JS event)
getDiagramNode($nodeId) Get a DiagramNode PHP object by ID (for schema rendering)
renderNodeSection($nodeId, $section) Render a node section using its Filament schema
getRenderedNodeHtml($nodeId) Get full rendered HTML for a node (for JS refresh)
onNodeMoved($nodeId, $x, $y) Called when a node is dragged
onNodeSelected($nodeIds) Called when nodes are selected
onNodeDeleted($nodeId) Called when a node is deleted
onConnectionCreated(...) Called when a connection is created
onConnectionDeleted($id) Called when a connection is removed
openNodeEditor($nodeId) Open the node edit slide-over
openConnectionEditor($id) Open the connection edit slide-over
saveDiagram($nodes, $connections) Save full diagram state
duplicateNodes($nodeIds) Duplicate selected nodes with offset
disconnectAllConnections($nodeId) Remove all connections to/from a node
changeNodeColor($nodeId, $color) Change a node's accent color
updateConnectionLabel($id, $label) Update a connection's label text
updateConnectionConnector($id, $connector) Change a connection's routing style

Reusable Canvas Component

Include the diagram canvas in any Blade view:

@php
    $canvasConfig = $this->diagramCanvasConfig;
    $nodes = $this->diagramNodes;
    $connections = $this->diagramConnections;
@endphp

@include('filament-diagrammer::filament.components.diagram-canvas')

Theming

CSS Custom Properties

The plugin uses CSS custom properties for theming, set automatically from your PHP configuration. You can override them in your CSS:

.filament-diagrammer {
    --diagrammer-canvas-bg: #f8fafc;
    --diagrammer-canvas-dark-bg: #1a1b1e;
    --diagrammer-grid-color: #d1d5db;
    --diagrammer-grid-dark-color: #2a2b2e;
    --diagrammer-grid-size: 20px;
    --diagrammer-selection-color: #f59e0b;
    --diagrammer-connection-hover-width: 3;
}

Dark Mode

The plugin fully supports Filament's light and dark modes. Colors auto-adapt using pure CSS (.dark class selector) — no JavaScript theme detection required.

Property Light Default Dark Default
Canvas background #f8fafc #1a1b1e
Grid dots #d1d5db #2a2b2e
Connection stroke #9ca3af #6b7280
Selection glow #f59e0b (amber) #f59e0b (amber)

To set custom colors per mode:

DiagramCanvas::make()
    ->background('#ffffff')                   // Light mode
    ->canvasDarkBackground('#0f172a')         // Dark mode
    ->gridColor('#e5e7eb')                    // Light grid
    ->gridDarkColor('#1e3a5f');               // Dark grid

Undo / Redo

Client-side history tracking for node moves, connection creates, and connection deletes. The undo stack holds up to 50 entries.

DiagramCanvas::make()
    ->undoRedo(true);   // Enabled by default

Tracked actions:

  • Node move — dragging a node records old/new position
  • Connection create — creating a connection via endpoint drag
  • Connection delete — deleting a selected connection

Toolbar buttons show Undo/Redo with disabled state. Keyboard: Ctrl+Z (undo), Ctrl+Y or Ctrl+Shift+Z (redo).

Save Changes

The Save button appears in the toolbar with a pulsing amber dot when there are unsaved changes. Clicking it (or pressing Ctrl+S) calls saveDiagram() which collects all node positions and connections, sends them to the server via $wire.saveDiagram(), and triggers the onSave callback.

DiagramCanvas::make()
    ->save(true)        // Enabled by default
    ->onSave(fn (array $nodes, array $connections) => /* persist to DB */);

The onSave callback receives the full node array (with current x/y positions) and connections. Persistence is the application's responsibility — the plugin provides the callback, you decide the storage mechanism:

// Eloquent model with a JSON column
->onSave(fn ($nodes, $connections) => $this->diagram->update([
    'state' => ['nodes' => $nodes, 'connections' => $connections],
]))

// Simple JSON file (suitable for prototypes / demos)
->onSave(fn ($nodes, $connections) => file_put_contents(
    storage_path('app/diagram.json'),
    json_encode(compact('nodes', 'connections'))
))

The dirty indicator tracks any mutation: node drags, connection creates/deletes, or node deletions.

Export (PNG / PDF)

Export the diagram as a high-resolution PNG image or PDF document. The export captures nodes, connection lines, arrows, and labels. All assets are bundled — no CDN required, works in private networks.

DiagramCanvas::make()
    ->export(true);     // Enabled by default

The toolbar Export dropdown offers two formats: Download PNG and Download PDF. The export theme (current / light / dark) is controlled via the Settings slide-over.

The theme option temporarily switches the page's color scheme before capture, allowing you to export a light diagram while in dark mode (or vice versa).

Programmatic Export

From JavaScript/Alpine, call the exportAs method directly:

// Export as PNG with current theme
exportAs('png', 'current');

// Export as PDF in light mode
exportAs('pdf', 'light');

// Export as PNG in dark mode
exportAs('png', 'dark');

Connection Editing

Click to Edit

Single-click a connection line to select it (highlighted with the selection color). Double-click opens the connection edit slide-over via openConnectionEditor().

Create Connections

Drag from a source endpoint to a target endpoint to create a new connection. The server is notified via onConnectionCreated().

Delete Connections

Select a connection by clicking it, then press Delete or Backspace. The server is notified via onConnectionDeleted().

Connection Edit Form

Customize the connection edit form in your page class:

protected function getConnectionFormSchema(): array
{
    return [
        TextInput::make('label'),
        TextInput::make('value')->numeric(),
        Select::make('connector')->options([
            'bezier' => 'Bezier',
            'straight' => 'Straight',
            'flowchart' => 'Flowchart',
        ]),
    ];
}

Settings Slide-Over

A cog icon on the right of the toolbar opens a Filament slide-over for live canvas customization — no page reload required. Changes take effect immediately and trigger a diagram refresh.

Available settings:

Section Settings
Grid Show/hide grid, grid style (dots/lines), grid size, snap to grid
Connections Default connector type, arrow size, line width
Zoom Controls Position of the zoom controls panel
Export Default export theme (current / light / dark)

The settings slide-over is provided by HasDiagram out of the box. To suppress it, remove it from the toolbar by not calling diagramSettingsAction() in your page's actions.

Zoom Controls Position

Position the zoom controls anywhere on the canvas, dock them to the toolbar, or make them draggable:

use Codenzia\FilamentDiagrammer\Enums\ZoomControlsPosition;

DiagramCanvas::make()
    ->zoomControlsPosition(ZoomControlsPosition::BOTTOM_RIGHT);  // Default
Position Description
ZoomControlsPosition::TOP_LEFT Fixed top-left corner
ZoomControlsPosition::TOP_RIGHT Fixed top-right corner
ZoomControlsPosition::BOTTOM_LEFT Fixed bottom-left corner
ZoomControlsPosition::BOTTOM_RIGHT Fixed bottom-right corner (default)
ZoomControlsPosition::FLOAT Draggable — user can reposition by grabbing the drag handle
ZoomControlsPosition::DOCKED Embedded inline in the toolbar (zoom −/+/fit/reset/fullscreen shown horizontally)

Fullscreen Mode

Toggle fullscreen with the toolbar button or F11. Uses CSS-based fullscreen (position: fixed) instead of the browser Fullscreen API, so Filament modals and slide-overs remain visible on top of the diagram.

Context Menu

Right-click on nodes, connections, or the canvas to access context-specific actions. The context menu is built-in and respects all permission flags (readOnly, deletable, connectable, etc.).

DiagramCanvas::make()
    ->contextMenu(true)  // Enabled by default

Built-in Menu Items

Node context menu:

  • Duplicate — clones the node with offset (Ctrl+D)
  • Copy — copies node to clipboard
  • Bring to Front / Send to Back — z-index management
  • Connect to... — sub-menu listing available target nodes
  • Disconnect All — removes all connections to/from this node
  • Settings... — opens the node edit slide-over (also accessible via double-click)
  • Delete — removes the node (requires deletable flag)

Double-click on any node opens the settings editor directly — equivalent to the "Settings..." context menu item.

Connection context menu:

  • Edit Connection — opens the connection edit slide-over (also accessible via double-click on connection label)
  • Change Style — sub-menu with connector types (Bezier, Straight, Step, SmoothStep, Flowchart)
  • Delete Connection — removes the connection

Canvas context menu:

  • Paste — paste copied node (if clipboard has content)
  • Select All — select all nodes (Ctrl+A)
  • Fit View — fit all nodes in viewport
  • Reset Zoom — reset to 100%

Custom Menu Items

Add your own items using ContextMenuItem:

use Codenzia\FilamentDiagrammer\Components\ContextMenuItem;

DiagramCanvas::make()
    ->contextMenuItems([
        ContextMenuItem::make('export-json')
            ->label('Export as JSON')
            ->icon('heroicon-o-arrow-down-tray')
            ->group('node')                    // 'node', 'connection', or 'canvas'
            ->wireMethod('exportNodeAsJson')   // Livewire method to call
            ->shortcut('Ctrl+E')
            ->sort(50),

        ContextMenuItem::make('highlight')
            ->label('Highlight Node')
            ->group('node')
            ->wireMethod('highlightNode'),
    ]);

Multi-Select

Select multiple nodes for bulk operations. Enabled by default.

DiagramCanvas::make()
    ->multiSelect(true)

Selection Methods

Method Description
Click Select a single node (deselects others)
Shift+Click / Ctrl+Click Toggle node in/out of selection
Rubber-band Left-drag on empty canvas to draw selection rectangle
Ctrl+A Select all nodes
Escape Deselect all

Multi-Drag

When multiple nodes are selected, dragging any selected node moves the entire group. Position changes are tracked per-node for undo/redo.

Interaction Mode (Pan vs Select)

The toolbar includes a Pan/Select mode toggle that controls what left-click drag does on the canvas background:

Mode Left-click drag Override
Pan (default) Pans the canvas Hold Shift to rubber-band select
Select Rubber-band selection Hold Space to pan

Middle mouse button always pans regardless of mode.

Keyboard shortcuts: Press H for pan mode, V for select mode (Figma convention).

DiagramCanvas::make()
    ->interactionModeToggle(true)         // Show the toggle (default: true)
    ->defaultInteractionMode('pan')       // 'pan' or 'select' (default: 'pan')

Set interactionModeToggle(false) to hide the toggle from the toolbar. The H/V keyboard shortcuts still work.

Alignment Guides

Snap-to-alignment guides appear when dragging nodes, helping you align them with other nodes on the canvas. Guides appear for edge and center alignment within a configurable threshold.

DiagramCanvas::make()
    ->alignmentGuides(true)     // Disabled by default
    ->alignmentThreshold(5)     // Snap distance in pixels (default: 5)

Guides are shown as thin blue lines across the canvas during drag operations and automatically hide on drop.

Minimap

A miniature overview of the entire canvas, showing node positions, connections, and the current viewport. Click or drag on the minimap to navigate the main canvas.

DiagramCanvas::make()
    ->minimap(true)                            // Disabled by default
    ->minimapPosition('bottom-right')          // bottom-right, bottom-left, top-right, top-left

The minimap automatically updates when nodes are moved, connections change, or the viewport is panned/zoomed.

Dynamic Connection Creation

Create connections by dragging from a node's source port to another node's target port.

DiagramCanvas::make()
    ->dragToConnect()           // Disabled by default

When enabled, port elements are added to each node based on its sourceEndpoints and targetEndpoints configuration. Ports appear on hover and during drag operations, with visual feedback (scale, glow) to indicate interactivity.

Node-level connectable flag controls whether ports are shown:

DiagramNode::make('locked-node')
    ->connectable(false)        // No connection ports shown

New Connection Defaults

Configure default properties for connections created via drag or "Connect to..." context menu. Accepts a static array or a Closure that receives ($sourceId, $targetId):

// Static defaults
DiagramCanvas::make()
    ->newConnectionDefaults([
        'connector' => 'bezier',
        'color' => '#6b7280',
        'width' => 2,
        'animated' => false,
        'arrowSize' => 10,
        'arrowFoldback' => 0.8,
        'label' => null,
        'value' => null,
    ]);

// Dynamic defaults via Closure (resolved server-side per connection)
DiagramCanvas::make()
    ->newConnectionDefaults(fn (string $sourceId, string $targetId) => [
        'connector' => 'bezier',
        'color' => str_starts_with($sourceId, 'critical') ? '#ef4444' : '#6b7280',
        'label' => "Link: {$sourceId}{$targetId}",
        'animated' => true,
    ]);

Supported keys: label, value, connector, color, width, animated, arrowSize, arrowFoldback, labelPosition, hoverColor, deletable, editable, data.

When a Closure is used, the defaults are resolved on the server after the connection is created and the full connection data is dispatched back to the frontend for rendering.

Auto-Open Connection Editor

Automatically open the connection editor slide-over after a new connection is created, so users can immediately set label, value, or other properties:

DiagramCanvas::make()
    ->autoOpenConnectionEditor()
    ->newConnectionDefaults([
        'connector' => 'bezier',
    ]);

This pairs well with getConnectionFormSchema() — the editor opens with whatever form you've defined:

protected function getConnectionFormSchema(): array
{
    return [
        \Filament\Forms\Components\Slider::make('value')
            ->label('Contribution %')
            ->range(0, 100)
            ->step(5)
            ->tooltips(),
    ];
}

Clickable Connection Labels

Connection labels are interactive — click to select the connection, double-click to open the connection editor. Labels with editable(false) are still clickable for selection but won't open the editor on double-click.

DiagramConnection::make('a', 'b')
    ->label('Relationship')
    ->editable(true)            // Allow double-click editing (default: true)

Keyboard Shortcuts

Shortcut Action
Ctrl+Z Undo last action
Ctrl+Y / Ctrl+Shift+Z Redo last undone action
Ctrl+S Save diagram changes
Delete / Backspace Delete selected node(s) or connection
Ctrl+D Duplicate selected node(s)
Ctrl+0 Reset zoom to default
Ctrl+Shift+F Fit all nodes in view
Ctrl+A Select all nodes
Escape Deselect all / cancel operation
Arrow keys Nudge selected node(s) by 1px
Shift+Arrow keys Nudge selected node(s) by grid size
F11 Toggle fullscreen

Keyboard shortcuts can be disabled per-canvas:

DiagramCanvas::make()->keyboardShortcuts(false);

Configuration

The full config file (config/filament-diagrammer.php):

return [
    'assets' => [
        'css' => ['css/filament-diagrammer.css'],
        'js' => ['js/filament-diagrammer.js'],
    ],

    'canvas' => [
        'height' => '80vh',
        'background' => null,               // null = auto (follows theme)
        'grid' => [
            'enabled' => true,
            'size' => 20,
            'color' => null,                // null = auto
            'type' => 'dots',               // dots, lines, cross
        ],
        'zoom' => [
            'enabled' => true,
            'min' => 0.1,
            'max' => 3.0,
            'step' => 0.05,
            'default' => 1.0,
        ],
        'pan' => ['enabled' => true],
        'snap_to_grid' => true,
    ],

    'node' => [
        'width' => 240,
        'min_width' => 120,
        'max_width' => 600,
        'shape' => 'rectangle',
        'draggable' => true,
        'selectable' => true,
        'resizable' => false,
        'deletable' => true,
    ],

    'connection' => [
        'connector' => 'bezier',
        'color' => '#6b7280',
        'width' => 2,
        'animated' => false,
        'deletable' => true,
        'arrow_size' => 8,
        'label_position' => 0.5,
    ],

    'toolbar' => [
        'enabled' => true,
        'position' => 'top',
        'undo_redo' => true,
        'save' => true,
        'export' => true,
        'zoom_controls' => true,
        'zoom_controls_position' => 'bottom-right',  // top-left, top-right, bottom-left, bottom-right, float, docked
        'fit_view' => true,
        'minimap' => false,
    ],

    'read_only' => false,
    'keyboard_shortcuts' => true,
    'multi_select' => true,
    'drag_to_connect' => false,             // drag port-to-port connections
    'auto_open_connection_editor' => false,  // open editor after new connection
    'new_connection_defaults' => [],         // default props for new connections
    'context_menu' => true,
    'alignment_guides' => false,
    'alignment_threshold' => 5,             // px
];

Architecture

How It Works

  • Nodes are server-rendered by Blade @foreach (not Alpine x-for) — this enables full Filament component/action support inside nodes
  • Node sections (header, body, footer, sidebars) are rendered using Schema::make()->components()->state()->toEmbeddedHtml() — Filament's SDUI rendering pipeline
  • NodeType classes provide default schemas per node type; inline schemas override NodeType defaults
  • jsPlumb v6 (Community Edition) handles drag, connection rendering, endpoints, and repainting via @jsplumb/browser-ui
  • Alpine.js is used only for toolbar state (zoom %, grid toggle)
  • A wire:ignore directive on the nodes layer prevents Livewire from interfering with jsPlumb's DOM
  • Config changes dispatch a diagram-refreshed Livewire event, which the JS listens for to update with new settings
  • After editing a node via the slide-over, diagram-node-updated dispatches fresh HTML for the JS to replace the node content

Dual Storage

The HasDiagram trait maintains two representations of nodes:

  • $diagramNodes (public array) — serialized arrays for Livewire/JS (positions, IDs, structural data)
  • $diagramNodeObjects (protected array) — original DiagramNode PHP objects for Blade-time schema rendering

This is necessary because Filament schema components are PHP objects that cannot be serialized to Livewire wire format.

Dynamic Updates

When properties change at runtime (e.g., toggling draggable/selectable/deletable via Livewire), the plugin:

  1. Dispatches diagram-refreshed with the new node data, connections, and config
  2. JS updates DOM data attributes (data-draggable, data-selectable, data-deletable) since wire:ignore prevents Livewire DOM morphing
  3. Shows/hides the delete (X) button, clears stale selections
  4. Tears down and rebuilds the jsPlumb instance with the new config
  5. Uses AbortController to cleanly remove old event listeners before attaching new ones

When a node is edited via the slide-over:

  1. Both the serialized array and the PHP object are updated with the new data
  2. diagram-node-updated is dispatched with the node ID and fresh rendered HTML
  3. JS replaces the node's .diagram-node-inner content and revalidates jsPlumb

Output Ports

Nodes can define labeled output connectors for branching flows (e.g., "Yes"/"No" on condition nodes).

Defining Output Ports on a NodeType

class ConditionNodeType extends BaseNodeType
{
    public static function outputPorts(): array
    {
        return [
            ['id' => 'yes', 'label' => 'Yes', 'position' => 'BOTTOM_LEFT', 'color' => '#22c55e'],
            ['id' => 'no', 'label' => 'No', 'position' => 'BOTTOM_RIGHT', 'color' => '#ef4444'],
        ];
    }
}

Setting Output Ports on a DiagramNode

$node = DiagramNode::make('condition-1')
    ->type(ConditionNodeType::class)
    ->outputPorts([
        ['id' => 'yes', 'label' => 'Yes', 'position' => 'BOTTOM_LEFT', 'color' => '#22c55e'],
        ['id' => 'no', 'label' => 'No', 'position' => 'BOTTOM_RIGHT', 'color' => '#ef4444'],
    ]);

Output ports are rendered as small colored badges near the node endpoints. When connecting from a multi-port node, the sourcePort is tracked on the connection.

Connection Roles (sourcePort)

Connections can carry a sourcePort property indicating which output port they originated from:

$connection = DiagramConnection::make('node-1', 'node-2')
    ->sourcePort('yes')
    ->label('Yes');

This is set automatically when connecting from a node with output ports. Use it to determine which branch a connection represents in workflow engines.

Connection Validation (beforeConnectionCreate)

Validate or reject connections before they're committed:

$canvas = DiagramCanvas::make()
    ->beforeConnectionCreate(function (string $sourceId, string $targetId, ?string $sourcePort): bool {
        // Return false to reject the connection
        if ($sourceId === $targetId) {
            return false;
        }
        return true;
    });

The callback is evaluated server-side via Livewire. If it returns false, the connection is rejected and not created.

Node Type Constraints

NodeType definitions can declare connection constraints:

class TriggerNodeType extends BaseNodeType
{
    // Only accept connections from these node types
    public static function allowedInputTypes(): array
    {
        return []; // empty = accept any (default)
    }

    // Only allow connections to these node types
    public static function allowedOutputTypes(): array
    {
        return [ConditionNodeType::class, ActionNodeType::class];
    }

    // Limit the number of incoming connections
    public static function maxInputConnections(): ?int
    {
        return 0; // trigger nodes have no inputs
    }

    // Limit the number of outgoing connections
    public static function maxOutputConnections(): ?int
    {
        return null; // null = unlimited
    }
}

Constraints are enforced both client-side (via jsPlumb's beforeDrop) and server-side (via the validateConnection() method in HasDiagram).

Node Palette

Enable a drag-from-sidebar palette for creating nodes:

$canvas = DiagramCanvas::make()
    ->palette([
        TriggerNodeType::class,
        ConditionNodeType::class,
        ActionNodeType::class,
    ]);

The palette renders registered NodeTypes as draggable items with their icon, color, and label. Users drag items from the palette onto the canvas to create new nodes at the drop position.

Position the palette on the left (default) or right:

$canvas = DiagramCanvas::make()
    ->palette(nodeTypes: [...], position: 'right');

License

Proprietary - Codenzia

Credits