Segmented Button Filter for Chart Widgets

Filament 4/5

Swap Filament's default chart widget dropdown for a segmented pill-style filter — driven by a single $view override and Livewire's built-in wire:click="$set('filter', ...)" magic, with no JavaScript or extra component class.

New Project

Get the Source Code:

How it works

This project demonstrates how to replace a Filament chart widget's default dropdown filter with a segmented button group rendered directly inside the widget header. The widget itself stays a standard ChartWidget — only the Blade view is swapped, so getFilters(), the $filter property, sorting, polling, and Chart.js options all keep working exactly as Filament intends.

The key idea: Filament already wires the $filter property to a Livewire-reactive update path. Once you override the chart widget's view, rendering a <button wire:click="$set('filter', ...)"> per filter option is enough to turn that dropdown into a pill toggle — no new component class, no Alpine, no JavaScript.

The repository contains the complete Laravel + Filament project to demonstrate the functionality, including migrations and seeded weekly performance data.


How to install

  • Clone the repository with git clone
  • Copy the .env.example file to .env and edit database credentials there
  • Run composer install
  • Run php artisan key:generate
  • Run php artisan storage:link
  • Run php artisan migrate --seed (it has some seeded data for your testing)
  • Run npm ci and npm run build
  • That's it: launch the /admin and log in with credentials [email protected] and password to manage companies.

Screenshots


How It Works

The project has a single chart widget that reports the weekly engagement rate against an industry average. The filter chooses which traffic source to slice on — All, Organic Social, Paid Ads, or SEO — and the chart re-renders through Livewire on every click. The interesting part is that none of the chart logic changes when going from dropdown to buttons.

1. The Widget — Standard ChartWidget with a custom view

The widget is a plain ChartWidget. The only line that opts it out of Filament's default filter UI is the $view override pointing at a custom Blade file.

app/Filament/Widgets/EngagementRateChart.php

class EngagementRateChart extends ChartWidget
{
public ?string $filter = 'all';
 
protected ?string $heading = 'Engagement Rate';
 
protected ?string $description = 'Trailing 10 weeks';
 
protected string $view = 'filament.widgets.engagement-rate-chart';
 
protected function getFilters(): ?array
{
return [
'all' => 'All',
TrafficSource::OrganicSocial->value => TrafficSource::OrganicSocial->label(),
TrafficSource::PaidAds->value => TrafficSource::PaidAds->label(),
TrafficSource::Seo->value => TrafficSource::Seo->label(),
];
}
 
protected function getData(): array
{
$series = PerformanceMetric::weeklyTotals($this->selectedTrafficSource());
 
return [
'datasets' => [/* ... You / Industry average ... */],
'labels' => $series
->map(fn (object $week): string => $week->week_start_date->format('M j'))
->all(),
];
}
 
private function selectedTrafficSource(): ?TrafficSource
{
return TrafficSource::tryFrom($this->filter ?? '');
}
}

The key points:

  • public ?string $filter = 'all'; is the same property Filament's default dropdown writes to — the custom buttons reuse it via wire:click="$set('filter', ...)", so swapping UIs requires zero changes to the data layer
  • getFilters() returns an option_value => label map exactly as it would for the default dropdown — the array shape is the contract between widget and view, not between widget and dropdown
  • protected string $view = 'filament.widgets.engagement-rate-chart'; is the entire opt-in to the custom UI; without it, Filament renders its built-in chart widget view and ignores the Blade file in resources/views
  • selectedTrafficSource() uses TrafficSource::tryFrom() so the literal string 'all' resolves to null, which the forTrafficSource() scope on the model interprets as "no filter" — one helper handles both the enum cases and the catch-all bucket without an if/else chain
The FULL tutorial is available after the purchase: in the Readme file of the official repository you would get invited to.
Get the Source Code: All 167 Premium Examples for $99