XLSX charts in pure PHP: line, pie, and combo charts without Excel or heavyweight libraries
The request sounds harmless: “Export the sales report to Excel — and add a chart.” The first part needs an ordinary table export. The second usually leads to one of three approaches:
- “We’ll add the chart manually.” You export a table, and a manager creates the chart in Excel each time. This works until the report needs to run on a schedule without human intervention.
- PhpSpreadsheet. It supports charts, but brings a full workbook object model: hundreds of classes and substantial memory use. With large datasets, generating the export becomes a problem of its own.
- Rendering an image on the server. You use ImageMagick/GD or a headless browser to render a PNG, then insert it into the workbook. The image is static: change a number in the table, and the chart stays the same. Print quality depends on resolution, and image generation adds another component to maintain.
Check chart support separately: a library’s ability to write data and styles doesn’t imply that it can create charts.
avadim/fast-excel-writer fills this gap. It’s a fast, streaming XLSX generator written in pure PHP, requiring PHP 7.4 or later and only standard extensions. It can insert native Excel charts: line, column, bar, pie, doughnut, area, and combo charts.
All examples below were checked with FastExcelWriter 6.17.0 (tag v6.17) and PHP 8.4. Chart support arrived in the 5.x series; this article uses the 6.17 API.
Table formatting is covered in the previous article on styles, widths, and filters. Now we’ll add a visual representation of the data.
Why a native chart beats an image
An XLSX chart is an XML description rather than a bitmap: it stores the chart type, data series, and references to cell ranges. This gives it three practical advantages:
- It updates with the data. The chart references worksheet cells. When a user changes a value, the chart updates. A PNG cannot do this.
- It’s editable. Recipients can change the chart type, colors, labels, or series using Excel’s standard tools. Check compatibility with other editors separately, especially for combo charts and secondary axes.
- It’s vector-based. When printed or exported to PDF, the chart renders without the pixelation of a bitmap.
The size of the XML description depends on the number of series, settings, and cached values. Cell references let you avoid generating a separate raster image.
Your first chart: a complete example
Install the library:
composer require avadim/fast-excel-writer:6.17.0
Write a table of quarterly data and attach a line chart:
<?php
require __DIR__ . '/vendor/autoload.php';
use avadim\FastExcelWriter\Excel;
use avadim\FastExcelWriter\Charts\Chart;
use avadim\FastExcelWriter\Charts\Legend;
$excel = Excel::create(['Chart Demo']);
$sheet = $excel->sheet();
$data = [
['', 2010, 2011, 2012],
['Q1', 12, 15, 21],
['Q2', 56, 73, 86],
['Q3', 52, 61, 69],
['Q4', 30, 32, 0],
];
foreach ($data as $row) {
$sheet->writeRow($row);
}
// Chart type, title, and data series
$dataSeries = ['B1' => 'B2:B5', 'C1' => 'C2:C5', 'D1' => 'D2:D5'];
$chart = Chart::make(Chart::TYPE_LINE, 'Line Chart', $dataSeries)
->setCategoryAxisLabels('A2:A5') // X-axis labels from column A
->setLegendPosition(Legend::POSITION_TOPRIGHT); // legend in the top-right corner
// Add the chart to the worksheet at A7:H20
$sheet->addChart('A7:H20', $chart);
$excel->save('report.xlsx');
Save this example as report.php next to the vendor/ directory and run php report.php. The snippets below use the class imports, $sheet, and $dataSeries from this example; each shows a separate chart variation. To save a modified variation, call save() after adding it to the worksheet. The dashboard at the end is a separate, complete script.
The three key calls are:
Chart::make($type, $title, $dataSource)creates the chart. Pass series as an array where the key is the cell containing the series name (used in the legend) and the value is the data range:'B1' => 'B2:B5'. Cell addresses are case-insensitive.setCategoryAxisLabels('A2:A5')specifies the source of the horizontal-axis labels — Q1–Q4 in this example.$sheet->addChart('A7:H20', $chart)anchors the chart to a cell range. The range sets both position and size: the top-left corner is at A7, and the bottom-right corner is at H20. Use a wider range for a larger chart.
The result is an ordinary XLSX file with an editable chart, generated without Excel on the server, COM objects, or headless browsers.
Supported chart types
The Chart::TYPE_* constants are listed below; TYPE_DOUGHNUT is also available as an alias for TYPE_DONUT:
| Constant | Chart type |
|---|---|
TYPE_LINE / TYPE_LINE_STACKED |
line / stacked line |
TYPE_LINE_3D / TYPE_LINE_3D_STACKED |
3D line / stacked 3D line |
TYPE_COLUMN / TYPE_COLUMN_STACKED |
column (vertical bars) / stacked column |
TYPE_BAR / TYPE_BAR_STACKED |
bar (horizontal bars) / stacked bar |
TYPE_AREA / TYPE_AREA_STACKED |
area / stacked area |
TYPE_AREA_3D / TYPE_AREA_3D_STACKED |
3D area / stacked 3D area |
TYPE_PIE / TYPE_PIE_3D |
pie / 3D pie |
TYPE_DONUT |
doughnut |
TYPE_COMBO |
combination (columns and lines) |
Set the type when creating a chart with Chart::make() or later with setChartType().
Pie charts differ from line charts: they use one data series, and the category range supplies the slice labels:
$chart = Chart::make(Chart::TYPE_PIE, 'Pie Chart', ['C2:C5'])
->setCategoryAxisLabels('A2:A5')
->setLegendPosition(Legend::POSITION_TOPRIGHT)
->setPlotShowValues(true); // show values on slices
$sheet->addChart('A7:H20', $chart);
For pie and doughnut charts, use setPlotShowPercent(true) to display percentages instead of absolute values.
Customization: titles, axes, legends, and colors
These configuration methods return $this, so you can chain them:
$chart = Chart::make(Chart::TYPE_COLUMN, 'Quarterly sales', $dataSeries)
->setCategoryAxisLabels('A2:A5') // category-axis labels
->setCategoryAxisTitle('Quarter') // X-axis title
->setValueAxisTitle('Units') // Y-axis title
->setLegendPosition(Legend::POSITION_BOTTOM)
->setPlotShowValues(true) // values above columns
->setChartColors(['#4472c4', '#ed7d31', '#a5a5a5']); // custom series colors
Legend positions use the constants Legend::POSITION_RIGHT, POSITION_LEFT, POSITION_TOP, POSITION_BOTTOM, and POSITION_TOPRIGHT. Short methods such as setLegendPositionRight() are also available.
Colors accept the formats #ffcc00, ffcc00, #fc0, and fc0.
Adding individual series and setting options
Besides passing an array to Chart::make(), you can add series individually with addDataSeriesValues($dataSource, $dataLabel, $options) and configure each one:
$chart = Chart::make(Chart::TYPE_LINE)
->addDataSeriesValues('B2:B5', 'B1', [
'color' => '#ff0000', // series color
'width' => 3, // line width in points
'marker' => 'circle', // circular point markers
])
->addDataSeriesValues('C2:C5', 'Target');
A series label can be either a cell address ('B1' reads the name from that cell) or a literal string ('Target'). Pie series support the segment_colors option. In version 6.17, pass an array of colors, such as ['#4472c4', '#ed7d31', '#a5a5a5', '#ffc000']; delimiter-separated strings are handled incorrectly in this version.
Data from another worksheet
A short range such as B2:B5 expands to an absolute reference to the worksheet containing the chart: Worksheet!$B$2:$B$5. A fully qualified address with a worksheet name can reference data elsewhere — for example, a dashboard sheet drawing on data from the Jan and Feb sheets:
$chart = Chart::make(Chart::TYPE_COLUMN)
->addDataSeriesSet([
'Jan!$A$1' => 'Jan!$A$2:$A$5',
'Feb!$A$1' => 'Feb!$A$2:$A$5',
]);
Positioning with pixel offsets
The range passed to addChart() is usually enough, but you can adjust the corners manually. setTopLeftPosition($cell, $xOffset, $yOffset) and setPosition($cell, $xOffset, $yOffset) set the anchor cells for the top-left and bottom-right corners, with pixel offsets within each cell.
Combo charts: columns and a line
A common report shows revenue as columns with a trend or target line on top. Use TYPE_COMBO and add each series with addDataSeriesType(), specifying its chart type. The supported combination is column and line:
$chart = Chart::make(Chart::TYPE_COMBO, 'Combo Chart')
->addDataSeriesType(Chart::TYPE_COLUMN, 'B2:B5', 'B1') // columns
->addDataSeriesType(Chart::TYPE_COLUMN, 'C2:C5', 'C1') // another column series
->addDataSeriesType(Chart::TYPE_LINE, 'D2:D5', 'D1') // line on top
->setCategoryAxisLabels('A2:A5')
->setLegendPosition(Legend::POSITION_TOPRIGHT);
$sheet->addChart('A9:H22', $chart);
When the line and columns use different scales — units and percentages, for example — assign a series to a secondary Y axis. addDataSeriesType2() adds the series to the second axis, and setValueAxisTitle2() sets that axis’s title.
Multiple charts: a dashboard worksheet
You can place multiple charts on one worksheet, each anchored to its own range. The source data can also contain formulas, so charts can reference calculated cells:
<?php
require __DIR__ . '/vendor/autoload.php';
use avadim\FastExcelWriter\Excel;
use avadim\FastExcelWriter\Charts\Chart;
use avadim\FastExcelWriter\Charts\Legend;
$excel = Excel::create(['Dashboard']);
$sheet = $excel->sheet();
$data = [
['', 2010, 2011, 2012],
['Q1', 12, 15, 21],
['Q2', 56, 73, 86],
['Q3', 52, 61, 69],
['Q4', 30, 32, 0],
['Total', '=SUM(B2:B5)', '=SUM(C2:C5)', '=SUM(D2:D5)'],
];
foreach ($data as $row) {
$sheet->writeRow($row);
}
// Quarterly column chart on the left
$chart = Chart::make(Chart::TYPE_COLUMN, 'Column Chart', ['B1' => 'B2:B5', 'C1' => 'C2:C5', 'D1' => 'D2:D5'])
->setCategoryAxisLabels('A2:A5')
->setLegendPosition(Legend::POSITION_TOPRIGHT);
$sheet->addChart('A9:H22', $chart);
// Pie chart of annual totals (the Total row contains formulas) on the right
$chart = Chart::make(Chart::TYPE_PIE, 'Pie Chart', ['B6:D6'])
->setCategoryAxisLabels('B1:D1')
->setLegendPosition(Legend::POSITION_TOPRIGHT)
->setPlotShowPercent(true);
$sheet->addChart('I9:M22', $chart);
// Below: a stacked bar chart and a 3D pie chart
$chart = Chart::make(Chart::TYPE_BAR_STACKED, 'Stacked Bar Chart', ['B1' => 'B2:B5', 'C1' => 'C2:C5', 'D1' => 'D2:D5'])
->setCategoryAxisLabels('A2:A5')
->setLegendPosition(Legend::POSITION_TOPRIGHT);
$sheet->addChart('A23:H36', $chart);
$chart = Chart::make(Chart::TYPE_PIE_3D, '3D Pie Chart', ['B6:D6'])
->setCategoryAxisLabels('B1:D1')
->setLegendPosition(Legend::POSITION_TOPRIGHT)
->setPlotShowPercent(true);
$sheet->addChart('I23:M36', $chart);
$excel->save('dashboard.xlsx');
Four charts in a 2×2 grid make a dashboard worksheet. Printing it on a single page requires separate scaling and print-area settings. FastExcelWriter doesn’t evaluate formulas: totals and charts based on them depend on Excel recalculating the workbook when it opens.
Limitations
Keep these constraints in mind when planning a report:
- Not every Excel chart type is supported. Scatter, radar, bubble, stock, and other specialized types are unavailable through this API. The list above covers line, column, bar, area (including the listed stacked and 3D variants), pie, doughnut, and combo charts.
- Combo means column and line only. You cannot combine area and line charts, for example.
- Charts remain in memory until
save(). The library streams data rows to disk, but keepsChartobjects until the workbook is saved. With range references, the series do not duplicate the entire value array inside the chart object. Total memory use still depends on the number of charts, styles, and data retained by your code. - Data is written forward. Once a row has been flushed from the buffer to the temporary file, its values cannot be changed. This restriction doesn’t apply to charts: you can add them at any point before
save(), including references to future ranges. Plan the source table layout in advance. - Fine-tuning is limited by the API. Gradient fills, arbitrary label fonts, and trendlines are unavailable. You can set titles, axes (
setCategoryAxis(), with gridlines through the constructor), legends, series colors, and layout (setPlotLayout()). These cover typical reports. For precise visual adjustments, recipients can finish editing the chart in Excel.
Takeaways
- An XLSX chart is XML with cell references. It updates when data changes, remains editable, and renders as vector graphics for printing.
- FastExcelWriter 6.17 supports 15 basic chart variants plus combo charts, multiple series, a secondary Y axis, cross-sheet data, and multiple charts per worksheet.
- The minimal recipe is
Chart::make($type, $title, $dataSource)→setCategoryAxisLabels()→$sheet->addChart($range, $chart). - No server-side Excel installation, COM, headless browser, or full workbook object model is needed.
composer require avadim/fast-excel-writer:6.17.0
Documentation and runnable examples (demo/demo-21-* and demo/demo-22-*) are in the version 6.17 repository. Method names and combo restrictions were checked against Chart.php, and series options against DataSeriesValues.php.