summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Chart/Axis.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Chart/Axis.php')
-rw-r--r--library/Icinga/Chart/Axis.php485
1 files changed, 485 insertions, 0 deletions
diff --git a/library/Icinga/Chart/Axis.php b/library/Icinga/Chart/Axis.php
new file mode 100644
index 0000000..1639939
--- /dev/null
+++ b/library/Icinga/Chart/Axis.php
@@ -0,0 +1,485 @@
+<?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;
+ } elseif ($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;
+ }
+}