diff --git a/graph.php b/graph.php new file mode 100644 index 0000000..50eedcd --- /dev/null +++ b/graph.php @@ -0,0 +1,2586 @@ + */ + +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 + */ + public function drawImage () + { + // 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); + + // 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; + header ('Content-Type: image/png'); + imagepng ($gd); + imagedestroy ($gd); + } + + /** Return the image coded in base64 + * @return string The base64 string + */ + public function drawBase64 () + { + ob_start (); + $this->drawImage (); + 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 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 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"))) + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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|float $value The value to draw + * @param integer $width The width of the labels on the axis + */ + 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|float $value The value to draw + * @param integer $width The width of the labels on the axis + */ + 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->pointColor !== "transparent") + imageellipse ($gd, $posX, $posY, $this->pointWidth + 2, + $this->pointWidth + 2, $pointColor); + if ($this->pointBgcolor !== "transparent") + imagefilledellipse ($gd, $posX, $posY, $this->pointWidth, + $this->pointWidth, $pointBgcolor); + break; + case "lozenge": + 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); + if ($this->pointBgcolor !== "transparent") + imagefilledpolygon ($gd, array ($posX - $half, $posY, + $posX, $posY - $half, + $posX + $half, $posY, + $posX, $posY + $half), + 4, $pointBgcolor); + break; + case "square": + if ($this->pointColor !== "transparent") + imagerectangle ($gd, $posX - $half - 1, $posY - $half - 1, + $posX + $half + 1, $posY + $half + 1, + $pointColor); + if ($this->pointBgcolor !== "transparent") + imagefilledrectangle ($gd, $posX - $half, $posY - $half, + $posX + $half, $posY + $half, $pointBgcolor); + break; + case "triangle": + if ($this->pointColor !== "transparent") + imagepolygon ($gd, array ($posX - $half - 2, $posY + $half + 2, + $posX, $posY - $half - 4, + $posX + $half + 2, $posY + $half + 2), + 3, $pointColor); + if ($this->pointBgcolor !== "transparent") + imagefilledpolygon ($gd, array ($posX - $half, $posY + $half, + $posX, $posY - $half, + $posX + $half, $posY + $half), + 3, $pointBgcolor); + 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 */ + 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"=>"goldenrod", "color"=>"orange"), + 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]; + } +} +/* }}} */