Skip to content

refactor: Chart.js implementation in chart widgets#19681

Open
ahmed-rashad-alnaggar wants to merge 10 commits intofilamentphp:4.xfrom
ahmed-rashad-alnaggar:refactor/chartjs-implementation-in-chart-widget
Open

refactor: Chart.js implementation in chart widgets#19681
ahmed-rashad-alnaggar wants to merge 10 commits intofilamentphp:4.xfrom
ahmed-rashad-alnaggar:refactor/chartjs-implementation-in-chart-widget

Conversation

@ahmed-rashad-alnaggar
Copy link
Copy Markdown
Contributor

This is Phase 1 of 4.

Adhering to the feedback from @People-Sea comment, this PR refactors the Chart widget's Chart.js implementation. This is a pure refactor; no functional changes or new features are introduced (except animation).


What’s Changed

  • Removed Global Defaults Mutation: Stopped the practice of overriding global Chart.defaults (e.g. backgroundColor, borderColor, color, font.family) inside initChart(). Visual fallbacks (colors, fonts, etc.) are now applied locally to the per-chart options object instead. The two genuinely global defaults (legend.labels.boxWidth and legend.position) are hoisted to module level, where they belong.

  • Removed Animation Duration Default: The line Chart.defaults.animation.duration = 0 was removed.

  • Cleaner Event Handling: Moved logic for theme changes, data updates, and resizing into dedicated methods (updateChartTheme, updateChartData, resizeChart) instead of bulky inline anonymous functions.

  • initChart renamed to _initChart() and its data parameter removed: The leading
    underscore signals that this is an internal method not intended to be called directly.
    The data = null parameter was also dropped — _initChart now always reads from
    cachedData directly, since updateChartData handles live data updates independently.

  • Helper-Driven Architecture:

    • Introduced whenChart() to centralize null-safety checks before interacting with the Chart.js instance, eliminating three near-identical guard blocks that were duplicated across event handlers.
    • Extracted all getComputedStyle calls into getChartFallbackColors() for better readability and easier testing.
  • Decoupled Aspect Ratio Logic: options.maintainAspectRatio is now derived by directly checking this.$refs.canvas.style.maxHeight !== '' instead of relying on the maxHeight prop passed from outside. This decouples the logic from external property drilling and removes an unnecessary function parameter.

  • Minor — destroy() uses optional chaining: Replaced the manual if (this.resizeObserver) guard with this.resizeObserver?.disconnect().

Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com>
Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com>
Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com>
@ahmed-rashad-alnaggar
Copy link
Copy Markdown
Contributor Author

Upcoming Phases

To keep pull requests concise and maintainable, the following updates will be handled in separate stages:

  • Phase 2: Implement dynamic chart resizing (eliminating the need to re-initialize the instance).

  • Phase 3: Enable seamless theme updates without full re-initialization.

  • Phase 4: Add logic for reactive chart data updates.

Note: This staged approach ensures each PR remains focused and specific, as requested.

@danharrin danharrin added this to the v4 milestone Apr 12, 2026
Copy link
Copy Markdown
Member

@People-Sea People-Sea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I think I need to see where the rest of this is going before I can judge this phase confidently on its own.

The part I’m most concerned about is the manual dataset/state diff/state-sync path. I understand it is being introduced to support in-place updates, but if this approach is going to work, that part needs to be extremely robust. It is also the same area that caused issues in the previous PR, and a big reason that PR felt too broad to me.

Since this is already being presented in phases, this intermediate step is hard to evaluate in isolation without seeing the intended end state more clearly.

If you continue with the rest of the plan, it would help a lot if you could include the test scenarios you’re validating, along with a few short videos. That would make the review much easier.

@ahmed-rashad-alnaggar
Copy link
Copy Markdown
Contributor Author

Just to confirm I understood correctly — we’re good to proceed with this phase, and for the upcoming update/state-sync part you’d like me to include test scenarios and short videos demonstrating the behavior, right?

@People-Sea
Copy link
Copy Markdown
Member

People-Sea commented Apr 16, 2026

Yes, you can continue and submit the full implementation.

But it’s worth noting that, if the full approach still relies on the same manual dataset/state diff/state-sync shadow layer as before, then I would still consider it a broad and high-risk change. If that layer is not clearly proven stable first, I don’t think we’re even at the point of meaningfully validating whether moving to update()/resize() avoids regressions.

Put simply, I think this approach can only move forward if you can show that the frontend state is being maintained correctly and does not introduce new issues. We should not introduce instability just to improve performance or visual behavior.

Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com>
Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com>
Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com>
@ahmed-rashad-alnaggar ahmed-rashad-alnaggar marked this pull request as draft April 16, 2026 18:36
@ahmed-rashad-alnaggar
Copy link
Copy Markdown
Contributor Author

This is Phase 2 of 4.

Building on the refactor in Phase 1, this PR implements dynamic chart resizing — eliminating the destroy-and-reinitialize cycle that previously occurred on every resize event.


What's Changed

  • resizeChart() no longer destroys the instance: Previously, resizeChart() called
    chart.destroy() followed by _initChart(), which created a completely new Chart.js instance
    on every resize. It now calls chart.resize() directly, letting Chart.js handle dimension
    updates in-place.

  • options.responsive set to false: With responsive: true (Chart.js's default),
    Chart.js monitors and controls the canvas size itself — which conflicts with our
    ResizeObserver-driven approach. Setting responsive: false hands full sizing control to the
    CSS layer, making chart.resize() the single explicit trigger for dimension updates.

  • Canvas receives explicit CSS dimensions via @style: The canvas style attribute is
    replaced with a @style([...]) directive that always applies width: 100%, and conditionally
    applies either height: 100%; max-height: 100% (when no $maxHeight is set) or
    max-height: {$maxHeight} (when one is). This gives the canvas a reliable, CSS-defined size
    at all times — a prerequisite for chart.resize() to read the correct dimensions.

  • hasMaxHeight condition tightened: The check in _initChart() was updated from !== ''
    to !== '100%' to align with the new canvas style output, where a canvas without a
    user-defined max-height now explicitly receives max-height: 100% rather than an empty style.

  • _initChart() and ResizeObserver setup deferred to $nextTick: Both calls are now
    wrapped in $nextTick() inside init(), ensuring the DOM is fully rendered before the chart
    is initialized and the observer is attached. This also resolves an unwanted behavior caused by
    Alpine.effect's immediate execution: in the old code, _initChart() ran first, then
    Alpine.effect was defined — and because Alpine.effect invokes its callback once on
    definition, it immediately triggered updateChartTheme(), which destroyed and re-initialized
    the freshly created chart instance on every page load for no reason. By deferring
    _initChart() to $nextTick, the effect's initial run finds no instance yet and exits early
    via whenChart(), leaving the chart to be initialized exactly once.

  • The Alpine.debounce() wrapper on the ResizeObserver callback was removed — aside from being
    unnecessary now that resizing is a lightweight chart.resize() call rather than a full
    destroy-and-reinitialize, the debounce delay was itself the source of a visible flicker during
    resize.

  • Minor — filled($maxHeight) replaced with $maxHeight: The Blade class condition for
    fi-wi-chart-canvas-ctn-no-aspect-ratio was simplified from filled($maxHeight) to
    $maxHeight, since $maxHeight is already a falsy-safe value in this context.

Screen Recording

Note: Any perceived slowness in the recordings is due to hardware limitations of the recording device, not the implementation itself.

Before

filament-chart-resize-old.webm

After

filament-chart-resize-new.webm

Added user-defined color options for chart customization.

Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com>
Signed-off-by: ahmed-rashad-alnaggar <131385452+ahmed-rashad-alnaggar@users.noreply.github.com>
@ahmed-rashad-alnaggar
Copy link
Copy Markdown
Contributor Author

This is Phase 3 of 4.

Building on the in-place resize introduced in Phase 2, this PR eliminates the remaining destroy-and-reinitialize cycle — the one that fired on every theme change.


What's Changed

  • updateChartTheme() no longer destroys the instance: Previously, updateChartTheme() called chart.destroy() followed by this._initChart(), discarding the entire Chart.js instance on every light/dark switch or system theme change. It now directly mutates chart.options with the updated color values and calls chart.update('none') — applying the new theme in-place with no instance recreation.

  • Three new properties: userBackgroundColor, userBorderColor, and userTextColor — are now captured from the incoming options object at component initialization, mirroring the existing pattern for userPointBackgroundColor and the grid color properties, for updateChartTheme() to correctly re-apply user-supplied colors after a theme change

Screen Recording

filament-chart-theme-change.webm

@ahmed-rashad-alnaggar ahmed-rashad-alnaggar marked this pull request as ready for review April 17, 2026 05:46
@ahmed-rashad-alnaggar
Copy link
Copy Markdown
Contributor Author

@People-Sea

I think it would make sense for this to be the final phase in this PR.

The remaining work in Phase 4 (updating/syncing chart data with reactive and animation logic) feels like a separate concern from the refactor and lifecycle improvements covered in Phases 1–3. It might be cleaner to handle that in a dedicated follow-up PR, so this one stays focused and easier to review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants