The article begins with a simple example page with some data and shows how to use the two classes of this article to display a line chart. Next the classes are explained in detail, so you can adapt them to your preferences.
Introduction
To add a chart on your web page using PHP, you can use a tool like pChart, but if you just want a simple line chart, that may be a bit too much. And anyway, it is fun to do it yourself. So let us build a line chart in plain PHP. You can use the Chart
class in your own code and adapt the Chart
and ChartDraw
classes to your own preferences; for example, you may prefer a bar chart.
A line chart consists of:
- a horizontal x-axis and a vertical y-axis; the convention is to note a position on the chart as (x, y). The left lower corner is the (0, 0) position.
- usually a horizontal top line and a vertical right line to make a rectangle
- usually some labels to the left of the y-axis and below the x-axis
- and the one or more lines connecting the data points.
I will try to be consistent in calling all of this together "the chart" and using "the line" if I only mean the line connecting the data points.
I am using PHP 7.1.22 with PHP's built-in web server php.exe on a Windows 10 PC, but the code should work with PHP 5 on other environments. I started with the example in imagesetpixel and a tip from stackoverflow for the show()
function. The PHP image functions used are imagecreatetruecolor()
, imagecolorallocate()
and imagesetpixel()
for the lines and imagestring()
, imagefontheight()
and imagefontwidth()
for the labels.
The Code
The code consists of three PHP files:
- testChart.php which serves as example for using the
Chart
class - chart.php containing class
Chart
, which uses - chartDraw.php containing the class
ChartDraw
without labels
To run the code, save the three files in the same directory and load testChart.php in your browser.
Using the Code
If you want to add a line chart to your own project, replace testChart.php with your own code. In the testChart
code, three lines are created, blue, red and green. The data elements are:
$resultArray[0] = array('x'=>0, 'xlabel'=>'DEC', 'y1'=>2000, 'y2'=>1600);
$resultArray[1] = array('x'=>31, 'xlabel'=>'JAN', 'y1'=>1800, 'y2'=>1700);
$resultArray[2] = array('x'=>59, 'xlabel'=>'FEB', 'y1'=>1700, 'y2'=>1800);
$resultArray[3] = array('x'=>90, 'xlabel'=>'MAR', 'y1'=>1400, 'y2'=>1600);
$resultArray[4] = array('x'=>120, 'xlabel'=>'APR', 'y1'=>1250, 'y2'=>1400);
$resultArray[5] = array('x'=>151, 'xlabel'=>'MAY', 'y1'=>1900, 'y2'=>1300);
$resultArray[6] = array('x'=>181, 'xlabel'=>'JUN', 'y1'=>2050, 'y2'=>1500);
$resultArray[7] = array('x'=>212, 'xlabel'=>'JUL', 'y1'=>2200, 'y2'=>1700);
$resultArray[8] = array('x'=>243, 'xlabel'=>'AUG', 'y1'=>2100, 'y2'=>1900);
$resultArray[9] = array('x'=>273, 'xlabel'=>'SEP', 'y1'=>2300, 'y2'=>1800);
$resultArray[10] = array('x'=>304, 'xlabel'=>'OCT', 'y1'=>2350, 'y2'=>2000);
$resultArray[11] = array('x'=>334, 'xlabel'=>'NOV', 'y1'=>2400, 'y2'=>2100);
$resultArray[12] = array('x'=>365, 'xlabel'=>'DEC', 'y1'=>2450, 'y2'=>2200);
$resultGreenArray[0] = 1500;
$resultGreenArray[1] = 1900;
$resultGreenArray[2] = 1800;
$resultGreenArray[3] = 1500;
$resultGreenArray[4] = 1400;
The array resultArray
with test data contains figures 'y1
' for the blue line and 'y2
' for the red line. For x
, we could just take the index of the array: I added the 'x
' column to show the x
values do not have to be equidistant. resultGreenArray
shows the most basic version, the array index is the x
-axis element.
The code to build lines from these data and show the chart is:
$chart = new Chart();
$chart->setPixelSize(600, 400, 2);
$chart->setMinMaxX(0, 365, 3);
$chart->setMinMaxY(500, 3000);
$errorMessage = $chart->addNewLine(0, 0, 255);
foreach ($resultArray as $i=>$valueArray) {
$errorMessage = $chart->setPoint($valueArray['x'], $valueArray['y1'],
strval($valueArray['x']));
}
$errorMessage = $chart->addNewLine(255, 0, 0);
foreach ($resultArray as $i=>$valueArray) {
$errorMessage = $chart->setPoint($valueArray['x'], $valueArray['y2'], '');
}
$errorMessage = $chart->addNewLine(0, 255, 0);
$chart->setMinMaxX(0, 4, 0);
foreach ($resultGreenArray as $i=>$value) {
$errorMessage = $chart->setPoint($i, $value, '');
}
$chart->show(5);
The first lines create the chart and specify the dimensions, in both pixels and data (explanation below). The third argument of setPixelSize()
is the font, values between 1 and 5 for the built-in fonts, The third argument of setMinMaxX()
is the length of the most right x
label, which we need in ChartDraw
to set the margins:
$chart = new Chart();
$chart->setPixelSize(600, 400, 2);
$chart->setMinMaxX(0, 365, 3);
$chart->setMinMaxy(500, 3000);
Next, the three lines are created. The third argument of setPoint()
specifies the label on the x-axis. With the test data, this makes sense for the first line, not for the second and third line. If everything is ok, $errorMessage
is an empty string, for this example, I omitted code for when it's not.
Finally, show()
displays the chart. The argument specifies the number of labels left of the y-axis.
Notes:
setPoint()
must be called in order of increasing x-value x
can start at another value than 0
, but can not be negative - the
x
labels can be string texts y
values and minY
may be negative - the
y
labels are (numeric and equal to) the y
values - if your data are close together, you may want to set the
x
-label only for every fifth point or so, to ensure that the x
-labels don't overlap. One way to do that is to set the x
-labels by adding a white line with the labels you want minX
, maxX
, minY
and maxY
could be calculated by the Chart
class itself. TestChart.php shows that this is not always what you want for x
. Determining minY
and maxY
in Chart
requires saving the data of all lines in Chart
, then calculate the minY
and maxY
and only then start drawing the lines. It can be done, but will make the code less clear for this article.
Chart Step by Step
A chart consists of a rectangle of pixels, in TestChart.php a width of 600 and a height of 400 pixels. These are the physical dimensions of the chart and the ChartDraw
class only knows these. But our data does not fit these dimensions: the 12 data elements must be divided over the 600 pixels and the values have to be reduced to fit the 400 pixels height. This is done in the Chart
class.
The first three functions in Chart.php set the pixel size of the chart and the min
and max
values of x
and y
(I start the name of function arguments with lowercase a
; no subtle technical reason, just a reminder in the code that it is an argument). We also need the max string length of the labels for the x
-axis, to determine the margins:
public function setPixelSize($aWidth, $aHeight, $aFontSize)
{
$this->width = $aWidth;
$this->height = $aHeight;
$this->fontSize = $aFontSize;
}
public function setMinMaxX($aMinX, $aMaxX, $aRightTextLengthX)
{
$this->minX = $aMinX;
$this->maxX = $aMaxX;
$this->rightTextLengthX = $aRightTextLengthX;
}
public function setMinMaxY($aMinY, $aMaxY)
{
$this->minY = $aMinY;
$this->maxY = $aMaxY;
$this->maxTextLengthY = max(strlen(strval($aMinY)), strlen(strval($aMaxY)));
}
For every line you want to draw, you have to call addNewLine()
:
public function addNewLine($aRed, $aGreen, $aBlue)
{
if ($this->chartDraw == null) {
$errorMessage = $this->validateParameters();
if ($errorMessage != '') {
return $errorMessage;
}
$this->chartDraw = new ChartDraw($this->width, $this->height, $this->fontSize
, $this->maxTextLengthY, $this->maxTextLengthY);
}
$this->chartDraw->addNewLine($aRed, $aGreen, $aBlue);
return '';
}
At the first call of this function, the chartDraw
class is initiated (with a lot of arguments we discuss later). The reason to do it here is that all the arguments must have been set. Note that this and the next function return an errorMessage
, an empty string
if no errors are found.
Next, setPoint()
is the essential function of this class:
public function setPoint($aX, $aY, $aXLabelText)
{
$errorMessage = $this->validateXY($aX, $aY);
if ($errorMessage != '') {
return $errorMessage;
}
$xPixel = round(($aX - $this->minX) * $this->width / ($this->maxX - $this->minX));
$yPixel = round(($aY - $this->minY) * $this->height / ($this->maxY - $this->minY));
$this->chartDraw->set($xPixel, $yPixel, $aXLabelText);
return '';
}
This function is called for every data element. Note that the function must be called in order of increasing $aX
(otherwise connectTwoPoints()
in the ChartDraw
class does not work correctly). The $xPixel
and $yPixel
lines calculate the "pixel
" dimension from the "data
" dimension: if $aX = $this->minX
, $xPixel
evaluates to 0
. If $aX = $this->maxX
, $xPixel
evaluates to $this->width
, so all values of $aX
will fit within the "pixel borders" of the chart..
Finally, we pass the location of the point to chartDraw
in set()
, including the text of the label below the x
-axis.
The other public
function in the Chart
class is:
show()
: the final function call in your code to set the labels left of the y
-axis and show the chart. As argument to show()
, you give the number of labels you want on the y-axis
and the private
functions are:
setYLabels()
: sets labels left of the y
-axis validateParameters()
: returns a message if required parameters have not been set validateXY()
: returns a message when x
or y
not within range
ChartDraw Step by Step
In the Chart
class, we passed the data elements as (x, y
) pairs (transformed to the pixel dimensions). This assumes (0, 0
) is the origin of the white rectangle in the next figure:
But we also need the blue part:
- space for labels below the
x
-axis - a small space on the right, because we center the labels at the
x
values, so the right one goes a bit further than the right side of the "white" rectangle - space for labels left of the
y
-axis - a small space on the top, because we center the labels at the y values vertically, so the top one goes a bit higher than the top side of the "white" rectangle
Another challenge is that the PHP functions imagesetpixel()
and imagestring()
see the top left corner as the origin (0, 0), while in the world of charts the origin is the bottom left corner.
The task of the ChartDraw
class is handling all this and hiding it from the Chart
class. To set a pixel in ChartDraw
, we use:
private function setPixel($aX, $aY, $aColor)
{
imagesetpixel($this->gd, $this->marginLeftX + $aX,
$this->sizeY + $this->marginTopY - $aY, $aColor);
}
so for setPixel()
point (0, 0) is the lower left corner of the white rectangle.
In the constructor of the ChartDraw
class, we save the arguments $aSizeX
, $aSizeY
and $aFontSize
for later use. Note that we now switched to X and Y, the naming convention for charts, while in the Chart
class, we used width
and height
for the pixel dimensions. From $aRightTextLengthX
and $aMaxTextLengthY
, we calculate the margins in setMargins()
and then create the rectangle for the chart (including the "blue" part) with imagecreatetruecolor()
. The reason for the +1: if the low value is 0 and the high value n, we have n intervals but n + 1 points.
If you do nothing else and show the result, it is a black rectangle. So we define the color white and can then make all pixels white. Finally, we draw a black border (around the "white" rectangle).
public function __construct($aSizeX, $aSizeY, $aFontSize,
$aRightTextLengthX, $aMaxTextLengthY)
{
$this->sizeX = $aSizeX;
$this->sizeY = $aSizeY;
$this->fontSize = $aFontSize;
$this->setMargins($aRightTextLengthX, $aMaxTextLengthY);
$this->gd = imagecreatetruecolor($this->sizeX + 1 +
$this->marginLeftX + $this->marginRightX
, $this->sizeY + 1 + $this->marginBottomY + $this->marginTopY);
$white = imagecolorallocate($this->gd, 255, 255, 255);
for ($x = 0; $x <= $this->sizeX + $this->marginLeftX + $this->marginRightX; $x++) {
for ($y = 0; $y <= $this->sizeY + $this->marginBottomY + $this->marginTopY; $y++) {
imagesetpixel($this->gd, $x, $y, $white);
}
}
$this->black = imagecolorallocate($this->gd, 0, 0, 0);
for ($x = 0; $x <= $this->sizeX; $x++) {
$this->setPixel($x, 0, $this->black);
$this->setPixel($x, $this->sizeY, $this->black);
}
for ($y = 0; $y <= $this->sizeY; $y++) {
$this->setPixel(0, $y, $this->black);
$this->setPixel($this->sizeX, $y, $this->black);
}
}
When starting a new line, addNewLine()
is called from the Chart
class:
public function addNewLine($aRed, $aGreen, $aBlue)
{
$this->lineColor = imagecolorallocate($this->gd, $aRed, $aGreen, $aBlue);
$this->previousX = -1;
}
This function is needed to set the color of the line, but also to reset $previous
, so we know when we get the first point in set()
.
The essential function is set()
:
public function set($aX, $aY, $aText)
{
$this->setPixel($aX, $aY, $this->lineColor);
$this->setLabelX($aX, $aText);
if ($this->previousX != -1) {
$this->connectTwoPoints($this->previousX, $this->previousY, $aX, $aY);
}
$this->previousX = $aX;
$this->previousY = $aY;
}
First, we set a pixel at ($aX, $aY
). We also add the label below the x
-axis with setLabelX()
. If the previous point (x, y)
has been set, we connect the points to make it a line chart. This is done in connectTwoPoints()
in two steps. First, for all intermediate x
values between two points (x1, y1)
and (x2, y2)
, we add point (x, y)
. The y
value for a given x
value is determined from the following triangles:
As the figures illustrates:
y - y1 y2 - y1 y - y2 x2 - x1
------ = ------- for the left triangle and ------ = ------- for the right triangle
x - x1 x2 - x1 x2 - x y1 - y2
from which we derive y
, see the first if
in the code below.
Setting (x, y)
for all immediate points between x1
and x2
is not enough. Suppose x1=10
and x2=20
, but there is a big difference between y1
and y2
, say y1=100
and y2=150
. Setting the x
results in 10 dots covering the y-range from 100
to 150
, which gives a vague dotted line: we have to set a dot at each y
between y1
and y2
. So second, we derive the x
for all values between y1
and y2
, see the second if
in the code:
private function connectTwoPoints($aX1, $aY1, $aX2, $aY2)
{
if ($aY1 < $aY2) {
for ($x = $aX1 + 1; $x < $aX2; $x++) {
$y = $aY1 + round(($x - $aX1) * ($aY2 - $aY1) / ($aX2 - $aX1));
$this->setPixel($x, $y, $this->lineColor);
}
} else {
for ($x = $aX1 + 1; $x < $aX2; $x++) {
$y = $aY2 + round(($aX2 - $x) * ($aY1 - $aY2) / ($aX2 - $aX1));
$this->setPixel($x, $y, $this->lineColor);
}
}
if ($aY1 < $aY2) {
for ($y = $aY1 + 1; $y < $aY2; $y++) {
$x = $aX1 + round(($y - $aY1) * ($aX2 - $aX1) / ($aY2 - $aY1));
$this->setPixel($x, $y, $this->lineColor);
}
} else {
for ($y = $aY2 + 1; $y < $aY1; $y++) {
$x = $aX2 - round(($y - $aY2) * ($aX2 - $aX1) / ($aY1 - $aY2));
$this->setPixel($x, $y, $this->lineColor);
}
}
}
In the previous code fragment, some points will be hit by both the first and the second if
; when the angle of the line is 45 degrees, both if
s will hit the same points (bar rounding).
The show()
function displays the image:
public function show()
{
ob_start();
imagejpeg($this->gd, NULL, 100);
$rawImageBytes = ob_get_clean();
echo '<img src="data:image/jpeg;base64, '.base64_encode($rawImageBytes).'" />';
}
The remaining functions handle the labels: setMargins()
, setLabelX()
and setLabelY()
. The x and y arguments of imagestring()
refer to the top-left corner of the text, and setText()
positions relative to the bottom-left corner of the blue rectangle.
Conclusion
That's all there is to building a simple line chart. The PHP is basic, the main challenge is handling x and y in a consistent manner.
You can use the Chart
(and ChartDraw
) class "as-is", or improve the code. Things I didn't try:
- Add the option to replace the line chart with a bar chart for one series of data: replace
connectTwoPoints()
- Add code for a "stacked" bar chart, i.e., two (or more series) of data, each bar the length of the first in one color, the length of the second on top of that in a different color: maybe use imagecolorat() to find the top of the first one?
History
- 16th September, 2020: Initial version