icingaweb2/library/Icinga/Chart/GridChart.php

447 lines
14 KiB
PHP

<?php
/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
namespace Icinga\Chart;
use DOMElement;
use Icinga\Chart\Chart;
use Icinga\Chart\Axis;
use Icinga\Chart\Graph\BarGraph;
use Icinga\Chart\Graph\LineGraph;
use Icinga\Chart\Graph\StackedGraph;
use Icinga\Chart\Graph\Tooltip;
use Icinga\Chart\Primitive\Canvas;
use Icinga\Chart\Primitive\Rect;
use Icinga\Chart\Primitive\Path;
use Icinga\Chart\Render\LayoutBox;
use Icinga\Chart\Render\RenderContext;
use Icinga\Chart\Unit\AxisUnit;
/**
* Base class for grid based charts.
*
* Allows drawing of Line and Barcharts. See the graphing documentation for further details.
*
* Example:
* <pre>
* <code>
* $this->chart = new GridChart();
* $this->chart->setAxisLabel("X axis label", "Y axis label");
* $this->chart->setXAxis(Axis::CalendarUnit());
* $this->chart->drawLines(
* array(
* 'data' => array(
* array(time()-7200, 10),array(time()-3620, 30), array(time()-1800, 15), array(time(), 92))
* )
* );
* </code>
* </pre>
*/
class GridChart extends Chart
{
/**
* Internal identifier for Line Chart elements
*/
const TYPE_LINE = "LINE";
/**
* Internal identifier fo Bar Chart elements
*/
const TYPE_BAR = "BAR";
/**
* Internal array containing all elements to be drawn in the order they are drawn
*
* @var array
*/
private $graphs = array();
/**
* An associative array containing all axis of this Chart in the "name" => Axis() form.
*
* Currently only the 'default' axis is really supported
*
* @var array
*/
private $axis = array();
/**
* An associative array containing all StackedGraph objects used for cumulative graphs
*
* The array key is the 'stack' value given in the graph definitions
*
* @var array
*/
private $stacks = array();
/**
* An associative array containing all Tooltips used to render the titles
*
* Each tooltip represents the summary for all y-values of a certain x-value
* in the grid chart
*
* @var Tooltip
*/
private $tooltips = array();
public function __construct()
{
$this->title = t('Grid Chart');
$this->description = t('Contains data in a bar or line chart.');
parent::__construct();
}
/**
* Check if the current dataset has the proper structure for this chart.
*
* Needs to be overwritten by extending classes. The default implementation returns false.
*
* @return bool True when the dataset is valid, otherwise false
*/
public function isValidDataFormat()
{
foreach ($this->graphs as $values) {
foreach ($values as $value) {
if (!isset($value['data']) || !is_array($value['data'])) {
return false;
}
}
}
return true;
}
/**
* Calls Axis::addDataset for every graph added to this GridChart
*
* @see Axis::addDataset
*/
private function configureAxisFromDatasets()
{
foreach ($this->graphs as $axis => &$graphs) {
$axisObj = $this->axis[$axis];
foreach ($graphs as &$graph) {
$axisObj->addDataset($graph);
}
}
}
/**
* Add an arbitrary number of lines to be drawn
*
* Refer to the graphs.md for a detailed list of allowed attributes
*
* @param array $axis,... The line definitions to draw
*
* @return $this Fluid interface
*/
public function drawLines(array $axis)
{
$this->draw(self::TYPE_LINE, func_get_args());
return $this;
}
/**
* Add arbitrary number of bars to be drawn
*
* Refer to the graphs.md for a detailed list of allowed attributes
*
* @param array $axis
* @return $this
*/
public function drawBars(array $axis)
{
$this->draw(self::TYPE_BAR, func_get_args());
return $this;
}
/**
* Generic method for adding elements to the drawing stack
*
* @param string $type The type of the element to draw (see TYPE_ constants in this class)
* @param array $data The data given to the draw call
*/
private function draw($type, $data)
{
$axisName = 'default';
if (is_string($data[0])) {
$axisName = $data[0];
array_shift($data);
}
foreach ($data as &$graph) {
$graph['graphType'] = $type;
if (isset($graph['stack'])) {
if (!isset($this->stacks[$graph['stack']])) {
$this->stacks[$graph['stack']] = new StackedGraph();
}
$this->stacks[$graph['stack']]->addGraph($graph);
$graph['stack'] = $this->stacks[$graph['stack']];
}
if (!isset($graph['color'])) {
$colorType = isset($graph['palette']) ? $graph['palette'] : Palette::NEUTRAL;
$graph['color'] = $this->palette->getNext($colorType);
}
$this->graphs[$axisName][] = $graph;
if ($this->legend) {
$this->legend->addDataset($graph);
}
}
$this->initTooltips($data);
}
private function initTooltips($data)
{
foreach ($data as &$graph) {
foreach ($graph['data'] as $x => $point) {
if (!array_key_exists($x, $this->tooltips)) {
$this->tooltips[$x] = new Tooltip(
array(
'color' => $graph['color'],
)
);
}
$this->tooltips[$x]->addDataPoint($point);
}
}
}
/**
* Set the label for the x and y axis
*
* @param string $xAxisLabel The label to use for the x axis
* @param string $yAxisLabel The label to use for the y axis
* @param string $axisName The name of the axis, for now 'default'
*
* @return $this Fluid interface
*/
public function setAxisLabel($xAxisLabel, $yAxisLabel, $axisName = 'default')
{
$this->axis[$axisName]->setXLabel($xAxisLabel)->setYLabel($yAxisLabel);
return $this;
}
/**
* Set the AxisUnit to use for calculating the values of the x axis
*
* @param AxisUnit $unit The unit for the x axis
* @param string $axisName The name of the axis to set the label for, currently only 'default'
*
* @return $this Fluid interface
*/
public function setXAxis(AxisUnit $unit, $axisName = 'default')
{
$this->axis[$axisName]->setUnitForXAxis($unit);
return $this;
}
/**
* Set the AxisUnit to use for calculating the values of the y axis
*
* @param AxisUnit $unit The unit for the y axis
* @param string $axisName The name of the axis to set the label for, currently only 'default'
*
* @return $this Fluid interface
*/
public function setYAxis(AxisUnit $unit, $axisName = 'default')
{
$this->axis[$axisName]->setUnitForYAxis($unit);
return $this;
}
/**
* Pre-render setup of the axis
*
* @see Chart::build
*/
protected function build()
{
$this->configureAxisFromDatasets();
}
/**
* Initialize the renderer and overwrite it with an 2:1 ration renderer
*/
protected function init()
{
$this->renderer = new SVGRenderer(100, 100);
$this->setAxis(Axis::createLinearAxis());
}
/**
* Overwrite the axis to use
*
* @param Axis $axis The new axis to use
* @param string $name The name of the axis, currently only 'default'
*
* @return $this Fluid interface
*/
public function setAxis(Axis $axis, $name = 'default')
{
$this->axis = array($name => $axis);
return $this;
}
/**
* Add an axis to this graph (not really supported right now)
*
* @param Axis $axis The axis object to add
* @param string $name The name of the axis
*
* @return $this Fluid interface
*/
public function addAxis(Axis $axis, $name)
{
$this->axis[$name] = $axis;
return $this;
}
/**
* Set minimum values for the x and y axis.
*
* Setting null to an axis means this will use a value determined by the dataset
*
* @param int $xMin The minimum value for the x axis or null to use a dynamic value
* @param int $yMin The minimum value for the y axis or null to use a dynamic value
* @param string $axisName The name of the axis to set the minimum, currently only 'default'
*
* @return $this Fluid interface
*/
public function setAxisMin($xMin = null, $yMin = null, $axisName = 'default')
{
$this->axis[$axisName]->setXMin($xMin)->setYMin($yMin);
return $this;
}
/**
* Set maximum values for the x and y axis.
*
* Setting null to an axis means this will use a value determined by the dataset
*
* @param int $xMax The maximum value for the x axis or null to use a dynamic value
* @param int $yMax The maximum value for the y axis or null to use a dynamic value
* @param string $axisName The name of the axis to set the maximum, currently only 'default'
*
* @return $this Fluid interface
*/
public function setAxisMax($xMax = null, $yMax = null, $axisName = 'default')
{
$this->axis[$axisName]->setXMax($xMax)->setYMax($yMax);
return $this;
}
/**
* Render this GridChart to SVG
*
* @param RenderContext $ctx The context to use for rendering
*
* @return DOMElement
*/
public function toSvg(RenderContext $ctx)
{
$outerBox = new Canvas('outerGraph', new LayoutBox(0, 0, 100, 100));
$innerBox = new Canvas('graph', new LayoutBox(0, 0, 95, 90));
$maxPadding = array(0,0,0,0);
foreach ($this->axis as $axis) {
$padding = $axis->getRequiredPadding();
for ($i=0; $i < count($padding); $i++) {
$maxPadding[$i] = max($maxPadding[$i], $padding[$i]);
}
$innerBox->addElement($axis);
}
$this->renderGraphContent($innerBox);
$innerBox->getLayout()->setPadding($maxPadding[0], $maxPadding[1], $maxPadding[2], $maxPadding[3]);
$this->createContentClipBox($innerBox);
$outerBox->addElement($innerBox);
if ($this->legend) {
$outerBox->addElement($this->legend);
}
return $outerBox->toSvg($ctx);
}
/**
* Create a clip box that defines which area of the graph is drawable and adds it to the graph.
*
* The clipbox has the id '#clip' and can be used in the clip-mask element
*
* @param Canvas $innerBox The inner canvas of the graph to add the clip box to
*/
private function createContentClipBox(Canvas $innerBox)
{
$clipBox = new Canvas('clip', new LayoutBox(0, 0, 100, 100));
$clipBox->toClipPath();
$innerBox->addElement($clipBox);
$rect = new Rect(0.1, 0, 100, 99.9);
$clipBox->addElement($rect);
}
/**
* Render the content of the graph, i.e. the draw stack
*
* @param Canvas $innerBox The inner canvas of the graph to add the content to
*/
private function renderGraphContent(Canvas $innerBox)
{
foreach ($this->graphs as $axisName => $graphs) {
$axis = $this->axis[$axisName];
$graphObj = null;
foreach ($graphs as $dataset => $graph) {
// determine the type and create a graph object for it
switch ($graph['graphType']) {
case self::TYPE_BAR:
$graphObj = new BarGraph(
$axis->transform($graph['data']),
$graphs,
$dataset,
$this->tooltips
);
break;
case self::TYPE_LINE:
$graphObj = new LineGraph(
$axis->transform($graph['data']),
$graphs,
$dataset,
$this->tooltips
);
break;
default:
continue 2;
}
$el = $this->setupGraph($graphObj, $graph);
if ($el) {
$innerBox->addElement($el);
}
}
}
}
/**
* Setup the provided Graph type
*
* @param mixed $graphObject The graph class, needs the setStyleFromConfig method
* @param array $graphConfig The configration array of the graph
*
* @return mixed Either the graph to be added or null if the graph is not directly added
* to the document (e.g. stacked graphs are added by
* the StackedGraph Composite object)
*/
private function setupGraph($graphObject, array $graphConfig)
{
$graphObject->setStyleFromConfig($graphConfig);
// When in a stack return the StackedGraph object instead of the graphObject
if (isset($graphConfig['stack'])) {
$graphConfig['stack']->addToStack($graphObject);
if (!$graphConfig['stack']->stackEmpty()) {
return $graphConfig['stack'];
}
// return no object when the graph should not be rendered
return null;
}
return $graphObject;
}
}