Location: PHPKode > scripts > PHPlot > phplot-5.8.0/phplot.php
<?php
/* $Id: phplot.php 1264 2012-04-07 00:39:48Z lbayuk $ */
/*
 * PHPLOT Version 5.8.0
 *
 * A PHP class for creating scientific and business charts
 * Visit http://sourceforge.net/projects/phplot/
 * for PHPlot documentation, downloads, and discussions.
 * ---------------------------------------------------------------------
 * Copyright (C) 1998-2012 Afan Ottenheimer
 *
 * This is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 * ---------------------------------------------------------------------
 *
 * Co-author and maintainer (2003-2005)
 * Miguel de Benito Delgado <nonick AT vodafone DOT es>
 *
 * Maintainer (2006-present)
 * <lbayuk AT users DOT sourceforge DOT net>
 *
 * Requires PHP 5.3 or later.
 */

class PHPlot
{
    const version = '5.8.0';

    /* Declare class variables which are initialized to static values. Many more class variables
     * are used, defined as needed, but are unset by default.
     * All these are declared as public. While it is tempting to make them private or protected, this
     * is avoided for two reasons. First, it will break existing code, since all member variables
     * were public in PHP4 and who knows what internal variables people used. Second, it makes
     * testing harder and less effective. Nevertheless, your code should not modify these.
     */

    public $is_inline = FALSE;             // FALSE = Sends headers, TRUE = sends just raw image data
    public $browser_cache = FALSE;         // FALSE = Sends headers for browser to not cache the image,
                                           // (only if is_inline = FALSE also)
    public $print_image = TRUE;            // DrawGraph calls PrintImage. See SetPrintImage

    public $safe_margin = 5;               // Extra margin used in several places, in pixels

    public $x_axis_position = '';          // X axis position in Y world coordinates, blank for default.
    public $y_axis_position = '';          // Y axis position in X world coordinates, blank for default.

    public $xscale_type = 'linear';        // linear, log
    public $yscale_type = 'linear';

//Fonts
    public $use_ttf  = FALSE;              // Use True Type Fonts by default?
    public $ttf_path = '.';                // Default path to look in for TT Fonts.
    // public $default_ttfont;             // Initialized in GetDefaultTTFont
    public $line_spacing = 4;              // Controls line spacing of multi-line labels

    // Label angles: 0 or 90 degrees for fixed fonts, any for TTF
    public $x_label_angle = 0;             // For X tick labels
    // public $x_data_label_angle;         // For X data labels; defaults to x_label_angle - see CheckLabels()
    public $y_label_angle = 0;             // For Y tick labels
    public $y_data_label_angle = 0;        // For Y data labels

//Formats
    public $file_format = 'png';
    public $output_file = '';              // For output to a file instead of stdout

//Data
    public $data_type = 'text-data';       // Structure of the data array
    public $plot_type = 'linepoints';      // See $plots[] below

    public $label_scale_position = 0.5;    // Shifts data labels in pie charts. 1 = top, 0 = bottom
    public $group_frac_width = 0.7;        // Bars use this fraction (0 to 1) of a group's space
    public $bar_extra_space = 0.5;         // Number of extra bar's worth of space in a group
    public $bar_width_adjust = 1;          // 1 = bars of normal width, must be > 0

// Titles
    public $title_txt = '';

    public $x_title_txt = '';
    public $x_title_pos = 'none';          // plotdown, plotup, both, none

    public $y_title_txt = '';
    public $y_title_pos = 'none';          // plotleft, plotright, both, none

//Labels
    // There are two types of labels in PHPlot:
    //    Tick labels: Follow the grid, next to ticks in axis.
    //                 Are drawn at grid drawing time, by DrawXTicks() and DrawYTicks()
    //    Data labels: Follow the data points, and can be placed on the axis or the plot (x/y)
    //                 Are drawn at graph plotting time, by Draw*DataLabel(), called by DrawLines(), etc.
    //                 DrawXDataLabel() also draws vertical lines to data points, depending on
    //                 draw_x_data_label_lines.
    // Tick and Data label positions are not initialized, because PHPlot needs to tell if they
    // defaulted or are set by the user. See CheckLabels() for details. The variables and
    // effective defaults are shown here in comments (but CheckLabels adjusts the defaults).
    // public $x_tick_label_pos = 'plotdown';     // X tick label position
    // public $y_tick_label_pos = 'plotleft';     // Y tick label position
    // public $x_data_label_pos = 'plotdown';     // X data label position
    // public $y_data_label_pos = 'none';         // Y data label position

    public $draw_x_data_label_lines = FALSE;   // Draw a line from the data point to the axis?

    // Label format controls: See SetLabelType() and FormatLabel()
    // Outer index is the type of label: x, y (tick labels); xd, yd (data labels); p (pie labels).
    // Inner indexes are:
    //    type, precision, prefix, suffix, time_format, printf_format, custom_callback, custom_arg.
    public $label_format = array('x' => array(), 'xd' => array(), 'y' => array(), 'yd' => array());
    // data_units_text is retained for backward compatibility, because there was never a function
    // to set it. Use the 'suffix' argument to Set[XY]LabelType instead.
    public $data_units_text = '';              // Units text for 'data' labels (i.e: 'ยค', '$', etc.)

// Legend
    public $legend = '';                       // An array with legend titles
    // Other legend_* variables are set as needed, unset for default values.

//Ticks
    public $x_tick_length = 5;                 // tick length in pixels for upper/lower axis
    public $y_tick_length = 5;                 // tick length in pixels for left/right axis

    public $x_tick_cross = 3;                  // ticks cross x axis this many pixels
    public $y_tick_cross = 3;                  // ticks cross y axis this many pixels

    public $x_tick_pos = 'plotdown';           // plotdown, plotup, both, xaxis, none
    public $y_tick_pos = 'plotleft';           // plotright, plotleft, both, yaxis, none

    public $num_x_ticks = '';
    public $num_y_ticks = '';

    public $x_tick_inc = '';                   // Set num_x_ticks or x_tick_inc, not both.
    public $y_tick_inc = '';                   // Set num_y_ticks or y_tick_inc, not both.

    public $skip_top_tick = FALSE;
    public $skip_bottom_tick = FALSE;
    public $skip_left_tick = FALSE;
    public $skip_right_tick = FALSE;

//Grid Formatting
    // public $draw_x_grid = FALSE;            // Default is False except for swapped data type
    // public $draw_y_grid = TRUE;             // Default is True except for swapped data type

    public $dashed_grid = TRUE;
    public $grid_at_foreground = FALSE;        // Chooses whether to draw the grid below or above the graph

//Colors and styles       (all colors can be array (R,G,B) or named color)
    public $color_array = 'small';             // 'small', 'large' or array (define your own colors)
                                            // See rgb.inc.php and SetRGBArray()
    public $default_colors = array(       // The default colors for data and error bars
        'SkyBlue', 'green', 'orange', 'blue', 'red', 'DarkGreen', 'purple', 'peru',
        'cyan', 'salmon', 'SlateBlue', 'YellowGreen', 'magenta', 'aquamarine1', 'gold', 'violet');

    // See SetDefaultStyles() for default colors for PHPlot elements.

    public $line_widths = 1;                  // single value or array
    public $line_styles = array('solid', 'solid', 'dashed');   // single value or array
    public $dashed_style = '2-4';              // colored dots-transparent dots

    public $point_sizes = array(6);            // Array of sizes for points. See CheckPointParams()
    public $point_shapes = array(              // Array of point shapes. See SetPointShapes() and DrawDot()
          'diamond', 'dot', 'delta', 'home', 'yield', 'box', 'circle', 'up', 'down', 'cross'
       );

    public $error_bar_size = 5;                // right and left size of tee
    public $error_bar_shape = 'tee';           // 'tee' or 'line'
    public $error_bar_line_width = 1;          // single value (or array TODO)

    public $image_border_type = 'none';        // 'raised', 'plain', 'none'

    public $shading = 5;                       // 0 for no shading, > 0 is size of shadows in pixels

    public $draw_plot_area_background = FALSE;
    public $draw_broken_lines = FALSE;          // Tells not to draw lines for missing Y data.

//Miscellaneous
    public $callbacks = array(                  // Valid callback reasons (see SetCallBack)
        'data_points' => NULL,
        'draw_setup' => NULL,
        'draw_image_background' => NULL,
        'draw_plotarea_background' => NULL,
        'draw_titles' => NULL,
        'draw_axes' => NULL,
        'draw_graph' => NULL,
        'draw_border' => NULL,
        'draw_legend' => NULL,
        'draw_all' => NULL,
        'data_color' => NULL,
        'debug_textbox' => NULL,  // For testing/debugging text box alignment
        'debug_scale' => NULL,    // For testing/debugging scale setup
    );

    // Defined plot types static array:
    // Array key is the plot type. (Upper case letters are not allowed due to CheckOption)
    // Value is an array with these keys:
    //   draw_method (required) : Class method to call to draw the plot.
    //   draw_arg : Optional array of arguments to pass to draw_method.
    //   draw_axes : If FALSE, do not draw X/Y axis lines, labels, ticks, grid, titles.
    //   abs_vals, sum_vals : Data array processing flags. See FindDataLimits().
    static protected $plots = array(
        'area' => array(
            'draw_method' => 'DrawArea',
            'abs_vals' => TRUE,
        ),
        'bars' => array(
            'draw_method' => 'DrawBars',
        ),
        'bubbles' => array(
            'draw_method' => 'DrawBubbles',
        ),
        'candlesticks' => array(
            'draw_method' => 'DrawOHLC',
            'draw_arg' => array(TRUE, FALSE), // Draw candlesticks, only fill if "closed down"
        ),
        'candlesticks2' => array(
            'draw_method' => 'DrawOHLC',
            'draw_arg' => array(TRUE, TRUE), // Draw candlesticks, fill always
        ),
        'linepoints' => array(
            'draw_method' => 'DrawLinePoints',
        ),
        'lines' => array(
            'draw_method' => 'DrawLines',
        ),
        'ohlc' => array(
            'draw_method' => 'DrawOHLC',
            'draw_arg' => array(FALSE), // Don't draw candlesticks
        ),
        'pie' => array(
            'draw_method' => 'DrawPieChart',
            'draw_axes' => FALSE,
            'abs_vals' => TRUE,
        ),
        'points' => array(
            'draw_method' => 'DrawDots',
        ),
        'squared' => array(
            'draw_method' => 'DrawSquared',
        ),
        'stackedarea' => array(
            'draw_method' => 'DrawArea',
            'draw_arg' => array(TRUE), // Tells DrawArea to draw stacked area plot
            'sum_vals' => TRUE,
            'abs_vals' => TRUE,
        ),
        'stackedbars' => array(
            'draw_method' => 'DrawStackedBars',
            'sum_vals' => TRUE,
        ),
        'thinbarline' => array(
            'draw_method' => 'DrawThinBarLines',
        ),
    );

//////////////////////////////////////////////////////
//BEGIN CODE
//////////////////////////////////////////////////////

    /*
     * Constructor: Setup img resource, colors and size of the image, and font sizes.
     *   $width : Image width in pixels.
     *   $height : Image height in pixels.
     *   $output_file : Filename for output.
     *   $input_file : Path to a file to be used as background.
     */
    function PHPlot($width=600, $height=400, $output_file=NULL, $input_file=NULL)
    {
        $this->initialize('imagecreate', $width, $height, $output_file, $input_file);
    }

    /*
     * Initialize a PHPlot object. This is used by both PHPlot and PHPlot_truecolor constructors.
     */
    protected function initialize($imagecreate_function, $width, $height, $output_file, $input_file)
    {
        $this->SetRGBArray($this->color_array);

        if (!empty($output_file))
            $this->SetOutputFile($output_file);

        if (!empty($input_file)) {
            $this->SetInputFile($input_file);
        } else {
            $this->image_width = $width;
            $this->image_height = $height;
            $this->img = call_user_func($imagecreate_function, $width, $height);
            if (!$this->img)
                return $this->PrintError(get_class($this) . '(): Could not create image resource.');
        }
        $this->SetDefaultStyles();
        $this->SetDefaultFonts();
    }

    /*
     * Support for serialize/unserialize: Prepare object for serialization.
     * The image resource cannot be serialized. But rather than try to filter it out from the other
     * properties, just let PHP serialize it (it will become an integer=0), and then fix it in __wakeup.
     * This way the object is still usable after serialize().
     * Note: This does not work if an input file was provided to the constructor.
     */
    function __sleep()
    {
        $this->truecolor = imageistruecolor($this->img); // Remember image type
        $this->saved_version = PHPlot::version; // Remember version of PHPlot, for checking on unserialize
        return array_keys(get_object_vars($this));
    }

    /*
     * Support for serialize/unserialize: Cleanup after unserialization - recreate the image resource.
     */
    function __wakeup()
    {
        if (strcmp($this->saved_version, PHPlot::version) != 0)
            $this->PrintError(get_class($this) . '(): Unserialize version mismatch');
        $imagecreate_function = $this->truecolor ? 'imagecreatetruecolor' : 'imagecreate';
        $this->img = call_user_func($imagecreate_function, $this->image_width, $this->image_height);
        if (!$this->img)
            $this->PrintError(get_class($this) . '(): Could not create image resource.');
        unset($this->truecolor, $this->saved_version);
    }

    /*
     * Reads an image file. Stores width and height, and returns the image
     * resource. On error, calls PrintError and returns False.
     * This is used by the constructor via SetInputFile, and by tile_img().
     */
    protected function GetImage($image_filename, &$width, &$height)
    {
        $error = '';
        $size = getimagesize($image_filename);
        if (!$size) {
            $error = "Unable to query image file $image_filename";
        } else {
            $image_type = $size[2];
            switch ($image_type) {
            case IMAGETYPE_GIF:
                $img = @ ImageCreateFromGIF ($image_filename);
                break;
            case IMAGETYPE_PNG:
                $img = @ ImageCreateFromPNG ($image_filename);
                break;
            case IMAGETYPE_JPEG:
                $img = @ ImageCreateFromJPEG ($image_filename);
                break;
            default:
                $error = "Unknown image type ($image_type) for image file $image_filename";
                break;
            }
        }
        if (empty($error) && !$img) {
            // getimagesize is OK, but GD won't read it. Maybe unsupported format.
            $error = "Failed to read image file $image_filename";
        }
        if (!empty($error)) {
            return $this->PrintError("GetImage(): $error");
        }
        $width = $size[0];
        $height = $size[1];
        return $img;
    }

    /*
     * Selects an input file to be used as background for the whole graph.
     * This resets the graph size to the image's size.
     * Note: This is used by the constructor. It is deprecated for direct use.
     */
    function SetInputFile($which_input_file)
    {
        $im = $this->GetImage($which_input_file, $this->image_width, $this->image_height);
        if (!$im)
            return FALSE;  // GetImage already produced an error message.

        // Deallocate any resources previously allocated
        if (isset($this->img))
            imagedestroy($this->img);

        $this->img = $im;

        // Do not overwrite the input file with the background color.
        $this->done['background'] = TRUE;

        return TRUE;
    }

/////////////////////////////////////////////
//////////////                         COLORS
/////////////////////////////////////////////

    /*
     * Allocate a GD color index for a color specified by a 4 component array.
     * When a color is requested, it is parsed and checked by SetRGBColor, and then saved as an array
     * of (R,G,B,A) components. At graph drawing time, this function is used to allocate the color.
     *   $color : The color specification as a 4 component array: R, G, B, A. This is passed as
     *            a reference argument because it might be unset (see next argument).
     *   $default_color_index : An already-allocated GD color index to use as default, if $color is unset.
     * Returns: A GD color index that can be used when drawing.
     */
    protected function GetColorIndex(&$color, $default_color_index = 0)
    {
        if (empty($color)) return $default_color_index;
        list($r, $g, $b, $a) = $color;
        return imagecolorresolvealpha($this->img, $r, $g, $b, $a);
    }

    /*
     * Allocate an array of GD color indexes for an array of color specifications.
     * This is used for the data_colors array, for example.
     *  $color_array : Array of color specifications, each an array of R,G,B,A components.
     *     This must use 0-based sequential integer indexes.
     *  $max_colors : Limit color allocation to no more than this.
     * Returns an array of GD color indexes.
     */
    protected function GetColorIndexArray($color_array, $max_colors)
    {
        $n = min(count($color_array), $max_colors);
        $result = array();
        for ($i = 0; $i < $n; $i++)
            $result[] = $this->GetColorIndex($color_array[$i]);
        return $result;
    }

    /*
     * Allocate an array of GD color indexes for darker shades of an array of color specifications.
     *  $color_array : Array of color specifications, each an array of R,G,B,A components.
     *  $max_colors : Limit color allocation to this many colors from the array.
     * Returns an array of GD color indexes.
     */
    protected function GetDarkColorIndexArray($color_array, $max_colors)
    {
        $n = min(count($color_array), $max_colors);
        $result = array();
        for ($i = 0; $i < $n; $i++)
            $result[] = $this->GetDarkColorIndex($color_array[$i]);
        return $result;
    }

    /*
     * Allocate a GD color index for a darker shade of a color specified by a 4 component array.
     * See notes for GetColorIndex() above.
     *   $color : The color specification as a 4 component array: R, G, B, A.
     * Returns: A GD color index that can be used when drawing.
     */
    protected function GetDarkColorIndex($color)
    {
        list ($r, $g, $b, $a) = $color;
        $r = max(0, $r - 0x30);
        $g = max(0, $g - 0x30);
        $b = max(0, $b - 0x30);
        return imagecolorresolvealpha($this->img, $r, $g, $b, $a);
    }

    /*
     * Sets/reverts all colors and styles to their defaults.
     */
    protected function SetDefaultStyles()
    {
        $this->SetDefaultDashedStyle($this->dashed_style);
        $this->SetImageBorderColor(array(194, 194, 194));
        $this->SetPlotBgColor('white');
        $this->SetBackgroundColor('white');
        $this->SetTextColor('black');
        $this->SetGridColor('black');
        $this->SetLightGridColor('gray');
        $this->SetTickColor('black');
        $this->SetTitleColor('black');
        // These functions set up the default colors when called without parameters
        $this->SetDataColors();
        $this->SetErrorBarColors();
        $this->SetDataBorderColors();
        return TRUE;
    }

    /*
     * Set the image background color to $which_color.
     */
    function SetBackgroundColor($which_color)
    {
        return (bool)($this->bg_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the plot area background color (if enabled) to $which_color.
     */
    function SetPlotBgColor($which_color)
    {
        return (bool)($this->plot_bg_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the color of the titles (main, X, and Y) to $which_color.
     * See also SetXTitleColor and SetYTitleColor.
     */
    function SetTitleColor($which_color)
    {
        return (bool)($this->title_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the color of the X title to $which_color.
     * This overrides the color set with SetTitleColor.
     */
    function SetXTitleColor($which_color)
    {
        return (bool)($this->x_title_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the color of the Y title to $which_color.
     * This overrides the color set with SetTitleColor.
     */
    function SetYTitleColor($which_color)
    {
        return (bool)($this->y_title_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the color of the axis tick marks to $which_color.
     */
    function SetTickColor($which_color)
    {
        return (bool)($this->tick_color = $this->SetRGBColor($which_color));
    }

    /*
     * Deprecated. Use SetTitleColor()
     */
    function SetLabelColor($which_color)
    {
        return $this->SetTitleColor($which_color);
    }

    /*
     * Set the general text color (legend, and default for tick and data labels) to $which_color.
     */
    function SetTextColor($which_color)
    {
        return (bool)($this->text_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the color for data labels, overriding the default set with SetTextColor.
     */
    function SetDataLabelColor($which_color)
    {
        return (bool)($this->datalabel_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the color for data value labels, overriding defaults from SetTextColor and SetDataLabelColor.
     * Note: These are the labels on and in bars, and above points - within the plot area.
     */
    function SetDataValueLabelColor($which_color)
    {
        return (bool)($this->dvlabel_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the color for pie chart data labels. Historically, these used the GridColor.
     */
    function SetPieLabelColor($which_color)
    {
        return (bool)($this->pielabel_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the color for tick labels, overriding the default set with SetTextColor.
     */
    function SetTickLabelColor($which_color)
    {
        return (bool)($this->ticklabel_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the X and Y grid colors to $which_color. Also sets the data label line color.
     */
    function SetLightGridColor($which_color)
    {
        return (bool)($this->light_grid_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the color used for the X and Y axis, plot border, legend border to $which_color.
     * Note: This has nothing to do with the grid, and we don't recall where this name came from.
     */
    function SetGridColor($which_color)
    {
        return (bool)($this->grid_color = $this->SetRGBColor($which_color));
    }

    /*
     * Set the color used for the image border to $which_color.
     */
    function SetImageBorderColor($which_color)
    {
        return (bool)($this->i_border = $this->SetRGBColor($which_color));
    }

    /*
     * Designate color $which_color to be transparent, if supported by the image format.
     */
    function SetTransparentColor($which_color)
    {
        return (bool)($this->transparent_color = $this->SetRGBColor($which_color));
    }

    /*
     * Sets the array of colors to be used. It can be user defined, a small predefined one
     * or a large one included from 'rgb.inc.php'.
     *
     *    $which_color_array : A color array, or 'small' or 'large'.
     * Color arrays map color names into arrays of R, G, B and optionally A values.
     */
    function SetRGBArray($which_color_array)
    {
        if (is_array($which_color_array)) {           // User defined array
            $this->rgb_array = $which_color_array;
        } elseif ($which_color_array == 'small') {      // Small predefined color array
            $this->rgb_array = array(
                'white'          => array(255, 255, 255),
                'snow'           => array(255, 250, 250),
                'PeachPuff'      => array(255, 218, 185),
                'ivory'          => array(255, 255, 240),
                'lavender'       => array(230, 230, 250),
                'black'          => array(  0,   0,   0),
                'DimGrey'        => array(105, 105, 105),
                'gray'           => array(190, 190, 190),
                'grey'           => array(190, 190, 190),
                'navy'           => array(  0,   0, 128),
                'SlateBlue'      => array(106,  90, 205),
                'blue'           => array(  0,   0, 255),
                'SkyBlue'        => array(135, 206, 235),
                'cyan'           => array(  0, 255, 255),
                'DarkGreen'      => array(  0, 100,   0),
                'green'          => array(  0, 255,   0),
                'YellowGreen'    => array(154, 205,  50),
                'yellow'         => array(255, 255,   0),
                'orange'         => array(255, 165,   0),
                'gold'           => array(255, 215,   0),
                'peru'           => array(205, 133,  63),
                'beige'          => array(245, 245, 220),
                'wheat'          => array(245, 222, 179),
                'tan'            => array(210, 180, 140),
                'brown'          => array(165,  42,  42),
                'salmon'         => array(250, 128, 114),
                'red'            => array(255,   0,   0),
                'pink'           => array(255, 192, 203),
                'maroon'         => array(176,  48,  96),
                'magenta'        => array(255,   0, 255),
                'violet'         => array(238, 130, 238),
                'plum'           => array(221, 160, 221),
                'orchid'         => array(218, 112, 214),
                'purple'         => array(160,  32, 240),
                'azure1'         => array(240, 255, 255),
                'aquamarine1'    => array(127, 255, 212)
                );
        } elseif ($which_color_array == 'large')  {    // Large color array
            if (!@include('rgb.inc.php')) {
                return $this->PrintError("SetRGBArray(): Large color map could not be loaded\n"
                                       . "from 'rgb.inc.php'.");
            }
            $this->rgb_array = $ColorArray;
        } else {                                        // Default to black and white only.
            $this->rgb_array = array('white' => array(255, 255, 255), 'black' => array(0, 0, 0));
        }

        return TRUE;
    }

    /*
     * Parse a color description and return the color component values.
     * Arguments:
     *   $color_asked : The desired color description, in one of these forms:
     *       Component notation: array(R, G, B) or array(R, G, B, A) with each
     *          in the range described below for the return value.
     *          Examples: (255,255,0)  (204,0,0,30)
     *       Hex notation: "#RRGGBB" or "#RRGGBBAA" where each pair is a 2 digit hex number.
     *          Examples: #FF00FF (magenta)   #0000FF40 (Blue with alpha=64/127)
     *       Named color in the current colormap, with optional suffix ":alpha" for alpha value.
     *          Examples:  blue   red:60  yellow:20
     *   $alpha : optional default alpha value. This is applied to the color if it doesn't
     *       already have an alpha value. If not supplied, colors are opaque (alpha=0) by default.
     *
     * Returns an array describing a color as (R, G, B, Alpha).
     * R, G, and B are integers 0-255, and Alpha is 0 (opaque) to 127 (transparent).
     * Note: This function should be considered 'protected', and is not documented for public use.
     */
    function SetRGBColor($color_asked, $alpha = 0)
    {
        if (empty($color_asked)) {
            $ret_val = array(0, 0, 0);

        } elseif (is_array($color_asked) && (($n = count($color_asked)) == 3 || $n == 4) ) {
            // Already an array of 3 or 4 elements:
            $ret_val = $color_asked;

        } elseif (preg_match('/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i',
                             $color_asked, $ss)) {
            // #RRGGBB or #RRGGBBAA notation:
            $ret_val = array(hexdec($ss[1]), hexdec($ss[2]), hexdec($ss[3]));
            if (isset($ss[4])) $ret_val[] = hexdec($ss[4]);

        } elseif (isset($this->rgb_array[$color_asked])) {
            // Color by name:
            $ret_val = $this->rgb_array[$color_asked];

        } elseif (preg_match('/(.+):([\d]+)$/', $color_asked, $ss)
                  && isset($this->rgb_array[$ss[1]])) {
            // Color by name with ":alpha" suffix, alpha is a decimal number:
            $ret_val = $this->rgb_array[$ss[1]];
            $ret_val[3] = (int)$ss[2];

        } else {
            return $this->PrintError("SetRGBColor(): Color '$color_asked' is not valid.");
        }

        // Append alpha if not already provided for:
        if (count($ret_val) == 3)
            $ret_val[] = $alpha;
        return $ret_val;
    }

    /*
     * Sets the colors for the data, with optional default alpha value
     * Cases are:
     *    SetDataColors(array(...))  : Use the supplied array as the color map.
     *    SetDataColors(colorname)   : Use an array of just colorname as the color map.
     *    SetDataColors() or SetDataColors(NULL) : Load default color map if no color map is already set.
     *    SetDataColors('') or SetDataColors(False) : Load default color map (even if one is already set).
     *  $which_border is passed to SetDataBorderColors, for backward compatibility.
     *  $alpha is a default Alpha to apply to all data colors that do not have alpha.
     *    The default for this is NULL, not 0, so we can tell if it was defaulted. But the effective
     *    default value is 0 (opaque).
     */
    function SetDataColors($which_data = NULL, $which_border = NULL, $alpha = NULL)
    {
        if (is_array($which_data)) {
            $colors = $which_data;  // Use supplied array
        } elseif (!empty($which_data)) {
            $colors = array($which_data);  // Use supplied single color
        } elseif (empty($this->data_colors) || !is_null($which_data)) {
            $colors = $this->default_colors;  // Use default color array
        } else {
            // which_data is NULL or missing and a color array is already set.
            // The existing color array is left alone, except that if $alpha is
            // given this will replace the alpha value of each existing color.
            // This makes SetDataColors(NULL, NULL, $alpha) work.
            if (isset($alpha)) {
                $n_colors = count($this->data_colors);
                for ($i = 0; $i < $n_colors; $i++) {
                    $this->data_colors[$i][3] = $alpha; // Component 3 = alpha value
                }
            }
            // No need to reparse the colors or anything else.
            return TRUE;
        }

        if (!isset($alpha))
            $alpha = 0; // Actual default is opaque colors.

        // Check each color and convert to array (r,g,b,a) form.
        // Use the $alpha argument as a default for the alpha value of each color.
        $this->data_colors = array();
        foreach ($colors as $color) {
            $color_array = $this->SetRGBColor($color, $alpha);
            if (!$color_array) return FALSE; // SetRGBColor already did an error message.
            $this->data_colors[] = $color_array;
        }

        // For past compatibility:
        return $this->SetDataBorderColors($which_border);
    }

    /*
     * Set the colors for the bars and stacked bars outlines.
     * Argument usage is similar to SetDataColors(), except the default is just black.
     */
    function SetDataBorderColors($which_br = NULL)
    {
        if (is_array($which_br)) {
            $colors = $which_br; // Use supplied array
        } elseif (!empty($which_br)) {
            $colors = array($which_br);  // Use supplied single color
        } elseif (empty($this->data_border_colors) || !is_null($which_br)) {
            $colors = array('black'); // Use default
        } else {
            return TRUE; // Do nothing: which_br is NULL or missing and a color array is already set.
        }

        // Check each color and convert to array (r,g,b,a) form.
        $this->data_border_colors = array();
        foreach ($colors as $color) {
            $color_array = $this->SetRGBColor($color);
            if (!$color_array) return FALSE; // SetRGBColor already did an error message.
            $this->data_border_colors[] = $color_array;
        }
        return TRUE;
    }

    /*
     * Sets the colors for the data error bars.
     * Argument usage is the same as SetDataColors().
     */
    function SetErrorBarColors($which_err = NULL)
    {
        if (is_array($which_err)) {
            $colors = $which_err;  // Use supplied array
        } elseif (!empty($which_err)) {
            $colors = array($which_err);  // Use supplied single color
        } elseif (empty($this->error_bar_colors) || !is_null($which_err)) {
            $colors = $this->default_colors;  // Use default color array
        } else {
            return TRUE; // Do nothing: which_err is NULL or missing and a color array is already set.
        }

        // Check each color and convert to array (r,g,b,a) form.
        $this->error_bar_colors = array();
        foreach ($colors as $color) {
            $color_array = $this->SetRGBColor($color);
            if (!$color_array) return FALSE; // SetRGBColor already did an error message.
            $this->error_bar_colors[] = $color_array;
        }
        return TRUE;
    }

    /*
     * Sets the default dashed line style.
     *   $which_style : A string specifying the dashed line style, as alternating numbers
     *         of the length (in pixels) of lines and spaces, separated by dashes.
     *   For example: '2-3-1-2' means 2 dots of color, 3 transparent, 1 color, then 2 transparent.
     *   This builds a string which will evaluate to an array of integers. Each colored dot
     *   is  '$which_ndxcol' and each transparent dot is 'IMG_COLOR_TRANSPARENT'. When SetDashedStyle()
     *   eval's this with $which_ndxcol set, the result is a GD line style array.
     */
    function SetDefaultDashedStyle($which_style)
    {
        // Explode "numcol-numtrans-numcol-numtrans..." into segment counts:
        $asked = explode('-', $which_style);

        if (count($asked) < 2) {
            return $this->PrintError("SetDefaultDashedStyle(): Wrong parameter '$which_style'.");
        }

        // Build the string to be evaluated later by SetDashedStyle() with $which_ndxcolor set.
        $result = '';
        $vals = array('$which_ndxcol,', 'IMG_COLOR_TRANSPARENT,');
        $index = 0;
        foreach ($asked as $n) {
            $result .= str_repeat($vals[$index], $n);
            $index = 1 - $index;
        }
        $this->default_dashed_style = "array($result)";

        return TRUE;
    }

    /*
     * Sets the style before drawing a dashed line. Defaults to $this->default_dashed_style
     *    $which_ndxcol : Color index to be used.
     */
    protected function SetDashedStyle($which_ndxcol)
    {
        // See SetDefaultDashedStyle() to understand this.
        eval ("\$style = $this->default_dashed_style;");
        return imagesetstyle($this->img, $style);
    }

    /*
     * Set line widths for each data set.
     *   $which_lw : Array of line widths in pixels, or a single value to use for all data sets.
     */
    function SetLineWidths($which_lw=NULL)
    {
        if (is_array($which_lw)) {
            $this->line_widths = $which_lw; // Use provided array
        } elseif (!is_null($which_lw)) {
            $this->line_widths = array($which_lw); // Convert value to array
        }
        return TRUE;
    }

    /*
     * Set line style ('solid' or 'dashed') for each data set.
     *   $which_ls : Array of keywords, or a single keyword to use for all data sets.
     */
    function SetLineStyles($which_ls=NULL)
    {
        if (is_array($which_ls)) {
            $this->line_styles = $which_ls; // Use provided array
        } elseif (!is_null($which_ls)) {
            $this->line_styles = ($which_ls) ? array($which_ls) : array('solid');
        }
        return TRUE;
    }

/////////////////////////////////////////////
//////////////                 TEXT and FONTS
/////////////////////////////////////////////

    /*
     * Controls the line spacing of multi-line labels.
     *   $which_spc : Line spacing factor for text
     * For GD text, this is the number of pixels between lines.
     * For TTF text, it controls line spacing in proportion to the normal
     * spacing defined by the font.
     */
    function SetLineSpacing($which_spc)
    {
        $this->line_spacing = $which_spc;
        return TRUE;
    }

    /*
     * Select the default font type to use.
     *   $which_ttf : True to default to TrueType, False to default to GD (fixed) fonts.
     * This also resets all font settings to the defaults.
     */
    function SetUseTTF($which_ttf)
    {
        $this->use_ttf = $which_ttf;
        return $this->SetDefaultFonts();
    }

    /*
     * Sets the directory name to look into for TrueType fonts.
     */
    function SetTTFPath($which_path)
    {
        if (!is_dir($which_path) || !is_readable($which_path)) {
            return $this->PrintError("SetTTFPath(): $which_path is not a valid path.");
        }
        $this->ttf_path = $which_path;
        return TRUE;
    }

    /*
     * Sets the default TrueType font and updates all fonts to that.
     * The default font might be a full path, or relative to the TTFPath,
     * so let SetFont check that it exists.
     * Side effects: Enables use of TrueType fonts as the default font type,
     * and resets all font settings.
     */
    function SetDefaultTTFont($which_font)
    {
        $this->default_ttfont = $which_font;
        return $this->SetUseTTF(TRUE);
    }

    /*
     * Return the default TrueType font name. If no default has been set,
     * this tries some likely candidates for a font which can be loaded.
     * If it finds one that works, that becomes the default TT font.
     * If there is no default and it cannot find a working font, it falls
     * back to the original PHPlot default (which will not likely work either).
     */
    protected function GetDefaultTTFont()
    {
        if (!isset($this->default_ttfont)) {
            // No default font yet. Try some common sans-serif fonts.
            $fonts = array('LiberationSans-Regular.ttf',  // For Linux with a correct GD font search path
                           'Verdana.ttf', 'Arial.ttf', 'Helvetica.ttf', // For Windows, maybe others
                           'ttf-liberation/LiberationSans-Regular.ttf', // For Debian, Ubuntu, and friends
                           'benjamingothic.ttf',  // Original PHPlot default
                          );
            foreach ($fonts as $font) {
                // First try the font name alone, to see if GD can find and load it.
                if (@imagettfbbox(10, 0, $font, "1") !== False)
                    break;
                // If the font wasn't found, try it with the default TTF path in front.
                $font_with_path = $this->ttf_path . DIRECTORY_SEPARATOR . $font;
                if (@imagettfbbox(10, 0, $font_with_path, "1") !== False) {
                    $font = $font_with_path;
                    break;
                }
            }
            // We either have a working font, or are using the last one regardless.
            $this->default_ttfont = $font;
        }
        return $this->default_ttfont;
    }

    /*
     * Sets fonts to their defaults
     */
    protected function SetDefaultFonts()
    {
        // TTF:
        if ($this->use_ttf) {
            return $this->SetFont('generic', '', 8)
                && $this->SetFont('title', '', 14)
                && $this->SetFont('legend', '', 8)
                && $this->SetFont('x_label', '', 6)
                && $this->SetFont('y_label', '', 6)
                && $this->SetFont('x_title', '', 10)
                && $this->SetFont('y_title', '', 10);
        }
        // Fixed GD Fonts:
        return $this->SetFont('generic', 2)
            && $this->SetFont('title', 5)
            && $this->SetFont('legend', 2)
            && $this->SetFont('x_label', 1)
            && $this->SetFont('y_label', 1)
            && $this->SetFont('x_title', 3)
            && $this->SetFont('y_title', 3);
    }

    /*
     * Select a fixed (GD) font for an element.
     * This allows using a fixed font, even with SetUseTTF(True).
     *    $which_elem : The element whose font is to be changed.
     *       One of: title legend generic x_label y_label x_title y_title
     *    $which_font : A GD font number 1-5
     *    $which_spacing (optional) : Line spacing factor
     */
    function SetFontGD($which_elem, $which_font, $which_spacing = NULL)
    {
        if ($which_font < 1 || 5 < $which_font) {
            return $this->PrintError(__FUNCTION__ . ': Font size must be 1, 2, 3, 4 or 5');
        }
        if (!$this->CheckOption($which_elem,
                                'generic, title, legend, x_label, y_label, x_title, y_title',
                                __FUNCTION__)) {
            return FALSE;
        }

        // Store the font parameters: name/size, char cell height and width.
        $this->fonts[$which_elem] = array('ttf' => FALSE,
                                          'font' => $which_font,
                                          'height' => ImageFontHeight($which_font),
                                          'width' => ImageFontWidth($which_font),
                                          'line_spacing' => $which_spacing);
        return TRUE;
    }

    /*
     * Select a TrueType font for an element.
     * This allows using a TrueType font, even with SetUseTTF(False).
     *    $which_elem : The element whose font is to be changed.
     *       One of: title legend generic x_label y_label x_title y_title
     *    $which_font : A TrueType font filename or pathname.
     *    $which_size : Font point size.
     *    $which_spacing (optional) : Line spacing factor
     */
    function SetFontTTF($which_elem, $which_font, $which_size = 12, $which_spacing = NULL)
    {
        if (!$this->CheckOption($which_elem,
                                'generic, title, legend, x_label, y_label, x_title, y_title',
                                __FUNCTION__)) {
            return FALSE;
        }

        // Empty font name means use the default font.
        if (empty($which_font))
            $which_font = $this->GetDefaultTTFont();
        $path = $which_font;

        // First try the font name directly, if not then try with path.
        // Use GD imagettfbbox() to determine if this is a valid font.
        // The return $bbox is used below, if valid.
        if (($bbox = @imagettfbbox($which_size, 0, $path, "E")) === False) {
            $path = $this->ttf_path . DIRECTORY_SEPARATOR . $which_font;
            if (($bbox = @imagettfbbox($which_size, 0, $path, "E")) === False) {
                return $this->PrintError(__FUNCTION__ . ": Can't find TrueType font $which_font");
            }
        }

        // Calculate the font height and inherent line spacing. TrueType fonts have this information
        // internally, but PHP/GD has no way to directly access it. So get the bounding box size of
        // an upper-case character without descenders, and the baseline-to-baseline height.
        // Note: In practice, $which_size = $height, maybe +/-1 . But which_size is in points,
        // and height is in pixels, and someday GD may be able to tell the difference.
        // The character width is saved too, but not used by the normal text drawing routines - it
        // isn't necessarily a fixed-space font. It is used in DrawLegend.
        $height = $bbox[1] - $bbox[5];
        $width = $bbox[2] - $bbox[0];
        $bbox = ImageTTFBBox($which_size, 0, $path, "E\nE");
        $spacing = $bbox[1] - $bbox[5] - 2 * $height;

        // Store the font parameters:
        $this->fonts[$which_elem] = array('ttf' => TRUE,
                                          'font' => $path,
                                          'size' => $which_size,
                                          'height' => $height,
                                          'width' => $width,
                                          'spacing' => $spacing,
                                          'line_spacing' => $which_spacing);
        return TRUE;
    }

    /*
     * Select Fixed/TrueType font for an element. Which type of font is
     * selected depends on the $use_ttf class variable (see SetUseTTF()).
     * Before PHPlot supported mixing font types, only this function and
     * SetUseTTF were available to select an overall font type, but now
     * SetFontGD() and SetFontTTF() can be used for mixing font types.
     *    $which_elem : The element whose font is to be changed.
     *       One of: title legend generic x_label y_label x_title y_title
     *    $which_font : A number 1-5 for fixed fonts, or a TrueType font.
     *    $which_size : Ignored for Fixed fonts, point size for TrueType.
     *    $which_spacing (optional) : Line spacing factor
     */
    function SetFont($which_elem, $which_font, $which_size = 12, $line_spacing = NULL)
    {
        if ($this->use_ttf)
            return $this->SetFontTTF($which_elem, $which_font, $which_size, $line_spacing);
        return $this->SetFontGD($which_elem, $which_font, $line_spacing);
    }

    /*
     * Return the inter-line spacing for a font.
     * This is an internal function, used by ProcessText* and DrawLegend.
     *   $font : A font array variable.
     * Returns: Spacing, in pixels, between text lines.
     */
    protected function GetLineSpacing($font)
    {
        // Use the per-font line spacing preference, if set, else the global value:
        if (isset($font['line_spacing']))
            $line_spacing = $font['line_spacing'];
        else
            $line_spacing = $this->line_spacing;

        // For GD fonts, that is the spacing in pixels.
        // For TTF, adjust based on the 'natural' font spacing (see SetFontTTF):
        if ($font['ttf']) {
            $line_spacing = (int)($line_spacing * $font['spacing'] / 6.0);
        }
        return $line_spacing;
    }

    /*
     * Text drawing and sizing functions:
     * ProcessText is meant for use only by DrawText and SizeText.
     *    ProcessText(True, ...)  - Draw a block of text
     *    ProcessText(False, ...) - Just return ($width, $height) of
     *       the orthogonal bounding box containing the text.
     * ProcessText is further split into separate functions for GD and TTF
     * text, due to the size of the code.
     *
     * Horizontal and vertical alignment are relative to the drawing. That is:
     * vertical text (90 deg) gets centered along Y position with
     * v_align = 'center', and adjusted to the right of X position with
     * h_align = 'right'.  Another way to look at this is to say
     * that text rotation happens first, then alignment.
     *
     * Original multiple lines code submitted by Remi Ricard.
     * Original vertical code submitted by Marlin Viss.
     *
     * Text routines rewritten by ljb to fix alignment and position problems.
     * Here is my explanation and notes. More information and pictures will be
     * placed in the PHPlot Reference Manual.
     *
     *    + Process TTF text one line at a time, not as a block. (See below)
     *    + Flipped top vs bottom vertical alignment. The usual interpretation
     *  is: bottom align means bottom of the text is at the specified Y
     *  coordinate. For some reason, PHPlot did left/right the correct way,
     *  but had top/bottom reversed. I fixed it, and left the default valign
     *  argument as bottom, but the meaning of the default value changed.
     *
     *    For GD font text, only single-line text is handled by GD, and the
     *  basepoint is the upper left corner of each text line.
     *    For TTF text, multi-line text could be handled by GD, with the text
     *  basepoint at the lower left corner of the first line of text.
     *  (Behavior of TTF drawing routines on multi-line text is not documented.)
     *  But you cannot do left/center/right alignment on each line that way,
     *  or proper line spacing.
     *    Therefore, for either text type, we have to break up the text into
     *  lines and position each line independently.
     *
     *    There are 9 alignment modes: Horizontal = left, center, or right, and
     *  Vertical = top, center, or bottom. Alignment is interpreted relative to
     *  the image, not as the text is read. This makes sense when you consider
     *  for example X axis labels. They need to be centered below the marks
     *  (center, top alignment) regardless of the text angle.
     *  'Bottom' alignment really means baseline alignment.
     *
     *    GD font text is supported (by libgd) at 0 degrees and 90 degrees only.
     *  Multi-line or single line text works with any of the 9 alignment modes.
     *
     *    TTF text can be at any angle. The 9 alignment modes work for all angles,
     *  but the results might not be what you expect for multi-line text. See
     *  the PHPlot Reference Manual for pictures and details. In short, alignment
     *  applies to the orthogonal (aligned with X and Y axes) bounding box that
     *  contains the text, and to each line in the multi-line text box. Since
     *  alignment is relative to the image, 45 degree multi-line text aligns
     *  differently from 46 degree text.
     *
     *    Note that PHPlot allows multi-line text for the 3 titles, and they
     *  are only drawn at 0 degrees (main and X titles) or 90 degrees (Y title).
     *  Data labels can also be multi-line, and they can be drawn at any angle.
     *  -ljb 2007-11-03
     *
     */

    /*
     * ProcessTextGD() - Draw or size GD fixed-font text.
     * This is intended for use only by ProcessText().
     *    $draw_it : True to draw the text, False to just return the orthogonal width and height.
     *    $font : PHPlot font array (with 'ttf' = False) - see SetFontGD()
     *    $angle : Text angle in degrees. GD only supports 0 and 90. We treat >= 45 as 90, else 0.
     *    $x, $y : Reference point for the text (ignored if !$draw_it)
     *    $color : GD color index to use for drawing the text (ignored if !$draw_it)
     *    $text : The text to draw or size. Put a newline between lines.
     *    $h_factor : Horizontal alignment factor: 0(left), .5(center), or 1(right) (ignored if !$draw_it)
     *    $v_factor : Vertical alignment factor: 0(top), .5(center), or 1(bottom) (ignored if !$draw_it)
     * Returns: True, if drawing text, or an array of ($width, $height) if not.
     */
    protected function ProcessTextGD($draw_it, $font, $angle, $x, $y, $color, $text, $h_factor, $v_factor)
    {
        // Extract font parameters:
        $font_number = $font['font'];
        $font_width = $font['width'];
        $font_height = $font['height'];
        $line_spacing = $this->GetLineSpacing($font);

        // Break up the text into lines, trim whitespace, find longest line.
        // Save the lines and length for drawing below.
        $longest = 0;
        foreach (explode("\n", $text) as $each_line) {
            $lines[] = $line = trim($each_line);
            $line_lens[] = $line_len = strlen($line);
            if ($line_len > $longest) $longest = $line_len;
        }
        $n_lines = count($lines);

        // Width, height are based on font size and longest line, line count respectively.
        // These are relative to the text angle.
        $total_width = $longest * $font_width;
        $total_height = $n_lines * $font_height + ($n_lines - 1) * $line_spacing;

        if (!$draw_it) {
            if ($angle < 45) return array($total_width, $total_height);
            return array($total_height, $total_width);
        }

        $interline_step = $font_height + $line_spacing; // Line-to-line step

        if ($angle >= 45) {
            // Vertical text (90 degrees):
            // (Remember the alignment convention with vertical text)
            // For 90 degree text, alignment factors change like this:
            $temp = $v_factor;
            $v_factor = $h_factor;
            $h_factor = 1 - $temp;

            $draw_func = 'ImageStringUp';

            // Rotation matrix "R" for 90 degrees (with Y pointing down):
            $r00 = 0;  $r01 = 1;
            $r10 = -1; $r11 = 0;

        } else {
            // Horizontal text (0 degrees):
            $draw_func = 'ImageString';

            // Rotation matrix "R" for 0 degrees:
            $r00 = 1; $r01 = 0;
            $r10 = 0; $r11 = 1;
        }

        // Adjust for vertical alignment (horizontal text) or horizontal alignment (vertical text):
        $factor = (int)($total_height * $v_factor);
        $xpos = $x - $r01 * $factor;
        $ypos = $y - $r11 * $factor;

        // Debug callback provides the bounding box:
        if ($this->GetCallback('debug_textbox')) {
            if ($angle >= 45) {
                $bbox_width  = $total_height;
                $bbox_height = $total_width;
                $px = $xpos;
                $py = $ypos - (1 - $h_factor) * $total_width;
            } else {
                $bbox_width  = $total_width;
                $bbox_height = $total_height;
                $px = $xpos - $h_factor * $total_width;
                $py = $ypos;
            }
            $this->DoCallback('debug_textbox', $px, $py, $bbox_width, $bbox_height);
        }

        for ($i = 0; $i < $n_lines; $i++) {

            // Adjust for alignment of this line within the text block:
            $factor = (int)($line_lens[$i] * $font_width * $h_factor);
            $x = $xpos - $r00 * $factor;
            $y = $ypos - $r10 * $factor;

            // Call ImageString or ImageStringUp:
            $draw_func($this->img, $font_number, $x, $y, $lines[$i], $color);

            // Step to the next line of text. This is a rotation of (x=0, y=interline_spacing)
            $xpos += $r01 * $interline_step;
            $ypos += $r11 * $interline_step;
        }
        return TRUE;
    }

    /*
     * ProcessTextTTF() - Draw or size TTF text.
     * This is intended for use only by ProcessText().
     *    $draw_it : True to draw the text, False to just return the orthogonal width and height.
     *    $font : PHPlot font array (with 'ttf' = True) - see SetFontTTF()
     *    $angle : Text angle in degrees.
     *    $x, $y : Reference point for the text (ignored if !$draw_it)
     *    $color : GD color index to use for drawing the text (ignored if !$draw_it)
     *    $text : The text to draw or size. Put a newline between lines.
     *    $h_factor : Horizontal alignment factor: 0(left), .5(center), or 1(right) (ignored if !$draw_it)
     *    $v_factor : Vertical alignment factor: 0(top), .5(center), or 1(bottom) (ignored if !$draw_it)
     * Returns: True, if drawing text, or an array of ($width, $height) if not.
     */
    protected function ProcessTextTTF($draw_it, $font, $angle, $x, $y, $color, $text, $h_factor, $v_factor)
    {
        // Extract font parameters (see SetFontTTF):
        $font_file = $font['font'];
        $font_size = $font['size'];
        $font_height = $font['height'];
        $line_spacing = $this->GetLineSpacing($font);

        // Break up the text into lines, trim whitespace.
        // Calculate the total width and height of the text box at 0 degrees.
        // Save the trimmed lines and their widths for later when drawing.
        // To get uniform spacing, don't use the actual line heights.
        // Total height = Font-specific line heights plus inter-line spacing.
        // Total width = width of widest line.
        // Last Line Descent is the offset from the bottom to the text baseline.
        // Note: For some reason, ImageTTFBBox uses (-1,-1) as the reference point.
        //   So 1+bbox[1] is the baseline to bottom distance.
        $total_width = 0;
        $lastline_descent = 0;
        foreach (explode("\n", $text) as $each_line) {
            $lines[] = $line = trim($each_line);
            $bbox = ImageTTFBBox($font_size, 0, $font_file, $line);
            $line_widths[] = $width = $bbox[2] - $bbox[0];
            if ($width > $total_width) $total_width = $width;
            $lastline_descent = 1 + $bbox[1];
        }
        $n_lines = count($lines);
        $total_height = $n_lines * $font_height + ($n_lines - 1) * $line_spacing;

        // Calculate the rotation matrix for the text's angle. Remember that GD points Y down,
        // so the sin() terms change sign.
        $theta = deg2rad($angle);
        $cos_t = cos($theta);
        $sin_t = sin($theta);
        $r00 = $cos_t;    $r01 = $sin_t;
        $r10 = -$sin_t;   $r11 = $cos_t;

        // Make a bounding box of the right size, with upper left corner at (0,0).
        // By convention, the point order is: LL, LR, UR, UL.
        // Note this is still working with the text at 0 degrees.
        // When sizing text (SizeText), use the overall size with descenders.
        //   This tells the caller how much room to leave for the text.
        // When drawing text (DrawText), use the size without descenders - that
        //   is, down to the baseline. This is for accurate positioning.
        $b[0] = 0;
        if ($draw_it) {
            $b[1] = $total_height;
        } else {
            $b[1] = $total_height + $lastline_descent;
        }
        $b[2] = $total_width;  $b[3] = $b[1];
        $b[4] = $total_width;  $b[5] = 0;
        $b[6] = 0;             $b[7] = 0;

        // Rotate the bounding box, then offset to the reference point:
        for ($i = 0; $i < 8; $i += 2) {
            $x_b = $b[$i];
            $y_b = $b[$i+1];
            $c[$i]   = $x + $r00 * $x_b + $r01 * $y_b;
            $c[$i+1] = $y + $r10 * $x_b + $r11 * $y_b;
        }

        // Get an orthogonal (aligned with X and Y axes) bounding box around it, by
        // finding the min and max X and Y:
        $bbox_ref_x = $bbox_max_x = $c[0];
        $bbox_ref_y = $bbox_max_y = $c[1];
        for ($i = 2; $i < 8; $i += 2) {
            $x_b = $c[$i];
            if ($x_b < $bbox_ref_x) $bbox_ref_x = $x_b;
            elseif ($bbox_max_x < $x_b) $bbox_max_x = $x_b;
            $y_b = $c[$i+1];
            if ($y_b < $bbox_ref_y) $bbox_ref_y = $y_b;
            elseif ($bbox_max_y < $y_b) $bbox_max_y = $y_b;
        }
        $bbox_width = $bbox_max_x - $bbox_ref_x;
        $bbox_height = $bbox_max_y - $bbox_ref_y;

        if (!$draw_it) {
            // Return the bounding box, rounded up (so it always contains the text):
            return array((int)ceil($bbox_width), (int)ceil($bbox_height));
        }

        $interline_step = $font_height + $line_spacing; // Line-to-line step

        // Calculate the offsets from the supplied reference point to the
        // upper-left corner of the text.
        // Start at the reference point at the upper left corner of the bounding
        // box (bbox_ref_x, bbox_ref_y) then adjust it for the 9 point alignment.
        // h,v_factor are 0,0 for top,left, .5,.5 for center,center, 1,1 for bottom,right.
        //    $off_x = $bbox_ref_x + $bbox_width * $h_factor - $x;
        //    $off_y = $bbox_ref_y + $bbox_height * $v_factor - $y;
        // Then use that offset to calculate back to the supplied reference point x, y
        // to get the text base point.
        //    $qx = $x - $off_x;
        //    $qy = $y - $off_y;
        // Reduces to:
        $qx = 2 * $x - $bbox_ref_x - $bbox_width * $h_factor;
        $qy = 2 * $y - $bbox_ref_y - $bbox_height * $v_factor;

        // Check for debug callback. Don't calculate bounding box unless it is wanted.
        if ($this->GetCallback('debug_textbox')) {
            // Calculate the orthogonal bounding box coordinates for debug testing.

            // qx, qy is upper left corner relative to the text.
            // Calculate px,py: upper left corner (absolute) of the bounding box.
            // There are 4 equation sets for this, depending on the quadrant:
            if ($sin_t > 0) {
                if ($cos_t > 0) {
                    // Quadrant: 0d - 90d:
                    $px = $qx; $py = $qy - $total_width * $sin_t;
                } else {
                    // Quadrant: 90d - 180d:
                   $px = $qx + $total_width * $cos_t; $py = $qy - $bbox_height;
                }
            } else {
                if ($cos_t < 0) {
                    // Quadrant: 180d - 270d:
                    $px = $qx - $bbox_width; $py = $qy + $total_height * $cos_t;
                } else {
                    // Quadrant: 270d - 360d:
                    $px = $qx + $total_height * $sin_t; $py = $qy;
                }
            }
            $this->DoCallback('debug_textbox', $px, $py, $bbox_width, $bbox_height);
        }

        // Since alignment is applied after rotation, which parameter is used
        // to control alignment of each line within the text box varies with
        // the angle.
        //   Angle (degrees):       Line alignment controlled by:
        //  -45 < angle <= 45          h_align
        //   45 < angle <= 135         reversed v_align
        //  135 < angle <= 225         reversed h_align
        //  225 < angle <= 315         v_align
        if ($cos_t >= $sin_t) {
            if ($cos_t >= -$sin_t) $line_align_factor = $h_factor;
            else $line_align_factor = $v_factor;
        } else {
            if ($cos_t >= -$sin_t) $line_align_factor = 1-$v_factor;
            else $line_align_factor = 1-$h_factor;
        }

        // Now we have the start point, spacing and in-line alignment factor.
        // We are finally ready to start drawing the text, line by line.
        for ($i = 0; $i < $n_lines; $i++) {

            // For drawing TTF text, the reference point is the left edge of the
            // text baseline (not the lower left corner of the bounding box).
            // The following also adjusts for horizontal (relative to
            // the text) alignment of the current line within the box.
            // What is happening is rotation of this vector by the text angle:
            //    (x = (total_width - line_width) * factor, y = font_height)

            $width_factor = ($total_width - $line_widths[$i]) * $line_align_factor;
            $rx = $qx + $r00 * $width_factor + $r01 * $font_height;
            $ry = $qy + $r10 * $width_factor + $r11 * $font_height;

            // Finally, draw the text:
            ImageTTFText($this->img, $font_size, $angle, $rx, $ry, $color, $font_file, $lines[$i]);

            // Step to position of next line.
            // This is a rotation of (x=0,y=height+line_spacing) by $angle:
            $qx += $r01 * $interline_step;
            $qy += $r11 * $interline_step;
        }
        return TRUE;
    }

    /*
     * ProcessText() - Wrapper for ProcessTextTTF() and ProcessTextGD(). See notes above.
     * This is intended for use from within PHPlot only, and only by DrawText() and SizeText().
     *    $draw_it : True to draw the text, False to just return the orthogonal width and height.
     *    $font : PHPlot font array, or NULL or empty string to use 'generic'
     *    $angle : Text angle in degrees
     *    $x, $y : Reference point for the text (ignored if !$draw_it)
     *    $color : GD color index to use for drawing the text (ignored if !$draw_it)
     *    $text : The text to draw or size. Put a newline between lines.
     *    $halign : Horizontal alignment: left, center, or right (ignored if !$draw_it)
     *    $valign : Vertical alignment: top, center, or bottom (ignored if !$draw_it)
     *      Note: Alignment is relative to the image, not the text.
     * Returns: True, if drawing text, or an array of ($width, $height) if not.
     */
    protected function ProcessText($draw_it, $font, $angle, $x, $y, $color, $text, $halign, $valign)
    {
        // Empty text case:
        if ($text === '') {
            if ($draw_it) return TRUE;
            return array(0, 0);
        }

        // Calculate width and height offset factors using the alignment args:
        if ($valign == 'top') $v_factor = 0;
        elseif ($valign == 'center') $v_factor = 0.5;
        else $v_factor = 1.0; // 'bottom'
        if ($halign == 'left') $h_factor = 0;
        elseif ($halign == 'center') $h_factor = 0.5;
        else $h_factor = 1.0; // 'right'

        // Apply a default font. This is mostly for external (callback) users.
        if (empty($font)) $font = $this->fonts['generic'];

        if ($font['ttf']) {
            return $this->ProcessTextTTF($draw_it, $font, $angle, $x, $y, $color, $text,
                                         $h_factor, $v_factor);
        }
        return $this->ProcessTextGD($draw_it, $font, $angle, $x, $y, $color, $text, $h_factor, $v_factor);
    }

    /*
     * Draws a block of text. See comments above before ProcessText().
     *    $which_font : PHPlot font array, or NULL or empty string to use 'generic'
     *    $which_angle : Text angle in degrees
     *    $which_xpos, $which_ypos: Reference point for the text
     *    $which_color : GD color index to use for drawing the text
     *    $which_text :  The text to draw, with newlines (\n) between lines.
     *    $which_halign : Horizontal (relative to the image) alignment: left, center, or right.
     *    $which_valign : Vertical (relative to the image) alignment: top, center, or bottom.
     * Note: This function should be considered 'protected', and is not documented for public use.
     */
    function DrawText($which_font, $which_angle, $which_xpos, $which_ypos, $which_color, $which_text,
                      $which_halign = 'left', $which_valign = 'bottom')
    {
        return $this->ProcessText(TRUE,
                           $which_font, $which_angle, $which_xpos, $which_ypos,
                           $which_color, $which_text, $which_halign, $which_valign);
    }

    /*
     * Returns the size of block of text. This is the orthogonal width and height of a bounding
     * box aligned with the X and Y axes of the text. Only for angle=0 is this the actual
     * width and height of the text block, but for any angle it is the amount of space needed
     * to contain the text.
     *    $which_font : PHPlot font array, or NULL or empty string to use 'generic'
     *    $which_angle : Text angle in degrees
     *    $which_text :  The text to draw, with newlines (\n) between lines.
     * Returns a two element array with: $width, $height.
     * This is just a wrapper for ProcessText() - see above.
     * Note: This function should be considered 'protected', and is not documented for public use.
     */
    function SizeText($which_font, $which_angle, $which_text)
    {
        // Color, position, and alignment are not used when calculating the size.
        return $this->ProcessText(FALSE,
                           $which_font, $which_angle, 0, 0, 1, $which_text, '', '');
    }

/////////////////////////////////////////////
///////////            INPUT / OUTPUT CONTROL
/////////////////////////////////////////////

    /*
     * Sets output file format to $format (jpg, png, ...)
     */
    function SetFileFormat($format)
    {
        $asked = $this->CheckOption($format, 'jpg, png, gif, wbmp', __FUNCTION__);
        if (!$asked) return FALSE;
        switch ($asked) {
        case 'jpg':
            $format_test = IMG_JPG;
            break;
        case 'png':
            $format_test = IMG_PNG;
            break;
        case 'gif':
            $format_test = IMG_GIF;
            break;
        case 'wbmp':
            $format_test = IMG_WBMP;
            break;
        }
        if (!(imagetypes() & $format_test)) {
            return $this->PrintError("SetFileFormat(): File format '$format' not supported");
        }
        $this->file_format = $asked;
        return TRUE;
    }

    /*
     * Selects an input file to be used as graph background and scales or tiles this image
     * to fit the sizes.
     *   $input_file : Path to the file to be used (jpeg, png and gif accepted)
     *   $mode : 'centeredtile', 'tile', or 'scale' (the image to the graph's size)
     */
    function SetBgImage($input_file, $mode='centeredtile')
    {
        $this->bgmode = $this->CheckOption($mode, 'tile, centeredtile, scale', __FUNCTION__);
        $this->bgimg  = $input_file;
        return (boolean)$this->bgmode;
    }

    /*
     * Selects an input file to be used as plot area background and scales or tiles this image
     * to fit the sizes.
     *   $input_file : Path to the file to be used (jpeg, png and gif accepted)
     *   $mode : 'centeredtile', 'tile', or 'scale' (the image to the graph's size)
     */
    function SetPlotAreaBgImage($input_file, $mode='tile')
    {
        $this->plotbgmode = $this->CheckOption($mode, 'tile, centeredtile, scale', __FUNCTION__);
        $this->plotbgimg  = $input_file;
        return (boolean)$this->plotbgmode;
    }

    /*
     * Sets the name of the file to be used as output file.
     */
    function SetOutputFile($which_output_file)
    {
        $this->output_file = $which_output_file;
        return TRUE;
    }

    /*
     * Sets the output image as 'inline', that is: no Content-Type headers are sent
     * to the browser. Needed if you want to embed the images.
     */
    function SetIsInline($which_ii)
    {
        $this->is_inline = (bool)$which_ii;
        return TRUE;
    }

    /*
     * Get the MIME type and GD output function name for the current file type.
     */
    protected function GetImageType(&$mime_type, &$output_f)
    {
        switch ($this->file_format) {
        case 'png':
            $mime_type = 'image/png';
            $output_f = 'imagepng';
            break;
        case 'jpg':
            $mime_type = 'image/jpeg';
            $output_f = 'imagejpeg';
            break;
        case 'gif':
            $mime_type = 'image/gif';
            $output_f = 'imagegif';
            break;
        case 'wbmp':
            $mime_type = 'image/wbmp';
            $output_f = 'imagewbmp';
            break;
        default:
            // Report the error on PrintImage, because that is where this code used to be.
            return $this->PrintError('PrintImage(): Please select an image type!');
        }
        return TRUE;
    }

    /*
     * Helper for PrintImage() : tell browser not to cache the page.
     * Originally submitted by Thiemo Nagel; modified to add more options based on mjpg-streamer.
     */
    protected function DisableCaching()
    {
        header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
        header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
        header('Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0');
        header('Pragma: no-cache');
        return TRUE;
    }

    /*
     * Output the generated image to standard output or to a file.
     */
    function PrintImage()
    {
        if (!$this->browser_cache && !$this->is_inline)
            $this->DisableCaching();

        // Get MIME type and GD output function name:
        if (!$this->GetImageType($mime_type, $output_f)) return FALSE;

        if (!$this->is_inline) {
            Header("Content-type: $mime_type");
        }
        if ($this->is_inline && $this->output_file != '') {
            $output_f($this->img, $this->output_file);
        } else {
            $output_f($this->img);
        }
        return TRUE;
    }

    /*
     * Return the image data, as raw data, base64 encoded, or data URL (see RFC2397).
     */
    function EncodeImage($encoding = 'dataurl')
    {
        $enc = $this->CheckOption($encoding, 'dataurl, raw, base64', __FUNCTION__);
        if (!$enc || !$this->GetImageType($mime_type, $output_f)) return FALSE;
        ob_start();
        $output_f($this->img);
        switch ($enc) {
        case 'raw':
            return ob_get_clean();
        case 'base64':
            return base64_encode(ob_get_clean());
        default:  // 'dataurl', checked above.
            return "data:$mime_type;base64,\n" . chunk_split(base64_encode(ob_get_clean()));
        }
    }

    /*
     * Replace the image with a message. This is used for error handling, and is also available
     * as a public function for special purposes.
     *   $text : Text of the message to display in the image
     *   $options : Optional associative array of control options. See defaults below.
     * Default options are chosen for the error-handling case, which should be as fail-safe as possible.
     */
    function DrawMessage($text, $options = NULL)
    {
        // Merge options with defaults, and set as local variables:
        extract( array_merge( array(
            'draw_background' => FALSE,  // Draw image background per SetBgImage(), SetBackgroundColor()
            'draw_border' => FALSE,      // Draw image border as set with SetBorder*()
            'force_print' => TRUE,       // Ignore SetPrintImage() setting and always output
            'reset_font' => TRUE,        // Reset fonts (to avoid possible TTF error)
            'text_color' => '',          // If not empty, text color specification
            'text_wrap' => TRUE,         // Wrap the message text with wordwrap()
            'wrap_width' => 75,          // Width in characters for wordwrap()
                ), (array)$options));

        // Do colors, background, and border:
        if ($draw_border && empty($this->ndx_i_border) || $draw_background && empty($this->ndx_bg_color))
            $this->SetBgColorIndexes();
        if ($draw_background) {  // User-specified background
            $this->DrawBackground(TRUE);  // TRUE means force overwriting of background
        } else {  // Default to plain white background
            $bgcolor = imagecolorresolve($this->img, 255, 255, 255);
            ImageFilledRectangle($this->img, 0, 0, $this->image_width, $this->image_height, $bgcolor);
        }
        if ($draw_border) $this->DrawImageBorder(TRUE);
        if (empty($text_color)) $rgb = array(0, 0, 0);
        else $rgb = $this->SetRGBColor($text_color);
        $ndx_text_color = imagecolorresolve($this->img, $rgb[0], $rgb[1], $rgb[2]);

        // Error images should reset fonts, to avoid chance of a TTF error when displaying an error.
        if ($reset_font) $this->SetUseTTF(FALSE);

        // Determine the space needed for the text, and center the text box within the image:
        if ($text_wrap) $text = wordwrap($text, $wrap_width);
        list($text_width, $text_height) = $this->SizeText($this->fonts['generic'], 0, $text);
        $x = max($this->safe_margin, ($this->image_width - $text_width) / 2);
        $y = max($this->safe_margin, ($this->image_height - $text_height) / 2);
        $this->DrawText($this->fonts['generic'], 0, $x, $y, $ndx_text_color, $text, 'left', 'top');
        if ($force_print || $this->print_image) $this->PrintImage();
        return TRUE;
    }

    /*
     *  Error handling for 'fatal' errors:
     *   $error_message : Text of the error message
     *  Produce an image containing the error message, output the image, and then trigger
     *  a user-level error with the message. If no error handler is set up, PHP will log
     *  the message and exit. If there is an error handler, and it returns, PrintError
     *  returns FALSE.
     */
    protected function PrintError($error_message)
    {
        // Be sure not to loop recursively, e.g. PrintError - PrintImage - PrintError.
        if (isset($this->in_error)) return FALSE;
        $this->in_error = TRUE;

        // Output an image containing the error message:
        if (empty($this->suppress_error_image)) {
            // img will be empty if the error occurs very early - e.g. when allocating the image.
            if (!empty($this->img)) {
                $this->DrawMessage($error_message);
            } elseif (!$this->is_inline) {
                Header('HTTP/1.0 500 Internal Server Error');
            }
        }
        trigger_error($error_message, E_USER_ERROR);
        unset($this->in_error);
        return FALSE;  // In case error handler returns, rather than doing exit().
    }

    /*
     * Set error behavior. On failure, PHPlot normally creates an error image.
     */
    function SetFailureImage($error_image)
    {
        $this->suppress_error_image = !$error_image;
        return TRUE;
    }

    /*
     * Begin a Motion-JPEG (or other type) stream
     */
    function StartStream()
    {
        $this->GetImageType($mime_type, $this->stream_output_f);
        $this->stream_boundary = "PHPlot-Streaming-Frame"; // Arbitrary MIME boundary
        $this->stream_frame_header = "\r\n--$this->stream_boundary\r\nContent-Type: $mime_type\r\n";
        $this->DisableCaching();  // Send headers to disable browser-side caching
        header("Content-type: multipart/x-mixed-replace; boundary=\"$this->stream_boundary\"");
        return TRUE;
    }

    /*
     * End a Motion-JPEG (or other type) stream
     */
    function EndStream()
    {
        echo "\r\n--$this->stream_boundary--\r\n";
        flush();
        return TRUE;
    }

    /*
     * Output a plot as a frame in a Motion JPEG (or other type) stream, and set up for another.
     */
    function PrintImageFrame()
    {
        ob_start();
        call_user_func($this->stream_output_f, $this->img);
        $size = ob_get_length();
        $frame = ob_get_clean();
        echo $this->stream_frame_header, "Content-Length: $size\r\n\r\n", $frame, "\r\n";
        flush();
        // This gets the next DrawGraph() to do background and titles again.
        unset($this->done);
        return TRUE;
    }

/////////////////////////////////////////////
///////////                            LABELS
/////////////////////////////////////////////

    /*
     * Sets position for X data labels.
     * For vertical plots, these are X axis data labels, showing label strings from the data array.
     *    Accepted positions are: plotdown, plotup, both, none.
     * For horizontal plots (bar, stackedbar only), these are X data value labels, show the data values.
     *    Accepted positions are: plotin, plotstack, none.
     */
    function SetXDataLabelPos($which_xdlp)
    {
        $which_xdlp = $this->CheckOption($which_xdlp, 'plotdown, plotup, both, none, plotin, plotstack',
                                         __FUNCTION__);
        if (!$which_xdlp) return FALSE;
        $this->x_data_label_pos = $which_xdlp;

        return TRUE;
    }

    /*
     * Sets position for Y data labels.
     * For vertical plots (where available), these are Y data value labels, showing the data values.
     *    Accepted positions are: plotin, plotstack, none.
     * For horizontal plots, these are Y axis data labels, showing label strings from the data array.
     *    Accepted positions are: plotleft, plotright, both, none.
     */
    function SetYDataLabelPos($which_ydlp)
    {
        $which_ydlp = $this->CheckOption($which_ydlp, 'plotleft, plotright, both, none, plotin, plotstack',
                                          __FUNCTION__);
        if (!$which_ydlp) return FALSE;
        $this->y_data_label_pos = $which_ydlp;

        return TRUE;
    }

    /*
     * Set position for X tick labels.
     */
    function SetXTickLabelPos($which_xtlp)
    {
        $which_xtlp = $this->CheckOption($which_xtlp, 'plotdown, plotup, both, xaxis, none',
                                         __FUNCTION__);
        if (!$which_xtlp) return FALSE;
        $this->x_tick_label_pos = $which_xtlp;

        return TRUE;
    }

    /*
     * Set position for Y tick labels.
     */
    function SetYTickLabelPos($which_ytlp)
    {
        $which_ytlp = $this->CheckOption($which_ytlp, 'plotleft, plotright, both, yaxis, none',
                                         __FUNCTION__);
        if (!$which_ytlp) return FALSE;
        $this->y_tick_label_pos = $which_ytlp;

        return TRUE;
    }

    /*
     * Set formatting type for tick and data labels on X or Y axis, or pie labels.
     * This implements Set[XY]LabelType(), Set[XY]DataLabelType(), and part of SetPieLabelType().
     *    $mode  : 'x', 'y', 'xd', 'yd', or 'p' - which type of label to configure.
     *        'x' and 'y' set the type for tick labels, and the default type for data labels
     *        if they are not separately configured. 'xd' and 'yd' set the type for data labels.
     *        'p' sets the type for pie chart labels.
     *    $args  : Variable arguments, passed as an array.
     *       [0] = $type : Label format type: 'data', 'time', 'printf', 'custom', or empty.
     *             If this is missing or empty, the default formatting for $mode is restored.
     *     For type 'data':
     *       [1] = $precision (optional). Numeric precision. Can also be set by SetPrecision[XY]().
     *       [2] = $prefix (optional) - prefix string for labels.
     *       [3] = $suffix (optional) - suffix string for labels. This replaces data_units_text.
     *     For type 'time':
     *       [1] = $format for strftime (optional). Can also be set by Set[XY]TimeFormat().
     *     For type 'printf':
     *       [1] = $format (optional) for sprintf.
     *     For type 'custom':
     *       [1] = $callback (required) - Custom function or array of (instance,method) to call.
     *       [2] = $argument (optional) - Pass-through argument for the formatting function.
     */
    protected function SetLabelType($mode, $args)
    {
        if (!$this->CheckOption($mode, 'x, y, xd, yd, p', __FUNCTION__))
            return FALSE;

        $type = isset($args[0]) ? $args[0] : '';
        $format = &$this->label_format[$mode];  // Shorthand reference to format storage variables
        switch ($type) {
        case 'data':
            if (isset($args[1]))
                $format['precision'] = $args[1];
            elseif (!isset($format['precision']))
                $format['precision'] = 1;
            $format['prefix'] = isset($args[2]) ? $args[2] : '';
            $format['suffix'] = isset($args[3]) ? $args[3] : '';
            break;

        case 'time':
            if (isset($args[1]))
                $format['time_format'] = $args[1];
            elseif (!isset($format['time_format']))
                $format['time_format'] = '%H:%M:%S';
            break;

        case 'printf':
            if (isset($args[1]))
                $format['printf_format'] = $args[1];
            elseif (!isset($format['printf_format']))
                $format['printf_format'] = '%e';
            break;

        case 'custom':
            if (isset($args[1])) {
                $format['custom_callback'] = $args[1];
                $format['custom_arg'] = isset($args[2]) ? $args[2] : NULL;
            } else {
                $type = ''; // Error, 'custom' without a function, set to no-format mode.
            }
            break;

        case '':
        case 'title':   // Retained for backwards compatibility?
            break;

        default:
            $this->CheckOption($type, 'data, time, printf, custom', __FUNCTION__);
            $type = '';
        }
        $format['type'] = $type;
        return (boolean)$type;
    }

    /*
     * Select label formating for X tick labels, and for X data labels
     * (unless SetXDataLabelType was called).
     * See SetLabelType() for details.
     */
    function SetXLabelType()  // Variable arguments: $type, ...
    {
        $args = func_get_args();
        return $this->SetLabelType('x', $args);
    }

    /*
     * Select label formatting for X data labels, overriding SetXLabelType.
     */
    function SetXDataLabelType()  // Variable arguments: $type, ...
    {
        $args = func_get_args();
        return $this->SetLabelType('xd', $args);
    }

    /*
     * Select label formating for Y tick labels, and for Y data labels
     * (unless SetYDataLabelType was called).
     * See SetLabelType() for details.
     */
    function SetYLabelType()  // Variable arguments: $type, ...
    {
        $args = func_get_args();
        return $this->SetLabelType('y', $args);
    }

    /*
     * Select label formatting for Y data labels, overriding SetYLabelType.
     */
    function SetYDataLabelType()  // Variable arguments: $type, ...
    {
        $args = func_get_args();
        return $this->SetLabelType('yd', $args);
    }

    /*
     * Select label source and formating for pie chart labels.
     *   $source - What to use for labels (string or array): percent, value, label, index, or empty string.
     *             Empty string (or NULL, False, or 0) means to restore the default.
     *   ... - Additional arguments telling how to format the label. See SetLabelType() for details.
     */
    function SetPieLabelType()  // Variable arguments: $source, $type, ....
    {
        $args = func_get_args();
        $source = array_shift($args);
        if (empty($source)) {
            unset($this->pie_label_source); // Restore defaults - see CheckPieLabels()
            $args = array(''); // See below - tells SetLabelType to do no formatting or default.
        } else {
            $this->pie_label_source = $this->CheckOptionArray($source, 'percent, value, label, index',
                                                              __FUNCTION__);
            if (empty($this->pie_label_source)) return FALSE;
        }
        return $this->SetLabelType('p', $args);
    }

    /*
     * Set the date/time format code for X labels.
     * Note: Use of SetXLabelType('time', $which_xtf) is preferred, because
     * SetXTimeFormat does not also enable date/time formatting.
     */
    function SetXTimeFormat($which_xtf)
    {
        $this->label_format['x']['time_format'] = $which_xtf;
        return TRUE;
    }

    /*
     * Set the date/time format code for Y labels.
     * Note: Use of SetYLabelType('time', $which_ytf) is preferred, because
     * SetYTimeFormat does not also enable date/time formatting.
     */
    function SetYTimeFormat($which_ytf)
    {
        $this->label_format['y']['time_format'] = $which_ytf;
        return TRUE;
    }

    /*
     * Set number format parameters (decimal point and thousands separator) for
     * 'data' mode label formatting, overriding the locale-defaults.
     */
    function SetNumberFormat($decimal_point, $thousands_sep)
    {
        $this->decimal_point = $decimal_point;
        $this->thousands_sep = $thousands_sep;
        return TRUE;
    }

    /*
     * Set the text angle for X labels to $which_xla degrees.
     */
    function SetXLabelAngle($which_xla)
    {
        $this->x_label_angle = $which_xla;
        return TRUE;
    }

    /*
     * Set the text angle for Y labels to $which_xla degrees.
     */
    function SetYLabelAngle($which_yla)
    {
        $this->y_label_angle = $which_yla;
        return TRUE;
    }

    /*
     * Set the angle for X Data Labels to $which_xdla degrees.
     * If not used, this defaults to the value set with SetXLabelAngle.
     */
    function SetXDataLabelAngle($which_xdla)
    {
        $this->x_data_label_angle = $which_xdla;
        return TRUE;
    }

    /*
     * Set the angle for Y Data Labels to $which_ydla degrees.
     * If not used, this defaults to zero (unlike X data labels).
     */
    function SetYDataLabelAngle($which_ydla)
    {
        $this->y_data_label_angle = $which_ydla;
        return TRUE;
    }

/////////////////////////////////////////////
///////////                              MISC
/////////////////////////////////////////////

    /*
     * Checks the validity of an option.
     *   $which_opt  String to check, such as the provided value of a function argument.
     *   $which_acc  String of accepted choices. Must be lower-case, and separated
     *               by exactly ', ' (comma, space).
     *   $which_func Name of the calling function, for error messages.
     * Returns the supplied option value, downcased and trimmed, if it is valid.
     * Reports an error if the supplied option is not valid.
     */
    protected function CheckOption($which_opt, $which_acc, $which_func)
    {
        $asked = strtolower(trim($which_opt));

        // Look for the supplied value in a comma/space separated list.
        if (strpos(", $which_acc,", ", $asked,") !== FALSE)
            return $asked;

        $this->PrintError("$which_func(): '$which_opt' not in available choices: '$which_acc'.");
        return NULL;
    }

    /*
     * Checks the validity of an array of options.
     *   $opt  Array or string to check.
     *   $acc  String of accepted choices. Must be lower-case, and separated
     *               by exactly ', ' (comma, space).
     *   $func Name of the calling function, for error messages.
     * Returns an array of option value(s), downcased and trimmed, if all entries in $opt are valid.
     * Reports an error if any supplied option is not valid. Returns NULL if the error handler returns.
     */
    protected function CheckOption