summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Chart
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:46:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:46:43 +0000
commit3e02d5aff85babc3ffbfcf52313f2108e313aa23 (patch)
treeb01f3923360c20a6a504aff42d45670c58af3ec5 /library/Icinga/Chart
parentInitial commit. (diff)
downloadicingaweb2-3e02d5aff85babc3ffbfcf52313f2108e313aa23.tar.xz
icingaweb2-3e02d5aff85babc3ffbfcf52313f2108e313aa23.zip
Adding upstream version 2.12.1.upstream/2.12.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Icinga/Chart')
-rw-r--r--library/Icinga/Chart/Axis.php485
-rw-r--r--library/Icinga/Chart/Chart.php162
-rw-r--r--library/Icinga/Chart/Donut.php465
-rw-r--r--library/Icinga/Chart/Format.php21
-rw-r--r--library/Icinga/Chart/Graph/BarGraph.php163
-rw-r--r--library/Icinga/Chart/Graph/LineGraph.php202
-rw-r--r--library/Icinga/Chart/Graph/StackedGraph.php88
-rw-r--r--library/Icinga/Chart/Graph/Tooltip.php143
-rw-r--r--library/Icinga/Chart/GridChart.php446
-rw-r--r--library/Icinga/Chart/Inline/Inline.php96
-rw-r--r--library/Icinga/Chart/Inline/PieChart.php41
-rw-r--r--library/Icinga/Chart/Legend.php102
-rw-r--r--library/Icinga/Chart/Palette.php65
-rw-r--r--library/Icinga/Chart/PieChart.php306
-rw-r--r--library/Icinga/Chart/Primitive/Animatable.php43
-rw-r--r--library/Icinga/Chart/Primitive/Animation.php87
-rw-r--r--library/Icinga/Chart/Primitive/Canvas.php140
-rw-r--r--library/Icinga/Chart/Primitive/Circle.php84
-rw-r--r--library/Icinga/Chart/Primitive/Drawable.php22
-rw-r--r--library/Icinga/Chart/Primitive/Line.php103
-rw-r--r--library/Icinga/Chart/Primitive/Path.php187
-rw-r--r--library/Icinga/Chart/Primitive/PieSlice.php307
-rw-r--r--library/Icinga/Chart/Primitive/RawElement.php43
-rw-r--r--library/Icinga/Chart/Primitive/Rect.php119
-rw-r--r--library/Icinga/Chart/Primitive/Styleable.php161
-rw-r--r--library/Icinga/Chart/Primitive/Text.php184
-rw-r--r--library/Icinga/Chart/Render/LayoutBox.php200
-rw-r--r--library/Icinga/Chart/Render/RenderContext.php225
-rw-r--r--library/Icinga/Chart/Render/Rotator.php80
-rw-r--r--library/Icinga/Chart/SVGRenderer.php331
-rw-r--r--library/Icinga/Chart/Unit/AxisUnit.php56
-rw-r--r--library/Icinga/Chart/Unit/CalendarUnit.php167
-rw-r--r--library/Icinga/Chart/Unit/LinearUnit.php227
-rw-r--r--library/Icinga/Chart/Unit/LogarithmicUnit.php263
-rw-r--r--library/Icinga/Chart/Unit/StaticAxis.php130
35 files changed, 5944 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;
+ }
+}
diff --git a/library/Icinga/Chart/Chart.php b/library/Icinga/Chart/Chart.php
new file mode 100644
index 0000000..eaf69d1
--- /dev/null
+++ b/library/Icinga/Chart/Chart.php
@@ -0,0 +1,162 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use Imagick;
+use Icinga\Chart\Legend;
+use Icinga\Chart\Palette;
+use Icinga\Chart\Primitive\Drawable;
+use Icinga\Chart\SVGRenderer;
+use Icinga\Exception\IcingaException;
+
+/**
+ * Base class for charts, extended by all other Chart classes.
+ */
+abstract class Chart implements Drawable
+{
+ protected $align = false;
+
+ /**
+ * SVG renderer that handles
+ *
+ * @var SVGRenderer
+ */
+ protected $renderer;
+
+ /**
+ * Legend to use for this chart
+ *
+ * @var Legend
+ */
+ protected $legend;
+
+ /**
+ * The style-palette for this chart
+ *
+ * @var Palette
+ */
+ protected $palette;
+
+ /**
+ * The title of this chart, used for providing accessibility features
+ *
+ * @var string
+ */
+ public $title;
+
+ /**
+ * The description for this chart, mandatory for providing accessibility features
+ *
+ * @var string
+ */
+ public $description;
+
+ /**
+ * Create a new chart object and create internal objects
+ *
+ * If you want to extend this class use the init() method as an extension point,
+ * as this will be called at the end of the construct call
+ */
+ public function __construct()
+ {
+ $this->legend = new Legend();
+ $this->palette = new Palette();
+ $this->init();
+ }
+
+ /**
+ * Extension point for subclasses, called on __construct
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Extension point for implementing rendering logic
+ *
+ * This method is called after data validation, but before toSvg is called
+ */
+ protected function build()
+ {
+ }
+
+ /**
+ * 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
+ */
+ abstract public function isValidDataFormat();
+
+
+ /**
+ * Disable the legend for this chart
+ */
+ public function disableLegend()
+ {
+ $this->legend = null;
+ }
+
+ /**
+ * Render this graph and return the created SVG
+ *
+ * @return string The SVG created by the SvgRenderer
+ *
+ * @throws IcingaException Thrown wen the dataset is not valid for this graph
+ * @see SVGRenderer::render
+ */
+ public function render()
+ {
+ if (!$this->isValidDataFormat()) {
+ throw new IcingaException('Dataset for graph doesn\'t have the proper structure');
+ }
+ $this->build();
+ if ($this->align) {
+ $this->renderer->preserveAspectRatio();
+ $this->renderer->setXAspectRatioAlignment(SVGRenderer::X_ASPECT_RATIO_MIN);
+ $this->renderer->setYAspectRatioAlignment(SVGRenderer::Y_ASPECT_RATIO_MIN);
+ }
+
+ $this->renderer->setAriaDescription($this->description);
+ $this->renderer->setAriaTitle($this->title);
+ $this->renderer->getCanvas()->setAriaRole('presentation');
+
+ $this->renderer->getCanvas()->addElement($this);
+ return $this->renderer->render();
+ }
+
+ /**
+ * Return this graph rendered as PNG
+ *
+ * @param int $width The width of the PNG in pixel
+ * @param int $height The height of the PNG in pixel
+ *
+ * @return string A PNG binary string
+ *
+ * @throws IcingaException In case ImageMagick is not available
+ */
+ public function toPng($width, $height)
+ {
+ if (! class_exists('Imagick')) {
+ throw new IcingaException('Cannot render PNGs without ImageMagick');
+ }
+
+ $image = new Imagick();
+ $image->readImageBlob($this->render());
+ $image->setImageFormat('png24');
+ $image->resizeImage($width, $height, imagick::FILTER_LANCZOS, 1);
+ return $image;
+ }
+
+ /**
+ * Align the chart to the top left corner instead of centering it
+ *
+ * @param bool $align
+ */
+ public function alignTopLeft($align = true)
+ {
+ $this->align = $align;
+ }
+}
diff --git a/library/Icinga/Chart/Donut.php b/library/Icinga/Chart/Donut.php
new file mode 100644
index 0000000..9d2a2a8
--- /dev/null
+++ b/library/Icinga/Chart/Donut.php
@@ -0,0 +1,465 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use Icinga\Web\Url;
+
+/** Donut chart implementation */
+class Donut
+{
+ /**
+ * Big label in the middle of the donut, color is critical (red)
+ *
+ * @var string
+ */
+ protected $labelBig;
+
+ /**
+ * Url behind the big label
+ *
+ * @var Url
+ */
+ protected $labelBigUrl;
+
+ /**
+ * The state the big label shall indicate
+ *
+ * @var string|null
+ */
+ protected $labelBigState = 'critical';
+
+ /**
+ * Small label in the lower part of the donuts hole
+ *
+ * @var string
+ */
+ protected $labelSmall;
+
+ /**
+ * Thickness of the donut ring
+ *
+ * @var int
+ */
+ protected $thickness = 6;
+
+ /**
+ * Radius based of 100 to simplify the calculations
+ *
+ * 100 / (2 * M_PI)
+ *
+ * @var float
+ */
+ protected $radius = 15.9154943092;
+
+ /**
+ * Color of the hole in the donut
+ *
+ * Transparent by default so it can be placed anywhere with ease
+ *
+ * @var string
+ */
+ protected $centerColor = 'transparent';
+
+ /**
+ * The different colored parts that represent the data
+ *
+ * @var array
+ */
+ protected $slices = array();
+
+ /**
+ * The total amount of data units
+ *
+ * @var int
+ */
+ protected $count = 0;
+
+ /**
+ * Adds a colored part that represent the data
+ *
+ * @param integer $data Units of data
+ * @param array $attributes HTML attributes for this slice. (For example ['class' => 'slice-state-ok'])
+ *
+ * @return $this
+ */
+ public function addSlice($data, $attributes = array())
+ {
+ $this->slices[] = array($data, $attributes);
+
+ $this->count += $data;
+
+ return $this;
+ }
+
+ /**
+ * Set the thickness for this Donut
+ *
+ * @param integer $thickness
+ *
+ * @return $this
+ */
+ public function setThickness($thickness)
+ {
+ $this->thickness = $thickness;
+
+ return $this;
+ }
+ /**
+ * Get the thickness for this Donut
+ *
+ * @return integer
+ */
+ public function getThickness()
+ {
+ return $this->thickness;
+ }
+
+ /**
+ * Set the center color for this Donut
+ *
+ * @param string $centerColor
+ *
+ * @return $this
+ */
+ public function setCenterColor($centerColor)
+ {
+ $this->centerColor = $centerColor;
+
+ return $this;
+ }
+
+ /**
+ * Get the center color for this Donut
+ *
+ * @return string
+ */
+ public function getCenterColor()
+ {
+ return $this->centerColor;
+ }
+
+ /**
+ * Set the text of the big label
+ *
+ * @param string $labelBig
+ *
+ * @return $this
+ */
+ public function setLabelBig($labelBig)
+ {
+ $this->labelBig = $labelBig;
+
+ return $this;
+ }
+
+ /**
+ * Get the text of the big label
+ *
+ * @return string
+ */
+ public function getLabelBig()
+ {
+ return $this->labelBig;
+ }
+
+ /**
+ * Set the url behind the big label
+ *
+ * @param Url $labelBigUrl
+ *
+ * @return $this
+ */
+ public function setLabelBigUrl($labelBigUrl)
+ {
+ $this->labelBigUrl = $labelBigUrl;
+
+ return $this;
+ }
+
+ /**
+ * Get the url behind the big label
+ *
+ * @return Url
+ */
+ public function getLabelBigUrl()
+ {
+ return $this->labelBigUrl;
+ }
+
+ /**
+ * Get whether the big label shall be eye-catching
+ *
+ * @return bool
+ */
+ public function getLabelBigEyeCatching()
+ {
+ return $this->labelBigState !== null;
+ }
+
+ /**
+ * Set whether the big label shall be eye-catching
+ *
+ * @param bool $labelBigEyeCatching
+ *
+ * @return $this
+ */
+ public function setLabelBigEyeCatching($labelBigEyeCatching = true)
+ {
+ $this->labelBigState = $labelBigEyeCatching ? 'critical' : null;
+
+ return $this;
+ }
+
+ /**
+ * Get the state the big label shall indicate
+ *
+ * @return string|null
+ */
+ public function getLabelBigState()
+ {
+ return $this->labelBigState;
+ }
+
+ /**
+ * Set the state the big label shall indicate
+ *
+ * @param string|null $labelBigState
+ *
+ * @return $this
+ */
+ public function setLabelBigState($labelBigState)
+ {
+ $this->labelBigState = $labelBigState;
+
+ return $this;
+ }
+
+ /**
+ * Set the text of the small label
+ *
+ * @param string $labelSmall
+ *
+ * @return $this
+ */
+ public function setLabelSmall($labelSmall)
+ {
+ $this->labelSmall = $labelSmall;
+
+ return $this;
+ }
+
+ /**
+ * Get the text of the small label
+ *
+ * @return string
+ */
+ public function getLabelSmall()
+ {
+ return $this->labelSmall;
+ }
+
+ /**
+ * Put together all slices of this Donut
+ *
+ * @return array $svg
+ */
+ protected function assemble()
+ {
+ // svg tag containing the ring
+ $svg = array(
+ 'tag' => 'svg',
+ 'attributes' => array(
+ 'xmlns' => 'http://www.w3.org/2000/svg',
+ 'viewbox' => '0 0 40 40',
+ 'class' => 'donut-graph'
+ ),
+ 'content' => array()
+ );
+
+ // Donut hole
+ $svg['content'][] = array(
+ 'tag' => 'circle',
+ 'attributes' => array(
+ 'cx' => 20,
+ 'cy' => 20,
+ 'r' => sprintf('%F', $this->radius),
+ 'fill' => $this->getCenterColor()
+ )
+ );
+
+ // When there is no data show gray circle
+ $svg['content'][] = array(
+ 'tag' => 'circle',
+ 'attributes' => array(
+ 'aria-hidden' => true,
+ 'cx' => 20,
+ 'cy' => 20,
+ 'r' => sprintf('%F', $this->radius),
+ 'fill' => $this->getCenterColor(),
+ 'stroke-width' => $this->getThickness(),
+ 'class' => 'slice-state-not-checked'
+ )
+ );
+
+ $slices = $this->slices;
+
+ if ($this->count !== 0) {
+ array_walk($slices, function (&$slice) {
+ $slice[0] = round(100 / $this->count * $slice[0], 2);
+ });
+ }
+
+ // on 0 the donut would start at "3 o'clock" and the offset shifts counterclockwise
+ $offset = 25;
+
+ foreach ($slices as $slice) {
+ $svg['content'][] = array(
+ 'tag' => 'circle',
+ 'attributes' => $slice[1] + array(
+ 'cx' => 20,
+ 'cy' => 20,
+ 'r' => sprintf('%F', $this->radius),
+ 'fill' => 'transparent',
+ 'stroke-width' => $this->getThickness(),
+ 'stroke-dasharray' => sprintf('%F', $slice[0])
+ . ' '
+ . sprintf('%F', (99.9 - $slice[0])), // 99.9 prevents gaps (slight overlap)
+ 'stroke-dashoffset' => sprintf('%F', $offset)
+ )
+ );
+ // negative values shift in the clockwise direction
+ $offset -= $slice[0];
+ }
+
+ $result = array(
+ 'tag' => 'div',
+ 'content' => array($svg)
+ );
+
+ $labelBig = (string) $this->getLabelBig();
+ $labelSmall = (string) $this->getLabelSmall();
+
+ if ($labelBig !== '' || $labelSmall !== '') {
+ $labels = array(
+ 'tag' => 'div',
+ 'attributes' => array(
+ 'class' => 'donut-label'
+ ),
+ 'content' => array()
+ );
+
+ if ($labelBig !== '') {
+ $labels['content'][] =
+ array(
+ 'tag' => 'a',
+ 'attributes' => array(
+ 'aria-label' => $labelBig . ' ' . $labelSmall,
+ 'href' => $this->getLabelBigUrl() ? $this->getLabelBigUrl()->getAbsoluteUrl() : null,
+ 'class' => $this->labelBigState === null
+ ? 'donut-label-big'
+ : 'donut-label-big state-' . $this->labelBigState
+ ),
+ 'content' => $this->shortenLabel($labelBig)
+ );
+ }
+
+ if ($labelSmall !== '') {
+ $labels['content'][] = array(
+ 'tag' => 'p',
+ 'attributes' => array(
+ 'class' => 'donut-label-small',
+ 'x' => '50%',
+ 'y' => '50%'
+ ),
+ 'content' => $labelSmall
+ );
+ }
+
+ $result['content'][] = $labels;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Shorten the label to 3 digits if it is numeric
+ *
+ * 10 => 10 ... 1111 => ~1k ... 1888 => ~2k
+ *
+ * @param int|string $label
+ *
+ * @return string
+ */
+ protected function shortenLabel($label)
+ {
+ if (is_numeric($label) && strlen($label) > 3) {
+ return round($label, -3)/1000 . 'k';
+ }
+
+ return $label;
+ }
+
+ protected function encode($content)
+ {
+ return htmlspecialchars($content, ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8', true);
+ }
+
+ protected function renderAttributes(array $attributes)
+ {
+ $html = array();
+
+ foreach ($attributes as $name => $value) {
+ if ($value === null) {
+ continue;
+ }
+
+ if (is_bool($value) && $value) {
+ $html[] = $name;
+ continue;
+ }
+
+ if (is_array($value)) {
+ $value = implode(' ', $value);
+ }
+
+ $html[] = "$name=\"" . $this->encode($value) . '"';
+ }
+
+ return implode(' ', $html);
+ }
+
+ protected function renderContent(array $element)
+ {
+ $tag = $element['tag'];
+ $attributes = isset($element['attributes']) ? $element['attributes'] : array();
+ $content = isset($element['content']) ? $element['content'] : null;
+
+ $html = array(
+ // rtrim because attributes may be empty
+ rtrim("<$tag " . $this->renderAttributes($attributes))
+ . ">"
+ );
+
+ if ($content !== null) {
+ if (is_array($content)) {
+ foreach ($content as $child) {
+ $html[] = is_array($child) ? $this->renderContent($child) : $this->encode($child);
+ }
+ } else {
+ $html[] = $this->encode($content);
+ }
+ }
+
+ $html[] = "</$tag>";
+
+ return implode("\n", $html);
+ }
+
+ public function render()
+ {
+ $svg = $this->assemble();
+
+ return $this->renderContent($svg);
+ }
+}
diff --git a/library/Icinga/Chart/Format.php b/library/Icinga/Chart/Format.php
new file mode 100644
index 0000000..9e6c4db
--- /dev/null
+++ b/library/Icinga/Chart/Format.php
@@ -0,0 +1,21 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+class Format
+{
+ /**
+ * Format a number into a number-string as defined by the SVG-Standard
+ *
+ * @see http://www.w3.org/TR/SVG/types.html#DataTypeNumber
+ *
+ * @param $number
+ *
+ * @return string
+ */
+ public static function formatSVGNumber($number)
+ {
+ return number_format($number, 1, '.', '');
+ }
+}
diff --git a/library/Icinga/Chart/Graph/BarGraph.php b/library/Icinga/Chart/Graph/BarGraph.php
new file mode 100644
index 0000000..be142bf
--- /dev/null
+++ b/library/Icinga/Chart/Graph/BarGraph.php
@@ -0,0 +1,163 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Graph;
+
+use DOMElement;
+use Icinga\Chart\Primitive\Animation;
+use Icinga\Chart\Primitive\Drawable;
+use Icinga\Chart\Primitive\Rect;
+use Icinga\Chart\Primitive\Styleable;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Bar graph implementation
+ */
+class BarGraph extends Styleable implements Drawable
+{
+ /**
+ * The dataset order
+ *
+ * @var int
+ */
+ private $order = 0;
+
+ /**
+ * The width of the bars.
+ *
+ * @var int
+ */
+ private $barWidth = 3;
+
+ /**
+ * The dataset to use for this bar graph
+ *
+ * @var array
+ */
+ private $dataSet;
+
+ /**
+ * The tooltips
+ *
+ * @var
+ */
+ private $tooltips;
+
+ /**
+ * All graphs
+ *
+ * @var
+ */
+ private $graphs;
+
+ /**
+ * Create a new BarGraph with the given dataset
+ *
+ * @param array $dataSet An array of data points
+ * @param int $order The graph number displayed by this BarGraph
+ * @param array $tooltips The tooltips to display for each value
+ */
+ public function __construct(
+ array $dataSet,
+ array &$graphs,
+ $order,
+ array $tooltips = null
+ ) {
+ $this->order = $order;
+ $this->dataSet = $dataSet;
+
+ $this->tooltips = $tooltips;
+ $ts = [];
+ foreach ($this->tooltips as $value) {
+ $ts[] = $value;
+ }
+ $this->tooltips = $ts;
+
+ $this->graphs = $graphs;
+ }
+
+ /**
+ * Apply configuration styles from the $cfg
+ *
+ * @param array $cfg The configuration as given in the drawBars call
+ */
+ public function setStyleFromConfig(array $cfg)
+ {
+ foreach ($cfg as $elem => $value) {
+ if ($elem === 'color') {
+ $this->setFill($value);
+ } elseif ($elem === 'width') {
+ $this->setStrokeWidth($value);
+ }
+ }
+ }
+
+ /**
+ * Draw a single rectangle
+ *
+ * @param array $point The
+ * @param string $fill The fill color to use
+ * @param $strokeWidth
+ * @param ?int $index
+ *
+ * @return Rect
+ */
+ private function drawSingleBar($point, $fill, $strokeWidth, $index = null)
+ {
+ $rect = new Rect($point[0] - ($this->barWidth / 2), $point[1], $this->barWidth, 100 - $point[1]);
+ $rect->setFill($fill);
+ $rect->setStrokeWidth($strokeWidth);
+ $rect->setStrokeColor('black');
+ if (isset($index)) {
+ $rect->setAttribute('data-icinga-graph-index', $index);
+ }
+ $rect->setAttribute('data-icinga-graph-type', 'bar');
+ $rect->setAdditionalStyle(['clip-path' => 'url(#clip)']);
+ return $rect;
+ }
+
+ /**
+ * Render this BarChart
+ *
+ * @param RenderContext $ctx The rendering context to use for drawing
+ *
+ * @return DOMElement $dom Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ $group = $doc->createElement('g');
+ $idx = 0;
+
+ if (count($this->dataSet) > 15) {
+ $this->barWidth = 2;
+ }
+ if (count($this->dataSet) > 25) {
+ $this->barWidth = 1;
+ }
+
+ foreach ($this->dataSet as $x => $point) {
+ // add white background bar, to prevent other bars from altering transparency effects
+ $bar = $this->drawSingleBar($point, 'white', $this->strokeWidth, $idx++)->toSvg($ctx);
+ $group->appendChild($bar);
+
+ // draw actual bar
+ $bar = $this->drawSingleBar($point, $this->fill, $this->strokeWidth)->toSvg($ctx);
+ if (isset($this->tooltips[$x])) {
+ $data = array(
+ 'label' => isset($this->graphs[$this->order]['label']) ?
+ strtolower($this->graphs[$this->order]['label']) : '',
+ 'color' => isset($this->graphs[$this->order]['color']) ?
+ strtolower($this->graphs[$this->order]['color']) : '#fff'
+ );
+ $format = isset($this->graphs[$this->order]['tooltip'])
+ ? $this->graphs[$this->order]['tooltip'] : null;
+ $title = $ctx->getDocument()->createElement('title');
+ $title->textContent = $this->tooltips[$x]->renderNoHtml($this->order, $data, $format);
+ $bar->appendChild($title);
+ }
+ $group->appendChild($bar);
+ }
+ return $group;
+ }
+}
diff --git a/library/Icinga/Chart/Graph/LineGraph.php b/library/Icinga/Chart/Graph/LineGraph.php
new file mode 100644
index 0000000..21f930a
--- /dev/null
+++ b/library/Icinga/Chart/Graph/LineGraph.php
@@ -0,0 +1,202 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Graph;
+
+use DOMElement;
+use Icinga\Chart\Primitive\Drawable;
+use Icinga\Chart\Primitive\Path;
+use Icinga\Chart\Primitive\Circle;
+use Icinga\Chart\Primitive\Styleable;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * LineGraph implementation for drawing a set of datapoints as
+ * a connected path
+ */
+class LineGraph extends Styleable implements Drawable
+{
+ /**
+ * The dataset to use
+ *
+ * @var array
+ */
+ private $dataset;
+
+ /**
+ * True to show dots for each datapoint
+ *
+ * @var bool
+ */
+ private $showDataPoints = false;
+
+ /**
+ * When true, the path will be discrete, i.e. showing hard steps instead of a direct line
+ *
+ * @var bool
+ */
+ private $isDiscrete = false;
+
+ /**
+ * The tooltips
+ *
+ * @var
+ */
+ private $tooltips;
+
+ /** @var array */
+ private $graphs;
+
+ /** @var int */
+ private $order;
+
+ /**
+ * The default stroke width
+ * @var int
+ */
+ public $strokeWidth = 5;
+
+ /**
+ * The size of the displayed dots
+ *
+ * @var int
+ */
+ public $dotWith = 0;
+
+ /**
+ * Create a new LineGraph displaying the given dataset
+ *
+ * @param array $dataset An array of [x, y] arrays to display
+ */
+ public function __construct(
+ array $dataset,
+ array &$graphs,
+ $order,
+ array $tooltips = null
+ ) {
+ usort($dataset, array($this, 'sortByX'));
+ $this->dataset = $dataset;
+ $this->graphs = $graphs;
+
+ $this->tooltips = $tooltips;
+ $ts = [];
+ foreach ($this->tooltips as $value) {
+ $ts[] = $value;
+ }
+ $this->tooltips = $ts;
+ $this->order = $order;
+ }
+
+ /**
+ * Set datapoints to be emphased via dots
+ *
+ * @param bool $bool True to enable datapoints, otherwise false
+ */
+ public function setShowDataPoints($bool)
+ {
+ $this->showDataPoints = $bool;
+ }
+
+ /**
+ * Sort the daset by the xaxis
+ *
+ * @param array $v1
+ * @param array $v2
+ * @return int
+ */
+ private function sortByX(array $v1, array $v2)
+ {
+ if ($v1[0] === $v2[0]) {
+ return 0;
+ }
+ return ($v1[0] < $v2[0]) ? -1 : 1;
+ }
+
+ /**
+ * Configure this style
+ *
+ * @param array $cfg The configuration as given in the drawLine call
+ */
+ public function setStyleFromConfig(array $cfg)
+ {
+ $fill = false;
+ foreach ($cfg as $elem => $value) {
+ if ($elem === 'color') {
+ $this->setStrokeColor($value);
+ } elseif ($elem === 'width') {
+ $this->setStrokeWidth($value);
+ } elseif ($elem === 'showPoints') {
+ $this->setShowDataPoints($value);
+ } elseif ($elem === 'fill') {
+ $fill = $value;
+ } elseif ($elem === 'discrete') {
+ $this->isDiscrete = true;
+ }
+ }
+ if ($fill) {
+ $this->setFill($this->strokeColor);
+ $this->setStrokeColor('black');
+ }
+ }
+
+ /**
+ * Render this BarChart
+ *
+ * @param RenderContext $ctx The rendering context to use for drawing
+ *
+ * @return DOMElement $dom Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $path = new Path($this->dataset);
+ if ($this->isDiscrete) {
+ $path->setDiscrete(true);
+ }
+ $path->setStrokeColor($this->strokeColor);
+ $path->setStrokeWidth($this->strokeWidth);
+
+ $path->setAttribute('data-icinga-graph-type', 'line');
+ if ($this->fill !== 'none') {
+ $firstX = $this->dataset[0][0];
+ $lastX = $this->dataset[count($this->dataset)-1][0];
+ $path->prepend(array($firstX, 100))
+ ->append(array($lastX, 100));
+ $path->setFill($this->fill);
+ }
+
+ $path->setAdditionalStyle(['clip-path' => 'url(#clip)']);
+ $path->setId($this->id ?? uniqid('line-graph-'));
+ $group = $path->toSvg($ctx);
+
+ foreach ($this->dataset as $x => $point) {
+ if ($this->showDataPoints === true) {
+ $dot = new Circle($point[0], $point[1], $this->dotWith);
+ $dot->setFill($this->strokeColor);
+ $group->appendChild($dot->toSvg($ctx));
+ }
+
+ // Draw invisible circle for tooltip hovering
+ if (isset($this->tooltips[$x])) {
+ $invisible = new Circle($point[0], $point[1], 20);
+ $invisible->setFill($this->strokeColor);
+ $invisible->setAdditionalStyle(['opacity' => '0.0']);
+ $data = array(
+ 'label' => isset($this->graphs[$this->order]['label']) ?
+ strtolower($this->graphs[$this->order]['label']) : '',
+ 'color' => isset($this->graphs[$this->order]['color']) ?
+ strtolower($this->graphs[$this->order]['color']) : '#fff'
+ );
+ $format = isset($this->graphs[$this->order]['tooltip'])
+ ? $this->graphs[$this->order]['tooltip'] : null;
+ $title = $ctx->getDocument()->createElement('title');
+ $title->textContent = $this->tooltips[$x]->renderNoHtml($this->order, $data, $format);
+ $invisibleRendered = $invisible->toSvg($ctx);
+ $invisibleRendered->appendChild($title);
+ $group->appendChild($invisibleRendered);
+ }
+ }
+
+ return $group;
+ }
+}
diff --git a/library/Icinga/Chart/Graph/StackedGraph.php b/library/Icinga/Chart/Graph/StackedGraph.php
new file mode 100644
index 0000000..49801a9
--- /dev/null
+++ b/library/Icinga/Chart/Graph/StackedGraph.php
@@ -0,0 +1,88 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Graph;
+
+use DOMElement;
+use Icinga\Chart\Primitive\Drawable;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Graph implementation that stacks several graphs and displays them in a cumulative way
+ */
+class StackedGraph implements Drawable
+{
+ /**
+ * All graphs displayed in this stackedgraph
+ *
+ * @var array
+ */
+ private $stack = array();
+
+ /**
+ * An associative array containing x points as the key and an array of y values as the value
+ *
+ * @var array
+ */
+ private $points = array();
+
+ /**
+ * Add a graph to this stack and aggregate the values on the fly
+ *
+ * This modifies the dataset as a side effect
+ *
+ * @param array $subGraph
+ */
+ public function addGraph(array &$subGraph)
+ {
+ foreach ($subGraph['data'] as &$point) {
+ $x = $point[0];
+ if (!isset($this->points[$x])) {
+ $this->points[$x] = 0;
+ }
+ // store old y-value for displaying the actual (non-aggregated)
+ // value in the tooltip
+ $point[2] = $point[1];
+
+ $this->points[$x] += $point[1];
+ $point[1] = $this->points[$x];
+ }
+ }
+
+ /**
+ * Add a graph to the stack
+ *
+ * @param $graph
+ */
+ public function addToStack($graph)
+ {
+ $this->stack[] = $graph;
+ }
+
+ /**
+ * Empty the stack
+ *
+ * @return bool
+ */
+ public function stackEmpty()
+ {
+ return empty($this->stack);
+ }
+
+ /**
+ * Render this stack in the correct order
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG representation of this graph
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $group = $ctx->getDocument()->createElement('g');
+ $renderOrder = array_reverse($this->stack);
+ foreach ($renderOrder as $stackElem) {
+ $group->appendChild($stackElem->toSvg($ctx));
+ }
+ return $group;
+ }
+}
diff --git a/library/Icinga/Chart/Graph/Tooltip.php b/library/Icinga/Chart/Graph/Tooltip.php
new file mode 100644
index 0000000..7236685
--- /dev/null
+++ b/library/Icinga/Chart/Graph/Tooltip.php
@@ -0,0 +1,143 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Graph;
+
+/**
+ * A tooltip that stores and aggregates information about displayed data
+ * points of a graph and replaces them in a format string to render the description
+ * for specific data points of the graph.
+ *
+ * When render() is called, placeholders for the keys for each data entry will be replaced by
+ * the current value of this data set and the formatted string will be returned.
+ * The content of the replaced keys can change for each data set and depends on how the data
+ * is passed to this class. There are several types of properties:
+ *
+ * <ul>
+ * <li>Global properties</li>: Key-value pairs that stay the same every time render is called, and are
+ * passed to an instance in the constructor.
+ * <li>Aggregated properties</li>: Global properties that are created automatically from
+ * all attached data points.
+ * <li>Local properties</li>: Key-value pairs that only apply to a single data point and
+ * are passed to the render-function.
+ * </ul>
+ */
+class Tooltip
+{
+ /**
+ * The default format string used
+ * when no other format is specified
+ *
+ * @var string
+ */
+ private $defaultFormat;
+
+ /**
+ * All aggregated points
+ *
+ * @var array
+ */
+ private $points = array();
+
+ /**
+ * Contains all static replacements
+ *
+ * @var array
+ */
+ private $data = array(
+ 'sum' => 0
+ );
+
+ /**
+ * Used to format the displayed tooltip.
+ *
+ * @var string
+ */
+ protected $tooltipFormat;
+
+ /**
+ * Create a new tooltip with the specified default format string
+ *
+ * Allows you to set the global data for this tooltip, that is displayed every
+ * time render is called.
+ *
+ * @param array $data Map of global properties
+ * @param string $format The default format string
+ */
+ public function __construct(
+ $data = array(),
+ $format = '<b>{title}</b>: {value} {label}'
+ ) {
+ $this->data = array_merge($this->data, $data);
+ $this->defaultFormat = $format;
+ }
+
+ /**
+ * Add a single data point to update the aggregated properties for this tooltip
+ *
+ * @param $point array Contains the (x,y) values of the data set
+ */
+ public function addDataPoint($point)
+ {
+ // set x-value
+ if (!isset($this->data['title'])) {
+ $this->data['title'] = $point[0];
+ }
+
+ // aggregate y-values
+ $y = (int)$point[1];
+ if (isset($point[2])) {
+ // load original value in case value already aggregated
+ $y = (int)$point[2];
+ }
+
+ if (!isset($this->data['min']) || $this->data['min'] > $y) {
+ $this->data['min'] = $y;
+ }
+ if (!isset($this->data['max']) || $this->data['max'] < $y) {
+ $this->data['max'] = $y;
+ }
+ $this->data['sum'] += $y;
+ $this->points[] = $y;
+ }
+
+ /**
+ * Format the tooltip for a certain data point
+ *
+ * @param array $order Which data set to render
+ * @param array $data The local data for this tooltip
+ * @param string $format Use a custom format string for this data set
+ *
+ * @return mixed|string The tooltip value
+ */
+ public function render($order, $data = array(), $format = null)
+ {
+ if (isset($format)) {
+ $str = $format;
+ } else {
+ $str = $this->defaultFormat;
+ }
+ $data['value'] = $this->points[$order];
+ foreach (array_merge($this->data, $data) as $key => $value) {
+ $str = str_replace('{' . $key . '}', $value, $str);
+ }
+ return $str;
+ }
+
+ /**
+ * Format the tooltip for a certain data point but remove all
+ * occurring html tags
+ *
+ * This is useful for rendering clean tooltips on client without JavaScript
+ *
+ * @param array $order Which data set to render
+ * @param array $data The local data for this tooltip
+ * @param string $format Use a custom format string for this data set
+ *
+ * @return mixed|string The tooltip value, without any HTML tags
+ */
+ public function renderNoHtml($order, $data, $format)
+ {
+ return strip_tags($this->render($order, $data, $format));
+ }
+}
diff --git a/library/Icinga/Chart/GridChart.php b/library/Icinga/Chart/GridChart.php
new file mode 100644
index 0000000..a8cfca6
--- /dev/null
+++ b/library/Icinga/Chart/GridChart.php
@@ -0,0 +1,446 @@
+<?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;
+ }
+}
diff --git a/library/Icinga/Chart/Inline/Inline.php b/library/Icinga/Chart/Inline/Inline.php
new file mode 100644
index 0000000..3acbd73
--- /dev/null
+++ b/library/Icinga/Chart/Inline/Inline.php
@@ -0,0 +1,96 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Inline;
+
+/**
+ * Class to render and inline chart directly from the request params.
+ *
+ * When rendering huge amounts of inline charts it is too expensive
+ * to bootstrap the complete application for ever single chart and
+ * we need to be able render Charts in a compact environment without
+ * the other Icinga classes.
+ *
+ * Class Inline
+ * @package Icinga\Chart\Inline
+ */
+class Inline
+{
+
+ /**
+ * The data displayed in this chart
+ *
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * The colors used to display this chart
+ *
+ * @var array
+ */
+ protected $colors = array(
+ '#00FF00', // OK
+ '#FFFF00', // Warning
+ '#FF0000', // Critical
+ '#E066FF' // Unreachable
+ );
+
+ /**
+ * The labels displayed on this chart
+ *
+ * @var array
+ */
+ protected $labels = array();
+
+ /**
+ * The height in percent
+ *
+ * @var int
+ */
+ protected $height = 100;
+
+ /**
+ * The width in percent
+ *
+ * @var int
+ */
+ protected $width = 100;
+
+ protected function sanitizeStringArray(array $arr)
+ {
+ $sanitized = array();
+ foreach ($arr as $key => $value) {
+ $sanitized[$key] = htmlspecialchars($value);
+ }
+ return $sanitized;
+ }
+
+ /**
+ * Populate the properties from the current request.
+ */
+ public function initFromRequest()
+ {
+ $this->data = explode(',', $_GET['data']);
+ foreach ($this->data as $key => $value) {
+ $this->data[$key] = (int)$value;
+ }
+ for ($i = 0; $i < count($this->data); $i++) {
+ $this->labels[] = '';
+ }
+
+ if (array_key_exists('colors', $_GET)) {
+ $this->colors = $this->sanitizeStringArray(explode(',', $_GET['colors']));
+ }
+ while (count($this->colors) < count($this->data)) {
+ $this->colors[] = '#FEFEFE';
+ }
+
+ if (array_key_exists('width', $_GET)) {
+ $this->width = (int)$_GET['width'];
+ }
+ if (array_key_exists('height', $_GET)) {
+ $this->height = (int)$_GET['height'];
+ }
+ }
+}
diff --git a/library/Icinga/Chart/Inline/PieChart.php b/library/Icinga/Chart/Inline/PieChart.php
new file mode 100644
index 0000000..de68213
--- /dev/null
+++ b/library/Icinga/Chart/Inline/PieChart.php
@@ -0,0 +1,41 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Inline;
+
+use Icinga\Chart\PieChart as PieChartRenderer;
+
+/**
+ * Draw an inline pie-chart directly from the available request parameters.
+ */
+class PieChart extends Inline
+{
+ protected function getChart()
+ {
+ $pie = new PieChartRenderer();
+ $pie->alignTopLeft();
+ $pie->disableLegend();
+ $pie->drawPie(array(
+ 'data' => $this->data, 'colors' => $this->colors, 'labels' => $this->labels
+ ));
+ return $pie;
+ }
+
+ public function toSvg($output = true)
+ {
+ if ($output) {
+ echo $this->getChart()->render();
+ } else {
+ return $this->getChart()->render();
+ }
+ }
+
+ public function toPng($output = true)
+ {
+ if ($output) {
+ echo $this->getChart()->toPng($this->width, $this->height);
+ } else {
+ return $this->getChart()->toPng($this->width, $this->height);
+ }
+ }
+}
diff --git a/library/Icinga/Chart/Legend.php b/library/Icinga/Chart/Legend.php
new file mode 100644
index 0000000..ab1c9e0
--- /dev/null
+++ b/library/Icinga/Chart/Legend.php
@@ -0,0 +1,102 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use DOMElement;
+use Icinga\Chart\Palette;
+use Icinga\Chart\Primitive\Canvas;
+use Icinga\Chart\Primitive\Drawable;
+use Icinga\Chart\Primitive\Rect;
+use Icinga\Chart\Primitive\Text;
+use Icinga\Chart\Render\LayoutBox;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Drawable for creating a Graph Legend on the bottom of a graph.
+ *
+ * Usually used by the GridChart class internally.
+ */
+class Legend implements Drawable
+{
+
+ /**
+ * Internal counter for unnamed label identifiers
+ *
+ * @var int
+ */
+ private $internalCtr = 0;
+
+ /**
+ *
+ * Content of this legend
+ *
+ * @var array
+ */
+ private $dataset = array();
+
+
+ /**
+ * Set the content to be displayed by this legend
+ *
+ * @param array $dataset An array of datasets in the form they are provided to the graphing implementation
+ */
+ public function addDataset(array $dataset)
+ {
+ if (!isset($dataset['label'])) {
+ $dataset['label'] = 'Dataset ' . (++$this->internalCtr);
+ }
+ if (!isset($dataset['color'])) {
+ return;
+ }
+ $this->dataset[$dataset['color']] = $dataset['label'];
+ }
+
+ /**
+ * Render the legend to an SVG object
+ *
+ * @param RenderContext $ctx The context to use for rendering this legend
+ *
+ * @return DOMElement The SVG representation of this legend
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $outer = new Canvas('legend', new LayoutBox(0, 0, 100, 100));
+ $outer->getLayout()->setPadding(2, 2, 2, 2);
+ $nrOfColumns = 4;
+
+ $topstep = 10 / $nrOfColumns + 2;
+
+ $top = 0;
+ $left = 0;
+ $lastLabelEndPos = -1;
+ foreach ($this->dataset as $color => $text) {
+ $leftstep = 100 / $nrOfColumns + strlen($text);
+
+ // Make sure labels don't overlap each other
+ while ($lastLabelEndPos >= $left) {
+ $left += $leftstep;
+ }
+ // When a label is longer than the available space, use the next line
+ if ($left + strlen($text) > 100) {
+ $top += $topstep;
+ $left = 0;
+ }
+
+ $colorBox = new Rect($left, $top, 2, 2);
+ $colorBox->setFill($color)->setStrokeWidth(2);
+ $colorBox->keepRatio();
+ $outer->addElement($colorBox);
+
+ $textBox = new Text($left+5, $top+2, $text);
+ $textBox->setFontSize('2em');
+ $outer->addElement($textBox);
+
+ // readjust layout
+ $lastLabelEndPos = $left + strlen($text);
+ $left += $leftstep;
+ }
+ $svg = $outer->toSvg($ctx);
+ return $svg;
+ }
+}
diff --git a/library/Icinga/Chart/Palette.php b/library/Icinga/Chart/Palette.php
new file mode 100644
index 0000000..90ad74b
--- /dev/null
+++ b/library/Icinga/Chart/Palette.php
@@ -0,0 +1,65 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+/**
+ * Provide a set of colors that will be used by the chart as default values
+ */
+class Palette
+{
+ /**
+ * Neutral colors without special meaning
+ */
+ const NEUTRAL = 'neutral';
+
+ /**
+ * A set of problem (i.e. red) colors
+ */
+ const PROBLEM = 'problem';
+
+ /**
+ * A set of ok (i.e. green) colors
+ */
+ const OK = 'ok';
+
+ /**
+ * A set of warning (i.e. yellow) colors
+ */
+ const WARNING = 'warning';
+
+ /**
+ * The colorsets for specific categories
+ *
+ * @var array
+ */
+ public $colorSets = array(
+ self::OK => array('#00FF00'),
+ self::PROBLEM => array('#FF0000'),
+ self::WARNING => array('#FFFF00'),
+ self::NEUTRAL => array('#f3f3f3')
+ );
+
+ /**
+ * Return the next available color as an hex string for the given type
+ *
+ * @param string $type The type to receive a color from
+ *
+ * @return string The color in hex format
+ */
+ public function getNext($type = self::NEUTRAL)
+ {
+ if (!isset($this->colorSets[$type])) {
+ $type = self::NEUTRAL;
+ }
+
+ $color = current($this->colorSets[$type]);
+ if ($color === false) {
+ reset($this->colorSets[$type]);
+
+ $color = current($this->colorSets[$type]);
+ }
+ next($this->colorSets[$type]);
+ return $color;
+ }
+}
diff --git a/library/Icinga/Chart/PieChart.php b/library/Icinga/Chart/PieChart.php
new file mode 100644
index 0000000..1bcf380
--- /dev/null
+++ b/library/Icinga/Chart/PieChart.php
@@ -0,0 +1,306 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use DOMElement;
+use Icinga\Chart\Chart;
+use Icinga\Chart\Primitive\Canvas;
+use Icinga\Chart\Primitive\PieSlice;
+use Icinga\Chart\Primitive\RawElement;
+use Icinga\Chart\Primitive\Rect;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Render\LayoutBox;
+
+/**
+ * Graphing component for rendering Pie Charts.
+ *
+ * See the graphs.md documentation for further information about how to use this component
+ */
+class PieChart extends Chart
+{
+ /**
+ * Stack multiple pies
+ */
+ const STACKED = "stacked";
+
+ /**
+ * Draw multiple pies beneath each other
+ */
+ const ROW = "row";
+
+ /**
+ * The drawing stack containing all pie definitions in the order they will be drawn
+ *
+ * @var array
+ */
+ private $pies = array();
+
+ /**
+ * The composition type currently used
+ *
+ * @var string
+ */
+ private $type = PieChart::STACKED;
+
+ /**
+ * Disable drawing of captions when set true
+ *
+ * @var bool
+ */
+ private $noCaption = false;
+
+ public function __construct()
+ {
+ $this->title = t('Pie Chart');
+ $this->description = t('Contains data in a pie chart.');
+ parent::__construct();
+ }
+
+ /**
+ * Test if the given pies have the correct format
+ *
+ * @return bool True when the given pies are correct, otherwise false
+ */
+ public function isValidDataFormat()
+ {
+ foreach ($this->pies as $pie) {
+ if (!isset($pie['data']) || !is_array($pie['data'])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Create renderer and normalize the dataset to represent percentage information
+ */
+ protected function build()
+ {
+ $this->renderer = new SVGRenderer(($this->type === self::STACKED) ? 1 : count($this->pies), 1);
+ foreach ($this->pies as &$pie) {
+ $this->normalizeDataSet($pie);
+ }
+ }
+
+ /**
+ * Normalize the given dataset to represent percentage information instead of absolute valuess
+ *
+ * @param array $pie The pie definition given in the drawPie call
+ */
+ private function normalizeDataSet(&$pie)
+ {
+ $total = array_sum($pie['data']);
+ if ($total === 100) {
+ return;
+ }
+ if ($total == 0) {
+ return;
+ }
+ foreach ($pie['data'] as &$slice) {
+ $slice = $slice/$total * 100;
+ }
+ }
+
+ /**
+ * Draw an arbitrary number of pies in this chart
+ *
+ * @param array $dataSet,... The pie definition, see graphs.md for further details concerning the format
+ *
+ * @return $this Fluent interface
+ */
+ public function drawPie(array $dataSet)
+ {
+ $dataSets = func_get_args();
+ $this->pies += $dataSets;
+ foreach ($dataSets as $dataSet) {
+ $this->legend->addDataset($dataSet);
+ }
+ return $this;
+ }
+
+ /**
+ * Return the SVG representation of this graph
+ *
+ * @param RenderContext $ctx The context to use for drawings
+ *
+ * @return DOMElement The SVG representation of this graph
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $labelBox = $ctx->getDocument()->createElement('g');
+ if (!$this->noCaption) {
+ // Scale SVG to make room for captions
+ $outerBox = new Canvas('outerGraph', new LayoutBox(33, -5, 40, 40));
+ $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100));
+ $innerBox->getLayout()->setPadding(10, 10, 10, 10);
+ } else {
+ $outerBox = new Canvas('outerGraph', new LayoutBox(1.5, -10, 124, 124));
+ $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100));
+ $innerBox->getLayout()->setPadding(0, 0, 0, 0);
+ }
+ $this->createContentClipBox($innerBox);
+ $this->renderPies($innerBox, $labelBox);
+ $innerBox->addElement(new RawElement($labelBox));
+ $outerBox->addElement($innerBox);
+
+ return $outerBox->toSvg($ctx);
+ }
+
+ /**
+ * Render the pies in the draw stack using the selected algorithm for composition
+ *
+ * @param Canvas $innerBox The canvas to use for inserting the pies
+ * @param DOMElement $labelBox The DOM element to add the labels to (so they can't be overlapped by pie elements)
+ */
+ private function renderPies(Canvas $innerBox, DOMElement $labelBox)
+ {
+ if ($this->type === self::STACKED) {
+ $this->renderStackedPie($innerBox, $labelBox);
+ } else {
+ $this->renderPieRow($innerBox, $labelBox);
+ }
+ }
+
+ /**
+ * Return the color to be used for the given pie slice
+ *
+ * @param array $pie The pie configuration as provided in the drawPie call
+ * @param int $dataIdx The index of the pie slice in the pie configuration
+ *
+ * @return string The hex color string to use for the pie slice
+ */
+ private function getColorForPieSlice(array $pie, $dataIdx)
+ {
+ if (isset($pie['colors']) && is_array($pie['colors']) && isset($pie['colors'][$dataIdx])) {
+ return $pie['colors'][$dataIdx];
+ }
+ $type = Palette::NEUTRAL;
+ if (isset($pie['palette']) && is_array($pie['palette']) && isset($pie['palette'][$dataIdx])) {
+ $type = $pie['palette'][$dataIdx];
+ }
+ return $this->palette->getNext($type);
+ }
+
+ /**
+ * Render a row of pies
+ *
+ * @param Canvas $innerBox The canvas to insert the pies to
+ * @param DOMElement $labelBox The DOMElement to use for adding label elements
+ */
+ private function renderPieRow(Canvas $innerBox, DOMElement $labelBox)
+ {
+ $radius = 50 / count($this->pies);
+ $x = $radius;
+ foreach ($this->pies as $pie) {
+ $labelPos = 0;
+ $lastRadius = 0;
+
+ foreach ($pie['data'] as $idx => $dataset) {
+ $slice = new PieSlice($radius, $dataset, $lastRadius);
+ $slice->setX($x)
+ ->setStrokeColor('#000')
+ ->setStrokeWidth(1)
+ ->setY(50)
+ ->setFill($this->getColorForPieSlice($pie, $idx));
+ $innerBox->addElement($slice);
+ // add caption if not disabled
+ if (!$this->noCaption && isset($pie['labels'])) {
+ $slice->setCaption($pie['labels'][$labelPos++])
+ ->setLabelGroup($labelBox);
+ }
+ $lastRadius += $dataset;
+ }
+ // shift right for next pie
+ $x += $radius*2;
+ }
+ }
+
+ /**
+ * Render pies in a stacked way so one pie is nested in the previous pie
+ *
+ * @param Canvas $innerBox The canvas to insert the pie to
+ * @param DOMElement $labelBox The DOMElement to use for adding label elements
+ */
+ private function renderStackedPie(Canvas $innerBox, DOMElement $labelBox)
+ {
+ $radius = 40;
+ $minRadius = 20;
+ if (count($this->pies) == 0) {
+ return;
+ }
+ $shrinkStep = ($radius - $minRadius) / count($this->pies);
+ $x = $radius;
+
+ for ($i = 0; $i < count($this->pies); $i++) {
+ $pie = $this->pies[$i];
+ // the offset for the caption path, outer caption indicator shouldn't point
+ // to the middle of the slice as there will be another pie
+ $offset = isset($this->pies[$i+1]) ? $radius - $shrinkStep : 0;
+ $labelPos = 0;
+ $lastRadius = 0;
+ foreach ($pie['data'] as $idx => $dataset) {
+ $color = $this->getColorForPieSlice($pie, $idx);
+ if ($dataset == 0) {
+ $labelPos++;
+ continue;
+ }
+ $slice = new PieSlice($radius, $dataset, $lastRadius);
+ $slice->setY(50)
+ ->setX($x)
+ ->setStrokeColor('#000')
+ ->setStrokeWidth(1)
+ ->setFill($color)
+ ->setLabelGroup($labelBox);
+
+ if (!$this->noCaption && isset($pie['labels'])) {
+ $slice->setCaption($pie['labels'][$labelPos++])
+ ->setCaptionOffset($offset)
+ ->setOuterCaptionBound(50);
+ }
+ $innerBox->addElement($slice);
+ $lastRadius += $dataset;
+ }
+ // shrinken the next pie
+ $radius -= $shrinkStep;
+ }
+ }
+
+ /**
+ * Set the composition type of this PieChart
+ *
+ * @param string $type Either self::STACKED or self::ROW
+ *
+ * @return $this Fluent interface
+ */
+ public function setType($type)
+ {
+ $this->type = $type;
+ return $this;
+ }
+
+ /**
+ * Hide the caption from this PieChart
+ *
+ * @return $this Fluent interface
+ */
+ public function disableLegend()
+ {
+ $this->noCaption = true;
+ return $this;
+ }
+
+ /**
+ * Create the content for this PieChart
+ *
+ * @param Canvas $innerBox The innerbox to add the clip mask 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);
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Animatable.php b/library/Icinga/Chart/Primitive/Animatable.php
new file mode 100644
index 0000000..69ba0e1
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Animatable.php
@@ -0,0 +1,43 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Base interface for animatable objects
+ */
+abstract class Animatable extends Styleable
+{
+ /**
+ * The animation object set
+ *
+ * @var Animation
+ */
+ public $animation = null;
+
+ /**
+ * Set the animation for this object
+ *
+ * @param Animation $anim The animation to use
+ */
+ public function setAnimation(Animation $anim)
+ {
+ $this->animation = $anim;
+ }
+
+ /**
+ * Append the animation to the given element
+ *
+ * @param DOMElement $dom The element to append the animation to
+ * @param RenderContext $ctx The context to use for rendering the animation object
+ */
+ protected function appendAnimation(DOMElement $dom, RenderContext $ctx)
+ {
+ if ($this->animation) {
+ $dom->appendChild($this->animation->toSvg($ctx));
+ }
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Animation.php b/library/Icinga/Chart/Primitive/Animation.php
new file mode 100644
index 0000000..e620fa7
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Animation.php
@@ -0,0 +1,87 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Drawable for the SVG animate tag
+ */
+class Animation implements Drawable
+{
+ /**
+ * The attribute to animate
+ *
+ * @var string
+ */
+ private $attribute;
+
+ /**
+ * The 'from' value
+ *
+ * @var mixed
+ */
+ private $from;
+
+ /**
+ * The to value
+ *
+ * @var mixed
+ */
+ private $to;
+
+ /**
+ * The begin value (in seconds)
+ *
+ * @var float
+ */
+ private $begin = 0;
+
+ /**
+ * The duration value (in seconds)
+ *
+ * @var float
+ */
+ private $duration = 0.5;
+
+ /**
+ * Create an animation object
+ *
+ * @param string $attribute The attribute to animate
+ * @param string $from The from value for the animation
+ * @param string $to The to value for the animation
+ * @param float $duration The duration of the duration
+ * @param float $begin The begin of the duration
+ */
+ public function __construct($attribute, $from, $to, $duration = 0.5, $begin = 0.0)
+ {
+ $this->attribute = $attribute;
+ $this->from = $from;
+ $this->to = $to;
+ $this->duration = $duration;
+ $this->begin = $begin;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+
+ $animate = $ctx->getDocument()->createElement('animate');
+ $animate->setAttribute('attributeName', $this->attribute);
+ $animate->setAttribute('attributeType', 'XML');
+ $animate->setAttribute('from', $this->from);
+ $animate->setAttribute('to', $this->to);
+ $animate->setAttribute('begin', $this->begin . 's');
+ $animate->setAttribute('dur', $this->duration . 's');
+ $animate->setAttribute('fill', "freeze");
+
+ return $animate;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Canvas.php b/library/Icinga/Chart/Primitive/Canvas.php
new file mode 100644
index 0000000..32f06bf
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Canvas.php
@@ -0,0 +1,140 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Chart\Render\LayoutBox;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Canvas SVG component that encapsulates grouping and padding and allows rendering
+ * multiple elements in a group
+ *
+ */
+class Canvas implements Drawable
+{
+ /**
+ * The name of the canvas, will be used as the id
+ *
+ * @var string
+ */
+ private $name;
+
+ /**
+ * An array of child elements of this Canvas
+ *
+ * @var array
+ */
+ private $children = array();
+
+ /**
+ * When true, this canvas is encapsulated in a clipPath tag and not drawn
+ *
+ * @var bool
+ */
+ private $isClipPath = false;
+
+ /**
+ * The LayoutBox of this Canvas
+ *
+ * @var LayoutBox
+ */
+ private $rect;
+
+ /**
+ * The aria role used to describe this canvas' purpose in the accessibility tree
+ *
+ * @var string
+ */
+ private $ariaRole;
+
+ /**
+ * Create this canvas
+ *
+ * @param String $name The name of this canvas
+ * @param LayoutBox $rect The layout and size of this canvas
+ */
+ public function __construct($name, LayoutBox $rect)
+ {
+ $this->rect = $rect;
+ $this->name = $name;
+ }
+
+ /**
+ * Convert this canvas to a clipPath element
+ */
+ public function toClipPath()
+ {
+ $this->isClipPath = true;
+ }
+
+ /**
+ * Return the layout of this canvas
+ *
+ * @return LayoutBox
+ */
+ public function getLayout()
+ {
+ return $this->rect;
+ }
+
+ /**
+ * Add an element to this canvas
+ *
+ * @param Drawable $child
+ */
+ public function addElement(Drawable $child)
+ {
+ $this->children[] = $child;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ if ($this->isClipPath) {
+ $outer = $doc->createElement('defs');
+ $innerContainer = $element = $doc->createElement('clipPath');
+ $outer->appendChild($element);
+ } else {
+ $outer = $element = $doc->createElement('g');
+ $innerContainer = $doc->createElement('g');
+ $innerContainer->setAttribute('x', 0);
+ $innerContainer->setAttribute('y', 0);
+ $innerContainer->setAttribute('id', $this->name . '_inner');
+ $innerContainer->setAttribute('transform', $this->rect->getInnerTransform($ctx));
+ $element->appendChild($innerContainer);
+ }
+
+ $element->setAttribute('id', $this->name);
+ foreach ($this->children as $child) {
+ $innerContainer->appendChild($child->toSvg($ctx));
+ }
+
+ if (isset($this->ariaRole)) {
+ $outer->setAttribute('role', $this->ariaRole);
+ }
+ return $outer;
+ }
+
+ /**
+ * Set the aria role used to determine the meaning of this canvas in the accessibility tree
+ *
+ * The role 'presentation' will indicate that the purpose of this canvas is entirely decorative, while the role
+ * 'img' will indicate that the canvas contains an image, with a possible title or a description. For other
+ * possible roles, see http://www.w3.org/TR/wai-aria/roles
+ *
+ * @param $role string The aria role to set
+ */
+ public function setAriaRole($role)
+ {
+ $this->ariaRole = $role;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Circle.php b/library/Icinga/Chart/Primitive/Circle.php
new file mode 100644
index 0000000..f98ffac
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Circle.php
@@ -0,0 +1,84 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Primitive;
+
+use DOMDocument;
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+
+/**
+ * Drawable for svg circles
+ */
+class Circle extends Styleable implements Drawable
+{
+ /**
+ * The circles x position
+ *
+ * @var int
+ */
+ private $x;
+
+ /**
+ * The circles y position
+ *
+ * @var int
+ */
+ private $y;
+
+ /**
+ * The circles radius
+ *
+ * @var int
+ */
+ private $radius;
+
+ /**
+ * Construct the circle
+ *
+ * @param int $x The x position of the circle
+ * @param int $y The y position of the circle
+ * @param int $radius The radius of the circle
+ */
+ public function __construct($x, $y, $radius)
+ {
+ $this->x = $x;
+ $this->y = $y;
+ $this->radius = $radius;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $coords = $ctx->toAbsolute($this->x, $this->y);
+ $circle = $ctx->getDocument()->createElement('circle');
+ $circle->setAttribute('cx', Format::formatSVGNumber($coords[0]));
+ $circle->setAttribute('cy', Format::formatSVGNumber($coords[1]));
+ $circle->setAttribute('r', $this->radius);
+
+ $id = $this->id ?? uniqid('circle-');
+ $circle->setAttribute('id', $id);
+ $this->setId($id);
+
+ $this->applyAttributes($circle);
+
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $circle->appendChild(
+ $circle->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ return $circle;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Drawable.php b/library/Icinga/Chart/Primitive/Drawable.php
new file mode 100644
index 0000000..5b4355c
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Drawable.php
@@ -0,0 +1,22 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Drawable element for creating svg out of components
+ */
+interface Drawable
+{
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx);
+}
diff --git a/library/Icinga/Chart/Primitive/Line.php b/library/Icinga/Chart/Primitive/Line.php
new file mode 100644
index 0000000..d83cbea
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Line.php
@@ -0,0 +1,103 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMDocument;
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+
+/**
+ * Drawable for the svg line element
+ */
+class Line extends Styleable implements Drawable
+{
+
+ /**
+ * The default stroke width
+ *
+ * @var int
+ */
+ public $strokeWidth = 1;
+
+ /**
+ * The line's start x coordinate
+ *
+ * @var int
+ */
+ private $xStart = 0;
+
+ /**
+ * The line's end x coordinate
+ *
+ * @var int
+ */
+ private $xEnd = 0;
+
+ /**
+ * The line's start y coordinate
+ *
+ * @var int
+ */
+ private $yStart = 0;
+
+ /**
+ * The line's end y coordinate
+ *
+ * @var int
+ */
+ private $yEnd = 0;
+
+ /**
+ * Create a line object starting at the first coordinate and ending at the second one
+ *
+ * @param int $x1 The line's start x coordinate
+ * @param int $y1 The line's start y coordinate
+ * @param int $x2 The line's end x coordinate
+ * @param int $y2 The line's end y coordinate
+ */
+ public function __construct($x1, $y1, $x2, $y2)
+ {
+ $this->xStart = $x1;
+ $this->xEnd = $x2;
+ $this->yStart = $y1;
+ $this->yEnd = $y2;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ list($x1, $y1) = $ctx->toAbsolute($this->xStart, $this->yStart);
+ list($x2, $y2) = $ctx->toAbsolute($this->xEnd, $this->yEnd);
+ $line = $doc->createElement('line');
+ $line->setAttribute('x1', Format::formatSVGNumber($x1));
+ $line->setAttribute('x2', Format::formatSVGNumber($x2));
+ $line->setAttribute('y1', Format::formatSVGNumber($y1));
+ $line->setAttribute('y2', Format::formatSVGNumber($y2));
+
+ $id = $this->id ?? uniqid('line-');
+ $line->setAttribute('id', $id);
+ $this->setId($id);
+
+ $this->applyAttributes($line);
+
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $line->appendChild(
+ $line->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ return $line;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Path.php b/library/Icinga/Chart/Primitive/Path.php
new file mode 100644
index 0000000..b9d5f7b
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Path.php
@@ -0,0 +1,187 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMDocument;
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+
+/**
+ * Drawable for creating a svg path element
+ */
+class Path extends Styleable implements Drawable
+{
+ /**
+ * Syntax template for moving
+ *
+ * @see http://www.w3.org/TR/SVG/paths.html#PathDataMovetoCommands
+ */
+ const TPL_MOVE = 'M %s %s ';
+
+ /**
+ * Syntax template for bezier curve
+ *
+ * @see http://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands
+ */
+ const TPL_BEZIER = 'S %s %s ';
+
+ /**
+ * Syntax template for straight lines
+ *
+ * @see http://www.w3.org/TR/SVG/paths.html#PathDataLinetoCommands
+ */
+ const TPL_STRAIGHT = 'L %s %s ';
+
+ /**
+ * The default stroke width
+ *
+ * @var int
+ */
+ public $strokeWidth = 1;
+
+ /**
+ * True to treat coordinates as absolute values
+ *
+ * @var bool
+ */
+ protected $isAbsolute = false;
+
+ /**
+ * The points to draw, in the order they are drawn
+ *
+ * @var array
+ */
+ protected $points = array();
+
+ /**
+ * True to draw the path discrete, i.e. make hard steps between points
+ *
+ * @var bool
+ */
+ protected $discrete = false;
+
+ /**
+ * Create the path using the given points
+ *
+ * @param array $points Either a single [x, y] point or an array of x, y points
+ */
+ public function __construct(array $points)
+ {
+ $this->append($points);
+ }
+
+ /**
+ * Append a single point or an array of points to this path
+ *
+ * @param array $points Either a single [x, y] point or an array of x, y points
+ *
+ * @return $this Fluid interface
+ */
+ public function append(array $points)
+ {
+ if (count($points) === 0) {
+ return $this;
+ }
+ if (!is_array($points[0])) {
+ $points = array($points);
+ }
+ $this->points = array_merge($this->points, $points);
+ return $this;
+ }
+
+ /**
+ * Prepend a single point or an array of points to this path
+ *
+ * @param array $points Either a single [x, y] point or an array of x, y points
+ *
+ * @return $this Fluid interface
+ */
+ public function prepend(array $points)
+ {
+ if (count($points) === 0) {
+ return $this;
+ }
+ if (!is_array($points[0])) {
+ $points = array($points);
+ }
+ $this->points = array_merge($points, $this->points);
+ return $this;
+ }
+
+ /**
+ * Set this path to be discrete
+ *
+ * @param boolean $bool True to draw discrete or false to draw straight lines between points
+ *
+ * @return $this Fluid interface
+ */
+ public function setDiscrete($bool)
+ {
+ $this->discrete = $bool;
+ return $this;
+ }
+
+ /**
+ * Mark this path as containing absolute coordinates
+ *
+ * @return $this Fluid interface
+ */
+ public function toAbsolute()
+ {
+ $this->isAbsolute = true;
+ return $this;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ $group = $doc->createElement('g');
+
+ $pathDescription = '';
+ $tpl = self::TPL_MOVE;
+ $lastPoint = null;
+ foreach ($this->points as $point) {
+ if (!$this->isAbsolute) {
+ $point = $ctx->toAbsolute($point[0], $point[1]);
+ }
+ $point[0] = Format::formatSVGNumber($point[0]);
+ $point[1] = Format::formatSVGNumber($point[1]);
+ if ($lastPoint && $this->discrete) {
+ $pathDescription .= sprintf($tpl, $point[0], $lastPoint[1]);
+ }
+ $pathDescription .= vsprintf($tpl, $point);
+ $lastPoint = $point;
+ $tpl = self::TPL_STRAIGHT;
+ }
+
+ $path = $doc->createElement('path');
+
+ $id = $this->id ?? uniqid('path-');
+ $path->setAttribute('id', $id);
+ $this->setId($id);
+
+ $path->setAttribute('d', $pathDescription);
+
+ $this->applyAttributes($path);
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $path->appendChild(
+ $path->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ $group->appendChild($path);
+ return $group;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/PieSlice.php b/library/Icinga/Chart/Primitive/PieSlice.php
new file mode 100644
index 0000000..f898435
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/PieSlice.php
@@ -0,0 +1,307 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMDocument;
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+
+/**
+ * Component for drawing a pie slice
+ */
+class PieSlice extends Animatable implements Drawable
+{
+ /**
+ * The radius of this pieslice relative to the canvas
+ *
+ * @var int
+ */
+ private $radius = 50;
+
+ /**
+ * The start radian of the pie slice
+ *
+ * @var float
+ */
+ private $startRadian = 0;
+
+ /**
+ * The end radian of the pie slice
+ *
+ * @var float
+ */
+ private $endRadian = 0;
+
+ /**
+ * The x position of the pie slice's center
+ *
+ * @var int
+ */
+ private $x;
+
+ /**
+ * The y position of the pie slice's center
+ *
+ * @var int
+ */
+ private $y;
+
+ /**
+ * The caption of the pie slice, empty string means no caption
+ *
+ * @var string
+ */
+ private $caption = "";
+
+ /**
+ * The offset of the caption, shifting the indicator from the center of the pie slice
+ *
+ * This is required for nested pie slices.
+ *
+ * @var int
+ */
+ private $captionOffset = 0;
+
+ /**
+ * The minimum radius the label must respect
+ *
+ * @var int
+ */
+ private $outerCaptionBound = 0;
+
+ /**
+ * An optional group element to add labels to when rendering
+ *
+ * @var DOMElement
+ */
+ private $labelGroup;
+
+ /**
+ * Create a pie slice
+ *
+ * @param int $radius The radius of the slice
+ * @param int $percent The percentage the slice represents
+ * @param int $percentStart The percentage where this slice starts
+ */
+ public function __construct($radius, $percent, $percentStart = 0)
+ {
+ $this->x = $this->y = $this->radius = $radius;
+
+ $this->startRadian = M_PI * $percentStart/50;
+ $this->endRadian = M_PI * ($percent + $percentStart)/50;
+ }
+
+ /**
+ * Create the path for the pie slice
+ *
+ * @param int $x The x position of the pie slice
+ * @param int $y The y position of the pie slice
+ * @param int $r The absolute radius of the pie slice
+ *
+ * @return string A SVG path string
+ */
+ private function getPieSlicePath($x, $y, $r)
+ {
+ // The coordinate system is mirrored on the Y axis, so we have to flip cos and sin
+ $xStart = $x + ($r * sin($this->startRadian));
+ $yStart = $y - ($r * cos($this->startRadian));
+
+ if ($this->endRadian - $this->startRadian == 2*M_PI) {
+ // To draw a full circle, adjust arc endpoint by a small (unvisible) value
+ $this->endRadian -= 0.001;
+ $pathString = 'M ' . Format::formatSVGNumber($xStart) . ' ' . Format::formatSVGNumber($yStart);
+ } else {
+ // Start at the center of the pieslice
+ $pathString = 'M ' . $x . ' ' . $y;
+ // Draw a straight line to the upper part of the arc
+ $pathString .= ' L ' . Format::formatSVGNumber($xStart) . ' ' . Format::formatSVGNumber($yStart);
+ }
+
+ // Instead of directly connecting the upper part of the arc (leaving a triangle), draw a bow with the radius
+ $pathString .= ' A ' . Format::formatSVGNumber($r) . ' ' . Format::formatSVGNumber($r);
+ // These are the flags for the bow, see the SVG path documentation for details
+ // http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
+ $pathString .= ' 0 ' . (($this->endRadian - $this->startRadian > M_PI) ? '1' : '0 ') . ' 1';
+
+ // xEnd and yEnd are the lower point of the arc
+ $xEnd = $x + ($r * sin($this->endRadian));
+ $yEnd = $y - ($r * cos($this->endRadian));
+ $pathString .= ' ' . Format::formatSVGNumber($xEnd) . ' ' . Format::formatSVGNumber($yEnd);
+
+ return $pathString;
+ }
+
+ /**
+ * Draw the label handler and the text for this pie slice
+ *
+ * @param RenderContext $ctx The rendering context to use for coordinate translation
+ * @param int $r The radius of the pie in absolute coordinates
+ *
+ * @return DOMElement The group DOMElement containing the handle and label
+ */
+ private function drawDescriptionLabel(RenderContext $ctx, $r)
+ {
+ $group = $ctx->getDocument()->createElement('g');
+ $rOuter = ($ctx->xToAbsolute($this->outerCaptionBound) + $ctx->yToAbsolute($this->outerCaptionBound)) / 2;
+ $addOffset = $rOuter - $r ;
+ if ($addOffset < 0) {
+ $addOffset = 0;
+ }
+ list($x, $y) = $ctx->toAbsolute($this->x, $this->y);
+ $midRadius = $this->startRadian + ($this->endRadian - $this->startRadian) / 2;
+ list($offsetX, $offsetY) = $ctx->toAbsolute($this->captionOffset, $this->captionOffset);
+
+ $midX = $x + intval(($offsetX + $r)/2 * sin($midRadius));
+ $midY = $y - intval(($offsetY + $r)/2 * cos($midRadius));
+
+ // Draw the handle
+ $path = new Path(array($midX, $midY));
+
+ $midX += ($addOffset + $r/3) * ($midRadius > M_PI ? -1 : 1);
+ $path->append(array($midX, $midY))->toAbsolute();
+
+ $midX += intval($r/2 * sin(M_PI/9)) * ($midRadius > M_PI ? -1 : 1);
+ $midY -= intval($r/2 * cos(M_PI/3)) * ($midRadius < M_PI*1.4 && $midRadius > M_PI/3 ? -1 : 1);
+
+ if ($ctx->yToRelative($midY) > 100) {
+ $midY = $ctx->yToAbsolute(100);
+ } elseif ($ctx->yToRelative($midY) < 0) {
+ $midY = $ctx->yToAbsolute($ctx->yToRelative(100+$midY));
+ }
+
+ $path->append(array($midX , $midY));
+ $rel = $ctx->toRelative($midX, $midY);
+
+ // Draw the text box
+ $text = new Text($rel[0]+1.5, $rel[1], $this->caption);
+ $text->setFontSize('5em');
+ $text->setAlignment(($midRadius > M_PI ? Text::ALIGN_END : Text::ALIGN_START));
+
+ $group->appendChild($path->toSvg($ctx));
+ $group->appendChild($text->toSvg($ctx));
+
+ return $group;
+ }
+
+ /**
+ * Set the x position of the pie slice
+ *
+ * @param int $x The new x position
+ *
+ * @return $this Fluid interface
+ */
+ public function setX($x)
+ {
+ $this->x = $x;
+ return $this;
+ }
+
+ /**
+ * Set the y position of the pie slice
+ *
+ * @param int $y The new y position
+ *
+ * @return $this Fluid interface
+ */
+ public function setY($y)
+ {
+ $this->y = $y;
+ return $this;
+ }
+
+ /**
+ * Set a root element to be used for drawing labels
+ *
+ * @param DOMElement $group The label group
+ *
+ * @return $this Fluid interface
+ */
+ public function setLabelGroup(DOMElement $group)
+ {
+ $this->labelGroup = $group;
+ return $this;
+ }
+
+ /**
+ * Set the caption for this label
+ *
+ * @param string $caption The caption for this element
+ *
+ * @return $this Fluid interface
+ */
+ public function setCaption($caption)
+ {
+ $this->caption = $caption;
+ return $this;
+ }
+
+ /**
+ * Set the internal offset of the caption handle
+ *
+ * @param int $offset The offset for the caption handle
+ *
+ * @return $this Fluid interface
+ */
+ public function setCaptionOffset($offset)
+ {
+ $this->captionOffset = $offset;
+ return $this;
+ }
+
+ /**
+ * Set the minimum radius to be used for drawing labels
+ *
+ * @param int $bound The offset for the caption text
+ *
+ * @return $this Fluid interface
+ */
+ public function setOuterCaptionBound($bound)
+ {
+ $this->outerCaptionBound = $bound;
+ return $this;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ $group = $doc->createElement('g');
+ $r = ($ctx->xToAbsolute($this->radius) + $ctx->yToAbsolute($this->radius)) / 2;
+ list($x, $y) = $ctx->toAbsolute($this->x, $this->y);
+
+ $slicePath = $doc->createElement('path');
+
+ $slicePath->setAttribute('d', $this->getPieSlicePath($x, $y, $r));
+ $slicePath->setAttribute('data-icinga-graph-type', 'pieslice');
+
+ $id = $this->id ?? uniqid('slice-');
+ $slicePath->setAttribute('id', $id);
+ $this->setId($id);
+
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $slicePath->appendChild(
+ $slicePath->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ $this->applyAttributes($slicePath);
+ $group->appendChild($slicePath);
+ if ($this->caption != "") {
+ $lblGroup = ($this->labelGroup ? $this->labelGroup : $group);
+ $lblGroup->appendChild($this->drawDescriptionLabel($ctx, $r));
+ }
+ return $group;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/RawElement.php b/library/Icinga/Chart/Primitive/RawElement.php
new file mode 100644
index 0000000..721b6e0
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/RawElement.php
@@ -0,0 +1,43 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Wrapper for raw elements to be added as Drawable's
+ */
+class RawElement implements Drawable
+{
+
+ /**
+ * The DOMElement wrapped by this Drawable
+ *
+ * @var DOMElement
+ */
+ private $domEl;
+
+ /**
+ * Create this RawElement
+ *
+ * @param DOMElement $el The element to wrap here
+ */
+ public function __construct(DOMElement $el)
+ {
+ $this->domEl = $el;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ return $this->domEl;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Rect.php b/library/Icinga/Chart/Primitive/Rect.php
new file mode 100644
index 0000000..0c0835f
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Rect.php
@@ -0,0 +1,119 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use DOMDocument;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+
+/**
+ * Drawable representing the SVG rect element
+ */
+class Rect extends Animatable implements Drawable
+{
+ /**
+ * The x position
+ *
+ * @var int
+ */
+ private $x;
+
+ /**
+ * The y position
+ *
+ * @var int
+ */
+ private $y;
+
+ /**
+ * The width of this rect
+ *
+ * @var int
+ */
+ private $width;
+
+ /**
+ * The height of this rect
+ *
+ * @var int
+ */
+ private $height;
+
+ /**
+ * Whether to keep the ratio
+ *
+ * @var bool
+ */
+ private $keepRatio = false;
+
+ /**
+ * Create this rect
+ *
+ * @param int $x The x position of the rect
+ * @param int $y The y position of the rectangle
+ * @param int $width The width of the rectangle
+ * @param int $height The height of the rectangle
+ */
+ public function __construct($x, $y, $width, $height)
+ {
+ $this->x = $x;
+ $this->y = $y;
+ $this->width = $width;
+ $this->height = $height;
+ }
+
+ /**
+ * Call to let the rectangle keep the ratio
+ */
+ public function keepRatio()
+ {
+ $this->keepRatio = true;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ $rect = $doc->createElement('rect');
+
+ list($x, $y) = $ctx->toAbsolute($this->x, $this->y);
+ if ($this->keepRatio) {
+ $ctx->keepRatio();
+ }
+ list($width, $height) = $ctx->toAbsolute($this->width, $this->height);
+ if ($this->keepRatio) {
+ $ctx->ignoreRatio();
+ }
+ $rect->setAttribute('x', Format::formatSVGNumber($x));
+ $rect->setAttribute('y', Format::formatSVGNumber($y));
+ $rect->setAttribute('width', Format::formatSVGNumber($width));
+ $rect->setAttribute('height', Format::formatSVGNumber($height));
+
+ $id = $this->id ?? uniqid('rect-');
+ $rect->setAttribute('id', $id);
+ $this->setId($id);
+
+ $this->applyAttributes($rect);
+ $this->appendAnimation($rect, $ctx);
+
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $rect->appendChild(
+ $rect->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ return $rect;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Styleable.php b/library/Icinga/Chart/Primitive/Styleable.php
new file mode 100644
index 0000000..15025bf
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Styleable.php
@@ -0,0 +1,161 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Util\Csp;
+use ipl\Web\Style;
+
+/**
+ * Base class for stylable drawables
+ */
+class Styleable
+{
+
+ /**
+ * The stroke width to use
+ *
+ * @var int|float
+ */
+ public $strokeWidth = 0;
+
+ /**
+ * The stroke color to use
+ *
+ * @var string
+ */
+ public $strokeColor = '#000';
+
+ /**
+ * The fill color to use
+ *
+ * @var string
+ */
+ public $fill = 'none';
+
+ /**
+ * Additional styles to be appended to the style attribute
+ *
+ * @var array<string, string>
+ */
+ public $additionalStyle = [];
+
+ /**
+ * The id of this element
+ *
+ * @var ?string
+ */
+ public $id = null;
+
+ /**
+ * Additional attributes to be set
+ *
+ * @var array
+ */
+ public $attributes = array();
+
+ /**
+ * Set the stroke width for this drawable
+ *
+ * @param int|float $width The stroke with unit
+ *
+ * @return $this Fluid interface
+ */
+ public function setStrokeWidth($width)
+ {
+ $this->strokeWidth = $width;
+ return $this;
+ }
+
+ /**
+ * Set the color for the stroke or none for no stroke
+ *
+ * @param string $color The color to set for the stroke
+ *
+ * @return $this Fluid interface
+ */
+ public function setStrokeColor($color)
+ {
+ $this->strokeColor = $color ? $color : 'none';
+ return $this;
+ }
+
+ /**
+ * Set additional styles for this drawable
+ *
+ * @param array<string, string> $styles The styles to set additionally
+ *
+ * @return $this Fluid interface
+ */
+ public function setAdditionalStyle($styles)
+ {
+ $this->additionalStyle = $styles;
+ return $this;
+ }
+
+ /**
+ * Set the fill for this styleable
+ *
+ * @param string $color The color to use for filling or null to use no fill
+ *
+ * @return $this Fluid interface
+ */
+ public function setFill($color = null)
+ {
+ $this->fill = $color ? $color : 'none';
+ return $this;
+ }
+
+ /**
+ * Set the id for this element
+ *
+ * @param string $id The id to set for this element
+ *
+ * @return $this Fluid interface
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ /**
+ * Return the ruleset used for styling the DOMNode
+ *
+ * @return Style A ruleset containing styles
+ */
+ public function getStyle()
+ {
+ $styles = $this->additionalStyle;
+ $styles['fill'] = $this->fill;
+ $styles['stroke'] = $this->strokeColor;
+ $styles['stroke-width'] = (string) $this->strokeWidth;
+
+ return (new Style())
+ ->setNonce(Csp::getStyleNonce())
+ ->add("#$this->id", $styles);
+ }
+
+ /**
+ * Add an additional attribute to this element
+ */
+ public function setAttribute($key, $value)
+ {
+ $this->attributes[$key] = $value;
+ }
+
+ /**
+ * Apply attribute to a DOMElement
+ *
+ * @param DOMElement $el Element to apply attributes
+ */
+ protected function applyAttributes(DOMElement $el)
+ {
+ foreach ($this->attributes as $name => $value) {
+ $el->setAttribute($name, $value);
+ }
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Text.php b/library/Icinga/Chart/Primitive/Text.php
new file mode 100644
index 0000000..f6bf365
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Text.php
@@ -0,0 +1,184 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Primitive;
+
+use DOMDocument;
+use DOMElement;
+use DOMText;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+use ipl\Html\HtmlDocument;
+
+/**
+ * Wrapper for the SVG text element
+ */
+class Text extends Styleable implements Drawable
+{
+ /**
+ * Align the text to end at the x and y position
+ */
+ const ALIGN_END = 'end';
+
+ /**
+ * Align the text to start at the x and y position
+ */
+ const ALIGN_START = 'start';
+
+ /**
+ * Align the text to be centered at the x and y position
+ */
+ const ALIGN_MIDDLE = 'middle';
+
+ /**
+ * The x position of the Text
+ *
+ * @var int
+ */
+ private $x;
+
+ /**
+ * The y position of the Text
+ *
+ * @var int
+ */
+ private $y;
+
+ /**
+ * The text content
+ *
+ * @var string
+ */
+ private $text;
+
+ /**
+ * The size of the font
+ *
+ * @var string
+ */
+ private $fontSize = '1.5em';
+
+ /**
+ * The weight of the font
+ *
+ * @var string
+ */
+ private $fontWeight = 'normal';
+
+ /**
+ * The default fill color
+ *
+ * @var string
+ */
+ public $fill = '#000';
+
+ /**
+ * The alignment of the text
+ *
+ * @var string
+ */
+ private $alignment = self::ALIGN_START;
+
+ /**
+ * Set the font-stretch property of the text
+ */
+ private $fontStretch = 'semi-condensed';
+
+ /**
+ * Construct a new text drawable
+ *
+ * @param int $x The x position of the text
+ * @param int $y The y position of the text
+ * @param string $text The text this component should contain
+ * @param string $fontSize The font size of the text
+ */
+ public function __construct($x, $y, $text, $fontSize = '1.5em')
+ {
+ $this->x = $x;
+ $this->y = $y;
+ $this->text = $text;
+ $this->fontSize = $fontSize;
+
+ $this->setAdditionalStyle([
+ 'font-size' => $this->fontSize,
+ 'font-family' => 'Ubuntu, Calibri, Trebuchet MS, Helvetica, Verdana, sans-serif',
+ 'font-weight' => $this->fontWeight,
+ 'font-stretch' => $this->fontStretch,
+ 'font-style' => 'normal',
+ 'text-anchor' => $this->alignment
+ ]);
+ }
+
+ /**
+ * Set the font size of the svg text element
+ *
+ * @param string $size The font size including a unit
+ *
+ * @return $this Fluid interface
+ */
+ public function setFontSize($size)
+ {
+ $this->fontSize = $size;
+ return $this;
+ }
+
+ /**
+ * Set the text alignment with one of the ALIGN_* constants
+ *
+ * @param String $align Value how to align
+ *
+ * @return $this Fluid interface
+ */
+ public function setAlignment($align)
+ {
+ $this->alignment = $align;
+ return $this;
+ }
+
+ /**
+ * Set the weight of the current font
+ *
+ * @param string $weight The weight of the string
+ *
+ * @return $this Fluid interface
+ */
+ public function setFontWeight($weight)
+ {
+ $this->fontWeight = $weight;
+ return $this;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ list($x, $y) = $ctx->toAbsolute($this->x, $this->y);
+ $text = $ctx->getDocument()->createElement('text');
+ $text->setAttribute('x', Format::formatSVGNumber($x - 15));
+
+ $id = $this->id ?? uniqid('text-');
+ $text->setAttribute('id', $id);
+ $this->setId($id);
+
+ $text->setAttribute('y', Format::formatSVGNumber($y));
+ $text->appendChild(new DOMText($this->text));
+
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $text->appendChild(
+ $text->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ return $text;
+ }
+}
diff --git a/library/Icinga/Chart/Render/LayoutBox.php b/library/Icinga/Chart/Render/LayoutBox.php
new file mode 100644
index 0000000..fa49461
--- /dev/null
+++ b/library/Icinga/Chart/Render/LayoutBox.php
@@ -0,0 +1,200 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Render;
+
+use Icinga\Chart\Format;
+
+/**
+ * Layout class encapsulating size, padding and margin information
+ */
+class LayoutBox
+{
+ /**
+ * Padding index for top padding
+ */
+ const PADDING_TOP = 0;
+
+ /**
+ * Padding index for right padding
+ */
+ const PADDING_RIGHT = 1;
+
+ /**
+ * Padding index for bottom padding
+ */
+ const PADDING_BOTTOM = 2;
+
+ /**
+ * Padding index for left padding
+ */
+ const PADDING_LEFT = 3;
+
+ /**
+ * The height of this layout element
+ *
+ * @var int
+ */
+ private $height;
+
+ /**
+ * The width of this layout element
+ *
+ * @var int
+ */
+ private $width;
+
+ /**
+ * The x position of this layout
+ *
+ * @var int
+ */
+ private $x;
+
+ /**
+ * The y position of this layout
+ *
+ * @var int
+ */
+ private $y;
+
+ /**
+ * The padding of this layout
+ *
+ * @var array
+ */
+ private $padding = array(0, 0, 0, 0);
+
+ /**
+ * Create this layout box
+ *
+ * Note that x, y, width and height are relative: x with 0 means leftmost, x with 100 means rightmost
+ *
+ * @param int $x The relative x coordinate
+ * @param int $y The relative y coordinate
+ * @param int $width The optional, relative width
+ * @param int $height The optional, relative height
+ */
+ public function __construct($x, $y, $width = null, $height = null)
+ {
+ $this->height = $height ? $height : 100;
+ $this->width = $width ? $width : 100;
+ $this->x = $x;
+ $this->y = $y;
+ }
+
+ /**
+ * Set a padding to all four sides uniformly
+ *
+ * @param int $padding The padding to set for all four sides
+ */
+ public function setUniformPadding($padding)
+ {
+ $this->padding = array($padding, $padding, $padding, $padding);
+ }
+
+ /**
+ * Set the padding for this LayoutBox
+ *
+ * @param int $top The top side padding
+ * @param int $right The right side padding
+ * @param int $bottom The bottom side padding
+ * @param int $left The left side padding
+ */
+ public function setPadding($top, $right, $bottom, $left)
+ {
+ $this->padding = array($top, $right, $bottom, $left);
+ }
+
+ /**
+ * Return a string containing the SVG transform attribute values for the padding
+ *
+ * @param RenderContext $ctx The context to determine the translation coordinates
+ *
+ * @return string The transformation string
+ */
+ public function getInnerTransform(RenderContext $ctx)
+ {
+ list($translateX, $translateY) = $ctx->toAbsolute(
+ $this->padding[self::PADDING_LEFT] + $this->getX(),
+ $this->padding[self::PADDING_TOP] + $this->getY()
+ );
+ list($scaleX, $scaleY) = $ctx->paddingToScaleFactor($this->padding);
+
+ $scaleX *= $this->getWidth()/100;
+ $scaleY *= $this->getHeight()/100;
+ return sprintf(
+ 'translate(%s, %s) scale(%s, %s)',
+ Format::formatSVGNumber($translateX),
+ Format::formatSVGNumber($translateY),
+ Format::formatSVGNumber($scaleX),
+ Format::formatSVGNumber($scaleY)
+ );
+ }
+
+ /**
+ * String representation for this Layout, for debug purposes
+ *
+ * @return string A string containing the bounds of this LayoutBox
+ */
+ public function __toString()
+ {
+ return sprintf(
+ 'Rectangle: x: %s y: %s, height: %s, width: %s',
+ $this->x,
+ $this->y,
+ $this->height,
+ $this->width
+ );
+ }
+
+ /**
+ * Return a four element array with the padding
+ *
+ * @return array The padding of this LayoutBox
+ */
+ public function getPadding()
+ {
+ return $this->padding;
+ }
+
+ /**
+ * Return the height of this LayoutBox
+ *
+ * @return int The height of this box
+ */
+ public function getHeight()
+ {
+ return $this->height;
+ }
+
+ /**
+ * Return the width of this LayoutBox
+ *
+ * @return int The width of this box
+ */
+ public function getWidth()
+ {
+ return $this->width;
+ }
+
+ /**
+ * Return the x position of this LayoutBox
+ *
+ * @return int The x position of this box
+ */
+ public function getX()
+ {
+ return $this->x;
+ }
+
+ /**
+ * Return the y position of this LayoutBox
+ *
+ * @return int The y position of this box
+ */
+ public function getY()
+ {
+ return $this->y;
+ }
+}
diff --git a/library/Icinga/Chart/Render/RenderContext.php b/library/Icinga/Chart/Render/RenderContext.php
new file mode 100644
index 0000000..457fbf3
--- /dev/null
+++ b/library/Icinga/Chart/Render/RenderContext.php
@@ -0,0 +1,225 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Render;
+
+use DOMDocument;
+
+/**
+ * Context for rendering, handles ratio based coordinate calculations.
+ *
+ * The most important functions when rendering are the toAbsolute and roRelative
+ * values, taking world coordinates and translating them into local coordinates.
+ */
+class RenderContext
+{
+
+ /**
+ * The base size of the viewport, i.e. how many units are available on a 1:1 ratio
+ *
+ * @var array
+ */
+ private $viewBoxSize = array(1000, 1000);
+
+
+ /**
+ * The DOMDocument for modifying the elements
+ *
+ * @var DOMDocument
+ */
+ private $document;
+
+ /**
+ * If true no ratio correction will be made
+ *
+ * @var bool
+ */
+ private $respectRatio = false;
+
+ /**
+ * The ratio on the x side. A x ration of 2 means that the width of the SVG is divided in 2000
+ * units (see $viewBox)
+ *
+ * @var int
+ */
+ private $xratio = 1;
+
+ /**
+ * The ratio on the y side. A y ration of 2 means that the height of the SVG is divided in 2000
+ * units (see $viewBox)
+ *
+ * @var int
+ */
+ private $yratio = 1;
+
+ /**
+ * Creates a new context for the given DOM Document
+ *
+ * @param DOMDocument $document The DOM document represented by this context
+ * @param int $width The width (may be approximate) of the document
+ * (only required for ratio calculation)
+ * @param int $height The height (may be approximate) of the document
+ * (only required for ratio calculation)
+ */
+ public function __construct(DOMDocument $document, $width, $height)
+ {
+ $this->document = $document;
+ if ($width > $height) {
+ $this->xratio = $width / $height;
+ } elseif ($height > $width) {
+ $this->yratio = $height / $width;
+ }
+ }
+
+ /**
+ * Return the document represented by this Rendering context
+ *
+ * @return DOMDocument The DOMDocument for creating files
+ */
+ public function getDocument()
+ {
+ return $this->document;
+ }
+
+ /**
+ * Let successive toAbsolute operations ignore ratio correction
+ *
+ * This can be called to avoid distortion on certain elements like rectangles.
+ */
+ public function keepRatio()
+ {
+ $this->respectRatio = true;
+ }
+
+ /**
+ * Let successive toAbsolute operations perform ratio correction
+ *
+ * This will cause distortion on certain elements like rectangles.
+ */
+ public function ignoreRatio()
+ {
+ $this->respectRatio = false;
+ }
+
+ /**
+ * Return how many unit s are available in the Y axis
+ *
+ * @return int The number of units available on the y axis
+ */
+ public function getNrOfUnitsY()
+ {
+ return intval($this->viewBoxSize[1] * $this->yratio);
+ }
+
+ /**
+ * Return how many unit s are available in the X axis
+ *
+ * @return int The number of units available on the x axis
+ */
+ public function getNrOfUnitsX()
+ {
+ return intval($this->viewBoxSize[0] * $this->xratio);
+ }
+
+ /**
+ * Transforms the x,y coordinate from relative coordinates to absolute world coordinates
+ *
+ * (50, 50) would be a point in the middle of the document and map to 500, 1000 on a
+ * 1000 x 1000 viewbox with a 1:2 ratio.
+ *
+ * @param int $x The relative x coordinate
+ * @param int $y The relative y coordinate
+ *
+ * @return array An x,y tuple containing absolute coordinates
+ * @see RenderContext::toRelative
+ */
+ public function toAbsolute($x, $y)
+ {
+ return array($this->xToAbsolute($x), $this->yToAbsolute($y));
+ }
+
+ /**
+ * Transforms the x,y coordinate from absolute coordinates to relative world coordinates
+ *
+ * This is the inverse function of toAbsolute
+ *
+ * @param int $x The absolute x coordinate
+ * @param int $y The absolute y coordinate
+ *
+ * @return array An x,y tupel containing absolute coordinates
+ * @see RenderContext::toAbsolute
+ */
+ public function toRelative($x, $y)
+ {
+ return array($this->xToRelative($x), $this->yToRelative($y));
+ }
+
+ /**
+ * Calculates the scale transformation required to apply the padding on an Canvas
+ *
+ * @param array $padding A 4 element array containing top, right, bottom and left padding
+ *
+ * @return array An array containing the x and y scale
+ */
+ public function paddingToScaleFactor(array $padding)
+ {
+ list($horizontalPadding, $verticalPadding) = $this->toAbsolute(
+ $padding[LayoutBox::PADDING_RIGHT] + $padding[LayoutBox::PADDING_LEFT],
+ $padding[LayoutBox::PADDING_TOP] + $padding[LayoutBox::PADDING_BOTTOM]
+ );
+
+ return array(
+ ($this->getNrOfUnitsX() - $horizontalPadding) / $this->getNrOfUnitsX(),
+ ($this->getNrOfUnitsY() - $verticalPadding) / $this->getNrOfUnitsY()
+ );
+ }
+
+ /**
+ * Transform a relative x coordinate to an absolute one
+ *
+ * @param int $x A relative x coordinate
+ *
+ * @return int An absolute x coordinate
+ **/
+ public function xToAbsolute($x)
+ {
+ return $this->getNrOfUnitsX() / 100 * $x / ($this->respectRatio ? $this->xratio : 1);
+ }
+
+ /**
+ * Transform a relative y coordinate to an absolute one
+ *
+ * @param int $y A relative y coordinate
+ *
+ * @return int An absolute y coordinate
+ */
+ public function yToAbsolute($y)
+ {
+ return $this->getNrOfUnitsY() / 100 * $y / ($this->respectRatio ? $this->yratio : 1);
+ }
+
+ /**
+ * Transform a absolute x coordinate to an relative one
+ *
+ * @param int $x An absolute x coordinate
+ *
+ * @return int A relative x coordinate
+ */
+ public function xToRelative($x)
+ {
+ return $x / $this->getNrOfUnitsX() * 100 * ($this->respectRatio ? $this->xratio : 1);
+ }
+
+ /**
+ * Transform a absolute y coordinate to an relative one
+ *
+ * @param int $y An absolute x coordinate
+ *
+ * @return int A relative x coordinate
+ */
+ public function yToRelative($y)
+ {
+ return $y / $this->getNrOfUnitsY() * 100 * ($this->respectRatio ? $this->yratio : 1);
+ }
+}
diff --git a/library/Icinga/Chart/Render/Rotator.php b/library/Icinga/Chart/Render/Rotator.php
new file mode 100644
index 0000000..3e7071c
--- /dev/null
+++ b/library/Icinga/Chart/Render/Rotator.php
@@ -0,0 +1,80 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Render;
+
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Primitive\Drawable;
+use DOMElement;
+
+/**
+ * Class Rotator
+ * @package Icinga\Chart\Render
+ */
+class Rotator implements Drawable
+{
+ /**
+ * The drawable element to rotate
+ *
+ * @var Drawable
+ */
+ private $element;
+
+ /**
+ * @var int
+ */
+ private $degrees;
+
+ /**
+ * Wrap an element into a new instance of Rotator
+ *
+ * @param Drawable $element The element to rotate
+ * @param int $degrees The amount of degrees
+ */
+ public function __construct(Drawable $element, $degrees)
+ {
+ $this->element = $element;
+ $this->degrees = $degrees;
+ }
+
+ /**
+ * Rotate the given element.
+ *
+ * @param RenderContext $ctx The rendering context
+ * @param DOMElement $el The element to rotate
+ * @param $degrees The amount of degrees
+ *
+ * @return DOMElement The rotated DOMElement
+ */
+ private function rotate(RenderContext $ctx, DOMElement $el, $degrees)
+ {
+ // Create a box containing the rotated element relative to the original element 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');
+
+ // Put the element into a rotated group
+ //$rotate = $ctx->getDocument()->createElement('g');
+ $el->setAttribute('transform', 'rotate(' . $degrees . ')');
+ //$rotate->appendChild($el);
+
+ $container->appendChild($el);
+ return $container;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $el = $this->element->toSvg($ctx);
+ return $this->rotate($ctx, $el, $this->degrees);
+ }
+}
diff --git a/library/Icinga/Chart/SVGRenderer.php b/library/Icinga/Chart/SVGRenderer.php
new file mode 100644
index 0000000..d3891f2
--- /dev/null
+++ b/library/Icinga/Chart/SVGRenderer.php
@@ -0,0 +1,331 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use DOMNode;
+use DOMElement;
+use DOMDocument;
+use DOMImplementation;
+use Icinga\Chart\Render\LayoutBox;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Primitive\Canvas;
+
+/**
+ * SVG Renderer component.
+ *
+ * Creates the basic DOM tree of the SVG to use
+ */
+class SVGRenderer
+{
+ const X_ASPECT_RATIO_MIN = 'xMin';
+
+ const X_ASPECT_RATIO_MID = 'xMid';
+
+ const X_ASPECT_RATIO_MAX = 'xMax';
+
+ const Y_ASPECT_RATIO_MIN = 'YMin';
+
+ const Y_ASPECT_RATIO_MID = 'YMid';
+
+ const Y_ASPECT_RATIO_MAX = 'YMax';
+
+ const ASPECT_RATIO_PAD = 'meet';
+
+ const ASPECT_RATIO_CUTOFF = 'slice';
+
+ /**
+ * The XML-document
+ *
+ * @var DOMDocument
+ */
+ private $document;
+
+ /**
+ * The SVG-element
+ *
+ * @var DOMNode
+ */
+ private $svg;
+
+ /**
+ * The description of this SVG, useful for screen readers
+ *
+ * @var string
+ */
+ private $ariaDescription;
+
+ /**
+ * The title of this SVG, useful for screen readers
+ *
+ * @var string
+ */
+ private $ariaTitle;
+
+ /**
+ * The aria role used by this svg element
+ *
+ * @var string
+ */
+ private $ariaRole = 'img';
+
+ /**
+ * The root layer for all elements
+ *
+ * @var Canvas
+ */
+ private $rootCanvas;
+
+ /**
+ * The width of this renderer
+ *
+ * @var int
+ */
+ private $width = 100;
+
+ /**
+ * The height of this renderer
+ *
+ * @var int
+ */
+ private $height = 100;
+
+ /**
+ * Whether the aspect ratio is preversed
+ *
+ * @var bool
+ */
+ private $preserveAspectRatio = false;
+
+ /**
+ * Horizontal alignment of SVG element
+ *
+ * @var string
+ */
+ private $xAspectRatio = self::X_ASPECT_RATIO_MID;
+
+ /**
+ * Vertical alignment of SVG element
+ *
+ * @var string
+ */
+ private $yAspectRatio = self::Y_ASPECT_RATIO_MID;
+
+ /**
+ * Define whether aspect differences should be handled using padding (default) or cutoff
+ *
+ * @var string
+ */
+ private $xFillMode = "meet";
+
+
+ /**
+ * Create the root document and the SVG root node
+ */
+ private function createRootDocument()
+ {
+ $implementation = new DOMImplementation();
+ $docType = $implementation->createDocumentType(
+ 'svg',
+ '-//W3C//DTD SVG 1.1//EN',
+ 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'
+ );
+
+ $this->document = $implementation->createDocument(null, '', $docType);
+ $this->svg = $this->createOuterBox();
+ $this->document->appendChild($this->svg);
+ }
+
+ /**
+ * Create the outer SVG box containing the root svg element and namespace and return it
+ *
+ * @return DOMElement The SVG root node
+ */
+ private function createOuterBox()
+ {
+ $ctx = $this->createRenderContext();
+ $svg = $this->document->createElement('svg');
+ $svg->setAttribute('xmlns', 'http://www.w3.org/2000/svg');
+ $svg->setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
+ $svg->setAttribute('role', $this->ariaRole);
+ $svg->setAttribute('width', '100%');
+ $svg->setAttribute('height', '100%');
+ $svg->setAttribute(
+ 'viewBox',
+ sprintf(
+ '0 0 %s %s',
+ $ctx->getNrOfUnitsX(),
+ $ctx->getNrOfUnitsY()
+ )
+ );
+ if ($this->preserveAspectRatio) {
+ $svg->setAttribute(
+ 'preserveAspectRatio',
+ sprintf(
+ '%s%s %s',
+ $this->xAspectRatio,
+ $this->yAspectRatio,
+ $this->xFillMode
+ )
+ );
+ }
+ return $svg;
+ }
+
+ /**
+ * Add aria title and description
+ *
+ * Adds an aria title and desc element to the given SVG node, which are used to describe this SVG by accessibility
+ * tools such as screen readers.
+ *
+ * @param DOMNode $svg The SVG DOMNode to which the aria attributes should be attached
+ * @param $title The title text
+ * @param $description The description text
+ */
+ private function addAriaDescription(DOMNode $svg, $titleText, $descriptionText)
+ {
+ $doc = $svg->ownerDocument;
+
+ $titleId = $descId = '';
+ if (isset($this->ariaTitle)) {
+ $titleId = 'aria-title-' . $this->stripNonAlphanumeric($titleText);
+ $title = $doc->createElement('title');
+ $title->setAttribute('id', $titleId);
+
+ $title->appendChild($doc->createTextNode($titleText));
+ $svg->appendChild($title);
+ }
+
+ if (isset($this->ariaDescription)) {
+ $descId = 'aria-desc-' . $this->stripNonAlphanumeric($descriptionText);
+ $desc = $doc->createElement('desc');
+ $desc->setAttribute('id', $descId);
+
+ $desc->appendChild($doc->createTextNode($descriptionText));
+ $svg->appendChild($desc);
+ }
+
+ $svg->setAttribute('aria-labelledby', join(' ', array($titleId, $descId)));
+ }
+
+ /**
+ * Initialises the XML-document, SVG-element and this figure's root canvas
+ *
+ * @param int $width The width ratio
+ * @param int $height The height ratio
+ */
+ public function __construct($width, $height)
+ {
+ $this->width = $width;
+ $this->height = $height;
+ $this->rootCanvas = new Canvas('root', new LayoutBox(0, 0));
+ }
+
+ /**
+ * Render the SVG-document
+ *
+ * @return string The resulting XML structure
+ */
+ public function render()
+ {
+ $this->createRootDocument();
+ $ctx = $this->createRenderContext();
+ $this->addAriaDescription($this->svg, $this->ariaTitle, $this->ariaDescription);
+ $this->svg->appendChild($this->rootCanvas->toSvg($ctx));
+ $this->document->formatOutput = true;
+ $this->document->encoding = 'UTF-8';
+ return $this->document->saveXML();
+ }
+
+ /**
+ * Create a render context that will be used for rendering elements
+ *
+ * @return RenderContext The created RenderContext instance
+ */
+ public function createRenderContext()
+ {
+ return new RenderContext($this->document, $this->width, $this->height);
+ }
+
+ /**
+ * Return the root canvas of this rendered
+ *
+ * @return Canvas The canvas that will be the uppermost element in this figure
+ */
+ public function getCanvas()
+ {
+ return $this->rootCanvas;
+ }
+
+ /**
+ * Preserve the aspect ratio of the rendered object
+ *
+ * Do not deform the content of the SVG when the aspect ratio of the viewBox
+ * differs from the aspect ratio of the SVG element, but add padding or cutoff
+ * instead
+ *
+ * @param bool $preserve Whether the aspect ratio should be preserved
+ */
+ public function preserveAspectRatio($preserve = true)
+ {
+ $this->preserveAspectRatio = $preserve;
+ }
+
+ /**
+ * Change the horizontal alignment of the SVG element
+ *
+ * Change the horizontal alignment of the svg, when preserveAspectRatio is used and
+ * padding is present. Defaults to
+ */
+ public function setXAspectRatioAlignment($alignment)
+ {
+ $this->xAspectRatio = $alignment;
+ }
+
+ /**
+ * Change the vertical alignment of the SVG element
+ *
+ * Change the vertical alignment of the svg, when preserveAspectRatio is used and
+ * padding is present.
+ */
+ public function setYAspectRatioAlignment($alignment)
+ {
+ $this->yAspectRatio = $alignment;
+ }
+
+ /**
+ * Set the aria description, that is used as a title for this SVG in screen readers
+ *
+ * @param $text
+ */
+ public function setAriaTitle($text)
+ {
+ $this->ariaTitle = $text;
+ }
+
+ /**
+ * Set the aria description, that is used to describe this SVG in screen readers
+ *
+ * @param $text
+ */
+ public function setAriaDescription($text)
+ {
+ $this->ariaDescription = $text;
+ }
+
+ /**
+ * Set the aria role, that is used to describe the purpose of this SVG in screen readers
+ *
+ * @param $text
+ */
+ public function setAriaRole($text)
+ {
+ $this->ariaRole = $text;
+ }
+
+
+ private function stripNonAlphanumeric($str)
+ {
+ return preg_replace('/[^A-Za-z]+/', '', $str);
+ }
+}
diff --git a/library/Icinga/Chart/Unit/AxisUnit.php b/library/Icinga/Chart/Unit/AxisUnit.php
new file mode 100644
index 0000000..251787f
--- /dev/null
+++ b/library/Icinga/Chart/Unit/AxisUnit.php
@@ -0,0 +1,56 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Unit;
+
+use Iterator;
+
+/**
+ * Base class for Axis Units
+ *
+ * An AxisUnit takes a set of values and places them on a given range
+ *
+ * Concrete subclasses must implement the iterator interface, with
+ * getCurrent returning the axis relative position and getValue the label
+ * that will be displayed
+ */
+interface AxisUnit extends Iterator
+{
+ /**
+ * Add a dataset to this AxisUnit, required for dynamic min and max vlaues
+ *
+ * @param array $dataset The dataset that will be shown in the Axis
+ * @param int $id The idx in the dataset (0 for x, 1 for y)
+ */
+ public function addValues(array $dataset, $id = 0);
+
+ /**
+ * Transform the given absolute value in an axis relative value
+ *
+ * @param int $value The absolute, dataset dependent value
+ *
+ * @return int An axis relative value
+ */
+ public function transform($value);
+
+ /**
+ * Set the axis minimum value to a fixed value
+ *
+ * @param int $min The new minimum value
+ */
+ public function setMin($min);
+
+ /**
+ * Set the axis maximum value to a fixed value
+ *
+ * @param int $max The new maximum value
+ */
+ public function setMax($max);
+
+ /**
+ * Get the amount of ticks of this axis
+ *
+ * @return int
+ */
+ public function getTicks();
+}
diff --git a/library/Icinga/Chart/Unit/CalendarUnit.php b/library/Icinga/Chart/Unit/CalendarUnit.php
new file mode 100644
index 0000000..74680c7
--- /dev/null
+++ b/library/Icinga/Chart/Unit/CalendarUnit.php
@@ -0,0 +1,167 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Unit;
+
+use DateTime;
+
+/**
+ * Calendar Axis Unit that transforms timestamps into user-readable values
+ *
+ */
+class CalendarUnit extends LinearUnit
+{
+ /**
+ * Constant for a minute
+ */
+ const MINUTE = 60;
+
+ /**
+ * Constant for an hour
+ */
+ const HOUR = 3600;
+
+ /**
+ * Constant for a day
+ */
+ const DAY = 864000;
+
+ /**
+ * Constant for ~a month
+ * 30 Days, this is sufficient for our needs
+ */
+ const MONTH = 2592000; // x
+
+ /**
+ * An array containing all labels that will be displayed
+ *
+ * @var array
+ */
+ private $labels = array();
+
+ /**
+ * The date format to use
+ *
+ * @var string
+ */
+ private $dateFormat = 'd-m';
+
+ /**
+ * The time format to use
+ *
+ * @var string
+ */
+ private $timeFormat = 'g:i:s';
+
+ /**
+ * Create the labels for the given dataset
+ */
+ private function createLabels()
+ {
+ $this->labels = array();
+ $duration = $this->getMax() - $this->getMin();
+
+ if ($duration <= self::HOUR) {
+ $unit = self::MINUTE;
+ } elseif ($duration <= self::DAY) {
+ $unit = self::HOUR;
+ } elseif ($duration <= self::MONTH) {
+ $unit = self::DAY;
+ } else {
+ $unit = self::MONTH;
+ }
+ $this->calculateLabels($unit);
+ }
+
+ /**
+ * Calculate the labels for this dataset
+ *
+ * @param integer $unit The unit to use as the basis for calculation
+ */
+ private function calculateLabels($unit)
+ {
+ $fac = new DateTime();
+
+ $duration = $this->getMax() - $this->getMin();
+
+ // Calculate number of ticks, but not more than 30
+ $tickCount = ($duration/$unit * 10);
+ if ($tickCount > 30) {
+ $tickCount = 30;
+ }
+
+ $step = $duration / $tickCount;
+ $format = $this->timeFormat;
+ if ($unit === self::DAY) {
+ $format = $this->dateFormat;
+ } elseif ($unit === self::MONTH) {
+ $format = $this->dateFormat;
+ }
+
+ for ($i = 0; $i <= $duration; $i += $step) {
+ $this->labels[] = $fac->setTimestamp($this->getMin() + $i)->format($format);
+ }
+ }
+
+ /**
+ * Add a dataset to this CalendarUnit and update labels
+ *
+ * @param array $dataset The dataset to update
+ * @param int $idx The index to use for determining the data
+ *
+ * @return $this Fluid interface
+ */
+ public function addValues(array $dataset, $idx = 0)
+ {
+ parent::addValues($dataset, $idx);
+ $this->createLabels();
+ return $this;
+ }
+
+ /**
+ * Return the current axis relative position
+ *
+ * @return int The position of the next tick (between 0 and 100)
+ */
+ public function current(): int
+ {
+ return 100 * (key($this->labels) / count($this->labels));
+ }
+
+ /**
+ * Move to next tick
+ */
+ public function next(): void
+ {
+ next($this->labels);
+ }
+
+ /**
+ * Return the current tick caption
+ *
+ * @return string
+ */
+ public function key(): string
+ {
+ return current($this->labels);
+ }
+
+ /**
+ * Return true when the iterator is in a valid range
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ return current($this->labels) !== false;
+ }
+
+ /**
+ * Rewind the internal array
+ */
+ public function rewind(): void
+ {
+ reset($this->labels);
+ }
+}
diff --git a/library/Icinga/Chart/Unit/LinearUnit.php b/library/Icinga/Chart/Unit/LinearUnit.php
new file mode 100644
index 0000000..ea4792b
--- /dev/null
+++ b/library/Icinga/Chart/Unit/LinearUnit.php
@@ -0,0 +1,227 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Unit;
+
+/**
+ * Linear tick distribution over the axis
+ */
+class LinearUnit implements AxisUnit
+{
+ /**
+ * The minimum value to display
+ *
+ * @var int
+ */
+ protected $min;
+
+ /**
+ * The maximum value to display
+ *
+ * @var int
+ */
+ protected $max;
+
+ /**
+ * True when the minimum value is static and isn't affected by the dataset
+ *
+ * @var bool
+ */
+ protected $staticMin = false;
+
+ /**
+ * True when the maximum value is static and isn't affected by the dataset
+ *
+ * @var bool
+ */
+ protected $staticMax = false;
+
+ /**
+ * The number of ticks to use
+ *
+ * @var int
+ */
+ protected $nrOfTicks = 10;
+
+ /**
+ * The currently displayed tick
+ *
+ * @var int
+ */
+ protected $currentTick = 0;
+
+ /**
+ * The currently displayed value
+ * @var int
+ */
+ protected $currentValue = 0;
+
+ /**
+ * Create and initialize this AxisUnit
+ *
+ * @param int $nrOfTicks The number of ticks to use
+ */
+ public function __construct($nrOfTicks = 10)
+ {
+ $this->min = PHP_INT_MAX;
+ $this->max = ~PHP_INT_MAX;
+ $this->nrOfTicks = $nrOfTicks;
+ }
+
+ /**
+ * Add a dataset and calculate the minimum and maximum value for this AxisUnit
+ *
+ * @param array $dataset The dataset to add
+ * @param int $idx The idx (0 for x, 1 for y)
+ *
+ * @return $this Fluent interface
+ */
+ public function addValues(array $dataset, $idx = 0)
+ {
+ $datapoints = array();
+
+ foreach ($dataset['data'] as $points) {
+ $datapoints[] = $points[$idx];
+ }
+ if (empty($datapoints)) {
+ return $this;
+ }
+ sort($datapoints);
+ if (!$this->staticMax) {
+ $this->max = max($this->max, $datapoints[count($datapoints) - 1]);
+ }
+ if (!$this->staticMin) {
+ $this->min = min($this->min, $datapoints[0]);
+ }
+ $this->currentTick = 0;
+ $this->currentValue = $this->min;
+ if ($this->max === $this->min) {
+ $this->max = $this->min + 10;
+ }
+ $this->nrOfTicks = $this->max - $this->min;
+ return $this;
+ }
+
+ /**
+ * Transform the absolute value to an axis relative value
+ *
+ * @param int $value The absolute coordinate from the dataset
+ * @return float|int The axis relative coordinate (between 0 and 100)
+ */
+ public function transform($value)
+ {
+ if ($value < $this->min) {
+ return 0;
+ } elseif ($value > $this->max) {
+ return 100;
+ } else {
+ return 100 * ($value - $this->min) / $this->nrOfTicks;
+ }
+ }
+
+ /**
+ * Return the position of the current tick
+ *
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ return $this->currentTick;
+ }
+
+ /**
+ * Calculate the next tick and tick value
+ */
+ public function next(): void
+ {
+ $this->currentTick += (100 / $this->nrOfTicks);
+ $this->currentValue += (($this->max - $this->min) / $this->nrOfTicks);
+ }
+
+ /**
+ * Return the label for the current tick
+ *
+ * @return string The label for the current tick
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return (string) intval($this->currentValue);
+ }
+
+ /**
+ * True when we're at a valid tick (iterator interface)
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ return $this->currentTick >= 0 && $this->currentTick <= 100;
+ }
+
+ /**
+ * Reset the current tick and label value
+ */
+ public function rewind(): void
+ {
+ $this->currentTick = 0;
+ $this->currentValue = $this->min;
+ }
+
+ /**
+ * Set the axis maximum value to a fixed value
+ *
+ * @param int $max The new maximum value
+ */
+ public function setMax($max)
+ {
+ if ($max !== null) {
+ $this->max = $max;
+ $this->staticMax = true;
+ }
+ }
+
+ /**
+ * Set the axis minimum value to a fixed value
+ *
+ * @param int $min The new minimum value
+ */
+ public function setMin($min)
+ {
+ if ($min !== null) {
+ $this->min = $min;
+ $this->staticMin = true;
+ }
+ }
+
+ /**
+ * Return the current minimum value of the axis
+ *
+ * @return int The minimum set for this axis
+ */
+ public function getMin()
+ {
+ return $this->min;
+ }
+
+ /**
+ * Return the current maximum value of the axis
+ *
+ * @return int The maximum set for this axis
+ */
+ public function getMax()
+ {
+ return $this->max;
+ }
+
+ /**
+ * Get the amount of ticks necessary to display this AxisUnit
+ *
+ * @return int
+ */
+ public function getTicks()
+ {
+ return $this->nrOfTicks;
+ }
+}
diff --git a/library/Icinga/Chart/Unit/LogarithmicUnit.php b/library/Icinga/Chart/Unit/LogarithmicUnit.php
new file mode 100644
index 0000000..70961e2
--- /dev/null
+++ b/library/Icinga/Chart/Unit/LogarithmicUnit.php
@@ -0,0 +1,263 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Unit;
+
+/**
+ * Logarithmic tick distribution over the axis
+ *
+ * This class does not use the actual logarithm, but a slightly altered version called the
+ * Log-Modulo transformation. This is necessary, since a regular logarithmic scale is not able to display negative
+ * values and zero-points. See <a href="http://blogs.sas.com/content/iml/2014/07/14/log-transformation-of-pos-neg>
+ * this article </a> for a more detailed description.
+ */
+class LogarithmicUnit implements AxisUnit
+{
+ /**
+ * @var int
+ */
+ protected $base;
+
+ /**
+ * @var
+ */
+ protected $currentTick;
+
+ /**
+ * @var
+ */
+ protected $minExp;
+
+ /**
+ * @var
+ */
+ protected $maxExp;
+
+ /**
+ * True when the minimum value is static and isn't affected by the data set
+ *
+ * @var bool
+ */
+ protected $staticMin = false;
+
+ /**
+ * True when the maximum value is static and isn't affected by the data set
+ *
+ * @var bool
+ */
+ protected $staticMax = false;
+
+ /**
+ * Create and initialize this AxisUnit
+ *
+ * @param int $nrOfTicks The number of ticks to use
+ */
+ public function __construct($base = 10)
+ {
+ $this->base = $base;
+ $this->minExp = PHP_INT_MAX;
+ $this->maxExp = ~PHP_INT_MAX;
+ }
+
+ /**
+ * Add a dataset and calculate the minimum and maximum value for this AxisUnit
+ *
+ * @param array $dataset The dataset to add
+ * @param int $idx The idx (0 for x, 1 for y)
+ *
+ * @return $this Fluent interface
+ */
+ public function addValues(array $dataset, $idx = 0)
+ {
+ $datapoints = array();
+
+ foreach ($dataset['data'] as $points) {
+ $datapoints[] = $points[$idx];
+ }
+ if (empty($datapoints)) {
+ return $this;
+ }
+ sort($datapoints);
+ if (!$this->staticMax) {
+ $this->maxExp = max($this->maxExp, $this->logCeil($datapoints[count($datapoints) - 1]));
+ }
+ if (!$this->staticMin) {
+ $this->minExp = min($this->minExp, $this->logFloor($datapoints[0]));
+ }
+ $this->currentTick = 0;
+
+ return $this;
+ }
+
+ /**
+ * Transform the absolute value to an axis relative value
+ *
+ * @param int $value The absolute coordinate from the data set
+ * @return float|int The axis relative coordinate (between 0 and 100)
+ */
+ public function transform($value)
+ {
+ if ($value < $this->pow($this->minExp)) {
+ return 0;
+ } elseif ($value > $this->pow($this->maxExp)) {
+ return 100;
+ } else {
+ return 100 * ($this->log($value) - $this->minExp) / $this->getTicks();
+ }
+ }
+
+ /**
+ * Return the position of the current tick
+ *
+ * @return int
+ */
+ public function current(): int
+ {
+ return $this->currentTick * (100 / $this->getTicks());
+ }
+
+ /**
+ * Calculate the next tick and tick value
+ */
+ public function next(): void
+ {
+ ++ $this->currentTick;
+ }
+
+ /**
+ * Return the label for the current tick
+ *
+ * @return string The label for the current tick
+ */
+ public function key(): string
+ {
+ $currentBase = $this->currentTick + $this->minExp;
+ if (abs($currentBase) > 4) {
+ return $this->base . 'E' . $currentBase;
+ }
+ return (string) intval($this->pow($currentBase));
+ }
+
+ /**
+ * True when we're at a valid tick (iterator interface)
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ return $this->currentTick >= 0 && $this->currentTick < $this->getTicks();
+ }
+
+ /**
+ * Reset the current tick and label value
+ */
+ public function rewind(): void
+ {
+ $this->currentTick = 0;
+ }
+
+ /**
+ * Perform a log-modulo transformation
+ *
+ * @param $value The value to transform
+ *
+ * @return double The transformed value
+ */
+ protected function log($value)
+ {
+ $sign = $value > 0 ? 1 : -1;
+ return $sign * log1p($sign * $value) / log($this->base);
+ }
+
+ /**
+ * Calculate the biggest exponent necessary to display the given data point
+ *
+ * @param $value
+ *
+ * @return float
+ */
+ protected function logCeil($value)
+ {
+ return ceil($this->log($value)) + 1;
+ }
+
+ /**
+ * Calculate the smallest exponent necessary to display the given data point
+ *
+ * @param $value
+ *
+ * @return float
+ */
+ protected function logFloor($value)
+ {
+ return floor($this->log($value));
+ }
+
+ /**
+ * Inverse function to the log-modulo transformation
+ *
+ * @param $value
+ *
+ * @return double
+ */
+ protected function pow($value)
+ {
+ if ($value == 0) {
+ return 0;
+ }
+ $sign = $value > 0 ? 1 : -1;
+ return $sign * (pow($this->base, $sign * $value));
+ }
+
+ /**
+ * Set the axis minimum value to a fixed value
+ *
+ * @param int $min The new minimum value
+ */
+ public function setMin($min)
+ {
+ $this->minExp = $this->logFloor($min);
+ $this->staticMin = true;
+ }
+
+ /**
+ * Set the axis maximum value to a fixed value
+ *
+ * @param int $max The new maximum value
+ */
+ public function setMax($max)
+ {
+ $this->maxExp = $this->logCeil($max);
+ $this->staticMax = true;
+ }
+
+ /**
+ * Return the current minimum value of the axis
+ *
+ * @return int The minimum set for this axis
+ */
+ public function getMin()
+ {
+ return $this->pow($this->minExp);
+ }
+
+ /**
+ * Return the current maximum value of the axis
+ *
+ * @return int The maximum set for this axis
+ */
+ public function getMax()
+ {
+ return $this->pow($this->maxExp);
+ }
+
+ /**
+ * Get the amount of ticks necessary to display this AxisUnit
+ *
+ * @return int
+ */
+ public function getTicks()
+ {
+ return $this->maxExp - $this->minExp;
+ }
+}
diff --git a/library/Icinga/Chart/Unit/StaticAxis.php b/library/Icinga/Chart/Unit/StaticAxis.php
new file mode 100644
index 0000000..6b32aca
--- /dev/null
+++ b/library/Icinga/Chart/Unit/StaticAxis.php
@@ -0,0 +1,130 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Unit;
+
+class StaticAxis implements AxisUnit
+{
+ private $items = array();
+
+ /**
+ * Add a dataset to this AxisUnit, required for dynamic min and max values
+ *
+ * @param array $dataset The dataset that will be shown in the Axis
+ * @param int $idx The idx in the dataset (0 for x, 1 for y)
+ *
+ * @return $this Fluent interface
+ */
+ public function addValues(array $dataset, $idx = 0)
+ {
+ $datapoints = array();
+ foreach ($dataset['data'] as $points) {
+ $this->items[] = $points[$idx];
+ }
+ $this->items = array_unique($this->items);
+
+ return $this;
+ }
+
+ /**
+ * Transform the given absolute value in an axis relative value
+ *
+ * @param int $value The absolute, dataset dependent value
+ *
+ * @return int An axis relative value
+ */
+ public function transform($value)
+ {
+ $flipped = array_flip($this->items);
+ if (!isset($flipped[$value])) {
+ return 0;
+ }
+ $pos = $flipped[$value];
+ return 1 + (99 / count($this->items) * $pos);
+ }
+ /**
+ * Set the axis minimum value to a fixed value
+ *
+ * @param int $min The new minimum value
+ */
+ public function setMin($min)
+ {
+ }
+
+ /**
+ * Set the axis maximum value to a fixed value
+ *
+ * @param int $max The new maximum value
+ */
+ public function setMax($max)
+ {
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Return the current element
+ * @link http://php.net/manual/en/iterator.current.php
+ * @return int.
+ */
+ public function current(): int
+ {
+ return 1 + (99 / count($this->items) * key($this->items));
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Move forward to next element
+ * @link http://php.net/manual/en/iterator.next.php
+ * @return void Any returned value is ignored.
+ */
+ public function next(): void
+ {
+ next($this->items);
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Return the key of the current element
+ * @link http://php.net/manual/en/iterator.key.php
+ * @return mixed scalar on success, or null on failure.
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return current($this->items);
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ * @return boolean The return value will be casted to boolean and then evaluated.
+ * Returns true on success or false on failure.
+ */
+ public function valid(): bool
+ {
+ return current($this->items) !== false;
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Rewind the Iterator to the first element
+ * @link http://php.net/manual/en/iterator.rewind.php
+ * @return void Any returned value is ignored.
+ */
+ public function rewind(): void
+ {
+ reset($this->items);
+ }
+
+ /**
+ * Get the amount of ticks of this axis
+ *
+ * @return int
+ */
+ public function getTicks()
+ {
+ return count($this->items);
+ }
+}