]> git.sur5r.net Git - bacula/bacula/blob - gui/bacula-web/external_packages/phplot/phplot.php
c7f323c38db97808c0dbe78910879803ed9228f7
[bacula/bacula] / gui / bacula-web / external_packages / phplot / phplot.php
1 <?php
2 /* $Id: phplot.php,v 1.201 2010/10/03 21:57:09 lbayuk Exp $ */
3 /*
4  * PHPLOT Version 5.2.0
5  *
6  * A PHP class for creating scientific and business charts
7  * Visit http://sourceforge.net/projects/phplot/
8  * for PHPlot documentation, downloads, and discussions.
9  * ---------------------------------------------------------------------
10  * Copyright (C) 1998-2010 Afan Ottenheimer
11  *
12  * This is free software; you can redistribute it and/or
13  * modify it under the terms of the GNU Lesser General Public
14  * License as published by the Free Software Foundation;
15  * version 2.1 of the License.
16  *
17  * This software is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20  * Lesser General Public License for more details.
21  *
22  * You should have received a copy of the GNU Lesser General Public
23  * License along with this software; if not, write to the Free Software
24  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
25  * ---------------------------------------------------------------------
26  *
27  * Co-author and maintainer (2003-2005)
28  * Miguel de Benito Delgado <nonick AT vodafone DOT es>
29  *
30  * Maintainer (2006-present)
31  * <lbayuk AT users DOT sourceforge DOT net>
32  *
33  * Requires PHP 5.2.x or later. (PHP 4 is unsupported as of Jan 2008)
34  */
35
36 class PHPlot
37 {
38     /* Declare class variables which are initialized to static values. Many more class variables
39      * are used, defined as needed, but are unset by default.
40      * All these are declared as public. While it is tempting to make them private or protected, this
41      * is avoided for two reasons. First, it will break existing code, since all member variables
42      * were public in PHP4 and who knows what internal variables people used. Second, it makes
43      * testing harder and less effective. Nevertheless, your code should not modify these.
44      */
45
46     public $is_inline = FALSE;             // FALSE = Sends headers, TRUE = sends just raw image data
47     public $browser_cache = FALSE;         // FALSE = Sends headers for browser to not cache the image,
48                                            // (only if is_inline = FALSE also)
49     public $print_image = TRUE;            // DrawGraph calls PrintImage. See SetPrintImage
50     public $background_done = FALSE;       // TRUE after background image is drawn once
51
52     public $safe_margin = 5;               // Extra margin used in several places, in pixels
53
54     public $x_axis_position = '';          // X axis position in Y world coordinates, blank for default.
55     public $y_axis_position = '';          // Y axis position in X world coordinates, blank for default.
56
57     public $xscale_type = 'linear';        // linear, log
58     public $yscale_type = 'linear';
59
60 //Fonts
61     public $use_ttf  = FALSE;              // Use True Type Fonts by default?
62     public $ttf_path = '.';                // Default path to look in for TT Fonts.
63     // public $default_ttfont;             // Initialized in GetDefaultTTFont
64     public $line_spacing = 4;              // Controls line spacing of multi-line labels
65
66     // Label angles: 0 or 90 degrees for fixed fonts, any for TTF
67     public $x_label_angle = 0;             // For X tick labels
68     // public $x_data_label_angle;         // For X data labels; defaults to x_label_angle - see CheckLabels()
69     public $y_label_angle = 0;             // For Y tick labels
70     public $y_data_label_angle = 0;        // For Y data labels
71
72 //Formats
73     public $file_format = 'png';
74     public $output_file = '';              // For output to a file instead of stdout
75
76 //Data
77     public $data_type = 'text-data';       // Structure of the data array
78     public $plot_type= 'linepoints';       // bars, lines, linepoints, area, points, pie, thinbarline, squared
79
80     public $label_scale_position = 0.5;    // Shifts data labels in pie charts. 1 = top, 0 = bottom
81     public $group_frac_width = 0.7;        // Bars use this fraction (0 to 1) of a group's space
82     public $bar_extra_space = 0.5;         // Number of extra bar's worth of space in a group
83     public $bar_width_adjust = 1;          // 1 = bars of normal width, must be > 0
84
85 // Titles
86     public $title_txt = '';
87
88     public $x_title_txt = '';
89     public $x_title_pos = 'none';          // plotdown, plotup, both, none
90
91     public $y_title_txt = '';
92     public $y_title_pos = 'none';          // plotleft, plotright, both, none
93
94 //Labels
95     // There are two types of labels in PHPlot:
96     //    Tick labels: Follow the grid, next to ticks in axis.
97     //                 Are drawn at grid drawing time, by DrawXTicks() and DrawYTicks()
98     //    Data labels: Follow the data points, and can be placed on the axis or the plot (x/y)
99     //                 Are drawn at graph plotting time, by Draw*DataLabel(), called by DrawLines(), etc.
100     //                 DrawXDataLabel() also draws vertical lines to data points, depending on
101     //                 draw_x_data_label_lines.
102     // Tick Labels
103     // Tick and Data label positions are not initialized, because PHPlot needs to tell if they
104     // defaulted or are set by the user. See CheckLabels() for details. The variables and
105     // effective defaults are shown here in comments (but CheckLabels adjusts the defaults).
106     // public $x_tick_label_pos = 'plotdown';     // X tick label position
107     // public $y_tick_label_pos = 'plotleft';     // Y tick label position
108     // public $x_data_label_pos = 'plotdown';     // X data label position
109     // public $y_data_label_pos = 'none';         // Y data label position
110
111     public $draw_x_data_label_lines = FALSE;   // Draw a line from the data point to the axis?
112
113     // Label format controls: (for tick, data and plot labels)
114     // Unset by default, these array members are used as needed for 'x' (x tick labels), 'xd' (x data
115     // labels), 'y' (y tick labels), and 'yd' (y data labels).
116     //    type, precision, prefix, suffix, time_format, printf_format, custom_callback, custom_arg.
117     // These replace the former: x_label_type, x_time_format, x_precision (similar for y), data_units_text.
118     public $label_format = array('x' => array(), 'xd' => array(), 'y' => array(), 'yd' => array());
119     // data_units_text is retained for backward compatibility, because there was never a function
120     // to set it. Use the 'suffix' argument to Set[XY]LabelType instead.
121     public $data_units_text = '';              // Units text for 'data' labels (i.e: 'ยค', '$', etc.)
122
123 // Legend
124     public $legend = '';                       // An array with legend titles
125     // These variables are unset to take default values:
126     // public $legend_x_pos;                   // User-specified upper left coordinates of legend box
127     // public $legend_y_pos;
128     // public $legend_xy_world;                // If set, legend_x/y_pos are world coords, else pixel coords
129     // public $legend_text_align;              // left or right, Unset means right
130     // public $legend_colorbox_align;          // left, right, or none; Unset means same as text_align
131
132 //Ticks
133     public $x_tick_length = 5;                 // tick length in pixels for upper/lower axis
134     public $y_tick_length = 5;                 // tick length in pixels for left/right axis
135
136     public $x_tick_cross = 3;                  // ticks cross x axis this many pixels
137     public $y_tick_cross = 3;                  // ticks cross y axis this many pixels
138
139     public $x_tick_pos = 'plotdown';           // plotdown, plotup, both, xaxis, none
140     public $y_tick_pos = 'plotleft';           // plotright, plotleft, both, yaxis, none
141
142     public $num_x_ticks = '';
143     public $num_y_ticks = '';
144
145     public $x_tick_inc = '';                   // Set num_x_ticks or x_tick_inc, not both.
146     public $y_tick_inc = '';                   // Set num_y_ticks or y_tick_inc, not both.
147
148     public $skip_top_tick = FALSE;
149     public $skip_bottom_tick = FALSE;
150     public $skip_left_tick = FALSE;
151     public $skip_right_tick = FALSE;
152
153 //Grid Formatting
154     // public $draw_x_grid = FALSE;            // Default is False except for swapped data type
155     // public $draw_y_grid = TRUE;             // Default is True except for swapped data type
156
157     public $dashed_grid = TRUE;
158     public $grid_at_foreground = FALSE;        // Chooses whether to draw the grid below or above the graph
159
160 //Colors and styles       (all colors can be array (R,G,B) or named color)
161     public $color_array = 'small';             // 'small', 'large' or array (define your own colors)
162                                             // See rgb.inc.php and SetRGBArray()
163     public $default_colors = array(       // The default colors for data and error bars
164         'SkyBlue', 'green', 'orange', 'blue', 'red', 'DarkGreen', 'purple', 'peru',
165         'cyan', 'salmon', 'SlateBlue', 'YellowGreen', 'magenta', 'aquamarine1', 'gold', 'violet');
166
167     // See SetDefaultStyles() for default colors for PHPlot elements.
168
169     public $line_widths = 1;                  // single value or array
170     public $line_styles = array('solid', 'solid', 'dashed');   // single value or array
171     public $dashed_style = '2-4';              // colored dots-transparent dots
172
173     public $point_sizes = array(6);            // Array of sizes for points. See CheckPointParams()
174     public $point_shapes = array(              // Array of point shapes. See SetPointShapes() and DrawDot()
175           'diamond', 'dot', 'delta', 'home', 'yield', 'box', 'circle', 'up', 'down', 'cross'
176        );
177
178     public $error_bar_size = 5;                // right and left size of tee
179     public $error_bar_shape = 'tee';           // 'tee' or 'line'
180     public $error_bar_line_width = 1;          // single value (or array TODO)
181
182     public $plot_border_type = 'sides';        // left, right, top, bottom, sides, none, full; or array
183     public $image_border_type = 'none';        // 'raised', 'plain', 'none'
184     // public $image_border_width;             // NULL, 0, or unset for default. Default depends on type.
185
186     public $shading = 5;                       // 0 for no shading, > 0 is size of shadows in pixels
187
188     public $draw_plot_area_background = FALSE;
189     public $draw_broken_lines = FALSE;          // Tells not to draw lines for missing Y data.
190
191 //Miscellaneous
192     public $callbacks = array(                  // Valid callback reasons (see SetCallBack)
193         'draw_setup' => NULL,
194         'draw_image_background' => NULL,
195         'draw_plotarea_background' => NULL,
196         'draw_titles' => NULL,
197         'draw_axes' => NULL,
198         'draw_graph' => NULL,
199         'draw_border' => NULL,
200         'draw_legend' => NULL,
201         'draw_all' => NULL,
202         'data_color' => NULL,
203         'debug_textbox' => NULL,  // For testing/debugging text box alignment
204         'debug_scale' => NULL,    // For testing/debugging scale setup
205     );
206
207 //////////////////////////////////////////////////////
208 //BEGIN CODE
209 //////////////////////////////////////////////////////
210
211     /*
212      * Constructor: Setup img resource, colors and size of the image, and font sizes.
213      *
214      *   $which_width : Image width in pixels.
215      *   $which_height : Image height in pixels.
216      *   $which_output_file : Filename for output.
217      *   $which_input_file : Path to a file to be used as background.
218      */
219     function PHPlot($which_width=600, $which_height=400, $which_output_file=NULL, $which_input_file=NULL)
220     {
221         $this->SetRGBArray($this->color_array);
222
223         if ($which_output_file)
224             $this->SetOutputFile($which_output_file);
225
226         if ($which_input_file) {
227             $this->SetInputFile($which_input_file);
228         } else {
229             $this->image_width = $which_width;
230             $this->image_height = $which_height;
231
232             $this->img = ImageCreate($this->image_width, $this->image_height);
233             if (! $this->img)
234                 return $this->PrintError('PHPlot(): Could not create image resource.');
235         }
236
237         $this->SetDefaultStyles();
238         $this->SetDefaultFonts();
239     }
240
241     /*
242      * Reads an image file. Stores width and height, and returns the image
243      * resource. On error, calls PrintError and returns False.
244      * This is used by the constructor via SetInputFile, and by tile_img().
245      */
246     protected function GetImage($image_filename, &$width, &$height)
247     {
248         $error = '';
249         $size = getimagesize($image_filename);
250         if (!$size) {
251             $error = "Unable to query image file $image_filename";
252         } else {
253             $image_type = $size[2];
254             switch ($image_type) {
255             case IMAGETYPE_GIF:
256                 $img = @ ImageCreateFromGIF ($image_filename);
257                 break;
258             case IMAGETYPE_PNG:
259                 $img = @ ImageCreateFromPNG ($image_filename);
260                 break;
261             case IMAGETYPE_JPEG:
262                 $img = @ ImageCreateFromJPEG ($image_filename);
263                 break;
264             default:
265                 $error = "Unknown image type ($image_type) for image file $image_filename";
266                 break;
267             }
268         }
269         if (empty($error) && !$img) {
270             // getimagesize is OK, but GD won't read it. Maybe unsupported format.
271             $error = "Failed to read image file $image_filename";
272         }
273         if (!empty($error)) {
274             return $this->PrintError("GetImage(): $error");
275         }
276         $width = $size[0];
277         $height = $size[1];
278         return $img;
279     }
280
281     /*
282      * Selects an input file to be used as background for the whole graph.
283      * This resets the graph size to the image's size.
284      * Note: This is used by the constructor. It is deprecated for direct use.
285      */
286     function SetInputFile($which_input_file)
287     {
288         $im = $this->GetImage($which_input_file, $this->image_width, $this->image_height);
289         if (!$im)
290             return FALSE;  // GetImage already produced an error message.
291
292         // Deallocate any resources previously allocated
293         if (isset($this->img))
294             imagedestroy($this->img);
295
296         $this->img = $im;
297
298         // Do not overwrite the input file with the background color.
299         $this->background_done = TRUE;
300
301         return TRUE;
302     }
303
304 /////////////////////////////////////////////
305 //////////////                         COLORS
306 /////////////////////////////////////////////
307
308     /*
309      * Allocate a GD color index for a color specified by a 4 component array.
310      * When a color is requested, it is parsed and checked by SetRGBColor, and then saved as an array
311      * of (R,G,B,A) components. At graph drawing time, this function is used to allocate the color.
312      *   $color : The color specification as a 4 component array: R, G, B, A.
313      * Returns: A GD color index that can be used when drawing.
314      */
315     protected function GetColorIndex($color)
316     {
317         list($r, $g, $b, $a) = $color;
318         return imagecolorresolvealpha($this->img, $r, $g, $b, $a);
319     }
320
321     /*
322      * Allocate a GD color index for a darker shade of a color specified by a 4 component array.
323      * See notes for GetColorIndex() above.
324      *   $color : The color specification as a 4 component array: R, G, B, A.
325      * Returns: A GD color index that can be used when drawing.
326      */
327     protected function GetDarkColorIndex($color)
328     {
329         list ($r, $g, $b, $a) = $color;
330         $r = max(0, $r - 0x30);
331         $g = max(0, $g - 0x30);
332         $b = max(0, $b - 0x30);
333         return imagecolorresolvealpha($this->img, $r, $g, $b, $a);
334     }
335
336     /*
337      * Sets/reverts all colors and styles to their defaults.
338      */
339     protected function SetDefaultStyles()
340     {
341         $this->SetDefaultDashedStyle($this->dashed_style);
342         $this->SetImageBorderColor(array(194, 194, 194));
343         $this->SetPlotBgColor('white');
344         $this->SetBackgroundColor('white');
345         $this->SetLabelColor('black');
346         $this->SetTextColor('black');
347         $this->SetGridColor('black');
348         $this->SetLightGridColor('gray');
349         $this->SetTickColor('black');
350         $this->SetTitleColor('black');
351         // These functions set up the default colors when called without parameters
352         $this->SetDataColors();
353         $this->SetErrorBarColors();
354         $this->SetDataBorderColors();
355         return TRUE;
356     }
357
358     /*
359      * Set the image background color to $which_color.
360      */
361     function SetBackgroundColor($which_color)
362     {
363         return (bool)($this->bg_color = $this->SetRGBColor($which_color));
364     }
365
366     /*
367      * Set the plot area background color (if enabled) to $which_color.
368      */
369     function SetPlotBgColor($which_color)
370     {
371         return (bool)($this->plot_bg_color = $this->SetRGBColor($which_color));
372     }
373
374     /*
375      * Set the color of the titles (main, X, and Y) to $which_color.
376      * See also SetXTitleColor and SetYTitleColor.
377      */
378     function SetTitleColor($which_color)
379     {
380         return (bool)($this->title_color = $this->SetRGBColor($which_color));
381     }
382
383     /*
384      * Set the color of the X title to $which_color.
385      * This overrides the color set with SetTitleColor.
386      */
387     function SetXTitleColor($which_color)
388     {
389         return (bool)($this->x_title_color = $this->SetRGBColor($which_color));
390     }
391
392     /*
393      * Set the color of the Y title to $which_color.
394      * This overrides the color set with SetTitleColor.
395      */
396     function SetYTitleColor($which_color)
397     {
398         return (bool)($this->y_title_color = $this->SetRGBColor($which_color));
399     }
400
401     /*
402      * Set the color of the axis tick marks to $which_color.
403      */
404     function SetTickColor($which_color)
405     {
406         return (bool)($this->tick_color = $this->SetRGBColor($which_color));
407     }
408
409     /*
410      * Do not use. Use SetTitleColor instead.
411      */
412     function SetLabelColor($which_color)
413     {
414         return $this->SetTitleColor($which_color);
415     }
416
417     /*
418      * Set the general text color (tick and data labels, legend, etc) to $which_color.
419      */
420     function SetTextColor($which_color)
421     {
422         return (bool)($this->text_color = $this->SetRGBColor($which_color));
423     }
424
425     /*
426      * Set the X and Y grid colors to $which_color. Also sets the data label line color.
427      */
428     function SetLightGridColor($which_color)
429     {
430         return (bool)($this->light_grid_color = $this->SetRGBColor($which_color));
431     }
432
433     /*
434      * Set the color used for the X and Y axis, plot border, legend border to $which_color.
435      * Note: This has nothing to do with the grid, and we don't recall where this name came from.
436      */
437     function SetGridColor($which_color)
438     {
439         return (bool)($this->grid_color = $this->SetRGBColor($which_color));
440     }
441
442     /*
443      * Set the color used for the image border to $which_color.
444      */
445     function SetImageBorderColor($which_color)
446     {
447         return (bool)($this->i_border = $this->SetRGBColor($which_color));
448     }
449
450     /*
451      * Designate color $which_color to be transparent, if supported by the image format.
452      */
453     function SetTransparentColor($which_color)
454     {
455         return (bool)($this->transparent_color = $this->SetRGBColor($which_color));
456     }
457
458     /*
459      * Sets the array of colors to be used. It can be user defined, a small predefined one
460      * or a large one included from 'rgb.inc.php'.
461      *
462      *    $which_color_array : A color array, or 'small' or 'large'.
463      * Color arrays map color names into arrays of R, G, B and optionally A values.
464      */
465     function SetRGBArray($which_color_array)
466     {
467         if (is_array($which_color_array)) {           // User defined array
468             $this->rgb_array = $which_color_array;
469         } elseif ($which_color_array == 'small') {      // Small predefined color array
470             $this->rgb_array = array(
471                 'white'          => array(255, 255, 255),
472                 'snow'           => array(255, 250, 250),
473                 'PeachPuff'      => array(255, 218, 185),
474                 'ivory'          => array(255, 255, 240),
475                 'lavender'       => array(230, 230, 250),
476                 'black'          => array(  0,   0,   0),
477                 'DimGrey'        => array(105, 105, 105),
478                 'gray'           => array(190, 190, 190),
479                 'grey'           => array(190, 190, 190),
480                 'navy'           => array(  0,   0, 128),
481                 'SlateBlue'      => array(106,  90, 205),
482                 'blue'           => array(  0,   0, 255),
483                 'SkyBlue'        => array(135, 206, 235),
484                 'cyan'           => array(  0, 255, 255),
485                 'DarkGreen'      => array(  0, 100,   0),
486                 'green'          => array(  0, 255,   0),
487                 'YellowGreen'    => array(154, 205,  50),
488                 'yellow'         => array(255, 255,   0),
489                 'orange'         => array(255, 165,   0),
490                 'gold'           => array(255, 215,   0),
491                 'peru'           => array(205, 133,  63),
492                 'beige'          => array(245, 245, 220),
493                 'wheat'          => array(245, 222, 179),
494                 'tan'            => array(210, 180, 140),
495                 'brown'          => array(165,  42,  42),
496                 'salmon'         => array(250, 128, 114),
497                 'red'            => array(255,   0,   0),
498                 'pink'           => array(255, 192, 203),
499                 'maroon'         => array(176,  48,  96),
500                 'magenta'        => array(255,   0, 255),
501                 'violet'         => array(238, 130, 238),
502                 'plum'           => array(221, 160, 221),
503                 'orchid'         => array(218, 112, 214),
504                 'purple'         => array(160,  32, 240),
505                 'azure1'         => array(240, 255, 255),
506                 'aquamarine1'    => array(127, 255, 212)
507                 );
508         } elseif ($which_color_array == 'large')  {    // Large color array
509             if (!@include('rgb.inc.php')) {
510                 return $this->PrintError("SetRGBArray(): Large color map could not be loaded\n"
511                                        . "from 'rgb.inc.php'.");
512             }
513             $this->rgb_array = $ColorArray;
514         } else {                                        // Default to black and white only.
515             $this->rgb_array = array('white' => array(255, 255, 255), 'black' => array(0, 0, 0));
516         }
517
518         return TRUE;
519     }
520
521     /*
522      * Parse a color description and return the color component values.
523      * Arguments:
524      *   $color_asked : The desired color description, in one of these forms:
525      *       Component notation: array(R, G, B) or array(R, G, B, A) with each
526      *          in the range described below for the return value.
527      *          Examples: (255,255,0)  (204,0,0,30)
528      *       Hex notation: "#RRGGBB" or "#RRGGBBAA" where each pair is a 2 digit hex number.
529      *          Examples: #FF00FF (magenta)   #0000FF40 (Blue with alpha=64/127)
530      *       Named color in the current colormap, with optional suffix ":alpha" for alpha value.
531      *          Examples:  blue   red:60  yellow:20
532      *   $alpha : optional default alpha value. This is applied to the color if it doesn't
533      *       already have an alpha value. If not supplied, colors are opaque (alpha=0) by default.
534      *
535      * Returns an array describing a color as (R, G, B, Alpha).
536      * R, G, and B are integers 0-255, and Alpha is 0 (opaque) to 127 (transparent).
537      * Note: This function should be considered 'protected', and is not documented for public use.
538      */
539     function SetRGBColor($color_asked, $alpha = 0)
540     {
541         if (empty($color_asked)) {
542             $ret_val = array(0, 0, 0);
543
544         } elseif (is_array($color_asked) && (($n = count($color_asked)) == 3 || $n == 4) ) {
545             // Already an array of 3 or 4 elements:
546             $ret_val = $color_asked;
547
548         } elseif (preg_match('/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i',
549                              $color_asked, $ss)) {
550             // #RRGGBB or #RRGGBBAA notation:
551             $ret_val = array(hexdec($ss[1]), hexdec($ss[2]), hexdec($ss[3]));
552             if (isset($ss[4])) $ret_val[] = hexdec($ss[4]);
553
554         } elseif (isset($this->rgb_array[$color_asked])) {
555             // Color by name:
556             $ret_val = $this->rgb_array[$color_asked];
557
558         } elseif (preg_match('/(.+):([\d]+)$/', $color_asked, $ss)
559                   && isset($this->rgb_array[$ss[1]])) {
560             // Color by name with ":alpha" suffix, alpha is a decimal number:
561             $ret_val = $this->rgb_array[$ss[1]];
562             $ret_val[3] = (int)$ss[2];
563
564         } else {
565             return $this->PrintError("SetRGBColor(): Color '$color_asked' is not valid.");
566         }
567
568         // Append alpha if not already provided for:
569         if (count($ret_val) == 3)
570             $ret_val[] = $alpha;
571         return $ret_val;
572     }
573
574     /*
575      * Sets the colors for the data, with optional default alpha value (for PHPlot_truecolor only)
576      * Cases are:
577      *    SetDataColors(array(...))  : Use the supplied array as the color map.
578      *    SetDataColors(colorname)   : Use an array of just colorname as the color map.
579      *    SetDataColors() or SetDataColors(NULL) : Load default color map if no color map is already set.
580      *    SetDataColors('') or SetDataColors(False) : Load default color map (even if one is already set).
581      *  $which_border is passed to SetDataBorderColors, for backward compatibility.
582      *  $alpha is a default Alpha to apply to all data colors that do not have alpha.
583      *    The default for this is NULL, not 0, so we can tell if it was defaulted. But the effective
584      *    default value is 0 (opaque).
585      */
586     function SetDataColors($which_data = NULL, $which_border = NULL, $alpha = NULL)
587     {
588         if (is_array($which_data)) {
589             $colors = $which_data;  // Use supplied array
590         } elseif (!empty($which_data)) {
591             $colors = array($which_data);  // Use supplied single color
592         } elseif (empty($this->data_colors) || !is_null($which_data)) {
593             $colors = $this->default_colors;  // Use default color array
594         } else {
595             // which_data is NULL or missing and a color array is already set.
596             // The existing color array is left alone, except that if $alpha is
597             // given this will replace the alpha value of each existing color.
598             // This makes SetDataColors(NULL, NULL, $alpha) work.
599             if (isset($alpha)) {
600                 $n_colors = count($this->data_colors);
601                 for ($i = 0; $i < $n_colors; $i++) {
602                     $this->data_colors[$i][3] = $alpha; // Component 3 = alpha value
603                 }
604             }
605             // No need to reparse the colors or anything else.
606             return TRUE;
607         }
608
609         if (!isset($alpha))
610             $alpha = 0; // Actual default is opaque colors.
611
612         // Check each color and convert to array (r,g,b,a) form.
613         // Use the $alpha argument as a default for the alpha value of each color.
614         $this->data_colors = array();
615         foreach ($colors as $color) {
616             $color_array = $this->SetRGBColor($color, $alpha);
617             if (!$color_array) return FALSE; // SetRGBColor already did an error message.
618             $this->data_colors[] = $color_array;
619         }
620
621         // For past compatibility:
622         return $this->SetDataBorderColors($which_border);
623     }
624
625     /*
626      * Set the colors for the bars and stacked bars outlines.
627      * Argument usage is similar to SetDataColors(), except the default is just black.
628      */
629     function SetDataBorderColors($which_br = NULL)
630     {
631         if (is_array($which_br)) {
632             $colors = $which_br; // Use supplied array
633         } elseif (!empty($which_br)) {
634             $colors = array($which_br);  // Use supplied single color
635         } elseif (empty($this->data_border_colors) || !is_null($which_br)) {
636             $colors = array('black'); // Use default
637         } else {
638             return TRUE; // Do nothing: which_br is NULL or missing and a color array is already set.
639         }
640
641         // Check each color and convert to array (r,g,b,a) form.
642         $this->data_border_colors = array();
643         foreach ($colors as $color) {
644             $color_array = $this->SetRGBColor($color);
645             if (!$color_array) return FALSE; // SetRGBColor already did an error message.
646             $this->data_border_colors[] = $color_array;
647         }
648         return TRUE;
649     }
650
651     /*
652      * Sets the colors for the data error bars.
653      * Argument usage is the same as SetDataColors().
654      */
655     function SetErrorBarColors($which_err = NULL)
656     {
657         if (is_array($which_err)) {
658             $colors = $which_err;  // Use supplied array
659         } elseif (!empty($which_err)) {
660             $colors = array($which_err);  // Use supplied single color
661         } elseif (empty($this->error_bar_colors) || !is_null($which_err)) {
662             $colors = $this->default_colors;  // Use default color array
663         } else {
664             return TRUE; // Do nothing: which_err is NULL or missing and a color array is already set.
665         }
666
667         // Check each color and convert to array (r,g,b,a) form.
668         $this->error_bar_colors = array();
669         foreach ($colors as $color) {
670             $color_array = $this->SetRGBColor($color);
671             if (!$color_array) return FALSE; // SetRGBColor already did an error message.
672             $this->error_bar_colors[] = $color_array;
673         }
674         return TRUE;
675     }
676
677     /*
678      * Sets the default dashed line style.
679      *   $which_style : A string specifying the dashed line style, as alternating numbers
680      *         of the length (in pixels) of lines and spaces, separated by dashes.
681      *   For example: '2-3-1-2' means 2 dots of color, 3 transparent, 1 color, then 2 transparent.
682      *   This builds a string which will evaluate to an array of integers. Each colored dot
683      *   is  '$which_ndxcol' and each transparent dot is 'IMG_COLOR_TRANSPARENT'. When SetDashedStyle()
684      *   eval's this with $which_ndxcol set, the result is a GD line style array.
685      */
686     function SetDefaultDashedStyle($which_style)
687     {
688         // Explode "numcol-numtrans-numcol-numtrans..." into segment counts:
689         $asked = explode('-', $which_style);
690
691         if (count($asked) < 2) {
692             return $this->PrintError("SetDefaultDashedStyle(): Wrong parameter '$which_style'.");
693         }
694
695         // Build the string to be evaluated later by SetDashedStyle() with $which_ndxcolor set.
696         $result = '';
697         $vals = array('$which_ndxcol,', 'IMG_COLOR_TRANSPARENT,');
698         $index = 0;
699         foreach ($asked as $n) {
700             $result .= str_repeat($vals[$index], $n);
701             $index = 1 - $index;
702         }
703         $this->default_dashed_style = "array($result)";
704
705         return TRUE;
706     }
707
708     /*
709      * Sets the style before drawing a dashed line. Defaults to $this->default_dashed_style
710      *    $which_ndxcol : Color index to be used.
711      */
712     protected function SetDashedStyle($which_ndxcol)
713     {
714         // See SetDefaultDashedStyle() to understand this.
715         eval ("\$style = $this->default_dashed_style;");
716         return imagesetstyle($this->img, $style);
717     }
718
719     /*
720      * Set line widths for each data set.
721      *   $which_lw : Array of line widths in pixels, or a single value to use for all data sets.
722      */
723     function SetLineWidths($which_lw=NULL)
724     {
725         if (is_array($which_lw)) {
726             $this->line_widths = $which_lw; // Use provided array
727         } elseif (!is_null($which_lw)) {
728             $this->line_widths = array($which_lw); // Convert value to array
729         }
730         return TRUE;
731     }
732
733     /*
734      * Set line style ('solid' or 'dashed') for each data set.
735      *   $which_ls : Array of keywords, or a single keyword to use for all data sets.
736      */
737     function SetLineStyles($which_ls=NULL)
738     {
739         if (is_array($which_ls)) {
740             $this->line_styles = $which_ls; // Use provided array
741         } elseif (!is_null($which_ls)) {
742             $this->line_styles = ($which_ls) ? array($which_ls) : array('solid');
743         }
744         return TRUE;
745     }
746
747 /////////////////////////////////////////////
748 //////////////                 TEXT and FONTS
749 /////////////////////////////////////////////
750
751     /*
752      * Controls the line spacing of multi-line labels.
753      *   $which_spc : Line spacing factor for text
754      * For GD text, this is the number of pixels between lines.
755      * For TTF text, it controls line spacing in proportion to the normal
756      * spacing defined by the font.
757      */
758     function SetLineSpacing($which_spc)
759     {
760         $this->line_spacing = $which_spc;
761         return TRUE;
762     }
763
764     /*
765      * Select the default font type to use.
766      *   $which_ttf : True to default to TrueType, False to default to GD (fixed) fonts.
767      * This also resets all font settings to the defaults.
768      */
769     function SetUseTTF($which_ttf)
770     {
771         $this->use_ttf = $which_ttf;
772         return $this->SetDefaultFonts();
773     }
774
775     /*
776      * Sets the directory name to look into for TrueType fonts.
777      */
778     function SetTTFPath($which_path)
779     {
780         if (!is_dir($which_path) || !is_readable($which_path)) {
781             return $this->PrintError("SetTTFPath(): $which_path is not a valid path.");
782         }
783         $this->ttf_path = $which_path;
784         return TRUE;
785     }
786
787     /*
788      * Sets the default TrueType font and updates all fonts to that.
789      * The default font might be a full path, or relative to the TTFPath,
790      * so let SetFont check that it exists.
791      * Side effects: Enables use of TrueType fonts as the default font type,
792      * and resets all font settings.
793      */
794     function SetDefaultTTFont($which_font)
795     {
796         $this->default_ttfont = $which_font;
797         return $this->SetUseTTF(TRUE);
798     }
799
800     /*
801      * Return the default TrueType font name. If no default has been set,
802      * this tries some likely candidates for a font which can be loaded.
803      * If it finds one that works, that becomes the default TT font.
804      * If there is no default and it cannot find a working font, it falls
805      * back to the original PHPlot default (which will not likely work either).
806      */
807     protected function GetDefaultTTFont()
808     {
809         if (!isset($this->default_ttfont)) {
810             // No default font yet. Try some common sans-serif fonts.
811             $fonts = array('LiberationSans-Regular.ttf',  // For Linux with a correct GD font search path
812                            'Verdana.ttf', 'Arial.ttf', 'Helvetica.ttf', // For Windows, maybe others
813                            'ttf-liberation/LiberationSans-Regular.ttf', // For Debian, Ubuntu, and friends
814                            'benjamingothic.ttf',  // Original PHPlot default 
815                           );
816             foreach ($fonts as $font) {
817                 // First try the font name alone, to see if GD can find and load it.
818                 if (@imagettfbbox(10, 0, $font, "1") !== False)
819                     break;
820                 // If the font wasn't found, try it with the default TTF path in front.
821                 $font_with_path = $this->ttf_path . DIRECTORY_SEPARATOR . $font;
822                 if (@imagettfbbox(10, 0, $font_with_path, "1") !== False) {
823                     $font = $font_with_path;
824                     break;
825                 }
826             }
827             // We either have a working font, or are using the last one regardless.
828             $this->default_ttfont = $font;
829         }
830         return $this->default_ttfont;
831     }
832
833     /*
834      * Sets fonts to their defaults
835      */
836     protected function SetDefaultFonts()
837     {
838         // TTF:
839         if ($this->use_ttf) {
840             return $this->SetFont('generic', '', 8)
841                 && $this->SetFont('title', '', 14)
842                 && $this->SetFont('legend', '', 8)
843                 && $this->SetFont('x_label', '', 6)
844                 && $this->SetFont('y_label', '', 6)
845                 && $this->SetFont('x_title', '', 10)
846                 && $this->SetFont('y_title', '', 10);
847         }
848         // Fixed GD Fonts:
849         return $this->SetFont('generic', 2)
850             && $this->SetFont('title', 5)
851             && $this->SetFont('legend', 2)
852             && $this->SetFont('x_label', 1)
853             && $this->SetFont('y_label', 1)
854             && $this->SetFont('x_title', 3)
855             && $this->SetFont('y_title', 3);
856     }
857
858     /*
859      * Select a fixed (GD) font for an element.
860      * This allows using a fixed font, even with SetUseTTF(True).
861      *    $which_elem : The element whose font is to be changed.
862      *       One of: title legend generic x_label y_label x_title y_title
863      *    $which_font : A GD font number 1-5
864      *    $which_spacing (optional) : Line spacing factor
865      */
866     function SetFontGD($which_elem, $which_font, $which_spacing = NULL)
867     {
868         if ($which_font < 1 || 5 < $which_font) {
869             return $this->PrintError(__FUNCTION__ . ': Font size must be 1, 2, 3, 4 or 5');
870         }
871         if (!$this->CheckOption($which_elem,
872                                 'generic, title, legend, x_label, y_label, x_title, y_title',
873                                 __FUNCTION__)) {
874             return FALSE;
875         }
876
877         // Store the font parameters: name/size, char cell height and width.
878         $this->fonts[$which_elem] = array('ttf' => FALSE,
879                                           'font' => $which_font,
880                                           'height' => ImageFontHeight($which_font),
881                                           'width' => ImageFontWidth($which_font),
882                                           'line_spacing' => $which_spacing);
883         return TRUE;
884     }
885
886     /*
887      * Select a TrueType font for an element.
888      * This allows using a TrueType font, even with SetUseTTF(False).
889      *    $which_elem : The element whose font is to be changed.
890      *       One of: title legend generic x_label y_label x_title y_title
891      *    $which_font : A TrueType font filename or pathname.
892      *    $which_size : Font point size.
893      *    $which_spacing (optional) : Line spacing factor
894      */
895     function SetFontTTF($which_elem, $which_font, $which_size = 12, $which_spacing = NULL)
896     {
897         if (!$this->CheckOption($which_elem,
898                                 'generic, title, legend, x_label, y_label, x_title, y_title',
899                                 __FUNCTION__)) {
900             return FALSE;
901         }
902
903         // Empty font name means use the default font.
904         if (empty($which_font))
905             $which_font = $this->GetDefaultTTFont();
906         $path = $which_font;
907
908         // First try the font name directly, if not then try with path.
909         // Use GD imagettfbbox() to determine if this is a valid font.
910         // The return $bbox is used below, if valid.
911         if (($bbox = @imagettfbbox($which_size, 0, $path, "E")) === False) {
912             $path = $this->ttf_path . DIRECTORY_SEPARATOR . $which_font;
913             if (($bbox = @imagettfbbox($which_size, 0, $path, "E")) === False) {
914                 return $this->PrintError(__FUNCTION__ . ": Can't find TrueType font $which_font");
915             }
916         }
917
918         // Calculate the font height and inherent line spacing. TrueType fonts have this information
919         // internally, but PHP/GD has no way to directly access it. So get the bounding box size of
920         // an upper-case character without descenders, and the baseline-to-baseline height.
921         // Note: In practice, $which_size = $height, maybe +/-1 . But which_size is in points,
922         // and height is in pixels, and someday GD may be able to tell the difference.
923         // The character width is saved too, but not used by the normal text drawing routines - it
924         // isn't necessarily a fixed-space font. It is used in DrawLegend.
925         $height = $bbox[1] - $bbox[5];
926         $width = $bbox[2] - $bbox[0];
927         $bbox = ImageTTFBBox($which_size, 0, $path, "E\nE");
928         $spacing = $bbox[1] - $bbox[5] - 2 * $height;
929
930         // Store the font parameters:
931         $this->fonts[$which_elem] = array('ttf' => TRUE,
932                                           'font' => $path,
933                                           'size' => $which_size,
934                                           'height' => $height,
935                                           'width' => $width,
936                                           'spacing' => $spacing,
937                                           'line_spacing' => $which_spacing);
938         return TRUE;
939     }
940
941     /*
942      * Select Fixed/TrueType font for an element. Which type of font is
943      * selected depends on the $use_ttf class variable (see SetUseTTF()).
944      * Before PHPlot supported mixing font types, only this function and
945      * SetUseTTF were available to select an overall font type, but now
946      * SetFontGD() and SetFontTTF() can be used for mixing font types.
947      *    $which_elem : The element whose font is to be changed.
948      *       One of: title legend generic x_label y_label x_title y_title
949      *    $which_font : A number 1-5 for fixed fonts, or a TrueType font.
950      *    $which_size : Ignored for Fixed fonts, point size for TrueType.
951      *    $which_spacing (optional) : Line spacing factor
952      */
953     function SetFont($which_elem, $which_font, $which_size = 12, $line_spacing = NULL)
954     {
955         if ($this->use_ttf)
956             return $this->SetFontTTF($which_elem, $which_font, $which_size, $line_spacing);
957         return $this->SetFontGD($which_elem, $which_font, $line_spacing);
958     }
959
960     /*
961      * Return the inter-line spacing for a font.
962      * This is an internal function, used by ProcessText* and DrawLegend.
963      *   $font : A font array variable.
964      * Returns: Spacing, in pixels, between text lines.
965      */
966     protected function GetLineSpacing($font)
967     {
968         // Use the per-font line spacing preference, if set, else the global value:
969         if (isset($font['line_spacing']))
970             $line_spacing = $font['line_spacing'];
971         else
972             $line_spacing = $this->line_spacing;
973
974         // For GD fonts, that is the spacing in pixels.
975         // For TTF, adjust based on the 'natural' font spacing (see SetFontTTF):
976         if ($font['ttf']) {
977             $line_spacing = (int)($line_spacing * $font['spacing'] / 6.0);
978         }
979         return $line_spacing;
980     }
981
982     /*
983      * Text drawing and sizing functions:
984      * ProcessText is meant for use only by DrawText and SizeText.
985      *    ProcessText(True, ...)  - Draw a block of text
986      *    ProcessText(False, ...) - Just return ($width, $height) of
987      *       the orthogonal bounding box containing the text.
988      * ProcessText is further split into separate functions for GD and TTF
989      * text, due to the size of the code.
990      *
991      * Horizontal and vertical alignment are relative to the drawing. That is:
992      * vertical text (90 deg) gets centered along Y position with
993      * v_align = 'center', and adjusted to the right of X position with
994      * h_align = 'right'.  Another way to look at this is to say
995      * that text rotation happens first, then alignment.
996      *
997      * Original multiple lines code submitted by Remi Ricard.
998      * Original vertical code submitted by Marlin Viss.
999      *
1000      * Text routines rewritten by ljb to fix alignment and position problems.
1001      * Here is my explanation and notes. More information and pictures will be
1002      * placed in the PHPlot Reference Manual.
1003      *
1004      *    + Process TTF text one line at a time, not as a block. (See below)
1005      *    + Flipped top vs bottom vertical alignment. The usual interpretation
1006      *  is: bottom align means bottom of the text is at the specified Y
1007      *  coordinate. For some reason, PHPlot did left/right the correct way,
1008      *  but had top/bottom reversed. I fixed it, and left the default valign
1009      *  argument as bottom, but the meaning of the default value changed.
1010      *
1011      *    For GD font text, only single-line text is handled by GD, and the
1012      *  basepoint is the upper left corner of each text line.
1013      *    For TTF text, multi-line text could be handled by GD, with the text
1014      *  basepoint at the lower left corner of the first line of text.
1015      *  (Behavior of TTF drawing routines on multi-line text is not documented.)
1016      *  But you cannot do left/center/right alignment on each line that way,
1017      *  or proper line spacing.
1018      *    Therefore, for either text type, we have to break up the text into
1019      *  lines and position each line independently.
1020      *
1021      *    There are 9 alignment modes: Horizontal = left, center, or right, and
1022      *  Vertical = top, center, or bottom. Alignment is interpreted relative to
1023      *  the image, not as the text is read. This makes sense when you consider
1024      *  for example X axis labels. They need to be centered below the marks
1025      *  (center, top alignment) regardless of the text angle.
1026      *  'Bottom' alignment really means baseline alignment.
1027      *
1028      *    GD font text is supported (by libgd) at 0 degrees and 90 degrees only.
1029      *  Multi-line or single line text works with any of the 9 alignment modes.
1030      *
1031      *    TTF text can be at any angle. The 9 alignment modes work for all angles,
1032      *  but the results might not be what you expect for multi-line text. See
1033      *  the PHPlot Reference Manual for pictures and details. In short, alignment
1034      *  applies to the orthogonal (aligned with X and Y axes) bounding box that
1035      *  contains the text, and to each line in the multi-line text box. Since
1036      *  alignment is relative to the image, 45 degree multi-line text aligns
1037      *  differently from 46 degree text.
1038      *
1039      *    Note that PHPlot allows multi-line text for the 3 titles, and they
1040      *  are only drawn at 0 degrees (main and X titles) or 90 degrees (Y title).
1041      *  Data labels can also be multi-line, and they can be drawn at any angle.
1042      *  -ljb 2007-11-03
1043      *
1044      */
1045
1046     /*
1047      * ProcessTextGD() - Draw or size GD fixed-font text.
1048      * This is intended for use only by ProcessText().
1049      *    $draw_it : True to draw the text, False to just return the orthogonal width and height.
1050      *    $font : PHPlot font array (with 'ttf' = False) - see SetFontGD()
1051      *    $angle : Text angle in degrees. GD only supports 0 and 90. We treat >= 45 as 90, else 0.
1052      *    $x, $y : Reference point for the text (ignored if !$draw_it)
1053      *    $color : GD color index to use for drawing the text (ignored if !$draw_it)
1054      *    $text : The text to draw or size. Put a newline between lines.
1055      *    $h_factor : Horizontal alignment factor: 0(left), .5(center), or 1(right) (ignored if !$draw_it)
1056      *    $v_factor : Vertical alignment factor: 0(top), .5(center), or 1(bottom) (ignored if !$draw_it)
1057      * Returns: True, if drawing text, or an array of ($width, $height) if not.
1058      */
1059     protected function ProcessTextGD($draw_it, $font, $angle, $x, $y, $color, $text, $h_factor, $v_factor)
1060     {
1061         // Extract font parameters:
1062         $font_number = $font['font'];
1063         $font_width = $font['width'];
1064         $font_height = $font['height'];
1065         $line_spacing = $this->GetLineSpacing($font);
1066
1067         // Break up the text into lines, trim whitespace, find longest line.
1068         // Save the lines and length for drawing below.
1069         $longest = 0;
1070         foreach (explode("\n", $text) as $each_line) {
1071             $lines[] = $line = trim($each_line);
1072             $line_lens[] = $line_len = strlen($line);
1073             if ($line_len > $longest) $longest = $line_len;
1074         }
1075         $n_lines = count($lines);
1076
1077         // Width, height are based on font size and longest line, line count respectively.
1078         // These are relative to the text angle.
1079         $total_width = $longest * $font_width;
1080         $total_height = $n_lines * $font_height + ($n_lines - 1) * $line_spacing;
1081
1082         if (!$draw_it) {
1083             if ($angle < 45) return array($total_width, $total_height);
1084             return array($total_height, $total_width);
1085         }
1086
1087         $interline_step = $font_height + $line_spacing; // Line-to-line step
1088
1089         if ($angle >= 45) {
1090             // Vertical text (90 degrees):
1091             // (Remember the alignment convention with vertical text)
1092             // For 90 degree text, alignment factors change like this:
1093             $temp = $v_factor;
1094             $v_factor = $h_factor;
1095             $h_factor = 1 - $temp;
1096
1097             $draw_func = 'ImageStringUp';
1098
1099             // Rotation matrix "R" for 90 degrees (with Y pointing down):
1100             $r00 = 0;  $r01 = 1;
1101             $r10 = -1; $r11 = 0;
1102
1103         } else {
1104             // Horizontal text (0 degrees):
1105             $draw_func = 'ImageString';
1106
1107             // Rotation matrix "R" for 0 degrees:
1108             $r00 = 1; $r01 = 0;
1109             $r10 = 0; $r11 = 1;
1110         }
1111
1112         // Adjust for vertical alignment (horizontal text) or horizontal alignment (vertical text):
1113         $factor = (int)($total_height * $v_factor);
1114         $xpos = $x - $r01 * $factor;
1115         $ypos = $y - $r11 * $factor;
1116
1117         // Debug callback provides the bounding box:
1118         if ($this->GetCallback('debug_textbox')) {
1119             if ($angle >= 45) {
1120                 $bbox_width  = $total_height;
1121                 $bbox_height = $total_width;
1122                 $px = $xpos;
1123                 $py = $ypos - (1 - $h_factor) * $total_width;
1124             } else {
1125                 $bbox_width  = $total_width;
1126                 $bbox_height = $total_height;
1127                 $px = $xpos - $h_factor * $total_width;
1128                 $py = $ypos;
1129             }
1130             $this->DoCallback('debug_textbox', $px, $py, $bbox_width, $bbox_height);
1131         }
1132
1133         for ($i = 0; $i < $n_lines; $i++) {
1134
1135             // Adjust for alignment of this line within the text block:
1136             $factor = (int)($line_lens[$i] * $font_width * $h_factor);
1137             $x = $xpos - $r00 * $factor;
1138             $y = $ypos - $r10 * $factor;
1139
1140             // Call ImageString or ImageStringUp:
1141             $draw_func($this->img, $font_number, $x, $y, $lines[$i], $color);
1142
1143             // Step to the next line of text. This is a rotation of (x=0, y=interline_spacing)
1144             $xpos += $r01 * $interline_step;
1145             $ypos += $r11 * $interline_step;
1146         }
1147         return TRUE;
1148     }
1149
1150     /*
1151      * ProcessTextTTF() - Draw or size TTF text.
1152      * This is intended for use only by ProcessText().
1153      *    $draw_it : True to draw the text, False to just return the orthogonal width and height.
1154      *    $font : PHPlot font array (with 'ttf' = True) - see SetFontTTF()
1155      *    $angle : Text angle in degrees.
1156      *    $x, $y : Reference point for the text (ignored if !$draw_it)
1157      *    $color : GD color index to use for drawing the text (ignored if !$draw_it)
1158      *    $text : The text to draw or size. Put a newline between lines.
1159      *    $h_factor : Horizontal alignment factor: 0(left), .5(center), or 1(right) (ignored if !$draw_it)
1160      *    $v_factor : Vertical alignment factor: 0(top), .5(center), or 1(bottom) (ignored if !$draw_it)
1161      * Returns: True, if drawing text, or an array of ($width, $height) if not.
1162      */
1163     protected function ProcessTextTTF($draw_it, $font, $angle, $x, $y, $color, $text, $h_factor, $v_factor)
1164     {
1165         // Extract font parameters (see SetFontTTF):
1166         $font_file = $font['font'];
1167         $font_size = $font['size'];
1168         $font_height = $font['height'];
1169         $line_spacing = $this->GetLineSpacing($font);
1170
1171         // Break up the text into lines, trim whitespace.
1172         // Calculate the total width and height of the text box at 0 degrees.
1173         // Save the trimmed lines and their widths for later when drawing.
1174         // To get uniform spacing, don't use the actual line heights.
1175         // Total height = Font-specific line heights plus inter-line spacing.
1176         // Total width = width of widest line.
1177         // Last Line Descent is the offset from the bottom to the text baseline.
1178         // Note: For some reason, ImageTTFBBox uses (-1,-1) as the reference point.
1179         //   So 1+bbox[1] is the baseline to bottom distance.
1180         $total_width = 0;
1181         $lastline_descent = 0;
1182         foreach (explode("\n", $text) as $each_line) {
1183             $lines[] = $line = trim($each_line);
1184             $bbox = ImageTTFBBox($font_size, 0, $font_file, $line);
1185             $line_widths[] = $width = $bbox[2] - $bbox[0];
1186             if ($width > $total_width) $total_width = $width;
1187             $lastline_descent = 1 + $bbox[1];
1188         }
1189         $n_lines = count($lines);
1190         $total_height = $n_lines * $font_height + ($n_lines - 1) * $line_spacing;
1191
1192         // Calculate the rotation matrix for the text's angle. Remember that GD points Y down,
1193         // so the sin() terms change sign.
1194         $theta = deg2rad($angle);
1195         $cos_t = cos($theta);
1196         $sin_t = sin($theta);
1197         $r00 = $cos_t;    $r01 = $sin_t;
1198         $r10 = -$sin_t;   $r11 = $cos_t;
1199
1200         // Make a bounding box of the right size, with upper left corner at (0,0).
1201         // By convention, the point order is: LL, LR, UR, UL.
1202         // Note this is still working with the text at 0 degrees.
1203         // When sizing text (SizeText), use the overall size with descenders.
1204         //   This tells the caller how much room to leave for the text.
1205         // When drawing text (DrawText), use the size without descenders - that
1206         //   is, down to the baseline. This is for accurate positioning.
1207         $b[0] = 0;
1208         if ($draw_it) {
1209             $b[1] = $total_height;
1210         } else {
1211             $b[1] = $total_height + $lastline_descent;
1212         }
1213         $b[2] = $total_width;  $b[3] = $b[1];
1214         $b[4] = $total_width;  $b[5] = 0;
1215         $b[6] = 0;             $b[7] = 0;
1216
1217         // Rotate the bounding box, then offset to the reference point:
1218         for ($i = 0; $i < 8; $i += 2) {
1219             $x_b = $b[$i];
1220             $y_b = $b[$i+1];
1221             $c[$i]   = $x + $r00 * $x_b + $r01 * $y_b;
1222             $c[$i+1] = $y + $r10 * $x_b + $r11 * $y_b;
1223         }
1224
1225         // Get an orthogonal (aligned with X and Y axes) bounding box around it, by
1226         // finding the min and max X and Y:
1227         $bbox_ref_x = $bbox_max_x = $c[0];
1228         $bbox_ref_y = $bbox_max_y = $c[1];
1229         for ($i = 2; $i < 8; $i += 2) {
1230             $x_b = $c[$i];
1231             if ($x_b < $bbox_ref_x) $bbox_ref_x = $x_b;
1232             elseif ($bbox_max_x < $x_b) $bbox_max_x = $x_b;
1233             $y_b = $c[$i+1];
1234             if ($y_b < $bbox_ref_y) $bbox_ref_y = $y_b;
1235             elseif ($bbox_max_y < $y_b) $bbox_max_y = $y_b;
1236         }
1237         $bbox_width = $bbox_max_x - $bbox_ref_x;
1238         $bbox_height = $bbox_max_y - $bbox_ref_y;
1239
1240         if (!$draw_it) {
1241             // Return the bounding box, rounded up (so it always contains the text):
1242             return array((int)ceil($bbox_width), (int)ceil($bbox_height));
1243         }
1244
1245         $interline_step = $font_height + $line_spacing; // Line-to-line step
1246
1247         // Calculate the offsets from the supplied reference point to the
1248         // upper-left corner of the text.
1249         // Start at the reference point at the upper left corner of the bounding
1250         // box (bbox_ref_x, bbox_ref_y) then adjust it for the 9 point alignment.
1251         // h,v_factor are 0,0 for top,left, .5,.5 for center,center, 1,1 for bottom,right.
1252         //    $off_x = $bbox_ref_x + $bbox_width * $h_factor - $x;
1253         //    $off_y = $bbox_ref_y + $bbox_height * $v_factor - $y;
1254         // Then use that offset to calculate back to the supplied reference point x, y
1255         // to get the text base point.
1256         //    $qx = $x - $off_x;
1257         //    $qy = $y - $off_y;
1258         // Reduces to:
1259         $qx = 2 * $x - $bbox_ref_x - $bbox_width * $h_factor;
1260         $qy = 2 * $y - $bbox_ref_y - $bbox_height * $v_factor;
1261
1262         // Check for debug callback. Don't calculate bounding box unless it is wanted.
1263         if ($this->GetCallback('debug_textbox')) {
1264             // Calculate the orthogonal bounding box coordinates for debug testing.
1265
1266             // qx, qy is upper left corner relative to the text.
1267             // Calculate px,py: upper left corner (absolute) of the bounding box.
1268             // There are 4 equation sets for this, depending on the quadrant:
1269             if ($sin_t > 0) {
1270                 if ($cos_t > 0) {
1271                     // Quadrant: 0d - 90d:
1272                     $px = $qx; $py = $qy - $total_width * $sin_t;
1273                 } else {
1274                     // Quadrant: 90d - 180d:
1275                    $px = $qx + $total_width * $cos_t; $py = $qy - $bbox_height;
1276                 }
1277             } else {
1278                 if ($cos_t < 0) {
1279                     // Quadrant: 180d - 270d:
1280                     $px = $qx - $bbox_width; $py = $qy + $total_height * $cos_t;
1281                 } else {
1282                     // Quadrant: 270d - 360d:
1283                     $px = $qx + $total_height * $sin_t; $py = $qy;
1284                 }
1285             }
1286             $this->DoCallback('debug_textbox', $px, $py, $bbox_width, $bbox_height);
1287         }
1288
1289         // Since alignment is applied after rotation, which parameter is used
1290         // to control alignment of each line within the text box varies with
1291         // the angle.
1292         //   Angle (degrees):       Line alignment controlled by:
1293         //  -45 < angle <= 45          h_align
1294         //   45 < angle <= 135         reversed v_align
1295         //  135 < angle <= 225         reversed h_align
1296         //  225 < angle <= 315         v_align
1297         if ($cos_t >= $sin_t) {
1298             if ($cos_t >= -$sin_t) $line_align_factor = $h_factor;
1299             else $line_align_factor = $v_factor;
1300         } else {
1301             if ($cos_t >= -$sin_t) $line_align_factor = 1-$v_factor;
1302             else $line_align_factor = 1-$h_factor;
1303         }
1304
1305         // Now we have the start point, spacing and in-line alignment factor.
1306         // We are finally ready to start drawing the text, line by line.
1307         for ($i = 0; $i < $n_lines; $i++) {
1308
1309             // For drawing TTF text, the reference point is the left edge of the
1310             // text baseline (not the lower left corner of the bounding box).
1311             // The following also adjusts for horizontal (relative to
1312             // the text) alignment of the current line within the box.
1313             // What is happening is rotation of this vector by the text angle:
1314             //    (x = (total_width - line_width) * factor, y = font_height)
1315
1316             $width_factor = ($total_width - $line_widths[$i]) * $line_align_factor;
1317             $rx = $qx + $r00 * $width_factor + $r01 * $font_height;
1318             $ry = $qy + $r10 * $width_factor + $r11 * $font_height;
1319
1320             // Finally, draw the text:
1321             ImageTTFText($this->img, $font_size, $angle, $rx, $ry, $color, $font_file, $lines[$i]);
1322
1323             // Step to position of next line.
1324             // This is a rotation of (x=0,y=height+line_spacing) by $angle:
1325             $qx += $r01 * $interline_step;
1326             $qy += $r11 * $interline_step;
1327         }
1328         return TRUE;
1329     }
1330
1331     /*
1332      * ProcessText() - Wrapper for ProcessTextTTF() and ProcessTextGD(). See notes above.
1333      * This is intended for use from within PHPlot only, and only by DrawText() and SizeText().
1334      *    $draw_it : True to draw the text, False to just return the orthogonal width and height.
1335      *    $font : PHPlot font array, or NULL or empty string to use 'generic'
1336      *    $angle : Text angle in degrees
1337      *    $x, $y : Reference point for the text (ignored if !$draw_it)
1338      *    $color : GD color index to use for drawing the text (ignored if !$draw_it)
1339      *    $text : The text to draw or size. Put a newline between lines.
1340      *    $halign : Horizontal alignment: left, center, or right (ignored if !$draw_it)
1341      *    $valign : Vertical alignment: top, center, or bottom (ignored if !$draw_it)
1342      *      Note: Alignment is relative to the image, not the text.
1343      * Returns: True, if drawing text, or an array of ($width, $height) if not.
1344      */
1345     protected function ProcessText($draw_it, $font, $angle, $x, $y, $color, $text, $halign, $valign)
1346     {
1347         // Empty text case:
1348         if ($text === '') {
1349             if ($draw_it) return TRUE;
1350             return array(0, 0);
1351         }
1352
1353         // Calculate width and height offset factors using the alignment args:
1354         if ($valign == 'top') $v_factor = 0;
1355         elseif ($valign == 'center') $v_factor = 0.5;
1356         else $v_factor = 1.0; // 'bottom'
1357         if ($halign == 'left') $h_factor = 0;
1358         elseif ($halign == 'center') $h_factor = 0.5;
1359         else $h_factor = 1.0; // 'right'
1360
1361         // Apply a default font. This is mostly for external (callback) users.
1362         if (empty($font)) $font = $this->fonts['generic'];
1363
1364         if ($font['ttf']) {
1365             return $this->ProcessTextTTF($draw_it, $font, $angle, $x, $y, $color, $text,
1366                                          $h_factor, $v_factor);
1367         }
1368         return $this->ProcessTextGD($draw_it, $font, $angle, $x, $y, $color, $text, $h_factor, $v_factor);
1369     }
1370
1371     /*
1372      * Draws a block of text. See comments above before ProcessText().
1373      *    $which_font : PHPlot font array, or NULL or empty string to use 'generic'
1374      *    $which_angle : Text angle in degrees
1375      *    $which_xpos, $which_ypos: Reference point for the text
1376      *    $which_color : GD color index to use for drawing the text
1377      *    $which_text :  The text to draw, with newlines (\n) between lines.
1378      *    $which_halign : Horizontal (relative to the image) alignment: left, center, or right.
1379      *    $which_valign : Vertical (relative to the image) alignment: top, center, or bottom.
1380      * Note: This function should be considered 'protected', and is not documented for public use.
1381      */
1382     function DrawText($which_font, $which_angle, $which_xpos, $which_ypos, $which_color, $which_text,
1383                       $which_halign = 'left', $which_valign = 'bottom')
1384     {
1385         return $this->ProcessText(TRUE,
1386                            $which_font, $which_angle, $which_xpos, $which_ypos,
1387                            $which_color, $which_text, $which_halign, $which_valign);
1388     }
1389
1390     /*
1391      * Returns the size of block of text. This is the orthogonal width and height of a bounding
1392      * box aligned with the X and Y axes of the text. Only for angle=0 is this the actual
1393      * width and height of the text block, but for any angle it is the amount of space needed
1394      * to contain the text.
1395      *    $which_font : PHPlot font array, or NULL or empty string to use 'generic'
1396      *    $which_angle : Text angle in degrees
1397      *    $which_text :  The text to draw, with newlines (\n) between lines.
1398      * Returns a two element array with: $width, $height.
1399      * This is just a wrapper for ProcessText() - see above.
1400      * Note: This function should be considered 'protected', and is not documented for public use.
1401      */
1402     function SizeText($which_font, $which_angle, $which_text)
1403     {
1404         // Color, position, and alignment are not used when calculating the size.
1405         return $this->ProcessText(FALSE,
1406                            $which_font, $which_angle, 0, 0, 1, $which_text, '', '');
1407     }
1408
1409 /////////////////////////////////////////////
1410 ///////////            INPUT / OUTPUT CONTROL
1411 /////////////////////////////////////////////
1412
1413     /*
1414      * Sets output file format to $format (jpg, png, ...)
1415      */
1416     function SetFileFormat($format)
1417     {
1418         $asked = $this->CheckOption($format, 'jpg, png, gif, wbmp', __FUNCTION__);
1419         if (!$asked) return FALSE;
1420         switch ($asked) {
1421         case 'jpg':
1422             $format_test = IMG_JPG;
1423             break;
1424         case 'png':
1425             $format_test = IMG_PNG;
1426             break;
1427         case 'gif':
1428             $format_test = IMG_GIF;
1429             break;
1430         case 'wbmp':
1431             $format_test = IMG_WBMP;
1432             break;
1433         }
1434         if (!(imagetypes() & $format_test)) {
1435             return $this->PrintError("SetFileFormat(): File format '$format' not supported");
1436         }
1437         $this->file_format = $asked;
1438         return TRUE;
1439     }
1440
1441     /*
1442      * Selects an input file to be used as graph background and scales or tiles this image
1443      * to fit the sizes.
1444      *   $input_file : Path to the file to be used (jpeg, png and gif accepted)
1445      *   $mode : 'centeredtile', 'tile', or 'scale' (the image to the graph's size)
1446      */
1447     function SetBgImage($input_file, $mode='centeredtile')
1448     {
1449         $this->bgmode = $this->CheckOption($mode, 'tile, centeredtile, scale', __FUNCTION__);
1450         $this->bgimg  = $input_file;
1451         return (boolean)$this->bgmode;
1452     }
1453
1454     /*
1455      * Selects an input file to be used as plot area background and scales or tiles this image
1456      * to fit the sizes.
1457      *   $input_file : Path to the file to be used (jpeg, png and gif accepted)
1458      *   $mode : 'centeredtile', 'tile', or 'scale' (the image to the graph's size)
1459      */
1460     function SetPlotAreaBgImage($input_file, $mode='tile')
1461     {
1462         $this->plotbgmode = $this->CheckOption($mode, 'tile, centeredtile, scale', __FUNCTION__);
1463         $this->plotbgimg  = $input_file;
1464         return (boolean)$this->plotbgmode;
1465     }
1466
1467     /*
1468      * Sets the name of the file to be used as output file.
1469      */
1470     function SetOutputFile($which_output_file)
1471     {
1472         $this->output_file = $which_output_file;
1473         return TRUE;
1474     }
1475
1476     /*
1477      * Sets the output image as 'inline', that is: no Content-Type headers are sent
1478      * to the browser. Needed if you want to embed the images.
1479      */
1480     function SetIsInline($which_ii)
1481     {
1482         $this->is_inline = (bool)$which_ii;
1483         return TRUE;
1484     }
1485
1486     /*
1487      * Performs the actual outputting of the generated graph.
1488      */
1489     function PrintImage()
1490     {
1491         // Browser cache stuff submitted by Thiemo Nagel
1492         if ( (! $this->browser_cache) && (! $this->is_inline)) {
1493             header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
1494             header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
1495             header('Cache-Control: no-cache, must-revalidate');
1496             header('Pragma: no-cache');
1497         }
1498
1499         switch ($this->file_format) {
1500         case 'png':
1501             $mime_type = 'image/png';
1502             $output_f = 'imagepng';
1503             break;
1504         case 'jpg':
1505             $mime_type = 'image/jpeg';
1506             $output_f = 'imagejpeg';
1507             break;
1508         case 'gif':
1509             $mime_type = 'image/gif';
1510             $output_f = 'imagegif';
1511             break;
1512         case 'wbmp':
1513             $mime_type = 'image/wbmp';
1514             $output_f = 'imagewbmp';
1515             break;
1516         default:
1517             return $this->PrintError('PrintImage(): Please select an image type!');
1518         }
1519         if (!$this->is_inline) {
1520             Header("Content-type: $mime_type");
1521         }
1522         if ($this->is_inline && $this->output_file != '') {
1523             $output_f($this->img, $this->output_file);
1524         } else {
1525             $output_f($this->img);
1526         }
1527         return TRUE;
1528     }
1529
1530     /*
1531      *  Error handling for 'fatal' errors:
1532      *   $error_message       Text of the error message
1533      *  Standard output from PHPlot is expected to be an image file, such as
1534      *  when handling an <img> tag browser request. So it is not permitted to
1535      *  output text to standard output. (You should have display_errors=off)
1536      *  Here is how PHPlot handles fatal errors:
1537      *    + Write the error message into an image, and output the image.
1538      *    + If no image can be output, write nothing and produce an HTTP
1539      *      error header.
1540      *    + Trigger a user-level error containing the error message.
1541      *      If no error handler was set up, the script will log the
1542      *      error and exit with non-zero status.
1543      *
1544      *  PrintError() and DrawError() are now equivalent. Both are provided for
1545      *  compatibility. (In earlier releases, PrintError sent the message to
1546      *  stdout only, and DrawError sent it in an image only.)
1547      *
1548      *  This function does not return, unless the calling script has set up
1549      *  an error handler which does not exit. In that case, PrintError will
1550      *  return False. But not all of PHPlot will handle this correctly, so
1551      *  it is probably a bad idea for an error handler to return.
1552      */
1553     protected function PrintError($error_message)
1554     {
1555         // Be sure not to loop recursively, e.g. PrintError - PrintImage - PrintError.
1556         if (isset($this->in_error)) return FALSE;
1557         $this->in_error = TRUE;
1558
1559         // Output an image containing the error message:
1560         if (!empty($this->img)) {
1561             $ypos = $this->image_height/2;
1562             $xpos = $this->image_width/2;
1563             $bgcolor = ImageColorResolve($this->img, 255, 255, 255);
1564             $fgcolor = ImageColorResolve($this->img, 0, 0, 0);
1565             ImageFilledRectangle($this->img, 0, 0, $this->image_width, $this->image_height, $bgcolor);
1566
1567             // Switch to built-in fonts, in case of error with TrueType fonts:
1568             $this->SetUseTTF(FALSE);
1569
1570             $this->DrawText($this->fonts['generic'], 0, $xpos, $ypos, $fgcolor,
1571                             wordwrap($error_message), 'center', 'center');
1572
1573             $this->PrintImage();
1574         } elseif (! $this->is_inline) {
1575             Header('HTTP/1.0 500 Internal Server Error');
1576         }
1577         trigger_error($error_message, E_USER_ERROR);
1578         unset($this->in_error);
1579         return FALSE;  // In case error handler returns, rather than doing exit().
1580     }
1581
1582     /*
1583      * Display an error message and exit.
1584      * This is provided for backward compatibility only. Use PrintError() instead.
1585      *   $error_message       Text of the error message
1586      *   $where_x, $where_y   Ignored, provided for compatibility.
1587      */
1588     protected function DrawError($error_message, $where_x = NULL, $where_y = NULL)
1589     {
1590         return $this->PrintError($error_message);
1591     }
1592
1593 /////////////////////////////////////////////
1594 ///////////                            LABELS
1595 /////////////////////////////////////////////
1596
1597     /*
1598      * Sets position for X data labels. For most plot types, these are
1599      * labels along the X axis (but different from X tick labels).
1600      *    Accepted positions are: plotdown, plotup, both, none.
1601      * For horizontal bar charts, these are the labels right (or left) of the bars.
1602      *    Accepted positions are: plotin, plotstack, none.
1603      */
1604     function SetXDataLabelPos($which_xdlp)
1605     {
1606         $which_xdlp = $this->CheckOption($which_xdlp, 'plotdown, plotup, both, none, plotin, plotstack',
1607                                          __FUNCTION__);
1608         if (!$which_xdlp) return FALSE;
1609         $this->x_data_label_pos = $which_xdlp;
1610
1611         return TRUE;
1612     }
1613
1614     /*
1615      * Sets position for Y data labels.
1616      * For bars and stackedbars, these are labels above the bars with the Y values.
1617      *    Accepted positions are: plotin, plotstack, none.
1618      * For horizontal bar charts, these are the labels along the Y axis.
1619      *    Accepted positions are: plotleft, plotright, both, none.
1620      */
1621     function SetYDataLabelPos($which_ydlp)
1622     {
1623         $which_ydlp = $this->CheckOption($which_ydlp, 'plotleft, plotright, both, none, plotin, plotstack',
1624                                           __FUNCTION__);
1625         if (!$which_ydlp) return FALSE;
1626         $this->y_data_label_pos = $which_ydlp;
1627
1628         return TRUE;
1629     }
1630
1631     /*
1632      * Set position for X tick labels.
1633      */
1634     function SetXTickLabelPos($which_xtlp)
1635     {
1636         $which_xtlp = $this->CheckOption($which_xtlp, 'plotdown, plotup, both, xaxis, none',
1637                                          __FUNCTION__);
1638         if (!$which_xtlp) return FALSE;
1639         $this->x_tick_label_pos = $which_xtlp;
1640
1641         return TRUE;
1642     }
1643
1644     /*
1645      * Set position for Y tick labels.
1646      */
1647     function SetYTickLabelPos($which_ytlp)
1648     {
1649         $which_ytlp = $this->CheckOption($which_ytlp, 'plotleft, plotright, both, yaxis, none',
1650                                          __FUNCTION__);
1651         if (!$which_ytlp) return FALSE;
1652         $this->y_tick_label_pos = $which_ytlp;
1653
1654         return TRUE;
1655     }
1656
1657     /*
1658      * Set formatting type for tick and data labels on X or Y axis.
1659      * This implements the 4 functions Set[XY]LabelType() and Set[XY]DataLabelType().
1660      *    $mode  : 'x', 'y', 'xd', or 'yd' - which type of label to configure.
1661      *        'x' and 'y' set the type for tick labels, and the default type for data labels
1662      *        if they are not separately configured. 'xd' and 'yd' set the type for data labels.
1663      *    $args  : Variable arguments, passed as an array.
1664      *       [0] = $type (required) : Label type. 'data', 'time', 'printf', or 'custom'.
1665      *     For type 'data':
1666      *       [1] = $precision (optional). Numeric precision. Can also be set by SetPrecision[XY]().
1667      *       [2] = $prefix (optional) - prefix string for labels.
1668      *       [3] = $suffix (optional) - suffix string for labels. This replaces data_units_text.
1669      *     For type 'time':
1670      *       [1] = $format for strftime (optional). Can also be set by Set[XY]TimeFormat().
1671      *     For type 'printf':
1672      *       [1] = $format (optional) for sprintf.
1673      *     For type 'custom':
1674      *       [1] = $callback (required) - Custom function or array of (instance,method) to call.
1675      *       [2] = $argument (optional) - Pass-through argument for the formatting function.
1676      */
1677     protected function SetLabelType($mode, $args)
1678     {
1679         if (!$this->CheckOption($mode, 'x, y, xd, yd', __FUNCTION__))
1680             return FALSE;
1681
1682         $type = isset($args[0]) ? $args[0] : '';
1683         $format =& $this->label_format[$mode];  // Shorthand reference to format storage variables
1684         switch ($type) {
1685         case 'data':
1686             if (isset($args[1]))
1687                 $format['precision'] = $args[1];
1688             elseif (!isset($format['precision']))
1689                 $format['precision'] = 1;
1690             $format['prefix'] = isset($args[2]) ? $args[2] : '';
1691             $format['suffix'] = isset($args[3]) ? $args[3] : '';
1692             break;
1693
1694         case 'time':
1695             if (isset($args[1]))
1696                 $format['time_format'] = $args[1];
1697             elseif (!isset($format['time_format']))
1698                 $format['time_format'] = '%H:%M:%S';
1699             break;
1700
1701         case 'printf':
1702             if (isset($args[1]))
1703                 $format['printf_format'] = $args[1];
1704             elseif (!isset($format['printf_format']))
1705                 $format['printf_format'] = '%e';
1706             break;
1707
1708         case 'custom':
1709             if (isset($args[1])) {
1710                 $format['custom_callback'] = $args[1];
1711                 $format['custom_arg'] = isset($args[2]) ? $args[2] : NULL;
1712             } else {
1713                 $type = ''; // Error, 'custom' without a function, set to no-format mode.
1714             }
1715             break;
1716
1717         case '':
1718         case 'title':   // Retained for backwards compatibility?
1719             break;
1720
1721         default:
1722             $this->CheckOption($type, 'data, time, printf, custom', __FUNCTION__);
1723             $type = '';
1724         }
1725         $format['type'] = $type;
1726         return (boolean)$type;
1727     }
1728
1729     /*
1730      * Select label formating for X tick labels, and for X data labels
1731      * (unless SetXDataLabelType was called).
1732      * See SetLabelType() for details.
1733      */
1734     function SetXLabelType()  // Variable arguments: $type, ...
1735     {
1736         $args = func_get_args();
1737         return $this->SetLabelType('x', $args);
1738     }
1739
1740     /*
1741      * Select label formatting for X data labels, overriding SetXLabelType.
1742      */
1743     function SetXDataLabelType()  // Variable arguments: $type, ...
1744     {
1745         $args = func_get_args();
1746         return $this->SetLabelType('xd', $args);
1747     }
1748
1749     /*
1750      * Select label formating for Y tick labels, and for Y data labels
1751      * (unless SetYDataLabelType was called).
1752      * See SetLabelType() for details.
1753      */
1754     function SetYLabelType()  // Variable arguments: $type, ...
1755     {
1756         $args = func_get_args();
1757         return $this->SetLabelType('y', $args);
1758     }
1759
1760     /*
1761      * Select label formatting for Y data labels, overriding SetYLabelType.
1762      */
1763     function SetYDataLabelType()  // Variable arguments: $type, ...
1764     {
1765         $args = func_get_args();
1766         return $this->SetLabelType('yd', $args);
1767     }
1768
1769     /*
1770      * Set the date/time format code for X labels.
1771      * Note: Use of SetXLabelType('time', $which_xtf) is preferred, because
1772      * SetXTimeFormat does not also enable date/time formatting.
1773      */
1774     function SetXTimeFormat($which_xtf)
1775     {
1776         $this->label_format['x']['time_format'] = $which_xtf;
1777         return TRUE;
1778     }
1779
1780     /*
1781      * Set the date/time format code for Y labels.
1782      * Note: Use of SetYLabelType('time', $which_ytf) is preferred, because
1783      * SetYTimeFormat does not also enable date/time formatting.
1784      */
1785     function SetYTimeFormat($which_ytf)
1786     {
1787         $this->label_format['y']['time_format'] = $which_ytf;
1788         return TRUE;
1789     }
1790
1791     /*
1792      * Set number format parameters (decimal point and thousands separator) for
1793      * 'data' mode label formatting, overriding the locale-defaults.
1794      */
1795     function SetNumberFormat($decimal_point, $thousands_sep)
1796     {
1797         $this->decimal_point = $decimal_point;
1798         $this->thousands_sep = $thousands_sep;
1799         return TRUE;
1800     }
1801
1802     /*
1803      * Set the text angle for X labels to $which_xla degrees.
1804      */
1805     function SetXLabelAngle($which_xla)
1806     {
1807         $this->x_label_angle = $which_xla;
1808         return TRUE;
1809     }
1810
1811     /*
1812      * Set the text angle for Y labels to $which_xla degrees.
1813      */
1814     function SetYLabelAngle($which_yla)
1815     {
1816         $this->y_label_angle = $which_yla;
1817         return TRUE;
1818     }
1819
1820     /*
1821      * Set the angle for X Data Labels to $which_xdla degrees.
1822      * If not used, this defaults to the value set with SetXLabelAngle.
1823      */
1824     function SetXDataLabelAngle($which_xdla)
1825     {
1826         $this->x_data_label_angle = $which_xdla;
1827         return TRUE;
1828     }
1829
1830     /*
1831      * Set the angle for Y Data Labels to $which_ydla degrees.
1832      * If not used, this defaults to zero (unlike X data labels).
1833      */
1834     function SetYDataLabelAngle($which_ydla)
1835     {
1836         $this->y_data_label_angle = $which_ydla;
1837         return TRUE;
1838     }
1839
1840 /////////////////////////////////////////////
1841 ///////////                              MISC
1842 /////////////////////////////////////////////
1843
1844     /*
1845      * Checks the validity of an option.
1846      *   $which_opt  String to check, such as the provided value of a function argument.
1847      *   $which_acc  String of accepted choices. Must be lower-case, and separated
1848      *               by exactly ', ' (comma, space).
1849      *   $which_func Name of the calling function, for error messages.
1850      * Returns the supplied option value, downcased and trimmed, if it is valid.
1851      * Reports an error if the supplied option is not valid.
1852      */
1853     protected function CheckOption($which_opt, $which_acc, $which_func)
1854     {
1855         $asked = strtolower(trim($which_opt));
1856
1857         // Look for the supplied value in a comma/space separated list.
1858         if (strpos(", $which_acc,", ", $asked,") !== FALSE)
1859             return $asked;
1860
1861         $this->PrintError("$which_func(): '$which_opt' not in available choices: '$which_acc'.");
1862         return NULL;
1863     }
1864
1865     /*
1866      * Checks the validity of an array of options.
1867      *   $opt  Array or string to check.
1868      *   $acc  String of accepted choices. Must be lower-case, and separated
1869      *               by exactly ', ' (comma, space).
1870      *   $func Name of the calling function, for error messages.
1871      * Returns a array option value(s), downcased and trimmed, if all entries in $opt are valid.
1872      * Reports an error if any supplied option is not valid. Returns NULL if the error handler returns.
1873      */
1874     protected function CheckOptionArray($opt, $acc, $func)
1875     {
1876         $opt_array = (array)$opt;
1877         $result = array();
1878         foreach ($opt_array as $option) {
1879             $choice = $this->CheckOption($option, $acc, $func);
1880             if (is_null($choice)) return NULL; // In case CheckOption error handler returns
1881             $result[] = $choice;
1882         }
1883         return $result;
1884     }
1885
1886     /*
1887      * Check compatibility of a plot type and data type.
1888      * This is called by the plot-type-specific drawing functions.
1889      *   $valid_types  String of supported data types. Multiple values must be
1890      *      separated by exactly ', ' (comma, space).
1891      * Returns True if the type is valid for this plot.
1892      * Reports an error if the data type is not value. If the error is handled and
1893      *   the handler returns, this returns False.
1894      */
1895     protected function CheckDataType($valid_types)
1896     {
1897         if (strpos(", $valid_types,", ", $this->data_type,") !== FALSE)
1898             return TRUE;
1899
1900         $this->PrintError("Data type '$this->data_type' is not valid for '$this->plot_type' plots."
1901                . " Supported data type(s): '$valid_types'");
1902         return FALSE;
1903     }
1904
1905     /*
1906      * Decode the data type into variables used to determine how to process a data array.
1907      * The goal is minimize which functions understand the actual data type values.
1908      * This sets the datatype_* variables for use by other member functions.
1909      *   datatype_implied : Implicit independent variable (e.g. text-data vs data-data)
1910      *   datatype_swapped_xy : Swapped X/Y (horizontal plot)
1911      *   datatype_error_bars : Data array has error bar data
1912      *   datatype_pie_single : Data array is for a pie chart with one row per slice
1913      */
1914     protected function DecodeDataType()
1915     {
1916         $dt = $this->data_type;
1917
1918         $this->datatype_implied = ($dt == 'text-data' || $dt == 'text-data-single'
1919                                 || $dt == 'text-data-yx');
1920         $this->datatype_swapped_xy = ($dt == 'text-data-yx' || $dt == 'data-data-yx');
1921         $this->datatype_error_bars = ($dt == 'data-data-error');
1922         $this->datatype_pie_single = ($dt == 'text-data-single');
1923     }
1924
1925     /*
1926      * Make sure the data array is populated, and calculate the number of columns.
1927      * This is called from DrawGraph. Calculates data_columns, which is the
1928      * maximum number of dependent variable values (usually Y) in the data array rows.
1929      * (For pie charts, this is the number of slices.)
1930      * This depends on the data_type, unlike records_per_group (which was
1931      * previously used to pad style arrays, but is not accurate).
1932      * Returns True if the data array is OK, else reports an error (and may return False).
1933      * Note error messages refer to the caller, the public DrawGraph().
1934      */
1935     protected function CheckDataArray()
1936     {
1937         // Test for missing image, which really should never happen.
1938         if (!$this->img) {
1939             return $this->PrintError('DrawGraph(): No image resource allocated');
1940         }
1941
1942         // Test for missing or empty data array:
1943         if (empty($this->data) || !is_array($this->data)) {
1944             return $this->PrintError("DrawGraph(): No data array");
1945         }
1946         if ($this->total_records == 0) {
1947             return $this->PrintError('DrawGraph(): Empty data set');
1948         }
1949
1950         // Decode the data type into functional flags.
1951         $this->DecodeDataType();
1952
1953         // Calculate the maximum number of dependent values per independent value
1954         // (e.g. Y for each X), or the number of pie slices.
1955         if ($this->datatype_pie_single) {
1956             $this->data_columns = $this->num_data_rows; // Special case for 1 type of pie chart.
1957         } else {
1958             $skip = $this->datatype_implied ? 1 : 2; // Skip data label and independent variable if used
1959             $this->data_columns = $this->records_per_group - $skip;
1960             if ($this->datatype_error_bars) // Each Y has +err and -err along with it
1961                 $this->data_columns = (int)($this->data_columns / 3);
1962         }
1963         return TRUE;
1964     }
1965
1966     /*
1967      * Control headers for browser-side image caching.
1968      *   $which_browser_cache : True to allow browsers to cache the image.
1969      */
1970     function SetBrowserCache($which_browser_cache)
1971     {
1972         $this->browser_cache = $which_browser_cache;
1973         return TRUE;
1974     }
1975
1976     /*
1977      * Set whether DrawGraph automatically outputs the image too.
1978      *   $which_pi : True to have DrawGraph call PrintImage at the end.
1979      */
1980     function SetPrintImage($which_pi)
1981     {
1982         $this->print_image = $which_pi;
1983         return TRUE;
1984     }
1985
1986     /*
1987      * Set text to display in the graph's legend.
1988      *   $which_leg : Array of strings for the complete legend, or a single string
1989      *                to be appended to the legend.
1990      */
1991     function SetLegend($which_leg)
1992     {
1993         if (is_array($which_leg)) {             // use array
1994             $this->legend = $which_leg;
1995         } elseif (! is_null($which_leg)) {     // append string
1996             $this->legend[] = $which_leg;
1997         } else {
1998             return $this->PrintError("SetLegend(): argument must not be null.");
1999         }
2000         return TRUE;
2001     }
2002
2003     /*
2004      * Specifies the position of the legend's upper/leftmost corner,
2005      * in pixel (device) coordinates.
2006      */
2007     function SetLegendPixels($which_x, $which_y)
2008     {
2009         $this->legend_x_pos = $which_x;
2010         $this->legend_y_pos = $which_y;
2011         // Make sure this is unset, meaning we have pixel coords:
2012         unset($this->legend_xy_world);
2013
2014         return TRUE;
2015     }
2016
2017     /*
2018      * Specifies the position of the legend's upper/leftmost corner,
2019      * in world (data space) coordinates.
2020      */
2021     function SetLegendWorld($which_x, $which_y)
2022     {
2023         // Since conversion from world to pixel coordinates is not yet available, just
2024         // remember the coordinates and set a flag to indicate conversion is needed.
2025         $this->legend_x_pos = $which_x;
2026         $this->legend_y_pos = $which_y;
2027         $this->legend_xy_world = TRUE;
2028
2029         return TRUE;
2030     }
2031
2032     /*
2033      * Set legend text alignment, color box alignment, and style options.
2034      *   $text_align : Alignment of the text, 'left' or 'right'.
2035      *   $colorbox_align : Alignment of the color boxes, 'left', 'right', 'none', or missing/empty.
2036      *       If missing or empty, the same alignment as $text_align is used. Color box is positioned first.
2037      *   $style : reserved for future use.
2038      */
2039     function SetLegendStyle($text_align, $colorbox_align = '', $style = '')
2040     {
2041         $this->legend_text_align = $this->CheckOption($text_align, 'left, right', __FUNCTION__);
2042         if (empty($colorbox_align))
2043             $this->legend_colorbox_align = $this->legend_text_align;
2044         else
2045             $this->legend_colorbox_align = $this->CheckOption($colorbox_align, 'left, right, none',
2046                                                               __FUNCTION__);
2047         return ((boolean)$this->legend_text_align && (boolean)$this->legend_colorbox_align);
2048     }
2049
2050     /*
2051      * Set border for the plot area.
2052      * Accepted values are: left, right, top, bottom, sides, none, full or an array of those.
2053      */
2054     function SetPlotBorderType($pbt)
2055     {
2056         $this->plot_border_type = $this->CheckOptionArray($pbt, 'left, right, top, bottom, sides, none, full',
2057                                                           __FUNCTION__);
2058         return !empty($this->plot_border_type);
2059     }
2060
2061     /*
2062      * Set border style for the image.
2063      * Accepted values are: raised, plain, solid, none
2064      *  'solid' is the same as 'plain' except it fixes the color (see DrawImageBorder)
2065      */
2066     function SetImageBorderType($sibt)
2067     {
2068         $this->image_border_type = $this->CheckOption($sibt, 'raised, plain, solid, none', __FUNCTION__);
2069         return (boolean)$this->image_border_type;
2070     }
2071
2072     /*
2073      * Set border width for the image to $width in pixels.
2074      */
2075     function SetImageBorderWidth($width)
2076     {
2077         $this->image_border_width = $width;
2078         return TRUE;
2079     }
2080
2081     /*
2082      * Enable or disable drawing of the plot area background color.
2083      */
2084     function SetDrawPlotAreaBackground($dpab)
2085     {
2086         $this->draw_plot_area_background = (bool)$dpab;
2087         return TRUE;
2088     }
2089
2090     /*
2091      * Enable or disable drawing of the X grid lines.
2092      */
2093     function SetDrawXGrid($dxg)
2094     {
2095         $this->draw_x_grid = (bool)$dxg;
2096         return TRUE;
2097     }
2098
2099     /*
2100      * Enable or disable drawing of the Y grid lines.
2101      */
2102     function SetDrawYGrid($dyg)
2103     {
2104         $this->draw_y_grid = (bool)$dyg;
2105         return TRUE;
2106     }
2107
2108     /*
2109      * Select dashed or solid grid lines.
2110      *   $ddg : True for dashed grid lines, false for solid grid lines.
2111      */
2112     function SetDrawDashedGrid($ddg)
2113     {
2114         $this->dashed_grid = (bool)$ddg;
2115         return TRUE;
2116     }
2117
2118     /*
2119      * Enable or disable drawing of X Data Label Lines.
2120      */
2121     function SetDrawXDataLabelLines($dxdl)
2122     {
2123         $this->draw_x_data_label_lines = (bool)$dxdl;
2124         return TRUE;
2125     }
2126
2127     /*
2128      * Set the main title text for the plot.
2129      */
2130     function SetTitle($which_title)
2131     {
2132         $this->title_txt = $which_title;
2133         return TRUE;
2134     }
2135
2136     /*
2137      * Set the X axis title and position.
2138      */
2139     function SetXTitle($which_xtitle, $which_xpos = 'plotdown')
2140     {
2141         if ($which_xtitle == '')
2142             $which_xpos = 'none';
2143
2144         $this->x_title_pos = $this->CheckOption($which_xpos, 'plotdown, plotup, both, none', __FUNCTION__);
2145         if (!$this->x_title_pos) return FALSE;
2146         $this->x_title_txt = $which_xtitle;
2147         return TRUE;
2148     }
2149
2150     /*
2151      * Set the Y axis title and position.
2152      */
2153     function SetYTitle($which_ytitle, $which_ypos = 'plotleft')
2154     {
2155         if ($which_ytitle == '')
2156             $which_ypos = 'none';
2157
2158         $this->y_title_pos = $this->CheckOption($which_ypos, 'plotleft, plotright, both, none', __FUNCTION__);
2159         if (!$this->y_title_pos) return FALSE;
2160         $this->y_title_txt = $which_ytitle;
2161         return TRUE;
2162     }
2163
2164     /*
2165      * Set the size of the drop shadow for bar and pie charts.
2166      *   $which_s : Size of the drop shadow in pixels.
2167      */
2168     function SetShading($which_s)
2169     {
2170         $this->shading = (int)$which_s;
2171         return TRUE;
2172     }
2173
2174     /*
2175      * Set the plot type (bars, points, ...)
2176      */
2177     function SetPlotType($which_pt)
2178     {
2179         $this->plot_type = $this->CheckOption($which_pt, 'bars, stackedbars, lines, linepoints,'
2180                             . ' area, points, pie, thinbarline, squared, stackedarea',
2181                             __FUNCTION__);
2182         return (boolean)$this->plot_type;
2183     }
2184
2185     /*
2186      * Set the position of the X axis.
2187      *  $pos : Axis position in world coordinates (as an integer).
2188      */
2189     function SetXAxisPosition($pos)
2190     {
2191         $this->x_axis_position = (int)$pos;
2192         return TRUE;
2193     }
2194
2195     /*
2196      * Set the position of the Y axis.
2197      *  $pos : Axis position in world coordinates (as an integer).
2198      */
2199     function SetYAxisPosition($pos)
2200     {
2201         $this->y_axis_position = (int)$pos;
2202         return TRUE;
2203     }
2204
2205     /*
2206      * Select linear or log scale for the X axis.
2207      */
2208     function SetXScaleType($which_xst)
2209     {
2210         $this->xscale_type = $this->CheckOption($which_xst, 'linear, log', __FUNCTION__);
2211         return (boolean)$this->xscale_type;
2212     }
2213
2214     /*
2215      * Select linear or log scale for the Y axis.
2216      */
2217     function SetYScaleType($which_yst)
2218     {
2219         $this->yscale_type = $this->CheckOption($which_yst, 'linear, log',  __FUNCTION__);
2220         return (boolean)$this->yscale_type;
2221     }
2222
2223     /*
2224      * Set the precision for numerically formatted X labels.
2225      *   $which_prec : Number of digits to display.
2226      * Note: This is equivalent to: SetXLabelType('data', $which_prec)
2227      */
2228     function SetPrecisionX($which_prec)
2229     {
2230         return $this->SetXLabelType('data', $which_prec);
2231     }
2232
2233     /*
2234      * Set the precision for numerically formatted Y labels.
2235      *   $which_prec : Number of digits to display.
2236      * Note: This is equivalent to: SetYLabelType('data', $which_prec)
2237      */
2238     function SetPrecisionY($which_prec)
2239     {
2240         return $this->SetYLabelType('data', $which_prec);
2241     }
2242
2243     /*
2244      * Set the line width (in pixels) for error bars.
2245      */
2246     function SetErrorBarLineWidth($which_seblw)
2247     {
2248         $this->error_bar_line_width = $which_seblw;
2249         return TRUE;
2250     }
2251
2252     /*
2253      * Set the position for pie chart percentage labels.
2254      *   $which_blb : Real number between 0 and 1.
2255      *      Smaller values move the labels in towards the center.
2256      */
2257     function SetLabelScalePosition($which_blp)
2258     {
2259         $this->label_scale_position = $which_blp;
2260         return TRUE;
2261     }
2262
2263     /*
2264      * Set the size (in pixels) of the "T" in error bars.
2265      */
2266     function SetErrorBarSize($which_ebs)
2267     {
2268         $this->error_bar_size = $which_ebs;
2269         return TRUE;
2270     }
2271
2272     /*
2273      * Set the shape of the in error bars.
2274      *   $which_ebs : Error bar shape, 'tee' or 'line'.
2275      */
2276     function SetErrorBarShape($which_ebs)
2277     {
2278         $this->error_bar_shape = $this->CheckOption($which_ebs, 'tee, line', __FUNCTION__);
2279         return (boolean)$this->error_bar_shape;
2280     }
2281
2282     /*
2283      * Synchronize the point shape and point size arrays.
2284      * This is called just before drawing any plot that needs 'points'.
2285      */
2286     protected function CheckPointParams()
2287     {
2288         // Make both point_shapes and point_sizes the same size, by padding the smaller.
2289         $ps = count($this->point_sizes);
2290         $pt = count($this->point_shapes);
2291
2292         if ($ps < $pt) {
2293             $this->pad_array($this->point_sizes, $pt);
2294             $this->point_counts = $pt;
2295         } elseif ($ps > $pt) {
2296             $this->pad_array($this->point_shapes, $ps);
2297             $this->point_counts = $ps;
2298         } else {
2299             $this->point_counts = $ps;
2300         }
2301
2302         // Note: PHPlot used to check and adjust point_sizes to be an even number here,
2303         // for all 'diamond' and 'triangle' shapes. The reason for this having been
2304         // lost, and the current maintainer seeing no sense it doing this for only
2305         // some shapes, the code has been removed. But see what DrawDot() does.
2306     }
2307
2308     /*
2309      * Set the point shape for each data set.
2310      *   $which_pt : Array (or single value) of valid point shapes.
2311      * The point shape and point sizes arrays are synchronized before drawing a graph
2312      * that uses points. See CheckPointParams()
2313      */
2314     function SetPointShapes($which_pt)
2315     {
2316         if (is_array($which_pt)) {
2317             // Use provided array:
2318             $this->point_shapes = $which_pt;
2319         } elseif (!is_null($which_pt)) {
2320             // Make the single value into an array:
2321             $this->point_shapes = array($which_pt);
2322         }
2323
2324         // Validate all the shapes. This list must agree with DrawDot().
2325         foreach ($this->point_shapes as $shape)
2326         {
2327             if (!$this->CheckOption($shape, 'halfline, line, plus, cross, rect, circle, dot,'
2328                        . ' diamond, triangle, trianglemid, delta, yield, star, hourglass,'
2329                        . ' bowtie, target, box, home, up, down, none', __FUNCTION__))
2330                 return FALSE;
2331         }
2332         return TRUE;
2333     }
2334
2335     /*
2336      * Set the point size for point plots.
2337      *   $which_ps : Array (or single value) of point sizes in pixels.
2338      * The point shape and point sizes arrays are synchronized before drawing a graph
2339      * that uses points. See CheckPointParams()
2340      */
2341     function SetPointSizes($which_ps)
2342     {
2343         if (is_array($which_ps)) {
2344             // Use provided array:
2345             $this->point_sizes = $which_ps;
2346         } elseif (!is_null($which_ps)) {
2347             // Make the single value into an array:
2348             $this->point_sizes = array($which_ps);
2349         }
2350         return TRUE;
2351     }
2352
2353     /*
2354      * Sets whether lines should be broken at missing data.
2355      *   $bl : True to break the lines, false to connect around missing data.
2356      * This only works with 'lines' and 'squared' plots.
2357      */
2358     function SetDrawBrokenLines($bl)
2359     {
2360         $this->draw_broken_lines = (bool)$bl;
2361         return TRUE;
2362     }
2363
2364     /*
2365      * Set the data type, which defines the structure of the data array
2366      *  text-data: ('label', y1, y2, y3, ...)
2367      *  text-data-single: ('label', data), for some pie charts.
2368      *  data-data: ('label', x, y1, y2, y3, ...)
2369      *  data-data-error: ('label', x1, y1, e1+, e2-, y2, e2+, e2-, y3, e3+, e3-, ...)
2370      *  data-data-yx: ('label', y, x1, x2, x3, ..)
2371      *  text-data-yx: ('label', x1, x2, x3, ...)
2372      */
2373     function SetDataType($which_dt)
2374     {
2375         //The next four lines are for past compatibility.
2376         if ($which_dt == 'text-linear') $which_dt = 'text-data';
2377         elseif ($which_dt == 'linear-linear') $which_dt = 'data-data';
2378         elseif ($which_dt == 'linear-linear-error') $which_dt = 'data-data-error';
2379         elseif ($which_dt == 'text-data-pie') $which_dt = 'text-data-single';
2380
2381         $this->data_type = $this->CheckOption($which_dt, 'text-data, text-data-single, '.
2382                                                          'data-data, data-data-error, '.
2383                                                          'data-data-yx, text-data-yx',
2384                                                          __FUNCTION__);
2385         return (boolean)$this->data_type;
2386     }
2387
2388     /*
2389      * Copy the array passed as data values. We convert to numerical indexes, for its
2390      * use for (or while) loops, which sometimes are faster. Performance improvements
2391      * vary from 28% in DrawLines() to 49% in DrawArea() for plot drawing functions.
2392      */
2393     function SetDataValues($which_dv)
2394     {
2395         $this->num_data_rows = count($which_dv);
2396         $this->total_records = 0;
2397         $this->data = array();
2398         $this->num_recs = array();
2399         for ($i = 0; $i < $this->num_data_rows; $i++) {
2400             $this->data[$i] = array_values($which_dv[$i]);   // convert to numerical indices.
2401
2402             // Count size of each row, and total for the array.
2403             $recs = count($this->data[$i]);
2404             $this->total_records += $recs;
2405             $this->num_recs[$i] = $recs;
2406         }
2407         // This is the size of the widest row in the data array
2408         // Note records_per_group isn't used much anymore. See data_columns in CheckDataArray()
2409         $this->records_per_group = max($this->num_recs);
2410         return TRUE;
2411     }
2412
2413     /*
2414      * Pad styles arrays for later use by plot drawing functions:
2415      * This removes the need for $max_data_colors, etc. and $color_index = $color_index % $max_data_colors
2416      * in DrawBars(), DrawLines(), etc.
2417      * The arrays are padded to data_columns which is the maximum number of data sets.
2418      * See CheckDataArray() for the calculation.
2419      */
2420     protected function PadArrays()
2421     {
2422         $this->pad_array($this->line_widths, $this->data_columns);
2423         $this->pad_array($this->line_styles, $this->data_columns);
2424         $this->pad_array($this->ndx_data_colors, $this->data_columns);
2425         $this->pad_array($this->ndx_data_border_colors, $this->data_columns);
2426         // Other data color arrays are handled in the Need*Colors() functions.
2427
2428         return TRUE;
2429     }
2430
2431     /*
2432      * Pads an array with itself. This only works on 0-based sequential integer indexed arrays.
2433      *    $arr : The array (or scalar) to pad. This argument is modified.
2434      *    $size : Minimum size of the resulting array.
2435      * If $arr is a scalar, it will be converted first to a single element array.
2436      * If $arr has at least $size elements, it is unchanged.
2437      * Otherwise, append elements of $arr to itself until it reaches $size elements.
2438      */
2439     protected function pad_array(&$arr, $size)
2440     {
2441         if (! is_array($arr)) {
2442             $arr = array($arr);
2443         }
2444         $n = count($arr);
2445         $base = 0;
2446         while ($n < $size) $arr[$n++] = $arr[$base++];
2447     }
2448
2449     /*
2450      * Truncate an array to a maximum size.
2451      * This only works on 0-based sequential integer indexed arrays.
2452      *    $arr : The array to truncate.
2453      *    $size : Maximum size of the resulting array.
2454      */
2455     protected function truncate_array(&$arr, $size)
2456     {
2457         for ($n = count($arr) - 1; $n >= $size; $n--) unset($arr[$n]);
2458     }
2459
2460     /*
2461      * Format a floating-point number.
2462      *   $number : A floating point number to format
2463      *   $decimals : Number of decimal places in the result
2464      *   Returns the formatted result.
2465      * This is like PHP's number_format, but uses class variables for separators.
2466      * The separators will default to locale-specific values, if available.
2467      */
2468     protected function number_format($number, $decimals=0)
2469     {
2470         if (!isset($this->decimal_point) || !isset($this->thousands_sep)) {
2471             // Load locale-specific values from environment, unless disabled:
2472             if (empty($this->locale_override))
2473                 @setlocale(LC_ALL, '');
2474             // Fetch locale settings:
2475             $locale = @localeconv();
2476             if (!empty($locale) && isset($locale['decimal_point']) &&
2477                     isset($locale['thousands_sep'])) {
2478               $this->decimal_point = $locale['decimal_point'];
2479               $this->thousands_sep = $locale['thousands_sep'];
2480             } else {
2481               // Locale information not available.
2482               $this->decimal_point = '.';
2483               $this->thousands_sep = ',';
2484             }
2485         }
2486         return number_format($number, $decimals, $this->decimal_point, $this->thousands_sep);
2487     }
2488
2489     /*
2490      * Register a callback (hook) function
2491      *   $reason : A pre-defined name where a callback can be defined.
2492      *   $function : The name of a function to register for callback, or an instance/method
2493      *      pair in an array (see 'callbacks' in the PHP reference manual).
2494      *   $arg : Optional argument to supply to the callback function when it is triggered.
2495      *      (Often called "clientData")
2496      *   Returns True if the callback reason is valid, else False.
2497      */
2498     function SetCallback($reason, $function, $arg = NULL)
2499     {
2500         // Use array_key_exists because valid reason keys have NULL as value.
2501         if (!array_key_exists($reason, $this->callbacks))
2502             return FALSE;
2503         $this->callbacks[$reason] = array($function, $arg);
2504         return TRUE;
2505     }
2506
2507     /*
2508      * Return the name of a function registered for callback. See SetCallBack.
2509      *   $reason - A pre-defined name where a callback can be defined.
2510      *   Returns the current callback function (name or array) for the given reason,
2511      *   or False if there was no active callback or the reason is not valid.
2512      * Note you can safely test the return value with a simple 'if', as
2513      * no valid function name evaluates to false.
2514      */
2515     function GetCallback($reason)
2516     {
2517         if (isset($this->callbacks[$reason]))
2518             return $this->callbacks[$reason][0];
2519         return FALSE;
2520     }
2521
2522     /*
2523      * Un-register (remove) a function registered for callback.
2524      *   $reason - A pre-defined name where a callback can be defined.
2525      *   Returns: True if it was a valid callback reason, else False.
2526      * Note: Returns True whether or not there was a callback registered.
2527      */
2528     function RemoveCallback($reason)
2529     {
2530         if (!array_key_exists($reason, $this->callbacks))
2531             return FALSE;
2532         $this->callbacks[$reason] = NULL;
2533         return TRUE;
2534     }
2535
2536     /*
2537      * Invoke a callback, if one is registered.
2538      * Accepts a variable number of arguments >= 1:
2539      *   $reason : A string naming the callback.
2540      *   ... : Zero or more additional arguments to be passed to the
2541      *         callback function, after the passthru argument:
2542      *           callback_function($image, $passthru, ...)
2543      *   Returns: whatever value (if any) was returned by the callback.
2544      */
2545     protected function DoCallback() // Note: Variable arguments
2546     {
2547         $args = func_get_args();
2548         $reason = $args[0];
2549         if (!isset($this->callbacks[$reason]))
2550             return;
2551         list($function, $args[0]) = $this->callbacks[$reason];
2552         array_unshift($args, $this->img);
2553         // Now args[] looks like: img, passthru, extra args...
2554         return call_user_func_array($function, $args);
2555     }
2556
2557     /*
2558      * Allocate colors for the plot.
2559      * This is called by DrawGraph to allocate the colors needed for the plot.  Each selectable
2560      * color has already been validated, parsed into an array (r,g,b,a), and stored into a member
2561      * variable. Now the GD color indexes are assigned and stored into the ndx_*_color variables.
2562      * This is deferred here to avoid allocating unneeded colors and to avoid order dependencies,
2563      * especially with the transparent color.
2564      *
2565      * For drawing data elements, only the main data colors and border colors are allocated here.
2566      * Dark colors and error bar colors are allocated by Need*Color() functions.
2567      * (Data border colors default to just black, so there is no cost to always allocating.)
2568      *
2569      * Data color allocation works as follows. If there is a data_color callback, then allocate all
2570      * defined data colors (because the callback can use them however it wants). Otherwise, truncate
2571      * the array to the number of colors that will be used. This is the larger of the number of data
2572      * sets and the number of legend lines.
2573      */
2574     protected function SetColorIndexes()
2575     {
2576         $this->ndx_bg_color         = $this->GetColorIndex($this->bg_color); // Background first
2577         $this->ndx_plot_bg_color    = $this->GetColorIndex($this->plot_bg_color);
2578         if ($this->image_border_type != 'none') {
2579             $this->ndx_i_border         = $this->GetColorIndex($this->i_border);
2580             $this->ndx_i_border_dark    = $this->GetDarkColorIndex($this->i_border);
2581         }
2582
2583         // Handle defaults for X and Y title colors.
2584         $this->ndx_title_color      = $this->GetColorIndex($this->title_color);
2585         if (empty($this->x_title_color)) {
2586             $this->ndx_x_title_color = $this->ndx_title_color;
2587         } else {
2588             $this->ndx_x_title_color = $this->GetColorIndex($this->x_title_color);
2589         }
2590         if (empty($this->y_title_color)) {
2591             $this->ndx_y_title_color = $this->ndx_title_color;
2592         } else {
2593             $this->ndx_y_title_color = $this->GetColorIndex($this->y_title_color);
2594         }
2595
2596         $this->ndx_text_color       = $this->GetColorIndex($this->text_color);
2597         $this->ndx_grid_color       = $this->GetColorIndex($this->grid_color);
2598         $this->ndx_light_grid_color = $this->GetColorIndex($this->light_grid_color);
2599         $this->ndx_tick_color       = $this->GetColorIndex($this->tick_color);
2600
2601         // If no data_color callback is being used, only allocate needed colors.
2602         if (!$this->GetCallback('data_color')) {
2603             $data_colors_needed = max($this->data_columns, empty($this->legend) ? 0 : count($this->legend));
2604             $this->truncate_array($this->data_colors, $data_colors_needed);
2605             $this->truncate_array($this->data_border_colors, $data_colors_needed);
2606             $this->truncate_array($this->error_bar_colors, $data_colors_needed);
2607         }
2608
2609         // Allocate main data colors. For other colors used for data, see the functions which follow.
2610         $getcolor_cb = array($this, 'GetColorIndex');
2611         $this->ndx_data_colors = array_map($getcolor_cb, $this->data_colors);
2612         $this->ndx_data_border_colors = array_map($getcolor_cb, $this->data_border_colors);
2613
2614         // Set up a color as transparent, if SetTransparentColor was used.
2615         if (!empty($this->transparent_color)) {
2616             imagecolortransparent($this->img, $this->GetColorIndex($this->transparent_color));
2617         }
2618     }
2619
2620     /*
2621      * Allocate dark-shade data colors. Called if needed by graph drawing functions.
2622      */
2623     protected function NeedDataDarkColors()
2624     {
2625         $getdarkcolor_cb = array($this, 'GetDarkColorIndex');
2626         $this->ndx_data_dark_colors = array_map($getdarkcolor_cb, $this->data_colors);
2627         $this->pad_array($this->ndx_data_dark_colors, $this->data_columns);
2628     }
2629
2630     /*
2631      * Allocate error bar colors. Called if needed by graph drawing functions.
2632      */
2633     protected function NeedErrorBarColors()
2634     {
2635         $getcolor_cb = array($this, 'GetColorIndex');
2636         $this->ndx_error_bar_colors = array_map($getcolor_cb, $this->error_bar_colors);
2637         $this->pad_array($this->ndx_error_bar_colors, $this->data_columns);
2638     }
2639
2640 //////////////////////////////////////////////////////////
2641 ///////////         DATA ANALYSIS, SCALING AND TRANSLATION
2642 //////////////////////////////////////////////////////////
2643
2644     /*
2645      * Analyzes the data array and calculates the minimum and maximum values.
2646      * In this function, IV refers to the independent variable, and DV the dependent variable.
2647      * For most plots, IV is X and DV is Y. For swapped X/Y plots, IV is Y and DV is X.
2648      * At the end of the function, IV and DV ranges get assigned into X or Y.
2649      *
2650      * This has to know how certain plot types use the data. 'area' and 'pie' use absolute
2651      * values, 'stackedbars' sums values, and 'stackedarea' sums absolute values.
2652      *
2653      * This calculates min_x, max_x, min_y, and max_y. It also calculates two arrays
2654      * data_min[] and data_max[] with per-row min and max values. These are used for
2655      * data label lines. For normal (unswapped) data, these are the Y range for each X.
2656      * For swapped X/Y data, they are the X range for each Y.
2657      */
2658     protected function FindDataLimits()
2659     {
2660         // Special case processing for certain plot types:
2661         $sum_abs = ($this->plot_type == 'stackedarea'); // Sum of absolute values
2662         $sum_val = ($this->plot_type == 'stackedbars'); // Sum of values
2663         $abs_val = ($this->plot_type == 'area' || $this->plot_type == 'pie'); // Absolute values
2664
2665         // These need to be initialized in case there are multiple plots and missing data points.
2666         $this->data_min = array();
2667         $this->data_max = array();
2668
2669         // Independent values are in the data array or assumed?
2670         if ($this->datatype_implied) {
2671             $all_iv = array(0, $this->num_data_rows - 1);
2672         } else {
2673             $all_iv = array();
2674         }
2675
2676         // Process all rows of data:
2677         for ($i = 0; $i < $this->num_data_rows; $i++) {
2678             $n_vals = $this->num_recs[$i];
2679             $j = 1; // Skips label at [0]
2680
2681             if (!$this->datatype_implied) {
2682                 $all_iv[] = (double)$this->data[$i][$j++];
2683             }
2684
2685             if ($sum_abs || $sum_val) {
2686                 $all_dv = array(0, 0); // One limit is 0, other calculated below
2687             } else {
2688                 $all_dv = array();
2689             }
2690             while ($j < $n_vals) {
2691                 if (is_numeric($this->data[$i][$j])) {
2692                     $val = (double)$this->data[$i][$j++];
2693
2694                     if ($this->datatype_error_bars) {
2695                         $all_dv[] = $val + (double)$this->data[$i][$j++];
2696                         $all_dv[] = $val - (double)$this->data[$i][$j++];
2697                     } elseif ($sum_abs) {
2698                         $all_dv[1] += abs($val); // Sum of absolute values
2699                     } elseif ($sum_val) {
2700                         $all_dv[1] += $val;  // Sum of values
2701                     } elseif ($abs_val) {
2702                         $all_dv[] = abs($val); // List of all absolute values
2703                     } else {
2704                         $all_dv[] = $val; // List of all values
2705                     }
2706                 } else {    // Missing DV value
2707                   $j++;
2708                   if ($this->datatype_error_bars) $j += 2;
2709                 }
2710             }
2711             if (!empty($all_dv)) {
2712                 $this->data_min[$i] = min($all_dv);  // Store per-row DV range
2713                 $this->data_max[$i] = max($all_dv);
2714             }
2715         }
2716
2717         if ($this->datatype_swapped_xy) {
2718             // Assign min and max for swapped X/Y plots: IV=Y and DV=X
2719             $this->min_y = min($all_iv);
2720             $this->max_y = max($all_iv);
2721             if (empty($this->data_min)) { // Guard against regressive case: No X at all
2722                 $this->min_x = 0;
2723                 $this->max_x = 0;
2724             } else {
2725                 $this->min_x = min($this->data_min);  // Store global X range
2726                 $this->max_x = max($this->data_max);
2727             }
2728         } else {
2729             // Assign min and max for normal plots: IV=X and DV=Y
2730             $this->min_x = min($all_iv);
2731             $this->max_x = max($all_iv);
2732             if (empty($this->data_min)) { // Guard against regressive case: No Y at all
2733                 $this->min_y = 0;
2734                 $this->max_y = 0;
2735             } else {
2736                 $this->min_y = min($this->data_min);  // Store global Y range
2737                 $this->max_y = max($this->data_max);
2738             }
2739         }
2740
2741         if ($this->GetCallback('debug_scale')) {
2742             $this->DoCallback('debug_scale', __FUNCTION__, array(
2743                 'min_x' => $this->min_x, 'min_y' => $this->min_y,
2744                 'max_x' => $this->max_x, 'max_y' => $this->max_y));
2745         }
2746         return TRUE;
2747     }
2748
2749     /*
2750      * Calculates image margins on the fly from title positions and sizes,
2751      * and tick labels positions and sizes.
2752      *
2753      * A picture of the locations of elements and spacing can be found in the
2754      * PHPlot Reference Manual.
2755      *
2756      * Calculates the following (class variables unless noted):
2757      *
2758      * Plot area margins (see note below):
2759      *     y_top_margin
2760      *     y_bot_margin
2761      *     x_left_margin
2762      *     x_right_margin
2763      *
2764      * Title sizes (these are now local, not class variables, since they are not used elsewhere):
2765      *     title_height : Height of main title
2766      *     x_title_height : Height of X axis title, 0 if no X title
2767      *     y_title_width : Width of Y axis title, 0 if no Y title
2768      *
2769      * Tick/Data label offsets, relative to plot_area:
2770      *     x_label_top_offset, x_label_bot_offset, x_label_axis_offset
2771      *     y_label_left_offset, y_label_right_offset, y_label_axis_offset
2772      *
2773      * Title offsets, relative to plot area:
2774      *     x_title_top_offset, x_title_bot_offset
2775      *     y_title_left_offset, y_title_left_offset
2776      *     title_offset (for main title, relative to image edge)
2777      *
2778      *  Note: The margins are calculated, but not stored, if margins or plot area were
2779      *  set by the user with SetPlotAreaPixels or SetMarginsPixels. The margin
2780      *  calculation is mixed in with the offset variables, so it doesn't seem worth the
2781      *  trouble to separate them.
2782      *
2783      * If the $maximize argument is true, we use the full image size, minus safe_margin
2784      * and main title, for the plot. This is for pie charts which have no axes or X/Y titles.
2785      */
2786     protected function CalcMargins($maximize)
2787     {
2788         // This is the line-to-line or line-to-text spacing:
2789         $gap = $this->safe_margin;
2790         // Initial margin on each side takes into account a possible image border.
2791         // For compatibility, if border is 1 or 2, don't increase the margins.
2792         $base_margin = max($gap, $this->GetImageBorderWidth() + 3);
2793         $this->title_offset = $base_margin;  // For use in DrawTitle
2794
2795         // Minimum margin on each side. This reduces the chance that the
2796         // right-most tick label (for example) will run off the image edge
2797         // if there are no titles on that side.
2798         $min_margin = 2 * $gap + $base_margin;
2799
2800         // Calculate the title sizes:
2801         list($unused, $title_height) = $this->SizeText($this->fonts['title'], 0, $this->title_txt);
2802         list($unused, $x_title_height) = $this->SizeText($this->fonts['x_title'], 0, $this->x_title_txt);
2803         list($y_title_width, $unused) = $this->SizeText($this->fonts['y_title'], 90, $this->y_title_txt);
2804
2805         // Special case for maximum area usage with no X/Y titles or labels, only main title:
2806         if ($maximize) {
2807             if (!isset($this->x_left_margin))
2808                 $this->x_left_margin = $base_margin;
2809             if (!isset($this->x_right_margin))
2810                 $this->x_right_margin = $base_margin;
2811             if (!isset($this->y_top_margin)) {
2812                 $this->y_top_margin = $base_margin;
2813                 if ($title_height > 0)
2814                     $this->y_top_margin += $title_height + $gap;
2815             }
2816             if (!isset($this->y_bot_margin))
2817                 $this->y_bot_margin = $base_margin;
2818
2819             return TRUE;
2820         }
2821
2822         // Make local variables for these. (They get used a lot and I'm tired of this, this, this.)
2823         $x_tick_label_pos = $this->x_tick_label_pos;
2824         $x_data_label_pos = $this->x_data_label_pos;
2825         $x_tick_pos       = $this->x_tick_pos;
2826         $x_tick_len       = $this->x_tick_length;
2827         $y_tick_label_pos = $this->y_tick_label_pos;
2828         $y_tick_pos       = $this->y_tick_pos;
2829         $y_tick_len       = $this->y_tick_length;
2830         $y_data_label_pos = $this->y_data_label_pos;
2831
2832         // For X/Y tick and label position of 'xaxis' or 'yaxis', determine if the axis happens to be
2833         // on an edge of a plot. If it is, we need to account for the margins there.
2834         if ($this->x_axis_position <= $this->plot_min_y)
2835             $x_axis_pos = 'bottom';
2836         elseif ($this->x_axis_position >= $this->plot_max_y)
2837             $x_axis_pos = 'top';
2838         else
2839             $x_axis_pos = 'none';
2840         if ($this->y_axis_position <= $this->plot_min_x)
2841             $y_axis_pos = 'left';
2842         elseif ($this->y_axis_position >= $this->plot_max_x)
2843             $y_axis_pos = 'right';
2844         else
2845             $y_axis_pos = 'none';
2846
2847         // Calculate the heights for X tick and data labels, and the max (used if they are overlaid):
2848         $x_data_label_height = ($x_data_label_pos == 'none') ? 0 : $this->CalcMaxDataLabelSize('x');
2849         $x_tick_label_height = ($x_tick_label_pos == 'none') ? 0 : $this->CalcMaxTickLabelSize('x');
2850         $x_max_label_height = max($x_data_label_height, $x_tick_label_height);
2851
2852         // Calculate the space needed above and below the plot for X tick and X data labels:
2853
2854         // Above the plot:
2855         $tick_labels_above = ($x_tick_label_pos == 'plotup' || $x_tick_label_pos == 'both'
2856                           || ($x_tick_label_pos == 'xaxis' && $x_axis_pos == 'top'));
2857         $data_labels_above = ($x_data_label_pos == 'plotup' || $x_data_label_pos == 'both');
2858         if ($tick_labels_above) {
2859             if ($data_labels_above) {
2860                 $label_height_above = $x_max_label_height;
2861             } else {
2862                 $label_height_above = $x_tick_label_height;
2863             }
2864         } elseif ($data_labels_above) {
2865             $label_height_above = $x_data_label_height;
2866         } else {
2867             $label_height_above = 0;
2868         }
2869
2870         // Below the plot:
2871         $tick_labels_below = ($x_tick_label_pos == 'plotdown' || $x_tick_label_pos == 'both'
2872                           || ($x_tick_label_pos == 'xaxis' && $x_axis_pos == 'bottom'));
2873         $data_labels_below = ($x_data_label_pos == 'plotdown' || $x_data_label_pos == 'both');
2874         if ($tick_labels_below) {
2875             if ($data_labels_below) {
2876                 $label_height_below = $x_max_label_height;
2877             } else {
2878                 $label_height_below = $x_tick_label_height;
2879             }
2880         } elseif ($data_labels_below) {
2881             $label_height_below = $x_data_label_height;
2882         } else {
2883             $label_height_below = 0;
2884         }
2885
2886         // Calculate the width for Y tick and data labels, if on, and the max:
2887         // Note CalcMaxDataLabelSize('y') returns 0 except for swapped X/Y plots.
2888         $y_data_label_width = ($y_data_label_pos == 'none') ? 0 : $this->CalcMaxDataLabelSize('y');
2889         $y_tick_label_width = ($y_tick_label_pos == 'none') ? 0 : $this->CalcMaxTickLabelSize('y');
2890         $y_max_label_width = max($y_data_label_width, $y_tick_label_width);
2891
2892         // Calculate the space needed left and right of the plot for Y tick and Y data labels:
2893         // (Y data labels here are for swapped X/Y plots such has horizontal bars)
2894
2895         // Left of the plot:
2896         $tick_labels_left = ($y_tick_label_pos == 'plotleft' || $y_tick_label_pos == 'both'
2897                          || ($y_tick_label_pos == 'yaxis' && $y_axis_pos == 'left'));
2898         $data_labels_left = ($y_data_label_pos == 'plotleft' || $y_data_label_pos == 'both');
2899         if ($tick_labels_left) {
2900             if ($data_labels_left) {
2901                 $label_width_left = $y_max_label_width;
2902             } else {
2903                 $label_width_left = $y_tick_label_width;
2904             }
2905         } elseif ($data_labels_left) {
2906             $label_width_left = $y_data_label_width;
2907         } else {
2908             $label_width_left = 0;
2909         }
2910
2911         // Right of the plot:
2912         $tick_labels_right = ($y_tick_label_pos == 'plotright' || $y_tick_label_pos == 'both'
2913                           || ($y_tick_label_pos == 'yaxis' && $y_axis_pos == 'right'));
2914         $data_labels_right = ($y_data_label_pos == 'plotright' || $y_data_label_pos == 'both');
2915         if ($tick_labels_right) {
2916             if ($data_labels_right) {
2917                 $label_width_right = $y_max_label_width;
2918             } else {
2919                 $label_width_right = $y_tick_label_width;
2920             }
2921         } elseif ($data_labels_right) {
2922             $label_width_right = $y_data_label_width;
2923         } else {
2924             $label_width_right = 0;
2925         }
2926
2927         ///////// Calculate margins:
2928
2929         // Calculating Top and Bottom margins:
2930         // y_top_margin: Main title, Upper X title, X ticks and tick labels, and X data labels:
2931         // y_bot_margin: Lower title, ticks and tick labels, and data labels:
2932         $top_margin = $base_margin;
2933         $bot_margin = $base_margin;
2934         $this->x_title_top_offset = $gap;
2935         $this->x_title_bot_offset = $gap;
2936
2937         // Space for main title?
2938         if ($title_height > 0)
2939             $top_margin += $title_height + $gap;
2940
2941         // Space for X Title?
2942         if ($x_title_height > 0) {
2943             $pos = $this->x_title_pos;
2944             if ($pos == 'plotup' || $pos == 'both')
2945                 $top_margin += $x_title_height + $gap;
2946             if ($pos == 'plotdown' || $pos == 'both')
2947                 $bot_margin += $x_title_height + $gap;
2948         }
2949
2950         // Space for X Labels above the plot?
2951         if ($label_height_above > 0) {
2952             $top_margin += $label_height_above + $gap;
2953             $this->x_title_top_offset += $label_height_above + $gap;
2954         }
2955
2956         // Space for X Labels below the plot?
2957         if ($label_height_below > 0) {
2958             $bot_margin += $label_height_below + $gap;
2959             $this->x_title_bot_offset += $label_height_below + $gap;
2960         }
2961
2962         // Space for X Ticks above the plot?
2963         if ($x_tick_pos == 'plotup' || $x_tick_pos == 'both'
2964            || ($x_tick_pos == 'xaxis' && $x_axis_pos == 'top')) {
2965             $top_margin += $x_tick_len;
2966             $this->x_label_top_offset = $x_tick_len + $gap;
2967             $this->x_title_top_offset += $x_tick_len;
2968         } else {
2969             // No X Ticks above the plot:
2970             $this->x_label_top_offset = $gap;
2971         }
2972
2973         // Space for X Ticks below the plot?
2974         if ($x_tick_pos == 'plotdown' || $x_tick_pos == 'both'
2975            || ($x_tick_pos == 'xaxis' && $x_axis_pos == 'bottom')) {
2976             $bot_margin += $x_tick_len;
2977             $this->x_label_bot_offset = $x_tick_len + $gap;
2978             $this->x_title_bot_offset += $x_tick_len;
2979         } else {
2980             // No X Ticks below the plot:
2981             $this->x_label_bot_offset = $gap;
2982         }
2983         // Label offsets for on-axis ticks:
2984         if ($x_tick_pos == 'xaxis') {
2985             $this->x_label_axis_offset = $x_tick_len + $gap;
2986         } else {
2987             $this->x_label_axis_offset = $gap;
2988         }
2989
2990         // Calculating Left and Right margins:
2991         // x_left_margin: Left Y title, Y ticks and tick labels:
2992         // x_right_margin: Right Y title, Y ticks and tick labels:
2993         $left_margin = $base_margin;
2994         $right_margin = $base_margin;
2995         $this->y_title_left_offset = $gap;
2996         $this->y_title_right_offset = $gap;
2997
2998         // Space for Y Title?
2999         if ($y_title_width > 0) {
3000             $pos = $this->y_title_pos;
3001             if ($pos == 'plotleft' || $pos == 'both')
3002                 $left_margin += $y_title_width + $gap;
3003             if ($pos == 'plotright' || $pos == 'both')
3004                 $right_margin += $y_title_width + $gap;
3005         }
3006
3007         // Space for Y Labels left of the plot?
3008         if ($label_width_left > 0) {
3009             $left_margin += $label_width_left + $gap;
3010             $this->y_title_left_offset += $label_width_left + $gap;
3011         }
3012
3013         // Space for Y Labels right of the plot?
3014         if ($label_width_right > 0) {
3015             $right_margin += $label_width_right + $gap;
3016             $this->y_title_right_offset += $label_width_right + $gap;
3017         }
3018
3019         // Space for Y Ticks left of plot?
3020         if ($y_tick_pos == 'plotleft' || $y_tick_pos == 'both'
3021            || ($y_tick_pos == 'yaxis' && $y_axis_pos == 'left')) {
3022             $left_margin += $y_tick_len;
3023             $this->y_label_left_offset = $y_tick_len + $gap;
3024             $this->y_title_left_offset += $y_tick_len;
3025         } else {
3026             // No Y Ticks left of plot:
3027             $this->y_label_left_offset = $gap;
3028         }
3029
3030         // Space for Y Ticks right of plot?
3031         if ($y_tick_pos == 'plotright' || $y_tick_pos == 'both'
3032            || ($y_tick_pos == 'yaxis' && $y_axis_pos == 'right')) {
3033             $right_margin += $y_tick_len;
3034             $this->y_label_right_offset = $y_tick_len + $gap;
3035             $this->y_title_right_offset += $y_tick_len;
3036         } else {
3037             // No Y Ticks right of plot:
3038             $this->y_label_right_offset = $gap;
3039         }
3040
3041         // Label offsets for on-axis ticks:
3042         if ($x_tick_pos == 'yaxis') {
3043             $this->y_label_axis_offset = $y_tick_len + $gap;
3044         } else {
3045             $this->y_label_axis_offset = $gap;
3046         }
3047
3048         // Apply the minimum margins and store in the object.
3049         // Do not set margins which were user-defined (see note at top of function).
3050         if (!isset($this->y_top_margin))
3051             $this->y_top_margin = max($min_margin, $top_margin);
3052         if (!isset($this->y_bot_margin))
3053             $this->y_bot_margin = max($min_margin, $bot_margin);
3054         if (!isset($this->x_left_margin))
3055             $this->x_left_margin = max($min_margin, $left_margin);
3056         if (!isset($this->x_right_margin))
3057             $this->x_right_margin = max($min_margin, $right_margin);
3058
3059         if ($this->GetCallback('debug_scale')) {
3060             // (Too bad compact() doesn't work on class member variables...)
3061             $this->DoCallback('debug_scale', __FUNCTION__, array(
3062                 'label_height_above' => $label_height_above,
3063                 'label_height_below' => $label_height_below,
3064                 'label_width_left' => $label_width_left,
3065                 'label_width_right' => $label_width_right,
3066                 'x_tick_len' => $x_tick_len,
3067                 'y_tick_len' => $y_tick_len,
3068                 'x_left_margin' => $this->x_left_margin,
3069                 'x_right_margin' => $this->x_right_margin,
3070                 'y_top_margin' => $this->y_top_margin,
3071                 'y_bot_margin' => $this->y_bot_margin,
3072                 'x_label_top_offset' => $this->x_label_top_offset,
3073                 'x_label_bot_offset' => $this->x_label_bot_offset,
3074                 'y_label_left_offset' => $this->y_label_left_offset,
3075                 'y_label_right_offset' => $this->y_label_right_offset,
3076                 'x_title_top_offset' => $this->x_title_top_offset,
3077                 'x_title_bot_offset' => $this->x_title_bot_offset,
3078                 'y_title_left_offset' => $this->y_title_left_offset,
3079                 'y_title_right_offset' => $this->y_title_right_offset));
3080         }
3081
3082         return TRUE;
3083     }
3084
3085     /*
3086      * Calculate the plot area (device coordinates) from the margins.
3087      * (This used to be part of SetPlotAreaPixels.)
3088      * The margins might come from SetMarginsPixels, SetPlotAreaPixels,
3089      * or CalcMargins.
3090      */
3091     protected function CalcPlotAreaPixels()
3092     {
3093         $this->plot_area = array($this->x_left_margin, $this->y_top_margin,
3094                                  $this->image_width - $this->x_right_margin,
3095                                  $this->image_height - $this->y_bot_margin);
3096         $this->plot_area_width = $this->plot_area[2] - $this->plot_area[0];
3097         $this->plot_area_height = $this->plot_area[3] - $this->plot_area[1];
3098
3099         $this->DoCallback('debug_scale', __FUNCTION__, $this->plot_area);
3100         return TRUE;
3101     }
3102
3103     /*
3104      * Set the margins in pixels (left, right, top, bottom)
3105      * This determines the plot area, equivalent to SetPlotAreaPixels().
3106      * Deferred calculations now occur in CalcPlotAreaPixels().
3107      */
3108     function SetMarginsPixels($which_lm = NULL, $which_rm = NULL, $which_tm = NULL, $which_bm = NULL)
3109     {
3110         $this->x_left_margin = $which_lm;
3111         $this->x_right_margin = $which_rm;
3112         $this->y_top_margin = $which_tm;
3113         $this->y_bot_margin = $which_bm;
3114
3115         return TRUE;
3116     }
3117
3118     /*
3119      * Sets the limits for the plot area.
3120      * This stores the margins, not the area. That may seem odd, but
3121      * the idea is to make SetPlotAreaPixels and SetMarginsPixels two
3122      * ways to accomplish the same thing, and the deferred calculations
3123      * in CalcMargins and CalcPlotAreaPixels don't need to know which
3124      * was used.
3125      *   (x1, y1) - Upper left corner of the plot area
3126      *   (x2, y2) - Lower right corner of the plot area
3127      */
3128     function SetPlotAreaPixels($x1 = NULL, $y1 = NULL, $x2 = NULL, $y2 = NULL)
3129     {
3130         $this->x_left_margin = $x1;
3131         if (isset($x2)) $this->x_right_margin = $this->image_width - $x2;
3132         else unset($this->x_right_margin);
3133         $this->y_top_margin = $y1;
3134         if (isset($y2)) $this->y_bot_margin = $this->image_height - $y2;
3135         else unset($this->y_bot_margin);
3136
3137         return TRUE;
3138     }
3139
3140     /*
3141      * Calculate the World Coordinate limits of the plot area.
3142      * This goes with SetPlotAreaWorld, but the calculations are
3143      * deferred until the graph is being drawn.
3144      * Uses and sets: plot_min_x, plot_max_x, plot_min_y, plot_max_y
3145      * These can be user-supplied or NULL to auto-calculate.
3146      * Pre-requisites: FindDataLimits() calculates min_x, max_x, min_y, max_y
3147      * which are the limits of the data to be plotted.
3148      *
3149      * Note: $implied_y and $swapped_xy are currently equivalent, but in the
3150      * future there may be a data type with swapped X/Y and explicit Y values.
3151      * The 4 code blocks below for plot_min_x, plot_max_x, plot_min_y, and
3152      * plot_max_y already contain logic for this case.
3153      * The general method is this:
3154      *   If any part of the range is user-defined (via SetPlotAreaWorld),
3155      *      use the user-defined value.
3156      *   Else, if this is an implicitly-defined independent variable,
3157      *      use the fixed range of 0 to (max+1).
3158      *   Else, if this is an explicitly-defined independent variable,
3159      *      use the exact data range (min to max).
3160      *   Else, this is the dependent variable, so define a range which
3161      *      includes and exceeds the data range by a bit.
3162      */
3163     protected function CalcPlotAreaWorld()
3164     {
3165         // Data array omits X or Y?
3166         $implied_x = $this->datatype_implied && !$this->datatype_swapped_xy;
3167         $implied_y = $this->datatype_implied && $this->datatype_swapped_xy;
3168
3169         if (isset($this->plot_min_x) && $this->plot_min_x !== '')
3170             $xmin = $this->plot_min_x; // Use user-provided value
3171         elseif ($implied_x)
3172             $xmin = 0;          // Implied X starts at zero
3173         elseif ($this->datatype_swapped_xy)
3174             // If X is the dependent variable, leave some room below.
3175             $xmin = floor($this->min_x - abs($this->min_x) * 0.1);
3176         else
3177             $xmin = $this->min_x;  // Otherwise just start at the min data X
3178
3179         if (isset($this->plot_max_x) && $this->plot_max_x !== '')
3180             $xmax = $this->plot_max_x; // Use user-provided value
3181         elseif ($implied_x)
3182             $xmax = $this->max_x + 1; // Implied X ends after last value
3183         elseif ($this->datatype_swapped_xy)
3184             // If X is the dependent variable, leave some room above.
3185             $xmax = ceil($this->max_x + abs($this->max_x) * 0.1);
3186         else
3187             $xmax = $this->max_x; // Otherwise just end at the max data X
3188
3189         if (isset($this->plot_min_y) && $this->plot_min_y !== '')
3190             $ymin = $this->plot_min_y;  // Use user-provided value
3191         elseif ($implied_y)
3192             $ymin = 0;    // Implied Y starts at zero
3193         elseif ($this->datatype_swapped_xy)
3194             $ymin = $this->min_y; // Start at min data Y
3195         else
3196             // If Y is the dependent variable, leave some room below.
3197             $ymin = floor($this->min_y - abs($this->min_y) * 0.1);
3198
3199         if (isset($this->plot_max_y) && $this->plot_max_y !== '')
3200             $ymax = $this->plot_max_y; // Use user-provided value
3201         elseif ($implied_y)
3202             $ymax = $this->max_y + 1; // Implied Y ends after last value
3203         elseif ($this->datatype_swapped_xy)
3204             $ymax = $this->max_y;  // End at max data Y
3205         else
3206             // If Y is the dependent variable, leave some room above.
3207             $ymax = ceil($this->max_y + abs($this->max_y) * 0.1);
3208
3209         // Error checking
3210
3211         if ($ymin == $ymax)
3212             $ymax++;
3213         if ($xmin == $xmax)
3214             $xmax++;
3215
3216         if ($this->yscale_type == 'log') {
3217             if ($ymin <= 0) {
3218                 $ymin = 1;
3219             }
3220             if ($ymax <= 0) {
3221                 // Note: Error messages reference the user function, not this function.
3222                 return $this->PrintError('SetPlotAreaWorld(): Log plots need data greater than 0');
3223             }
3224         }
3225
3226         if ($ymax <= $ymin) {
3227             return $this->PrintError('SetPlotAreaWorld(): Error in data - max not greater than min');
3228         }
3229
3230         $this->plot_min_x = $xmin;
3231         $this->plot_max_x = $xmax;
3232         $this->plot_min_y = $ymin;
3233         $this->plot_max_y = $ymax;
3234         if ($this->GetCallback('debug_scale')) {
3235             $this->DoCallback('debug_scale', __FUNCTION__, array(
3236                 'plot_min_x' => $this->plot_min_x, 'plot_min_y' => $this->plot_min_y,
3237                 'plot_max_x' => $this->plot_max_x, 'plot_max_y' => $this->plot_max_y));
3238         }
3239
3240         return TRUE;
3241     }
3242
3243     /*
3244      * Stores the desired World Coordinate range of the plot.
3245      * The user calls this to force one or more of the range limits to
3246      * specific values. Anything not set will be calculated in CalcPlotAreaWorld().
3247      */
3248     function SetPlotAreaWorld($xmin=NULL, $ymin=NULL, $xmax=NULL, $ymax=NULL)
3249     {
3250         $this->plot_min_x = $xmin;
3251         $this->plot_max_x = $xmax;
3252         $this->plot_min_y = $ymin;
3253         $this->plot_max_y = $ymax;
3254         return TRUE;
3255     }
3256
3257     /*
3258      * Calculate the width (or height) of bars for bar plots.
3259      * This calculates:
3260      *    record_bar_width : Allocated width for each bar (including gaps)
3261      *    actual_bar_width : Actual drawn width of each bar
3262      *    bar_adjust_gap  : Gap on each side of each bar (0 if they touch)
3263      * For the case $verticals=False, horizontal bars are being drawn,
3264      * but the same variable names are used. Think of "bar_width" as being
3265      * the width if you are standing on the Y axis looking towards positive X.
3266      */
3267     protected function CalcBarWidths($verticals = TRUE)
3268     {
3269         // group_width is the width of a group, including padding
3270         if ($verticals) {
3271             $group_width = $this->plot_area_width / $this->num_data_rows;
3272         } else {
3273             $group_width = $this->plot_area_height / $this->num_data_rows;
3274         }
3275
3276         // Actual number of bar spaces in the group. This includes the drawn bars, and
3277         // 'bar_extra_space'-worth of extra bars.
3278         if ($this->plot_type == 'stackedbars') {
3279           $num_spots = 1 + $this->bar_extra_space;
3280         } else {
3281           $num_spots = $this->data_columns + $this->bar_extra_space;
3282         }
3283
3284         // record_bar_width is the width of each bar's allocated area.
3285         // If bar_width_adjust=1 this is the width of the bar, otherwise
3286         // the bar is centered inside record_bar_width.
3287         // The equation is:
3288         //   group_frac_width * group_width = record_bar_width * num_spots
3289         $this->record_bar_width = $this->group_frac_width * $group_width / $num_spots;
3290
3291         // Note that the extra space due to group_frac_width and bar_extra_space will be
3292         // evenly divided on each side of the group: the drawn bars are centered in the group.
3293
3294         // Within each bar's allocated space, if bar_width_adjust=1 the bar fills the
3295         // space, otherwise it is centered.
3296         // This is the actual drawn bar width:
3297         $this->actual_bar_width = $this->record_bar_width * $this->bar_width_adjust;
3298         // This is the gap on each side of the bar (0 if bar_width_adjust=1):
3299         $this->bar_adjust_gap = ($this->record_bar_width - $this->actual_bar_width) / 2;
3300
3301         if ($this->GetCallback('debug_scale')) {
3302             $this->DoCallback('debug_scale', __FUNCTION__, array(
3303                 'record_bar_width' => $this->record_bar_width,
3304                 'actual_bar_width' => $this->actual_bar_width,
3305                 'bar_adjust_gap' => $this->bar_adjust_gap));
3306         }
3307         return TRUE;
3308     }
3309
3310     /*
3311      * Calculate X and Y Axis Positions, world coordinates.
3312      * This needs the min/max x/y range set by CalcPlotAreaWorld.
3313      * It adjusts or sets x_axis_position and y_axis_position per the data.
3314      * Empty string means the values need to be calculated; otherwise they
3315      * are supplied but need to be validated against the World area.
3316      *
3317      * Note: This used to be in CalcTranslation, but CalcMargins needs it too.
3318      * This does not calculate the pixel values of the axes. That happens in
3319      * CalcTranslation, after scaling is set up (which has to happen after
3320      * margins are set up).
3321      *
3322      * For vertical plots, the X axis defaults to Y=0 if that is inside the plot range, else whichever
3323      * of the top or bottom that has the smallest absolute value (that is, the value closest to 0).
3324      * The Y axis defaults to the left edge. For horizontal plots, the axis roles and defaults are switched.
3325      */
3326     protected function CalcAxisPositions()
3327     {
3328         // Validate user-provided X axis position, or calculate a default if not provided:
3329         if ($this->x_axis_position !== '') {
3330             // Force user-provided X axis position to be within the plot range:
3331             $this->x_axis_position = min(max($this->plot_min_y, $this->x_axis_position), $this->plot_max_y);
3332         } elseif ($this->yscale_type == 'log') {
3333             // Always use 1 for X axis position on log scale plots.
3334             $this->x_axis_position = 1;
3335         } elseif ($this->datatype_swapped_xy || $this->plot_min_y > 0) {
3336             // Horizontal plot, or Vertical Plot with all Y > 0: Place X axis on the bottom.
3337             $this->x_axis_position = $this->plot_min_y;
3338         } elseif ($this->plot_max_y < 0) {
3339             // Vertical plot with all Y < 0, so place the X axis at the top.
3340             $this->x_axis_position = $this->plot_max_y;
3341         } else {
3342             // Vertical plot range includes Y=0, so place X axis at 0.
3343             $this->x_axis_position = 0;
3344         }
3345
3346         // Validate user-provided Y axis position, or calculate a default if not provided:
3347         if ($this->y_axis_position !== '') {
3348             // Force user-provided Y axis position to be within the plot range:
3349             $this->y_axis_position = min(max($this->plot_min_x, $this->y_axis_position), $this->plot_max_x);
3350         } elseif ($this->xscale_type == 'log') {
3351             // Always use 1 for Y axis position on log scale plots.
3352             $this->y_axis_position = 1;
3353         } elseif (!$this->datatype_swapped_xy || $this->plot_min_x > 0) {
3354             // Vertical plot, or Horizontal Plot with all X > 0: Place Y axis on left side.
3355             $this->y_axis_position = $this->plot_min_x;
3356         } elseif ($this->plot_max_x < 0) {
3357             // Horizontal plot with all X < 0, so place the Y axis on the right side.
3358             $this->y_axis_position = $this->plot_max_x;
3359         } else {
3360             // Horizontal plot range includes X=0: place Y axis at 0.
3361             $this->y_axis_position = 0;
3362         }
3363
3364         if ($this->GetCallback('debug_scale')) {
3365             $this->DoCallback('debug_scale', __FUNCTION__, array(
3366                 'x_axis_position' => $this->x_axis_position,
3367                 'y_axis_position' => $this->y_axis_position));
3368         }
3369
3370         return TRUE;
3371     }
3372
3373     /*
3374      * Calculates scaling stuff...
3375      */
3376     protected function CalcTranslation()
3377     {
3378         if ($this->plot_max_x - $this->plot_min_x == 0) { // Check for div by 0
3379             $this->xscale = 0;
3380         } else {
3381             if ($this->xscale_type == 'log') {
3382                 $this->xscale = $this->plot_area_width /
3383                                 (log10($this->plot_max_x) - log10($this->plot_min_x));
3384             } else {
3385                 $this->xscale = $this->plot_area_width / ($this->plot_max_x - $this->plot_min_x);
3386             }
3387         }
3388
3389         if ($this->plot_max_y - $this->plot_min_y == 0) { // Check for div by 0
3390             $this->yscale = 0;
3391         } else {
3392             if ($this->yscale_type == 'log') {
3393                 $this->yscale = $this->plot_area_height /
3394                                 (log10($this->plot_max_y) - log10($this->plot_min_y));
3395             } else {
3396                 $this->yscale = $this->plot_area_height / ($this->plot_max_y - $this->plot_min_y);
3397             }
3398         }
3399         // GD defines x = 0 at left and y = 0 at TOP so -/+ respectively
3400         if ($this->xscale_type == 'log') {
3401             $this->plot_origin_x = $this->plot_area[0] - ($this->xscale * log10($this->plot_min_x) );
3402         } else {
3403             $this->plot_origin_x = $this->plot_area[0] - ($this->xscale * $this->plot_min_x);
3404         }
3405         if ($this->yscale_type == 'log') {
3406             $this->plot_origin_y = $this->plot_area[3] + ($this->yscale * log10($this->plot_min_y));
3407         } else {
3408             $this->plot_origin_y = $this->plot_area[3] + ($this->yscale * $this->plot_min_y);
3409         }
3410
3411         // Convert axis positions to device coordinates:
3412         $this->y_axis_x_pixels = $this->xtr($this->y_axis_position);
3413         $this->x_axis_y_pixels = $this->ytr($this->x_axis_position);
3414
3415         if ($this->GetCallback('debug_scale')) {
3416             $this->DoCallback('debug_scale', __FUNCTION__, array(
3417                 'xscale' => $this->xscale, 'yscale' => $this->yscale,
3418                 'plot_origin_x' => $this->plot_origin_x, 'plot_origin_y' => $this->plot_origin_y,
3419                 'y_axis_x_pixels' => $this->y_axis_x_pixels,
3420                 'x_axis_y_pixels' => $this->x_axis_y_pixels));
3421         }
3422
3423         return TRUE;
3424     }
3425
3426     /*
3427      * Translate X world coordinate into pixel coordinate
3428      * See CalcTranslation() for calculation of xscale.
3429      * Note: This function should be 'protected', but is left public for historical reasons.
3430      * See GetDeviceXY() for a preferred public method.
3431      */
3432     function xtr($x_world)
3433     {
3434         if ($this->xscale_type == 'log') {
3435             $x_pixels = $this->plot_origin_x + log10($x_world) * $this->xscale ;
3436         } else {
3437             $x_pixels = $this->plot_origin_x + $x_world * $this->xscale ;
3438         }
3439         return round($x_pixels);
3440     }
3441
3442     /*
3443      * Translate Y world coordinate into pixel coordinate.
3444      * See CalcTranslation() for calculation of yscale.
3445      * Note: This function should be 'protected', but is left public for historical reasons.
3446      * See GetDeviceXY() for a preferred public method.
3447      */
3448     function ytr($y_world)
3449     {
3450         if ($this->yscale_type == 'log') {
3451             //minus because GD defines y = 0 at top. doh!
3452             $y_pixels =  $this->plot_origin_y - log10($y_world) * $this->yscale ;
3453         } else {
3454             $y_pixels =  $this->plot_origin_y - $y_world * $this->yscale ;
3455         }
3456         return round($y_pixels);
3457     }
3458
3459     /* A public interface to xtr and ytr. Translates (x,y) in world coordinates
3460      * to (x,y) in device coordinates and returns them as an array.
3461      * Usage is: list($x_pixel, $y_pixel) = $plot->GetDeviceXY($x_world, $y_world)
3462      */
3463     function GetDeviceXY($x_world, $y_world)
3464     {
3465         if (!isset($this->xscale)) {
3466             return $this->PrintError("GetDeviceXY() was called before translation factors were calculated");
3467         }
3468         return array($this->xtr($x_world), $this->ytr($y_world));
3469     }
3470
3471     /*
3472      * Calculate tick parameters: Start, end, and delta values. This is used
3473      * by both DrawXTicks() and DrawYTicks().
3474      * This currently uses the same simplistic method previously used by
3475      * PHPlot (basically just range/10), but splitting this out into its
3476      * own function is the first step in replacing the method.
3477      * This is also used by CalcMaxTickSize() for CalcMargins().
3478      *
3479      *   $which : 'x' or 'y' : Which tick parameters to calculate
3480      *
3481      * Returns an array of 3 elements: tick_start, tick_end, tick_step
3482      */
3483     protected function CalcTicks($which)
3484     {
3485         if ($which == 'x') {
3486             $num_ticks = $this->num_x_ticks;
3487             $tick_inc = $this->x_tick_inc;
3488             $data_max = $this->plot_max_x;
3489             $data_min = $this->plot_min_x;
3490             $skip_lo = $this->skip_left_tick;
3491             $skip_hi = $this->skip_right_tick;
3492         } elseif ($which == 'y') {
3493             $num_ticks = $this->num_y_ticks;
3494             $tick_inc = $this->y_tick_inc;
3495             $data_max = $this->plot_max_y;
3496             $data_min = $this->plot_min_y;
3497             $skip_lo = $this->skip_bottom_tick;
3498             $skip_hi = $this->skip_top_tick;
3499         } else {
3500           return $this->PrintError("CalcTicks: Invalid usage ($which)");
3501         }
3502
3503         if (!empty($tick_inc)) {
3504             $tick_step = $tick_inc;
3505         } elseif (!empty($num_ticks)) {
3506             $tick_step = ($data_max - $data_min) / $num_ticks;
3507         } else {
3508             $tick_step = ($data_max - $data_min) / 10;
3509         }
3510
3511         // NOTE: When working with floats, because of approximations when adding $tick_step,
3512         // the value may not quite reach the end, or may exceed it very slightly.
3513         // So apply a "fudge" factor.
3514         $tick_start = (double)$data_min;
3515         $tick_end = (double)$data_max + ($data_max - $data_min) / 10000.0;
3516
3517         if ($skip_lo)
3518             $tick_start += $tick_step;
3519
3520         if ($skip_hi)
3521             $tick_end -= $tick_step;
3522
3523         return array($tick_start, $tick_end, $tick_step);
3524     }
3525
3526     /*
3527      * Calculate the size of the biggest tick label. This is used by CalcMargins().
3528      * For 'x' ticks, it returns the height . For 'y' ticks, it returns the width.
3529      * This means height along Y, or width along X - not relative to the text angle.
3530      * That is what we need to calculate the needed margin space.
3531      * (Previous versions of PHPlot estimated this, using the maximum X or Y value,
3532      * or maybe the longest string. That doesn't work. -10 is longer than 9, etc.
3533      * So this gets the actual size of each label, slow as that may be.
3534      */
3535     protected function CalcMaxTickLabelSize($which)
3536     {
3537         list($tick_start, $tick_end, $tick_step) = $this->CalcTicks($which);
3538
3539         if ($which == 'x') {
3540           $font = $this->fonts['x_label'];
3541           $angle = $this->x_label_angle;
3542         } elseif ($which == 'y') {
3543           $font = $this->fonts['y_label'];
3544           $angle = $this->y_label_angle;
3545         } else {
3546           return $this->PrintError("CalcMaxTickLabelSize: Invalid usage ($which)");
3547         }
3548
3549         $max_width = 0;
3550         $max_height = 0;
3551
3552         // Loop over ticks, same as DrawXTicks and DrawYTicks:
3553         // Avoid cumulative round-off errors from $val += $delta
3554         $n = 0;
3555         $tick_val = $tick_start;
3556         while ($tick_val <= $tick_end) {
3557             $tick_label = $this->FormatLabel($which, $tick_val);
3558             list($width, $height) = $this->SizeText($font, $angle, $tick_label);
3559             if ($width > $max_width) $max_width = $width;
3560             if ($height > $max_height) $max_height = $height;
3561             $tick_val = $tick_start + ++$n * $tick_step;
3562         }
3563         if ($this->GetCallback('debug_scale')) {
3564             $this->DoCallback('debug_scale', __FUNCTION__, array(
3565                 'which' => $which, 'height' => $max_height, 'width' => $max_width));
3566         }
3567
3568         if ($which == 'x')
3569             return $max_height;
3570         return $max_width;
3571     }
3572
3573     /*
3574      * Calculate the size of the biggest data label. This is used by CalcMargins().
3575      * For $which='x', it returns the height of labels along the top or bottom.
3576      * For $which='y', it returns the width of labels along the left or right sides.
3577      * There is only one set of data labels (the first position in each data record).
3578      * They normally go along the top or bottom (or both). If the data type indicates
3579      * X/Y swapping (which is used for horizontal bar charts), the data labels go
3580      * along the sides instead. So CalcMaxDataLabelSize('x') returns 0 if the
3581      * data is X/Y swapped, and CalcMaxDataLabelSize('y') returns 0 if the data is
3582      * is not X/Y swapped.
3583      */
3584     protected function CalcMaxDataLabelSize($which = 'x')
3585     {
3586         if ($which == 'x') {
3587           if ($this->datatype_swapped_xy)
3588               return 0; // Shortcut: labels aren't on top/bottom.
3589           $font = $this->fonts['x_label'];
3590           $angle = $this->x_data_label_angle;
3591           $format_code = 'xd';
3592         } elseif ($which == 'y') {
3593           if (!$this->datatype_swapped_xy)
3594               return 0; // Shortcut: labels aren't on left/right.
3595           $font = $this->fonts['y_label'];
3596           $angle = $this->y_data_label_angle;
3597           $format_code = 'yd';
3598         } else {
3599           return $this->PrintError("CalcMaxDataLabelSize: Invalid usage ($which)");
3600         }
3601         $max_width = 0;
3602         $max_height = 0;
3603
3604         // Loop over all data labels and find the biggest:
3605         for ($i = 0; $i < $this->num_data_rows; $i++) {
3606             $label = $this->FormatLabel($format_code, $this->data[$i][0]);
3607             list($width, $height) = $this->SizeText($font, $angle, $label);
3608             if ($width > $max_width) $max_width = $width;
3609             if ($height > $max_height) $max_height = $height;
3610         }
3611         if ($this->GetCallback('debug_scale')) {
3612             $this->DoCallback('debug_scale', __FUNCTION__, array(
3613                 'height' => $max_height, 'width' => $max_width));
3614         }
3615
3616         if ($this->datatype_swapped_xy)
3617             return $max_width;
3618         return $max_height;
3619     }
3620
3621     /*
3622      * Set grid control defaults.
3623      * X grid defaults off, Y grid defaults on, except the reverse is true
3624      * with swapped graphs such as horizontal bars.
3625      */
3626     protected function CalcGridSettings()
3627     {
3628         if (!isset($this->draw_x_grid))
3629             $this->draw_x_grid = $this->datatype_swapped_xy;
3630         if (!isset($this->draw_y_grid))
3631             $this->draw_y_grid = !$this->datatype_swapped_xy;
3632     }
3633
3634     /*
3635      * Helper for CheckLabels() - determine if there are any non-empty labels.
3636      * Returns True if all data labels are empty, else False.
3637      */
3638     protected function CheckLabelsAllEmpty()
3639     {
3640         for ($i = 0; $i < $this->num_data_rows; $i++)
3641             if ($this->data[$i][0] !== '') return FALSE;
3642         return TRUE;
3643     }
3644
3645     /*
3646      * Check and set label parameters. This handles deferred processing for label
3647      * positioning and other label-related parameters.
3648      *   Copy label_format from 'x' to 'xd', and 'y' to 'yd', if not already set.
3649      *   Set x_data_label_angle from x_label_angle, if not already set.
3650      *   Apply defaults to X and Y tick and data label positions.
3651      * Note: the label strings in the data array are used as X data labels in
3652      * the normal case, but as Y data labels in the swapped X/Y case.
3653      */
3654     protected function CheckLabels()
3655     {
3656         // The X and Y data labels are formatted the same as X and Y tick labels,
3657         // unless overridden. Check and apply defaults for FormatLabel here:
3658         if (empty($this->label_format['xd']) && !empty($this->label_format['x']))
3659             $this->label_format['xd'] = $this->label_format['x'];
3660         if (empty($this->label_format['yd']) && !empty($this->label_format['y']))
3661             $this->label_format['yd'] = $this->label_format['y'];
3662
3663         // The X tick label angle setting controls X data label angles too,
3664         // unless overridden. Check and apply the default here:
3665         if (!isset($this->x_data_label_angle))
3666             $this->x_data_label_angle = $this->x_label_angle;
3667         // Note: Y data label angle defaults to zero, unlike X,
3668         // for compatibility with older releases.
3669
3670         // X Label position fixups, for x_data_label_pos and x_tick_label_pos:
3671         if ($this->datatype_swapped_xy) {
3672             // Just apply defaults - there is no position conflict for X labels.
3673             if (!isset($this->x_tick_label_pos))
3674                 $this->x_tick_label_pos = 'plotdown';
3675             if (!isset($this->x_data_label_pos))
3676                 $this->x_data_label_pos = 'none';
3677         } else {
3678             // Apply defaults but do not allow conflict between tick and data labels.
3679             if (isset($this->x_data_label_pos)) {
3680                 if (!isset($this->x_tick_label_pos)) {
3681                     // Case: data_label_pos is set, tick_label_pos needs a default:
3682                     if ($this->x_data_label_pos == 'none')
3683                         $this->x_tick_label_pos = 'plotdown';
3684                     else
3685                         $this->x_tick_label_pos = 'none';
3686                 }
3687             } elseif (isset($this->x_tick_label_pos)) {
3688                 // Case: tick_label_pos is set, data_label_pos needs a default:
3689                 if ($this->x_tick_label_pos == 'none')
3690                     $this->x_data_label_pos = 'plotdown';
3691                 else
3692                     $this->x_data_label_pos = 'none';
3693             } else {
3694                 // Case: Neither tick_label_pos nor data_label_pos is set.
3695                 // We do not want them to be both on (as PHPlot used to do in this case).
3696                 // Turn on data labels if any were supplied, else tick labels.
3697                 if ($this->CheckLabelsAllEmpty()) {
3698                     $this->x_data_label_pos = 'none';
3699                     $this->x_tick_label_pos = 'plotdown';
3700                 } else {
3701                     $this->x_data_label_pos = 'plotdown';
3702                     $this->x_tick_label_pos = 'none';
3703                 }
3704             }
3705         }
3706
3707         // Y Label position fixups, for y_data_label_pos and y_tick_label_pos:
3708         if (!$this->datatype_swapped_xy) {
3709             // Just apply defaults - there is no position conflict.
3710             if (!isset($this->y_tick_label_pos))
3711                 $this->y_tick_label_pos = 'plotleft';
3712             if (!isset($this->y_data_label_pos))
3713                 $this->y_data_label_pos = 'none';
3714         } else {
3715             // Apply defaults but do not allow conflict between tick and data labels.
3716             if (isset($this->y_data_label_pos)) {
3717                 if (!isset($this->y_tick_label_pos)) {
3718                     // Case: data_label_pos is set, tick_label_pos needs a default:
3719                     if ($this->y_data_label_pos == 'none')
3720                         $this->y_tick_label_pos = 'plotleft';
3721                     else
3722                         $this->y_tick_label_pos = 'none';
3723                 }
3724             } elseif (isset($this->y_tick_label_pos)) {
3725                 // Case: tick_label_pos is set, data_label_pos needs a default:
3726                 if ($this->y_tick_label_pos == 'none')
3727                     $this->y_data_label_pos = 'plotleft';
3728                 else
3729                     $this->y_data_label_pos = 'none';
3730             } else {
3731                 // Case: Neither tick_label_pos nor data_label_pos is set.
3732                 // Turn on data labels if any were supplied, else tick labels.
3733                 if ($this->CheckLabelsAllEmpty()) {
3734                     $this->y_data_label_pos = 'none';
3735                     $this->y_tick_label_pos = 'plotleft';
3736                 } else {
3737                     $this->y_data_label_pos = 'plotleft';
3738                     $this->y_tick_label_pos = 'none';
3739                 }
3740             }
3741         }
3742         return TRUE;
3743     }
3744
3745     /*
3746      * Formats a tick or data label.
3747      *    which_pos - 'x', 'xd', 'y', or 'yd', selects formatting controls.
3748      *        x, y are for tick labels; xd, yd are for data labels.
3749      *    which_lab - String to format as a label.
3750      * Credits: Time formatting suggested by Marlin Viss
3751      *          Custom formatting suggested by zer0x333
3752      * Notes:
3753      *   Type 'title' is obsolete and retained for compatibility.
3754      *   Class variable 'data_units_text' is retained as a suffix for 'data' type formatting for
3755      *      backward compatibility. Since there was never a function/method to set it, there
3756      *      could be somebody out there who sets it directly in the object.
3757      */
3758     protected function FormatLabel($which_pos, $which_lab)
3759     {
3760         // Assign a reference shortcut to the label format controls.
3761         // Note CheckLabels() made sure the 'xd' and 'yd' arrays are set.
3762         $format =& $this->label_format[$which_pos];
3763
3764         // Don't format empty strings (especially as time or numbers), or if no type was set.
3765         if ($which_lab !== '' && !empty($format['type'])) {
3766             switch ($format['type']) {
3767             case 'title':  // Note: This is obsolete
3768                 $which_lab = @ $this->data[$which_lab][0];
3769                 break;
3770             case 'data':
3771                 $which_lab = $format['prefix']
3772                            . $this->number_format($which_lab, $format['precision'])
3773                            . $this->data_units_text  // Obsolete
3774                            . $format['suffix'];
3775                 break;
3776             case 'time':
3777                 $which_lab = strftime($format['time_format'], $which_lab);
3778                 break;
3779             case 'printf':
3780                 $which_lab = sprintf($format['printf_format'], $which_lab);
3781                 break;
3782             case 'custom':
3783                 $which_lab = call_user_func($format['custom_callback'], $which_lab, $format['custom_arg']);
3784                 break;
3785
3786             }
3787         }
3788         return $which_lab;
3789     }
3790
3791 /////////////////////////////////////////////
3792 ///////////////                         TICKS
3793 /////////////////////////////////////////////
3794
3795     /*
3796      * Set the step (interval) between X ticks.
3797      * Use either this or SetNumXTicks(), not both, to control the X tick marks.
3798      */
3799     function SetXTickIncrement($which_ti='')
3800     {
3801         $this->x_tick_inc = $which_ti;
3802         if (!empty($which_ti)) {
3803             $this->num_x_ticks = '';
3804         }
3805         return TRUE;
3806     }
3807
3808     /*
3809      * Set the step (interval) between Y ticks.
3810      * Use either this or SetNumYTicks(), not both, to control the Y tick marks.
3811      */
3812     function SetYTickIncrement($which_ti='')
3813     {
3814         $this->y_tick_inc = $which_ti;
3815         if (!empty($which_ti)) {
3816             $this->num_y_ticks = '';
3817         }
3818         return TRUE;
3819     }
3820
3821     /*
3822      * Set the number of X tick marks.
3823      * Use either this or SetXTickIncrement(), not both, to control the X tick marks.
3824      */
3825     function SetNumXTicks($which_nt)
3826     {
3827         $this->num_x_ticks = $which_nt;
3828         if (!empty($which_nt)) {
3829             $this->x_tick_inc = '';
3830         }
3831         return TRUE;
3832     }
3833
3834     /*
3835      * Set the number of Y tick marks.
3836      * Use either this or SetYTickIncrement(), not both, to control the Y tick marks.
3837      */
3838     function SetNumYTicks($which_nt)
3839     {
3840         $this->num_y_ticks = $which_nt;
3841         if (!empty($which_nt)) {
3842             $this->y_tick_inc = '';  //either use num_y_ticks or y_tick_inc, not both
3843         }
3844         return TRUE;
3845     }
3846
3847     /*
3848      * Set the position for the X tick marks.
3849      * These can be above the plot, below, both positions, at the X axis, or suppressed.
3850      */
3851     function SetXTickPos($which_tp)
3852     {
3853         $this->x_tick_pos = $this->CheckOption($which_tp, 'plotdown, plotup, both, xaxis, none',
3854                                                __FUNCTION__);
3855         return (boolean)$this->x_tick_pos;
3856     }
3857
3858     /*
3859      * Set the position for the Y tick marks.
3860      * These can be left of the plot, right, both positions, at the Y axis, or suppressed.
3861      */
3862     function SetYTickPos($which_tp)
3863     {
3864         $this->y_tick_pos = $this->CheckOption($which_tp, 'plotleft, plotright, both, yaxis, none',
3865                                               __FUNCTION__);
3866         return (boolean)$this->y_tick_pos;
3867     }
3868
3869     /*
3870      * Skip the top-most Y axis tick mark and label if $skip is true.
3871      */
3872     function SetSkipTopTick($skip)
3873     {
3874         $this->skip_top_tick = (bool)$skip;
3875         return TRUE;
3876     }
3877
3878     /*
3879      * Skip the bottom-most Y axis tick mark and label if $skip is true.
3880      */
3881     function SetSkipBottomTick($skip)
3882     {
3883         $this->skip_bottom_tick = (bool)$skip;
3884         return TRUE;
3885     }
3886
3887     /*
3888      * Skip the left-most X axis tick mark and label if $skip is true.
3889      */
3890     function SetSkipLeftTick($skip)
3891     {
3892         $this->skip_left_tick = (bool)$skip;
3893         return TRUE;
3894     }
3895
3896     /*
3897      * Skip the right-most X axis tick mark and label if $skip is true.
3898      */
3899     function SetSkipRightTick($skip)
3900     {
3901         $this->skip_right_tick = (bool)$skip;
3902         return TRUE;
3903     }
3904
3905     /*
3906      * Set the outer length of X tick marks to $which_xln pixels.
3907      * This is the part of the tick mark that sticks out from the plot area.
3908      */
3909     function SetXTickLength($which_xln)
3910     {
3911         $this->x_tick_length = $which_xln;
3912         return TRUE;
3913     }
3914
3915     /*
3916      * Set the outer length of Y tick marks to $which_yln pixels.
3917      * This is the part of the tick mark that sticks out from the plot area.
3918      */
3919     function SetYTickLength($which_yln)
3920     {
3921         $this->y_tick_length = $which_yln;
3922         return TRUE;
3923     }
3924
3925     /*
3926      * Set the crossing length of X tick marks to $which_xc pixels.
3927      * This is the part of the tick mark that sticks into the plot area.
3928      */
3929     function SetXTickCrossing($which_xc)
3930     {
3931         $this->x_tick_cross = $which_xc;
3932         return TRUE;
3933     }
3934
3935     /*
3936      * Set the crossing length of Y tick marks to $which_yc pixels.
3937      * This is the part of the tick mark that sticks into the plot area.
3938      */
3939     function SetYTickCrossing($which_yc)
3940     {
3941         $this->y_tick_cross = $which_yc;
3942         return TRUE;
3943     }
3944
3945 /////////////////////////////////////////////
3946 ////////////////////          GENERIC DRAWING
3947 /////////////////////////////////////////////
3948
3949     /*
3950      * Fill the image background, with a tiled image file or solid color.
3951      */
3952     protected function DrawBackground()
3953     {
3954         // Don't draw this twice if drawing two plots on one image
3955         if (! $this->background_done) {
3956             if (isset($this->bgimg)) {    // If bgimg is defined, use it
3957                 $this->tile_img($this->bgimg, 0, 0, $this->image_width, $this->image_height, $this->bgmode);
3958             } else {                        // Else use solid color
3959                 ImageFilledRectangle($this->img, 0, 0, $this->image_width, $this->image_height,
3960                                      $this->ndx_bg_color);
3961             }
3962             $this->background_done = TRUE;
3963         }
3964         return TRUE;
3965     }
3966
3967     /*
3968      * Fill the plot area background, with a tiled image file or solid color.
3969      */
3970     protected function DrawPlotAreaBackground()
3971     {
3972         if (isset($this->plotbgimg)) {
3973             $this->tile_img($this->plotbgimg, $this->plot_area[0], $this->plot_area[1],
3974                             $this->plot_area_width, $this->plot_area_height, $this->plotbgmode);
3975         } elseif ($this->draw_plot_area_background) {
3976             ImageFilledRectangle($this->img, $this->plot_area[0], $this->plot_area[1],
3977                                  $this->plot_area[2], $this->plot_area[3], $this->ndx_plot_bg_color);
3978         }
3979         return TRUE;
3980     }
3981
3982     /*
3983      * Tiles an image at some given coordinates.
3984      *   $file : Filename of the picture to be used as tile.
3985      *   $xorig : X device coordinate of where the tile is to begin.
3986      *   $yorig : Y device coordinate of where the tile is to begin.
3987      *   $width : Width of the area to be tiled.
3988      *   $height : Height of the area to be tiled.
3989      *   $mode : Tiling mode. One of 'centeredtile', 'tile', 'scale'.
3990      */
3991     protected function tile_img($file, $xorig, $yorig, $width, $height, $mode)
3992     {
3993         $im = $this->GetImage($file, $tile_width, $tile_height);
3994         if (!$im)
3995             return FALSE;  // GetImage already produced an error message.
3996
3997         if ($mode == 'scale') {
3998             imagecopyresampled($this->img, $im, $xorig, $yorig, 0, 0, $width, $height,
3999                                $tile_width, $tile_height);
4000             return TRUE;
4001         }
4002
4003         if ($mode == 'centeredtile') {
4004             $x0 = - floor($tile_width/2);   // Make the tile look better
4005             $y0 = - floor($tile_height/2);
4006         } else {      // Accept anything else as $mode == 'tile'
4007             $x0 = 0;
4008             $y0 = 0;
4009         }
4010
4011         // Draw the tile onto a temporary image first.
4012         $tmp = imagecreate($width, $height);
4013         if (! $tmp)
4014             return $this->PrintError('tile_img(): Could not create image resource.');
4015
4016         for ($x = $x0; $x < $width; $x += $tile_width)
4017             for ($y = $y0; $y < $height; $y += $tile_height)
4018                 imagecopy($tmp, $im, $x, $y, 0, 0, $tile_width, $tile_height);
4019
4020         // Copy the temporary image onto the final one.
4021         imagecopy($this->img, $tmp, $xorig, $yorig, 0,0, $width, $height);
4022
4023         // Free resources
4024         imagedestroy($tmp);
4025         imagedestroy($im);
4026
4027         return TRUE;
4028     }
4029
4030     /*
4031      * Return the image border width.
4032      * This is used by CalcMargins() and DrawImageBorder().
4033      */
4034     protected function GetImageBorderWidth()
4035     {
4036         if ($this->image_border_type == 'none')
4037             return 0; // No border
4038         if (!empty($this->image_border_width))
4039             return $this->image_border_width; // Specified border width
4040         if ($this->image_border_type == 'raised')
4041             return 2; // Default for raised border is 2 pixels.
4042         return 1; // Default for other border types is 1 pixel.
4043     }
4044
4045     /*
4046      * Draws a border around the final image.
4047      * Note: 'plain' draws a flat border using the dark shade of the border color.
4048      * This probably should have been written to use the actual border color, but
4049      * it is too late to fix it without changing plot appearances. Therefore a
4050      * new type 'solid' was added to use the SetImageBorderColor color.
4051      */
4052     protected function DrawImageBorder()
4053     {
4054         if ($this->image_border_type == 'none')
4055             return TRUE; // Early test for default case.
4056         $width = $this->GetImageBorderWidth();
4057         $color1 = $this->ndx_i_border;
4058         $color2 = $this->ndx_i_border_dark;
4059         $ex = $this->image_width - 1;
4060         $ey = $this->image_height - 1;
4061         switch ($this->image_border_type) {
4062         case 'raised':
4063             // Top and left lines use border color, right and bottom use the darker shade.
4064             // Drawing order matters in the upper right and lower left corners.
4065             for ($i = 0; $i < $width; $i++, $ex--, $ey--) {
4066                 imageline($this->img, $i,  $i,  $ex, $i,  $color1); // Top
4067                 imageline($this->img, $ex, $i,  $ex, $ey, $color2); // Right
4068                 imageline($this->img, $i,  $i,  $i,  $ey, $color1); // Left
4069                 imageline($this->img, $i,  $ey, $ex, $ey, $color2); // Bottom
4070             }
4071             break;
4072         case 'plain': // See note above re colors
4073             $color1 = $color2;
4074             // Fall through
4075         case 'solid':
4076             for ($i = 0; $i < $width; $i++, $ex--, $ey--) {
4077                 imagerectangle($this->img, $i, $i, $ex, $ey, $color1);
4078             }
4079             break;
4080         default:
4081             return $this->PrintError(
4082                           "DrawImageBorder(): unknown image_border_type: '$this->image_border_type'");
4083         }
4084         return TRUE;
4085     }
4086
4087     /*
4088      * Draws the main title on the graph.
4089      * The title must not be drawn more than once (in the case of multiple plots
4090      * on the image), because TTF text antialiasing makes it look bad.
4091      */
4092     protected function DrawTitle()
4093     {
4094         if (isset($this->title_done) || $this->title_txt === '')
4095             return TRUE;
4096
4097         // Center of the image:
4098         $xpos = $this->image_width / 2;
4099
4100         // Place it at almost at the top
4101         $ypos = $this->title_offset;
4102
4103         $this->DrawText($this->fonts['title'], 0, $xpos, $ypos,
4104                         $this->ndx_title_color, $this->title_txt, 'center', 'top');
4105
4106         $this->title_done = TRUE;
4107         return TRUE;
4108     }
4109
4110     /*
4111      * Draws the X-Axis Title
4112      */
4113     protected function DrawXTitle()
4114     {
4115         if ($this->x_title_pos == 'none')
4116             return TRUE;
4117
4118         // Center of the plot
4119         $xpos = ($this->plot_area[2] + $this->plot_area[0]) / 2;
4120
4121         // Upper title
4122         if ($this->x_title_pos == 'plotup' || $this->x_title_pos == 'both') {
4123             $ypos = $this->plot_area[1] - $this->x_title_top_offset;
4124             $this->DrawText($this->fonts['x_title'], 0, $xpos, $ypos, $this->ndx_x_title_color,
4125                             $this->x_title_txt, 'center', 'bottom');
4126         }
4127         // Lower title
4128         if ($this->x_title_pos == 'plotdown' || $this->x_title_pos == 'both') {
4129             $ypos = $this->plot_area[3] + $this->x_title_bot_offset;
4130             $this->DrawText($this->fonts['x_title'], 0, $xpos, $ypos, $this->ndx_x_title_color,
4131                             $this->x_title_txt, 'center', 'top');
4132         }
4133         return TRUE;
4134     }
4135
4136     /*
4137      * Draws the Y-Axis Title
4138      */
4139     protected function DrawYTitle()
4140     {
4141         if ($this->y_title_pos == 'none')
4142             return TRUE;
4143
4144         // Center the title vertically to the plot area
4145         $ypos = ($this->plot_area[3] + $this->plot_area[1]) / 2;
4146
4147         if ($this->y_title_pos == 'plotleft' || $this->y_title_pos == 'both') {
4148             $xpos = $this->plot_area[0] - $this->y_title_left_offset;
4149             $this->DrawText($this->fonts['y_title'], 90, $xpos, $ypos, $this->ndx_y_title_color,
4150                             $this->y_title_txt, 'right', 'center');
4151         }
4152         if ($this->y_title_pos == 'plotright' || $this->y_title_pos == 'both') {
4153             $xpos = $this->plot_area[2] + $this->y_title_right_offset;
4154             $this->DrawText($this->fonts['y_title'], 90, $xpos, $ypos, $this->ndx_y_title_color,
4155                             $this->y_title_txt, 'left', 'center');
4156         }
4157
4158         return TRUE;
4159     }
4160
4161     /*
4162      * Draw the X axis, including ticks and labels, and X (vertical) grid lines.
4163      */
4164     protected function DrawXAxis()
4165     {
4166         // Draw ticks, labels and grid
4167         $this->DrawXTicks();
4168
4169         //Draw X Axis at Y = x_axis_y_pixels
4170         ImageLine($this->img, $this->plot_area[0]+1, $this->x_axis_y_pixels,
4171                   $this->plot_area[2]-1, $this->x_axis_y_pixels, $this->ndx_grid_color);
4172
4173         return TRUE;
4174     }
4175
4176     /*
4177      * Draw the Y axis, including ticks and labels, and Y (horizontal) grid lines.
4178      * Horizontal grid lines overwrite horizontal axis with y=0, so call this first, then DrawXAxis()
4179      */
4180     protected function DrawYAxis()
4181     {
4182         // Draw ticks, labels and grid, if any
4183         $this->DrawYTicks();
4184
4185         // Draw Y axis at X = y_axis_x_pixels
4186         ImageLine($this->img, $this->y_axis_x_pixels, $this->plot_area[1],
4187                   $this->y_axis_x_pixels, $this->plot_area[3], $this->ndx_grid_color);
4188
4189         return TRUE;
4190     }
4191
4192     /*
4193      * Draw one X tick mark and its tick label.
4194      *   $which_xlab : Formatted X value for the label.
4195      *   $which_xpix : X device coordinate for this tick mark.
4196      */
4197     protected function DrawXTick($which_xlab, $which_xpix)
4198     {
4199         // Ticks on X axis
4200         if ($this->x_tick_pos == 'xaxis') {
4201             ImageLine($this->img, $which_xpix, $this->x_axis_y_pixels - $this->x_tick_cross,
4202                       $which_xpix, $this->x_axis_y_pixels + $this->x_tick_length, $this->ndx_tick_color);
4203         }
4204
4205         // Ticks on top of the Plot Area
4206         if ($this->x_tick_pos == 'plotup' || $this->x_tick_pos == 'both') {
4207             ImageLine($this->img, $which_xpix, $this->plot_area[1] - $this->x_tick_length,
4208                       $which_xpix, $this->plot_area[1] + $this->x_tick_cross, $this->ndx_tick_color);
4209         }
4210
4211         // Ticks on bottom of Plot Area
4212         if ($this->x_tick_pos == 'plotdown' || $this->x_tick_pos == 'both') {
4213             ImageLine($this->img, $which_xpix, $this->plot_area[3] + $this->x_tick_length,
4214                       $which_xpix, $this->plot_area[3] - $this->x_tick_cross, $this->ndx_tick_color);
4215         }
4216
4217         // Label on X axis
4218         if ($this->x_tick_label_pos == 'xaxis') {
4219             $this->DrawText($this->fonts['x_label'], $this->x_label_angle,
4220                             $which_xpix, $this->x_axis_y_pixels + $this->x_label_axis_offset,
4221                             $this->ndx_text_color, $which_xlab, 'center', 'top');
4222         }
4223
4224         // Label on top of the Plot Area
4225         if ($this->x_tick_label_pos == 'plotup' || $this->x_tick_label_pos == 'both') {
4226             $this->DrawText($this->fonts['x_label'], $this->x_label_angle,
4227                             $which_xpix, $this->plot_area[1] - $this->x_label_top_offset,
4228                             $this->ndx_text_color, $which_xlab, 'center', 'bottom');
4229         }
4230
4231         // Label on bottom of the Plot Area
4232         if ($this->x_tick_label_pos == 'plotdown' || $this->x_tick_label_pos == 'both') {
4233             $this->DrawText($this->fonts['x_label'], $this->x_label_angle,
4234                             $which_xpix, $this->plot_area[3] + $this->x_label_bot_offset,
4235                             $this->ndx_text_color, $which_xlab, 'center', 'top');
4236         }
4237         return TRUE;
4238     }
4239
4240     /*
4241      * Draw one Y tick mark and its tick label. Called from DrawYTicks() and DrawXAxis()
4242      *   $which_ylab : Formatted Y value for the label.
4243      *   $which_ypix : Y device coordinate for this tick mark.
4244      */
4245     protected function DrawYTick($which_ylab, $which_ypix)
4246     {
4247         // Ticks on Y axis
4248         if ($this->y_tick_pos == 'yaxis') {
4249             ImageLine($this->img, $this->y_axis_x_pixels - $this->y_tick_length, $which_ypix,
4250                       $this->y_axis_x_pixels + $this->y_tick_cross, $which_ypix, $this->ndx_tick_color);
4251         }
4252
4253         // Ticks to the left of the Plot Area
4254         if (($this->y_tick_pos == 'plotleft') || ($this->y_tick_pos == 'both') ) {
4255             ImageLine($this->img, $this->plot_area[0] - $this->y_tick_length, $which_ypix,
4256                       $this->plot_area[0] + $this->y_tick_cross, $which_ypix, $this->ndx_tick_color);
4257         }
4258
4259         // Ticks to the right of the Plot Area
4260         if (($this->y_tick_pos == 'plotright') || ($this->y_tick_pos == 'both') ) {
4261             ImageLine($this->img, $this->plot_area[2] + $this->y_tick_length, $which_ypix,
4262                       $this->plot_area[2] - $this->y_tick_cross, $which_ypix, $this->ndx_tick_color);
4263         }
4264
4265         // Labels on Y axis
4266         if ($this->y_tick_label_pos == 'yaxis') {
4267             $this->DrawText($this->fonts['y_label'], $this->y_label_angle,
4268                             $this->y_axis_x_pixels - $this->y_label_axis_offset, $which_ypix,
4269                             $this->ndx_text_color, $which_ylab, 'right', 'center');
4270         }
4271
4272         // Labels to the left of the plot area
4273         if ($this->y_tick_label_pos == 'plotleft' || $this->y_tick_label_pos == 'both') {
4274             $this->DrawText($this->fonts['y_label'], $this->y_label_angle,
4275                             $this->plot_area[0] - $this->y_label_left_offset, $which_ypix,
4276                             $this->ndx_text_color, $which_ylab, 'right', 'center');
4277         }
4278         // Labels to the right of the plot area
4279         if ($this->y_tick_label_pos == 'plotright' || $this->y_tick_label_pos == 'both') {
4280             $this->DrawText($this->fonts['y_label'], $this->y_label_angle,
4281                             $this->plot_area[2] + $this->y_label_right_offset, $which_ypix,
4282                             $this->ndx_text_color, $which_ylab, 'left', 'center');
4283         }
4284         return TRUE;
4285     }
4286
4287     /*
4288      * Draws Grid, Ticks and Tick Labels along X-Axis
4289      * Ticks and tick labels can be down of plot only, up of plot only,
4290      * both on up and down of plot, or crossing a user defined X-axis
4291      *
4292      * Original vertical code submitted by Marlin Viss
4293      */
4294     protected function DrawXTicks()
4295     {
4296         // Sets the line style for IMG_COLOR_STYLED lines (grid)
4297         if ($this->dashed_grid) {
4298             $this->SetDashedStyle($this->ndx_light_grid_color);
4299             $style = IMG_COLOR_STYLED;
4300         } else {
4301             $style = $this->ndx_light_grid_color;
4302         }
4303
4304         // Calculate the tick start, end, and step:
4305         list($x_start, $x_end, $delta_x) = $this->CalcTicks('x');
4306
4307         // Loop, avoiding cumulative round-off errors from $x_tmp += $delta_x
4308         $n = 0;
4309         $x_tmp = $x_start;
4310         while ($x_tmp <= $x_end) {
4311             $xlab = $this->FormatLabel('x', $x_tmp);
4312             $x_pixels = $this->xtr($x_tmp);
4313
4314             // Vertical grid lines
4315             if ($this->draw_x_grid) {
4316                 ImageLine($this->img, $x_pixels, $this->plot_area[1], $x_pixels, $this->plot_area[3], $style);
4317             }
4318
4319             // Draw tick mark(s)
4320             $this->DrawXTick($xlab, $x_pixels);
4321
4322             // Step to next X, without accumulating error
4323             $x_tmp = $x_start + ++$n * $delta_x;
4324         }
4325         return TRUE;
4326     }
4327
4328     /*
4329      * Draw the grid, ticks, and tick labels along the Y axis.
4330      * Ticks and tick labels can be left of plot only, right of plot only,
4331      * both on the left and right of plot, or crossing a user defined Y-axis
4332      */
4333     protected function DrawYTicks()
4334     {
4335         // Sets the line style for IMG_COLOR_STYLED lines (grid)
4336         if ($this->dashed_grid) {
4337             $this->SetDashedStyle($this->ndx_light_grid_color);
4338             $style = IMG_COLOR_STYLED;
4339         } else {
4340             $style = $this->ndx_light_grid_color;
4341         }
4342
4343         // Calculate the tick start, end, and step:
4344         list($y_start, $y_end, $delta_y) = $this->CalcTicks('y');
4345
4346         // Loop, avoiding cumulative round-off errors from $y_tmp += $delta_y
4347         $n = 0;
4348         $y_tmp = $y_start;
4349         while ($y_tmp <= $y_end) {
4350             $ylab = $this->FormatLabel('y', $y_tmp);
4351             $y_pixels = $this->ytr($y_tmp);
4352
4353             // Horizontal grid line
4354             if ($this->draw_y_grid) {
4355                 ImageLine($this->img, $this->plot_area[0]+1, $y_pixels, $this->plot_area[2]-1,
4356                           $y_pixels, $style);
4357             }
4358
4359             // Draw tick mark(s)
4360             $this->DrawYTick($ylab, $y_pixels);
4361
4362             // Step to next Y, without accumulating error
4363             $y_tmp = $y_start + ++$n * $delta_y;
4364         }
4365         return TRUE;
4366     }
4367
4368     /*
4369      *  Draw a border around the plot area. See SetPlotBorderType.
4370      *  Note: SetPlotBorderType sets plot_border_type to an array, but
4371      *  it won't be an array if it defaults or is set directly (backward compatibility).
4372      */
4373     protected function DrawPlotBorder()
4374     {
4375         $pbt = (array)$this->plot_border_type;
4376         $sides = 0;  // Bitmap: 1=left 2=top 4=right 8=bottom
4377         $map = array('left' => 1, 'plotleft' => 1, 'right' => 4, 'plotright' => 4, 'top' => 2,
4378                       'bottom' => 8, 'both' => 5, 'sides' => 5, 'full' => 15, 'none' => 0);
4379         foreach ($pbt as $option) $sides |= $map[$option];
4380         if ($sides == 15) { // Border on all 4 sides
4381             imagerectangle($this->img, $this->plot_area[0], $this->plot_area[1],
4382                            $this->plot_area[2], $this->plot_area[3], $this->ndx_grid_color);
4383         } else {
4384             if ($sides & 1) // Left
4385                 imageline($this->img, $this->plot_area[0], $this->plot_area[1],
4386                                       $this->plot_area[0], $this->plot_area[3], $this->ndx_grid_color);
4387             if ($sides & 2) // Top
4388                 imageline($this->img, $this->plot_area[0], $this->plot_area[1],
4389                                       $this->plot_area[2], $this->plot_area[1], $this->ndx_grid_color);
4390             if ($sides & 4) // Right
4391                 imageline($this->img, $this->plot_area[2], $this->plot_area[1],
4392                                       $this->plot_area[2], $this->plot_area[3], $this->ndx_grid_color);
4393             if ($sides & 8) // Bottom
4394                 imageline($this->img, $this->plot_area[0], $this->plot_area[3],
4395                                       $this->plot_area[2], $this->plot_area[3], $this->ndx_grid_color);
4396         }
4397         return TRUE;
4398     }
4399
4400     /*
4401      * Draw the data value label associated with a point in the plot.
4402      * This is used for bar and stacked bar charts. These are the labels above,
4403      * to the right, or within the bars, not the axis labels.
4404      *
4405      *    $x_or_y : Specify 'x' or 'y' labels. This selects font, angle, and formatting.
4406      *    $x_world, $y_world : World coordinates of the text (see also x/y_adjustment).
4407      *    $text : The text to draw, after formatting with FormatLabel().
4408      *    $halign, $valign : Selects from 9-point text alignment.
4409      *    $x_adjustment, $y_adjustment : Text position offsets, in device coordinates.
4410      *    $min_width, $min_height : If supplied, suppress the text if it will not fit.
4411      * Returns True, if the text was drawn, or False, if it will not fit.
4412      *
4413      */
4414     protected function DrawDataValueLabel($x_or_y, $x_world, $y_world, $text, $halign, $valign,
4415                       $x_adjustment=0, $y_adjustment=0, $min_width=NULL, $min_height=NULL)
4416     {
4417         if ($x_or_y == 'x') {
4418             $angle = $this->x_data_label_angle;
4419             $font = $this->fonts['x_label'];
4420             $formatted_text = $this->FormatLabel('xd', $text);
4421         } else { // Assumed 'y'
4422             $angle = $this->y_data_label_angle;
4423             $font = $this->fonts['y_label'];
4424             $formatted_text = $this->FormatLabel('yd', $text);
4425         }
4426         $color = $this->ndx_title_color; // Currently this is the same for X and Y labels
4427
4428         // Check to see if the text fits in the available space, if requested.
4429         if (isset($min_width) || isset($min_height)) {
4430             list($width, $height) = $this->SizeText($font, $angle, $formatted_text);
4431             if ((isset($min_width) && ($min_width - $width)  < 2)
4432                 || (isset($min_height) && ($min_height - $height) < 2))
4433                 return FALSE;
4434         }
4435
4436         $this->DrawText($font, $angle, $this->xtr($x_world) + $x_adjustment,
4437                         $this->ytr($y_world) + $y_adjustment,
4438                         $color, $formatted_text, $halign, $valign);
4439         return TRUE;
4440     }
4441
4442     /*
4443      * Draws the data label associated with a point in the plot.
4444      * This is different from x_labels drawn by DrawXTicks() and care
4445      * should be taken not to draw both, as they'd probably overlap.
4446      * Calling of this function in DrawLines(), etc is decided after x_data_label_pos value.
4447      * Leave the last parameter out, to avoid the drawing of vertical lines, no matter
4448      * what the setting is (for plots that need it, like DrawSquared())
4449      */
4450     protected function DrawXDataLabel($xlab, $xpos, $row=FALSE)
4451     {
4452         $xlab = $this->FormatLabel('xd', $xlab);
4453
4454         // Labels below the plot area
4455         if ($this->x_data_label_pos == 'plotdown' || $this->x_data_label_pos == 'both')
4456             $this->DrawText($this->fonts['x_label'], $this->x_data_label_angle,
4457                             $xpos, $this->plot_area[3] + $this->x_label_bot_offset,
4458                             $this->ndx_text_color, $xlab, 'center', 'top');
4459
4460         // Labels above the plot area
4461         if ($this->x_data_label_pos == 'plotup' || $this->x_data_label_pos == 'both')
4462             $this->DrawText($this->fonts['x_label'], $this->x_data_label_angle,
4463                             $xpos, $this->plot_area[1] - $this->x_label_top_offset,
4464                             $this->ndx_text_color, $xlab, 'center', 'bottom');
4465
4466         // $row=0 means this is the first row. $row=FALSE means don't do any rows.
4467         if ($row !== FALSE && $this->draw_x_data_label_lines)
4468             $this->DrawXDataLine($xpos, $row);
4469         return TRUE;
4470     }
4471
4472     /*
4473      * Draw a data label along the Y axis or side.
4474      * This is only used by horizontal bar charts.
4475      */
4476     protected function DrawYDataLabel($ylab, $ypos)
4477     {
4478         $ylab = $this->FormatLabel('yd', $ylab);
4479
4480         // Labels left of the plot area
4481         if ($this->y_data_label_pos == 'plotleft' || $this->y_data_label_pos == 'both')
4482             $this->DrawText($this->fonts['y_label'], $this->y_data_label_angle,
4483                             $this->plot_area[0] - $this->y_label_left_offset, $ypos,
4484                             $this->ndx_text_color, $ylab, 'right', 'center');
4485
4486         // Labels right of the plot area
4487         if ($this->y_data_label_pos == 'plotright' || $this->y_data_label_pos == 'both')
4488             $this->DrawText($this->fonts['y_label'], $this->y_data_label_angle,
4489                             $this->plot_area[2] + $this->y_label_right_offset, $ypos,
4490                             $this->ndx_text_color, $ylab, 'left', 'center');
4491         return TRUE;
4492     }
4493
4494     /*
4495      * Draws Vertical lines from data points up and down.
4496      * Which lines are drawn depends on the value of x_data_label_pos,
4497      * and whether this is at all done or not, on draw_x_data_label_lines
4498      *
4499      *   $xpos : position in pixels of the line.
4500      *   $row : index of the data row being drawn.
4501      */
4502     protected function DrawXDataLine($xpos, $row)
4503     {
4504         // Sets the line style for IMG_COLOR_STYLED lines (grid)
4505         if ($this->dashed_grid) {
4506             $this->SetDashedStyle($this->ndx_light_grid_color);
4507             $style = IMG_COLOR_STYLED;
4508         } else {
4509             $style = $this->ndx_light_grid_color;
4510         }
4511
4512         if ($this->x_data_label_pos == 'both') {
4513             // Lines from the bottom up
4514             ImageLine($this->img, $xpos, $this->plot_area[3], $xpos, $this->plot_area[1], $style);
4515         } elseif ($this->x_data_label_pos == 'plotdown' && isset($this->data_max[$row])) {
4516             // Lines from the bottom of the plot up to the max Y value at this X:
4517             $ypos = $this->ytr($this->data_max[$row]);
4518             ImageLine($this->img, $xpos, $ypos, $xpos, $this->plot_area[3], $style);
4519         } elseif ($this->x_data_label_pos == 'plotup' && isset($this->data_min[$row])) {
4520             // Lines from the top of the plot down to the min Y value at this X:
4521             $ypos = $this->ytr($this->data_min[$row]);
4522             ImageLine($this->img, $xpos, $this->plot_area[1], $xpos, $ypos, $style);
4523         }
4524         return TRUE;
4525     }
4526
4527     /*
4528      * Draws the graph legend
4529      *
4530      * Base code submitted by Marlin Viss
4531      */
4532     protected function DrawLegend()
4533     {
4534         $font = &$this->fonts['legend'];
4535
4536         // Find maximum legend label line width.
4537         $max_width = 0;
4538         foreach ($this->legend as $line) {
4539             list($width, $unused) = $this->SizeText($font, 0, $line);
4540             if ($width > $max_width) $max_width = $width;
4541         }
4542
4543         // Use the font parameters to size the color boxes:
4544         $char_w = $font['width'];
4545         $char_h = $font['height'];
4546         $line_spacing = $this->GetLineSpacing($font);
4547
4548         // Normalize text alignment and colorbox alignment variables:
4549         $text_align = isset($this->legend_text_align) ? $this->legend_text_align : 'right';
4550         $colorbox_align = isset($this->legend_colorbox_align) ? $this->legend_colorbox_align : 'right';
4551
4552         // Sizing parameters:
4553         $v_margin = $char_h/2;                   // Between vertical borders and labels
4554         $dot_height = $char_h + $line_spacing;   // Height of the small colored boxes
4555         // Overall legend box width e.g.: | space colorbox space text space |
4556         // where colorbox and each space are 1 char width.
4557         if ($colorbox_align != 'none') {
4558             $width = $max_width + 4 * $char_w;
4559             $draw_colorbox = TRUE;
4560         } else {
4561             $width = $max_width + 2 * $char_w;
4562             $draw_colorbox = FALSE;
4563         }
4564
4565         //////// Calculate box position
4566         // User-defined position specified?
4567         if ( !isset($this->legend_x_pos) || !isset($this->legend_y_pos)) {
4568             // No, use default
4569             $box_start_x = $this->plot_area[2] - $width - $this->safe_margin;
4570             $box_start_y = $this->plot_area[1] + $this->safe_margin;
4571         } elseif (isset($this->legend_xy_world)) {
4572             // User-defined position in world-coordinates (See SetLegendWorld).
4573             $box_start_x = $this->xtr($this->legend_x_pos);
4574             $box_start_y = $this->ytr($this->legend_y_pos);
4575             unset($this->legend_xy_world);
4576         } else {
4577             // User-defined position in pixel coordinates.
4578             $box_start_x = $this->legend_x_pos;
4579             $box_start_y = $this->legend_y_pos;
4580         }
4581
4582         // Lower right corner
4583         $box_end_y = $box_start_y + $dot_height*(count($this->legend)) + 2*$v_margin;
4584         $box_end_x = $box_start_x + $width;
4585
4586         // Draw outer box
4587         ImageFilledRectangle($this->img, $box_start_x, $box_start_y, $box_end_x, $box_end_y,
4588                              $this->ndx_bg_color);
4589         ImageRectangle($this->img, $box_start_x, $box_start_y, $box_end_x, $box_end_y,
4590                        $this->ndx_grid_color);
4591
4592         $color_index = 0;
4593         $max_color_index = count($this->ndx_data_colors) - 1;
4594
4595         // Calculate color box and text horizontal positions.
4596         if (!$draw_colorbox) {
4597             if ($text_align == 'left')
4598                 $x_pos = $box_start_x + $char_w;
4599             else
4600                 $x_pos = $box_end_x - $char_w;
4601         } elseif ($colorbox_align == 'left') {
4602             $dot_left_x = $box_start_x + $char_w;
4603             $dot_right_x = $dot_left_x + $char_w;
4604             if ($text_align == 'left')
4605                 $x_pos = $dot_left_x + 2 * $char_w;
4606             else
4607                 $x_pos = $box_end_x - $char_w;
4608         } else {
4609             $dot_left_x = $box_end_x - 2 * $char_w;
4610             $dot_right_x = $dot_left_x + $char_w;
4611             if ($text_align == 'left')
4612                 $x_pos = $box_start_x + $char_w;
4613             else
4614                 $x_pos = $dot_left_x - $char_w;
4615         }
4616
4617         // Calculate starting position of first text line.  The bottom of each color box
4618         // lines up with the bottom (baseline) of its text line.
4619         $y_pos = $box_start_y + $v_margin + $dot_height;
4620
4621         foreach ($this->legend as $leg) {
4622             // Draw text with requested alignment:
4623             $this->DrawText($font, 0, $x_pos, $y_pos, $this->ndx_text_color, $leg, $text_align, 'bottom');
4624             if ($draw_colorbox) {
4625                 // Draw a box in the data color
4626                 $y1 = $y_pos - $dot_height + 1;
4627                 $y2 = $y_pos - 1;
4628                 ImageFilledRectangle($this->img, $dot_left_x, $y1, $dot_right_x, $y2,
4629                                      $this->ndx_data_colors[$color_index]);
4630                 // Draw a rectangle around the box
4631                 ImageRectangle($this->img, $dot_left_x, $y1, $dot_right_x, $y2,
4632                                $this->ndx_text_color);
4633             }
4634             $y_pos += $dot_height;
4635
4636             $color_index++;
4637             if ($color_index > $max_color_index)
4638                 $color_index = 0;
4639         }
4640         return TRUE;
4641     }
4642
4643 /////////////////////////////////////////////
4644 ////////////////////             PLOT DRAWING
4645 /////////////////////////////////////////////
4646
4647     /*
4648      * Draws a pie chart. Data is 'text-data', 'data-data', or 'text-data-single'.
4649      *
4650      *  For text-data-single, the data array contains records with an ignored label,
4651      *  and one Y value. Each record defines a sector of the pie, as a portion of
4652      *  the sum of all Y values.
4653      *
4654      *  For text-data and data-data, the data array contains records with an ignored label,
4655      *  an ignored X value (for data-data only), and N (N>=1) Y values per record.
4656      *  The pie chart will be produced with N segments. The relative size of the first
4657      *  sector of the pie is the sum of the first Y data value in each record, etc.
4658      *
4659      *  Note: With text-data-single, the data labels could be used, but are not currently.
4660      *
4661      *  If there are no valid data points > 0 at all, just draw nothing. It may seem more correct to
4662      *  raise an error, but all of the other plot types handle it this way implicitly. DrawGraph
4663      *  checks for an empty data array, but this is different: a non-empty data array with no Y values,
4664      *  or all Y=0.
4665      */
4666     protected function DrawPieChart()
4667     {
4668         if (!$this->CheckDataType('text-data, text-data-single, data-data'))
4669             return FALSE;
4670
4671         // Allocate dark colors only if they will be used for shading.
4672         if ($this->shading > 0)
4673             $this->NeedDataDarkColors();
4674
4675         $xpos = $this->plot_area[0] + $this->plot_area_width/2;
4676         $ypos = $this->plot_area[1] + $this->plot_area_height/2;
4677         $diameter = min($this->plot_area_width, $this->plot_area_height);
4678         $radius = $diameter/2;
4679
4680         $num_slices = $this->data_columns;  // See CheckDataArray which calculates this for us.
4681         if ($num_slices < 1) return TRUE;   // Give up early if there is no data at all.
4682         $sumarr = array_fill(0, $num_slices, 0);
4683
4684         if ($this->datatype_pie_single) {
4685             // text-data-single: One data column per row, one pie slice per row.
4686             for ($i = 0; $i < $num_slices; $i++) {
4687                 // $legend[$i] = $this->data[$i][0];                // Note: Labels are not used yet
4688                 if (is_numeric($this->data[$i][1]))
4689                     $sumarr[$i] = abs($this->data[$i][1]);
4690             }
4691         } else {
4692             // text-data: Sum each column (skipping label), one pie slice per column.
4693             // data-data: Sum each column (skipping X value and label), one pie slice per column.
4694             $skip = ($this->datatype_implied) ? 1 : 2; // Leading values to skip in each row.
4695             for ($i = 0; $i < $this->num_data_rows; $i++) {
4696                 for ($j = $skip; $j < $this->num_recs[$i]; $j++) {
4697                     if (is_numeric($this->data[$i][$j]))
4698                         $sumarr[$j-$skip] += abs($this->data[$i][$j]);
4699                 }
4700             }
4701         }
4702
4703         $total = array_sum($sumarr);
4704
4705         if ($total == 0) {
4706             // There are either no valid data points, or all are 0.
4707             // See top comment about why not to make this an error.
4708             return TRUE;
4709         }
4710
4711         if ($this->shading) {
4712             $diam2 = $diameter / 2;
4713         } else {
4714             $diam2 = $diameter;
4715         }
4716         $max_data_colors = count($this->ndx_data_colors);
4717
4718         // Use the Y label format precision, with default value:
4719         if (isset($this->label_format['y']['precision']))
4720             $precision = $this->label_format['y']['precision'];
4721         else
4722             $precision = 1;
4723
4724         for ($h = $this->shading; $h >= 0; $h--) {
4725             $color_index = 0;
4726             $start_angle = 0;
4727             $end_angle = 0;
4728             for ($j = 0; $j < $num_slices; $j++) {
4729                 $val = $sumarr[$j];
4730
4731                 // For shaded pies: the last one (at the top of the "stack") has a brighter color:
4732                 if ($h == 0)
4733                     $slicecol = $this->ndx_data_colors[$color_index];
4734                 else
4735                     $slicecol = $this->ndx_data_dark_colors[$color_index];
4736
4737                 $label_txt = $this->number_format(($val / $total * 100), $precision) . '%';
4738                 $val = 360 * ($val / $total);
4739
4740                 // NOTE that imagefilledarc measures angles CLOCKWISE (go figure why),
4741                 // so the pie chart would start clockwise from 3 o'clock, would it not be
4742                 // for the reversal of start and end angles in imagefilledarc()
4743                 // Also note ImageFilledArc only takes angles in integer degrees, and if the
4744                 // the start and end angles match then you get a full circle not a zero-width
4745                 // pie. This is bad. So skip any zero-size wedge. On the other hand, we cannot
4746                 // let cumulative error from rounding to integer result in missing wedges. So
4747                 // keep the running total as a float, and round the angles. It should not
4748                 // be necessary to check that the last wedge ends at 360 degrees.
4749                 $start_angle = $end_angle;
4750                 $end_angle += $val;
4751                 // This method of conversion to integer - truncate after reversing it - was
4752                 // chosen to match the implicit method of PHPlot<=5.0.4 to get the same slices.
4753                 $arc_start_angle = (int)(360 - $start_angle);
4754                 $arc_end_angle = (int)(360 - $end_angle);
4755
4756                 if ($arc_start_angle > $arc_end_angle) {
4757                     $mid_angle = deg2rad($end_angle - ($val / 2));
4758
4759                     // Draw the slice
4760                     ImageFilledArc($this->img, $xpos, $ypos+$h, $diameter, $diam2,
4761                                    $arc_end_angle, $arc_start_angle,
4762                                    $slicecol, IMG_ARC_PIE);
4763
4764                     // Draw the labels only once
4765                     if ($h == 0) {
4766                         // Draw the outline
4767                         if (! $this->shading)
4768                             ImageFilledArc($this->img, $xpos, $ypos+$h, $diameter, $diam2,
4769                                            $arc_end_angle, $arc_start_angle, $this->ndx_grid_color,
4770                                            IMG_ARC_PIE | IMG_ARC_EDGED |IMG_ARC_NOFILL);
4771
4772                         // The '* 1.2' trick is to get labels out of the pie chart so there are more
4773                         // chances they can be seen in small sectors.
4774                         $label_x = $xpos + ($diameter * 1.2 * cos($mid_angle)) * $this->label_scale_position;
4775                         $label_y = $ypos+$h - ($diam2 * 1.2 * sin($mid_angle)) * $this->label_scale_position;
4776
4777                         $this->DrawText($this->fonts['generic'], 0, $label_x, $label_y, $this->ndx_grid_color,
4778                                         $label_txt, 'center', 'center');
4779                     }
4780                 }
4781                 if (++$color_index >= $max_data_colors)
4782                     $color_index = 0;
4783             }   // end for
4784         }   // end for
4785         return TRUE;
4786     }
4787
4788     /*
4789      * Get data color to use for plotting.
4790      *   $row, $idx : Index arguments for the current data point.
4791      *   &$vars : Variable storage. Caller makes an empty array, and this function uses it.
4792      *   &$data_color : Returned result - Color index for the data point.
4793      *   $extra : Extra info flag passed through to data color callback.
4794      */
4795     protected function GetDataColor($row, $idx, &$vars, &$data_color, $extra = 0)
4796     {
4797         // Initialize or extract variables:
4798         if (empty($vars)) {
4799             $custom_color = (bool)$this->GetCallback('data_color');
4800             $num_data_colors = count($this->ndx_data_colors);
4801             $vars = compact('custom_color', 'num_data_colors');
4802         } else {
4803           extract($vars);
4804         }
4805
4806         // Select the colors.
4807         if ($custom_color) {
4808             $col_i = $this->DoCallback('data_color', $row, $idx, $extra); // Custom color index
4809             $data_color = $this->ndx_data_colors[$col_i % $num_data_colors];
4810         } else {
4811             $data_color = $this->ndx_data_colors[$idx];
4812         }
4813     }
4814
4815     /*
4816      * Get data color and error bar color to use for plotting.
4817      *   $row, $idx : Index arguments for the current bar.
4818      *   &$vars : Variable storage. Caller makes an empty array, and this function uses it.
4819      *   &$data_color : Returned result - Color index for the data (bar fill)
4820      *   &$error_color : Returned result - Color index for the error bars
4821      *   $extra : Extra info flag passed through to data color callback.
4822      */
4823     protected function GetDataErrorColors($row, $idx, &$vars, &$data_color, &$error_color, $extra = 0)
4824     {
4825         // Initialize or extract variables:
4826         if (empty($vars)) {
4827             $this->NeedErrorBarColors();   // This plot needs error bar colors.
4828             $custom_color = (bool)$this->GetCallback('data_color');
4829             $num_data_colors = count($this->ndx_data_colors);
4830             $num_error_colors = count($this->ndx_error_bar_colors);
4831             $vars = compact('custom_color', 'num_data_colors', 'num_error_colors');
4832         } else {
4833           extract($vars);
4834         }
4835
4836         // Select the colors.
4837         if ($custom_color) {
4838             $col_i = $this->DoCallback('data_color', $row, $idx, $extra); // Custom color index
4839             $data_color = $this->ndx_data_colors[$col_i % $num_data_colors];
4840             $error_color = $this->ndx_error_bar_colors[$col_i % $num_error_colors];
4841         } else {
4842             $data_color = $this->ndx_data_colors[$idx];
4843             $error_color = $this->ndx_error_bar_colors[$idx];
4844         }
4845     }
4846
4847     /*
4848      * Draw the points and errors bars for an error plot of types points and linepoints
4849      * Supports only data-data-error format, with each row of the form
4850      *   array("title", x, y1, error1+, error1-, y2, error2+, error2-, ...)
4851      * This is called from DrawDots, with data type already checked.
4852      *   $paired is true for linepoints error plots, to make sure elements are
4853      *       only drawn once.  If true, data labels are drawn by DrawLinesError, and error
4854      *       bars are drawn by DrawDotsError. (This choice is for backwards compatibility.)
4855      */
4856     protected function DrawDotsError($paired = FALSE)
4857     {
4858         // Adjust the point shapes and point sizes arrays:
4859         $this->CheckPointParams();
4860
4861         $gcvars = array(); // For GetDataErrorColors, which initializes and uses this.
4862         // Special flag for data color callback to indicate the 'points' part of 'linepoints':
4863         $alt_flag = $paired ? 1 : 0;
4864
4865         for ($row = 0, $cnt = 0; $row < $this->num_data_rows; $row++) {
4866             $record = 1;                                // Skip record #0 (title)
4867
4868             $x_now = $this->data[$row][$record++];  // Read it, advance record index
4869
4870             $x_now_pixels = $this->xtr($x_now);             // Absolute coordinates.
4871
4872             // Draw X Data labels?
4873             if ($this->x_data_label_pos != 'none' && !$paired)
4874                 $this->DrawXDataLabel($this->data[$row][0], $x_now_pixels, $row);
4875
4876             // Now go for Y, E+, E-
4877             for ($idx = 0; $record < $this->num_recs[$row]; $idx++) {
4878                 if (is_numeric($this->data[$row][$record])) {         // Allow for missing Y data
4879
4880                     // Select the colors:
4881                     $this->GetDataErrorColors($row, $idx, $gcvars, $data_color, $error_color, $alt_flag);
4882
4883                     // Y:
4884                     $y_now = $this->data[$row][$record++];
4885                     $this->DrawDot($x_now, $y_now, $idx, $data_color);
4886
4887                     // Error +
4888                     $val = $this->data[$row][$record++];
4889                     $this->DrawYErrorBar($x_now, $y_now, $val, $this->error_bar_shape, $error_color);
4890                     // Error -
4891                     $val = $this->data[$row][$record++];
4892                     $this->DrawYErrorBar($x_now, $y_now, -$val, $this->error_bar_shape, $error_color);
4893                 } else {
4894                     $record += 3;  // Skip over missing Y and its error values
4895                 }
4896             }
4897         }
4898         return TRUE;
4899     }
4900
4901     /*
4902      * Draw a points plot, or the points for a linepoints plot
4903      * Data format can be text-data (label, y1, y2, ...) or data-data (label, x, y1, y2, ...)
4904      * Points plot with error bars (data-data-error format) is redirected to DrawDotsError.
4905      *   $paired is true for linepoints plots, to make sure elements are only drawn once.
4906      */
4907     protected function DrawDots($paired = FALSE)
4908     {
4909         if (!$this->CheckDataType('text-data, data-data, data-data-error'))
4910             return FALSE;
4911         if ($this->datatype_error_bars)
4912             return $this->DrawDotsError($paired); // Redirect for points+errorbars plot
4913
4914         // Adjust the point shapes and point sizes arrays:
4915         $this->CheckPointParams();
4916
4917         $gcvars = array(); // For GetDataColor, which initializes and uses this.
4918         // Special flag for data color callback to indicate the 'points' part of 'linepoints':
4919         $alt_flag = $paired ? 1 : 0;
4920
4921         for ($row = 0, $cnt = 0; $row < $this->num_data_rows; $row++) {
4922             $rec = 1;                    // Skip record #0 (data label)
4923
4924             if ($this->datatype_implied)                    // Implied X values?
4925                 $x_now = 0.5 + $cnt++;                      // Place text-data at X = 0.5, 1.5, 2.5, etc...
4926             else
4927                 $x_now = $this->data[$row][$rec++];         // Read it, advance record index
4928
4929             $x_now_pixels = $this->xtr($x_now);
4930
4931             // Draw X Data labels?
4932             if ($this->x_data_label_pos != 'none' && !$paired)
4933                 $this->DrawXDataLabel($this->data[$row][0], $x_now_pixels, $row);
4934
4935             // Proceed with Y values
4936             for ($idx = 0;$rec < $this->num_recs[$row]; $rec++, $idx++) {
4937                 if (is_numeric($this->data[$row][$rec])) {              // Allow for missing Y data
4938
4939                     // Select the color:
4940                     $this->GetDataColor($row, $idx, $gcvars, $data_color, $alt_flag);
4941                     // Draw the marker:
4942                     $this->DrawDot($x_now, $this->data[$row][$rec], $idx, $data_color);
4943                 }
4944             }
4945         }
4946         return TRUE;
4947     }
4948
4949     /*
4950      * Draw a Thin Bar Line plot, also known as an Impulse plot.
4951      * A clean, fast routine for when you just want charts like stock volume charts.
4952      * Supports data-data and text-data formats for vertical plots,
4953      * and data-data-yx and text-data-yx for horizontal plots.
4954      * Note that although this plot type supports multiple data sets, it rarely makes
4955      * sense to have more than 1, because the lines will overlay.
4956      * This one function does both vertical and horizontal plots. "iv" is used for the
4957      * independent variable (X for vertical plots, Y for horizontal) and "dv" is used
4958      * for the dependent variable (Y for vertical plots, X for horizontal).
4959      */
4960     protected function DrawThinBarLines()
4961     {
4962         if (!$this->CheckDataType('text-data, data-data, text-data-yx, data-data-yx'))
4963             return FALSE;
4964
4965         $gcvars = array(); // For GetDataColor, which initializes and uses this.
4966
4967         for ($row = 0, $cnt = 0; $row < $this->num_data_rows; $row++) {
4968             $rec = 1;                    // Skip record #0 (data label)
4969
4970             if ($this->datatype_implied)                    // Implied independent variable values?
4971                 $iv_now = 0.5 + $cnt++;                     // Place text-data at 0.5, 1.5, 2.5, etc...
4972             else
4973                 $iv_now = $this->data[$row][$rec++];        // Read it, advance record index
4974
4975             if ($this->datatype_swapped_xy) {
4976                 $y_now_pixels = $this->ytr($iv_now);
4977                 // Draw Y Data labels?
4978                 if ($this->y_data_label_pos != 'none')
4979                     $this->DrawYDataLabel($this->data[$row][0], $y_now_pixels);
4980             } else {
4981                 $x_now_pixels = $this->xtr($iv_now);
4982                 // Draw X Data labels?
4983                 if ($this->x_data_label_pos != 'none')
4984                     $this->DrawXDataLabel($this->data[$row][0], $x_now_pixels);
4985             }
4986
4987             // Proceed with dependent values
4988             for ($idx = 0; $rec < $this->num_recs[$row]; $rec++, $idx++) {
4989                 if (is_numeric($this->data[$row][$rec])) {              // Allow for missing data
4990                     $dv = $this->data[$row][$rec];
4991                     ImageSetThickness($this->img, $this->line_widths[$idx]);
4992
4993                     // Select the color:
4994                     $this->GetDataColor($row, $idx, $gcvars, $data_color);
4995
4996                     if ($this->datatype_swapped_xy) {
4997                         // Draw a line from user defined y axis position right (or left) to xtr($dv)
4998                         ImageLine($this->img, $this->y_axis_x_pixels, $y_now_pixels,
4999                                               $this->xtr($dv), $y_now_pixels, $data_color);
5000                     } else {
5001                         // Draw a line from user defined x axis position up (or down) to ytr($dv)
5002                         ImageLine($this->img, $x_now_pixels, $this->x_axis_y_pixels,
5003                                               $x_now_pixels, $this->ytr($dv), $data_color);
5004                    }
5005                 }
5006             }
5007         }
5008
5009         ImageSetThickness($this->img, 1);
5010         return TRUE;
5011     }
5012
5013     /*
5014      *  Draw an Error Bar set. Used by DrawDotsError and DrawLinesError
5015      */
5016     protected function DrawYErrorBar($x_world, $y_world, $error_height, $error_bar_type, $color)
5017     {
5018         $x1 = $this->xtr($x_world);
5019         $y1 = $this->ytr($y_world);
5020         $y2 = $this->ytr($y_world+$error_height) ;
5021
5022         ImageSetThickness($this->img, $this->error_bar_line_width);
5023         ImageLine($this->img, $x1, $y1 , $x1, $y2, $color);
5024         if ($error_bar_type == 'tee') {
5025             ImageLine($this->img, $x1-$this->error_bar_size, $y2, $x1+$this->error_bar_size, $y2, $color);
5026         }
5027         ImageSetThickness($this->img, 1);
5028         return TRUE;
5029     }
5030
5031     /*
5032      * Draws a styled dot. Uses world coordinates.
5033      * The list of supported shapes can also be found in SetPointShapes().
5034      * All shapes are drawn using a 3x3 grid, centered on the data point.
5035      * The center is (x_mid, y_mid) and the corners are (x1, y1) and (x2, y2).
5036      *   $record is the 0-based index that selects the shape and size.
5037      */
5038     protected function DrawDot($x_world, $y_world, $record, $color)
5039     {
5040         $index = $record % $this->point_counts;
5041         $point_size = $this->point_sizes[$index];
5042
5043         $half_point = (int)($point_size / 2);
5044
5045         $x_mid = $this->xtr($x_world);
5046         $y_mid = $this->ytr($y_world);
5047
5048         $x1 = $x_mid - $half_point;
5049         $x2 = $x_mid + $half_point;
5050         $y1 = $y_mid - $half_point;
5051         $y2 = $y_mid + $half_point;
5052
5053         switch ($this->point_shapes[$index]) {
5054         case 'halfline':
5055             ImageLine($this->img, $x1, $y_mid, $x_mid, $y_mid, $color);
5056             break;
5057         case 'line':
5058             ImageLine($this->img, $x1, $y_mid, $x2, $y_mid, $color);
5059             break;
5060         case 'plus':
5061             ImageLine($this->img, $x1, $y_mid, $x2, $y_mid, $color);
5062             ImageLine($this->img, $x_mid, $y1, $x_mid, $y2, $color);
5063             break;
5064         case 'cross':
5065             ImageLine($this->img, $x1, $y1, $x2, $y2, $color);
5066             ImageLine($this->img, $x1, $y2, $x2, $y1, $color);
5067             break;
5068         case 'circle':
5069             ImageArc($this->img, $x_mid, $y_mid, $point_size, $point_size, 0, 360, $color);
5070             break;
5071         case 'dot':
5072             ImageFilledEllipse($this->img, $x_mid, $y_mid, $point_size, $point_size, $color);
5073             break;
5074         case 'diamond':
5075             $arrpoints = array( $x1, $y_mid, $x_mid, $y1, $x2, $y_mid, $x_mid, $y2);
5076             ImageFilledPolygon($this->img, $arrpoints, 4, $color);
5077             break;
5078         case 'triangle':
5079             $arrpoints = array( $x1, $y_mid, $x2, $y_mid, $x_mid, $y2);
5080             ImageFilledPolygon($this->img, $arrpoints, 3, $color);
5081             break;
5082         case 'trianglemid':
5083             $arrpoints = array( $x1, $y1, $x2, $y1, $x_mid, $y_mid);
5084             ImageFilledPolygon($this->img, $arrpoints, 3, $color);
5085             break;
5086         case 'yield':
5087             $arrpoints = array( $x1, $y1, $x2, $y1, $x_mid, $y2);
5088             ImageFilledPolygon($this->img, $arrpoints, 3, $color);
5089             break;
5090         case 'delta':
5091             $arrpoints = array( $x1, $y2, $x2, $y2, $x_mid, $y1);
5092             ImageFilledPolygon($this->img, $arrpoints, 3, $color);
5093             break;
5094         case 'star':
5095             ImageLine($this->img, $x1, $y_mid, $x2, $y_mid, $color);
5096             ImageLine($this->img, $x_mid, $y1, $x_mid, $y2, $color);
5097             ImageLine($this->img, $x1, $y1, $x2, $y2, $color);
5098             ImageLine($this->img, $x1, $y2, $x2, $y1, $color);
5099             break;
5100         case 'hourglass':
5101             $arrpoints = array( $x1, $y1, $x2, $y1, $x1, $y2, $x2, $y2);
5102             ImageFilledPolygon($this->img, $arrpoints, 4, $color);
5103             break;
5104         case 'bowtie':
5105             $arrpoints = array( $x1, $y1, $x1, $y2, $x2, $y1, $x2, $y2);
5106             ImageFilledPolygon($this->img, $arrpoints, 4, $color);
5107             break;
5108         case 'target':
5109             ImageFilledRectangle($this->img, $x1, $y1, $x_mid, $y_mid, $color);
5110             ImageFilledRectangle($this->img, $x_mid, $y_mid, $x2, $y2, $color);
5111             ImageRectangle($this->img, $x1, $y1, $x2, $y2, $color);
5112             break;
5113         case 'box':
5114             ImageRectangle($this->img, $x1, $y1, $x2, $y2, $color);
5115             break;
5116         case 'home': /* As in: "home plate" (baseball), also looks sort of like a house. */
5117             $arrpoints = array( $x1, $y2, $x2, $y2, $x2, $y_mid, $x_mid, $y1, $x1, $y_mid);
5118             ImageFilledPolygon($this->img, $arrpoints, 5, $color);
5119             break;
5120         case 'up':
5121             ImagePolygon($this->img, array($x_mid, $y1, $x2, $y2, $x1, $y2), 3, $color);
5122             break;
5123         case 'down':
5124             ImagePolygon($this->img, array($x_mid, $y2, $x1, $y1, $x2, $y1), 3, $color);
5125             break;
5126         case 'none': /* Special case, no point shape here */
5127             break;
5128         default: /* Also 'rect' */
5129             ImageFilledRectangle($this->img, $x1, $y1, $x2, $y2, $color);
5130             break;
5131         }
5132         return TRUE;
5133     }
5134
5135     /*
5136      * Draw an 'area' or 'stacked area' plot.
5137      * Both of these fill the area between lines, but in the stacked area graph the Y values
5138      * are accumulated for each X, same as stacked bars. In the regular area graph, the areas
5139      * are filled in order from the X axis up to each Y (so the Y values for each X need to be
5140      * in decreasing order in this case).
5141      * Data format can be text-data (label, y1, y2, ...) or data-data (label, x, y1, y2, ...)
5142      * Notes:
5143      *   All Y values must be >= 0. (If any Y<0 the absolute value is used.)
5144      *   Missing data points are NOT handled. (They are counted as 0.)
5145      *   All rows must have the same number of Y points, or an error image will be produced.
5146      */
5147     protected function DrawArea($do_stacked = FALSE)
5148     {
5149         if (!$this->CheckDataType('text-data, data-data'))
5150             return FALSE;
5151
5152         $n = $this->num_data_rows;  // Number of X values
5153
5154         // These arrays store the device X and Y coordinates for all lines:
5155         $xd = array();
5156         $yd = array();
5157
5158         // Make sure each row has the same number of values. Note records_per_group is max(num_recs).
5159         if ($this->records_per_group != min($this->num_recs)) {
5160             return $this->PrintError("DrawArea(): Data array must contain the same number"
5161                       . " of Y values for each X");
5162         }
5163
5164         // Calculate the Y value for each X, and store the device
5165         // coordinates into the xd and yd arrays.
5166         // For stacked area plots, the Y values accumulate.
5167         for ($row = 0; $row < $n; $row++) {
5168             $rec = 1;                                       // Skip record #0 (data label)
5169
5170             if ($this->datatype_implied)                    // Implied X values?
5171                 $x_now = 0.5 + $row;                        // Place text-data at X = 0.5, 1.5, 2.5, etc...
5172             else
5173                 $x_now = $this->data[$row][$rec++];         // Read it, advance record index
5174
5175             $x_now_pixels = $this->xtr($x_now);
5176
5177             if ($this->x_data_label_pos != 'none')          // Draw X Data labels?
5178                 $this->DrawXDataLabel($this->data[$row][0], $x_now_pixels);
5179
5180             // Store the X value.
5181             // There is an artificial Y value at the axis. For 'area' it goes
5182             // at the end; for stackedarea it goes before the start.
5183             $xd[$row] = $x_now_pixels;
5184             $yd[$row] = array();
5185             if ($do_stacked)
5186                 $yd[$row][] = $this->x_axis_y_pixels;
5187
5188             // Store the Y values for this X.
5189             // All Y values are clipped to the x axis which should be zero but can be moved.
5190             $y = 0;
5191             while ($rec < $this->records_per_group) {
5192                 if (is_numeric($this->data[$row][$rec])) {  // Treat missing values as 0.
5193                     $y += abs($this->data[$row][$rec]);
5194                 }
5195                 $yd[$row][] = $this->ytr(max($this->x_axis_position, $y));
5196                 if (!$do_stacked) $y = 0;
5197                 $rec++;
5198             }
5199
5200             if (!$do_stacked)
5201                 $yd[$row][] = $this->x_axis_y_pixels;
5202         }
5203
5204         // Now draw the filled polygons.
5205         // Note data_columns is the number of Y points (columns excluding label and X), and the
5206         // number of entries in the yd[] arrays is data_columns+1.
5207         $prev_row = 0;
5208         for ($row = 1; $row <= $this->data_columns; $row++) { // 1 extra for X axis artificial row
5209             $pts = array();
5210             // Previous data set forms top (for area) or bottom (for stackedarea):
5211             for ($j = 0; $j < $n; $j++) {
5212                 $pts[] = $xd[$j];
5213                 $pts[] = $yd[$j][$prev_row];
5214             }
5215             // Current data set forms bottom (for area) or top (for stackedarea):
5216             for ($j = $n- 1; $j >= 0; $j--) {
5217                 $pts[] = $xd[$j];
5218                 $pts[] = $yd[$j][$row];
5219             }
5220             // Draw it:
5221             ImageFilledPolygon($this->img, $pts, $n * 2, $this->ndx_data_colors[$prev_row]);
5222
5223             $prev_row = $row;
5224         }
5225         return TRUE;
5226     }
5227
5228     /*
5229      * Draw a line plot, or the lines part of a linepoints plot
5230      * Data format can be text-data (label, y1, y2, ...) or data-data (label, x, y1, y2, ...)
5231      * Line plot with error bars (data-data-error format) is redirected to DrawLinesError.
5232      *   $paired is true for linepoints plots, to make sure elements are only drawn once.
5233      */
5234     protected function DrawLines($paired = FALSE)
5235     {
5236         if (!$this->CheckDataType('text-data, data-data, data-data-error'))
5237             return FALSE;
5238         if ($this->datatype_error_bars)
5239             return $this->DrawLinesError($paired); // Redirect for lines+errorbar plot
5240
5241         // Flag array telling if the current point is valid, one element per plot line.
5242         // If start_lines[i] is true, then (lastx[i], lasty[i]) is the previous point.
5243         $start_lines = array_fill(0, $this->data_columns, FALSE);
5244
5245         $gcvars = array(); // For GetDataColor, which initializes and uses this.
5246
5247         for ($row = 0, $cnt = 0; $row < $this->num_data_rows; $row++) {
5248             $record = 1;                                    // Skip record #0 (data label)
5249
5250             if ($this->datatype_implied)                    // Implied X values?
5251                 $x_now = 0.5 + $cnt++;                      // Place text-data at X = 0.5, 1.5, 2.5, etc...
5252             else
5253                 $x_now = $this->data[$row][$record++];      // Read it, advance record index
5254
5255             $x_now_pixels = $this->xtr($x_now);             // Absolute coordinates
5256
5257             if ($this->x_data_label_pos != 'none')          // Draw X Data labels?
5258                 $this->DrawXDataLabel($this->data[$row][0], $x_now_pixels, $row);
5259
5260             for ($idx = 0; $record < $this->num_recs[$row]; $record++, $idx++) {
5261                 if (($line_style = $this->line_styles[$idx]) == 'none')
5262                     continue; //Allow suppressing entire line, useful with linepoints
5263                 if (is_numeric($this->data[$row][$record])) {           //Allow for missing Y data
5264
5265                     // Select the color:
5266                     $this->GetDataColor($row, $idx, $gcvars, $data_color);
5267
5268                     $y_now_pixels = $this->ytr($this->data[$row][$record]);
5269
5270                     if ($start_lines[$idx]) {
5271                         // Set line width, revert it to normal at the end
5272                         ImageSetThickness($this->img, $this->line_widths[$idx]);
5273
5274                         if ($line_style == 'dashed') {
5275                             $this->SetDashedStyle($data_color);
5276                             $data_color = IMG_COLOR_STYLED;
5277                         }
5278                         ImageLine($this->img, $x_now_pixels, $y_now_pixels,
5279                                   $lastx[$idx], $lasty[$idx], $data_color);
5280                     }
5281                     $lasty[$idx] = $y_now_pixels;
5282                     $lastx[$idx] = $x_now_pixels;
5283                     $start_lines[$idx] = TRUE;
5284                 } elseif ($this->draw_broken_lines) {  // Y data missing, leave a gap.
5285                     $start_lines[$idx] = FALSE;
5286                 }
5287             }   // end for
5288         }   // end for
5289
5290         ImageSetThickness($this->img, 1);       // Revert to original state for lines to be drawn later.
5291         return TRUE;
5292     }
5293
5294     /*
5295      * Draw lines with error bars for an error plot of types lines and linepoints
5296      * Supports only data-data-error format, with each row of the form
5297      *   array("title", x, y1, error1+, error1-, y2, error2+, error2-, ...)
5298      * This is called from DrawLines, with data type already checked.
5299      *   $paired is true for linepoints error plots, to make sure elements are
5300      *       only drawn once.  If true, data labels are drawn by DrawLinesError, and error
5301      *       bars are drawn by DrawDotsError. (This choice is for backwards compatibility.)
5302      */
5303     protected function DrawLinesError($paired = FALSE)
5304     {
5305         $start_lines = array_fill(0, $this->data_columns, FALSE);
5306
5307         $gcvars = array(); // For GetDataErrorColors, which initializes and uses this.
5308
5309         for ($row = 0, $cnt = 0; $row < $this->num_data_rows; $row++) {
5310             $record = 1;                                    // Skip record #0 (data label)
5311
5312             $x_now = $this->data[$row][$record++];          // Read X value, advance record index
5313
5314             $x_now_pixels = $this->xtr($x_now);             // Absolute coordinates.
5315
5316             if ($this->x_data_label_pos != 'none')          // Draw X Data labels?
5317                 $this->DrawXDataLabel($this->data[$row][0], $x_now_pixels, $row);
5318
5319             // Now go for Y, E+, E-
5320             for ($idx = 0; $record < $this->num_recs[$row]; $idx++) {
5321                 if (($line_style = $this->line_styles[$idx]) == 'none')
5322                     continue; //Allow suppressing entire line, useful with linepoints
5323                 if (is_numeric($this->data[$row][$record])) {    // Allow for missing Y data
5324
5325                     // Select the colors:
5326                     $this->GetDataErrorColors($row, $idx, $gcvars, $data_color, $error_color);
5327
5328                     // Y
5329                     $y_now = $this->data[$row][$record++];
5330                     $y_now_pixels = $this->ytr($y_now);
5331
5332                     if ($start_lines[$idx]) {
5333                         ImageSetThickness($this->img, $this->line_widths[$idx]);
5334
5335                         if ($line_style == 'dashed') {
5336                             $this->SetDashedStyle($data_color);
5337                             $data_color = IMG_COLOR_STYLED;
5338                         }
5339                         ImageLine($this->img, $x_now_pixels, $y_now_pixels,
5340                                   $lastx[$idx], $lasty[$idx], $data_color);
5341                     }
5342
5343                     if ($paired) {
5344                         $record += 2; // Skip error bars - done in the 'points' part of 'linepoints'.
5345                     } else {
5346                         // Error+
5347                         $val = $this->data[$row][$record++];
5348                         $this->DrawYErrorBar($x_now, $y_now, $val, $this->error_bar_shape, $error_color);
5349
5350                         // Error-
5351                         $val = $this->data[$row][$record++];
5352                         $this->DrawYErrorBar($x_now, $y_now, -$val, $this->error_bar_shape, $error_color);
5353                     }
5354
5355                     // Update indexes:
5356                     $start_lines[$idx] = TRUE;   // Tells us if we already drew the first column of points,
5357                                              // thus having $lastx and $lasty ready for the next column.
5358                     $lastx[$idx] = $x_now_pixels;
5359                     $lasty[$idx] = $y_now_pixels;
5360
5361                 } else {
5362                     $record += 3;  // Skip over missing Y and its error values
5363                     if ($this->draw_broken_lines) {
5364                         $start_lines[$idx] = FALSE;
5365                     }
5366                 }
5367             }   // end for
5368         }   // end for
5369
5370         ImageSetThickness($this->img, 1);   // Revert to original state for lines to be drawn later.
5371         return TRUE;
5372     }
5373
5374     /*
5375      * Draw a Lines+Points plot (linepoints).
5376      * This just uses DrawLines and DrawDots. They handle the error-bar case themselves.
5377      */
5378     protected function DrawLinePoints()
5379     {
5380         // This check is redundant, as DrawLines and DrawDots do it, but left here as insurance.
5381         if (!$this->CheckDataType('text-data, data-data, data-data-error'))
5382             return FALSE;
5383         $this->DrawLines(TRUE);
5384         $this->DrawDots(TRUE);
5385         return TRUE;
5386     }
5387
5388     /*
5389      * Draw a Squared Line plot.
5390      * Data format can be text-data (label, y1, y2, ...) or data-data (label, x, y1, y2, ...)
5391      * This is based on DrawLines(), with one more line drawn for each point.
5392      */
5393     protected function DrawSquared()
5394     {
5395         if (!$this->CheckDataType('text-data, data-data'))
5396             return FALSE;
5397
5398         // Flag array telling if the current point is valid, one element per plot line.
5399         // If start_lines[i] is true, then (lastx[i], lasty[i]) is the previous point.
5400         $start_lines = array_fill(0, $this->data_columns, FALSE);
5401
5402         $gcvars = array(); // For GetDataColor, which initializes and uses this.
5403
5404         for ($row = 0, $cnt = 0; $row < $this->num_data_rows; $row++) {
5405             $record = 1;                                    // Skip record #0 (data label)
5406
5407             if ($this->datatype_implied)                    // Implied X values?
5408                 $x_now = 0.5 + $cnt++;                      // Place text-data at X = 0.5, 1.5, 2.5, etc...
5409             else
5410                 $x_now = $this->data[$row][$record++];      // Read it, advance record index
5411
5412             $x_now_pixels = $this->xtr($x_now);             // Absolute coordinates
5413
5414             if ($this->x_data_label_pos != 'none')          // Draw X Data labels?
5415                 $this->DrawXDataLabel($this->data[$row][0], $x_now_pixels); // notice there is no last param.
5416
5417             // Draw Lines
5418             for ($idx = 0; $record < $this->num_recs[$row]; $record++, $idx++) {
5419                 if (is_numeric($this->data[$row][$record])) {               // Allow for missing Y data
5420                     $y_now_pixels = $this->ytr($this->data[$row][$record]);
5421
5422                     if ($start_lines[$idx]) {
5423                         // Set line width, revert it to normal at the end
5424                         ImageSetThickness($this->img, $this->line_widths[$idx]);
5425
5426                         // Select the color:
5427                         $this->GetDataColor($row, $idx, $gcvars, $data_color);
5428
5429                         if ($this->line_styles[$idx] == 'dashed') {
5430                             $this->SetDashedStyle($data_color);
5431                             $data_color = IMG_COLOR_STYLED;
5432                         }
5433                         ImageLine($this->img, $lastx[$idx], $lasty[$idx],
5434                                   $x_now_pixels, $lasty[$idx], $data_color);
5435                         ImageLine($this->img, $x_now_pixels, $lasty[$idx],
5436                                   $x_now_pixels, $y_now_pixels, $data_color);
5437                     }
5438                     $lastx[$idx] = $x_now_pixels;
5439                     $lasty[$idx] = $y_now_pixels;
5440                     $start_lines[$idx] = TRUE;
5441                 } elseif ($this->draw_broken_lines) {  // Y data missing, leave a gap.
5442                     $start_lines[$idx] = FALSE;
5443                 }
5444             }
5445         }   // end while
5446
5447         ImageSetThickness($this->img, 1);
5448         return TRUE;
5449     }
5450
5451     /*
5452      * Draw a bar (or segment of a bar), with optional shading or border.
5453      * This is used by the bar and stackedbar plots, vertical and horizontal.
5454      *   $x1, $y1 : One corner of the bar.
5455      *   $x2, $y2 : Other corner of the bar.
5456      *   $data_color : Color index to use for the bar fill.
5457      *   $alt_color : Color index to use for the shading (if shading is on), else for the border.
5458      *      Note the same color is NOT used for shading and border - just the same argument.
5459      *      See GetBarColors() for where these arguments come from.
5460      *   $shade_top : Shade the top? (Suppressed for downward stack segments except first.)
5461      *   $shade_side : Shade the right side? (Suppressed for leftward stack segments except first.)
5462      *      Only one of $shade_top or $shade_side can be FALSE. Both default to TRUE.
5463      */
5464     protected function DrawBar($x1, $y1, $x2, $y2, $data_color, $alt_color,
5465             $shade_top = TRUE, $shade_side = TRUE)
5466     {
5467         // Sort the points so x1,y1 is upper left and x2,y2 is lower right. This
5468         // is needed in order to get the shading right, and imagerectangle may require it.
5469         if ($x1 > $x2) {
5470             $t = $x1; $x1 = $x2; $x2 = $t;
5471         }
5472         if ($y1 > $y2) {
5473             $t = $y1; $y1 = $y2; $y2 = $t;
5474         }
5475
5476         // Draw the bar
5477         ImageFilledRectangle($this->img, $x1, $y1, $x2, $y2, $data_color);
5478
5479         // Draw a shade, or a border.
5480         if (($shade = $this->shading) > 0) {
5481             if ($shade_top && $shade_side) {
5482                 $npts = 6;
5483                 $pts = array($x1, $y1, $x1 + $shade, $y1 - $shade, $x2 + $shade, $y1 - $shade,
5484                              $x2 + $shade, $y2 - $shade, $x2, $y2, $x2, $y1);
5485             } else {
5486                 $npts = 4;
5487                 if ($shade_top) { // Suppress side shading
5488                     $pts = array($x1, $y1, $x1 + $shade, $y1 - $shade, $x2 + $shade, $y1 - $shade, $x2, $y1);
5489                 } else { // Suppress top shading
5490                     $pts = array($x2, $y2, $x2, $y1, $x2 + $shade, $y1 - $shade, $x2 + $shade, $y2 - $shade);
5491                 }
5492             }
5493             ImageFilledPolygon($this->img, $pts, $npts, $alt_color);
5494         } else {
5495             ImageRectangle($this->img, $x1, $y1, $x2,$y2, $alt_color);
5496         }
5497     }
5498
5499     /*
5500      * Get colors to use for a bar chart. There is a data color, and either a border color
5501      * or a shading color (data dark color).
5502      *   $row, $idx : Index arguments for the current bar.
5503      *   &$vars : Variable storage. Caller makes an empty array, and this function uses it.
5504      *   &$data_color : Returned result - Color index for the data (bar fill).
5505      *   &$alt_color : Returned result - Color index for the shading or outline.
5506      */
5507     protected function GetBarColors($row, $idx, &$vars, &$data_color, &$alt_color)
5508     {
5509         // Initialize or extract variables:
5510         if (empty($vars)) {
5511             if ($this->shading > 0)    // This plot needs dark colors if shading is on.
5512                 $this->NeedDataDarkColors();
5513             $custom_color = (bool)$this->GetCallback('data_color');
5514             $num_data_colors = count($this->ndx_data_colors);
5515             $num_border_colors = count($this->ndx_data_border_colors);
5516             $vars = compact('custom_color', 'num_data_colors', 'num_border_colors');
5517         } else {
5518           extract($vars);
5519         }
5520
5521         // Select the colors.
5522         if ($custom_color) {
5523             $col_i = $this->DoCallback('data_color', $row, $idx); // Custom color index
5524             $i_data = $col_i % $num_data_colors; // Index for data colors and dark colors
5525             $i_border = $col_i % $num_border_colors; // Index for data borders (if used)
5526         } else {
5527             $i_data = $i_border = $idx;
5528         }
5529         $data_color = $this->ndx_data_colors[$i_data];
5530         if ($this->shading > 0) {
5531             $alt_color = $this->ndx_data_dark_colors[$i_data];
5532         } else {
5533             $alt_color = $this->ndx_data_border_colors[$i_border];
5534         }
5535     }
5536
5537     /*
5538      * Draw a Bar chart
5539      * Supports text-data format, with each row in the form array(label, y1, y2, y3, ...)
5540      * Horizontal bars (text-data-yx format) are sent to DrawHorizBars() instead.
5541      */
5542     protected function DrawBars()
5543     {
5544         if (!$this->CheckDataType('text-data, text-data-yx'))
5545             return FALSE;
5546         if ($this->datatype_swapped_xy)
5547             return $this->DrawHorizBars();
5548         $this->CalcBarWidths();
5549
5550         // This is the X offset from the bar group's label center point to the left side of the first bar
5551         // in the group. See also CalcBarWidths above.
5552         $x_first_bar = ($this->data_columns * $this->record_bar_width) / 2 - $this->bar_adjust_gap;
5553
5554         $gcvars = array(); // For GetBarColors, which initializes and uses this.
5555
5556         for ($row = 0; $row < $this->num_data_rows; $row++) {
5557             $record = 1;                                    // Skip record #0 (data label)
5558
5559             $x_now_pixels = $this->xtr(0.5 + $row);         // Place text-data at X = 0.5, 1.5, 2.5, etc...
5560
5561             if ($this->x_data_label_pos != 'none')          // Draw X Data labels?
5562                 $this->DrawXDataLabel($this->data[$row][0], $x_now_pixels);
5563
5564             // Lower left X of first bar in the group:
5565             $x1 = $x_now_pixels - $x_first_bar;
5566
5567             // Draw the bars in the group:
5568             for ($idx = 0; $record < $this->num_recs[$row]; $record++, $idx++) {
5569                 if (is_numeric($this->data[$row][$record])) {       // Allow for missing Y data
5570                     $y = $this->data[$row][$record];
5571                     $x2 = $x1 + $this->actual_bar_width;
5572
5573                     if (($upgoing_bar = $y >= $this->x_axis_position)) {
5574                         $y1 = $this->ytr($y);
5575                         $y2 = $this->x_axis_y_pixels;
5576                     } else {
5577                         $y1 = $this->x_axis_y_pixels;
5578                         $y2 = $this->ytr($y);
5579                     }
5580
5581                     // Select the colors:
5582                     $this->GetBarColors($row, $idx, $gcvars, $data_color, $alt_color);
5583
5584                     // Draw the bar, and the shade or border:
5585                     $this->DrawBar($x1, $y1, $x2, $y2, $data_color, $alt_color);
5586
5587                     // Draw optional data labels above the bars (or below, for negative values).
5588                     if ( $this->y_data_label_pos == 'plotin') {
5589                         if ($upgoing_bar) {
5590                           $v_align = 'bottom';
5591                           $y_offset = -5 - $this->shading;
5592                         } else {
5593                           $v_align = 'top';
5594                           $y_offset = 2;
5595                         }
5596                         $this->DrawDataValueLabel('y', $row+0.5, $y, $y, 'center', $v_align,
5597                                 ($idx + 0.5) * $this->record_bar_width - $x_first_bar, $y_offset);
5598                     }
5599                 }
5600                 // Step to next bar in group:
5601                 $x1 += $this->record_bar_width;
5602             }   // end for
5603         }   // end for
5604         return TRUE;
5605     }
5606
5607     /*
5608      * Draw a Horizontal Bar chart
5609      * Supports only text-data-yx format, with each row in the form array(label, x1, x2, x3, ...)
5610      * Note that the data values are X not Y, and the bars are drawn horizontally.
5611      * This is called from DrawBars, which has already checked the data type.
5612      */
5613     protected function DrawHorizBars()
5614     {
5615         $this->CalcBarWidths(FALSE); // Calculate bar sizes for horizontal plots
5616
5617         // This is the Y offset from the bar group's label center point to the bottom of the first bar
5618         // in the group. See also CalcBarWidths above.
5619         $y_first_bar = ($this->data_columns * $this->record_bar_width) / 2 - $this->bar_adjust_gap;
5620
5621         $gcvars = array(); // For GetBarColors, which initializes and uses this.
5622
5623         for ($row = 0; $row < $this->num_data_rows; $row++) {
5624             $record = 1;                                    // Skip record #0 (data label)
5625
5626             $y_now_pixels = $this->ytr(0.5 + $row);         // Place bars at Y=0.5, 1.5, 2.5, etc...
5627
5628             if ($this->y_data_label_pos != 'none')          // Draw Y Data Labels?
5629                 $this->DrawYDataLabel($this->data[$row][0], $y_now_pixels);
5630
5631             // Lower left Y of first bar in the group:
5632             $y1 = $y_now_pixels + $y_first_bar;
5633
5634             // Draw the bars in the group:
5635             for ($idx = 0; $record < $this->num_recs[$row]; $record++, $idx++) {
5636                 if (is_numeric($this->data[$row][$record])) {       // Allow for missing X data
5637                     $x = $this->data[$row][$record];
5638                     $y2 = $y1 - $this->actual_bar_width;
5639
5640                     if (($rightwards_bar = $x >= $this->y_axis_position)) {
5641                         $x1 = $this->xtr($x);
5642                         $x2 = $this->y_axis_x_pixels;
5643                     } else {
5644                         $x1 = $this->y_axis_x_pixels;
5645                         $x2 = $this->xtr($x);
5646                     }
5647
5648                     // Select the colors:
5649                     $this->GetBarColors($row, $idx, $gcvars, $data_color, $alt_color);
5650
5651                     // Draw the bar, and the shade or border:
5652                     $this->DrawBar($x1, $y1, $x2, $y2, $data_color, $alt_color);
5653
5654                     // Draw optional data labels to the right of the bars (or left, if the bar
5655                     // goes left of the Y axis line).
5656                     if ($this->x_data_label_pos == 'plotin') {
5657                         if ($rightwards_bar) {
5658                           $h_align = 'left';
5659                           $x_offset = 5 + $this->shading;
5660                         } else {
5661                           $h_align = 'right';
5662                           $x_offset = -2;
5663                         }
5664                         $this->DrawDataValueLabel('x', $x, $row+0.5, $x, $h_align, 'center',
5665                                 $x_offset, $y_first_bar - ($idx + 0.5) * $this->record_bar_width);
5666                     }
5667
5668                 }
5669                 // Step to next bar in group:
5670                 $y1 -= $this->record_bar_width;
5671             }   // end for
5672         }   // end for
5673
5674         return TRUE;
5675     }
5676
5677     /*
5678      * Draw a Stacked Bar chart
5679      * Supports text-data format, with each row in the form array(label, y1, y2, y3, ...)
5680      * Horizontal stacked bars (text-data-yx format) are sent to DrawHorizStackedBars() instead.
5681      * Original stacked bars idea by Laurent Kruk < lolok at users.sourceforge.net >
5682      */
5683     protected function DrawStackedBars()
5684     {
5685         if (!$this->CheckDataType('text-data, text-data-yx'))
5686             return FALSE;
5687         if ($this->datatype_swapped_xy)
5688             return $this->DrawHorizStackedBars();
5689         $this->CalcBarWidths();
5690
5691         // This is the X offset from the bar's label center point to the left side of the bar.
5692         $x_first_bar = $this->record_bar_width / 2 - $this->bar_adjust_gap;
5693
5694         $gcvars = array(); // For GetBarColors, which initializes and uses this.
5695
5696         // Determine if any data labels are on:
5697         $data_labels_within = ($this->y_data_label_pos == 'plotstack');
5698         $data_labels_end = $data_labels_within || ($this->y_data_label_pos == 'plotin');
5699         $data_label_y_offset = -5 - $this->shading; // For upward labels only.
5700
5701         for ($row = 0; $row < $this->num_data_rows; $row++) {
5702             $record = 1;                                    // Skip record #0 (data label)
5703
5704             $x_now_pixels = $this->xtr(0.5 + $row);         // Place text-data at X = 0.5, 1.5, 2.5, etc...
5705
5706             if ($this->x_data_label_pos != 'none')          // Draw X Data labels?
5707                 $this->DrawXDataLabel($this->data[$row][0], $x_now_pixels);
5708
5709             // Lower left and lower right X of the bars in this stack:
5710             $x1 = $x_now_pixels - $x_first_bar;
5711             $x2 = $x1 + $this->actual_bar_width;
5712
5713             // Draw the bar segments in this stack.
5714             $wy1 = 0;                       // World coordinates Y1, current sum of values
5715             $wy2 = $this->x_axis_position;  // World coordinates Y2, last drawn value
5716             $first = TRUE;
5717
5718             for ($idx = 0; $record < $this->num_recs[$row]; $record++, $idx++) {
5719
5720                 // Skip missing Y values, and ignore Y=0 values.
5721                 if (is_numeric($this->data[$row][$record])
5722                     && ($this_y = $this->data[$row][$record]) != 0) {
5723
5724                     // First non-zero value sets the direction, $upward. Note this compares to 0,
5725                     // not the axis position. Segments are based at 0 but clip to the axis.
5726                     if ($first)
5727                         $upward = ($this_y > 0);
5728
5729                     $wy1 += $this_y;    // Keep the running total for this bar stack
5730
5731                     // Draw nothing if this segment would not increase the bar height.
5732                     // Upward bars: draw if wy1>wy2.  Downward bars: Draw if wy1<wy2.
5733                     if (($wy1 < $wy2) XOR $upward) {
5734
5735                         $y1 = $this->ytr($wy1); // Convert to device coordinates. $y1 is outermost value.
5736                         $y2 = $this->ytr($wy2); // $y2 is innermost (closest to axis).
5737
5738                         // Select the colors:
5739                         $this->GetBarColors($row, $idx, $gcvars, $data_color, $alt_color);
5740
5741                         // Draw the bar, and the shade or border:
5742                         $this->DrawBar($x1, $y1, $x2, $y2, $data_color, $alt_color,
5743                             // Only shade the top for upward bars, or the first segment of downward bars:
5744                             $upward || $first, TRUE);
5745
5746                         // Draw optional data label for this bar segment just inside the end.
5747                         // Text value is the current Y, but position is the cumulative Y.
5748                         // The label is only drawn if it fits in the segment height |y2-y1|.
5749                         if ($data_labels_within) {
5750                             $this->DrawDataValueLabel('y', $row+0.5, $wy1, $this_y,
5751                                                       'center', $upward ? 'top' : 'bottom',
5752                                                       0, $upward ? 3 : -3, NULL, abs($y1 - $y2));
5753                         }
5754                         // Mark the new end of the bar, conditional on segment height > 0.
5755                         $wy2 = $wy1;
5756                     }
5757                     $first = FALSE;
5758                 }
5759             }   // end for
5760
5761             // Draw optional data label above the bar with the total value.
5762             // Value is wy1 (total value), but position is wy2 (end of the bar stack).
5763             // These differ only with wrong-direction segments, or a stack completely clipped by the axis.
5764             if ($data_labels_end) {
5765                 $this->DrawDataValueLabel('y', $row+0.5, $wy2, $wy1, 'center', $upward ? 'bottom' : 'top',
5766                                           0, $upward ? $data_label_y_offset : 5);
5767             }
5768         }   // end for
5769         return TRUE;
5770     }
5771
5772     /*
5773      * Draw a Horizontal Stacked Bar chart
5774      * Supports only text-data-yx format, with each row in the form array(label, x1, x2, x3, ...)
5775      * Note that the data values are X not Y, and the bars are drawn horizontally.
5776      * This is called from DrawStackedBars, which has already checked the data type.
5777      */
5778     protected function DrawHorizStackedBars()
5779     {
5780         $this->CalcBarWidths(FALSE); // Calculate bar sizes for horizontal plots
5781
5782         // This is the Y offset from the bar's label center point to the bottom of the bar
5783         $y_first_bar = $this->record_bar_width / 2 - $this->bar_adjust_gap;
5784
5785         $gcvars = array(); // For GetBarColors, which initializes and uses this.
5786
5787         // Determine if any data labels are on:
5788         $data_labels_within = ($this->x_data_label_pos == 'plotstack');
5789         $data_labels_end = $data_labels_within || ($this->x_data_label_pos == 'plotin');
5790         $data_label_x_offset = 5 + $this->shading; // For rightward labels only
5791
5792         for ($row = 0; $row < $this->num_data_rows; $row++) {
5793             $record = 1;                                    // Skip record #0 (data label)
5794
5795             $y_now_pixels = $this->ytr(0.5 + $row);         // Place bars at Y=0.5, 1.5, 2.5, etc...
5796
5797             if ($this->y_data_label_pos != 'none')          // Draw Y Data labels?
5798                 $this->DrawYDataLabel($this->data[$row][0], $y_now_pixels);
5799
5800             // Lower left and upper left Y of the bars in this stack:
5801             $y1 = $y_now_pixels + $y_first_bar;
5802             $y2 = $y1 - $this->actual_bar_width;
5803
5804             // Draw the bar segments in this stack:
5805             $wx1 = 0;                       // World coordinates X1, current sum of values
5806             $wx2 = $this->y_axis_position;  // World coordinates X2, last drawn value
5807             $first = TRUE;
5808
5809             for ($idx = 0; $record < $this->num_recs[$row]; $record++, $idx++) {
5810
5811                 // Skip missing X values, and ignore X<0 values.
5812                 if (is_numeric($this->data[$row][$record])
5813                     && ($this_x = $this->data[$row][$record]) != 0) {
5814
5815                     // First non-zero value sets the direction, $rightward. Note this compares to 0,
5816                     // not the axis position. Segments are based at 0 but clip to the axis.
5817                     if ($first)
5818                         $rightward = ($this_x > 0);
5819
5820                     $wx1 += $this_x;  // Keep the running total for this bar stack
5821
5822                     // Draw nothing if this segment would not increase the bar length.
5823                     // Rightward bars: draw if wx1>wx2. Leftward bars: Draw if wx1<wx2.
5824                     if (($wx1 < $wx2) XOR $rightward) {
5825
5826                         $x1 = $this->xtr($wx1); // Convert to device coordinates. $x1 is outermost value.
5827                         $x2 = $this->xtr($wx2); // $x2 is innermost (closest to axis).
5828
5829                         // Select the colors:
5830                         $this->GetBarColors($row, $idx, $gcvars, $data_color, $alt_color);
5831
5832                         // Draw the bar, and the shade or border:
5833                         $this->DrawBar($x1, $y1, $x2, $y2, $data_color, $alt_color,
5834                             // Only shade the side for rightward bars, or the first segment of leftward bars:
5835                             TRUE, $rightward || $first);
5836                         // Draw optional data label for this bar segment just inside the end.
5837                         // Text value is the current X, but position is the cumulative X.
5838                         // The label is only drawn if it fits in the segment width |x2-x1|.
5839                         if ($data_labels_within) {
5840                             $this->DrawDataValueLabel('x', $wx1, $row+0.5, $this_x,
5841                                                       $rightward ? 'right' : 'left', 'center',
5842                                                       $rightward ? -3 : 3, 0, abs($x1 - $x2), NULL);
5843                         }
5844                         // Mark the new end of the bar, conditional on segment width > 0.
5845                         $wx2 = $wx1;
5846                     }
5847                     $first = FALSE;
5848                 }
5849             }   // end for
5850
5851             // Draw optional data label right of the bar with the total value.
5852             // Value is wx1 (total value), but position is wx2 (end of the bar stack).
5853             // These differ only with wrong-direction segments, or a stack completely clipped by the axis.
5854             if ($data_labels_end) {
5855                 $this->DrawDataValueLabel('x', $wx2, $row+0.5, $wx1, $rightward ? 'left' : 'right', 'center',
5856                                           $rightward ? $data_label_x_offset : -5, 0);
5857             }
5858         }   // end for
5859         return TRUE;
5860     }
5861
5862     /*
5863      * Draw the graph.
5864      * This is the function that performs the actual drawing, after all
5865      * the parameters and data are set up.
5866      * It also outputs the finished image, unless told not to.
5867      * Note: It is possible for this to be called multiple times.
5868      */
5869     function DrawGraph()
5870     {
5871         // Test for missing image, missing data, empty data:
5872         if (!$this->CheckDataArray())
5873             return FALSE; // Error message already reported.
5874
5875         // Allocate colors for the plot:
5876         $this->SetColorIndexes();
5877
5878         // For pie charts: don't draw grid or border or axes, and maximize area usage.
5879         // These controls can be split up in the future if needed.
5880         $draw_axes = ($this->plot_type != 'pie');
5881
5882         // Get maxima and minima for scaling:
5883         if (!$this->FindDataLimits())
5884             return FALSE;
5885
5886         // Set plot area world values (plot_max_x, etc.):
5887         if (!$this->CalcPlotAreaWorld())
5888             return FALSE;
5889
5890         // Calculate X and Y axis positions in World Coordinates:
5891         $this->CalcAxisPositions();
5892
5893         // Process label-related parameters:
5894         $this->CheckLabels();
5895
5896         // Apply grid defaults:
5897         $this->CalcGridSettings();
5898
5899         // Calculate the plot margins, if needed.
5900         // For pie charts, set the $maximize argument to maximize space usage.
5901         $this->CalcMargins(!$draw_axes);
5902
5903         // Calculate the actual plot area in device coordinates:
5904         $this->CalcPlotAreaPixels();
5905
5906         // Calculate the mapping between world and device coordinates:
5907         $this->CalcTranslation();
5908
5909         // Pad color and style arrays to fit records per group:
5910         $this->PadArrays();
5911         $this->DoCallback('draw_setup');
5912
5913         $this->DrawBackground();
5914         $this->DrawImageBorder();
5915         $this->DoCallback('draw_image_background');
5916
5917         $this->DrawPlotAreaBackground();
5918         $this->DoCallback('draw_plotarea_background', $this->plot_area);
5919
5920         $this->DrawTitle();
5921         if ($draw_axes) {  // If no axes (pie chart), no axis titles either
5922             $this->DrawXTitle();
5923             $this->DrawYTitle();
5924         }
5925         $this->DoCallback('draw_titles');
5926
5927         if ($draw_axes && ! $this->grid_at_foreground) {   // Usually one wants grids to go back, but...
5928             $this->DrawYAxis();     // Y axis must be drawn before X axis (see DrawYAxis())
5929             $this->DrawXAxis();
5930             $this->DoCallback('draw_axes');
5931         }
5932
5933         switch ($this->plot_type) {
5934         case 'thinbarline':
5935             $this->DrawThinBarLines();
5936             break;
5937         case 'area':
5938             $this->DrawArea();
5939             break;
5940         case 'squared':
5941             $this->DrawSquared();
5942             break;
5943         case 'lines':
5944             $this->DrawLines();
5945             break;
5946         case 'linepoints':
5947             $this->DrawLinePoints();
5948             break;
5949         case 'points';
5950             $this->DrawDots();
5951             break;
5952         case 'pie':
5953             $this->DrawPieChart();
5954             break;
5955         case 'stackedbars':
5956             $this->DrawStackedBars();
5957             break;
5958         case 'stackedarea':
5959             $this->DrawArea(TRUE);
5960             break;
5961         // case 'bars':
5962         default:
5963             $this->DrawBars();
5964             break;
5965         }   // end switch
5966         $this->DoCallback('draw_graph', $this->plot_area);
5967
5968         if ($draw_axes && $this->grid_at_foreground) {   // Usually one wants grids to go back, but...
5969             $this->DrawYAxis();     // Y axis must be drawn before X axis (see DrawYAxis())
5970             $this->DrawXAxis();
5971             $this->DoCallback('draw_axes');
5972         }
5973
5974         if ($draw_axes) {
5975             $this->DrawPlotBorder();
5976             $this->DoCallback('draw_border');
5977         }
5978
5979         if ($this->legend) {
5980             $this->DrawLegend();
5981             $this->DoCallback('draw_legend');
5982         }
5983         $this->DoCallback('draw_all', $this->plot_area);
5984
5985         if ($this->print_image && !$this->PrintImage())
5986             return FALSE;
5987
5988         return TRUE;
5989     }
5990
5991 /////////////////////////////////////////////
5992 //////////////////         DEPRECATED METHODS
5993 /////////////////////////////////////////////
5994
5995     /*
5996      * Note on deprecated methods - as these pre-date the PHPlot Reference
5997      * Manual, and there is minimal documentation about them, I have neither
5998      * removed them nor changed them. They are not tested or documented, and
5999      * should not be used.
6000      */
6001
6002     /*
6003      * Deprecated, use SetYTickPos()
6004      */
6005     function SetDrawVertTicks($which_dvt)
6006     {
6007         if ($which_dvt != 1)
6008             $this->SetYTickPos('none');
6009         return TRUE;
6010     }
6011
6012     /*
6013      * Deprecated, use SetXTickPos()
6014      */
6015     function SetDrawHorizTicks($which_dht)
6016     {
6017         if ($which_dht != 1)
6018            $this->SetXTickPos('none');
6019         return TRUE;
6020     }
6021
6022     /*
6023      * Deprecated - use SetNumXTicks()
6024      */
6025     function SetNumHorizTicks($n)
6026     {
6027         return $this->SetNumXTicks($n);
6028     }
6029
6030     /*
6031      * Deprecated - use SetNumYTicks()
6032      */
6033     function SetNumVertTicks($n)
6034     {
6035         return $this->SetNumYTicks($n);
6036     }
6037
6038     /*
6039      * Deprecated - use SetXTickIncrement()
6040      */
6041     function SetHorizTickIncrement($inc)
6042     {
6043         return $this->SetXTickIncrement($inc);
6044     }
6045
6046     /*
6047      * Deprecated - use SetYTickIncrement()
6048      */
6049     function SetVertTickIncrement($inc)
6050     {
6051         return $this->SetYTickIncrement($inc);
6052     }
6053
6054     /*
6055      * Deprecated - use SetYTickPos()
6056      */
6057     function SetVertTickPosition($which_tp)
6058     {
6059         return $this->SetYTickPos($which_tp);
6060     }
6061
6062     /*
6063      * Deprecated - use SetXTickPos()
6064      */
6065     function SetHorizTickPosition($which_tp)
6066     {
6067         return $this->SetXTickPos($which_tp);
6068     }
6069
6070     /*
6071      * Deprecated - use SetFont()
6072      */
6073     function SetTitleFontSize($which_size)
6074     {
6075         return $this->SetFont('title', $which_size);
6076     }
6077
6078     /*
6079      * Deprecated - use SetFont()
6080      */
6081     function SetAxisFontSize($which_size)
6082     {
6083         $this->SetFont('x_label', $which_size);
6084         $this->SetFont('y_label', $which_size);
6085     }
6086
6087     /*
6088      * Deprecated - use SetFont()
6089      */
6090     function SetSmallFontSize($which_size)
6091     {
6092         return $this->SetFont('generic', $which_size);
6093     }
6094
6095     /*
6096      * Deprecated - use SetFont()
6097      */
6098     function SetXLabelFontSize($which_size)
6099     {
6100         return $this->SetFont('x_title', $which_size);
6101     }
6102
6103     /*
6104      * Deprecated - use SetFont()
6105      */
6106     function SetYLabelFontSize($which_size)
6107     {
6108         return $this->SetFont('y_title', $which_size);
6109     }
6110
6111     /*
6112      * Deprecated - use SetXTitle()
6113      */
6114     function SetXLabel($which_xlab)
6115     {
6116         return $this->SetXTitle($which_xlab);
6117     }
6118
6119     /*
6120      * Deprecated - use SetYTitle()
6121      */
6122     function SetYLabel($which_ylab)
6123     {
6124         return $this->SetYTitle($which_ylab);
6125     }
6126
6127     /*
6128      * Deprecated - use SetXTickLength() and SetYTickLength() instead.
6129      */
6130     function SetTickLength($which_tl)
6131     {
6132         $this->SetXTickLength($which_tl);
6133         $this->SetYTickLength($which_tl);
6134         return TRUE;
6135     }
6136
6137     /*
6138      * Deprecated - use SetYLabelType()
6139      */
6140     function SetYGridLabelType($which_yglt)
6141     {
6142         return $this->SetYLabelType($which_yglt);
6143     }
6144
6145     /*
6146      * Deprecated - use SetXLabelType()
6147      */
6148     function SetXGridLabelType($which_xglt)
6149     {
6150         return $this->SetXLabelType($which_xglt);
6151     }
6152     /*
6153      * Deprecated - use SetYTickLabelPos()
6154      */
6155     function SetYGridLabelPos($which_yglp)
6156     {
6157         return $this->SetYTickLabelPos($which_yglp);
6158     }
6159     /*
6160      * Deprecated - use SetXTickLabelPos()
6161      */
6162     function SetXGridLabelPos($which_xglp)
6163     {
6164         return $this->SetXTickLabelPos($which_xglp);
6165     }
6166
6167     /*
6168      * Deprecated - use SetXtitle()
6169      */
6170     function SetXTitlePos($xpos)
6171     {
6172         $this->x_title_pos = $xpos;
6173         return TRUE;
6174     }
6175
6176     /*
6177      * Deprecated - use SetYTitle()
6178      */
6179     function SetYTitlePos($xpos)
6180     {
6181         $this->y_title_pos = $xpos;
6182         return TRUE;
6183     }
6184
6185     /*
6186      * Deprecated - use SetXDataLabelPos()
6187      */
6188     function SetDrawXDataLabels($which_dxdl)
6189     {
6190         if ($which_dxdl == '1' )
6191             $this->SetXDataLabelPos('plotdown');
6192         else
6193             $this->SetXDataLabelPos('none');
6194     }
6195
6196     /*
6197      * Deprecated - use SetPlotAreaPixels()
6198      */
6199     function SetNewPlotAreaPixels($x1, $y1, $x2, $y2)
6200     {
6201         return $this->SetPlotAreaPixels($x1, $y1, $x2, $y2);
6202     }
6203
6204     /*
6205      * Deprecated - use SetLineWidths().
6206      */
6207     function SetLineWidth($which_lw)
6208     {
6209
6210         $this->SetLineWidths($which_lw);
6211
6212         if (!$this->error_bar_line_width) {
6213             $this->SetErrorBarLineWidth($which_lw);
6214         }
6215         return TRUE;
6216     }
6217
6218     /*
6219      * Deprecated - use SetPointShapes().
6220      */
6221     function SetPointShape($which_pt)
6222     {
6223         $this->SetPointShapes($which_pt);
6224         return TRUE;
6225     }
6226
6227     /*
6228      * Deprecated - use SetPointSizes().
6229      */
6230     function SetPointSize($which_ps)
6231     {
6232         $this->SetPointSizes($which_ps);
6233         return TRUE;
6234     }
6235 }
6236
6237 /*
6238  * The PHPlot_truecolor class extends PHPlot to use GD truecolor images.
6239  */
6240
6241 class PHPlot_truecolor extends PHPlot
6242 {
6243     /*
6244      * PHPlot Truecolor variation constructor: Create a PHPlot_truecolor object and initialize it.
6245      * Note this does NOT call the parent (PHPlot) constructor. It duplicates the code here.
6246      * Everything is the same as the PHPlot constructor except for imagecreatetruecolor.
6247      *
6248      * Parameters are the same as PHPlot:
6249      *   $which_width : Image width in pixels.
6250      *   $which_height : Image height in pixels.
6251      *   $which_output_file : Filename for output.
6252      *   $which_input_file : Path to a file to be used as background.
6253      */
6254     function __construct($which_width=600, $which_height=400, $which_output_file=NULL, $which_input_file=NULL)
6255     {
6256         $this->SetRGBArray($this->color_array);
6257
6258         if ($which_output_file)
6259             $this->SetOutputFile($which_output_file);
6260
6261         if ($which_input_file) {
6262             $this->SetInputFile($which_input_file);
6263         } else {
6264             $this->image_width = $which_width;
6265             $this->image_height = $which_height;
6266
6267             $this->img = imagecreatetruecolor($this->image_width, $this->image_height);
6268             if (! $this->img)
6269                 return $this->PrintError('PHPlot_truecolor(): Could not create image resource.');
6270         }
6271
6272         $this->SetDefaultStyles();
6273         $this->SetDefaultFonts();
6274     }
6275 }