[ Team LiB ] Previous Section Next Section

Bringing It Together

Let's build an example that uses some of the functions we have examined in this hour. Suppose that we have been asked to produce a dynamic bar chart that compares a range of labeled numbers. The bar chart must include the relevant label below each bar. Our client must be able to change the number of bars on the chart, the height and width of the image, and the size of the border around the chart. The bar chart will be used for consumer votes, and all that is needed is an at-a-glance representation of the data. A more detailed breakdown will be included in the HTML portion of the containing page.

To make our code reasonably reusable, we create a class called SimpleBar.

Before we even reach the constructor, we can set up some values that we don't intend to make changeable by the client. We declare them private, like so:


private $xgutter = 20; // left/right margin
private $ygutter = 20; // top/bottom margin
private $bottomspace = 30; // gap at the bottom
private $internalgap = 10; // space between bars
private $cells = array(); // labels/amounts for bar chart

The $xgutter and $ygutter properties determine the margin around the chart horizontally and vertically. $internalgap determines the space between the bars, and the $bottomspace property contains the space available to label the bars at the bottom of the screen.

In the constructor, we assign some values to properties we want the client coder to be able to influence:


function __construct( $width, $height, $font ) {
  $this->totalwidth = $width;
  $this->totalheight = $height;
  $this->font = $font;
}

The constructor is called with a width, height, font, and properties set accordingly. Now we have most of the parameters in place, except for the data to be displayed.

The easiest way of storing labels and values is in an associative array. Our class will have a property called $cells, and we will allow client code to add to this array through a method called addBar():


function addBar( $label, $amount ) {
  $this->cells[ $label ] = $amount;
}

With our parameters in place, we can define a draw() method to work with them:


function draw() {
    $image = imagecreate( $this->totalwidth, $this->totalheight );
    $red = ImageColorAllocate($image, 255, 0, 0);
    $blue = ImageColorAllocate($image, 0, 0, 255 );
    $black = ImageColorAllocate($image, 0, 0, 0 );
//...

First, we acquire an image resource and set up some colors. We won't make color a factor that client code can change, although we could consider this for the future:


    $max = max( $this->cells );
    $total = count( $this->cells );
    $graphCanX = ( $this->totalwidth - $this->xgutter*2 );
    $graphCanY = ( $this->totalheight - $this->ygutter*2
            - $this->bottomspace );
    $posX = $this->xgutter;
    $posY = $this->totalheight - $this->ygutter - $this->bottomspace;
    $cellwidth = (int)(( $graphCanX -
      ( $this->internalgap * ( $total-1 ) )) / $total) ;
    $textsize = $this->getTextSize( $cellwidth );

First, we cache the maximum value in our cells property and the number of elements it contains. We calculate the graph canvas (the space in which the bars are to be written). On the x-axis, this is the total width minus twice the size of the margin. On the y-axis, we also need to take account of the $bottomspace property to leave room for the labels.

$posX stores the point on the x-axis at which we will start drawing the bars, so we set this to the same value as $xgutter, which contains the value for the margin on the $x axis. $posY stores the bottom point of our bars; it is equivalent to the total height of the image less the margin and the space for the labels stored in $bottomheight.

$cellwidth contains the width of each bar. To arrive at this value, we must calculate the total amount of space between bars, take this from the chart width, and divide this result by the total number of bars.

Before we can create and work with our image, we need to determine the text size. Our problem is that we don't know how long the labels will be, and we want to ensure that each of the labels will fit within the width of the bar above it. We call a private method—getTextSize()—and pass it the $cellwidth variable we have calculated:


private function _getTextSize( $cellwidth ) {
  $textsize = (int)($this->bottomspace);
  if ( $cellwidth < 10 ) {
    $cellwidth = 10;
  }
  foreach ( $this->cells as $key=>$val ) {
    while ( true ) {
      $box = ImageTTFbBox( $textsize, 0, $this->font, $key );
      $textwidth = abs( $box[2] );
      if ( $textwidth < $cellwidth ) {
        break;
      }
      $textsize--;
    }
  }
  return $textsize;
}

We then loop through the $cells property array to calculate the maximum text size we can use.

For each of the elements, we begin a loop, acquiring dimension information for the label using imageTTFbbox(). We take the text width to be $box[2] and test it against the $cellwidth variable, which contains the width of a single bar in the chart. We break the loop if the text is smaller than the bar width; otherwise, we decrement $textsize and try again. $textsize continues to shrink until every label in the array fits within the bar width. We can now return a value for use in the draw() method.

Finally, we can create an image resource and begin to work with it:


//...
    foreach ( $this->cells as $key=>$val ) {
      $cellheight = (int)(($val/$max) * $graphCanY);
      $center = (int)($posX+($cellwidth/2));
      imagefilledrectangle( $image, $posX, ($posY-$cellheight),
        ($posX+$cellwidth), $posY, $blue );
      $box = imageTTFbBox( $textsize, 0, $this->font, $key );
      $tw = $box[2];
      ImageTTFText( $image, $textsize, 0, ($center-($tw/2)),
          ($this->totalheight-$this->ygutter), $black,
          $this->font, $key );
      $posX += ( $cellwidth + $this->internalgap);
    }
    imagepng( $image );

Once again, we loop through our $cells array and calculate the height of the bar, storing the result in $cellheight. We calculate the center point (on the x-axis) of the bar, which is $posX plus half the width of the bar.

Next, we draw the bar, using imagefilledrectangle() and the variables $posX, $posY, $cellheight, and $cellwidth.

To align our text, we use imageTTFbbox() again, storing its return array in $box. We use $box[2] as our working width and assign this to a temporary variable, $tw. We now have enough information to write the label. We derive our x position from the $center variable minus half the width of the text and derive our y position from the image's height minus the margin.

We increment $posX to start working with the next bar.

Finally, we output the image.

Although our basic bar chart class has some ugly internals, its interface code is simplicity itself from the point of view of a client coder:


$graph = new SimpleBar( 500, 300, "luxisri.ttf" );
$graph->addBar( "liked", 200 );
$graph->addBar( "hated", 400 );
$graph->addBar( "ok", 900 );
$graph->draw();

You can see the complete script in Listing 15.10 and sample output in Figure 15.11.

Figure 15.11. A dynamic bar chart.

graphics/15fig11.gif

Listing 15.10 A Dynamic Bar Chart
 1: <?php
 2: header("Content-type: image/png");
 3:
 4: class SimpleBar {
 5:   private $xgutter = 20; // left/right margin
 6:   private $ygutter = 20; // top/bottom margin
 7:   private $bottomspace = 30; // gap at the bottom
 8:   private $internalgap = 10; // space between bars
 9:   private $cells = array(); // labels/amounts for bar chart
10:   private $totalwidth; // width of the image
11:   private $totalheight; // height of the image
12:   private $font; // the font to use
13:
14:   function __construct( $width, $height, $font ) {
15:     $this->totalwidth = $width;
16:     $this->totalheight = $height;
17:     $this->font = $font;
18:   }
19:
20:   function addBar( $label, $amount ) {
21:     $this->cells[ $label ] = $amount;
22:   }
23:
24:   private function _getTextSize( $cellwidth ) {
25:     $textsize = (int)($this->bottomspace);
26:     if ( $cellwidth < 10 ) {
27:       $cellwidth = 10;
28:     }
29:     foreach ( $this->cells as $key=>$val ) {
30:       while ( true ) {
31:         $box = ImageTTFbBox( $textsize, 0, $this->font, $key );
32:         $textwidth = abs( $box[2] );
33:         if ( $textwidth < $cellwidth ) {
34:           break;
35:         }
36:         $textsize--;
37:      }
38:     }
39:     return $textsize;
40: }
41:
42: function draw() {
43:   $image = imagecreate( $this->totalwidth, $this->totalheight );
44:   $red = ImageColorAllocate($image, 255, 0, 0);
45:   $blue = ImageColorAllocate($image, 0, 0, 255 );
46:   $black = ImageColorAllocate($image, 0, 0, 0 );
47:
48:   $max = max( $this->cells );
49:   $total = count( $this->cells );
50:   $graphCanX = ( $this->totalwidth - $this->xgutter*2 );
51:   $graphCanY = ( $this->totalheight - $this->ygutter*2
52:          - $this->bottomspace );
53:   $posX = $this->xgutter;
54:   $posY = $this->totalheight - $this->ygutter - $this->bottomspace;
55:   $cellwidth = (int)(( $graphCanX -
56:     ( $this->internalgap * ( $total-1 ) )) / $total) ;
57:   $textsize = $this->_getTextSize( $cellwidth );
58:
59:   foreach ( $this->cells as $key=>$val ) {
60:     $cellheight = (int)(($val/$max) * $graphCanY);
61:     $center = (int)($posX+($cellwidth/2));
62:     imagefilledrectangle( $image, $posX, ($posY-$cellheight),
63:       ($posX+$cellwidth), $posY, $blue );
64:     $box = imageTTFbBox( $textsize, 0, $this->font, $key );
65:     $tw = $box[2];
66:     ImageTTFText( $image, $textsize, 0, ($center-($tw/2)),
67:         ($this->totalheight-$this->ygutter), $black,
68:         $this->font, $key );
69:     $posX += ( $cellwidth + $this->internalgap);
70:    }
71:
72:    imagepng( $image );
73:  }
74: }
75:
76: $graph = new SimpleBar( 500, 300, "luxisri.ttf" );
77: $graph->addBar( "liked", 200 );
78: $graph->addBar( "hated", 400 );
79: $graph->addBar( "ok", 900 );
80: $graph->draw();
81: ?>
    [ Team LiB ] Previous Section Next Section