amenadiel/jpgraph
A Laravel wrapper for JpGraph, the popular PHP charting library. Generate bar, line, and pie charts server-side with a simple API, embedding image charts in Blade views without JavaScript. Bundles JpGraph and adds config and facades for building dynamic charts quickly in Laravel.
amenadiel/jpgraph is a Composer-based fork of the classic JpGraph 3.5 charting library. It is not a Laravel package: there is no service provider, no facade, no publishable config. It is a namespaced class library that renders images by writing directly to PHP output or to a file. You wire it into Laravel yourself.
Install and verify the runtime
composer require amenadiel/jpgraph
php -m | grep -i gd # must print "gd"
JpGraph is a GD-based renderer. If php-gd is missing you get fatal errors on the first new Graph(...), usually buried under JpGraph's own error handler. Check this before anything else.
First working use case
The refactor moved the old jpgraph_graph.php / jpgraph_line.php includes into one autoloaded namespace tree, roughly Amenadiel\JpGraph\{Graph,Plot,Image,Text,Axis,Themes,Util}. You no longer require a file per chart type.
use Amenadiel\JpGraph\Graph\Graph;
use Amenadiel\JpGraph\Plot\LinePlot;
Route::get('/charts/signups', function () {
$data = [3, 5, 2, 8, 4, 6, 9];
$graph = new Graph(640, 400);
$graph->SetScale('textlin'); // x = text, y = linear
$graph->title->Set('Signups');
$graph->xaxis->title->Set('Day');
$graph->yaxis->title->Set('Count');
$plot = new LinePlot($data);
$graph->Add($plot);
ob_start(); // JpGraph echoes the image
$graph->Stroke(); // or ->Stroke('/path/chart.png')
$png = ob_get_clean();
return response($png, 200, ['Content-Type' => 'image/png']);
});
Where to look first
vendor/amenadiel/jpgraph/ — the class tree is the real documentation. Namespaces map almost 1:1 onto the old JpGraph filenames (jpgraph_pie.php → Plot\PiePlot, jpgraph_theme.php → Themes\*).vendor/amenadiel/jpgraph/examples/ if it is shipped — every chart type has a runnable example. This is the fastest way to learn the API.require lines and namespaces changed. Any tutorial written for v3.5 will work after you fix the class names.Wrap rendering in a service, not a controller
Charts are imperative and side-effect heavy. Keep them out of controllers:
class ChartRenderer
{
public function line(array $data, string $title): string
{
$graph = new Graph(800, 400);
$graph->SetScale('textlin');
$graph->title->Set($title);
$graph->SetMargin(60, 20, 40, 50);
$plot = new LinePlot($data);
$plot->SetColor('#3b82f6');
$plot->SetWeight(2);
$graph->Add($plot);
return $this->capture($graph);
}
private function capture(Graph $graph): string
{
ob_start();
$graph->Stroke();
return ob_get_clean();
}
}
Serve from a route and cache aggressively
Regenerating a PNG per request is wasteful. Cache the binary with Laravel's cache or on disk, and let the browser/HTTP cache do the rest:
Route::get('/charts/revenue', function (ChartRenderer $renderer) {
$png = Cache::remember('chart.revenue', now()->addHour(), fn () =>
$renderer->line(Revenue::last30Days(), 'Revenue')
);
return response($png)->header('Content-Type', 'image/png')
->header('Cache-Control', 'public, max-age=3600');
});
Embed in Blade
Two clean options: inline base64 (no route, no storage), or store the file and reference the route.
// base64 inline
$src = 'data:image/png;base64,' . base64_encode($png);
// <img src="{{ $src }}">
For anything reused, save once to storage/app/public/charts/ (via the ob-capture string + Storage::put) and expose it through the public disk.
Use Stroke($filename) when you need a file
$graph->Stroke() echoes; $graph->Stroke('/absolute/path/chart.png') writes to disk. The path must be a real writable directory — storage_path('app/charts/') is the right target, not public/, if you want to control exposure.
Bar, pie and combination charts
use Amenadiel\JpGraph\Plot\BarPlot;
use Amenadiel\JpGraph\Graph\PieGraph;
use Amenadiel\JpGraph\Plot\PiePlot;
// bars
$bar = new BarPlot([12, 9, 30, 18]);
$graph->Add($bar);
// pie (has its own Graph subclass and its own setup)
$pie = new PieGraph(500, 500);
$pie->title->Set('Traffic sources');
$pie->Add(new PiePlot([40, 30, 20, 10], ['Direct', 'Search', 'Social', 'Referral']));
Compose plots instead of drawing twice
Grouped/stacked bars, lines over bars, and secondary axes are all built by constructing multiple Plot objects and passing them to GroupBarPlot, AccLinePlot, etc. — then a single $graph->Add(...). Do not render two images and overlay them.
Theming
$graph->SetTheme(new ...Theme()) applies a palette and fonts globally. Grab a theme class from Themes\* if you need brand consistency across many charts; otherwise set fonts and colors per-axis/per-plot, which is more predictable.
Legend and axis helpers
$graph->legend->SetPos(), $graph->legend->SetFont(), $graph->xaxis->SetTickLabels() — the fluent-ish API is extensive. Keep a small helper class per chart type (LineChart, BarChart) that hides the setup boilerplate; otherwise chart configuration leaks into every controller.
The license is the biggest risk
NOASSERTION on this package is not a benign license. Upstream JpGraph 3.5 was not MIT — it is QPL 1.0 for non-commercial use with a separate paid commercial license. The refactor does not change that upstream obligation. Audit before shipping in a commercial Laravel app; treat this as the deciding factor, not the API.
It is a 2021 release of a 2010-era API
The codebase predates modern PHP. Expect deprecation notices on PHP 8.1+ — dynamic property creation, ${var} interpolation, implicit nullable parameters. Set error_reporting(E_ALL & ~E_DEPRECATED) in a scoped way around rendering rather than globally, and pin PHP in CI so you find breakage early rather than in production.
Rendering is a side effect — never assume a return value
Stroke() echoes the image. If you call it outside of an output buffer, Laravel has already sent headers and you get a corrupt response. Always wrap in ob_start() / ob_get_clean(), or use the filename form of Stroke() and return a file response.
JpGraph has its own error handler and will die()
Its JpGraphException / ErrMsg machinery can terminate the request and echo HTML straight to the browser, bypassing Laravel's exception handler and your logging. For production, wrap rendering in a try/catch, and prefer building the graph inside a job/command that produces a file so a chart failure cannot take down a user request.
Fonts and TTF paths
Bitmap fonts (FF_FONT1) work out of the box but look dated. For real typography you need TTF files and JpGraph's global config constants — most importantly TTF_DIR (and the cache directory CACHE_DIR) — defined before the first graph object. If the refactor doesn't bootstrap these for you, define('TTF_DIR', base_path('vendor/.../fonts/')) in a service provider; do not hardcode absolute paths that differ across environments.
The cache directory must be writable
$graph->cache->SetTime(...) writes PNGs into a directory. Point it at storage_path('framework/cache/jpgraph'), ensure it exists and is writable, and set file permissions in your deploy script. Silent chart failures on a fresh container are almost always this.
SetScale() and axis type strings are easy to typo
Valid values include textlin, linlin, textint, intint, loglin, linlog. A wrong string throws a JpGraph exception (which may die()), not a PHP TypeError. Double-check before you debug output formatting.
Prefer Storage and public disks over direct file writes
Stroke($path) writes with plain PHP file functions and respects the process user, not Laravel's filesystem abstraction. On S3 or read-only filesystems this breaks. If you need remote storage, always use the ob-capture string path and Storage::put().
Anti-aliasing and sharpness
Small charts come out soft. Set $graph->img->SetAntiAliasing(false) for pixel-crisp small line charts, or bump the pixel density and scale down with $graph->img->SetScale() — JpGraph has no retina concept, so 2x rendering with CSS downscaling is the usual workaround.
Extension points are shallow
There is no modern extension API — no PSR interfaces, no events, no container bindings. If you need to plug in custom data providers, write a thin adapter in your app that converts domain models to arrays of numbers before handing them to LinePlot, BarPlot, etc. That is the extension point; keep it in your code, not in the vendor tree.
Pin the version
Given the release cadence (last tag April 2021) and the abandoned upstream, pin an exact version in composer.json and note it in the risk register. New Laravel majors will not test against it, so you are the maintainer.
How can I help you explore Laravel packages today?