A powerful and elegant Leaflet integration for Filament PHP that makes creating interactive maps a breeze. Build beautiful, feature-rich maps with markers, clusters, shapes, and more using a fluent, expressive API.
- 🗺️ Interactive Maps - Full Leaflet integration with customizable tile layers
- 📍 Markers & Clusters - Beautiful markers with popup/tooltip support and intelligent clustering
- 🎨 Shapes - Circles, polygons, polylines, rectangles, and circle markers
- 🎯 Click Events - Handle clicks on markers, shapes, and the map itself
- 📊 GeoJSON Support - Display density maps with custom color schemes
- 🔄 Model Binding - Automatically create markers from Eloquent models
- 🎨 Multiple Tile Layers - Switch between OpenStreetMap, Satellite, and custom layers
- 💾 CRUD Operations - Create markers directly from map clicks
- 🎭 Customizable - Extensive configuration options for every element
composer require eduardoribeirodev/filament-leafletPublish the assets:
php artisan filament:assetsThis will publish the Leaflet assets used by the package — the distribution now includes draw toolbar, marker cluster control, fullscreen control and geosearch toolbar assets.
- Getting Started
- Map Elements
- User Interaction
- Advanced Features
- API Reference
- Best Practices
- Troubleshooting
Create your first map widget:
namespace App\Filament\Widgets;
use EduardoRibeiroDev\FilamentLeaflet\Widgets\MapWidget;
use EduardoRibeiroDev\FilamentLeaflet\Support\Markers\Marker;
class MyMapWidget extends MapWidget
{
protected static ?string $heading = 'My Locations';
protected static array $mapCenter = [-23.5505, -46.6333]; // São Paulo
protected static int $defaultZoom = 12;
protected function getMarkers(): array
{
return [
Marker::make(-23.5505, -46.6333)
->title('São Paulo')
->popupContent('The largest city in Brazil'),
];
}
}Configure your map's initial state and behavior:
class MyMapWidget extends MapWidget
{
// Map heading
protected static ?string $heading = 'Store Locations';
// Center coordinates [latitude, longitude]
protected static array $mapCenter = [-14.235, -51.9253];
// Initial zoom level (1-18)
protected static int $defaultZoom = 4;
// Map height in pixels
protected static int $mapHeight = 600;
// Zoom configuration
protected static int $maxZoom = 18;
protected static int $minZoom = 2;
}You can enable or disable UI controls individually using the widget flags. Use the provided toggles to show controls:
hasAttributionControl: show/hide the attribution controlhasScaleControl: show/hide the scale controlhasZoomControl: show/hide the zoom controlhasFullscreenControl: show/hide the fullscreen controlhasSearchControl: show/hide the search controlhasDrawControl: enable/disable the draw toolbar control
Examples
- Enable controls from the widget class:
class MyMapWidget extends MapWidget
{
protected static bool $hasAttributionControl = false;
protected static bool $hasScaleControl = true;
protected static bool $hasZoomControl = true;
protected static bool $hasFullscreenControl = true;
protected static bool $hasDrawControl = true;
protected static bool $hasSearchControl = true;
}- Conditionally toggle controls per runtime using
getMapControls()override. This is useful when you want control visibility to depend on user permissions or widget state:
public static function getMapControls(): array
{
$controls = parent::getMapControls();
// Example: hide fullscreen for non-admins
if (!auth()?->user()?->is_admin) {
$controls['fullscreenControl'] = false;
}
return $controls;
}Tile layers can be provided as a single TileLayer enum, a plain URL string, or an array of layers. When using an associative array you may provide custom labels for the layer selector. If a TileLayer enum is used the widget will also include the provider attribution automatically.
Choose from multiple tile layer providers or add your own:
use EduardoRibeiroDev\FilamentLeaflet\Enums\TileLayer;
class MyMapWidget extends MapWidget
{
// Single layer
protected static TileLayer|string|array $tileLayersUrl = TileLayer::OpenStreetMap;
// Multiple layers
protected static TileLayer|string|array $tileLayersUrl = [
TileLayer::OpenStreetMap,
TileLayer::GoogleSatellite,
TileLayer::EsriNatGeo,
];
// Multiple layers with custom names
protected static TileLayer|string|array $tileLayersUrl = [
'Street Map' => TileLayer::OpenStreetMap,
'Satellite' => TileLayer::EsriWorldStreetMap,
'Terrain' => TileLayer::GoogleTerrain,
];
// Custom tile server
protected static TileLayer|string|array $tileLayersUrl = [
'Custom' => 'https://{s}.tile.custom.com/{z}/{x}/{y}.png',
];
}Available tile layers:
TileLayer::OpenStreetMapTileLayer::GoogleStreetsTileLayer::GoogleSatelliteTileLayer::GoogleHybridTileLayer::GoogleTerrainTileLayer::EsriWorldImageryTileLayer::EsriWorldStreetMapTileLayer::EsriNatGeoTileLayer::CartoPositronTileLayer::CartoDarkMatter
Create markers with various configurations:
use EduardoRibeiroDev\FilamentLeaflet\Support\Markers\Marker;
use EduardoRibeiroDev\FilamentLeaflet\Enums\Color;
protected function getMarkers(): array
{
return [
// Simple marker
Marker::make(-23.5505, -46.6333),
// Marker with title (shows as tooltip and popup title)
Marker::make(-23.5505, -46.6333)
->title('São Paulo'),
// Colored marker
Marker::make(-23.5505, -46.6333)
->blue(), // or ->color(Color::Blue)
// Custom icon
Marker::make(-23.5505, -46.6333)
->icon('https://example.com/icon.png', [32, 32]),
// Draggable marker
Marker::make(-23.5505, -46.6333)
->draggable(),
// Complete marker
Marker::make(-23.5505, -46.6333)
->id('sao-paulo')
->title('São Paulo')
->red()
->popupContent('The largest city in Brazil')
->group('cities'),
];
}Use the built-in color system:
Marker::make($lat, $lng)
->blue() // Blue marker
->red() // Red marker
->green() // Green marker
->orange() // Orange marker
->yellow() // Yellow marker
->violet() // Violet marker
->grey() // Grey marker
->black() // Black marker
->gold() // Gold marker
->randomColor(); // Random color
// Or use the Color enum
Marker::make($lat, $lng)
->color(Color::Blue);Automatically create markers from your database records:
use App\Models\Store;
protected function getMarkers(): array
{
return Store::all()->map(function ($store) {
return Marker::fromRecord(
record: $store,
latColumn: 'latitude',
lngColumn: 'longitude',
titleColumn: 'name',
descriptionColumn: 'description',
popupFieldsColumns: ['address', 'phone', 'email'],
color: Color::Blue,
);
})->toArray();
}If your coordinates are stored as JSON:
// Database structure: coordinates => {"lat": -23.5505, "lng": -46.6333}
Marker::fromRecord(
record: $store,
jsonColumn: 'coordinates', // Column containing JSON
latColumn: 'lat', // Key in JSON object
lngColumn: 'lng', // Key in JSON object
titleColumn: 'name',
);Use the mapRecordCallback to customize each marker:
Marker::fromRecord(
record: $store,
latColumn: 'latitude',
lngColumn: 'longitude',
mapRecordCallback: function (Marker $marker, Model $record) {
// Customize based on record data
if ($record->is_featured) {
$marker->gold();
}
if ($record->status === 'closed') {
$marker->grey();
}
// Add custom popup fields
$marker->popupFields([
'opening_hours' => $record->hours,
'rating' => $record->rating . ' ⭐',
]);
}
);Layer groups are a powerful way to organize and manage multiple layers on your map. They allow you to:
- Toggle visibility - Show/hide entire groups of layers at once
- Organize layers - Group related markers and shapes together
- Improve performance - Manage large datasets efficiently
- Control layer management - Add/remove layers from groups dynamically
A simple container for organizing related layers. Perfect for grouping logically related markers and shapes without any automatic behavior:
use EduardoRibeiroDev\FilamentLeaflet\Support\Groups\LayerGroup;
protected function getMarkers(): array
{
return [
LayerGroup::make([
Marker::make(-23.5505, -46.6333)->title('Store 1'),
Marker::make(-23.5515, -46.6343)->title('Store 2'),
Marker::make(-23.5525, -46.6353)->title('Store 3'),
])
->name('Active Stores')
->id('active-stores'),
];
}Using the group() helper method (shorthand):
Instead of wrapping layers in LayerGroup::make(), you can use the group() method on any layer to automatically group multiple layers:
protected function getMarkers(): array
{
return [
Marker::make(-23.5505, -46.6333)
->title('Store 1')
->group('Active Stores'),
Marker::make(-23.5515, -46.6343)
->title('Store 2')
->group('Active Stores'),
Marker::make(-23.5525, -46.6353)
->title('Store 3')
->group('Active Stores'),
];
}The group() method automatically creates a LayerGroup instance for all layers with the same group name, providing a cleaner syntax when you don't need LayerGroup::make() complexity.
Advanced example with mixed layers:
LayerGroup::make([
// Markers
Marker::make(-23.5505, -46.6333)->title('Store 1')->blue(),
Marker::make(-23.5515, -46.6343)->title('Store 2')->blue(),
// Shapes
Circle::make(-23.5505, -46.6333)
->radiusInKilometers(5)
->blue()
->fillOpacity(0.1),
// Popups and tooltips work on all layers
])
->name('Store Coverage')
->id('store-coverage-group');Creates a polygon envelope around all layers in the group. This is useful for visualizing the coverage area or boundary of a set of points:
use EduardoRibeiroDev\FilamentLeaflet\Support\Groups\FeatureGroup;
protected function getMarkers(): array
{
return [
FeatureGroup::make([
Marker::make(-23.5505, -46.6333)->title('Point 1'),
Marker::make(-23.5515, -46.6343)->title('Point 2'),
Marker::make(-23.5525, -46.6323)->title('Point 3'),
])
->name('Delivery Zone')
->blue()
->fillBlue()
->fillOpacity(0.2)
->weight(2)
->dashArray('5, 10'),
];
}Real-world example with custom styling:
FeatureGroup::make([
Marker::make(-23.5505, -46.6333)->title('Warehouse A'),
Marker::make(-23.5615, -46.6443)->title('Warehouse B'),
Marker::make(-23.5425, -46.6223)->title('Warehouse C'),
])
->name('Supply Chain Network')
->id('supply-chain')
->orange() // Border color
->fillColor(Color::Yellow) // Fill color
->fillOpacity(0.15) // Semi-transparent fill
->weight(3) // Thicker border
->opacity(0.8);Feature groups with event handlers:
FeatureGroup::make($warehouseMarkers)
->name('Warehouses')
->green()
->action(function (FeatureGroup $group) {
Notification::make()
->title('Warehouse Zone Clicked')
->body('This is the warehouse coverage area')
->send();
});Groups nearby markers into clusters for better performance and visual clarity, especially with large datasets. Clusters automatically expand when zooming in:
use EduardoRibeiroDev\FilamentLeaflet\Support\Groups\MarkerCluster;
protected function getMarkers(): array
{
return [
MarkerCluster::make([
Marker::make(-23.5505, -46.6333)->title('Location 1'),
Marker::make(-23.5515, -46.6343)->title('Location 2'),
Marker::make(-23.5525, -46.6353)->title('Location 3'),
])
->blue()
->maxClusterRadius(80)
->showCoverageOnHover()
->spiderfyOnMaxZoom(),
];
}Cluster from Model:
Create clusters directly from Eloquent models with powerful customization:
use App\Models\Store;
protected function getMarkers(): array
{
return [
MarkerCluster::fromModel(
model: Store::class,
latColumn: 'latitude',
lngColumn: 'longitude',
titleColumn: 'name',
descriptionColumn: 'description',
popupFieldsColumns: ['address', 'phone'],
color: Color::Green,
)
->maxClusterRadius(60)
->disableClusteringAtZoom(15),
];
}Cluster with Query Modification:
Filter and customize the query used to load markers:
MarkerCluster::fromModel(
model: Store::class,
latColumn: 'latitude',
lngColumn: 'longitude',
modifyQueryCallback: function ($query) {
return $query
->where('status', 'active')
->where('city', 'São Paulo')
->orderBy('name');
},
mapRecordCallback: function (Marker $marker, Model $record) {
// Customize each marker based on record properties
if ($record->isPremium()) {
$marker->gold()->icon('/images/premium-icon.png');
}
// Add status-based styling
match($record->status) {
'open' => $marker->green(),
'busy' => $marker->orange(),
'closed' => $marker->red(),
default => $marker->grey(),
};
// Add popup with custom fields
$marker->popupFields([
'manager' => $record->manager_name,
'staff' => $record->staff_count . ' employees',
'rating' => $record->rating . ' ⭐',
]);
}
);Advanced cluster configuration:
MarkerCluster::make($markers)
->maxClusterRadius(80) // Cluster radius in pixels
->showCoverageOnHover(true) // Highlight cluster area on hover
->zoomToBoundsOnClick(true) // Zoom to cluster bounds when clicked
->spiderfyOnMaxZoom(true) // Spread markers at max zoom
->removeOutsideVisibleBounds(true) // Remove markers outside viewport for performance
->disableClusteringAtZoom(15) // Stop clustering at zoom level 15+
->animate(true) // Animate cluster changes
->options([ // Custom Leaflet options
'maxClusterRadius' => 100,
'animateAddingMarkers' => true,
]);You can combine different layer groups in the same map to create complex, multi-layered visualizations:
use App\Models\Store;
use App\Models\Warehouse;
use App\Models\Partner;
protected function getLayers(): array
{
return [
// Group 1: Stores with clustering
MarkerCluster::fromModel(
model: Store::class,
latColumn: 'latitude',
lngColumn: 'longitude',
titleColumn: 'name',
color: Color::Blue,
)
->name('Retail Stores')
->maxClusterRadius(80),
// Group 2: Warehouses with feature group
FeatureGroup::make([
Warehouse::all()->map(fn($w) =>
Marker::make($w->latitude, $w->longitude)
->title($w->name)
->red()
)->toArray()
])
->name('Warehouses')
->orange()
->fillOpacity(0.1),
// Group 3: Partners as simple layer group
LayerGroup::make([
Partner::active()->get()->map(fn($p) =>
Marker::make($p->latitude, $p->longitude)
->title($p->company_name)
->green()
->popupFields([
'contact' => $p->contact_name,
'phone' => $p->phone,
])
)->toArray()
])
->name('Partner Locations')
->id('partners-group'),
// Group 4: Service areas with shapes
LayerGroup::make([
Circle::make(-23.5505, -46.6333)
->radiusInKilometers(25)
->blue()
->fillBlue()
->fillOpacity(0.05)
->popupContent('Primary service area'),
Circle::make(-23.5505, -46.6333)
->radiusInKilometers(50)
->blue()
->dashArray('5, 5')
->fillOpacity(0)
->popupContent('Extended service area'),
])
->name('Service Areas')
->id('service-areas'),
];
}This example demonstrates:
- Clustering for high-volume data (stores)
- Feature groups for geographic boundaries (warehouse coverage)
- Simple groups for categorical data (partners)
- Shape combinations for visualizing service areas
Toggling visibility in the UI:
Layer groups automatically appear in the Leaflet controls when a name is set, allowing users to toggle them on/off from the map interface.
Draw various geometric shapes on your map:
Circles with radius in various units:
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Circle;
protected function getShapes(): array
{
return [
// Radius in meters (default)
Circle::make(-23.5505, -46.6333)
->radius(5000)
->blue()
->fillBlue()
->title('Coverage Area'),
// Radius in kilometers
Circle::make(-23.5505, -46.6333)
->radiusInKilometers(5)
->red()
->fillOpacity(0.3),
// Radius in miles
Circle::make(-23.5505, -46.6333)
->radiusInMiles(3)
->green(),
// Radius in feet
Circle::make(-23.5505, -46.6333)
->radiusInFeet(10000)
->orange(),
// Styled circle
Circle::make(-23.5505, -46.6333)
->radiusInKilometers(10)
->color(Color::Blue) // Border color
->fillColor(Color::Blue) // Fill color
->weight(3) // Border width
->opacity(0.8) // Border opacity
->fillOpacity(0.2) // Fill opacity
->dashArray('5, 10') // Dashed border
->popupContent('10km radius coverage'),
];
}Small circles with pixel-based radius (like markers but circular):
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\CircleMarker;
CircleMarker::make(-23.5505, -46.6333)
->radius(15) // Radius in pixels
->red()
->fillRed()
->weight(2)
->title('Point of Interest');Draw custom polygons:
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Polygon;
// Define a polygon area
Polygon::make([
[-23.5505, -46.6333],
[-23.5515, -46.6343],
[-23.5525, -46.6323],
[-23.5505, -46.6333], // Close the polygon
])
->green()
->fillGreen()
->fillOpacity(0.3)
->title('Delivery Zone')
->popupContent('We deliver to this area');
// Or build point by point
Polygon::make()
->addPoint(-23.5505, -46.6333)
->addPoint(-23.5515, -46.6343)
->addPoint(-23.5525, -46.6323)
->addPoint(-23.5505, -46.6333)
->blue();Draw lines connecting multiple points:
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Polyline;
// Route or path
Polyline::make([
[-23.5505, -46.6333],
[-23.5515, -46.6343],
[-23.5525, -46.6353],
[-23.5535, -46.6363],
])
->blue()
->weight(4)
->opacity(0.7)
->dashArray('10, 5') // Dashed line
->smoothFactor(1.5) // Smooth curves
->title('Delivery Route');
// Or build incrementally
Polyline::make()
->addPoint(-23.5505, -46.6333)
->addPoint(-23.5515, -46.6343)
->addPoint(-23.5525, -46.6353)
->red()
->weight(3);Draw rectangular bounds:
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Rectangle;
// Using corner coordinates
Rectangle::make(
[-23.5505, -46.6333], // Southwest corner
[-23.5525, -46.6353] // Northeast corner
)
->orange()
->fillOrange()
->fillOpacity(0.2)
->title('Restricted Area');
// Alternative syntax
Rectangle::makeFromCoordinates(
-23.5505, -46.6333, // Southwest lat, lng
-23.5525, -46.6353 // Northeast lat, lng
)
->red();Circle::make(-23.5505, -46.6333)
->radius(5000)
// Border styling
->color(Color::Blue) // Border color
->weight(3) // Border width in pixels
->opacity(0.8) // Border opacity (0-1)
->dashArray('5, 10') // Dashed border pattern
// Fill styling
->fillColor(Color::Green) // Fill color
->fillOpacity(0.3) // Fill opacity (0-1)
// Custom options
->options([
'className' => 'custom-shape',
'interactive' => true,
]);Make markers and shapes editable directly on the map by enabling the draw control:
class MyMapWidget extends MapWidget
{
protected static bool $hasDrawControl = true;
protected function getMarkers(): array
{
return [
Marker::make(-23.5505, -46.6333)
->title('Editable Marker')
->editable(), // Make this marker editable
Circle::make(-23.5505, -46.6333)
->radiusInKilometers(5)
->editable(), // Make this circle editable
];
}
}You can also make all layers in a group editable:
LayerGroup::make([
Marker::make(-23.5505, -46.6333)->title('Point 1'),
Marker::make(-23.5515, -46.6343)->title('Point 2'),
Marker::make(-23.5525, -46.6353)->title('Point 3'),
])
->name('Editable Points')
->editable(), // All markers in the group are now editableTooltips appear on hover:
Marker::make(-23.5505, -46.6333)
->tooltip(
content: 'São Paulo City',
permanent: false, // Always visible
direction: 'top', // 'top', 'bottom', 'left', 'right', 'auto'
options: [
'offset' => [0, -20],
'className' => 'custom-tooltip',
]
);
// Or use individual methods
Marker::make(-23.5505, -46.6333)
->tooltipContent('São Paulo')
->tooltipPermanent(true)
->tooltipDirection('top')
->tooltipOptions(['opacity' => 0.9]);Popups appear on click and support rich content:
Marker::make(-23.5505, -46.6333)
->popupTitle('Store Location')
->popupContent('Visit our main store in downtown São Paulo')
->popupFields([
'address' => '123 Main Street',
'phone' => '+55 11 1234-5678',
'email' => '[email protected]',
'opening_hours' => 'Mon-Fri: 9AM-6PM',
])
->popupOptions([
'maxWidth' => 300,
'className' => 'custom-popup',
]);
// Or use the shorthand
Marker::make(-23.5505, -46.6333)
->popup(
content: 'Store description',
fields: [
'address' => '123 Main Street',
'phone' => '+55 11 1234-5678',
],
options: ['maxWidth' => 300]
);The popupFields() method automatically formats your data into a clean, structured display:
Marker::make(-23.5505, -46.6333)
->popupFields([
'store' => 'Pizza Palace',
'phone_number' => '+55 11 1234-5678',
'opening_hours' => '10AM - 10PM',
]);This generates HTML like:
<p><span class="field-label">Store:</span> Pizza Palace</p>
<p><span class="field-label">Phone Number:</span> +55 11 1234-5678</p>
<p><span class="field-label">Opening Hours:</span> 10AM - 10PM</p>The keys are automatically:
- Converted to title case
- Underscores replaced with spaces
- Translated using Laravel's
__()helper
Both keys and values are translated, so you can use translation keys:
->popupFields([
'store.name' => $store->name,
'store.contact' => $store->phone,
])Marker::make(-23.5505, -46.6333)
->title('Pizza Palace') // Sets both tooltip and popup title
->popupContent('Best pizza in town')
->popupFields([
'address' => '123 Main St',
'phone' => '+55 11 1234-5678',
'rating' => '4.5 ⭐',
]);Handle user interactions with layers:
use Filament\Notifications\Notification;
Marker::make(-23.5505, -46.6333)
->title('Interactive Marker')
->onClick(function (Marker $marker) {
Notification::make()
->title('Marker Clicked!')
->body('You clicked on: ' . $marker->getId())
->success()
->send();
});
// Or use the action() method
Marker::make(-23.5505, -46.6333)
->action(function (Marker $marker) {
// Handle click
});Circle::make(-23.5505, -46.6333)
->radius(5000)
->action(function (Circle $circle) {
Notification::make()
->title('Circle clicked')
->send();
});
Polygon::make($coordinates)
->action(function (Polygon $polygon) {
// Handle polygon click
});When using markers from models, access the record in click actions:
protected function getMarkers(): array
{
return Store::all()->map(function ($store) {
return Marker::fromRecord(
record: $store,
latColumn: 'latitude',
lngColumn: 'longitude',
)->action(function (Marker $marker, Store $record) {
Notification::make()
->title("You clicked: {$record->name}")
->body("Address: {$record->address}")
->send();
// You can also redirect
return redirect()->route('stores.show', $record);
});
})->toArray();
}Handle clicks on the map itself:
public function onMapClick(float $latitude, float $longitude): void
{
Notification::make()
->title('Map clicked')
->body("Coordinates: {$latitude}, {$longitude}")
->send();
// Or create a new marker dynamically
// This will trigger the create modal if $markerModel is set
parent::onMapClick($latitude, $longitude);
}Enable creating markers directly from map clicks:
use App\Models\Location;
class LocationMapWidget extends MapWidget
{
protected static ?string $markerModel = Location::class;
// Column names in your database
protected static string $latitudeColumnName = 'latitude';
protected static string $longitudeColumnName = 'longitude';
// For JSON storage
protected static ?string $jsonCoordinatesColumnName = 'coordinates';
// Form configuration
protected static int $formColumns = 2;
protected static function getFormComponents(): array
{
return [
TextInput::make('name')
->required(),
Select::make('color')
->options(Color::class),
Textarea::make('description')
->columnSpanFull(),
];
}
}Notes:
- You can set
protected static ?string $markerResource = YourResource::class;to reuse an existing Filament Resource form instead of the widget's default form. The widget will call the resource's form builder when building the create modal. - If the widget form schema doesn't include your latitude/longitude fields, the widget will automatically add them as
Hiddenfields so the create flow still receives coordinates from map clicks. - If you store coordinates as a JSON column, set
protected static ?string $jsonCoordinatesColumnName = 'coordinates';and the widget will convert the latitude/longitude into the configured JSON column before creating the record.
Now when users click the map, a form modal opens to create a new location!
Integrate with existing Filament resources:
use App\Filament\Resources\Locations\LocationResource;
class LocationMapWidget extends MapWidget
{
protected static ?string $markerModel = Location::class;
protected static ?string $markerResource = LocationResource::class;
// The resource's form will be used automatically
}protected function afterMarkerCreated(Model $record): void
{
Notification::make()
->title('Location created!')
->body("Created: {$record->name}")
->success()
->send();
// Send email, log activity, etc.
}Transform data before saving:
protected function mutateFormDataBeforeCreate(array $data): array
{
$data['user_id'] = auth()->id();
$data['status'] = 'active';
// Convert coordinates to JSON if needed
return parent::mutateFormDataBeforeCreate($data);
}Refresh the map when table actions are performed:
use EduardoRibeiroDev\FilamentLeaflet\Traits\InteractsWithMap;
class ManageLocations extends ManageRecords
{
use InteractsWithMap;
// Your resource code...
}This automatically:
- Refreshes the map after create/edit/delete actions
- Keeps the map in sync with your table
Display choropleth maps with custom density data:
class BrazilDensityWidget extends MapWidget
{
protected static ?string $geoJsonUrl = 'https://example.com/brazil-states.json';
protected static array $geoJsonColors = [
'#FED976', // Lightest
'#FEB24C',
'#FD8D3C',
'#FC4E2A',
'#E31A1C',
'#BD0026',
'#800026', // Darkest
];
public function getGeoJsonData(): array
{
// Return density data for each region
return [
'SP' => 166.23, // São Paulo
'RJ' => 365.23, // Rio de Janeiro
'MG' => 33.41, // Minas Gerais
// ... more states
];
}
public static function getGeoJsonTooltip(): string
{
return <<<HTML
<h4>{state}</h4>
<b>Population Density: {density} per km²</b>
HTML;
}
}The colors are automatically applied based on data distribution, creating a beautiful density visualization.
Add custom CSS to your map:
public function getCustomStyles(): string
{
return <<<CSS
.custom-marker {
filter: hue-rotate(45deg);
}
.leaflet-popup-content {
font-family: 'Inter', sans-serif;
}
CSS;
}Execute JavaScript after map initialization:
public function afterMapInit(): string
{
return <<<JS
console.log('Map initialized!');
// Add custom controls
L.control.scale().addTo(map);
JS;
}
public function getAdditionalScripts(): string
{
return <<<JS
// Additional JavaScript code
function customFunction() {
// Your code
}
JS;
}Fine-tune Leaflet behavior:
public static function getMapOptions(): array
{
return [
'scrollWheelZoom' => true,
'doubleClickZoom' => true,
'dragging' => true,
'zoomControl' => false,
'attributionControl' => false,
'touchZoom' => true,
'boxZoom' => true,
'keyboard' => true,
];
}Notes:
- Please, keep the
zoomControlandattributionControlset asfalse. It is managed in the Map Controls section.
The package includes built-in support for multiple languages including:
- English (en)
- Portuguese (pt_BR, pt_PT)
- Spanish (es)
- French (fr)
- German (de)
- Italian (it)
All draw control labels, tooltips, and messages are automatically translated based on your application's locale. The package uses Laravel's translation system, so you can customize translations in your resources/lang directory:
resources/lang/
├── en/
│ └── filament-leaflet.php
├── pt_BR/
│ └── filament-leaflet.php
├── de/
│ └── filament-leaflet.php
└── ...
To customize translations, publish the language files:
php artisan vendor:publish --tag=filament-leaflet-translationsThen edit the translation files in public/vendor/filament-leaflet/lang.
Here's a comprehensive example combining multiple features:
namespace App\Filament\Widgets;
use App\Models\Store;
use EduardoRibeiroDev\FilamentLeaflet\Widgets\MapWidget;
use EduardoRibeiroDev\FilamentLeaflet\Support\Markers\Marker;
use EduardoRibeiroDev\FilamentLeaflet\Support\Groups\MarkerCluster;
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Circle;
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Polygon;
use EduardoRibeiroDev\FilamentLeaflet\Enums\Color;
use EduardoRibeiroDev\FilamentLeaflet\Enums\TileLayer;
use Filament\Notifications\Notification;
class StoreMapWidget extends MapWidget
{
protected static ?string $heading = 'Store Network';
protected static array $mapCenter = [-23.5505, -46.6333];
protected static int $defaultZoom = 11;
protected static int $mapHeight = 700;
protected static array $tileLayersUrl = [
'Street' => TileLayer::OpenStreetMap,
'Satellite' => TileLayer::GoogleSatellite,
];
// Enable marker creation
protected static ?string $markerModel = Store::class;
protected static string $latitudeColumnName = 'latitude';
protected static string $longitudeColumnName = 'longitude';
protected function getMarkers(): array
{
return [
// Clustered stores
MarkerCluster::fromModel(
model: Store::class,
latColumn: 'latitude',
lngColumn: 'longitude',
titleColumn: 'name',
descriptionColumn: 'description',
popupFieldsColumns: ['address', 'phone', 'manager'],
color: Color::Blue,
modifyQueryCallback: fn($q) => $q->where('status', 'active'),
mapRecordCallback: function (Marker $marker, $record) {
if ($record->is_flagship) {
$marker->gold()->icon('/images/flagship-icon.png');
}
$marker->action(function (Marker $m, $r) {
Notification::make()
->title("Store: {$r->name}")
->success()
->send();
});
}
)
->maxClusterRadius(60)
->spiderfyOnMaxZoom(),
// Featured location
Marker::make(-23.5505, -46.6333)
->title('Headquarters')
->red()
->icon('/images/hq-icon.png', [40, 40])
->popupContent('Our main office')
->popupFields([
'address' => 'Av. Paulista, 1000',
'phone' => '+55 11 1234-5678',
'opening_hours' => 'Mon-Fri: 9AM-6PM',
]),
];
}
protected function getShapes(): array
{
return [
// Delivery radius
Circle::make(-23.5505, -46.6333)
->radiusInKilometers(5)
->blue()
->fillBlue()
->fillOpacity(0.1)
->weight(2)
->dashArray('5, 5')
->popupContent('5km delivery radius'),
// Exclusive zone
Polygon::make([
[-23.5505, -46.6333],
[-23.5605, -46.6433],
[-23.5705, -46.6333],
[-23.5505, -46.6333],
])
->green()
->fillGreen()
->fillOpacity(0.2)
->popupContent('VIP delivery zone')
->action(function () {
Notification::make()
->title('VIP Zone')
->body('Exclusive delivery area')
->send();
}),
];
}
protected static function getFormComponents(): array
{
return [
TextInput::make('name')
->required()
->maxLength(255),
Select::make('type')
->options([
'retail' => 'Retail Store',
'warehouse' => 'Warehouse',
'office' => 'Office',
])
->required(),
Select::make('color')
->options(Color::class),
TextInput::make('phone')
->tel(),
Textarea::make('description')
->columnSpanFull()
->maxLength(500),
];
}
protected function afterMarkerCreated(Model $record): void
{
Notification::make()
->title('Store Created!')
->body("New store '{$record->name}' added to the map")
->success()
->duration(5000)
->send();
}
public function onMapClick(float $latitude, float $longitude): void
{
// Custom logic before opening create form
logger("Map clicked at: {$latitude}, {$longitude}");
parent::onMapClick($latitude, $longitude);
}
}| Method | Description |
|---|---|
getHeading() |
Returns the widget heading |
getMarkers() |
Returns array of markers to display |
getShapes() |
Returns array of shapes to display |
getLayers() |
Returns combined markers and shapes |
onMapClick($lat, $lng) |
Handles map click events |
onLayerClick($layerId) |
Handles layer click events |
refreshMap() |
Manually refresh the map |
afterMarkerCreated($record) |
Hook after marker creation |
mutateFormDataBeforeCreate($data) |
Transform form data before save |
| Method | Description |
|---|---|
make($lat, $lng) |
Create a new marker |
fromRecord() |
Create marker from Eloquent model |
id($id) |
Set marker ID |
title($title) |
Set title (tooltip & popup) |
color($color) |
Set marker color |
icon($url, $size) |
Set custom icon |
draggable($bool) |
Make marker draggable |
editable($bool) |
Make marker editable on the map |
group($group) |
Assign to group (string or BaseLayerGroup) |
popup($content, $fields, $options) |
Configure popup |
tooltip($content, $permanent, $direction, $options) |
Configure tooltip |
action($callback) |
Set click handler |
distanceTo($marker) |
Calculate distance to another marker |
validate() |
Validate coordinates |
| Method | Description |
|---|---|
color($color) |
Set border color |
fillColor($color) |
Set fill color |
weight($pixels) |
Set border width |
opacity($value) |
Set border opacity (0-1) |
fillOpacity($value) |
Set fill opacity (0-1) |
dashArray($pattern) |
Set dash pattern |
editable($bool) |
Make shape editable on the map |
options($array) |
Set custom options |
popup($content, $fields, $options) |
Configure popup |
tooltip($content, $permanent, $direction, $options) |
Configure tooltip |
action($callback) |
Set click handler |
group($group) |
Assign to group (string or BaseLayerGroup) |
getCoordinates() |
Get center coordinates of the shape |
| Method | Description |
|---|---|
make($lat, $lng) |
Create circle |
radius($meters) |
Set radius in meters |
radiusInMeters($meters) |
Set radius in meters |
radiusInKilometers($km) |
Set radius in kilometers |
radiusInMiles($miles) |
Set radius in miles |
radiusInFeet($feet) |
Set radius in feet |
| Method | Description |
|---|---|
make($lat, $lng) |
Create circle marker |
radius($pixels) |
Set radius in pixels |
| Method | Description |
|---|---|
make($coordinates) |
Create with coordinates |
addPoint($lat, $lng) |
Add vertex/point |
| Method | Description |
|---|---|
smoothFactor($factor) |
Set line smoothing |
| Method | Description |
|---|---|
make($corner1, $corner2) |
Create with corners |
makeFromCoordinates($lat1, $lng1, $lat2, $lng2) |
Create with coordinates |
| Method | Description |
|---|---|
make($layers) |
Create layer group with layers |
id($id) |
Set group ID |
name($name) |
Set group name |
option($key, $value) |
Set a group option |
options($array) |
Set multiple group options |
getLayers() |
Get all layers in the group |
| Method | Description |
|---|---|
make($layers) |
Create simple layer group |
name($name) |
Set user-visible group name |
id($id) |
Set group ID for controls |
editable($bool) |
Make all layers in group editable |
| Method | Description |
|---|---|
make($markers) |
Create feature group from markers |
name($name) |
Set zone/area name |
blue(), red(), etc. |
Set border color |
fillBlue(), fillRed(), etc. |
Set fill color |
fillOpacity($value) |
Set fill transparency (0-1) |
weight($pixels) |
Set border width |
editable($bool) |
Make all layers in group editable |
| Method | Description |
|---|---|
make($markers) |
Create cluster with markers |
fromModel() |
Create cluster from Eloquent model |
marker($marker) |
Add single marker |
markers($array) |
Add multiple markers |
name($name) |
Set cluster group name |
editable($bool) |
Make all markers in cluster editable |
maxClusterRadius($pixels) |
Set cluster radius (pixels) |
showCoverageOnHover($bool) |
Show cluster coverage on hover |
zoomToBoundsOnClick($bool) |
Zoom to bounds when clicked |
spiderfyOnMaxZoom($bool) |
Spread markers at max zoom |
disableClusteringAtZoom($level) |
Disable clustering at zoom level |
animate($bool) |
Animate cluster changes |
modifyQueryUsing($callback) |
Modify database query |
mapRecordUsing($callback) |
Customize each marker |
Available colors for markers and shapes:
Color::Blue/->blue()- #3388ffColor::Red/->red()- #f03Color::Green/->green()- #3c3Color::Orange/->orange()- #f80Color::Yellow/->yellow()- #fd0Color::Violet/->violet()- #a0fColor::Grey/->grey()- #666Color::Black/->black()- #000Color::Gold/->gold()- #ffd700
- Use Marker Clusters for large datasets:
// Bad: 1000 individual markers
protected function getMarkers(): array
{
return Store::all()->map(fn($s) => Marker::fromRecord($s))->toArray();
}
// Good: Clustered markers
protected function getMarkers(): array
{
return [
MarkerCluster::fromModel(Store::class)
->maxClusterRadius(80)
];
}- Limit data with query modifications:
MarkerCluster::fromModel(
model: Store::class,
modifyQueryCallback: fn($q) => $q->limit(100)->latest()
)- Use appropriate zoom levels:
protected static int $defaultZoom = 12; // City level
protected static int $maxZoom = 18; // Street level
protected static int $minZoom = 3; // Country level- Provide context with popups:
Marker::make($lat, $lng)
->title('Store Name')
->popupContent('Visit our location')
->popupFields([
'address' => '123 Main St',
'hours' => '9AM-6PM',
'phone' => '+55 11 1234-5678',
]);- Use appropriate colors:
// Status-based coloring
$marker->color(match($store->status) {
'open' => Color::Green,
'busy' => Color::Orange,
'closed' => Color::Red,
default => Color::Grey,
});- Add visual feedback:
Circle::make($lat, $lng)
->radiusInKilometers(5)
->blue()
->fillBlue()
->onMouseOver("this.setStyle({fillOpacity: 0.6})")
->onMouseOut("this.setStyle({fillOpacity: 0.3})")
->fillOpacity(0.1) // Subtle fill
->dashArray('5, 5'); // Dashed border- Extract complex logic:
protected function getMarkers(): array
{
return [
$this->getStoreMarkers(),
$this->getWarehouseMarkers(),
];
}
private function getStoreMarkers(): MarkerCluster
{
return MarkerCluster::fromModel(Store::class)
->blue()
->mapRecordUsing($this->configureStoreMarker(...));
}
private function configureStoreMarker(Marker $marker, Model $store): void
{
if ($store->is_flagship) {
$marker->gold()->icon('/images/flagship.png');
}
$marker->popupFields($store->only(['address', 'phone']));
}- Use dedicated widget classes:
// Good structure
app/Filament/Widgets/
├── Maps/
│ ├── StoreMapWidget.php
│ ├── DeliveryMapWidget.php
│ └── AnalyticsMapWidget.phpEnable logging for map interactions:
public function onLayerClick(string $layerId): void
{
logger("Layer clicked: {$layerId}");
parent::onLayerClick($layerId);
}
public function onMapClick(float $latitude, float $longitude): void
{
logger("Map clicked", compact('latitude', 'longitude'));
parent::onMapClick($latitude, $longitude);
}The draw control is disabled by default for better performance. To enable it:
class MyMapWidget extends MapWidget
{
protected static bool $hasDrawControl = true;
protected function getMarkers(): array
{
return [
// Your markers...
];
}
}Once enabled, users can:
- Draw new markers, shapes (circles, polygons, polylines, rectangles)
- Edit existing editable layers
- Delete layers by clicking the delete tool
Note: Only layers marked with ->editable() can be edited on the map.
- Check coordinate validity:
Marker::make($lat, $lng)->validate(); // Throws exception if invalid- Verify zoom level:
protected static int $defaultZoom = 12; // Try different values- Check marker is in bounds:
// Ensure coordinates are visible in your map center/zoom
protected static array $mapCenter = [$your_marker_lat, $your_marker_lng];- Ensure content is set:
$marker->popupContent('Some content'); // Required- Check for JavaScript errors in browser console
- Increase cluster radius:
MarkerCluster::make($markers)
->maxClusterRadius(100) // Increase this value- Check zoom level:
->disableClusteringAtZoom(15) // Clusters won't show at/above this zoom- Verify model is set:
protected static ?string $markerModel = YourModel::class;- Check column names match:
protected static string $latitudeColumnName = 'latitude'; // Must match DB
protected static string $longitudeColumnName = 'longitude';This package is open-sourced software licensed under the MIT license.
- Built for Filament
- Uses Leaflet for mapping
- Created by Eduardo Ribeiro
For issues, questions, or contributions, please visit the GitHub repository. Don't forget, Jesus loves you ❤️.