Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/08-styling/04-icons.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,4 @@ Using class `Filament\Support\View\SupportIconAlias`
Using class `Filament\Widgets\View\WidgetsIconAlias`

- `WidgetsIconAlias::CHART_WIDGET_FILTER` - Button of the filter action
- `WidgetsIconAlias::CHART_WIDGET_EMPTY_STATE` - Empty state icon for chart widgets
90 changes: 90 additions & 0 deletions packages/widgets/docs/03-charts.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,96 @@ public function filtersResetAction(Action $action): Action
}
```

## Empty State

When `getData()` method returns an empty array, the chart widget will automatically render an empty state.

To customize this behavior, override the `isEmpty()` method:

```php
public function isEmpty(): bool
{
$data = $this->getCachedData();

return empty($data['datasets'][0]['data'] ?? []);
}
```

### Setting the empty state heading

To customize the heading of the empty state, use the `$emptyStateHeading` property or `getEmptyStateHeading()` method:

```php
protected ?string $emptyStateHeading = 'No data available';
```

```php
public function getEmptyStateHeading(): string | Htmlable
{
return 'No sales yet for ' . $this->filter;
}
```

### Setting the empty state description

To customize the description of the empty state, use the `$emptyStateDescription` property or `getEmptyStateDescription()` method:

```php
protected ?string $emptyStateDescription = 'Check back later once data has been collected.';
```

```php
public function getEmptyStateDescription(): string | Htmlable | null
{
return 'Sales data will appear here once orders are placed.';
}
```

### Setting the empty state icon

To customize the [icon](../styling/icons) of the empty state, use the `$emptyStateIcon` property or `getEmptyStateIcon()` method:

```php
protected string | BackedEnum | null $emptyStateIcon = Heroicon::OutlinedChartBar;
```

```php
public function getEmptyStateIcon(): string | BackedEnum | Htmlable
{
return Heroicon::OutlinedShoppingCart;
}
```

### Adding empty state actions

You can add [Actions](../actions/overview) to the empty state to prompt users to take action using `getEmptyStateActions()` method:

```php
public function getEmptyStateActions(): array
{
return [
Action::make('refresh')
->label('Refresh')
->action('refresh'),
];
}
```

### Using a custom empty state view

You may use a completely custom empty state view by `$emptyState` property or `getEmptyState()` method:

```php
protected ?string $emptyState = 'widgets.charts.custom-empty-state';
```

```php
public function getEmptyState(): View | Htmlable | null
{
return view('widgets.charts.custom-empty-state');
}
```

## Live updating chart data (polling)

By default, chart widgets refresh their data every 5 seconds.
Expand Down
28 changes: 28 additions & 0 deletions packages/widgets/resources/css/chart-widget.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,32 @@
& .fi-wi-chart-text-color {
@apply text-gray-500 dark:text-gray-400;
}

& .fi-wi-chart-empty-state {
@apply -m-6 px-6 py-12;

& .fi-wi-chart-empty-state-content {
@apply mx-auto grid max-w-lg justify-items-center text-center;
}

& .fi-wi-chart-empty-state-icon-bg {
@apply mb-4 rounded-full bg-gray-100 p-3 dark:bg-gray-500/20;

& .fi-icon {
@apply text-gray-500 dark:text-gray-400;
}
}

& .fi-wi-chart-empty-state-heading {
@apply text-base leading-6 font-semibold text-gray-950 dark:text-white;
}

& .fi-wi-chart-empty-state-description {
@apply mt-1 text-sm text-gray-500 dark:text-gray-400;
}

& .fi-wi-chart-actions {
@apply mt-6;
}
}
}
4 changes: 4 additions & 0 deletions packages/widgets/resources/lang/ar/chart.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@

],

'empty' => [
'heading' => 'لا توجد بيانات للعرض',
],

];
4 changes: 4 additions & 0 deletions packages/widgets/resources/lang/en/chart.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@

],

'empty' => [
'heading' => 'No data to display',
],

];
122 changes: 79 additions & 43 deletions packages/widgets/resources/views/chart-widget.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
$filters = $this->getFilters();
$isCollapsible = $this->isCollapsible();
$type = $this->getType();
$isEmpty = $this->isEmpty();
@endphp

<x-filament-widgets::widget class="fi-wi-chart">
Expand Down Expand Up @@ -71,53 +72,88 @@ class="fi-wi-chart-filter-content-actions-ctn"
wire:poll.{{ $pollingInterval }}="updateChartData"
@endif
>
<div
x-load
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('chart', 'filament/widgets') }}"
wire:ignore
data-chart-type="{{ $type }}"
x-data="chart({
cachedData: @js($this->getCachedData()),
maxHeight: @js($maxHeight = $this->getMaxHeight()),
options: @js($this->getOptions()),
type: @js($type),
})"
{{
(new ComponentAttributeBag)
->color(ChartWidgetComponent::class, $color)
->class([
'fi-wi-chart-canvas-ctn',
'fi-wi-chart-canvas-ctn-no-aspect-ratio' => filled($maxHeight),
])
}}
>
<canvas
x-ref="canvas"
@if ($maxHeight)
style="max-height: {{ $maxHeight }}"
@endif
></canvas>
@if ($isEmpty)
@if ($emptyState = $this->getEmptyState())
{{ $emptyState }}
@else
<div class="fi-wi-chart-empty-state">
<div class="fi-wi-chart-empty-state-content">
<div class="fi-wi-chart-empty-state-icon-bg">
{{ \Filament\Support\generate_icon_html($this->getEmptyStateIcon(), size: \Filament\Support\Enums\IconSize::Large) }}
</div>

<span
x-ref="backgroundColorElement"
class="fi-wi-chart-bg-color"
></span>
<h2 class="fi-wi-chart-empty-state-heading">
{{ $this->getEmptyStateHeading() }}
<h2>

<span
x-ref="borderColorElement"
class="fi-wi-chart-border-color"
></span>
@if (filled($emptyStateDescription = $this->getEmptyStateDescription()))
<p class="fi-wi-chart-empty-state-description">
{{ $emptyStateDescription }}
</p>
@endif

<span
x-ref="gridColorElement"
class="fi-wi-chart-grid-color"
></span>
@if ($emptyStateActions = array_filter(
$this->getEmptyStateActions(),
fn (\Filament\Actions\Action | \Filament\Actions\ActionGroup $action): bool => $action->isVisible()
))
<div class="fi-wi-chart-actions fi-align-center fi-wrapped">
@foreach ($emptyStateActions as $action)
{{ $action }}
@endforeach
</div>
@endif
</div>
</div>
@endif
@else
<div
x-load
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('chart', 'filament/widgets') }}"
wire:ignore
data-chart-type="{{ $type }}"
x-data="chart({
cachedData: @js($this->getCachedData()),
maxHeight: @js($maxHeight = $this->getMaxHeight()),
options: @js($this->getOptions()),
type: @js($type),
})"
{{
(new ComponentAttributeBag)
->color(ChartWidgetComponent::class, $color)
->class([
'fi-wi-chart-canvas-ctn',
'fi-wi-chart-canvas-ctn-no-aspect-ratio' => filled($maxHeight),
])
}}
>
<canvas
x-ref="canvas"
@if ($maxHeight)
style="max-height: {{ $maxHeight }}"
@endif
></canvas>

<span
x-ref="textColorElement"
class="fi-wi-chart-text-color"
></span>
</div>
<span
x-ref="backgroundColorElement"
class="fi-wi-chart-bg-color"
></span>

<span
x-ref="borderColorElement"
class="fi-wi-chart-border-color"
></span>

<span
x-ref="gridColorElement"
class="fi-wi-chart-grid-color"
></span>

<span
x-ref="textColorElement"
class="fi-wi-chart-text-color"
></span>
</div>
@endif
</div>
</x-filament::section>
</x-filament-widgets::widget>
11 changes: 10 additions & 1 deletion packages/widgets/src/ChartWidget.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

namespace Filament\Widgets;

use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Support\RawJs;
use Illuminate\Contracts\Support\Htmlable;
use Livewire\Attributes\Locked;

abstract class ChartWidget extends Widget implements HasSchemas
abstract class ChartWidget extends Widget implements HasSchemas, HasActions
{
use Concerns\CanPoll;
use ChartWidget\Concerns\HasEmptyState;
use InteractsWithActions;
use InteractsWithSchemas;

/**
Expand Down Expand Up @@ -127,4 +131,9 @@ public function isCollapsible(): bool
{
return $this->isCollapsible;
}

public function isEmpty(): bool
{
return empty($this->getCachedData());
}
}
53 changes: 53 additions & 0 deletions packages/widgets/src/ChartWidget/Concerns/HasEmptyState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Filament\Widgets\ChartWidget\Concerns;

use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Support\Facades\FilamentIcon;
use Filament\Support\Icons\Heroicon;
use Filament\Widgets\View\WidgetsIconAlias;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Contracts\View\View;

trait HasEmptyState
{
protected ?string $emptyState = null;

protected ?string $emptyStateDescription = null;

protected ?string $emptyStateHeading = null;

protected string | BackedEnum | null $emptyStateIcon = null;

public function getEmptyState(): View | Htmlable | null
{
return is_string($this->emptyState) ? view($this->emptyState) : null;
}

/**
* @return array<Action | ActionGroup>
*/
public function getEmptyStateActions(): array
{
return [];
}

public function getEmptyStateDescription(): string | Htmlable | null
{
return $this->emptyStateDescription;
}

public function getEmptyStateHeading(): string | Htmlable
{
return $this->emptyStateHeading ?? __('filament-widgets::chart.empty.heading');
}

public function getEmptyStateIcon(): string | BackedEnum | Htmlable
{
return $this->emptyStateIcon
?? FilamentIcon::resolve(WidgetsIconAlias::CHART_WIDGET_EMPTY_STATE)
?? Heroicon::OutlinedXMark;
}
}
2 changes: 2 additions & 0 deletions packages/widgets/src/View/WidgetsIconAlias.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
class WidgetsIconAlias
{
const CHART_WIDGET_FILTER = 'widgets::chart-widget.filter';

const CHART_WIDGET_EMPTY_STATE = 'widgets::chart-widget.empty-state';
}
Loading