picture of Stefan Wiesendanger

Introduction

In this tutorial, I will give ideas on how the graphics capabilities of PHP can be used to create simple bar charts in GIF and PNG format. I will briefly touch on non-graphics alternatives as well. My objective is to show you some basics that will enable you to proceed to much more complex charts. The included code will give you a working foundation to start with. If you are looking for further inspiration after this tutorial, you can find pointers to related code - most notably for pie charts - in the very end.
The focus will be on drawing. We'll create a chart from an array $data whose keys will be taken to be the X axis labels and whose values are the numbers to be plotted on the Y axis. I will not address the question of how to get data into the array in the first place. If you are interested in that information, I recommend you read the column "Graphing With PHP and GD" by Allan Kent here on PHPBuilder.
One of the neat features of PHP is that you can create graphics on the fly. In order to accomplish this, you need to compile PHP with the GD image library or install it from packages that support GD. Under Linux, the standard mod_php3 RPM packages from RedHat have everything you'll need for running PHP3 as an Apache module. If you insist on the newer PHP4, RPMs are available from Troels Arvin at http://fsr.ku.dk/people/troels/rpms/php/. Before long, more official versions should also show up on RPMfind and other redhat-contrib mirrors.
To integrate a PHP generated image into your web pages, all it takes is an HTML IMG tag which will call a script that streams image data to the browser, instead of the IMG tag pointing to a file on disk:
<IMG SRC="chart.php" HEIGHT="height_of_chart" WIDTH="width_of_chart">
I have used the techniques described on several production sites for over a year without any problems. Just one word of caution: if you put dynamically generated graphics on all your pages, performance may suffer. But don't despair, I'll describe several cures for this problem later.
In general, I like to stick to the "keep it simple" rule. That's why I am going to cover some alternative ways for drawing charts before entering into the subject of how to create pixel graphics. For your needs, it may not even take all the layout control and flexibility of a bitmapped image.

Alternatives

PHP wouldn't be PHP if there were no other ways to achieve similar effects, without having to rely on graphics and the GD library. The simplest solution is to print a number of stars (*) proportional to the value you want to plot. You can of course use any other character you like: bullets ( - HTML: &bull;), hearts ( - HTML: &hearts;) etc. For layout control, use a preformatted section <PRE><PRE> or a table. This is probably the most economic way to create charts in terms of server load and page download times.
Let's assume the maximum data value you'll need to plot is $maxval and it will be drawn as a number $maxsize of stars. In order to plot the value $val, you could write:

<?php

echo "<PRE>"
for (
$i 0$i < (int)($val*$maxsize $maxval); $i++) echo "*"
echo 
"\n"
echo 
"</PRE>"

?>
For a whole series of values, this results in something like the following chart:
  535'385   *****
1'984'345   ********************
  354'899   ****
  893'423   *********
Another possibility is to employ tables or table cells that you force to a specific width and background color. This even lets you use border effects and graphical backgrounds on the columns and bars of your chart. However, it's not always easy to come up with a layout that will work as expected on all browsers.
The most elegant trick in my opinion is to use small GIFs of $base_of_gif by 1 pixels and to stretch them to a certain height or width using a $stretching factor = (int)($val*$maxsize / $maxval) as before when plotting stars. The only difference this time, $maxsize is in pixels instead of stars.
<IMG SRC="row.gif" HEIGHT="<?php echo $base_of_gif; ?>" WIDTH="<?php echo $stretching_factor; ?>"> 
The samples below were done in this way. Feel free to adapt the HTML source to your needs.
Charts can be done both vertically:
535'385 1'984'345 354'899 893'423
and horizontally:
535'385
1'984'345
354'899
893'423
Tim Perdue's PHPBuilder column "HTML Graphs" describes a library coincidentally called HTML_Graphs that unites all these ideas in a handy package.
Finally, you could dynamically generate Adobe PDF with PHP, but I don't really see why you would want to do that, unless you want to produce high quality material for printing. In most cases, the layout control offered by HTML will be sufficient and you won't annoy users who don't have Acrobat Reader.

How To Generate The Image

Now that I've kept you waiting for so long, on to create that image! The source code below shows the general stub for creating an image in PHP.
Let's define the image's $width and $height first, since you'll probably want to include those in the calling HTML IMG tag. Next, we allocate some memory for drawing the image using the imagecreate() function. Also, we need to allocate all the colors we are going to use. The first color to be allocated will be the background color of the image by default. The RGB color values can also be given in decimal numbers, instead of the hex I use below.
Before sending the image out, we must include an HTTP header telling the receiving browser how to interpret the binary data it gets thrown at. Finally, imagegif() outputs the image over the net and imagedestroy() frees up the internal memory we reserved in the beginning.

<?php

/* chart.php */ 

$width 480
$height 250
$image imagecreate($width$height); 

// colors 
$white imagecolorallocate($image0xFF0xFF0xFF); 
$navy imagecolorallocate($image0x000x000x80); 
$black imagecolorallocate($image0x000x000x00); 
$gray imagecolorallocate($image0xC00xC00xC0); 

// draw something here 

// flush image 
header("Content-type: image/gif"); // or "Content-type: image/png" 
imagegif($image); // or imagepng($image) 
imagedestroy($image); 

?>
If you want to use the PNG image format instead of GIF, simply change the Content-type to image/png and replace imagegif() by imagepng(). A final trick is to use the imagegif/imagepng($image, $path) functions with a second argument. This will save the image to the file in $path, instead of sending it to the browser. We'll use this later for caching the image.
Layout And Borders
As of now, the image is disappointingly empty. Let's assume you want to plot a time series with 12 values. For example, a fictional president's approval ratings throughout one year. A public relations disaster hits sometime in April and it took the spin doctors a full 6 months and everybody's Christmas mood to restablish some confidence in his leadership. This is what the data might look like:

<?php

$data 
= array( 
"Jan" => 55
"Feb" => 54
"Mar" => 53
"Apr" => 33
"May" => 13
"Jun" => 15
"Jul" => 23
"Aug" => 28
"Sep" => 32
"Oct" => 45
"Nov" => 73
"Dec" => 71); 

?>
We can easily get the maximum value to plot - 73 - and the number of elements - 12 - from this array using max() and sizeof(). This will allow us to scale the chart to the size of the image. But first, we'll have to decide on the general layout.
We need a vertical margin to leave some space both for a title on top and for X axis labels on the bottom. A horizontal margin will make room on the left-hand side for the Y axis labels. This leaves an area of $xsize by $ysize pixels for the chart.
In the code sniplet below, we use imagerectangle() to draw a frame for the chart area. Note that X and Y coordinates are counted from the upper left corner which is (0, 0).

<?php

$maxval 
max($data); 
$nval sizeof($data); 

$vmargin 20// top (bottom) vertical margin for title (x-labels) 
$hmargin 38// left horizontal margin for y-labels 

$base floor(($width $hmargin) / $nval); // distance between columns 

$ysize $height $vmargin// y-size of plot 
$xsize $nval $base// x-size of plot 

// plot frame 
imagerectangle($image$hmargin$vmargin
    
$hmargin $xsize$vmargin $ysize$black); 

?>

Adding A Title

Remember how your science teacher would always bug you about labels and units? Well, both really are important, so let's add a title using built-in font number 3 (out of 1..5). Fonts other than numbers 1-5 have to be loaded first.
The imagestring() function draws a string on the image, starting at a given (X, Y) position. In order to center the title, we need to calculate the left X limit of the string. For this, we subtract half of the width of the string from the middle position. The width of the string in pixels is obtained using imagefontwidth(), who returns the pixel width of a character in a given font. All of this put together gives:

<?php

// title 
$titlefont 3
$title "Presidential Approval Ratings 2000 (in %)"

// pixel-width of title
$txtsz imagefontwidth($titlefont) * strlen($title);

$xpos = (int)($hmargin + ($xsize $txtsz)/2); // center the title 
$xpos max(1$xpos); // force positive coordinates 
$ypos 3// distance from top 

imagestring($image$titlefont$xpos$ypos$title $black); 

?>
At this point, the chart looks like this:
Presidential Approval Ratings chart, unfinished
Adding Y Labels And A Horizontal Grid
The following code draws grid lines at the Y tick heights and prints the tick labels at the same time.
We first define the font to use for the labels and the number of grid lines on the chart. Next, we calculate how many data units are between grid lines. In our example, we are plotting percentages, so the 4 grid lines could be at 20%, 40%, 60% and 80%. They are separated by a number of pixels equal to one fifth of the height of the chart ($ysize). The code below actually doesn't produce 20% steps because it auto-scales the Y axis.
While iterating from 0% to 100%, we print a label on the left-hand side and a horizontal grid line each time. The label is centered vertically at the level of the grid line and horizontally in the middle of the left vertical margin. We use the same centering technique as before for the title.

<?php

// y labels and grid lines 
$labelfont 2
$ngrid 4// number of grid lines 

$dydat $maxval $ngrid// data units between grid lines 
$dypix $ysize / ($ngrid 1); // pixels between grid lines 

for ($i 0$i <= ($ngrid 1); $i++) { 
    
// iterate over y ticks 

    
$ydat = (int)($i $dydat); // height of grid line in units of data 
    
$ypos $vmargin $ysize - (int)($i*$dypix); // height of grid line in pixels 

    
$txtsz imagefontwidth($labelfont) * strlen($ydat); // pixel-width of label 
    
$txtht imagefontheight($labelfont); // pixel-height of label 

    
$xpos = (int)(($hmargin $txtsz) / 2); 
    
$xpos max(1$xpos); 

    
imagestring($image$labelfont$xpos$ypos - (int)($txtht/2), $ydat$black); 

    if (!(
$i == 0) && !($i $ngrid)) 
        
imageline($image$hmargin 3$ypos$hmargin $xsize$ypos$gray); 
        
// don't draw at Y=0 and top 


?>
With this, our glorious chart already takes on a more useful shape:
Presidential Approval Ratings, next step
Plotting Vertical Columns And X Labels
Plotting the labels is basically just a repetition of what's been done before for the Y axis, with slightly changing X and Y positions. The only thing to consider is that if the labels (i.e. the array keys taken from $data) are too big to fit in the width of a column ($base), they will overlap. However, since the code uses relative sizes all along, this can easily be corrected by just using a bigger $width.
We draw the vertical columns as filled rectangles in navy blue. This requires that we know the X and Y limits of the rectangle. In the vertical Y direction, we use a scaling factor that represents the number of pixels for one data unit. In our case, that would be the number of pixels per percentage point. In the X direction, the width of the column is just the distance between columns - $base - less some right and left padding.

<?php

// columns and x labels 
$padding 3// half of spacing between columns 
$yscale $ysize / (($ngrid+1) * $dydat); // pixels per data unit 

for ($i 0; list($xval$yval) = each($data); $i++) { 

    
// vertical columns 
    
$ymax $vmargin $ysize
    
$ymin $ymax - (int)($yval*$yscale); 
    
$xmax $hmargin + ($i+1)*$base $padding
    
$xmin $hmargin $i*$base $padding

    
imagefilledrectangle($image$xmin$ymin$xmax$ymax$navy); 

    
// x labels 
    
$txtsz imagefontwidth($labelfont) * strlen($xval); 

    
$xpos $xmin + (int)(($base $txtsz) / 2); 
    
$xpos max($xmin$xpos); 
    
$ypos $ymax 3// distance from x axis 

    
imagestring($image$labelfont$xpos$ypos$xval$black); 


?>
The final result of this whole effort looks like this:
final result of Presidential Approval Rating chart
What Remains To Be Done
You may note in the chart above that there seems to be a slight glitch for the first value. In January, approval was 55% but the column just reaches the 54% grid line. Actually, the grid line is at 54.75%. The cast to (int) used for the Y labels seems to truncate instead of rounding. If this bothers you, use the correct rounding function instead: floor($positive_value_to_round + 0.5). Another possibility is to never do casts and format numbers using sprintf().
To create more fancy graphs, you can fill the columns with a pattern loaded from another image file. In fact, the stretching of small GIFs we've described in the "Alternatives" section can also be done in PHP. First, you need to load the GIF with ImageCreateFromGif(). Then, you copy a stretched version of it into the chart using ImageCopyResized().
If there are multiple series to plot, line charts are more convenient than bar charts. In that case, you'd use ImageLine() to do the graphing. I refer you to Allan Kent's column "Graphing With PHP and GD" for further ideas.
Finally, you have to figure out a way to get your data into the $data array. Since I use MySQL for most of my work, I find the SQL COUNT(*) construct very useful. To create time series like the one above with records containing DATETIME fields, I'd use:
"SELECT COUNT(*) FROM table WHERE date LIKE '%-$month-%'"
Performance Issues
Now that you can generate graphics on the fly, you might worry (and rightly so) that this could bring your server to its knees. If your site is low-traffic, the problem is marginal. Generating an image like the one above takes well under a second on my Pentium 166 MHz and maybe a second if you include all the overhead for compiling the script. The PHP cache announced by Zend should bring down this latency dramatically.
While we're all waiting for the Zend cache to hit the shelves - or rather ftp servers around the world - there are other solutions to lower the response time. We'll just cache the image on disk. This can be done in the following way:

<?php

/* cached_chart.php */ 

$path "path/to/image/file"
$cachefor 3600// time to cache image in seconds 

if (filemtime($path) + $cachefor time()) { 

    
// draw the image 

    
imagegif($image$path); // write image to disk 
    
imagedestroy($image); 


header("Content-type: image/gif"); 
readfile($path); // send cached copy 

?>
To reduce server load even further, you can put everything between the if brackets in an include file and use include() for conditional inclusion. This means that the image-generating code will only be parsed when needed.
A last idea is to use the CGI version of PHP in the way suggested by Darrell Brogdon in "Using PHP As A Shell Scripting Language" here on PHPBuilder. The image generating script would then be called by cron on a schedule that will depend on how timely your chart has to be. This is probably the most modular way to generate charts. It will also do away with the inconsistent naming of GIF and PNG files with the .php extension.
Source Code
All the source of chart.php in one nice piece for your own tweaking and twiddling:

<?php

/* chart.php */ 
$data = array( 
"Jan" => 55
"Feb" => 54
"Mar" => 53
"Apr" => 33
"May" => 13
"Jun" => 15
"Jul" => 23
"Aug" => 28
"Sep" => 32
"Oct" => 45
"Nov" => 73
"Dec" => 71); 

// create image 
$width 480
$height 250
$image imagecreate($width$height); 

// colors 
$white imagecolorallocate($image0xFF0xFF0xFF); 
$navy imagecolorallocate($image0x000x000x80); 
$black imagecolorallocate($image0x000x000x00); 
$gray imagecolorallocate($image0xC00xC00xC0); 

// layout 
$maxval max($data); 
$nval sizeof($data); 

$vmargin 20// top (bottom) vertical margin for title (x-labels) 
$hmargin 38// left horizontal margin for y-labels 

$base floor(($width $hmargin) / $nval); // distance between columns 

$ysize $height $vmargin// y-size of plot 
$xsize $nval $base// x-size of plot 

// title 
$titlefont 3
$title "Presidential Approval Ratings 2000 (in %)"

$txtsz imagefontwidth($titlefont) * strlen($title); // pixel-width of title 

$xpos = (int)($hmargin + ($xsize $txtsz)/2); // center the title 
$xpos max(1$xpos); // force positive coordinates 
$ypos 3// distance from top 

imagestring($image$titlefont$xpos$ypos$title $black); 

// y labels and grid lines 
$labelfont 2
$ngrid 4// number of grid lines 

$dydat $maxval $ngrid// data units between grid lines 
$dypix $ysize / ($ngrid 1); // pixels between grid lines 

for ($i 0$i <= ($ngrid 1); $i++) { 
    
// iterate over y ticks 

    // height of grid line in units of data 
    
$ydat = (int)($i $dydat); 

    
// height of grid line in pixels
    
$ypos $vmargin $ysize - (int)($i*$dypix); 

    
$txtsz imagefontwidth($labelfont) * strlen($ydat); // pixel-width of label 
    
$txtht imagefontheight($labelfont); // pixel-height of label 

    
$xpos = (int)(($hmargin $txtsz) / 2); 
    
$xpos max(1$xpos); 

    
imagestring($image$labelfont$xpos
        
$ypos - (int)($txtht/2), $ydat$black); 

    if (!(
$i == 0) && !($i $ngrid)) {
        
imageline($image$hmargin 3
            
$ypos$hmargin $xsize$ypos$gray); 
        
// don't draw at Y=0 and top 
    
}


// columns and x labels 
$padding 3// half of spacing between columns 
$yscale $ysize / (($ngrid+1) * $dydat); // pixels per data unit 

for ($i 0; list($xval$yval) = each($data); $i++) { 

    
// vertical columns 
    
$ymax $vmargin $ysize
    
$ymin $ymax - (int)($yval*$yscale); 
    
$xmax $hmargin + ($i+1)*$base $padding
    
$xmin $hmargin $i*$base $padding

    
imagefilledrectangle($image$xmin$ymin$xmax$ymax$navy); 

    
// x labels 
    
$txtsz imagefontwidth($labelfont) * strlen($xval); 

    
$xpos $xmin + (int)(($base $txtsz) / 2); 
    
$xpos max($xmin$xpos); 
    
$ypos $ymax 3// distance from x axis 

    
imagestring($image$labelfont$xpos$ypos$xval$black); 


// plot frame 
imagerectangle($image$hmargin$vmargin
    
$hmargin $xsize$vmargin $ysize$black); 

// flush image 
header("Content-type: image/gif"); // or "Content-type: image/png" 
imagegif($image); // or imagepng($image) 
imagedestroy($image); 

?>
You'll note that unlike before, the frame around the chart is drawn in the very end. This is for aesthetic reasons: it now appears in front of the grid lines rather than behind.
Finally, here's the calling approval.html:
<HTML>
<BODY>
<H1>Everybody Loves your_country's_president</H1>
<IMG SRC="chart.php" HEIGHT="250" WIDTH="480">
</BODY>
<HTML>

Related Samples

One of many excellent code repositories for PHP sniplets is at px.sklar.com. The "Graphics" section features the following chart-related items. I've added sample output each time.
I hope that this article provided you with enough ideas to get you started. Don't hesitate to contact me with your remarks and suggestions.