*/ require_once ("domframework/color.php"); /** This class allow to generate an image which is a graphic. A graphic takes * an array of values and draw the lines/histo... like a spreadsheet * graph methods are : * ->height ($height) or ->width ($width) The heigh/width of the graph */ class graph /* {{{ */ { /** The X axis object */ public $axisX=null; /** The main Y axis object */ public $axisY1=null; /** The optional secondary Y axis object */ public $axisY2=null; /** The legend object */ public $legend=null; /** The graph title object */ public $title=null; /** The height of the graph (150px by default) */ private $height = 200; /** The width of the graph (200px by default) */ private $width = 300; /** The background color of the graph (white by default) */ private $bgcolor = "whitesmoke"; /** The graph style by default. Each serie can define another value */ private $style = null; /** The data object */ public $data = null; /** The series object */ public $series = null; /** Constructor : create the objects */ public function __construct () { if (! function_exists ("imagecreatetruecolor")) throw new \Exception (dgettext ("domframework", "No GD support in PHP : can't create image"), 500); $this->title = new graphTitle (); $this->legend = new graphLegend (); $this->data = new graphData (); $this->series = new graphSeries (); $this->axisX = new graphAxisX (); $this->axisY1 = new graphAxisY1 (); $this->axisY2 = new graphAxisY2 (); // Default values $defaultTitleFontFile = "/usr/share/fonts/truetype/liberation/". "LiberationSans-Bold.ttf"; $defaultFontFile = "/usr/share/fonts/truetype/liberation/". "LiberationSans-Regular.ttf"; $this->title->fontfile ($defaultTitleFontFile); $this->legend->fontfile ($defaultFontFile); $this->axisX->fontfile ($defaultFontFile); $this->axisY1->fontfile ($defaultFontFile); $this->axisY2->fontfile ($defaultFontFile); $this->axisX->axisColor ("grey"); $this->axisY1->axisColor ("grey"); $this->axisY1->gridColor ("grey"); $this->axisY2->axisColor ("grey"); $this->style ("line"); $this->style()->palette ("basic"); } /** Set the title position of the graph if the parameter is provided. * Get the title position of the graph if the parameter is not provided * @param string|null $titlePosition The title position of the graph */ public function titlePosition ($titlePosition = null) { if ($titlePosition === null) return $this->titlePosition; if (! is_string ($titlePosition) || ( $titlePosition !== "top" && $titlePosition !== "bottom")) throw new \Exception (dgettext ("domframework", "Invalid titlePosition provided to graph"), 406); $this->titlePosition = $titlePosition; return $this; } /** Set the height of the graph if the parameter is provided. * Get the height of the graph if the parameter is not provided * @param integer|null $height The height of the graph */ public function height ($height = null) { if ($height === null) return $this->height; if (! is_integer ($height) || $height < 0 || $height > 3000) throw new \Exception (dgettext ("domframework", "Invalid height provided to graph"), 406); $this->height = $height; return $this; } /** Set the background-color of the graph if the parameter is provided. * Get the background-color of the graph if the parameter is not provided * @param string|null $bgcolor The background-color of the graph */ public function bgcolor ($bgcolor = null) { if ($bgcolor === null) return $this->bgcolor; if (! is_string ($bgcolor) || ! in_array ($bgcolor, \color::colorList ())) throw new \Exception (dgettext ("domframework", "Invalid bgcolor provided to graph"), 406); $this->bgcolor = $bgcolor; return $this; } /** Set the width of the graph if the parameter is provided. * Get the width of the graph if the parameter is not provided * @param integer|null $width The width of the graph */ public function width ($width = null) { if ($width === null) return $this->width; if (! is_integer ($width) || $width < 0 || $width > 3000) throw new \Exception (dgettext ("domframework", "Invalid width provided to graph"), 406); $this->width = $width; return $this; } /** Set the default style of the graph if the parameter is provided. * Get the default style of the graph if the parameter is not provided * @param string|null $style The style of the graph */ public function style ($style = null) { if ($style === null) return $this->style; if (! is_string ($style) || ! in_array ($style, array ("line", "points", "linePoints"))) throw new \Exception (dgettext ("domframework", "Invalid style provided to graph"), 406); $styleClass = "graphStyle".$style; if ($this->style === null || $this->style ()->name () !== $style) { $this->style = new $styleClass (); $this->style()->palette ("basic"); } return $this->style; } /** Draw the graph to the screen with the previous defined parameters */ private function drawReal () { // Read the data $series = $this->data->getSeries (); foreach ($this->series->getList () as $serie) { // Remove the previous defined series which doesn't exists in the data if (! array_key_exists ($serie, $series)) $this->series->remove ($serie); } // Look for the min/max of the axis and use the data maximum if the user // doesn't define it previously. The min/max are defined on ALL the series // can not be defined by the axis directely $minValueX = null; $maxValueX = null; $minValueY1 = null; $maxValueY1 = null; $minValueY2 = null; $maxValueY2 = null; foreach ($series as $serie=>$data) { // Add the data to the series or create them if they doesn't exists $this->series->serie ($serie)->data ($data); // Look for min/max and set the data for X axis $this->axisX->data (array_keys ($data)); if ($minValueX === null) $minValueX = $this->series->serie ($serie)->minKey (); $minValueX = min ($minValueX, $this->series->serie ($serie)->minKey ()); if ($maxValueX === null) $maxValueX = $this->series->serie ($serie)->maxKey (); $maxValueX = max ($maxValueX, $this->series->serie ($serie)->maxKey ()); if (! $this->series->serie ($serie)->axisYsecondary ()) { // Look for min/max for Y1 axis if ($minValueY1 === null) $minValueY1 = $this->series->serie ($serie)->minValue (); $minValueY1 = min ($minValueY1, $this->series->serie ($serie)->minValue ()); $maxValueY1 = max ($maxValueY1, $this->series->serie ($serie)->maxValue ()); } else { // Look for min/max for Y2 axis if ($minValueY2 === null) $minValueY2 = $this->series->serie ($serie)->minValue (); $minValueY2 = min ($minValueY2, $this->series->serie ($serie)->minValue ()); $maxValueY2 = max ($maxValueY2, $this->series->serie ($serie)->maxValue ()); } } // Force the graph to display the 0 value //if ($minValueY1 > 0 && $maxValueY1 > 0) // $minValueY1 = 0; //if ($minValueY1 < 0 && $maxValueY1 < 0) // $minValueY1 = 0; //if ($minValueY2 > 0 && $maxValueY2 > 0) // $minValueY2 = 0; //if ($minValueY2 < 0 && $maxValueY2 < 0) // $minValueY2 = 0; // Look for numeric or labeled axis $numericalX = null; $numericalY1 = null; $numericalY2 = null; foreach ($series as $serie=>$data) { $numericalKey = $this->series->serie ($serie)->numericalKey (); if ($numericalX === null || $numericalX === true) { if ($numericalKey === false) $numericalX = false; else $numericalX = true; } $numericalValue = $this->series->serie ($serie)->numericalValue (); if (! $this->series->serie ($serie)->axisYsecondary ()) { if ($numericalY1 === null || $numericalY1 === true) { if ($numericalValue === false) $numericalY1 = false; else $numericalY1 = true; } } else { if ($numericalY2 === null || $numericalY2 === true) { if ($numericalValue === false) $numericalY2 = false; else $numericalY2 = true; } } } $this->axisX->numerical ($numericalX); $this->axisY1->numerical ($numericalY1); $this->axisY2->numerical ($numericalY2); if ($minValueX !== null && $this->axisX->min () === null) $this->axisX->min ($minValueX); if ($maxValueX !== null && $this->axisX->max () === null) $this->axisX->max ($maxValueX); if ($minValueY1 !== null && $this->axisY1->min () === null) $this->axisY1->min ($minValueY1); if ($maxValueY1 !== null && $this->axisY1->max () === null) $this->axisY1->max ($maxValueY1); if ($minValueY2 !== null && $this->axisY2->min () === null) $this->axisY2->min ($minValueY2); if ($maxValueY2 !== null && $this->axisY2->max () === null) $this->axisY2->max ($maxValueY2); // Manage the styles for each serie foreach ($this->series->getList () as $number=>$serie) { if ($this->series->serie ($serie)->style () === null) $this->series->serie ($serie)->style ($this->style); if ($this->series->serie ($serie)->style ()->palette () === null) $this->series->serie ($serie)->style ()->palette ( $this->style->palette ()); $this->series->serie ($serie)->style ()->number ($number); } // Create the image $gd = imagecreatetruecolor ($this->width, $this->height); // Put the background color imagefilledrectangle ($gd, 0, 0, $this->width - 1 , $this->height - 1 , \color::allocateFromText ($gd, $this->bgcolor)); // The coordinates of the free space. Will be modified each time something // is drawing on the graph // xtop, ytop, xbottom, ybottom $free = array (0, 0, imagesx ($gd), imagesy ($gd)); // Add the title on top $free = $this->title->draw ($gd, $free); // Add the legend on right $free = $this->legend->draw ($gd, $free, $this->series); // If there is no title, add an offset of 10px to display correctely the // first label of the Y axis if ($free[1] === 0) $free[1] = 10; // Add the axis // Need two passes as the X axis can modify the Y axes $this->axisX->top ($free[1]); $this->axisX->bottom ($free[3] - 1); $this->axisY1->top ($free[1]); $this->axisY2->top ($free[1]); $this->axisY1->bottom ($free[3] - 1); $this->axisY2->bottom ($free[3] - 1); $Xleft = $this->axisY1->getWidth ($gd); $Xright = $free[2] - $this->axisY2->getWidth ($gd); $this->axisX->left ($Xleft); $this->axisX->right ($Xright); $Ybottom = $this->height - $this->axisX->getHeight ($gd); $this->axisY1->right ($Xright); $this->axisY1->bottom ($Ybottom); //$this->axisY1->left (???); $this->axisY2->right ($Xleft); $this->axisY2->bottom ($Ybottom); $this->axisY2->left ($Xright); // Draw the axis $this->axisX->draw ($gd); $this->axisY1->draw ($gd); $this->axisY2->draw ($gd); // Add the graph part for each serie $lastFree = $free; foreach ($this->series->getList () as $number=>$serie) { // As the series are superposed, do not update the $free each time if (! $this->series->serie ($serie)->axisYsecondary ()) $lastFree = $this->series->serie ($serie)->draw ($gd, $free, $this->axisX, $this->axisY1); else $lastFree = $this->series->serie ($serie)->draw ($gd, $free, $this->axisX, $this->axisY2); } $free = $lastFree; imagepng ($gd); imagedestroy ($gd); } /** Draw the graph to the screen with the previous defined parameters */ public function drawImage () { header ('Content-Type: image/png'); $this->drawReal (); } /** Return the image coded in base64 * @return string The base64 string */ public function drawBase64 () { ob_start (); $this->drawReal (); return base64_encode (ob_get_clean()); } } /* }}} */ /** The series objects */ class graphSeries /* {{{ */ { /** The series stored */ private $series = array (); /** Return the serie object choosed. If doesn't exists, it is created before * be returned * @param string $name The name of the serie to create */ public function serie ($name) { if (is_integer ($name)) $name = dgettext ("domframework", "Serie")." $name"; if (! is_string ($name)) throw new \Exception (dgettext ("domframework", "Can't get a serie if the name is not a string"), 406); if (! array_key_exists ($name, $this->series)) $this->series[$name] = new graphSerie ($name); return $this->series[$name]; } /** Get the list of the defined series */ public function getList () { return array_keys ($this->series); } /** Remove an existing serie * @param string $name The name of the serie to remove */ public function remove ($name) { if (! is_string ($name)) throw new \Exception (dgettext ("domframework", "Can't remove a serie if the name is not a string"), 406); if (array_key_exists ($name, $this->series)) unset ($this->series[$name]); } } /* }}} */ /** The serie object */ class graphSerie /* {{{ */ { /** The name of the serie */ private $name; /** The data values for the serie */ private $data; /** The numericalKey is true if all the keys are numeric */ private $numericalKey =null; /** The minimum key of the serie */ private $minKey = null; /** The maximum key of the serie */ private $maxKey = null; /** The numericalValue is true if all the values are numeric */ private $numericalValue =null; /** The minimum value of the serie */ private $minValue = null; /** The maximum value of the serie */ private $maxValue = null; /** The style object for the serie */ private $style; /** The axis used to draw the serie. * If false for main Y axis * If true for secondary Y axis */ private $axisYsecondary = false; /** When creating the serie, save the name * @param string $name The name of the serie */ public function __construct ($name) { if (! is_string ($name)) throw new \Exception (dgettext ("domframework", "Can't create a serie if the name is not a string"), 406); $this->name = $name; } /** Set the data for the serie If the parameter is not provided, return the actual $data value * @param array|null $data The data to store in the serie */ public function data ($data = null) { if ($data === null) return $this->data; if (! is_array ($data)) throw new \Exception (dgettext ("domframework", "Can't create a serie data if the value is not an array"), 406); $this->data = $data; $this->minmax (); return $this; } /** Get the minimum and maximum values and keys */ private function minmax () { if ($this->data === null) return null; foreach ($this->data as $key=>$value) { if ($this->minValue === null && is_numeric ($value)) $this->minValue = $value; if ($this->minKey === null && is_numeric ($key)) $this->minKey = $key; if (is_numeric ($value)) { $this->minValue = min ($this->minValue, $value); $this->maxValue = max ($this->maxValue, $value); } if (is_numeric ($key)) { $this->minKey = min ($this->minKey, $key); $this->maxKey = max ($this->maxKey, $key); } if (! is_numeric ($key) && $this->numericalKey === null) $this->numericalKey = false; if (! is_numeric ($value) && $value !== null && $this->numericalValue === null) $this->numericalValue = false; } if ($this->numericalKey === null) $this->numericalKey = true; if ($this->numericalValue === null) $this->numericalValue = true; } /** Set/get the numeric value of the serie * If the parameter is not provided, return the actual state * @param boolean|null $numericalValue The state of the numeric value */ public function numericalValue ($numericalValue = null) { if ($numericalValue === null) return $this->numericalValue; if (! is_bool ($numericalValue)) throw new \Exception (dgettext ("domframework", "Invalid numericalValue provided to serie"), 406); $this->numericalValue = $numericalValue; return $this; } /** The minimum value of the serie */ public function minValue () { if ($this->minValue === null && $this->data !== null) $this->minmax (); return $this->minValue; } /** The maximum value of the serie */ public function maxValue () { if ($this->maxValue === null && $this->data !== null) $this->minmax (); return $this->maxValue; } /** Set/get the numeric key of the serie * If the parameter is not provided, return the actual state * @param boolean|null $numericalKey The state of the numeric key */ public function numericalKey ($numericalKey = null) { if ($numericalKey === null) return $this->numericalKey; if (! is_bool ($numericalKey)) throw new \Exception (dgettext ("domframework", "Invalid numericalKey provided to serie"), 406); $this->numericalKey = $numericalKey; return $this; } /** The minimum key of the serie */ public function minKey () { if ($this->minKey === null && $this->data !== null) $this->minmax (); return $this->minKey; } /** The maximum key of the serie */ public function maxKey () { if ($this->maxKey === null && $this->data !== null) $this->minmax (); return $this->maxKey; } /** The number of elements in the serie */ public function count () { return count ($this->data); } /** Set/Get the graph style for the serie * If the parameter is not provided, return the actual state * @param string|object|null $style The graph style */ public function style ($style = null) { if ($style === null) return $this->style; if (is_object ($style)) { $this->style = clone $style; return $this->style; } if (! is_string ($style) || ! in_array ($style, array ("line", "points", "linePoints"))) throw new \Exception (dgettext ("domframework", "Invalid style provided to serie"), 406); $styleClass = "graphStyle".$style; if ($this->style === null) $this->style = new $styleClass (); return $this->style; } /** The serie is based on the secondary Y axis * Set the value if the parameter is provided, get the value if the parameter * is not set * @param boolean|null $axisYsecondary The Serie on secondary Y axis */ public function axisYsecondary ($axisYsecondary = null) { if ($axisYsecondary === null) return $this->axisYsecondary; if (! is_bool ($axisYsecondary)) throw new \Exception (dgettext ("domframework", "Invalid axisYsecondary provided to graph axisYsecondary"), 406); $this->axisYsecondary = $axisYsecondary; return $this; } /** Draw the serie with the defined style class * @param resource $gd The resource to modify * @param array $free The free space coordinates on the graphic * @param object $axisX The axis X used on the graph * @param object $axisY The axis Y used on the graph */ public function draw ($gd, $free, $axisX, $axisY) { $this->style->draw ($gd, $free, $this->data, $axisX, $axisY); } } /* }}} */ /** Read the data */ class graphData /* {{{ */ { /** Store the data when the user provided them. Store them in array form */ private $data; /** The titles are on the first line */ private $titlesOnFirstLine = null; /** The titles are on the first column */ private $titlesOnFirstColumn = null; /** The data are stored horizontally */ private $horizontalData = null; /** Get the data from an indexed array * @param array $array The data array to graph */ public function arrayIndexed ($array) { $this->data = $array; if (! is_array ($this->data)) throw new \Exception (dgettext ("domframework", "Invalid Array Parameter provided: not an array"), 406); return $this; } /** Get the data from an associative array * The associative array are provided by database results * @param array $array The data array to graph */ public function arrayAssociative ($array) { if (! is_array ($array)) throw new \Exception (dgettext ("domframework", "Invalid Array Parameter provided: not an array"), 406); $titles = array (); $this->data = array (); foreach ($array as $line=>$lineArr) { foreach ($lineArr as $key=>$cell) { $titles[$key] = ""; $this->data[$line][] = $cell; } } array_unshift ($this->data, array_keys ($titles)); return $this; } /** Get the data from a CSV string * @param string $csv The CSV string */ public function csv ($csv) { $csv = trim ($csv); $lines = preg_split ('/( *\R)+/s', $csv); $this->data = array_map('str_getcsv', $lines); if (! is_array ($this->data)) throw new \Exception (dgettext ("domframework", "Invalid CSV provided: not converted to array"), 406); return $this; } /** Get the data from a JSON string * @param string $json The JSON string */ public function json ($json) { $this->data = json_decode ($json, true); if (! is_array ($this->data)) throw new \Exception (dgettext ("domframework", "Invalid JSON provided: not converted to array"), 406); return $this; } /** Titles on first line * Set the value if the parameter is provided, get the value if the parameter * is not set * @param boolean|null $titlesOnFirstLine The titles on first line */ public function titlesOnFirstLine ($titlesOnFirstLine = null) { if ($titlesOnFirstLine === null) return $this->titlesOnFirstLine; if (! is_bool ($titlesOnFirstLine)) throw new \Exception (dgettext ("domframework", "Invalid titlesOnFirstLine provided to graph titlesOnFirstLine"), 406); $this->titlesOnFirstLine = $titlesOnFirstLine; return $this; } /** Titles on first column * Set the value if the parameter is provided, get the value if the parameter * is not set * @param boolean|null $titlesOnFirstColumn The titles on first column */ public function titlesOnFirstColumn ($titlesOnFirstColumn = null) { if ($titlesOnFirstColumn === null) return $this->titlesOnFirstColumn; if (! is_bool ($titlesOnFirstColumn)) throw new \Exception (dgettext ("domframework", "Invalid titlesOnFirstColumn provided to graph titlesOnFirstColumn"), 406); $this->titlesOnFirstColumn = $titlesOnFirstColumn; return $this; } /** The data are stored horizontally in the array * Set the value if the parameter is provided, get the value if the parameter * is not set * @param boolean|null $horizontalData The data are stored horizontally */ public function horizontalData ($horizontalData = null) { if ($horizontalData === null) return $this->horizontalData; if (! is_bool ($horizontalData)) throw new \Exception (dgettext ("domframework", "Invalid horizontalData provided to graph horizontalData"), 406); $this->horizontalData = $horizontalData; return $this; } /** Get the series in an array with the associated values */ public function getSeries () { // 0. If there is no data to graph, nothing to do if (count ($this->data) === 0) return array (); // 1. If $this->titlesOnFirstLine === null, look if the first line contains // titles if ($this->titlesOnFirstLine === null) { $this->titlesOnFirstLine = true; if (count ($this->data) === 1) $this->titlesOnFirstLine = false; if (is_array ($this->data[0])) { foreach ($this->data[0] as $cell) { if (is_numeric ($cell)) { $this->titlesOnFirstLine = false; break; } } } else { $this->titlesOnFirstLine = false; } } // 2. If $this->titlesOnFirstColumn === null, look if the first column // contains titles if ($this->titlesOnFirstColumn === null) { if (count ($this->data[0]) === 1) $this->titlesOnFirstColumn = false; else { $this->titlesOnFirstColumn = true; foreach ($this->data as $lineArr) { if (is_numeric ($lineArr[0])) { $this->titlesOnFirstColumn = false; break; } } } } // 3. If $this->horizontalData === null, look for orientation with the // titles states if ($this->horizontalData === null) { if ($this->titlesOnFirstColumn === true || ! array_key_exists (1, $this->data)) $this->horizontalData = true; else $this->horizontalData = false; } // 4. Create the series $colTitles = array (); $lineTitles = array (); $series = array (); foreach ($this->data as $linePos => $lineArr) { if (! is_array ($lineArr)) $lineArr = array ($lineArr); if ($linePos === 0) { if ($this->titlesOnFirstLine) { $colTitles = $lineArr; if ($this->horizontalData === false) { // First line with titles and vertical data: get the series names $series = array_flip ($lineArr); foreach ($series as &$value) $value = array (); if ($this->titlesOnFirstColumn) array_shift ($series); } continue; } else { foreach ($lineArr as $i=>$value) $colTitles[$i] = $i; } } elseif (count ($lineArr) !== count ($colTitles)) throw new \Exception (sprintf (dgettext ("domframework", "Invalid data provided: line %d doesn't have the same number ". "of elements as the first line (%d != %d elements)"), $linePos+1, count ($lineArr), count ($colTitles)), 406); foreach ($lineArr as $colPos => $cell) { $cell = trim ($cell); if ($colPos === 0) { if ($this->titlesOnFirstColumn) { $lineTitles[$linePos] = $cell; if ($this->horizontalData === true) { $series[$cell] = array (); } continue; } else { $lineTitles[$linePos] = $linePos; } } if (! is_numeric ($cell)) $cell = null; else $cell = $cell+0.0; if ($this->horizontalData === false) $series[$colTitles[$colPos]][$lineTitles[$linePos]] = $cell; else $series[$lineTitles[$linePos]][$colTitles[$colPos]] = $cell; } } return $series; } } /* }}} */ /** The graphTitle object */ class graphTitle /* {{{ */ { /** The title text */ private $text = null; /** The TTF fontfile to use */ private $fontfile = null; /** The font size to use */ private $fontsize = 14; /** The title color */ private $color = "black"; /** The padding arround the title (in px) */ private $padding = 10; /** Set the text of the title if the parameter is provided. * Get the text of the title if the parameter is not provided * @param string|null $text The text of the title */ public function text ($text = null) { if ($text === null) return $this->text; if (! is_string ($text) || strlen ($text) < 0 || strlen ($text) > 50) throw new \Exception (dgettext ("domframework", "Invalid text provided to graph title"), 406); $this->text = $text; return $this; } /** Set the fontfile of the title if the parameter is provided. * Get the fontfile of the title if the parameter is not provided * @param string|null $fontfile The fontfile of the title */ public function fontfile ($fontfile = null) { if ($fontfile === null) return $this->fontfile; if (! is_string ($fontfile) || strlen ($fontfile) < 0 || ! file_exists ($fontfile) || ! is_readable ($fontfile)) throw new \Exception (dgettext ("domframework", "Invalid fontfile provided to graph title"), 406); $this->fontfile = $fontfile; return $this; } /** Set the font size of the title if the parameter is provided. * Get the font size of the title if the parameter is not provided * @param integer|null $fontsize The font size of the title */ public function fontsize ($fontsize = null) { if ($fontsize === null) return $this->fontsize; if (! is_integer ($fontsize) || $fontsize < 2 || $fontsize > 100) throw new \Exception (dgettext ("domframework", "Invalid fontsize provided to graph title"), 406); $this->fontsize = $fontsize; return $this; } /** Set the color of the title if the parameter is provided. * Get the color of the title if the parameter is not provided * @param string|null $color The color of the title */ public function color ($color = null) { if ($color === null) return $this->color; if (! is_string ($color) || ! in_array ($color, \color::colorList ())) throw new \Exception (dgettext ("domframework", "Invalid color provided to graph title"), 406); $this->color = $color; return $this; } /** Set the padding of the title if the parameter is provided. * Get the padding of the title if the parameter is not provided * @param integer|null $padding The padding of the title */ public function padding ($padding = null) { if ($padding === null) return $this->padding; if (! is_integer ($padding) || $padding < 0 || $padding > 200) throw new \Exception (dgettext ("domframework", "Invalid padding provided to graph title"), 406); $this->padding = $padding; return $this; } /** Draw the title in the $gd resource provided * @param resource $gd The resource to modify * @param array $free The free space coordinates on the graphic * @return array the new free coordinates array */ public function draw ($gd, $free) { if ($this->text === null) return $free; // Look for the bounding box around the text. The bounding is not write on // the image and return the coordinates for the text box $bbox = imagettfbbox ($this->fontsize, 0, $this->fontfile, $this->text); // Calculate the position of the text to be centered on the graph // The padding is only on vertical : the x is centered $x = floor (($free[2] - $free[0] - abs ($bbox[4] - $bbox[0])) / 2); $y = ceil ($free[1] + abs ($bbox[5] - $bbox[1])) + $this->padding; $x += $free[0]; $y += $free[1]; imagettftext ($gd, $this->fontsize, 0, $x, $y, \color::allocateFromText ($gd, $this->color), $this->fontfile, $this->text); return array (intval ($free[0]), intval ($free[1] + $y + $this->padding), intval ($free[2]), intval ($free[3])); } } /* }}} */ /** The graphLegend object */ class graphLegend /* {{{ */ { /** Show the legend (no legend by default) */ private $show = false; /** The TTF fontfile to use */ private $fontfile = null; /** The font size to use */ private $fontsize = 10; /** The legend color for the font */ private $color = "black"; /** The legend background-color */ private $bgcolor = "white"; /** The legend border color */ private $borderColor = "black"; /** The padding arround the title (in px) */ private $padding = 10; /** Set the legend display status if the parameter is provided. * Get the legend display status if the parameter is not provided * @param boolean|null $show The legend display status */ public function show ($show = null) { if ($show === null) return $this->show; if (! is_bool ($show)) throw new \Exception (dgettext ("domframework", "Invalid show value provided to graph legend"), 406); $this->show = $show; return $this; } /** Set the fontfile of the legend if the parameter is provided. * Get the fontfile of the legend if the parameter is not provided * @param string|null $fontfile The fontfile of the legend */ public function fontfile ($fontfile = null) { if ($fontfile === null) return $this->fontfile; if (! is_string ($fontfile) || strlen ($fontfile) < 0 || ! file_exists ($fontfile) || ! is_readable ($fontfile)) throw new \Exception (dgettext ("domframework", "Invalid fontfile provided to graph legend"), 406); $this->fontfile = $fontfile; return $this; } /** Set the font size of the legend if the parameter is provided. * Get the font size of the legend if the parameter is not provided * @param integer|null $fontsize The font size of the legend */ public function fontsize ($fontsize = null) { if ($fontsize === null) return $this->fontsize; if (! is_integer ($fontsize) || $fontsize < 2 || $fontsize > 100) throw new \Exception (dgettext ("domframework", "Invalid fontsize provided to graph legend"), 406); $this->fontsize = $fontsize; return $this; } /** Set the color of the legend if the parameter is provided. * Get the color of the legend if the parameter is not provided * @param string|null $color The color of the legend */ public function color ($color = null) { if ($color === null) return $this->color; if (! is_string ($color) || ! in_array ($color, \color::colorList ())) throw new \Exception (dgettext ("domframework", "Invalid color provided to graph legend"), 406); $this->color = $color; return $this; } /** Set the background-color of the legend if the parameter is provided. * Get the background-color of the legend if the parameter is not provided * @param string|null $bgcolor The background-color of the legend */ public function bgcolor ($bgcolor = null) { if ($bgcolor === null) return $this->bgcolor; if (! is_string ($bgcolor) || ! in_array ($bgcolor, \color::colorList ())) throw new \Exception (dgettext ("domframework", "Invalid bgcolor provided to graph legend"), 406); $this->bgcolor = $bgcolor; return $this; } /** Set the border color of the legend if the parameter is provided. * Get the border color of the legend if the parameter is not provided * @param string|null $borderColor The border color of the legend */ public function borderColor ($borderColor = null) { if ($borderColor === null) return $this->borderColor; if (! is_string ($borderColor) || ! in_array ($borderColor, \color::colorList ())) throw new \Exception (dgettext ("domframework", "Invalid borderColor provided to graph legend"), 406); $this->borderColor = $borderColor; return $this; } /** Set the padding of the legend if the parameter is provided. * Get the padding of the legend if the parameter is not provided * @param integer|null $padding The padding of the legend */ public function padding ($padding = null) { if ($padding === null) return $this->padding; if (! is_integer ($padding) || $padding < 0 || $padding > 200) throw new \Exception (dgettext ("domframework", "Invalid padding provided to graph legend"), 406); $this->padding = $padding; return $this; } /** Draw the legend in the $gd resource provided * @param resource $gd The resource to modify * @param array $free The free space coordinates on the graphic * @param object $series The series to graph * @return array the new free coordinates array */ public function draw ($gd, $free, $series) { if ($this->show === false) return $free; // Look for maxmimum width of the labels $maxwidth = 0; $height = 0; foreach ($series->getList () as $number=>$serie) { $bbox = imagettfbbox ($this->fontsize, 0, $this->fontfile, $serie); $width = abs ($bbox[4] - $bbox[0]); // TODO : If the serie name is too long, split it ! // 20px for the sample + the space before the label if ((20 + $maxwidth) > (($free[2] - $free[0]) / 2)) throw new \Exception (dgettext ("domframework", "The serie name in legend must not takes more than half of the graph"), 500); if ($number > 0) $height += $this->padding; $height += abs ($bbox[5] - $bbox[1]); $maxwidth = max ($maxwidth, $width); } $x1 = $free[2] - $maxwidth - 20 - $this->padding * 2; $x2 = $free[2] - $this->padding; $y1 = $free[1] + $this->padding; $y2 = $y1 + $height + $this->padding; // Size of the border : 1px $border = 1; // Draw the background rectangle imagefilledrectangle ($gd, $x1, $y1, $x2, $y2, \color::allocateFromText ($gd, $this->borderColor)); imagefilledrectangle ($gd, $x1+$border, $y1+$border, $x2-$border, $y2-$border, \color::allocateFromText ($gd, $this->bgcolor)); // Display the serie names $y = $y1; foreach ($series->getList () as $number=>$serie) { // Write the label $bbox = imagettfbbox ($this->fontsize, 0, $this->fontfile, $serie); $height = abs ($bbox[5] - $bbox[1]); $y += $height; imagettftext ($gd, $this->fontsize, 0, $x1 + 30, intval ($y + $height / 2), \color::allocateFromText ($gd, $this->color), $this->fontfile, $serie); // Draw the sample $series->serie ($serie)->style ()->sample ($gd, $x1 + 15, $y); $y += $this->padding; } $free = array ($free[0], $free[1], $x1 - 5, $free[3]); return $free; } } /* }}} */ /** The general axis management */ class graphAxisGeneral /* {{{ */ { /** The min value of the axis. Do not use it if the axis is composed of labels */ protected $min = null; /** The max value of the axis. Do not use it if the axis is composed of labels */ protected $max = null; /** The label max to display */ protected $labelMax; /** The label min to display */ protected $labelMin; /** The data displayed as values on the axis */ protected $data; /** Set if the axis is only numerical (true) or is composed of labels (false) */ protected $numerical; /** The minimum bottom position in pixels. Used on vertical axis */ protected $bottom; /** The maximum top position in pixels. Used on vertical axis */ protected $top; /** The minimum left position in pixels. Used on horizontal axis */ protected $left; /** The maximum right position in pixels. Used on horizontal axis */ protected $right; /** The fontfile to write the labels */ protected $fontfile; /** The fontsize to write the labels */ protected $fontsize = 8; /** Axis color */ protected $axisColor; /** Grid color on the axis * Can be set to null or "transparent" to not display the grid */ protected $gridColor; /** Set the min value of the axis if the parameter is provided. * Get the min value of the axis if the parameter is not provided * @param integer|null $min The min value of the axis */ public function min ($min = null) { if ($min === null) return $this->min; if (! is_numeric ($min)) throw new \Exception (dgettext ("domframework", "Invalid min provided to graph Axis")." ".get_class ($this), 406); $this->min = $min; return $this; } /** Set the max value of the axis if the parameter is provided. * Get the max value of the axis if the parameter is not provided * @param integer|null $max The max value of the axis */ public function max ($max = null) { if ($max === null) return $this->max; if (! is_numeric ($max)) throw new \Exception (dgettext ("domframework", "Invalid max provided to graph Axis")." ".get_class ($this), 406); $this->max = $max; return $this; } /** Set the data of the axis if the parameter is provided. * Get the data of the axis if the parameter is not provided * @param array|null $data The data of the axis */ public function data ($data = null) { if ($data === null) return $this->data; if (! is_array ($data)) throw new \Exception (dgettext ("domframework", "Invalid data provided to graph Axis")." ".get_class ($this), 406); $this->data = $data; if ($this->numerical === null) { // Look if the provided data are only numerical. Then define the numerical // property foreach ($data as $d) { if (! is_numeric ($d)) { $this->numerical = false; break; } } if ($this->numerical === null) $this->numerical = true; } return $this; } /** Set if the axis is numerical or composed of labels if the parameter is * provided. * Get if the axis is numerical if the parameter is not provided * @param boolean|null $numerical the axis is numerical */ public function numerical ($numerical = null) { if ($numerical === null) return $this->numerical; if (! is_bool ($numerical)) throw new \Exception (dgettext ("domframework", "Invalid numerical parameter provided to graph Axis")." ". get_class ($this), 406); $this->numerical = $numerical; return $this; } /** Set the bottom position of the axis if the parameter is provided. * Get the bottom position of the axis if the parameter is not provided * @param integer|null $bottom The bottom position of the axis */ public function bottom ($bottom = null) { if ($bottom === null) return $this->bottom; if (! is_integer ($bottom) || $bottom < 0 || $bottom > 5000) throw new \Exception (dgettext ("domframework", "Invalid bottom provided to graph Axis")." ".get_class ($this), 406); $this->bottom = $bottom; return $this; } /** Set the top position of the axis if the parameter is provided. * Get the top position of the axis if the parameter is not provided * @param integer|null $top The top position of the axis */ public function top ($top = null) { if ($top === null) return $this->top; if (! is_integer ($top) || $top < 0 || $top > 5000) throw new \Exception (dgettext ("domframework", "Invalid top provided to graph Axis")." ".get_class ($this), 406); $this->top = $top; return $this; } /** Set the left position of the axis if the parameter is provided. * Get the left position of the axis if the parameter is not provided * @param integer|null $left The left position of the axis */ public function left ($left = null) { if ($left === null) return $this->left; if (! is_integer ($left) || $left < 0 || $left > 5000) throw new \Exception (dgettext ("domframework", "Invalid left provided to graph Axis")." ".get_class ($this), 406); $this->left = $left; return $this; } /** Set the right position of the axis if the parameter is provided. * Get the right position of the axis if the parameter is not provided * @param integer|null $right The right position of the axis */ public function right ($right = null) { if ($right === null) return $this->right; if (! is_integer ($right) || $right < 0 || $right > 5000) throw new \Exception (dgettext ("domframework", "Invalid right provided to graph Axis")." ".get_class ($this), 406); $this->right = $right; return $this; } /** Set the fontfile of the labels if the parameter is provided. * Get the fontfile of the labels if the parameter is not provided * @param string|null $fontfile The fontfile of the title */ public function fontfile ($fontfile = null) { if ($fontfile === null) return $this->fontfile; if (! is_string ($fontfile) || strlen ($fontfile) < 0 || ! file_exists ($fontfile) || ! is_readable ($fontfile)) throw new \Exception (dgettext ("domframework", "Invalid fontfile provided to graph title"), 406); $this->fontfile = $fontfile; return $this; } /** Set the font size of the labels if the parameter is provided. * Get the font size of the labels if the parameter is not provided * @param integer|null $fontsize The font size of the title */ public function fontsize ($fontsize = null) { if ($fontsize === null) return $this->fontsize; if (! is_integer ($fontsize) || $fontsize < 2 || $fontsize > 100) throw new \Exception (dgettext ("domframework", "Invalid fontsize provided to graph title"), 406); $this->fontsize = $fontsize; return $this; } /** Set the axis color if the parameter is provided. * Get the axis color if the parameter is not provided * @param string|null $axisColor The axis color */ public function axisColor ($axisColor = null) { if ($axisColor === null) return $this->axisColor; if (! is_string ($axisColor) || ! in_array ($axisColor, \color::colorList ())) throw new \Exception (dgettext ("domframework", "Invalid axisColor provided to graph axis"), 406); $this->axisColor = $axisColor; return $this; } /** Set the grid color if the parameter is provided. * Get the grid color if the parameter is not provided * @param string|null $gridColor The grid color */ public function gridColor ($gridColor = null) { if ($gridColor === null) return $this->gridColor; if (! is_string ($gridColor) || ! in_array ($gridColor, \color::colorList ())) throw new \Exception (dgettext ("domframework", "Invalid gridColor provided to graph grid"), 406); $this->gridColor = $gridColor; return $this; } /** Calculate the labels that will be displayed on the axis * @param integer $nbMaxValues The maximum number of values to return * @return array The array of labels to display */ protected function labels ($nbMaxValues) { // Activate the debug of this method $deb = false; if (! is_int ($nbMaxValues)) throw new \Exception ( "Invalid parameter nbMaxValues provided to graphAxisGeneral::labels", 500); if ($deb) echo "=========== LABELS\n"; if ($this->min > 0) $minLabel = $this->min * 0.90; else $minLabel = $this->min * 1.05; if ($this->max > 0) $maxLabel = $this->max * 1.05; else $maxLabel = $this->max * 0.90; $minBase = intval (log10 ($minLabel)) - 1; $maxBase = intval (log10 ($maxLabel)) - 1; $base = max ($minBase, $maxBase); if ($deb) echo "minBase=$minBase, maxBase=$maxBase ===> base = $base\n"; if ($base < 0 && $base > -1) $base = $base - 1; // The while loop reduce the base to not explode the number of labels // If there is too much labels, remove one digit and retry while (1) { $this->labelMin = null; $this->labelMax = null; $min = round ($minLabel, -1 * $base); $max = round ($maxLabel, -1 * $base); if ($deb) echo "min = $min, max = $max\n"; $scale = pow (10, $base); if ($deb) echo "BASE=".($base).", SCALE = $scale\n"; if ($scale === 0 || $scale === 0.0) die ("Scale equal 0 on line ".__LINE__."\n"); $labels = array (); if ($this->min <= 0 && $this->max >= 0) { // Return the 0 label and the values arround it $zeroAlreadyDraw = false; for ($i = 0 ; bccomp ($i, 1.2 * $max, abs ($base) + 1) < 1 ; $i += $scale) { if ($i === 0) $zeroAlreadyDraw = true; if ($deb) echo "Values with Zero : add pos val $i\n"; $labels[] = $i; } for ($i = 0 ; bccomp ($i, 1.4 * $min, abs ($base) + 1) >= 0 ; $i -= $scale) { if ($zeroAlreadyDraw === true && $i === 0) continue; if ($deb) echo "Values with Zero : add neg val $i\n"; $labels[] = $i; } } else { // No 0 Axis : From $this->min to $this->max for ($i = $min ; bccomp ($i, $max, abs ($base) + 1) < 1 ; $i += $scale) { if ($deb) echo "Values no Zero : add val $i\n"; $labels[] = $i; } } if (count ($labels) <= $nbMaxValues) { foreach ($labels as &$label) { if ($base < 0) $label = sprintf ("%0.".intval (abs ($base))."f", $label); else $label = sprintf ("%".intval ($base)."d", $label); if ($this->labelMin === null || bccomp ($this->labelMin, $label, abs ($base) + 1) >= 0) $this->labelMin = $label; if ($this->labelMax === null || bccomp ($this->labelMax, $label, abs ($base) + 1) <= 0) $this->labelMax = $label; } if (bccomp ($this->labelMin, $this->min, abs ($base) + 1) > 0) { // Add a label in the minimum $this->labelMin = $this->labelMin - $scale; if ($deb) echo "Force Add Min $this->labelMin\n"; if ($base < 0) $labels[] = sprintf ("%0.".intval (abs ($base))."f", $this->labelMin); else $labels[] = sprintf ("%".intval ($base)."d", $this->labelMin); } if (bccomp ($this->labelMax, $this->max, abs ($base) + 1) < 0) { $this->labelMax = $this->labelMax + $scale; if ($deb) echo "Force Add Max $this->labelMax\n"; if ($base < 0) $labels[] = sprintf ("%0.".intval (abs ($base))."f", $this->labelMax); else $labels[] = sprintf ("%".intval ($base)."d", $this->labelMax); } if ($this->labelMin > $this->min) die ("labelMin > min ($this->labelMin > $this->min)\n"); if ($this->labelMax < $this->max) die ("labelMax < max ($this->labelMax < $this->max)\n"); if (count ($labels) <= $nbMaxValues) { $labels = array_unique ($labels); if ($deb) print_r ($labels); return $labels; } } if ($deb) echo "LOOP == ".count ($labels)." > $nbMaxValues ==========\n"; $base = $base + 0.1; } } } /* }}} */ /** The graph Axis Horizontal class */ class graphAxisHorizontal extends graphAxisGeneral /* {{{ */ { /** Calculate the position in pixels for a value * If the value is out of range, return null to not draw the point * @param string|float|integer $value The value to position */ public function position ($value) { if ($value === null) return null; if (! is_numeric ($value) && ! is_string ($value)) throw new \Exception (dgettext ("domframework", "Invalid value provided to graph Axis for position")." ". get_class ($this), 406); if ($this->numerical === null) throw new \Exception (dgettext ("domframework", "No numerical type defined for Axis")." ".get_class ($this), 406); if ($this->numerical) { // Numerical axis, use a standard scale if ($value < $this->min || $value > $this->max) return null; if (($this->max - $this->min) == 0) $dividor = 1; else $dividor = ($this->max - $this->min); $scale = ($value - $this->min) / $dividor; return intval ($this->left + $scale * ($this->right - $this->left)); } else { // Label axis, count them if (! is_array ($this->data)) throw new \Exception (dgettext ("domframework", "No data defined for Axis")." ".get_class ($this), 406); $pos = array_search ($value, $this->data); if ($pos === false) return null; $width = ($this->right - $this->left) / count ($this->data); return intval ($this->left + $width * $pos + $width / 2); } } /** Calculate the positionMin, used for labeled axies * If the value is out of range, return null to not draw the point * @param string|float|integer $value The value to position */ public function positionMin ($value) { if ($this->numerical) return $posCenter; if (! is_array ($this->data)) throw new \Exception (dgettext ("domframework", "No data defined for Axis")." ".get_class ($this), 406); $pos = array_search ($value, $this->data); if ($pos === false) return null; $width = ($this->right - $this->left) / count ($this->data); return intval ($this->left + $width * $pos); } /** Calculate the positionMax, used for labeled axies * If the value is out of range, return null to not draw the point * @param string|float|integer $value The value to position */ public function positionMax ($value) { $posCenter = $this->position ($value); if ($this->numerical) return $posCenter; if (! is_array ($this->data)) throw new \Exception (dgettext ("domframework", "No data defined for Axis")." ".get_class ($this), 406); $pos = array_search ($value, $this->data); if ($pos === false) return null; $width = ($this->right - $this->left) / count ($this->data); return intval ($this->left + $width * $pos + $width); } } /* }}} */ /** The X axis management */ class graphAxisX extends graphAxisHorizontal /* {{{ */ { /** The angle choosed to draw the graph */ private $angle; /** The padding between the label and the axis */ private $padding = 7; /** The heigth of the labels + padding (it is the base of the graph) in pixels */ private $height; /** The number of chars to be displayed in one label */ protected $nbcharsLabel = 0; /** Look for the height of the X axis based on the angle of the text when it * will be drawn * @param resource $gd The resource to modify */ public function getHeight ($gd) { // Look for the angle of the value. Start Horizontally (angle=0), then // try 45°, then finish at 90°. As all the values must in the same angle, // test all the values. If one is bad, change the angle and retry all the // values $width = null; if ($this->numerical) { if ($this->max === null) throw new \Exception (dgettext ("domframework", "The max is not defined for graphAxisHorizontal"), 406); $powMax = intval (log10 ($this->max)); $tenPercent = round ($this->max / pow (10, $powMax), 1) + 0.1; $labelMax = $tenPercent * pow (10, $powMax); $this->nbcharsLabel = strlen ($labelMax); $bbox = imagettfbbox ($this->fontsize, $this->angle, $this->fontfile, $labelMax); $height = abs ($bbox[4] - $bbox[0]); $this->height = $height + 2 * $this->padding; return $this->height; } else { if ($this->data === null) return 0; foreach (array (0, 45, 90) as $this->angle) { $bboxMaxHeight = 0; foreach ($this->data as $key=>$value) { if ($width === null) $width = $this->positionMax ($value) - $this->positionMin ($value); // Look for the bounding box around the text. The bounding is not // write on the image and return the coordinates for the text box $bbox = imagettfbbox ($this->fontsize, $this->angle, $this->fontfile, $value); if (abs ($bbox[4] - $bbox[0]) > $width) continue 2; $bboxMaxHeight = max ($bboxMaxHeight, abs ($bbox[5] - $bbox[1])); } // All the values are OK : we have found the angle : break the angle // loop break; } } $this->height = $bboxMaxHeight + 2 * $this->padding; return $bboxMaxHeight + 2 * $this->padding; } /** Draw the axis * @param resource $gd The resource to modify */ public function draw ($gd) { $axisColor = \color::allocateFromText ($gd, $this->axisColor); if ($this->numerical) { if ($this->angle === null) $this->getHeight ($gd); if ($this->data === null) return; foreach ($this->data as $key=>$value) { $position = $this->position ($value); // Draw the labels $bbox = imagettfbbox ($this->fontsize, $this->angle, $this->fontfile, $value); $width = abs ($bbox[4] - $bbox[0]); $x = $position - $width / 2; $y = $this->bottom - $this->padding; // The font color is forced to black imagettftext ($gd, $this->fontsize, $this->angle, $x, $y, \color::allocateFromText ($gd, "black"), $this->fontfile, $value); // Draw the scale imageline ($gd, $position, $this->bottom - $this->height + 1, $position, $this->bottom - $this->height + 1 + $this->padding, $axisColor); // Draw the grid $this->drawGrid ($gd, $this->bottom - $this->height, $position); } // Draw the axis $y = $this->bottom - $this->height + 1; imageline ($gd, $this->left, $y, $this->right, $y, $axisColor); } else { if ($this->angle === null) $this->getHeight ($gd); if ($this->data === null) return; foreach ($this->data as $key=>$value) { $position = $this->position ($value); // Draw the labels $bbox = imagettfbbox ($this->fontsize, $this->angle, $this->fontfile, $value); $width = abs ($bbox[4] - $bbox[0]); $x = $position - $width / 2; $y = $this->bottom - $this->padding; // The font color is forced to black imagettftext ($gd, $this->fontsize, $this->angle, $x, $y, \color::allocateFromText ($gd, "black"), $this->fontfile, $value); // Draw the separators $y = $this->bottom - $this->height + 1; $xmin = $this->positionMin ($value); $xmax = $this->positionMax ($value); imageline ($gd, $xmin, $y, $xmin, $y + $this->padding, $axisColor); imageline ($gd, $xmax, $y, $xmax, $y + $this->padding, $axisColor); // Draw the grid $this->drawGrid ($gd, $this->bottom - $this->height, $position); } // Draw the axis $y = $this->bottom - $this->height + 1; imageline ($gd, $this->left, $y, $this->right, $y, $axisColor); } } /** Draw the grid * @param resource $gd The resource to modify * @param integer $width The width of the labels on the axis * @param integer|float $position The position to draw */ protected function drawGrid ($gd, $width, $position) { if ($this->gridColor === null || $this->gridColor === "transparent") return; $gridColor = \color::allocateFromText ($gd, $this->gridColor); if ($position === null) return; $y = $this->bottom - $this->height; imageline ($gd, $position, $y, $position, $this->top, $gridColor); } } /* }}} */ /** Manage the vertical axis */ class graphAxisVertical extends graphAxisGeneral /* {{{ */ { /** The angle choosed to draw the graph */ private $angle; /** The padding between the label and the axis */ protected $padding = 7; /** The width of the labels + padding (it is the base of the graph) in pixels */ protected $width; /** The number of chars to be displayed in one label */ protected $nbcharsLabel = 0; /** Look for the width of the Y axis * @param resource $gd The resource to modify */ public function getWidth ($gd) { if ($this->numerical) { if ($this->max === null) throw new \Exception (dgettext ("domframework", "The max is not defined for graphAxisVertical"), 406); // Look at the minimum distance between two labeled values $bbox = imagettfbbox ($this->fontsize, 0, $this->fontfile, "NOT"); $height = abs ($bbox[5] - $bbox[1]) + $this->padding; for ($nbMaxValues = 10 ; $nbMaxValues > 2 ; $nbMaxValues--) { if ($nbMaxValues * $height < abs ($this->top - $this->bottom)) break; } $labels = $this->labels ($nbMaxValues); $labelBiggest = ""; $this->nbcharsLabel = 0; foreach ($labels as $label) { if (strlen ($label) > $this->nbcharsLabel) { $this->nbcharsLabel = strlen ($label); $labelBiggest = $label; } } $bbox = imagettfbbox ($this->fontsize, $this->angle, $this->fontfile, $labelBiggest); $width = abs ($bbox[4] - $bbox[0]); $this->width = $width + 2 * $this->padding; return $this->width; } else { if ($this->data === null) return $this->padding; die ("TODO : getWidth in labeled line ".__LINE__."\n"); } } /** Calculate the position in pixels for a value * If the value is out of range, return null to not draw the point * @param string|float|integer $value The value to position */ public function position ($value) { if ($value === null) return null; if (! is_numeric ($value) && ! is_string ($value)) throw new \Exception (dgettext ("domframework", "Invalid value provided to graph Axis for position")." ". get_class ($this), 406); if ($this->numerical === null) throw new \Exception (dgettext ("domframework", "No numerical type defined for Axis")." ".get_class ($this), 406); if ($this->numerical) { // Numerical axis, use a standard scale if ($value > $this->labelMax || $value < $this->labelMin) return null; $scale = ($value - $this->labelMin) / ($this->labelMax - $this->labelMin); $pos = intval ($this->bottom + $scale * ($this->top - $this->bottom)); return $pos; } else { // Label axis, count them if (! is_array ($this->data)) throw new \Exception (dgettext ("domframework", "No data defined for Axis")." ".get_class ($this), 406); $pos = array_search ($value, $this->data); if ($pos === false) return null; $width = ($this->top - $this->bottom) / count ($this->data); $pos = intval ($this->bottom + $width * $pos + $width / 2); return $pos; } } /** Calculate the positionMin, used for labeled axies * If the value is out of range, return null to not draw the point * @param string|float|integer $value The value to position */ public function positionMin ($value) { if ($this->numerical) return $posCenter; if (! is_array ($this->data)) throw new \Exception (dgettext ("domframework", "No data defined for Axis")." ".get_class ($this), 406); $pos = array_search ($value, $this->data); if ($pos === false) return null; $width = ($this->top - $this->bottom) / count ($this->data); return intval ($this->bottom + $width * $pos); } /** Calculate the positionMax, used for labeled axies * If the value is out of range, return null to not draw the point * @param string|float|integer $value The value to position */ public function positionMax ($value) { $posCenter = $this->position ($value); if ($this->numerical) return $posCenter; if (! is_array ($this->data)) throw new \Exception (dgettext ("domframework", "No data defined for Axis")." ".get_class ($this), 406); $pos = array_search ($value, $this->data); if ($pos === false) return null; $width = ($this->top - $this->bottom) / count ($this->data); return intval ($this->bottom + $width * $pos + $width); } /** Draw the axis labels and lines * @param resource $gd The resource to modify */ public function draw ($gd) { $axisColor = \color::allocateFromText ($gd, $this->axisColor); if ($this->numerical) { // Look at the minimum distance between two labeled values $bbox = imagettfbbox ($this->fontsize, 0, $this->fontfile, "NOT"); $height = abs ($bbox[5] - $bbox[1]) + $this->padding; for ($nbMaxValues = 10 ; $nbMaxValues > 2 ; $nbMaxValues--) { if ($nbMaxValues * $height < abs ($this->top - $this->bottom)) break; } $width = $this->getWidth ($gd); $labels = $this->labels ($nbMaxValues); if (count ($labels) > $nbMaxValues) die ("Too much labels to display (". count ($labels)." > $nbMaxValues)\n"); foreach ($labels as $label) { $this->drawOne ($gd, $width, $label); $this->drawGrid ($gd, $width, $label); } $this->drawAxis ($gd, $width); } else { // Labeled // If there is no data, there is nothing to draw : return if ($this->data === null) return; die ("graphAxisVertical::draw labeled TBD line ".__LINE__."\n"); } } } /* }}} */ /** The Y1 axis management */ class graphAxisY1 extends graphAxisVertical /* {{{ */ { /** Draw one value on the axis * @param resource $gd The resource to modify * @param integer $width The width of the labels on the axis * @param integer|float $val The value to draw */ protected function drawOne ($gd, $width, $val) { $axisColor = \color::allocateFromText ($gd, $this->axisColor); if ($this->numerical) { // Do not allow the label to be longer than $this->max if (strlen ($val) > $this->nbcharsLabel) $itmp = rtrim (substr ($val, 0, $this->nbcharsLabel), "."); else $itmp = $val; // Write the labels align to right $bbox = imagettfbbox ($this->fontsize, 0, $this->fontfile, $itmp); $labelwidth = abs ($bbox[4] - $bbox[0]) + $this->padding; $labelheight = abs ($bbox[5] - $bbox[1]); $y = $this->position ($val); if ($y === null) return; // The color is forced to black imagettftext ($gd, $this->fontsize, 0, $width - $labelwidth, $y + $labelheight / 2, \color::allocateFromText ($gd, "black"), $this->fontfile, $itmp); // Display the separators imageline ($gd, $width - $this->padding, $y, $width, $y, $axisColor); } else { die ("graphAxisY1:: drawOne NOT numerical line ".__LINE__."\n"); } } /** Draw the axis * @param resource $gd The resource to modify * @param integer $width The width of the labels on the axis */ protected function drawAxis ($gd, $width) { $axisColor = \color::allocateFromText ($gd, $this->axisColor); imageline ($gd, $width, $this->bottom, $width, $this->top, $axisColor); } /** Draw the grid * @param resource $gd The resource to modify * @param integer $width The width of the labels on the axis * @param integer|float $val The value to draw */ protected function drawGrid ($gd, $width, $val) { if ($this->gridColor === null || $this->gridColor === "transparent") return; $gridColor = \color::allocateFromText ($gd, $this->gridColor); if ($this->numerical) { $y = $this->position ($val); if ($y === null) return; imageline ($gd, $width, $y, $this->right, $y, $gridColor); } else { die ("graphAxisY1:: drawGrid NOT numerical line ".__LINE__."\n"); } } } /* }}} */ /** The Y2 axis management */ class graphAxisY2 extends graphAxisVertical /* {{{ */ { /** Draw one value on the axis * @param resource $gd The resource to modify * @param integer $width The width of the labels on the axis * @param integer|float $val The value to draw */ protected function drawOne ($gd, $width, $val) { $axisColor = \color::allocateFromText ($gd, $this->axisColor); if ($this->numerical) { // Do not allow the label to be longer than $this->max if (strlen ($val) > $this->nbcharsLabel) $itmp = rtrim (substr ($val, 0, $this->nbcharsLabel), "."); else $itmp = $val; // Write the labels align to left $bbox = imagettfbbox ($this->fontsize, 0, $this->fontfile, $itmp); $labelwidth = abs ($bbox[4] - $bbox[0]) + $this->padding; $labelheight = abs ($bbox[5] - $bbox[1]); $y = $this->position ($val); if ($y === null) return; // The color is forced to black imagettftext ($gd, $this->fontsize, 0, $this->left + $this->padding + 3, $y + $labelheight / 2, \color::allocateFromText ($gd, "black"), $this->fontfile, $itmp); // Display the separators imageline ($gd, $this->left, $y, $this->left + $this->padding, $y, $axisColor); } else { die ("graphAxisY2:: drawOne NOT numerical line ".__LINE__."\n"); } } /** Draw the axis * @param resource $gd The resource to modify * @param integer $width The width of the labels on the axis */ protected function drawAxis ($gd, $width) { $axisColor = \color::allocateFromText ($gd, $this->axisColor); imageline ($gd, $this->left, $this->bottom, $this->left, $this->top, $axisColor); } /** Draw the grid * @param resource $gd The resource to modify * @param integer $width The width of the labels on the axis * @param integer|float $val The value to draw */ protected function drawGrid ($gd, $width, $val) { // The grid can't be graphed for Y2 axis : only the Y1 axis is allowed return; } } /* }}} */ /** The graphStyleLine : draw a graph with lines */ class graphStyleLinePoints /* {{{ */ { /** The line color. To hide the lines, choose "transparent" */ protected $lineColor; /** The point color background. To hide the points, choose "transparent" */ protected $pointBgcolor; /** The point color border */ protected $pointColor; /** The point shape (square, circle, triangle, lozenge) */ protected $pointShape; /** The point width in pixel */ protected $pointWidth; /** The number of the colors/shapes to use */ protected $number; /** The allowed shapes */ protected $allowedShapes = array ("square", "circle", "triangle", "lozenge"); /** The palette to use */ protected $palette; /** Return the name of the style */ public function name () { return "lineAndPoints"; } /** Set the line color if the parameter is provided. * Get the line color if the parameter is not provided * @param string|null $lineColor The line color */ public function lineColor ($lineColor = null) { if ($lineColor === null) return $this->lineColor; if (! is_string ($lineColor) || ($lineColor !== "transparent" && ! in_array ($lineColor, \color::colorList ()))) throw new \Exception (dgettext ("domframework", "Invalid lineColor provided to line style"), 406); $this->lineColor = $lineColor; return $this; } /** Set the palette to use if the parameter is provided * Get the palette if the parameter is not provided * @param string|null $palette The palette to use */ public function palette ($palette = null) { if ($palette === null) return $this->palette; if (! is_string ($palette)) throw new \Exception (dgettext ("domframework", "Invalid palette provided to line style"), 406); $this->palette = $palette; return $this; } /** Set the point background color if the parameter is provided. * Get the point background color if the parameter is not provided * @param string|null $pointBgcolor The point background color */ public function pointBgcolor ($pointBgcolor = null) { if ($pointBgcolor === null) return $this->pointBgcolor; if (! is_string ($pointBgcolor) || ($pointBgcolor !== "transparent" && ! in_array ($pointBgcolor, \color::colorList ()))) throw new \Exception (dgettext ("domframework", "Invalid pointBgcolor provided to line style"), 406); $this->pointBgcolor = $pointBgcolor; return $this; } /** Set the point border color if the parameter is provided. * Get the point border color if the parameter is not provided * @param string|null $pointColor The point border color */ public function pointColor ($pointColor = null) { if ($pointColor === null) return $this->pointColor; if (! is_string ($pointColor) || ($pointColor !== "transparent" && ! in_array ($pointColor, \color::colorList ()))) throw new \Exception (dgettext ("domframework", "Invalid pointColor provided to line style"), 406); $this->pointColor = $pointColor; return $this; } /** Set the point shape if the parameter is provided. * Get the point shape if the parameter is not provided * @param string|null $pointShape The point shape */ public function pointShape ($pointShape = null) { if ($pointShape === null) return $this->pointShape; if (! is_string ($pointShape) || ! in_array ($pointShape, $this->allowedShapes)) throw new \Exception (dgettext ("domframework", "Invalid pointShape provided to line style"), 406); $this->pointShape = $pointShape; return $this; } /** Set the point width if the parameter is provided. * Get the point width if the parameter is not provided * @param string|null $pointWidth The point width */ public function pointWidth ($pointWidth = null) { if ($pointWidth === null) return $this->pointWidth; if (! is_integer ($pointWidth) || $pointWidth < 3 || $pointWidth > 20) throw new \Exception (dgettext ("domframework", "Invalid pointWidth provided to line style"), 406); $this->pointWidth = $pointWidth; return $this; } /** Select the colors, shapes by the serie number. * Do not change any property if the property is already defined * If the parameter is not provided, return the value * @param integer|null $number The serie number */ public function number ($number = null) { if ($number === null) return $this->number; if (! is_int ($number) || $number < 0) throw new \Exception (dgettext ("domframework", "Invalid number provided to line style number"), 406); $this->number = $number; if ($this->pointShape === null) $this->pointShape = $this->allowedShapes[ ($number%count($this->allowedShapes))]; $palette = graphPalette::getPalette ($this->palette); if ($this->pointBgcolor === null) $this->pointBgcolor = $palette[($number%count ($palette))]["bgcolor"]; if ($this->pointColor === null) $this->pointColor = $palette[($number%count ($palette))]["color"]; if ($this->lineColor === null) $this->lineColor = $palette[($number%count ($palette))]["color"]; } /** Draw in the $gd resource, in the $free array, the data with the parameter * of the style * @param resource $gd The resource to modify * @param array $free The free space coordinates on the graphic * @param array $data The data to graph * @param object $axisX The X axis used to graph * @param object $axisY The Y axis used to graph */ public function draw ($gd, $free, $data, $axisX, $axisY) { if ($this->lineColor !== "transparent") $lineColor = \color::allocateFromText ($gd, $this->lineColor); $lastX = null; $lastY = null; foreach ($data as $key=>$value) { $posX = $axisX->position ($key); $posY = $axisY->position ($value); if ($posX === null || $posY === null) { // NULL position : skip the point $lastX = null; $lastY = null; continue; } // Draw the lines between points (except if the last point was null, or // if the line color is "transparent") if ($lastX !== null && $lastY !== null && $this->lineColor !== "transparent") { imageline ($gd, $lastX, $lastY, $posX, $posY, $lineColor); // Redraw the old point which was scratch by the new created line $this->drawPoint ($gd, $lastX, $lastY); } if ($lastX === null) $lastX = $posX; if ($lastY === null) $lastY = $posY; // Draw the point. Will overwrite the lines if ($this->pointWidth === null) $this->pointWidth = 6; $this->drawPoint ($gd, $posX, $posY); $lastX = $posX; $lastY = $posY; } } /** Draw a point defined in the property of the class * @param resource $gd The resource to modify * @param integer $posX The X position to draw the point * @param integer $posY The Y position to draw the point */ private function drawPoint ($gd, $posX, $posY) { if ($this->pointColor !== "transparent") $pointColor = \color::allocateFromText ($gd, $this->pointColor); if ($this->pointBgcolor !== "transparent") $pointBgcolor = \color::allocateFromText ($gd, $this->pointBgcolor); $half = intval ($this->pointWidth / 2); switch ($this->pointShape) { case "circle": if ($this->pointBgcolor !== "transparent") imagefilledellipse ($gd, $posX, $posY, $this->pointWidth, $this->pointWidth, $pointBgcolor + 2); if ($this->pointColor !== "transparent") imageellipse ($gd, $posX, $posY, $this->pointWidth + 2, $this->pointWidth + 2, $pointColor); break; case "lozenge": if ($this->pointBgcolor !== "transparent") imagefilledpolygon ($gd, array ($posX - $half - 2, $posY, $posX, $posY - $half - 2, $posX + $half + 2, $posY, $posX, $posY + $half + 2), 4, $pointBgcolor); if ($this->pointColor !== "transparent") imagepolygon ($gd, array ($posX - $half - 2, $posY, $posX, $posY - $half - 2, $posX + $half + 2, $posY, $posX, $posY + $half + 2), 4, $pointColor); break; case "square": if ($this->pointBgcolor !== "transparent") imagefilledrectangle ($gd, $posX - $half - 1, $posY - $half - 1, $posX + $half + 1, $posY + $half + 1, $pointBgcolor); if ($this->pointColor !== "transparent") imagerectangle ($gd, $posX - $half - 1, $posY - $half - 1, $posX + $half + 1, $posY + $half + 1, $pointColor); break; case "triangle": if ($this->pointBgcolor !== "transparent") imagefilledpolygon ($gd, array ($posX - $half -2, $posY + $half + 2, $posX, $posY - $half -2, $posX + $half + 2, $posY + $half + 2), 3, $pointBgcolor); if ($this->pointColor !== "transparent") imagepolygon ($gd, array ($posX - $half - 2, $posY + $half + 2, $posX, $posY - $half - 2, $posX + $half + 2, $posY + $half + 2), 3, $pointColor); break; default: throw new \Exception (dgettext ("domframework", "Unknown pointShape for serie"), 406); } } /** Draw a sample of the style for the legend * @param resource $gd The resource to modify * @param integer $x The central position of the sample in x * @param integer $y The central position of the sample in y */ public function sample ($gd, $x, $y) { if ($this->lineColor !== "transparent") { $lineColor = \color::allocateFromText ($gd, $this->lineColor); imageline ($gd, $x-10, $y, $x+10, $y, $lineColor); } if ($this->pointWidth === null) $this->pointWidth = 6; $this->drawPoint ($gd, $x, $y); } } /* }}} */ /** The graphStylePoints : draw a graph with points */ class graphStylePoints extends graphStyleLinePoints /* {{{ */ { /** The line color : transparent */ protected $lineColor = "transparent"; /** Return the name of the style */ public function name () { return "points"; } } /* }}} */ /** The graphStyleLine : draw a graph with line */ class graphStyleLine extends graphStyleLinePoints /* {{{ */ { /** The point color background. To hide the points, choose "transparent" */ protected $pointBgcolor = "transparent"; /** The point color border */ protected $pointColor = "transparent"; /** Return the name of the style */ public function name () { return "line"; } } /* }}} */ /** The graphPalette class */ class graphPalette /* {{{ */ { /** Get the complete palette * @param string $name The palette name to get */ public static function getPalette ($name) { $palette = array ( "basic" => array ( array ("bgcolor"=>"indianred", "color"=>"firebrick"), array ("bgcolor"=>"lightblue", "color"=>"blue"), array ("bgcolor"=>"peru", "color"=>"maroon"), array ("bgcolor"=>"mediumaquamarine", "color"=>"teal"), array ("bgcolor"=>"orange", "color"=>"goldenrod"), array ("bgcolor"=>"limegreen", "color"=>"green"), array ("bgcolor"=>"darkgrey", "color"=>"grey"), array ("bgcolor"=>"darkyellow1", "color"=>"darkyellow2"), array ("bgcolor"=>"grey", "color"=>"black"), ), ); if (! is_string ($name) || ! array_key_exists ($name, $palette)) throw new \Exception (dgettext ("domframework", "Unknown palette name provided"), 406); return $palette[$name]; } } /* }}} */