khill/lavacharts
Lavacharts is a Laravel wrapper for Google Charts that lets you build interactive charts with a fluent PHP API. Define data tables, render charts and dashboards, and add controls or events without writing JavaScript.
Lavacharts is a server-side builder for Google Charts: you construct a DataTable in PHP with a fluent API, register a chart under a label, and render() emits the container <div> plus the inline google.visualization draw script. It does not replace Google Charts JS — it generates and validates the configuration for it.
Install
composer require khill/lavacharts
Laravel auto-discovery registers Khill\Lavacharts\Laravel\LavachartsServiceProvider on supported versions; older apps add it manually. Publish the package config if your version ships one:
php artisan vendor:publish --provider="Khill\Lavacharts\Laravel\LavachartsServiceProvider"
Composer 2 note: if you are pinned to the 3.x line, require ^3.1.14 at minimum. Earlier 3.x tags fail to install under Composer 2 because of a RenderableTrait autoload/constraint problem (issue #322); 3.1.14 is the patch that unblocks it. This is an install-time fix only — no API change.
Where to look first
src/DataTables/ — DataTable column/row methods and Formats/NumberFormat, DateFormat.src/Charts/ and src/Options/ — the valid chart types and the exact option keys each one accepts.First use case: one server-rendered dashboard chart
use Khill\Lavacharts\Lavacharts;
$lava = app(Lavacharts::class); // or new Lavacharts in plain PHP
$reasons = $lava->DataTable();
$reasons->addStringColumn('Reason')
->addNumberColumn('Percent')
->addRows([
['Check reviews', 5],
['Watch trailers', 2],
['Settle the argument', 89],
]);
$lava->PieChart('IMDB', $reasons, [
'title' => 'Why I visit IMDB',
'is3D' => true,
]);
return view('dashboard', ['lava' => $lava]);
{{-- Include the Google loader exactly once, ideally in the layout head. --}}
@lavacharts
{{-- If your version has no Blade directive: --}}
{{-- <script src="https://www.gstatic.com/charts/loader.js"></script> --}}
{!! $lava->render('PieChart', 'IMDB', 'imdb-chart') !!}
render() returns HTML, so it must be output unescaped. It creates the chart container for the given element ID — don’t add a second <div> with the same ID.
Controllers should return data; a dedicated builder keeps chart config testable and reusable.
final class SalesChartBuilder
{
public function __construct(private Lavacharts $lava) {}
public function build(): Lavacharts
{
$data = $this->lava->DataTable();
$data->addDateColumn('Day')
->addNumberColumn('Sales', new NumberFormat(['prefix' => '$', 'fractionDigits' => 0]))
->addNumberColumn('Expenses');
Order::query()
->where('created_at', '>=', now()->subDays(30))
->orderBy('created_at')
->get()
->each(fn ($o) => $data->addRow([$o->created_at, (float) $o->sales, (float) $o->expenses]));
$this->lava->LineChart('Sales', $data, [
'legend' => ['position' => 'bottom'],
'hAxis' => ['title' => 'Day'],
'curveType' => 'function',
]);
return $this->lava;
}
}
Inject the builder into the controller, pass the Lavacharts instance (or the rendered HTML) to the view.
Use one Lavacharts instance per request, create every chart, then render each one with a unique label and element ID:
$lava = app(Lavacharts::class);
$lava->ColumnChart('Revenue', $revenueTable, $revenueOptions);
$lava->PieChart('Channels', $channelTable, $channelOptions);
{!! $lava->render('ColumnChart', 'Revenue', 'revenue-chart') !!}
{!! $lava->render('PieChart', 'Channels', 'channel-chart') !!}
Keep labels ('Revenue') and DOM IDs ('revenue-chart') unique. The label is the lookup key used by render(); the ID is what Google Charts binds to.
Use NumberFormat / DateFormat on columns rather than pre-formatting strings — Google needs numeric/date types for axes and tooltips.
$data->addNumberColumn('Sales', new NumberFormat(['prefix' => '$']));
$data->addDateColumn('Month', new DateFormat(['formatType' => 'medium']));
Column roles (annotations, tooltips, styles) are set when adding the column; check your version’s addColumn signature for the role argument. This lets you push annotation text alongside a value without a second table.
Options mirror Google’s camelCase keys (hAxis, vAxis, chartArea, legend, pieHole). Nested arrays are fine. Build a base options array and merge per chart:
$base = ['chartArea' => ['width' => '80%', 'height' => '70%']];
$lava->ColumnChart('Revenue', $table, array_replace_recursive($base, [
'legend' => ['position' => 'none'],
]));
Inline JSON in the page does not scale past a few thousand rows. For larger tables, serve JSON from a controller and draw with Google’s JS DataTable / Query wrapper instead of embedding it; keep Lavacharts for the chart options and small/medium tables. Cache aggregated rows (not the DataTable object itself — cache the arrays and rebuild).
render() returns deterministic HTML, so assert on it:
$html = $lava->render('PieChart', 'IMDB', 'imdb-chart');
$this->assertStringContainsString('id="imdb-chart"', $html);
$this->assertStringContainsString('google.visualization.PieChart', $html);
Test the data builders separately (row counts, column order, computed values) — they are the part most likely to break.
Lavacharts only emits the initial draw script. Any DOM swap that replaces the container requires re-running the draw. Wrap the chart area in wire:ignore and trigger a redraw on navigation/DOM patch, or render the chart server-side on full page loads only.
Khill\Lavacharts\Exceptions\InvalidOption, usually with a list of valid keys. Options are also chart-specific — pieHole belongs to PieChart, not ColumnChart. When an option “doesn’t work,” check the package’s Options class for that chart before blaming Google.hAxis, vAxis, titleTextStyle, backgroundColor. Snake-case keys will fail validation.DateTime objects (or the date strings your version accepts) for date/datetime columns. Pre-formatting dates as strings turns them into categorical labels and breaks time axes.float/int in rows where possible; mixed types can produce unexpected formatting or NaN tooltips.google is not defined in the console means the loader script is missing or loaded after the inline draw script. google.charts.load called twice with different packages can warn or override; consolidate the loader config in your layout.display:none panels draw blank. Draw on “shown” or call chart.draw() again after making the container visible.{!! !!} is required but risky with user data. Labels and row values are JSON-encoded, but HTML in tooltips or option strings is not sanitized by Google. Escape or strip user-generated HTML before it reaches a chart label.Lavacharts instance can accumulate charts across requests. Use a fresh instance per request/job, or resolve it request-scoped.google is not defined, “Container element not found”, “Table has no columns”) → view-source the emitted <script> to confirm the data and options actually serialized → then inspect the PHP-side DataTable rows/columns.RenderableTrait blocker (issue #322) that prevented earlier 3.x versions from installing under Composer 2. If composer require fails with an autoload/trait or constraint error on the 3.x line, bump to ^3.1.14 before debugging anything else. No public API changed in this patch, so existing chart code needs no edits.^3.1.14 or ^4.x, whichever matches your PHP/Laravel versions) and read the changelog before upgrading. Don’t assume a 3.1.x patch release implies the 4.x line received the same fix — verify against the tag you actually install.NOASSERTION.How can I help you explore Laravel packages today?