codenzia / filament-diagrammer
A ReactFlow-inspired diagramming plugin for Filament 4+ with nodes, connections, and canvas
Requires
- php: ^8.3
- filament/filament: ^4.0
- livewire/livewire: ^3.0
Requires (Dev)
- orchestra/testbench: ^9.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
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
TextEntryconverts array state to a string ("Array") before passing it toformatStateUsing. 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). UseTextEntry::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
deletableflag)
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 Alpinex-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:ignoredirective on the nodes layer prevents Livewire from interfering with jsPlumb's DOM - Config changes dispatch a
diagram-refreshedLivewire event, which the JS listens for to update with new settings - After editing a node via the slide-over,
diagram-node-updateddispatches 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) — originalDiagramNodePHP 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:
- Dispatches
diagram-refreshedwith the new node data, connections, and config - JS updates DOM data attributes (
data-draggable,data-selectable,data-deletable) sincewire:ignoreprevents Livewire DOM morphing - Shows/hides the delete (X) button, clears stale selections
- Tears down and rebuilds the jsPlumb instance with the new config
- Uses
AbortControllerto cleanly remove old event listeners before attaching new ones
When a node is edited via the slide-over:
- Both the serialized array and the PHP object are updated with the new data
diagram-node-updatedis dispatched with the node ID and fresh rendered HTML- JS replaces the node's
.diagram-node-innercontent 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