(new FastChart\StockChart())
->setSize(1200, 600)
->setTitle('AAPL last 90 days')
->setTheme(FastChart\Chart::THEME_DARK)
->setOhlcv($ohlcvRows)
->setMovingAverages([20, 50, 200])
->setVolumePane(true)
->setCandleStyle(FastChart\Chart::STYLE_HOLLOW)
->renderToFile('/tmp/aapl.png');
That's a server-side OHLCV candlestick chart with three moving averages, a volume pane, and a hollow candle style. Roughly 68 ms on a single core at 1920×1080. No microservice, no Node sidecar, no JavaScript runtime. PHP, gd, fastchart.
fastchart 0.2.0 shipped two days ago. 19 chart types behind a fluent OO API, plus a Symbol family (Code 128 barcodes and QR codes) that landed in this release. The repo is at github.com/iliaal/fastchart.
Why charts in PHP again
Twenty years ago, Rasmus and I shipped the initial release of PECL/GDChart in January 2006. It wrapped Bruce Verderaime's gdchart C library from users.fred.net/brv/chart/. The PECL page is still up at https://pecl.php.net/package/GDChart.
Both projects died. Verderaime's gdchart library hasn't moved since the mid-2000s; the homepage at users.fred.net/brv/chart/ has been a tombstone for almost as long. The PECL extension followed. One release, then nothing.
The PHP charting ecosystem since then has been thin. JpGraph kept moving but active development went to the commercial fork; the OSS branch is calcifying. pChart is unmaintained. Many PHP teams that need server-side charts in 2026 either reach for a Node or Python microservice (Chart.js via Puppeteer, matplotlib via subprocess) or accept that "server-side rendering" means "render in the browser and screenshot it." Neither is good.
Between PECL/GDChart and now, I've kept needing charts and graphs in PHP. Mostly charts, occasionally barcodes and QR codes. Each new project I'd reach for the easy options first: command-line tools wrapped through shell_exec, pure-PHP libraries when they were fast enough, more recently chart.js renders shipped through a Puppeteer or headless-Chrome wrapper. Those work until they don't. When scale showed up the wrapper started dominating request latency, and I'd write a little PHP extension that handled the specific case causing pain.
Roughly six of those extensions accumulated over the years. Each did one thing. One generated QR codes for serial numbers on physical labels. One drew two chart types for an internal reporting dashboard. One was just OHLC candlesticks with moving averages. None of them shipped. They lived in private repos, solved the immediate problem, and never got cleaned up enough to release.
fastchart is the attempt to close that gap publicly. One extension, the breadth of shapes I've kept needing, a fluent OO API, BSD-licensed. StockChart got the deepest treatment in this release (seven candle styles, the full indicator stack) because the most recent of the six private extensions was the trading-chart one and it carried over almost verbatim. The other eighteen chart types and the Symbol family came from cleaning up and merging the rest.
What's in 0.2.0
Nineteen chart classes plus a two-class Symbol family for barcodes and QR codes. Five output formats. Two render paths. The full surface is 105 public methods covered by 97 tests. PHP 8.3+ minimum, NTS or ZTS.
The chart classes split into four shapes:
- Cartesian. Line, Area, Bar (vertical, horizontal, stacked, grouped, floating, layered), Scatter, Bubble.
-
Financial. A deep
StockChartclass: seven candle styles (CANDLE, BAR, DIAMOND, I_CAP, HOLLOW, VOLUME, VECTOR), SMA/EMA/WMA overlays, a volume pane, and indicator panes (RSI, MACD, Bollinger Bands, Parabolic SAR, Stochastic, OBV). - Non-Cartesian. Radar, Polar, Surface, Contour.
- Specialised. Pie (with donut hole and leader lines), Gauge, LinearMeter, Gantt (with dependencies and milestones), BoxPlot, Treemap, Funnel, Waterfall, Heatmap.
The Symbol family added in 0.2.0:
- Code 128. ISO/IEC 15417. Auto-switches between A/B/C subsets to minimize encoded length. Mod-103 checksum appended automatically. Optional human-readable payload rendered below the bars.
- QR Code. ISO/IEC 18004. Four error-correction levels (ECC_L/M/Q/H), versions 1 through 40. Encoder is the vendored nayuki/QR-Code-generator C library under MIT.
Output formats are the standard gd set plus the modern ones: PNG, JPEG, WebP, AVIF, GIF.
Why barcodes and QR codes in a chart library
Because they all render to a gd canvas, they all serve the same use case (server-side image generation in PHP), and they share the same painful problem: the existing PHP-native options are mostly dead or third-party packages with their own dependency stacks.
The unifying thread is gd, not "chart." If you're rendering a dashboard tile, a sales report PDF, an invoice with a scannable serial number, or a shipping label with a barcode, you're producing an image on the server. PHP has had ext/gd since 4.0.0. fastchart treats ext/gd as the substrate and adds higher-level shapes on top. The Symbol classes don't claim to be charts; they live in their own family parallel to Chart, with shared base setters and the same render-format set.
The public options before fastchart were mostly pure-PHP libraries shipping their own glyph tables and rasterizers, or wrappers around command-line tools like qrencode. Both work. Both add a dependency surface that a pie install doesn't cover. fastchart pulls QR and Code 128 into the same .so as the charts. One install, one dependency (gd), one fluent API.
The compose path
Charts let you hand fastchart a \GdImage canvas you own. It draws into your canvas and returns the same canvas back. Symbols don't accept a caller-owned canvas (a barcode's quiet zone makes compositing inside an existing image ambiguous); they render fresh and you imagecreatefromstring() to composite afterwards.
The composability is the differentiator from JpGraph, pChart, and most JS-bridged solutions. They own the canvas. You get a finished PNG file back and composite at the file or page level, never at the pixel level. fastchart's two-path design covers both:
// Path 1: "give me a file."
(new FastChart\LineChart(800, 600))
->setSeries([['data' => $values]])
->renderToFile('/tmp/line.png');
// Path 2: "draw onto my canvas." Two charts side by side on the same image.
$canvas = imagecreatetruecolor(1600, 900);
(new FastChart\LineChart(1600, 900))
->setTitle('Daily active users')
->setSeries([['data' => $values]])
->setPlotRect(80, 60, 720, 820)
->draw($canvas);
(new FastChart\BarChart(1600, 900))
->setTitle('Quarterly revenue')
->setSeries([['data' => $bars]])
->setPlotRect(880, 60, 1520, 820)
->draw($canvas);
// Stamp something gd-native on top.
$font = '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf';
imagettftext($canvas, 24, 0, 20, 40, $white, $font, 'Dashboard');
imagepng($canvas, '/tmp/dashboard.png');
A four-tile dashboard, a chart embedded in a PDF page, a chart and its legend baked together on a sprite, same primitives, no separate render passes, no temp files.
Performance
Every chart type renders under 100 ms at 1920×1080 on a single core. The lighter types break 100 renders per second per core at dashboard-tile size (640×480). Numbers from my workstation (Intel i9-13950HX, PHP 8.4 NTS), default font and DPI.
| Chart | 640×480 ms | 1920×1080 ms | 1080p ops/sec |
|---|---|---|---|
| AreaChart | 24 | 76 | 13 |
| BarChart | 39 | 84 | 12 |
| BoxPlot | 16 | 60 | 17 |
| BubbleChart | 13 | 62 | 16 |
| ContourChart | 9 | 52 | 19 |
| Funnel | 14 | 52 | 19 |
| GanttChart | 18 | 61 | 16 |
| GaugeChart | 10 | 60 | 17 |
| Heatmap | 9 | 56 | 18 |
| LineChart | 21 | 66 | 15 |
| LinearMeter | 9 | 50 | 20 |
| PieChart | 13 | 59 | 17 |
| PolarChart | 10 | 53 | 19 |
| RadarChart | 15 | 61 | 16 |
| ScatterChart | 17 | 60 | 17 |
| StockChart | 21 | 68 | 15 |
| SurfaceChart | 8 | 50 | 20 |
| Treemap | 18 | 60 | 17 |
| Waterfall | 18 | 61 | 16 |
These are not "we render faster than Chart.js running in Puppeteer" numbers. Headless-browser rendering is slow for completely different reasons (process startup, JS runtime, layout, paint). The honest framing is that fastchart removes the JS-render path entirely from server-side image generation. The benchmark is a sanity check that the C path is fast enough to stop reaching for a sidecar, not a marketing claim.
Bench source is at docs/bench/bench.php. Reproduce locally with php -d extension=gd -d extension=./modules/fastchart.so docs/bench/bench.php.
Install
pie install iliaal/fastchart
Or build from source against the PHP install you want to extend:
phpize
./configure --enable-fastchart
make -j
make test
sudo make install
PHP 8.3 or newer, plus ext/gd. fastchart declares ZEND_MOD_REQUIRED("gd") so the engine orders MINIT correctly regardless of php.ini / conf.d / -d extension= load order. (Earlier 0.1.0 didn't, and docker-php-ext-enable's alphabetical conf.d ordering caused fastchart to load before gd. That was the only thing 0.1.1 fixed.)
Twenty years later
The 2006 ext-gdchart was a hundred lines of glue around roughly 1,000 lines of upstream library. Single chart family, single output format, a tiny config surface. It worked. It died the moment its upstream did.
The bet with fastchart is the opposite: own enough of the substrate that the project's lifespan isn't bound to anything external besides gd, which has been in PHP since 4.0.0. Nineteen chart types, two symbol types, the whole stack lives in this repo. No third-party chart library to outlast, no microservice to keep alive, no JS toolchain to drag along.
Twenty years between PHP charting extensions is long enough.
Top comments (0)