Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Plotly.js Laravel Package

plotly/plotly.js

Plotly.js is an open-source JavaScript charting library for interactive, publication-quality graphs. It supports 40+ chart types, including statistical, 3D, scientific, and SVG/WebGL visualizations, with declarative APIs and MIT licensing.

Deep Wiki
Context7

Getting Started

Plotly.js is a client-side JavaScript library — there is no Composer package to install and no PHP API to learn. Your integration work in Laravel is therefore about delivery (Vite/npm vs CDN), data shaping (controller → JSON), and lifecycle management (mount/update/destroy) inside Blade, Alpine, Livewire, or Inertia.

Minimal path:

  1. Install via npm + Vite (preferred over CDN so you control versioning and can tree-shake):

    npm install plotly.js-dist-min
    

    Import once in resources/js/app.js, or lazily per page (see Gotchas for bundle size):

    import Plotly from 'plotly.js-dist-min';
    window.Plotly = Plotly;
    
  2. Return chart-ready data from the controller. Don't build Plotly config in Blade string literals; keep the shape in PHP and let @json handle escaping:

    public function index()
    {
        $rows = Order::query()
            ->selectRaw('DATE(created_at) as day, SUM(total) as revenue')
            ->groupBy('day')->orderBy('day')->get();
    
        return view('dashboard', [
            'chart' => [
                'data' => [[
                    'type' => 'scatter', 'mode' => 'lines+markers',
                    'x' => $rows->pluck('day'),
                    'y' => $rows->pluck('revenue'),
                    'name' => 'Revenue',
                ]],
                'layout' => ['margin' => ['t' => 20], 'height' => 360],
            ],
        ]);
    }
    
  3. Mount it in Blade with a pushed script and a container that has a height:

    <div id="revenue-chart" style="height:360px"></div>
    
    @push('scripts')
    <script>
      const chart = @json($chart);
      Plotly.newPlot('revenue-chart', chart.data, chart.layout, {
        responsive: true,
        displaylogo: false,
      });
    </script>
    @endpush
    

Where to look first in the docs: Plotly.newPlot / Plotly.react / Plotly.purge, the layout reference (axes, margin, autosize, template), the config object (responsive, displayModeBar, scrollZoom), and the "Partial bundles" page for trimming the ~3 MB default build.

First real use case: a metrics dashboard page where the controller aggregates with SQL (GROUP BY) and hands one or more {data, layout} payloads to the view. That covers 80% of day-to-day usage; interactive drill-downs are the natural second step.

Implementation Patterns

Controller → JSON endpoint for filtered charts. For anything with date pickers or dropdown filters, don't reload the page. Add a route returning the same {data, layout} shape and call it with fetch:

Route::get('/api/charts/revenue', fn (Request $r) => response()->json(
    RevenueChart::make($r->date('from'), $r->date('to'))
));

Keep the chart-shaping logic in a small App\Support\Charts\* class so the dashboard and the API share one implementation. Cache expensive aggregates (Cache::remember) keyed on the filter set.

Reusable Blade component + Alpine wrapper. This is the most idiomatic Laravel pattern and avoids scattering <script> blocks:

{{-- resources/views/components/chart.blade.php --}}
@props(['id', 'spec', 'config' => []])
<div id="{{ $id }}" style="height:{{ $spec['layout']['height'] ?? 360 }}px"
     x-data="plotlyChart(@js($spec), @js($config))"></div>
// resources/js/chart.js
document.addEventListener('alpine:init', () => {
  Alpine.data('plotlyChart', (spec, config) => ({
    chart: null,
    init() {
      this.chart = Plotly.newPlot(this.$el, spec.data, spec.layout, {
        responsive: true, displaylogo: false, ...config,
      });
      this.$watch('spec', s => Plotly.react(this.$el, s.data, s.layout, config));
    },
    destroy() { Plotly.purge(this.$el); },
  }));
});

Register it with Alpine.data globally so every chart on the page is declarative.

Livewire: wire:ignore plus explicit re-render. Livewire's DOM diffing will destroy Plotly's generated SVG. Wrap the container in wire:ignore, then listen for a browser event and call Plotly.react yourself:

<div wire:ignore id="rev" style="height:360px"
     x-data
     x-on:chart-updated.window="Plotly.react($el, $event.detail.data, $event.detail.layout)">
</div>
$this->dispatch('chart-updated', data: $this->chartData(), layout: $this->layout());

Never let Livewire re-render the div's children — only push data through events.

Inertia + Vue/React. Mount in onMounted, update in a watch, and always Plotly.purge(el) in onUnmounted. Forgetting the purge is the #1 cause of memory bloat and "ghost" resize listeners in SPA navigation.

Use Plotly.react, not newPlot, for updates. newPlot tears down and rebuilds (flicker, loses zoom state). react diffs. If the shape changes (different trace count/types), pass layout.datarevision or just newPlot again after purge — react can behave oddly when trace types change.

Theme from CSS variables. Don't hard-code colors. Read your Tailwind/theme tokens and inject them so dark mode "just works":

const css = getComputedStyle(document.documentElement);
const ink = css.getPropertyValue('--color-ink').trim();
const layout = { ...base, paper_bgcolor: 'transparent', plot_bgcolor: 'transparent',
                 font: { color: ink }, xaxis: { gridcolor: css.getPropertyValue('--color-grid') } };

Re-render on theme toggle by calling Plotly.relayout(el, {font: {color: newInk}}).

Drill-down via plot events. el.on('plotly_click', e => { ... }), fetch a detail endpoint, then Plotly.react a second chart or open a modal. Use customdata to carry IDs/URLs so you don't have to reverse-lookup from x/y.

Chart → image for reports. Plotly.toImage(el, {format: 'png', width: 1200, height: 600}) or Plotly.downloadImage. For server-generated PDFs (DomPDF/Snappy/Browsershot), the reliable route is Browsershot rendering the real page (Plotly needs a browser) — Plotly's own static export is Python's kaleido, which has no PHP equivalent. Don't promise server-side Plotly rendering in pure PHP.

Bundle trimming with partial bundles. If you only use a handful of trace types, import plotly.js/lib/core and register:

import Plotly from 'plotly.js/lib/core';
import scatter from 'plotly.js/lib/scatter';
import bar from 'plotly.js/lib/bar';
Plotly.register([scatter, bar]);

This is the single biggest perf win (3 MB → ~500 KB–1 MB).

Lazy-load per page with dynamic import. In Vite, const Plotly = await import('plotly.js-dist-min') inside the chart component keeps dashboards fast elsewhere. Combine with manualChunks in vite.config.js if you have several heavy pages.

Testing. In Dusk/Pest browser tests, assert on div.js-plotly-plot presence and on your data attributes/window state — not on generated SVG paths or <g> classes, which change between Plotly versions. Assert the JSON payload in a Feature test separately; that's where the real logic lives.

Gotchas and Tips

  • Bundle size will bite you. The default plotly.js import is enormous. Always prefer plotly.js-dist-min or partial bundles, and never import it into app.js if only one admin page needs charts.
  • Containers need explicit height. A div with no height produces a 0px plot with no error. Set height inline or via a class; responsive: true only handles width.
  • Hidden tabs / accordions render at zero width. If the chart lives in a collapse/tab, mount it after the container is visible, or call Plotly.Plots.resize(el) in the tab's shown callback.
  • Livewire destroys your chart silently. Symptom: chart disappears or throws Cannot read properties of null. Fix: wire:ignore on the container, drive updates with dispatched events.
  • Always Plotly.purge on teardown. Especially with Livewire wire:navigate, Inertia visits, and modals. Leaked instances keep resize observers and event handlers alive.
  • json_encode fails on NaN/INF. A single bad aggregate (e.g. AVG over empty set, division by zero) returns false and @json emits nothing — the chart silently gets no data. Cast/round in PHP and replace non-finite values with null. Plotly treats null as a gap; NaN is invalid JSON.
  • Dates and timezones. MySQL DATE() returns strings like 2025-01-01, which Plotly parses as UTC. If users are in another timezone the axis can look off by a day. Send ISO 8601 strings with offset, or use type: 'date' plus explicit tickformat, or send epoch millis.
  • Numeric precision from DECIMAL columns. PDO returns strings; Plotly will coerce but sorting/aggregation on the client can misbehave. Cast to float server-side for y arrays.
  • @json in a @push block is fine, but avoid building JS with Blade interpolation. {{ $userInput }} inside a JS string is an XSS and quoting hazard. @json(...) / Js::from(...) handles escaping.
  • Hover text can be injected. If you put user-controlled values into hovertemplate or text, HTML in the tooltip renders. Sanitize or use hovertext with hoverinfo: 'text'.
  • CSP. Plotly relies heavily on inline styles (and some dynamic code paths). A strict style-src without 'unsafe-inline' will break rendering. Either relax the policy for chart pages or accept the trade-off — there's no clean nonce story.
  • Datasets over ~10k points. Switch to scattergl (WebGL), drop markers, set hoverinfo: 'skip' or 'x+y', and downsample server-side (LTTB or bucketed averages). Plotly's client-side cost is dominated by SVG node count.
  • Zoom state resets. Calling newPlot clears zoom/pan. Use react for data ticks and relayout for cosmetic changes; only full newPlot when the trace schema changes.
  • Multiple charts: unique IDs and scoped lookups. Use Plotly.newPlot(document.getElementById(id), ...), not string IDs, if you have several charts or dynamically named containers — string IDs collide inside Blade loops.
  • Version pinning matters. Plotly ships frequently (v3 line has notable config/behavior changes). Pin the exact version in package.json, and read the changelog before minor bumps if you rely on react update semantics or hover template behavior.
  • config is not part of layout. responsive, displayModeBar, scrollZoom, toImageButtonOptions live in the config argument, not layout. Passing them in layout silently does nothing — a common source of "responsive doesn't work" reports.
  • Mobile. Set config: { scrollZoom: false, displayModeBar: false } on small screens, or the chart hijacks page scroll.
  • Accessibility. Plotly output is largely opaque to screen readers. Pair each chart with a visually hidden data table or summary text; it's also your fallback when JS fails.
  • No SSR. Charts only exist after JS runs. If you need charts in emails, PDFs, or OG images, render with Browsershot against a dedicated chart route, not with Plotly directly.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource