icingaweb2/library/Icinga/Chart/Axis.php

487 lines
14 KiB
PHP

<?php
/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
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\Render\Rotator;
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 its 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;
/**
* The minimum amount of units each step must take up
*
* @var int
*/
public $minUnitsPerStep = 80;
/**
* The minimum amount of units each tick must take up
*
* @var int
*/
public $minUnitsPerTick = 15;
/**
* If the displayed labels should be aligned horizontally or diagonally
*/
protected $labelRotationStyle = self::LABEL_ROTATE_HORIZONTAL;
/**
* 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 $this 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 $this 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)
{
$steps = $this->ticksPerX($this->xUnit->getTicks(), $ctx->getNrOfUnitsX(), $this->minUnitsPerStep);
$ticks = $this->ticksPerX($this->xUnit->getTicks(), $ctx->getNrOfUnitsX(), $this->minUnitsPerTick);
// Steps should always be ticks
if ($ticks !== $steps) {
$steps = $ticks * 5;
}
// Check whether there is enough room for regular labels
$labelRotationStyle = $this->labelRotationStyle;
if ($this->labelsOversized($this->xUnit, 6)) {
$labelRotationStyle = self::LABEL_ROTATE_DIAGONAL;
}
/*
$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;
$i = 0;
foreach ($this->xUnit as $label => $pos) {
if ($i % $ticks === 0) {
/*
$tick = new Line($pos, 100, $pos, 101);
$group->appendChild($tick->toSvg($ctx));
*/
}
if ($i % $steps === 0) {
if ($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;
}
}
$labelField = new Text($pos + 0.5, ($this->xLabel ? 107 : 105) + $shift, $label);
if ($labelRotationStyle === self::LABEL_ROTATE_HORIZONTAL) {
$labelField->setAlignment(Text::ALIGN_MIDDLE)
->setFontSize('2.5em');
} else {
$labelField->setFontSize('2.5em');
}
if ($labelRotationStyle === self::LABEL_ROTATE_DIAGONAL) {
$labelField = new Rotator($labelField, 45);
}
$labelField = $labelField->toSvg($ctx);
$group->appendChild($labelField);
if ($this->drawYGrid) {
$bgLine = new Line($pos, 0, $pos, 100);
$bgLine->setStrokeWidth(0.5)
->setStrokeColor('#BFBFBF');
$group->appendChild($bgLine->toSvg($ctx));
}
$lastLabelEnd = $pos + strlen($label) * 1.2;
}
$i++;
}
}
/**
* 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)
{
$steps = $this->ticksPerX($this->yUnit->getTicks(), $ctx->getNrOfUnitsY(), $this->minUnitsPerStep);
$ticks = $this->ticksPerX($this->yUnit->getTicks(), $ctx->getNrOfUnitsY(), $this->minUnitsPerTick);
// Steps should always be ticks
if ($ticks !== $steps) {
$steps = $ticks * 5;
}
/*
$line = new Line(0, 0, 0, 100);
$line->setStrokeWidth(2);
$group->appendChild($line->toSvg($ctx));
*/
$i = 0;
foreach ($this->yUnit as $label => $pos) {
$pos = 100 - $pos;
if ($i % $ticks === 0) {
// draw a tick
//$tick = new Line(0, $pos, -1, $pos);
//$group->appendChild($tick->toSvg($ctx));
}
if ($i % $steps === 0) {
// draw a step
$labelField = new Text(-0.5, $pos + 0.5, $label);
$labelField->setFontSize('2.5em')
->setAlignment(Text::ALIGN_END);
$group->appendChild($labelField->toSvg($ctx));
if ($this->drawXGrid) {
$bgLine = new Line(0, $pos, 100, $pos);
$bgLine->setStrokeWidth(0.5)
->setStrokeColor('#BFBFBF');
$group->appendChild($bgLine->toSvg($ctx));
}
}
$i++;
}
if ($this->yLabel || $this->xLabel) {
if ($this->yLabel && $this->xLabel) {
$txt = $this->yLabel . ' / ' . $this->xLabel;
} else if ($this->xLabel) {
$txt = $this->xLabel;
} else {
$txt = $this->yLabel;
}
$axisLabel = new Text(50, -3, $txt);
$axisLabel->setFontSize('2em')
->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 $this 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 $this 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 $this 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 $this 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 $this 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;
}
protected function ticksPerX($ticks, $units, $min)
{
$per = 1;
while ($per * $units / $ticks < $min) {
$per++;
}
return $per;
}
/**
* Returns whether at least one label of the given Axis
* is bigger than the given maxLength
*
* @param AxisUnit $axis The axis that contains the labels that will be checked
*
* @return boolean Whether at least one label is bigger than maxLength
*/
private function labelsOversized(AxisUnit $axis, $maxLength = 5)
{
$oversized = false;
foreach ($axis as $label => $pos) {
if (strlen($label) > $maxLength) {
$oversized = true;
}
}
return $oversized;
}
}