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: •),
hearts (♥ - HTML: ♥) 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($image, 0xFF, 0xFF, 0xFF);
$navy = imagecolorallocate($image, 0x00, 0x00, 0x80);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);
$gray = imagecolorallocate($image, 0xC0, 0xC0, 0xC0);
// 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 - 2 * $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:
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:
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:
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($image, 0xFF, 0xFF, 0xFF);
$navy = imagecolorallocate($image, 0x00, 0x00, 0x80);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);
$gray = imagecolorallocate($image, 0xC0, 0xC0, 0xC0);
// 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 - 2 * $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.
- "Bar charts" by Afan Ottenheimer. Similar in nature to what is described in this tutorial. Deals with
negative values as well.
- "Pie chart class" by Bjørn Bond. Because of some redundancy in the code, it could be
made much smaller. Some display glitches (background color showing through between slices) that could be
made to disappear by refining the draw methods. Recommended if you are going to use it as is,
without major changes.
- "»±ýͼ" by Xiaohu He is based on the previous "Pie chart class" in spite of its apparent unicode
title, but much smaller. Suffers from the same display glitches. Due to its small size, it is probably
better suited as a starting point if you want to build your own script.
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.