Files
DomFramework/graph.php

2599 lines
81 KiB
PHP

<?php
/** DomFramework
@package domframework
@author Dominique Fournier <dominique@fournier38.fr> */
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 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", "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
*/
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];
}
}
/* }}} */