laraveldaily / filawidgets
Reusable Filament dashboard widgets.
Requires
- php: ^8.2
- filament/filament: ^4.0|^5.0
- laravel/framework: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0
README
Reusable Filament dashboard widgets for Laravel. Five widget types — sparkline tables, breakdowns, progress bars, completion rate gauges, and heatmap calendars — that work on dashboards, resource pages, or any Filament page.
Requirements
- PHP 8.2+
- Laravel 11+
- Filament 4+
Installation
composer require laraveldaily/filawidgets
The service provider auto-registers via Composer's package discovery.
Custom Theme (Required)
This package uses Tailwind CSS classes that are not included in Filament's default compiled styles. You need a custom Filament theme so that Tailwind can scan the package's Blade views.
1. Create a theme (if you don't have one already):
php artisan make:filament-theme
Follow the instructions the command prints — it will add the theme to vite.config.js and your panel provider.
2. Add the package views to your theme CSS file (resources/css/filament/{panel}/theme.css):
@import '../../../../vendor/filament/filament/resources/css/theme.css'; @source '../../../../app/Filament/**/*'; @source '../../../../resources/views/filament/**/*'; @source '../../../../vendor/laraveldaily/filawidgets/resources/views/**/*';
Add this last line with vendor/laraveldaily/filawidgets.
3. Rebuild your assets:
npm run build
(or npm run dev)
Quick Start: Create Widgets
Scaffold a widget with the artisan command:
php artisan make:filawidget
The command prompts for a name and widget type interactively. You can also pass them directly:
php artisan make:filawidget RevenueByRegionWidget --type=Breakdown
Available types: SparklineTable, Breakdown, Progress, CompletionRate, HeatmapCalendar.
The generated class is placed in app/Filament/Widgets/ with a minimal skeleton — just fill in your query logic.
Every widget follows the same pattern: extend the base class, set properties, implement getData().
use LaravelDaily\FilaWidgets\Data\BreakdownItemData; use LaravelDaily\FilaWidgets\Data\BreakdownWidgetData; use LaravelDaily\FilaWidgets\Widgets\BreakdownWidget; class RevenueByRegionWidget extends BreakdownWidget { protected ?string $widgetLabel = 'Revenue by Region'; protected ?int $itemLimit = 4; protected bool $groupOther = true; protected function getData(): BreakdownWidgetData { // Your query logic here return new BreakdownWidgetData(items: [ new BreakdownItemData('United States', 27378.77, previousValue: 23120.50), new BreakdownItemData('Germany', 17230.02, previousValue: 13520.00), new BreakdownItemData('Lithuania', 11989.15, previousValue: 8840.30), ], description: 'Country mix for last 30 days'); } }
Displaying Widgets
Dashboard (auto-discovery): Filament auto-discovers widgets in app/Filament/Widgets/, so widgets created with make:filawidget appear on the default dashboard automatically — no registration needed.
Custom dashboard page: If you have a custom dashboard, register widgets explicitly:
public function getWidgets(): array { return [RevenueByRegionWidget::class]; }
Resource or custom pages: Add widgets to any page's header or footer:
protected function getHeaderWidgets(): array { return [ RevenueByRegionWidget::make(['range' => 'last_30_days']), ]; }
When range is passed directly, the widget does not depend on dashboard page filters.
Widget Types
1. SparklineTableWidget
A multi-row metric table with inline SVG sparkline charts and trend badges.
Base class: LaravelDaily\FilaWidgets\Widgets\SparklineTableWidget
Default column span: ['md' => 1, 'xl' => 3]
use LaravelDaily\FilaWidgets\Data\SparklineTableRowData; use LaravelDaily\FilaWidgets\Data\SparklineTableWidgetData; use LaravelDaily\FilaWidgets\Support\SparklineSeries; use LaravelDaily\FilaWidgets\Widgets\SparklineTableWidget; class RevenuePulseWidget extends SparklineTableWidget { protected ?string $widgetLabel = 'Revenue Pulse'; protected function getData(): SparklineTableWidgetData { $dateRange = DashboardDateRange::fromFilter($this->getRangeFilter()); [$currentStart, $currentEnd] = $dateRange->currentPeriod(); [$previousStart, $previousEnd] = $dateRange->previousPeriod(); $baseQuery = Order::query()->where('status', OrderStatus::Completed); return SparklineTableWidgetData::fromRows( new SparklineTableRowData( label: 'Revenue', value: (float) (clone $baseQuery)->whereBetween('created_at', [$currentStart, $currentEnd])->sum('amount'), previousValue: (float) (clone $baseQuery)->whereBetween('created_at', [$previousStart, $previousEnd])->sum('amount'), sparkline: SparklineSeries::daily($currentStart, $currentEnd, clone $baseQuery, 'SUM(amount)'), format: 'currency', ), new SparklineTableRowData( label: 'Orders', value: (float) (clone $baseQuery)->whereBetween('created_at', [$currentStart, $currentEnd])->count(), previousValue: (float) (clone $baseQuery)->whereBetween('created_at', [$previousStart, $previousEnd])->count(), sparkline: SparklineSeries::daily($currentStart, $currentEnd, clone $baseQuery, 'COUNT(*)'), format: 'number', precision: 0, ), ); } }
SparklineTableRowData
| Parameter | Type | Default | Description |
|---|---|---|---|
label |
string |
required | Row label |
value |
float |
required | Current period value |
previousValue |
?float |
null |
Previous period value (enables trend badge) |
sparkline |
array<int, float> |
[] |
Ordered daily data points for the SVG chart |
format |
?string |
null |
'currency', 'number', or 'percentage' (falls back to widget default) |
precision |
?int |
null |
Decimal places (falls back to widget default) |
url |
?string |
null |
Makes the row clickable |
openUrlInNewTab |
bool |
false |
Open URL in new tab |
color |
?string |
null |
Per-row color ('success', 'warning', 'danger') for sparkline and badge |
showSparkline |
bool |
true |
Set false to hide the sparkline for this row |
SparklineSeries Helper
Builds a zero-filled daily float array from a query. Eliminates the most common boilerplate:
use LaravelDaily\FilaWidgets\Support\SparklineSeries; $series = SparklineSeries::daily( start: $start, // CarbonInterface end: $end, // CarbonInterface query: Order::query()->where(...), // Eloquent Builder (cloned internally) aggregate: 'SUM(amount)', // Raw SQL aggregate dateColumn: 'created_at', // Column to group by (default) precision: 2, // Decimal rounding (default) ); // Returns: [0.0, 150.50, 0.0, 320.00, ...] (one float per day)
2. BreakdownWidget
A ranked list showing each item's value, contribution percentage, and period-over-period delta.
Base class: LaravelDaily\FilaWidgets\Widgets\BreakdownWidget
Default column span: ['md' => 1, 'xl' => 3]
use LaravelDaily\FilaWidgets\Data\BreakdownItemData; use LaravelDaily\FilaWidgets\Data\BreakdownWidgetData; use LaravelDaily\FilaWidgets\Widgets\BreakdownWidget; class RevenueByRegionWidget extends BreakdownWidget { protected ?string $widgetLabel = 'Revenue by Region'; protected ?int $itemLimit = 4; protected bool $groupOther = true; protected function getData(): BreakdownWidgetData { $dateRange = DashboardDateRange::fromFilter($this->getRangeFilter()); [$currentStart, $currentEnd] = $dateRange->currentPeriod(); [$previousStart, $previousEnd] = $dateRange->previousPeriod(); $currentItems = $this->totalsByCountry($currentStart, $currentEnd); $previousItems = $this->totalsByCountry($previousStart, $previousEnd); $items = $currentItems ->keys() ->merge($previousItems->keys()) ->unique() ->map(fn (string $country): BreakdownItemData => new BreakdownItemData( label: $this->countryName($country), value: (float) ($currentItems[$country] ?? 0), previousValue: (float) ($previousItems[$country] ?? 0), )) ->sortByDesc(fn (BreakdownItemData $item): float => $item->value) ->values() ->all(); return new BreakdownWidgetData( items: $items, description: 'Country mix for ' . strtolower($dateRange->label()), ); } }
BreakdownWidget Properties
| Property | Type | Default | Description |
|---|---|---|---|
$itemLimit |
?int |
null |
Max rows to display |
$groupOther |
bool |
false |
Aggregate overflow rows into "Other" |
$sortBy |
string |
'value' |
Sort field ('value' or 'label') |
$sortDirection |
string |
'desc' |
'asc' or 'desc' |
$showContribution |
bool |
true |
Show contribution percentage column |
$showDelta |
bool |
true |
Show delta percentage badge |
$deltaThresholds |
array |
[] |
Color thresholds for delta badges |
BreakdownItemData
| Parameter | Type | Default | Description |
|---|---|---|---|
label |
string |
required | Item name |
value |
float |
required | Current value |
previousValue |
?float |
null |
Previous period value (enables delta) |
color |
?string |
null |
Row color ('success', 'warning', 'danger', 'primary') |
icon |
?string |
null |
Heroicon name for the row |
url |
?string |
null |
Makes the row clickable |
BreakdownWidgetData::fromCollection()
Build from query results without manual mapping:
// From an array of associative arrays $data = BreakdownWidgetData::fromCollection( items: $queryResults, labelKey: 'category_name', valueKey: 'total_revenue', previousValueKey: 'previous_revenue', description: 'Revenue breakdown', ); // From a keyed collection using closures $data = BreakdownWidgetData::fromCollection( items: $totals, // Collection<string, float> e.g. ['US' => 15000, 'DE' => 8000] labelKey: fn ($value, $key) => $key, valueKey: fn ($value) => (float) $value, );
3. ProgressWidget
A horizontal progress bar with goal tracking and optional projection.
Base class: LaravelDaily\FilaWidgets\Widgets\ProgressWidget
Default column span: ['md' => 1, 'xl' => 2]
use LaravelDaily\FilaWidgets\Widgets\ProgressWidget; class RevenueGoalWidget extends ProgressWidget { protected ?string $widgetLabel = 'Revenue Goal'; protected float $goal = 50000; protected int $goalRangeDays = 30; protected function getCurrentValue(): float { $dateRange = DashboardDateRange::fromFilter($this->getRangeFilter()); [$start, $end] = $dateRange->currentPeriod(); return (float) Order::query() ->where('status', OrderStatus::Completed->value) ->whereBetween('created_at', [$start, $end]) ->sum('amount'); } }
ProgressWidget Properties
| Property | Type | Default | Description |
|---|---|---|---|
$goal |
float |
0 |
Target value |
$goalRangeDays |
int |
30 |
Days for projection calculation |
$showProjection |
bool |
true |
Show projected pace box |
4. CompletionRateWidget
An SVG arc gauge showing a completion rate with threshold-based coloring.
Base class: LaravelDaily\FilaWidgets\Widgets\CompletionRateWidget
Default column span: ['md' => 1, 'xl' => 2]
use LaravelDaily\FilaWidgets\Widgets\CompletionRateWidget; class FulfillmentRateWidget extends CompletionRateWidget { protected ?string $widgetLabel = 'Fulfillment Rate'; protected function getCounts(): array { $dateRange = DashboardDateRange::fromFilter($this->getRangeFilter()); [$start, $end] = $dateRange->currentPeriod(); $completed = Order::query() ->where('status', OrderStatus::Completed->value) ->whereBetween('created_at', [$start, $end]) ->count(); $total = Order::query() ->whereBetween('created_at', [$start, $end]) ->count(); return ['completed' => $completed, 'total' => $total]; } protected function getThresholds(): array { return [ ['threshold' => 50, 'color' => 'danger', 'label' => 'Critical'], ['threshold' => 75, 'color' => 'warning', 'label' => 'Needs attention'], ['threshold' => 100, 'color' => 'success', 'label' => 'Healthy'], ]; } }
5. HeatmapCalendarWidget
A GitHub-style heatmap grid showing daily activity density.
Base class: LaravelDaily\FilaWidgets\Widgets\HeatmapCalendarWidget
Default column span: ['md' => 2, 'xl' => 2]
use LaravelDaily\FilaWidgets\Data\HeatmapCalendarWidgetData; use LaravelDaily\FilaWidgets\Widgets\HeatmapCalendarWidget; class DailyRevenueWidget extends HeatmapCalendarWidget { protected ?string $widgetLabel = 'Daily Revenue'; protected function getData(): HeatmapCalendarWidgetData { $dateRange = DashboardDateRange::fromFilter($this->getRangeFilter()); [$start, $end] = $dateRange->currentPeriod(); $entries = Order::query() ->where('status', OrderStatus::Completed) ->whereBetween('created_at', [$start, $end]) ->selectRaw('DATE(created_at) as date, SUM(amount) as total') ->groupBy('date') ->orderBy('date') ->pluck('total', 'date') ->map(fn ($value): float => round((float) $value, 2)) ->all(); return new HeatmapCalendarWidgetData( entries: $entries, // ['2026-03-20' => 500.00, ...] description: 'Daily revenue for ' . strtolower($dateRange->label()), ); } protected function getWeeksToShow(): int { return 9; } }
HeatmapCalendarWidget Methods
| Method | Return Type | Default | Description |
|---|---|---|---|
getWeeksToShow() |
int |
12 |
Number of weeks to display |
getColorScheme() |
string |
'green' |
Color scheme: 'green' or 'blue' |
HeatmapCalendarWidgetData
| Parameter | Type | Default | Description |
|---|---|---|---|
entries |
array<string, float> |
required | Date-keyed values ('Y-m-d' => float) |
description |
?string |
null |
Subtitle text |
entryUrls |
array<string, string> |
[] |
Date-keyed URLs for clickable cells |
openEntryUrlsInNewTab |
bool |
false |
Open cell URLs in new tab |
Shared Configuration
All widgets inherit these properties from InteractsWithWidgetConfiguration:
| Property | Type | Default | Description |
|---|---|---|---|
$widgetLabel |
?string |
null |
Widget title |
$widgetFormat |
string |
'currency' |
Value format: 'currency', 'number', 'percentage' |
$widgetCurrency |
string |
'USD' |
ISO 4217 currency code |
$widgetPrecision |
int |
2 |
Decimal places |
$widgetColor |
string |
'primary' |
Theme color: 'primary', 'success', 'warning', 'danger' |
$widgetIcon |
Heroicon|string|null |
null |
Icon (Heroicon enum or string) |
$widgetEmptyStateHeading |
?string |
null |
Empty state title |
$widgetEmptyStateDescription |
?string |
null |
Empty state subtitle |
$widgetActionLabel |
?string |
null |
CTA button label |
$widgetActionUrl |
?string |
null |
CTA button URL |
$widgetActionOpenInNewTab |
bool |
false |
Open CTA in new tab |
$widgetCacheTtl |
?int |
null |
Cache TTL in seconds (null = no caching) |
$widgetCacheKey |
?string |
null |
Custom cache key prefix |
$range |
?string |
null |
Date range filter (public Livewire property) |
Date Range Filtering
Widgets resolve the date range from two sources, in priority order:
$rangeproperty (public Livewire prop) — passed directly viaWidget::make(['range' => 'last_7_days'])$pageFilters['range']— populated automatically on dashboard pages withHasFiltersForm
Access it in your widget with $this->getRangeFilter(), which returns the resolved ?string value.
Built-in range values: 'last_7_days', 'last_30_days' (default), 'last_60_days'.
Caching
Enable caching on any widget by setting the TTL:
protected ?int $widgetCacheTtl = 300; // 5 minutes protected ?string $widgetCacheKey = 'my-widget'; // Optional prefix
Cache keys are generated from the widget class, resolver, current filters, and options. Different filter combinations produce different cache entries automatically.
Value Formatting
The WidgetValueFormatter supports three formats:
| Format | Example | Description |
|---|---|---|
'currency' |
$1,234.56 |
Uses Number::currency() with configured currency code |
'number' |
1,234 |
Plain number with number_format() |
'percentage' |
85.67% |
Number with % suffix |
Set the format per widget ($widgetFormat) or per sparkline row (format parameter).
License
MIT