Demo
A live demo is available for registered users.
- Register or log in at plugins.premte.ch/customer
- Open the Demo section in the sidebar
Features
- Hierarchical Rows - Group data by multiple dimensions with expand/collapse
- Multi-level Columns - Nested column headers with collapse support
- Dynamic Configuration - Change rows, columns, and values on the fly
- Multiple Aggregations - Sum, Average, Count, Min, Max, Percentage
- Row & Column Totals - Automatic total calculations
- Grand Total - Overall summary row
- Drill-Down - Click any cell to see underlying data records
- CSV & Excel Export - Export visible data to CSV or Excel (configurable)
- Dimension Reordering - Reordering of row/column dimensions with arrows
- Column Sorting - Click column headers to sort data
- URL Deep Linking - Share specific pivot configurations via URL
- Trend Indicators - Show percentage change between adjacent columns
- Heat Map - Dynamic cell background colors based on value intensity
- Custom Views - Register custom views (charts, graphs) as alternatives to the table
- Configurable Styling - Override Tailwind CSS classes via config file
- Dark Mode - Full Tailwind dark mode support
- i18n Ready - Translation support included
- Array Data Source - Use raw arrays instead of Eloquent models (API, CSV, etc.)
Installation
composer require pt-plugins/filament-pivot-table
Optionally publish the config file:
php artisan vendor:publish --tag=pivot-table-config
Filament Resource Integration (Recommended)
The recommended way to use the pivot table is inside a Filament Resource. Extend ListPivotRecords to get automatic filter integration, drill-down with original records, and a consistent look within your Filament panel.
<?php
namespace App\Filament\Resources\SaleResource\Pages;
use App\Filament\Resources\SaleResource;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use PtPlugins\FilamentPivotTable\Pages\ListPivotRecords;
class ListSalesPivot extends ListPivotRecords
{
protected static string $resource = SaleResource::class;
public function getPivotData(Builder $query): array
{
return $query
->select(
'category',
'region',
'quarter',
'month',
DB::raw('SUM(cost) as cost'),
DB::raw('SUM(quantity) as quantity'),
DB::raw('COUNT(*) as count')
)
->groupBy('category', 'region', 'quarter', 'month')
->get()
->toArray();
}
public function getDrillDownData(Builder $query, array $rowFilters, array $colFilters): array
{
foreach ($rowFilters as $field => $value) {
$query->where($field, $value);
}
foreach ($colFilters as $field => $value) {
$query->where($field, $value);
}
return $query->get()->toArray();
}
public function getDrillDownColumns(): array
{
return [
['name' => 'id', 'label' => 'ID', 'type' => 'numeric'],
['name' => 'category', 'label' => 'Category', 'type' => 'string'],
['name' => 'product', 'label' => 'Product', 'type' => 'string'],
['name' => 'region', 'label' => 'Region', 'type' => 'string'],
['name' => 'cost', 'label' => 'Cost', 'type' => 'numeric'],
['name' => 'quantity', 'label' => 'Quantity', 'type' => 'numeric'],
];
}
public function getPivotConfig(): array
{
return [
'name' => 'sales-pivot',
'availableFields' => [
['name' => 'category', 'label' => 'Category', 'type' => 'string'],
['name' => 'region', 'label' => 'Region', 'type' => 'string'],
['name' => 'quarter', 'label' => 'Quarter', 'type' => 'string'],
['name' => 'month', 'label' => 'Month', 'type' => 'string'],
['name' => 'cost', 'label' => 'Amount', 'type' => 'numeric'],
['name' => 'quantity', 'label' => 'Quantity', 'type' => 'numeric'],
['name' => 'count', 'label' => 'Count', 'type' => 'numeric'],
],
'rowDimensions' => ['category'],
'columnDimensions' => ['quarter', 'month'],
'aggregationField' => 'cost',
'aggregationType' => 'sum',
'drillDownEnabled' => true,
];
}
}
Then register the page in your Resource:
public static function getPages(): array
{
return [
'index' => Pages\ListSales::route('/'),
'pivot' => Pages\ListSalesPivot::route('/pivot'),
];
}
Key benefits:
- Filament table filters are automatically applied to pivot data
- Drill-down shows original database records (not aggregated data)
- Filter indicators and collapsible filter panel included
- Full Resource navigation integration
Livewire Widget
For standalone use outside of a Resource, use the Livewire widget directly in any Blade view:
@livewire('pivot-table-widget', [
'name' => 'sales-pivot',
'model' => \App\Models\Sale::class,
'availableFields' => [
['name' => 'category', 'label' => 'Category', 'type' => 'string'],
['name' => 'product', 'label' => 'Product', 'type' => 'string'],
['name' => 'region', 'label' => 'Region', 'type' => 'string'],
['name' => 'quarter', 'label' => 'Quarter', 'type' => 'string'],
['name' => 'month', 'label' => 'Month', 'type' => 'string'],
['name' => 'amount', 'label' => 'Amount', 'type' => 'numeric'],
['name' => 'quantity', 'label' => 'Quantity', 'type' => 'numeric'],
],
'rowDimensions' => ['category', 'product'],
'columnDimensions' => ['quarter', 'month'],
'aggregationField' => 'amount',
'aggregationType' => 'sum',
'valuePrefix' => '$',
'drillDownEnabled' => true,
'drillDownColumns' => [
['name' => 'id', 'label' => 'ID', 'type' => 'numeric'],
['name' => 'category', 'label' => 'Category', 'type' => 'string'],
['name' => 'amount', 'label' => 'Amount', 'type' => 'numeric'],
],
'showTrends' => true,
'trendDepth' => 1,
'showHeatMap' => true,
'showRowTotals' => true,
'showGrandTotal' => true,
'csvExportEnabled' => true,
'xlsxExportEnabled' => true,
])
This is the full example with all common parameters. Subsequent sections show only the parameters that change.
Static Builder
For static, non-interactive pivot tables (no controls, no drill-down):
use PtPlugins\FilamentPivotTable\Components\PivotTable;
$table = PivotTable::make('report')
->data($salesData)
->rowDimensions(['category', 'product'])
->columnDimensions(['quarter'])
->aggregations([
['field' => 'amount', 'type' => 'sum'],
])
->showRowTotals()
->showColumnTotals()
->showGrandTotal()
->decimalPlaces(2);
// Render in Blade
{!! $table->toHtml() !!}
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
name |
string | 'pivot-table' |
Unique identifier for the pivot table |
model |
string | * | Eloquent model class |
data |
array | * | Raw array data (alternative to model) |
availableFields |
array | [] |
Fields available for pivot configuration |
rowDimensions |
array | [] |
Default row grouping fields |
columnDimensions |
array | [] |
Default column grouping fields |
aggregationField |
string | '' |
Field to aggregate |
aggregationType |
string | 'sum' |
Aggregation type (sum, avg, count, min, max, percentage, custom) |
showConfigPanel |
bool | false |
Show/hide configuration controls |
showToolbar |
bool | true |
Show toolbar with export buttons |
stickyRowHeaders |
bool | false |
Enable sticky row dimension columns |
stickyColumnWidth |
int | 150 |
Width of sticky columns in pixels |
rowHeight |
string | null |
CSS height for uniform rows (e.g., '3.5rem') |
filters |
array | [] |
Filters to apply to data query |
valuePrefix |
string | '' |
Prefix for formatted values (e.g., '$') |
valueSuffix |
string | '' |
Suffix for formatted values (e.g., '%') |
decimalPlaces |
int | 2 |
Decimal places for values |
trendDecimalPlaces |
int | 1 |
Decimal places for trend percentages |
drillDownEnabled |
bool | false |
Enable click-to-drill-down on cells |
drillDownColumns |
array | [] |
Columns for drill-down modal |
csvExportEnabled |
bool | true |
Show/hide CSV export button |
xlsxExportEnabled |
bool | true |
Show/hide Excel export button |
showTrends |
bool | false |
Show trend indicators between columns |
trendDepth |
int|null | null |
Max column depth for trend display (null = all levels) |
showHeatMap |
bool | false |
Enable heat map background colors |
showRowTotals |
bool | true |
Show/hide the row totals column |
showColumnTotals |
bool | true |
Show/hide column total rows |
showGrandTotal |
bool | true |
Show/hide grand total row |
filtersView |
string | null |
Custom Blade view for filters |
filtersViewData |
array | [] |
Data for custom filters view |
Note: Use either
modelORdata, not both. One is required.
Field Definition
Each field in availableFields:
['name' => 'field_name', 'label' => 'Display Label', 'type' => 'string']
Types: string (dimension) or numeric (can be aggregated).
Array Data Source
Instead of an Eloquent model, pass raw array data. Useful for APIs, CSV files, or pre-processed data:
'data' => [
['region' => 'North', 'product' => 'Widget A', 'quarter' => 'Q1', 'sales' => 1500],
['region' => 'North', 'product' => 'Widget A', 'quarter' => 'Q2', 'sales' => 1800],
['region' => 'South', 'product' => 'Widget B', 'quarter' => 'Q1', 'sales' => 2200],
],
'aggregationField' => 'sales',
When using array data:
- Filters are applied in-memory using Laravel Collections
- All filter operators work the same (
like,gt,between, etc.) - Drill-down functionality is fully supported
External Filtering
Filter pivot data from parent components:
'filters' => [
'year' => 2025,
'region' => ['North', 'South'], // whereIn
'status' => 'active',
],
Reactive Filters from Parent Livewire Component
The filters property is reactive (#[Reactive]), so changes from a parent Livewire component will automatically refresh the pivot:
// Parent component
class SalesPage extends Component
{
public int $selectedYear = 2025;
public function render()
{
return view('sales-page', [
'pivotFilters' => ['year' => $this->selectedYear],
]);
}
}
{{-- sales-page.blade.php --}}
<select wire:model.live="selectedYear">
<option value="2024">2024</option>
<option value="2025">2025</option>
</select>
@livewire('pivot-table-widget', [
'filters' => $pivotFilters,
// ...
])
Drill-Down
Enable drill-down to allow users to click on any cell and see the underlying data records.
'drillDownEnabled' => true,
'drillDownColumns' => [
['name' => 'order_id', 'label' => 'Order', 'type' => 'string'],
['name' => 'customer', 'label' => 'Customer', 'type' => 'string'],
['name' => 'amount', 'label' => 'Amount', 'type' => 'numeric'],
],
How It Works
- Click on any value cell - Cells become clickable with a hover effect
- Modal opens - A Filament modal displays the filtered data
- Automatic filtering - Data is filtered by the row and column dimensions of the clicked cell
- Drill-down filters - Auto-generated SelectFilter dropdowns for further filtering
Example
If your pivot table shows:
| Category | Product | North | South |
|---|---|---|---|
| Clothing | $500 | $300 | |
| T-Shirt | $300 | $200 |
Clicking $300 (T-Shirt / North) opens a modal with all sales records where category = 'Clothing', product = 'T-Shirt', region = 'North'.
Trend Indicators
Show percentage change between adjacent columns with colored arrows.
'showTrends' => true,
- ▲ +15.3% (green) - Value increased vs previous column
- ▼ -8.1% (red) - Value decreased vs previous column
- — (gray) - No previous column to compare
Trend Depth (Multi-level Columns)
When using multi-level columns (e.g., Quarter > Month), trendDepth controls how many column levels display trend indicators:
'showTrends' => true,
'columnDimensions' => ['quarter', 'month'],
'trendDepth' => 1, // Trends only on Quarter level, not on Month
| trendDepth | Behavior |
|---|---|
null (default) |
Trends on all column levels (backward compatible) |
1 |
Trends only on the first column level (e.g., Quarter) |
2 |
Trends on first two levels (e.g., Quarter + Month) |
When a column group is collapsed, the collapsed cell shows the parent-level trend regardless of trendDepth, as long as showTrends is enabled.
Custom Trend Calculation
use PtPlugins\FilamentPivotTable\Components\PivotTableWidget;
PivotTableWidget::trendUsing(function (float $current, float $previous): ?float {
return $current - $previous; // Absolute difference instead of percentage
});
Previous Period Value (First Column)
Supply a custom closure for the first column's comparison value:
PivotTableWidget::previousPeriodValueUsing(
function (string $columnKey, array $rowFilters, string $field): ?float {
return Sale::query()
->where($rowFilters)
->where('quarter', '2024-Q4')
->sum($field);
}
);
Heat Map
Color data cells with dynamic background intensity based on their value.
'showHeatMap' => true,
- Min/max values are calculated automatically across all cells
- Light mode: green tones (opacity 0–0.35)
- Dark mode: lighter green tones (opacity 0–0.35)
- Total cells are not colored
Custom Heat Map Colors
PivotTableWidget::heatMapColorUsing(function (float $value, float $min, float $max): ?string {
$intensity = ($value - $min) / ($max - $min);
return "rgba(59, 130, 246, " . ($intensity * 0.4) . ")"; // Blue theme
});
Combining Trends and Heat Map
'showTrends' => true,
'showHeatMap' => true,
Cells get a heat-colored background with trend arrows below each value.
Styling
The pivot table uses Tailwind CSS classes that can be fully customized via the config file.
Publish the config:
php artisan vendor:publish --tag=pivot-table-config
Then override any class in config/pivot-table.php:
'classes' => [
'container' => 'bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden',
'thead' => 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 border-b-2 border-gray-300 dark:border-gray-600',
'row' => 'hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors',
'cell' => 'px-4 py-2 text-right tabular-nums',
'cell_drilldown' => 'px-4 py-2 text-right tabular-nums cursor-pointer hover:bg-primary-50 dark:hover:bg-primary-900/20',
'grand_total_row' => 'bg-gray-100 dark:bg-gray-800 font-bold',
'expand_button' => 'w-5 h-5 flex-shrink-0 flex items-center justify-center text-gray-500 hover:text-gray-700 rounded hover:bg-gray-200',
// ... see config file for all available keys
],
Preset Actions
Add toolbar buttons that apply predefined configurations or toggle visual options. Override presetActions() in your ListPivotRecords page:
public function presetActions(): array
{
return [
'by-category' => [
'label' => 'By Category',
'icon' => 'grid', // grid, chart, or layers
'pivotConfig' => [
'rows' => ['category', 'region'],
'columns' => ['quarter'],
'value' => 'cost',
'aggregation' => 'sum',
],
],
'by-region' => [
'label' => 'By Region',
'icon' => 'layers',
'pivotConfig' => [
'rows' => ['region'],
'columns' => ['month'],
'value' => 'quantity',
'aggregation' => 'sum',
],
],
'toggle-trends' => [
'label' => 'Toggle Trends',
'icon' => 'chart',
'toggle' => 'showTrends', // toggles the option on/off
],
'toggle-heatmap' => [
'label' => 'Toggle Heatmap',
'icon' => 'chart',
'toggle' => 'showHeatMap',
],
];
}
Preset Action Types
Preset (pivotConfig) — Sets the full pivot configuration (rows, columns, value, aggregation). Can also set showTrends and showHeatMap.
Toggle (toggle) — Toggles a single boolean option on/off. Supported options: showTrends, showHeatMap, showRowTotals, showColumnTotals, showGrandTotal.
Available Icons
| Icon | Description |
|---|---|
grid |
Grid/layout icon (good for presets) |
chart |
Trend/chart icon (good for toggles) |
layers |
Stacked layers icon (good for grouped presets) |
Programmatic Configuration API
Control the pivot table configuration from parent components using Livewire events.
Setting Configuration
$this->dispatch('set-pivot-configuration', [
'rows' => ['category', 'region'],
'columns' => ['quarter'],
'value' => 'cost',
'aggregation' => 'sum',
'showTrends' => true,
'showHeatMap' => true,
'trendDepth' => 1,
'showRowTotals' => false,
'showGrandTotal' => false,
]);
Toggling Options
$this->dispatch('toggle-pivot-option', 'showTrends');
$this->dispatch('toggle-pivot-option', 'showHeatMap');
$this->dispatch('toggle-pivot-option', 'showRowTotals');
$this->dispatch('toggle-pivot-option', 'showColumnTotals');
$this->dispatch('toggle-pivot-option', 'showGrandTotal');
Getting Current Configuration
protected $listeners = [
'pivot-configuration-ready' => 'handlePivotConfiguration',
];
public function requestConfiguration(): void
{
$this->dispatch('request-pivot-configuration',
context: ['action' => 'save', 'saveName' => 'my-config']
);
}
public function handlePivotConfiguration(array $configuration, array $context = []): void
{
// $configuration = ['rows' => [...], 'columns' => [...], 'value' => '...', 'aggregation' => '...']
session(["pivot_config_{$context['saveName']}" => $configuration]);
}
URL Deep Linking
The pivot table automatically syncs state to URL parameters:
/admin/sales-pivot?rows[0]=category&rows[1]=product&cols[0]=quarter&value=amount&agg=sum
Users can share specific configurations, bookmark views, and use browser back/forward.
Custom Views
Register alternative views (charts, graphs) that users can switch to from the Controls panel.
Registering a Custom View
// In AppServiceProvider::boot()
use PtPlugins\FilamentPivotTable\Components\PivotTableWidget;
PivotTableWidget::registerView('pie', 'Pie Chart', 'components.pivot-views.pie-chart');
PivotTableWidget::registerView('bar', 'Bar Chart', 'components.pivot-views.bar-chart');
Available Variables in Custom Views
| Variable | Type | Description |
|---|---|---|
$pivotData |
array | Full pivot data (rows, columns, totals) |
$rawData |
array|null | Raw data array (if using array source) |
$modelClass |
string | Eloquent model class (if using model) |
$filters |
array | Active filters |
$selectedRowDimensions |
array | Current row dimensions |
$selectedColumnDimensions |
array | Current column dimensions |
$selectedAggregationField |
string | Aggregation field |
$selectedAggregationType |
string | Aggregation type |
$valuePrefix |
string | Value prefix |
$valueSuffix |
string | Value suffix |
$name |
string | Pivot table name |
Column Sorting
Click any column header to sort pivot rows by that column's values.
- First click - Sort descending (highest first)
- Second click - Sort ascending (lowest first)
- Third click - Reset to original order
Works with single-level columns, multi-level child headers, and the Total column.
Expand/Collapse
Rows
Click the arrow icon next to any parent row to collapse/expand its children.
Columns
Click a parent column header (e.g., "Q1") to collapse all child columns into a single aggregated column showing the sum.
Export Configuration
Export is client-side (browser-only). CSV uses pure JavaScript, Excel uses SheetJS from CDN. No data sent to server.
'csvExportEnabled' => false, // Hide CSV export
'xlsxExportEnabled' => true, // Show Excel export
Export settings also apply to the drill-down modal. The SheetJS CDN URL can be customized in config/pivot-table.php.
Dimension Reordering
- Open the configuration panel (click "Show Controls")
- Each dimension badge shows arrow buttons: ↑ (up) / ↓ (down)
- The pivot table re-renders immediately with the new order
Translations
php artisan vendor:publish --tag=pivot-table-translations
Available locales: en, sr
Configuration Quick Reference
Financial Report
'rowDimensions' => ['department', 'account'],
'columnDimensions' => ['quarter', 'month'],
'aggregationField' => 'amount',
'aggregationType' => 'sum',
'valuePrefix' => '$',
'showTrends' => true,
'showHeatMap' => true,
'drillDownEnabled' => true,
Inventory Dashboard
'rowDimensions' => ['warehouse', 'product'],
'columnDimensions' => ['month'],
'aggregationField' => 'stock_level',
'aggregationType' => 'avg',
'showHeatMap' => true,
'stickyRowHeaders' => true,
Customer Analytics
'rowDimensions' => ['segment', 'channel'],
'columnDimensions' => ['year', 'quarter'],
'aggregationField' => 'customer_id',
'aggregationType' => 'count',
'showTrends' => true,
Sales Performance
'rowDimensions' => ['region', 'salesperson'],
'columnDimensions' => ['quarter'],
'aggregationField' => 'revenue',
'aggregationType' => 'sum',
'valuePrefix' => '$',
'drillDownEnabled' => true,
'csvExportEnabled' => true,
'xlsxExportEnabled' => true,
Custom Aggregation
You can define your own aggregation logic using the custom aggregation type.
Aggregation Type Constants
Use the AggregationType class for cleaner code:
use PtPlugins\FilamentPivotTable\Enums\AggregationType;
'aggregationType' => AggregationType::SUM, // 'sum'
'aggregationType' => AggregationType::AVG, // 'avg'
'aggregationType' => AggregationType::COUNT, // 'count'
'aggregationType' => AggregationType::MIN, // 'min'
'aggregationType' => AggregationType::MAX, // 'max'
'aggregationType' => AggregationType::PERCENTAGE, // 'percentage'
'aggregationType' => AggregationType::CUSTOM, // 'custom'
Custom Aggregation Callback
Register a custom aggregation function in your AppServiceProvider:
use Illuminate\Support\Collection;
use PtPlugins\FilamentPivotTable\Components\PivotTableWidget;
public function boot(): void
{
// Weighted average example
PivotTableWidget::customAggregationUsing(
function (Collection $data, string $field, float $grandTotal): float|int {
$totalWeight = $data->sum('quantity');
if ($totalWeight === 0) {
return 0;
}
return $data->sum(fn ($row) => $row[$field] * $row['quantity']) / $totalWeight;
}
);
}
Then use it in your widget:
@livewire('pivot-table-widget', [
'aggregationType' => 'custom',
// ...
])
Requirements
- PHP 8.1+
- Laravel 10+
- Filament 3.x / 4.x / 5.x
- Livewire 3.x
License
MIT License. See LICENSE for details.
Support
- Email: plugins@premte.ch