A ReactFlow-inspired diagramming plugin for Filament 4+ with nodes, connections, and canvas
codenzia/filament-diagrammer is a Laravel package for a reactflow-inspired diagramming plugin for filament 4+ with nodes, connections, and canvas.
It currently has 0 GitHub stars and 1 downloads on Packagist (latest version v0.1.0).
Install it with composer require codenzia/filament-diagrammer.
Discover more Laravel packages by codenzia
or browse all Laravel packages to compare alternatives.
Last updated
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.
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
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).
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(),
];
}
}
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 define reusable schemas for node sections. Instead of hardcoding node content, define a NodeType class that provides Filament schema components for each section.
<?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(),
];
}
}
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:
NodeType::label() (shown as a badge)getBodySchema(), etc.) fall back to the NodeType's defaultsNodeType::editFormSchema() if availableYou can still use plain strings for simple labels: ->type('Process').
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
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.
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.
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.).
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) => ...);
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']);
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),
When autoFit() is enabled, the node width adapts to its content instead of using a fixed width:
DiagramNode::make('flexible')
->label('Auto-sized')
->autoFit();
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');
| 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 |
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.
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) => ...);
| 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) |
| 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 |
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.
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.
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
}
}
| 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 |
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')
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;
}
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
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:
Toolbar buttons show Undo/Redo with disabled state. Keyboard: Ctrl+Z (undo), Ctrl+Y or Ctrl+Shift+Z (redo).
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 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).
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');
Single-click a connection line to select it (highlighted with the selection color). Double-click opens the connection edit slide-over via openConnectionEditor().
Drag from a source endpoint to a target endpoint to create a new connection. The server is notified via onConnectionCreated().
Select a connection by clicking it, then press Delete or Backspace. The server is notified via onConnectionDeleted().
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',
]),
];
}
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.
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) |
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.
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
Node context menu:
deletable flag)Double-click on any node opens the settings editor directly — equivalent to the "Settings..." context menu item.
Connection context menu:
Canvas context menu:
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'),
]);
Select multiple nodes for bulk operations. Enabled by default.
DiagramCanvas::make()
->multiSelect(true)
| 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 |
When multiple nodes are selected, dragging any selected node moves the entire group. Position changes are tracked per-node for undo/redo.
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.
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.
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.
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
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.
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(),
];
}
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)
| 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);
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
];
@foreach (not Alpine x-for) — this enables full Filament component/action support inside nodesSchema::make()->components()->state()->toEmbeddedHtml() — Filament's SDUI rendering pipeline@jsplumb/browser-uiwire:ignore directive on the nodes layer prevents Livewire from interfering with jsPlumb's DOMdiagram-refreshed Livewire event, which the JS listens for to update with new settingsdiagram-node-updated dispatches fresh HTML for the JS to replace the node contentThe 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 renderingThis is necessary because Filament schema components are PHP objects that cannot be serialized to Livewire wire format.
When properties change at runtime (e.g., toggling draggable/selectable/deletable via Livewire), the plugin:
diagram-refreshed with the new node data, connections, and configdata-draggable, data-selectable, data-deletable) since wire:ignore prevents Livewire DOM morphingAbortController to cleanly remove old event listeners before attaching new onesWhen a node is edited via the slide-over:
diagram-node-updated is dispatched with the node ID and fresh rendered HTML.diagram-node-inner content and revalidates jsPlumbNodes can define labeled output connectors for branching flows (e.g., "Yes"/"No" on condition nodes).
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'],
];
}
}
$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.
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.
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.
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).
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');
Proprietary - Codenzia