DEV Community

Ilia Alshanetsky
Ilia Alshanetsky

Posted on • Originally published at ilia.ws

fastchart 0.2.0: Native PHP Charts, Barcodes, and QR Codes in One Extension

(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');
Enter fullscreen mode Exit fullscreen mode

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 StockChart class: 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');
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or build from source against the PHP install you want to extend:

phpize
./configure --enable-fastchart
make -j
make test
sudo make install
Enter fullscreen mode Exit fullscreen mode

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)