From 8ca6cc32b2c789a3149861159ad258f2cb9491e3 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 14:39:39 +0200 Subject: Adding upstream version 2.11.4. Signed-off-by: Daniel Baumann --- library/Icinga/Chart/Axis.php | 485 ++++++++++++++++++++++++++ library/Icinga/Chart/Chart.php | 162 +++++++++ library/Icinga/Chart/Donut.php | 465 ++++++++++++++++++++++++ library/Icinga/Chart/Format.php | 21 ++ library/Icinga/Chart/Graph/BarGraph.php | 162 +++++++++ library/Icinga/Chart/Graph/LineGraph.php | 195 +++++++++++ library/Icinga/Chart/Graph/StackedGraph.php | 88 +++++ library/Icinga/Chart/Graph/Tooltip.php | 143 ++++++++ library/Icinga/Chart/GridChart.php | 446 +++++++++++++++++++++++ library/Icinga/Chart/Inline/Inline.php | 96 +++++ library/Icinga/Chart/Inline/PieChart.php | 41 +++ library/Icinga/Chart/Legend.php | 102 ++++++ library/Icinga/Chart/Palette.php | 65 ++++ library/Icinga/Chart/PieChart.php | 306 ++++++++++++++++ library/Icinga/Chart/Primitive/Animatable.php | 43 +++ library/Icinga/Chart/Primitive/Animation.php | 87 +++++ library/Icinga/Chart/Primitive/Canvas.php | 140 ++++++++ library/Icinga/Chart/Primitive/Circle.php | 68 ++++ library/Icinga/Chart/Primitive/Drawable.php | 22 ++ library/Icinga/Chart/Primitive/Line.php | 87 +++++ library/Icinga/Chart/Primitive/Path.php | 174 +++++++++ library/Icinga/Chart/Primitive/PieSlice.php | 293 ++++++++++++++++ library/Icinga/Chart/Primitive/RawElement.php | 43 +++ library/Icinga/Chart/Primitive/Rect.php | 105 ++++++ library/Icinga/Chart/Primitive/Styleable.php | 154 ++++++++ library/Icinga/Chart/Primitive/Text.php | 168 +++++++++ library/Icinga/Chart/Render/LayoutBox.php | 200 +++++++++++ library/Icinga/Chart/Render/RenderContext.php | 225 ++++++++++++ library/Icinga/Chart/Render/Rotator.php | 80 +++++ library/Icinga/Chart/SVGRenderer.php | 331 ++++++++++++++++++ library/Icinga/Chart/Unit/AxisUnit.php | 56 +++ library/Icinga/Chart/Unit/CalendarUnit.php | 167 +++++++++ library/Icinga/Chart/Unit/LinearUnit.php | 227 ++++++++++++ library/Icinga/Chart/Unit/LogarithmicUnit.php | 263 ++++++++++++++ library/Icinga/Chart/Unit/StaticAxis.php | 129 +++++++ 35 files changed, 5839 insertions(+) create mode 100644 library/Icinga/Chart/Axis.php create mode 100644 library/Icinga/Chart/Chart.php create mode 100644 library/Icinga/Chart/Donut.php create mode 100644 library/Icinga/Chart/Format.php create mode 100644 library/Icinga/Chart/Graph/BarGraph.php create mode 100644 library/Icinga/Chart/Graph/LineGraph.php create mode 100644 library/Icinga/Chart/Graph/StackedGraph.php create mode 100644 library/Icinga/Chart/Graph/Tooltip.php create mode 100644 library/Icinga/Chart/GridChart.php create mode 100644 library/Icinga/Chart/Inline/Inline.php create mode 100644 library/Icinga/Chart/Inline/PieChart.php create mode 100644 library/Icinga/Chart/Legend.php create mode 100644 library/Icinga/Chart/Palette.php create mode 100644 library/Icinga/Chart/PieChart.php create mode 100644 library/Icinga/Chart/Primitive/Animatable.php create mode 100644 library/Icinga/Chart/Primitive/Animation.php create mode 100644 library/Icinga/Chart/Primitive/Canvas.php create mode 100644 library/Icinga/Chart/Primitive/Circle.php create mode 100644 library/Icinga/Chart/Primitive/Drawable.php create mode 100644 library/Icinga/Chart/Primitive/Line.php create mode 100644 library/Icinga/Chart/Primitive/Path.php create mode 100644 library/Icinga/Chart/Primitive/PieSlice.php create mode 100644 library/Icinga/Chart/Primitive/RawElement.php create mode 100644 library/Icinga/Chart/Primitive/Rect.php create mode 100644 library/Icinga/Chart/Primitive/Styleable.php create mode 100644 library/Icinga/Chart/Primitive/Text.php create mode 100644 library/Icinga/Chart/Render/LayoutBox.php create mode 100644 library/Icinga/Chart/Render/RenderContext.php create mode 100644 library/Icinga/Chart/Render/Rotator.php create mode 100644 library/Icinga/Chart/SVGRenderer.php create mode 100644 library/Icinga/Chart/Unit/AxisUnit.php create mode 100644 library/Icinga/Chart/Unit/CalendarUnit.php create mode 100644 library/Icinga/Chart/Unit/LinearUnit.php create mode 100644 library/Icinga/Chart/Unit/LogarithmicUnit.php create mode 100644 library/Icinga/Chart/Unit/StaticAxis.php (limited to 'library/Icinga/Chart') 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 @@ +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 @@ +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 @@ + '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[] = ""; + + 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 @@ +order = $order; + $this->dataSet = $dataSet; + + $this->tooltips = $tooltips; + 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 null $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..6954c59 --- /dev/null +++ b/library/Icinga/Chart/Graph/LineGraph.php @@ -0,0 +1,195 @@ +dataset = $dataset; + $this->graphs = $graphs; + + $this->tooltips = $tooltips; + 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); + $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 @@ +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 @@ + + *
  • Global properties
  • : Key-value pairs that stay the same every time render is called, and are + * passed to an instance in the constructor. + *
  • Aggregated properties
  • : Global properties that are created automatically from + * all attached data points. + *
  • Local properties
  • : Key-value pairs that only apply to a single data point and + * are passed to the render-function. + * + */ +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 = '{title}: {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 @@ + + * + * $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)) + * ) + * ); + * + * + */ +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 @@ + $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 @@ +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 @@ +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 @@ + 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 @@ +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 @@ +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..4e55d0e --- /dev/null +++ b/library/Icinga/Chart/Primitive/Animation.php @@ -0,0 +1,87 @@ +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 @@ +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..2cb2b72 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Circle.php @@ -0,0 +1,68 @@ +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); + $circle->setAttribute('style', $this->getStyle()); + $this->applyAttributes($circle); + 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 @@ +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)); + $line->setAttribute('style', $this->getStyle()); + $this->applyAttributes($line); + return $line; + } +} diff --git a/library/Icinga/Chart/Primitive/Path.php b/library/Icinga/Chart/Primitive/Path.php new file mode 100644 index 0000000..2fa793a --- /dev/null +++ b/library/Icinga/Chart/Primitive/Path.php @@ -0,0 +1,174 @@ +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'); + if ($this->id) { + $path->setAttribute('id', $this->id); + } + $path->setAttribute('d', $pathDescription); + $path->setAttribute('style', $this->getStyle()); + $this->applyAttributes($path); + $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..f5a0ce9 --- /dev/null +++ b/library/Icinga/Chart/Primitive/PieSlice.php @@ -0,0 +1,293 @@ +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('style', $this->getStyle()); + $slicePath->setAttribute('data-icinga-graph-type', 'pieslice'); + + $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 @@ +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..0145e07 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Rect.php @@ -0,0 +1,105 @@ +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)); + $rect->setAttribute('style', $this->getStyle()); + + $this->applyAttributes($rect); + $this->appendAnimation($rect, $ctx); + + return $rect; + } +} diff --git a/library/Icinga/Chart/Primitive/Styleable.php b/library/Icinga/Chart/Primitive/Styleable.php new file mode 100644 index 0000000..678b940 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Styleable.php @@ -0,0 +1,154 @@ +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 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 content of the style attribute as a string + * + * @return string A string containing styles + */ + public function getStyle() + { + $base = sprintf("fill: %s; stroke: %s;stroke-width: %s;", $this->fill, $this->strokeColor, $this->strokeWidth); + $base .= ';' . $this->additionalStyle . ';'; + return $base; + } + + /** + * 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..e647f40 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Text.php @@ -0,0 +1,168 @@ +x = $x; + $this->y = $y; + $this->text = $text; + $this->fontSize = $fontSize; + } + + /** + * 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)); + $text->setAttribute( + 'style', + $this->getStyle() + . ';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 + ); + + $text->setAttribute('y', Format::formatSVGNumber($y)); + $text->appendChild(new DOMText($this->text)); + 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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() + { + return 100 * (key($this->labels) / count($this->labels)); + } + + /** + * Move to next tick + */ + public function next() + { + next($this->labels); + } + + /** + * Return the current tick caption + * + * @return string + */ + public function key() + { + return current($this->labels); + } + + /** + * Return true when the iterator is in a valid range + * + * @return bool + */ + public function valid() + { + return current($this->labels) !== false; + } + + /** + * Rewind the internal array + */ + public function rewind() + { + 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 @@ +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..90cad57 --- /dev/null +++ b/library/Icinga/Chart/Unit/LogarithmicUnit.php @@ -0,0 +1,263 @@ + + * this article 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() + { + return $this->currentTick * (100 / $this->getTicks()); + } + + /** + * Calculate the next tick and tick value + */ + public function next() + { + ++ $this->currentTick; + } + + /** + * Return the label for the current tick + * + * @return string The label for the current tick + */ + public function key() + { + $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() + { + return $this->currentTick >= 0 && $this->currentTick < $this->getTicks(); + } + + /** + * Reset the current tick and label value + */ + public function rewind() + { + $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..d563091 --- /dev/null +++ b/library/Icinga/Chart/Unit/StaticAxis.php @@ -0,0 +1,129 @@ +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 >= 5.0.0)
    + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + */ + public function current() + { + return 1 + (99 / count($this->items) * key($this->items)); + } + + /** + * (PHP 5 >= 5.0.0)
    + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + */ + public function next() + { + return next($this->items); + } + + /** + * (PHP 5 >= 5.0.0)
    + * 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. + */ + public function key() + { + return current($this->items); + } + + /** + * (PHP 5 >= 5.0.0)
    + * 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() + { + return current($this->items) !== false; + } + + /** + * (PHP 5 >= 5.0.0)
    + * 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() + { + return reset($this->items); + } + + /** + * Get the amount of ticks of this axis + * + * @return int + */ + public function getTicks() + { + return count($this->items); + } +} -- cgit v1.2.3