Files
DomFramework/src/Graph.php
2022-11-25 21:21:30 +01:00

453 lines
15 KiB
PHP

<?php
/** DomFramework
* @package domframework
* @author Dominique Fournier <dominique@fournier38.fr>
* @license BSD
*/
namespace Domframework;
/** 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);
}
if (! function_exists("bccomp")) {
throw new \Exception(dgettext(
"domframework",
"No BCMath 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 = __NAMESPACE__ . "\\GraphStyle" . ucfirst($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());
}
}