Search code examples
javascriptchart.jslaravel-livewire

Why in livewire app chartjs element lose canvas properties on data refresh?


In laravel 10 / livewire 3 app I make chartjs report based on data from db and 4 filters. It work for me ok with blade file :

    <div class="editor_field_block_wrapper">
        <div class="editor_field_block_device_splitter">
            <div class="w-4/12 pb-0 pl-2 md:pt-3 ">
                <label for="filterModelType" class="editor_field_block_device_label">Model type: <span
                        class="editor_form_aria_required" aria-required="true"> * </span></label>
            </div>
            <div class="p-2 w-full">
                <select wire:model.blur="filterModelType" class="editor_form_input" tabindex="20"
                        id="filterModelType">
                    <option value=""> - Select all -</option>
                    @foreach($modelTypeSelectionItems as $key=>$label)
                    <option value="{{$key}}">{{$label}}</option>
                    @endforeach
                </select>
                @error('filterModelType')
                <span class="error_text">{{$message}}</span>
                @enderror
            </div>
        </div>
    </div>

    <div class="editor_field_block_wrapper">
        <div class="editor_field_block_device_splitter">
            <div class="w-4/12 pb-0 pl-2 md:pt-3 ">
                <label for="filterDateFrom" class="editor_field_block_device_label">From date</label>
            </div>
            <div class="p-2 w-full">
                <x-inputs.datepicker id="filterDateFrom" wire:model.lazy="filterDateFrom" tabindex="20"/>
                @error('filterDateFrom') <span
                    class="editor_form_validation_error">{{ $message }}</span> @enderror
            </div>
        </div>
    </div>

    <div class="editor_field_block_wrapper">
        <div class="editor_field_block_device_splitter">
            <div class="w-4/12 pb-0 pl-2 md:pt-3 ">
                <label for="filterDateTill" class="editor_field_block_device_label">Till date</label>
            </div>
            <div class="p-2 w-full">
                <x-inputs.datepicker id="filterDateTill" wire:model.lazy="filterDateTill" tabindex="30"/>
                @error('filterDateTill') <span
                    class="editor_form_validation_error">{{ $message }}</span> @enderror
            </div>
        </div>
    </div>
</fieldset>

@if($this->totalReactionCount > 0)
    <div class="min-w-full">
            <canvas id="reactionsChartStatisticsChart" style="background: #000000"></canvas>
        </div>
        @else
        <div class="m-2 p-2 warning_text">
            {!! AppIconFacade::get(IconEnum::Warning ) !!}
            There are no data found !
        </div>
        @endif

        @assets
        <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
        @endassets

        @script
        <script>
            const ctx = document.getElementById('reactionsChartStatisticsChart');
            const reactions = $wire.reactions;
            const labels = reactions.map(item => item.action_label)
            const values = reactions.map(item => item.reaction_count)

            var chartObj = new Chart(ctx, {
                type: 'pie',
                data: {
                    labels: labels,
                    datasets: [{
                        label: '# of Reactions',
                        data: values,
                        borderWidth: 2
                    }]
                },
                options: {
                    scales: {
                        y: {
                            beginAtZero: true
                        }
                    },

                    plugins: {
                        legend: {
                            position: 'top',
                            labels: {
                                font: {
                                    size: 18
                                },
                                fontColor: 'white'
                            }
                        },
                        title: {
                            display: true,
                            text: 'Reactions by type with total quality {{ $totalReactionCount }}'
                        }
                    }
                }
            });

        </script>
        @endscript


        console.log(chartObj)
        // chartObj.update()   // IF TO UNCOMMENT - RAISED ERROR : Uncaught SyntaxError: Unexpected end of input
        // chartObj.refresh()  // IF TO UNCOMMENT - RAISED ERROR : chartObj.refresh is not a function


    </div>
</div>

and when the page is reloaded for the first time I have a valid chart with max width in wrapping html elements :

enter image description here

When I change some of filter parameters data are reloaded and chart is small in size :

enter image description here

for the third filter changed reactionsChartStatisticsChart element lost its chart propertied and I have black block :

enter image description here

Searching in net I found some chartObj.update() methods, but they did not work for me.

In which way can I check/fix chart ?

Data source component code:

In ReactionsChartStatistics.php component class I have :

<?php

namespace App\Livewire\Admin;
use App\Enums\ReactionActionEnum;
use App\Models\Reaction;
...
class ReactionsChartStatistics extends Component
{
    public array $reactions = [];
    public int $totalReactionCount = 0;

    public function mount()
    {
        // Get data for filtering block
        $this->usersSelectionItems = User::get()->pluck('name','id')->toArray();
        $this->modelTypeSelectionItems = $this->getModelTypeSelectionItems();

        $statisticsDays = ConfigValueEnum::get(ConfigValueEnum::NEWS_REACTIONS_STATISTICS_DAYS);
        $this->filterDateFrom = Carbon::now()->addDays(-$statisticsDays)->startOfDay();
        $this->filterDateTill = Carbon::now()->endOfDay();
    }
    public function render(): View
    {

        $reactionTb = (new Reaction)->getTable();
        $this->totalReactionCount = 0;
            $this->reactions = Reaction
                ::getByCreatedAt($this->filterDateFrom, '>=')
                ->getByCreatedAt($this->filterDateTill, '<')
                ->groupBy('action')
                ->orderBy('reaction_count', 'desc')
                ->select(
                    $reactionTb . '.action',
                    DB::raw('count(' . $reactionTb . '.id) as reaction_count'))
                ->get()->toArray();
        }

        foreach ($this->reactions as $key => $reaction) {
            $this->reactions[$key]['action_label'] = ReactionActionEnum::getLabel($reaction['action']);
            $this->totalReactionCount += $reaction['reaction_count'];
        }

        return view('livewire.admin.reactions-chart-statistics')->layout('components.layouts.admin');
    }

}

Solution

  • Add a wire:ignore in the <div> containing the graph to prevent Livewire from updating it:

    @if($this->totalReactionCount > 0)
    
        <div wire:ignore class="min-w-full">  {{--  HERE -- }}
    

    Edit

    To refresh the chart when the inputs are updated we can send an event from the backend.

    The class

    class ReactionsChartStatistics extends Component
    {
        public array $reactions = [];
        public int $totalReactionCount = 0;
    
        public $filterModelType;
        public $filterDateFrom;
        public $filterDateTill;
        public $modelTypeSelectionItems;
    
    
        protected function prepareData()
        {
            // This code was previously found in the render() method
    
            $reactionTb = (new Reaction)->getTable();
    
            $this->totalReactionCount = 0;
    
            $this->reactions = Reaction
                 ::getByCreatedAt($this->filterDateFrom, '>=')
                 ->getByCreatedAt($this->filterDateTill, '<')
                 ->groupBy('action')
                 ->orderBy('reaction_count', 'desc')
                 ->select(
                      $reactionTb . '.action',
                      DB::raw('count(' . $reactionTb . '.id) as reaction_count'))
                 ->get()->toArray();
            }
    
            foreach ($this->reactions as $key => $reaction) {
                $this->reactions[$key]['action_label'] = ReactionActionEnum::getLabel($reaction['action']);
                $this->totalReactionCount += $reaction['reaction_count'];
            }
        }
    
    
        public function mount()
        {
            // Get data for filtering block
            $this->usersSelectionItems = User::get()->pluck('name','id')->toArray();
            $this->modelTypeSelectionItems = $this->getModelTypeSelectionItems();
    
            $statisticsDays = ConfigValueEnum::get(ConfigValueEnum::NEWS_REACTIONS_STATISTICS_DAYS);
            $this->filterDateFrom = Carbon::now()->addDays(-$statisticsDays)->startOfDay();
            $this->filterDateTill = Carbon::now()->endOfDay();
    
           $this->prepareData(); // <~~~ Added 
        }
    
    
        // Raises an event when the parameters are updated
        public function updated($property, $value)
        {
            $this->prepareData();
            $this->dispatch('refresh-chart');
        }
    
    
        public function render()
        {
            return view('livewire.admin.reactions-chart-statistics')->layout('components.layouts.admin');
        }
    }
    

    The javascript in the view

    @script
    <script>
    
        const ctx = document.getElementById('reactionsChartStatisticsChart');
    
        const getLabels = () => $wire.reactions.map(item => item.action_label);
        const getValues = () => $wire.reactions.map(item => item.reaction_count);
    
        let chartObj = new Chart(ctx, {
    
            type: 'pie',
    
            data: {
                labels: getLabels(),
                datasets: [{
                    label: '# of Reactions',
                    data: getValues(),
                    borderWidth: 2
                }]
            },
    
            options: {
    
                scales: {
                    y: { beginAtZero: true }
                },
    
                plugins: {
                    legend: {
                        position: 'top',
                        labels: {
                            font: { size: 18 },
                            fontColor: 'white'
                        }
                    },
    
                    title: {
                        display: true,
                        text: 'Reactions by type with total quality {{ $totalReactionCount }}'
                    }
                }
            }
        });
    
        // This event listener updates the chart 
        $wire.on("refresh-chart", () => {
    
            chartObj.data.labels = getLabels();
            chartObj.data.datasets = [{ data: getValues() }];
    
            chartObj.update()
        });
    
    </script>
    @endscript
    

    In the backend I've moved the data preparation to a specific method prepareData() which is called initially in the mount() method and then in the updated() method. The updated() method is invoked by Livewire everytime an input changes value and is used also to dispatch the refresh-chart event.

    In the frontend I've added the event listener $wire.on("refresh-chart", ....) which updates the chart data and then calls chartObj.update() to update the DOM

    wire:ignore in the chart <div> is still required