icingaweb2/library/Icinga/Chart/Axis.php

472 lines
14 KiB
PHP

<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga Web 2.
*
* Icinga Web 2 - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Chart;
use \DOMElement;
use \Icinga\Chart\Primitive\Drawable;
use \Icinga\Chart\Primitive\Line;
use \Icinga\Chart\Primitive\Text;
use \Icinga\Chart\Render\RenderContext;
use \Icinga\Chart\Unit\AxisUnit;
use \Icinga\Chart\Unit\CalendarUnit;
use \Icinga\Chart\Unit\LinearUnit;
/**
* Axis class for the GridChart class.
*
* Implements drawing functions for the axis and it's labels but delegates tick and label calculations
* to the AxisUnit implementations
*
* @see GridChart
* @see AxisUnit
*/
class Axis implements Drawable
{
/**
* Draw the label text horizontally
*/
const LABEL_ROTATE_HORIZONTAL = 'normal';
/**
* Draw the label text diagonally
*/
const LABEL_ROTATE_DIAGONAL = 'diagonal';
/**
* Whether to draw the horizontal lines for the background grid
*
* @var bool
*/
private $drawXGrid = true;
/**
* Whether to draw the vertical lines for the background grid
*
* @var bool
*/
private $drawYGrid = true;
/**
* The label for the x axis
*
* @var string
*/
private $xLabel = "";
/**
* The label for the y axis
*
* @var string
*/
private $yLabel = "";
/**
* The AxisUnit implementation to use for calculating the ticks for the x axis
*
* @var AxisUnit
*/
private $xUnit = null;
/**
* The AxisUnit implementation to use for calculating the ticks for the y axis
*
* @var AxisUnit
*/
private $yUnit = null;
/**
* If the displayed labels should be aligned horizontally or diagonally
*/
private $labelRotationStyle = self::LABEL_ROTATE_DIAGONAL;
/**
* Set the label rotation style for the horizontal axis
*
* <ul>
* <li><b>LABEL_ROTATE_HORIZONTAL</b>: Labels will be displayed horizontally </li>
* <li><b>LABEL_ROTATE_DIAGONAL</b>: Labels will be rotated by 45° </li>
* </ul>
*
* @param $style The rotation mode
*/
public function setHorizontalLabelRotationStyle($style)
{
$this->labelRotationStyle = $style;
}
/**
* Inform the axis about an added dataset
*
* This is especially needed when one or more AxisUnit implementations dynamically define
* their min or max values, as this is the point where they detect the min and max value
* from the datasets
*
* @param array $dataset An dataset to respect on axis generation
*/
public function addDataset(array $dataset)
{
$this->xUnit->addValues($dataset, 0);
$this->yUnit->addValues($dataset, 1);
}
/**
* Set the AxisUnit implementation to use for generating the x axis
*
* @param AxisUnit $unit The AxisUnit implementation to use for the x axis
*
* @return self This Axis Object
* @see Axis::CalendarUnit
* @see Axis::LinearUnit
*/
public function setUnitForXAxis(AxisUnit $unit)
{
$this->xUnit = $unit;
return $this;
}
/**
* Set the AxisUnit implementation to use for generating the y axis
*
* @param AxisUnit $unit The AxisUnit implementation to use for the y axis
*
* @return self This Axis Object
* @see Axis::CalendarUnit
* @see Axis::LinearUnit
*/
public function setUnitForYAxis(AxisUnit $unit)
{
$this->yUnit = $unit;
return $this;
}
/**
* Return the padding this axis requires
*
* @return array An array containing the padding for all sides
*/
public function getRequiredPadding()
{
return array(10, 5, 15, 10);
}
/**
* Render the horizontal axis
*
* @param RenderContext $ctx The context to use for rendering
* @param DOMElement $group The DOMElement this axis will be added to
*/
private function renderHorizontalAxis(RenderContext $ctx, DOMElement $group)
{
$line = new Line(0, 100, 100, 100);
$line->setStrokeWidth(2);
$group->appendChild($line->toSvg($ctx));
// contains the approximate end position of the last label
$lastLabelEnd = -1;
$shift = 0;
foreach ($this->xUnit as $label => $pos) {
if ($this->labelRotationStyle === self::LABEL_ROTATE_HORIZONTAL) {
// If the last label would overlap this label we shift the y axis a bit
if ($lastLabelEnd > $pos) {
$shift = ($shift + 5) % 10;
} else {
$shift = 0;
}
}
$tick = new Line($pos, 100, $pos, 102);
$group->appendChild($tick->toSvg($ctx));
$labelField = new Text($pos + 0.5, ($this->xLabel ? 107 : 105) + $shift, $label);
if ($this->labelRotationStyle === self::LABEL_ROTATE_HORIZONTAL) {
$labelField->setAlignment(Text::ALIGN_MIDDLE)
->setFontSize('1.8em');
} else {
$labelField->setFontSize('2.5em');
}
$labelField = $labelField->toSvg($ctx);
if ($this->labelRotationStyle === self::LABEL_ROTATE_DIAGONAL) {
$labelField = $this->rotate($ctx, $labelField, 45);
}
$group->appendChild($labelField);
if ($this->drawYGrid) {
$bgLine = new Line($pos, 0, $pos, 100);
$bgLine->setStrokeWidth(0.5)
->setStrokeColor('#232');
$group->appendChild($bgLine->toSvg($ctx));
}
$lastLabelEnd = $pos + strlen($label) * 1.2;
}
// render the label for this axis
if ($this->xLabel) {
$axisLabel = new Text(50, 104, $this->xLabel);
$axisLabel->setFontSize('2em')
->setFontWeight('bold')
->setAlignment(Text::ALIGN_MIDDLE);
$group->appendChild($axisLabel->toSvg($ctx));
}
}
/**
* Rotate the given element.
*
* @param RenderContext $ctx The rendering context
* @param DOMElement $el The element to rotate
* @param $degrees The rotation degrees
*
* @return DOMElement
*/
private function rotate(RenderContext $ctx, DOMElement $el, $degrees)
{
// Create a box containing the rotated element relative to the original text position
$container = $ctx->getDocument()->createElement('g');
$x = $el->getAttribute('x');
$y = $el->getAttribute('y');
$container->setAttribute('transform', 'translate(' . $x . ',' . $y . ')');
$el->removeAttribute('x');
$el->removeAttribute('y');
// Create a rotated box containing the text
$rotate = $ctx->getDocument()->createElement('g');
$rotate->setAttribute('transform', 'rotate(' . $degrees . ')');
$rotate->appendChild($el);
$container->appendChild($rotate);
return $container;
}
/**
* Render the vertical axis
*
* @param RenderContext $ctx The context to use for rendering
* @param DOMElement $group The DOMElement this axis will be added to
*/
private function renderVerticalAxis(RenderContext $ctx, DOMElement $group)
{
$line = new Line(0, 0, 0, 100);
$line->setStrokeWidth(2);
$group->appendChild($line->toSvg($ctx));
foreach ($this->yUnit as $label => $pos) {
$pos = 100 - $pos;
$tick = new Line(0, $pos, -1, $pos);
$group->appendChild($tick->toSvg($ctx));
$labelField = new Text(-0.5, $pos+0.5, $label);
$labelField->setFontSize('1.8em')
->setAlignment(Text::ALIGN_END);
$group->appendChild($labelField->toSvg($ctx));
if ($this->drawXGrid) {
$bgLine = new Line(0, $pos, 100, $pos);
$bgLine->setStrokeWidth(0.5)
->setStrokeColor('#343');
$group->appendChild($bgLine->toSvg($ctx));
}
}
if ($this->yLabel) {
$axisLabel = new Text(-8, 50, $this->yLabel);
$axisLabel->setFontSize('2em')
->setAdditionalStyle(Text::ORIENTATION_VERTICAL)
->setFontWeight('bold')
->setAlignment(Text::ALIGN_MIDDLE);
$group->appendChild($axisLabel->toSvg($ctx));
}
}
/**
* Factory method, create an Axis instance using Linear ticks as the unit
*
* @return Axis The axis that has been created
* @see LinearUnit
*/
public static function createLinearAxis()
{
$axis = new Axis();
$axis->setUnitForXAxis(self::linearUnit());
$axis->setUnitForYAxis(self::linearUnit());
return $axis;
}
/**
* Set the label for the x axis
*
* An empty string means 'no label'.
*
* @param string $label The label to use for the x axis
*
* @return $this Fluid interface
*/
public function setXLabel($label)
{
$this->xLabel = $label;
return $this;
}
/**
* Set the label for the y axis
*
* An empty string means 'no label'.
*
* @param string $label The label to use for the y axis
*
* @return self Fluid interface
*/
public function setYLabel($label)
{
$this->yLabel = $label;
return $this;
}
/**
* Set the labels minimum value for the x axis
*
* Setting the value to null let's the axis unit decide which value to use for the minimum
*
* @param int $xMin The minimum value to use for the x axis
*
* @return self Fluid interface
*/
public function setXMin($xMin)
{
$this->xUnit->setMin($xMin);
return $this;
}
/**
* Set the labels minimum value for the y axis
*
* Setting the value to null let's the axis unit decide which value to use for the minimum
*
* @param int $yMin The minimum value to use for the x axis
*
* @return self Fluid interface
*/
public function setYMin($yMin)
{
$this->yUnit->setMin($yMin);
return $this;
}
/**
* Set the labels maximum value for the x axis
*
* Setting the value to null let's the axis unit decide which value to use for the maximum
*
* @param int $xMax The minimum value to use for the x axis
*
* @return self Fluid interface
*/
public function setXMax($xMax)
{
$this->xUnit->setMax($xMax);
return $this;
}
/**
* Set the labels maximum value for the y axis
*
* Setting the value to null let's the axis unit decide which value to use for the maximum
*
* @param int $yMax The minimum value to use for the y axis
*
* @return self Fluid interface
*/
public function setYMax($yMax)
{
$this->yUnit->setMax($yMax);
return $this;
}
/**
* Transform all coordinates of the given dataset to coordinates that fit the graph's coordinate system
*
* @param array $dataSet The absolute coordinates as provided in the draw call
*
* @return array A graph relative representation of the given coordinates
*/
public function transform(array &$dataSet)
{
$result = array();
foreach ($dataSet as &$points) {
$result[] = array(
$this->xUnit->transform($points[0]),
100 - $this->yUnit->transform($points[1])
);
}
return $result;
}
/**
* Create an AxisUnit that can be used in the axis to represent timestamps
*
* @return CalendarUnit
*/
public static function calendarUnit()
{
return new CalendarUnit();
}
/**
* Create an AxisUnit that can be used in the axis to represent a dataset as equally distributed
* ticks
*
* @param int $ticks
* @return LinearUnit
*/
public static function linearUnit($ticks = 10)
{
return new LinearUnit($ticks);
}
/**
* Return the SVG representation of this object
*
* @param RenderContext $ctx The context to use for calculations
*
* @return DOMElement
* @see Drawable::toSvg
*/
public function toSvg(RenderContext $ctx)
{
$group = $ctx->getDocument()->createElement('g');
$this->renderHorizontalAxis($ctx, $group);
$this->renderVerticalAxis($ctx, $group);
return $group;
}
}