Click here to Skip to main content
14,635,491 members
Articles » Web Development » PHP » General
Article
Posted 16 Sep 2020

Tagged as

Stats

1.6K views
34 downloads
3 bookmarked

A Line Chart in Plain PHP

Rate this:
5.00 (1 vote)
Please Sign up or sign in to vote.
5.00 (1 vote)
16 Sep 2020CPOL
Explanation of a class that you can use to add a line chart to your PHP page
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.

Image 1

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:

// y1 for the blue line, y2 for the red line
// xlabel is not used in this example, see below
$resultArray[0]  = array('x'=>0, 'xlabel'=>'DEC', 'y1'=>2000, 'y2'=>1600); // x = days
                                                                           // since 01.01
$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);

// for the green line
$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);
	
	// blue line
	$errorMessage = $chart->addNewLine(0, 0, 255); // blue
	foreach ($resultArray as $i=>$valueArray) {
		$errorMessage = $chart->setPoint($valueArray['x'], $valueArray['y1'], 
                        strval($valueArray['x']));
// if you prefer the xlabel text values, make the previous line comment 
// and uncomment the next line
//		$chart->setPoint($valueArray['x'], $valueArray['y1'], $valueArray['xlabel']); 
	}
		
	// red line
	$errorMessage = $chart->addNewLine(255, 0, 0); // red
	foreach ($resultArray as $i=>$valueArray) {
		$errorMessage = $chart->setPoint($valueArray['x'], $valueArray['y2'], '');
	}
		
	// green line
	$errorMessage = $chart->addNewLine(0, 255, 0); // green
	$chart->setMinMaxX(0, 4, 0);					
	foreach ($resultGreenArray as $i=>$value) {
		$errorMessage = $chart->setPoint($i, $value, '');
	}

	// show
	
	$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;
    // if $aMinY negative, the text length can be longer than $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) { // create at first call of this function
        $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:

Image 2

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);

    // this creates a black rectangle, so make everything white:

    $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);
        }
    }

    // set border lines

    $this->black = imagecolorallocate($this->gd, 0, 0, 0); // also need this one later

    for ($x = 0; $x <= $this->sizeX; $x++) {
        $this->setPixel($x, 0, $this->black);              // bottom x-axis
        $this->setPixel($x, $this->sizeY, $this->black);   // top x-line
    }
    for ($y = 0; $y <= $this->sizeY; $y++) {
        $this->setPixel(0, $y, $this->black);              // left y-axis
        $this->setPixel($this->sizeX, $y, $this->black);   // right y-line
    }
}

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:

Image 3

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 ifs will hit the same points (bar rounding).

The show() function displays the image:

public function show()
{
    ob_start();                      // Begin capturing the byte stream

    imagejpeg($this->gd, NULL, 100); // generate the byte stream

    $rawImageBytes = ob_get_clean(); // and finally retrieve the byte stream

    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

mislc
Netherlands Netherlands
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --