From 45bf37b08521b720f77fe50a06b85d8af8aa7cb0 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Mon, 9 Sep 2013 13:55:29 +0200 Subject: [PATCH] Add library/Icinga/Chart skeleton refs #4614 --- library/Icinga/Chart/Axis.php | 198 ++++++++++++++++++ library/Icinga/Chart/Chart.php | 148 +++++++++++++ library/Icinga/Chart/Graph/BarGraph.php | 79 +++++++ library/Icinga/Chart/Graph/LineGraph.php | 90 ++++++++ library/Icinga/Chart/GridChart.php | 198 ++++++++++++++++++ library/Icinga/Chart/Legend.php | 123 +++++++++++ library/Icinga/Chart/Palette.php | 15 ++ library/Icinga/Chart/PieChart.php | 11 + library/Icinga/Chart/Primitive/Canvas.php | 88 ++++++++ library/Icinga/Chart/Primitive/Circle.php | 62 ++++++ library/Icinga/Chart/Primitive/Drawable.php | 36 ++++ library/Icinga/Chart/Primitive/Line.php | 65 ++++++ library/Icinga/Chart/Primitive/Path.php | 82 ++++++++ library/Icinga/Chart/Primitive/Rect.php | 66 ++++++ library/Icinga/Chart/Primitive/Styleable.php | 77 +++++++ library/Icinga/Chart/Primitive/Text.php | 87 ++++++++ library/Icinga/Chart/Render/LayoutBox.php | 145 +++++++++++++ library/Icinga/Chart/Render/RenderContext.php | 119 +++++++++++ library/Icinga/Chart/SVGRenderer.php | 175 ++++++++++++++++ library/Icinga/Chart/Unit/AxisUnit.php | 37 ++++ library/Icinga/Chart/Unit/LinearAxis.php | 135 ++++++++++++ .../controllers/ChartController.php | 121 +++++++++++ .../views/scripts/chart/test.phtml | 6 + 23 files changed, 2163 insertions(+) create mode 100644 library/Icinga/Chart/Axis.php create mode 100644 library/Icinga/Chart/Chart.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/GridChart.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/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/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/SVGRenderer.php create mode 100644 library/Icinga/Chart/Unit/AxisUnit.php create mode 100644 library/Icinga/Chart/Unit/LinearAxis.php create mode 100644 modules/monitoring/application/controllers/ChartController.php create mode 100644 modules/monitoring/application/views/scripts/chart/test.phtml diff --git a/library/Icinga/Chart/Axis.php b/library/Icinga/Chart/Axis.php new file mode 100644 index 000000000..a924decb6 --- /dev/null +++ b/library/Icinga/Chart/Axis.php @@ -0,0 +1,198 @@ +xUnit->addValues($dataset, 0); + $this->yUnit->addValues($dataset, 1); + } + + + public function setUnitForXAxis(AxisUnit $unit) + { + $this->xUnit = $unit; + return $this; + } + + public function setUnitForYAxis(AxisUnit $unit) + { + $this->yUnit = $unit; + return $this; + } + + public function getApproximateLabelSize() + { + return strlen($this->getMax()); + } + + public function toSvg(RenderContext $ctx) + { + $group = $ctx->getDocument()->createElement('g'); + $this->renderHorizontal($ctx, $group); + $this->renderVertical($ctx, $group); + return $group; + } + + public function getRequiredPadding() { + return array(5, 10, 5, 10); + } + + private function renderHorizontal(RenderContext $ctx, \DOMElement $group) + { + $line = new Line(0, 100, 100, 100); + $line->setStrokeWidth(2); + $group->appendChild($line->toSvg($ctx)); + foreach ($this->xUnit as $label => $pos) { + $tick = new Line($pos, 100, $pos, 102); + $group->appendChild($tick->toSvg($ctx)); + + $labelField = new Text($pos+0.5, 105, $label); + $labelField->setAlignment(Text::ALIGN_MIDDLE) + ->setFontSize('2em'); + + $group->appendChild($labelField->toSvg($ctx)); + + if ($this->drawYGrid) { + $bgLine = new Line($pos, 0, $pos, 102); + $bgLine->setStrokeWidth(0.5) + ->setStrokeColor('#232'); + $group->appendChild($bgLine->toSvg($ctx)); + } + } + + if ($this->xLabel) { + $label = new Text(50, 110, $this->xLabel); + $label->setFontSize('1.7em') + ->setAlignment(Text::ALIGN_MIDDLE); + $group->appendChild($label->toSvg($ctx)); + } + + + } + + private function renderVertical(RenderContext $ctx, \DOMElement $group) + { + $line = new Line(0, 0, 0, 100); + $line->setStrokeWidth(2); + $group->appendChild($line->toSvg($ctx)); + + foreach ($this->yUnit as $label => $pos) { + $pos = 100 - $pos; + $tick = new Line(0, $pos, -1, $pos); + $group->appendChild($tick->toSvg($ctx)); + + $labelField = new Text(-0.5, $pos+0.5, $label); + $labelField->setFontSize('2em') + ->setAlignment(Text::ALIGN_END); + + $group->appendChild($labelField->toSvg($ctx)); + if ($this->drawXGrid) { + $bgLine = new Line(0, $pos, 100, $pos); + $bgLine->setStrokeWidth(0.5) + ->setStrokeColor('#343'); + $group->appendChild($bgLine->toSvg($ctx)); + + } + } + + if ($this->yLabel) { + $label = new Text(-5, 50, $this->yLabel); + $label->setFontSize('1.7em') + ->setAdditionalStyle(Text::ORIENTATION_VERTICAL) + ->setAlignment(Text::ALIGN_MIDDLE); + + $group->appendChild($label->toSvg($ctx)); + } + } + + public static function createLinearAxis() + { + $axis = new Axis(); + $axis->setUnitForXAxis(new LinearAxis()); + $axis->setUnitForYAxis(new LinearAxis()); + return $axis; + } + + public function setXLabel($label) + { + $this->xLabel = $label; + return $this; + } + + public function setYLabel($label) + { + $this->yLabel = $label; + return $this; + } + + public function setXMin($xMin) + { + $this->xUnit->setMin($xMin); + return $this; + } + + public function setYMin($yMin) + { + $this->yUnit->setMin($yMin); + return $this; + } + + public function setXMax($xMax) + { + $this->xUnit->setMax($xMax); + return $this; + } + + public function setYMax($yMax) + { + $this->yUnit->setMax($yMax); + return $this; + } + + + 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; + } + +} + diff --git a/library/Icinga/Chart/Chart.php b/library/Icinga/Chart/Chart.php new file mode 100644 index 000000000..2e28c4601 --- /dev/null +++ b/library/Icinga/Chart/Chart.php @@ -0,0 +1,148 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Chart; + +use Exception; +use Iterator; +use Icinga\Chart\Legend; +use Icinga\Chart\Palette; +use Icinga\Chart\Primitive\Drawable; +use Icinga\Chart\SVGRenderer; + +/** + * Base class for charts + */ +abstract class Chart implements Drawable +{ + private $xAxis; + + private $yAxis; + + /** + * Series to plot + * + * @var array + */ + protected $dataSets = array(); + + /** + * SVG renderer + * + * @var SVGRenderer + */ + protected $renderer; + + /** + * Legend to use for this chart + * + * @var Legend + */ + public $legend; + + /** + * The style-palette for this chart + * + * @var Palette + */ + public $palette; + + /** + * Create a new chart + * + * See self::isValidDataFormat() for more information on how each series need to be structured + * + * @param array $dataSet, ... unlimited number of series to plot + * + * @see self::isValidDataFormat() + */ + public function __construct() + { + $this->legend = new Legend(); + $this->palette = new Palette(); + $this->renderer = new SVGRenderer(2,1); + $this->init(); + } + + protected function init() + { + + } + + /** + * Set up the legend for this chart + */ + protected function setupLegend() + { + + } + + /** + * Set up the elements for this chart + */ + 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 Whether the dataset is valid or not + */ + public function isValidDataFormat() + { + return false; + } + + /** + * Disable the legend for this chart + */ + public function disableLegend() + { + $this->legend = null; + } + + + public function render() + { + if (!$this->isValidDataFormat()) { + throw new Exception('Dataset for graph doesn\'t have the proper structure'); + } + $this->build(); + + $this->renderer->getCanvas()->addElement($this); + return $this->renderer->render(); + } + + + + +} diff --git a/library/Icinga/Chart/Graph/BarGraph.php b/library/Icinga/Chart/Graph/BarGraph.php new file mode 100644 index 000000000..3eec2054f --- /dev/null +++ b/library/Icinga/Chart/Graph/BarGraph.php @@ -0,0 +1,79 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Graph; + + +use Icinga\Chart\Primitive\Drawable; +use Icinga\Chart\Primitive\Rect; +use Icinga\Chart\Primitive\Styleable; +use Icinga\Chart\Render\RenderContext; + +class BarGraph extends Styleable implements Drawable +{ + private $dataSet; + public $fill = 'green'; + public function __construct(array $dataSet) + { + $this->dataSet = $dataSet; + } + + public function toSvg(RenderContext $ctx) + { + $doc = $ctx->getDocument(); + $group = $doc->createElement('g'); + foreach($this->dataSet as $point) + { + $rect = new Rect($point[0]-2, $point[1], 4, 100- $point[1]); + $rect->setFill($this->fill); + $rect->setStrokeWidth($this->strokeWidth); + $rect->setStrokeColor($this->strokeColor); + + $rect->setAdditionalStyle('clip-path: url(#clip);'); + + $group->appendChild($rect->toSvg($ctx)); + } + return $group; + } + + public function setStyleFromConfig($cfg) + { + foreach ($cfg as $elem => $value) { + if ($elem === 'color') { + $this->setStrokeColor($value); + } else if ($elem === 'width') { + $this->setStrokeWidth($value); + } else if ($elem === 'showPoints') { + $this->setShowDataPoints($value); + } else if ($elem == 'fill') { + $this->setFill($value); + } + } + } +} \ No newline at end of file diff --git a/library/Icinga/Chart/Graph/LineGraph.php b/library/Icinga/Chart/Graph/LineGraph.php new file mode 100644 index 000000000..406c4bb8e --- /dev/null +++ b/library/Icinga/Chart/Graph/LineGraph.php @@ -0,0 +1,90 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Graph; + +use Icinga\Chart\Primitive\Drawable; +use Icinga\Chart\Primitive\Path; +use Icinga\Chart\Primitive\Circle; +use Icinga\Chart\Primitive\Styleable; +use Icinga\Chart\Render\RenderContext; + +class LineGraph extends Styleable implements Drawable +{ + + private $showDataPoints = false; + + public function __construct(array $dataset) + { + usort($dataset, array($this, 'sortByX')); + $this->dataset = $dataset; + } + + public function setShowDataPoints($bool) + { + $this->showDataPoints = true; + } + + private function sortByX(array $v1, array $v2) { + if($v1[0] === $v2[0]) { + return 0; + } + return ($v1[0] < $v2[0]) ? -1 : 1; + } + + public function toSvg(RenderContext $ctx) + { + $path = new Path($this->dataset); + $path->setStrokeColor($this->strokeColor); + $path->setStrokeWidth($this->strokeWidth); + $group = $path->toSvg($ctx); + if ($this->showDataPoints === true) { + foreach ($this->dataset as $point) { + $dot = new Circle($point[0], $point[1], 25); + $dot->setFill($this->strokeColor); + $group->appendChild($dot->toSvg($ctx)); + } + } + return $group; + } + + public function setStyleFromConfig($cfg) + { + foreach ($cfg as $elem=>$value) { + if ($elem === 'color') { + $this->setStrokeColor($value); + } else if ($elem === 'width') { + $this->setStrokeWidth($value); + } else if ($elem === 'showPoints') { + $this->setShowDataPoints($value); + } + } + } + +} \ No newline at end of file diff --git a/library/Icinga/Chart/GridChart.php b/library/Icinga/Chart/GridChart.php new file mode 100644 index 000000000..e8111ce9d --- /dev/null +++ b/library/Icinga/Chart/GridChart.php @@ -0,0 +1,198 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Chart; + +use Icinga\Chart\Chart; +use Icinga\Chart\Axis; +use Icinga\Chart\Graph\BarGraph; +use Icinga\Chart\Graph\LineGraph; +use Icinga\Chart\Primitive\Canvas; +use Icinga\Chart\Primitive\Rect; +use Icinga\Chart\Primitive\Path; +use Icinga\Chart\Render\LayoutBox; +use Icinga\Chart\Render\RenderContext; + +/** + * Base class for grid based charts + */ +class GridChart extends Chart +{ + const TYPE_LINE = "LINE"; + const TYPE_BAR = "BAR"; + + private $graphs = array(); + private $axis = array(); + + public function isValidDataFormat() + { + foreach ($this->graphs as $axis => $values) { + foreach ($values as $value) { + if (!isset($value['data']) || !is_array($value['data'])) { + return false; + } + } + } + return true; + } + + + private function configureAxisFromDatasets() + { + foreach ($this->graphs as $axis => &$lines) { + $axisObj = $this->axis[$axis]; + foreach ($lines as &$line) { + $axisObj->addDataset($line); + } + } + } + + public function drawLines($axis /*,...*/) + { + $this->draw(self::TYPE_LINE, func_get_args()); + + } + + public function drawBars($axis) + { + $this->draw(self::TYPE_BAR, func_get_args()); + + } + + 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; + + $this->graphs[$axisName][] = $graph; + $this->legend->addDataset($graph); + } + } + + public function setAxisLabel($xAxisLabel, $yAxisLabel, $axisName = 'default') + { + $this->axis[$axisName]->setXLabel($xAxisLabel)->setYLabel($yAxisLabel); + return $this; + } + + protected function build() + { + $this->configureAxisFromDatasets(); + } + + protected function init() + { + $this->setAxis(Axis::createLinearAxis()); + } + + public function setAxis(Axis $axis, $name = 'default') + { + $this->axis = array($name => $axis); + return $this; + } + + public function addAxis(Axis $axis, $name) + { + $this->axis[$name] = $axis; + return $this; + } + + public function setAxisMin($xMin = null, $yMin = null, $axisName = 'default') + { + $this->axis[$axisName]->setXMin($xMin)->setYMin($yMin); + return $this; + } + + public function setAxisMax($xMax = null, $yMax = null, $axisName = 'default') + { + $this->axis[$axisName]->setXMax($xMax)->setYMax($yMax); + return $this; + } + + public function toSvg(RenderContext $ctx) + { + $outerBox = new Canvas('outerGraph', new LayoutBox(0, 0, 100 , 100)); + $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 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]); + $clipBox = new Canvas('clip', new LayoutBox(0,0,100,100)); + $clipBox->toClipPath(); + $innerBox->addElement($clipBox); + + $clipBox->addElement(new Rect(0,0,100,100)); + $outerBox->addElement($innerBox); + $outerBox->addElement($this->legend); + return $outerBox->toSvg($ctx); + } + + private function renderGraphContent($innerBox) + { + foreach ($this->graphs as $axisName => $graphs) { + $axis = $this->axis[$axisName]; + foreach ($graphs as $graph) { + switch ($graph['graphType']) { + case self::TYPE_BAR: + $this->renderBars($axis, $graph, $innerBox); + break; + case self::TYPE_LINE: + $this->renderLines($axis, $graph, $innerBox); + break; + } + } + } + } + + private function renderLines($axis, array $graph, Canvas $innerBox) + { + $path = new LineGraph($axis->transform($graph['data'])); + $path->setStyleFromConfig($graph); + $innerBox->addElement($path); + + } + + private function renderBars($axis, array $graph, Canvas $innerBox) + { + $path = new BarGraph($axis->transform($graph['data'])); + $innerBox->addElement($path); + } +} diff --git a/library/Icinga/Chart/Legend.php b/library/Icinga/Chart/Legend.php new file mode 100644 index 000000000..0a57628ba --- /dev/null +++ b/library/Icinga/Chart/Legend.php @@ -0,0 +1,123 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Chart; + +use Icinga\Chart\Palette; +use Icinga\Chart\Primitive\Canvas; +use Icinga\Chart\Primitive\Drawable; +use Icinga\Chart\Primitive\Rect; +use Icinga\Chart\Primitive\Text; +use Icinga\Chart\Render\LayoutBox; +use Icinga\Chart\Render\RenderContext; +use Icinga\Chart\SVGRenderer; + +class Legend implements Drawable +{ + private $internalCtr = 0; + /** + * + * Content of this legend + * + * @var array + */ + private $dataset = array(); + + /** + * Maximum space this legend can take in percent + * + * Is intelligently applied depending on the location of the legend. + * + * @var float + */ + public $maxVolume = 0.05; + + /** + * Where this legend is displayed + * + * Possible values are: left, right, top, bottom + * + * @var string + */ + public $location = 'bottom'; + + /** + * The style-palette this legend is using + * + * @var Palette + */ + public $palette; + + /** + * Create a new legend + */ + public function __construct() + { + + } + + /** + * Set the content to be displayed by this legend + * + * @param array $data Array of key-value pairs representing the labels and their colour + */ + public function addDataset(array $dataset) + { + if (!isset($dataset['label'])) { + $dataset['label'] = 'Dataset ' . (++$this->internalCtr); + } + $this->dataset[$dataset['color']] = $dataset['label']; + + } + + function toSvg(RenderContext $ctx) + { + $outer = new Canvas('legend', new LayoutBox(0, 95, 100, 100)); + $outer->getLayout()->setPadding(2,10,2,10); + + $step = 100/count($this->dataset); + $top = 0; + + foreach ($this->dataset as $color => $text) { + $colorBox = new Rect($top, 0, 2, 2); + $colorBox->setFill($color); + $colorBox->setStrokeWidth(2); + $outer->addElement($colorBox); + $textBox = new Text($top+5 , 1.8 , $text); + $outer->addElement($textBox); + $top += $step; + } + $ctx->keepRatio(); + $svg = $outer->toSvg($ctx); + $ctx->ignoreRatio(); + + return $svg; + } + + +} diff --git a/library/Icinga/Chart/Palette.php b/library/Icinga/Chart/Palette.php new file mode 100644 index 000000000..3ca70ab41 --- /dev/null +++ b/library/Icinga/Chart/Palette.php @@ -0,0 +1,15 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Primitive; + +use Icinga\Chart\Render\LayoutBox; +use Icinga\Chart\Render\RenderContext; + +use Icinga\Util\Dimension; + +class Canvas implements Drawable { + + private $name; + private $children = array(); + private $isClipPath = false; + private $keepRatio = false; + + public function __construct($name, LayoutBox $rect) { + $this->rect = $rect; + $this->name = $name; + } + + public function toClipPath() + { + $this->isClipPath = true; + } + + 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); + $element->setAttribute('transform', $this->rect->getOuterTranslate($ctx)); + + foreach($this->children as $child) { + $innerContainer->appendChild($child->toSvg($ctx)); + } + + return $outer; + } + + + public function getLayout() { + return $this->rect; + } + + public function addElement(Drawable $child) { + $this->children[] = $child; + } +} \ No newline at end of file diff --git a/library/Icinga/Chart/Primitive/Circle.php b/library/Icinga/Chart/Primitive/Circle.php new file mode 100644 index 000000000..29fbe3666 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Circle.php @@ -0,0 +1,62 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Primitive; + + +use Icinga\Chart\Render\RenderContext; + +class Circle extends Styleable implements Drawable +{ + private $x; + private $y; + private $radius; + + + + public function __construct($x, $y, $radius) + { + $this->x = $x; + $this->y = $y; + $this->radius = $radius; + } + + + public function toSvg(RenderContext $ctx) + { + $coords = $ctx->toAbsolute($this->x, $this->y); + $circle = $ctx->getDocument()->createElement('circle'); + $circle->setAttribute('cx', $coords[0]); + $circle->setAttribute('cy', $coords[1]); + $circle->setAttribute('r', 5); + $circle->setAttribute('style', $this->getStyle()); + return $circle; + } + +} \ No newline at end of file diff --git a/library/Icinga/Chart/Primitive/Drawable.php b/library/Icinga/Chart/Primitive/Drawable.php new file mode 100644 index 000000000..9feed329f --- /dev/null +++ b/library/Icinga/Chart/Primitive/Drawable.php @@ -0,0 +1,36 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Primitive; +use Icinga\Chart\Render\RenderContext; + +interface Drawable { + + function toSvg(RenderContext $ctx); +} \ No newline at end of file diff --git a/library/Icinga/Chart/Primitive/Line.php b/library/Icinga/Chart/Primitive/Line.php new file mode 100644 index 000000000..11cfc544e --- /dev/null +++ b/library/Icinga/Chart/Primitive/Line.php @@ -0,0 +1,65 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Primitive; + + +use Icinga\Chart\Render\RenderContext; + +class Line extends Styleable implements Drawable { + public $strokeWidth = 1; + + private $xStart = 0; + private $xEnd = 0; + private $yStart = 0; + private $yEnd = 0; + + + public function __construct($x1, $y1, $x2, $y2) + { + $this->xStart = $x1; + $this->xEnd = $x2; + $this->yStart = $y1; + $this->yEnd = $y2; + } + + 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', $x1); + $line->setAttribute('x2', $x2); + $line->setAttribute('y1', $y1); + $line->setAttribute('y2', $y2); + $line->setAttribute('style', $this->getStyle()); + return $line; + } +} \ No newline at end of file diff --git a/library/Icinga/Chart/Primitive/Path.php b/library/Icinga/Chart/Primitive/Path.php new file mode 100644 index 000000000..8ddb504af --- /dev/null +++ b/library/Icinga/Chart/Primitive/Path.php @@ -0,0 +1,82 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Primitive; + + +use Icinga\Chart\Render\RenderContext; + +class Path extends Styleable implements Drawable { + public $strokeWidth = 1; + + protected $points = array(); + protected $smooth = false; + + const TPL_MOVE = "M %s %s "; + const TPL_BEZIER = "S %s %s "; + const TPL_STRAIGHT = "L %s %s "; + + public function __construct(array $points) + { + $this->append($points); + } + + public function append(array $points) + { + $this->points += $points; + return $this; + } + + public function setSmooth($bool) + { + $this->smooth = $bool; + } + + + public function toSvg(RenderContext $ctx) { + $doc = $ctx->getDocument(); + $group = $doc->createElement('g'); + + $pathDescription = ''; + $tpl = self::TPL_MOVE; + + foreach ($this->points as $point) { + $coords = $ctx->toAbsolute($point[0], $point[1]); + $pathDescription .= vsprintf($tpl, $coords); + + $tpl = self::TPL_STRAIGHT; + } + $path = $doc->createElement('path'); + $path->setAttribute('d', $pathDescription); + $path->setAttribute('style', $this->getStyle()); + $group->appendChild($path); + return $group; + } + +} \ No newline at end of file diff --git a/library/Icinga/Chart/Primitive/Rect.php b/library/Icinga/Chart/Primitive/Rect.php new file mode 100644 index 000000000..22c9c9367 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Rect.php @@ -0,0 +1,66 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Primitive; + +use Icinga\Chart\Render\RenderContext; + +class Rect extends Styleable implements Drawable +{ + + private $x; + private $y; + private $width; + private $height; + + + public function __construct($x, $y, $width, $height) + { + $this->x = $x; + $this->y = $y; + $this->width = $width; + $this->height = $height; + } + + + public function toSvg(RenderContext $ctx) + { + $doc = $ctx->getDocument(); + $rect = $doc->createElement('rect'); + + list($x, $y) = $ctx->toAbsolute($this->x, $this->y); + list($width, $height) = $ctx->toAbsolute($this->width, $this->height); + $rect->setAttribute('x', $x); + $rect->setAttribute('y', $y); + $rect->setAttribute('width', $width); + $rect->setAttribute('height', $height); + $rect->setAttribute('style', $this->getStyle()); + return $rect; + } +} \ No newline at end of file diff --git a/library/Icinga/Chart/Primitive/Styleable.php b/library/Icinga/Chart/Primitive/Styleable.php new file mode 100644 index 000000000..e802c057b --- /dev/null +++ b/library/Icinga/Chart/Primitive/Styleable.php @@ -0,0 +1,77 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Primitive; + + +class Styleable { + + public $strokeWidth = 0; + public $strokeColor = '#000'; + public $fill = 'none'; + public $additionalStyle = ''; + public $opacity = '1'; + + + /** + * @param mixed $stroke + */ + public function setStrokeWidth($width) + { + $this->strokeWidth = $width; + return $this; + } + + public function setStrokeColor($color) + { + $this->strokeColor = $color ? $color : 'none'; + return $this; + } + + public function setAdditionalStyle($styles) + { + $this->additionalStyle = $styles; + return $this; + } + + public function setFill($color = null) { + $this->fill = $color ? $color : 'none'; + return $this; + } + + public function getStyle() + { + $base = sprintf("fill: %s; stroke: %s;stroke-width: %s;", $this->fill, $this->strokeColor, $this->strokeWidth); + $base .= ';' . $this->additionalStyle . ';'; + return $base; + } + + + +} \ No newline at end of file diff --git a/library/Icinga/Chart/Primitive/Text.php b/library/Icinga/Chart/Primitive/Text.php new file mode 100644 index 000000000..af0b9a084 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Text.php @@ -0,0 +1,87 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Primitive; + + +use Icinga\Chart\Render\RenderContext; +use DOMText; + +class Text extends Styleable implements Drawable +{ + const ALIGN_END = 'end'; + const ALIGN_START = 'start'; + const ALIGN_MIDDLE = 'middle'; + + + const ORIENTATION_HORIZONTAL = ""; + const ORIENTATION_VERTICAL = "writing-mode: tb;"; + + private $x; + private $y; + private $text; + private $fontSize = '1.5em'; + public $fill = '#000'; + private $alignment = self::ALIGN_START; + + public function __construct($x, $y, $text, $fontSize = '1.5em') + { + $this->x = $x; + $this->y = $y; + $this->text = $text; + } + + public function setFontSize($size) + { + $this->fontSize = $size; + return $this; + } + + public function setAlignment($align) + { + $this->alignment = $align; + return $this; + } + + + public function toSvg(RenderContext $ctx) + { + list($x, $y) = $ctx->toAbsolute($this->x, $this->y); + $text = $ctx->getDocument()->createElement('text'); + + $text->setAttribute('x', $x-15); + $text->setAttribute('style', $this->getStyle() . ';font-size:' . $this->fontSize . '; font-family: Verdana, serif;' + . 'font-weight: normal; font-style: normal;' + . 'text-anchor: ' . $this->alignment); + $text->setAttribute('y', $y); + $text->appendChild(new DOMText($this->text)); + + return $text; + } +} \ No newline at end of file diff --git a/library/Icinga/Chart/Render/LayoutBox.php b/library/Icinga/Chart/Render/LayoutBox.php new file mode 100644 index 000000000..58643c80d --- /dev/null +++ b/library/Icinga/Chart/Render/LayoutBox.php @@ -0,0 +1,145 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Render; + + +class LayoutBox +{ + private $height; + private $width; + private $x; + private $y; + private $padding = array(0,0,0,0); + private $margin = array(0,0,0,0); + + + public function __construct($x, $y, $width = null, $height = null) + { + $this->height = $height ? $height : 100; + $this->width = $width ? $width : 100; + $this->x = $x; + $this->y = $y; + } + + public function setUniformPadding($padding) + { + $this->padding = array($padding, $padding, $padding, $padding); + } + + public function setPadding($top, $right, $bottom, $left) + { + $this->padding = array($top, $right, $bottom, $left); + } + + public function setUniformMargin($margin) + { + $this->margin = array($margin, $margin, $margin, $margin); + } + + public function setMargin($top, $right, $bottom, $left) + { + $this->margin = array($top, $right, $bottom, $left); + } + + public function getOuterTranslate(RenderContext $ctx) + { + list($marginX, $marginY) = $ctx->toAbsolute($this->margin[3], $this->margin[0]); + return sprintf('translate(%s, %s)', $marginX, $marginY); + } + + public function getInnerTransform(RenderContext $ctx) + { + list($translateX, $translateY) = $ctx->toAbsolute($this->padding[3] + $this->getX(), $this->padding[0] + $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)', $translateX, $translateY, $scaleX, $scaleY); + } + + public function __toString() + { + return sprintf("Rectangle: x: %s y: %s, height: %s, width: %s\n", $this->x, $this->y, $this->height, $this->width); + } + + /** + * @return array + */ + public function getMargin() + { + return $this->margin; + } + + /** + * @return array + */ + public function getPadding() + { + return $this->padding; + } + + /** + * @return \Icinga\Util\Dimension + */ + public function getHeight() + { + return $this->height+($this->margin[0] + $this->margin[2]); + } + + /** + * @return \Icinga\Util\Dimension + */ + public function getWidth() + { + return $this->width-($this->margin[1] + $this->margin[3]); + } + + /** + * @return \Icinga\Util\Dimension + */ + public function getX() + { + return $this->x + $this->margin[1]; + } + + /** + * @return \Icinga\Util\Dimension + */ + public function getY() + { + return $this->y + $this->margin[0]; + } + + public function getRatio() + { + return $this->width->getValue()/$this->height->getValue(); + } + +} \ No newline at end of file diff --git a/library/Icinga/Chart/Render/RenderContext.php b/library/Icinga/Chart/Render/RenderContext.php new file mode 100644 index 000000000..c9d38a129 --- /dev/null +++ b/library/Icinga/Chart/Render/RenderContext.php @@ -0,0 +1,119 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Render; + +use Icinga\Util\Dimension; +use DOMDocument; + +class RenderContext { + + private $viewBoxSize = array(1000, 1000); + private $padding = array(0, 0); + + private $document; + private $ratio; + private $respectRatio = false; + + private $paddingFacX = 1; + private $paddingFacY = 1; + + public function __construct(DOMDocument $document, $width, $height) + { + $this->document = $document; + $this->ratio = $width/$height; + } + + public function setViewBoxSize($x, $y) + { + $this->viewBoxSize = array($x, $y); + } + + /** + * @return mixed + */ + public function getDocument() + { + return $this->document; + } + + /** + * @return mixed + */ + public function getNrOfUnitsX() + { + return intval($this->viewBoxSize[0] * $this->ratio); + } + + public function setPadding($horizontal, $vertical) { + $this->padding = array($horizontal, $vertical); + + + } + + public function getPadding() + { + return array($this->paddingFacX, $this->paddingFacY); + } + + public function keepRatio() + { + $this->respectRatio = true; + } + + public function ignoreRatio() + { + $this->respectRatio = false; + } + + /** + * @return mixed + */ + public function getNrOfUnitsY() + { + return $this->viewBoxSize[1]; + } + + public function toAbsolute($x, $y) + { + return array( + $this->getNrOfUnitsX() / 100 * $x / ($this->respectRatio ? $this->ratio : 1),// * $this->paddingFacX, + $this->getNrOfUnitsY() / 100 * $y// * $this->paddingFacY + ); + } + + + public function paddingToScaleFactor(array $padding) { + list($horizontalPadding, $verticalPadding) = $this->toAbsolute($padding[1] + $padding[3], $padding[0] + $padding[2]); + return array( + ($this->getNrOfUnitsX() - $horizontalPadding) / $this->getNrOfUnitsX(), + ($this->getNrOfUnitsY() - $verticalPadding) / $this->getNrOfUnitsY() + ); + } +} \ No newline at end of file diff --git a/library/Icinga/Chart/SVGRenderer.php b/library/Icinga/Chart/SVGRenderer.php new file mode 100644 index 000000000..ea77a0d51 --- /dev/null +++ b/library/Icinga/Chart/SVGRenderer.php @@ -0,0 +1,175 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Chart; + +use DOMNode; +use DOMDocument; +use DOMImplementation; +use Exception; +use Icinga\Util\Dimension; +use Icinga\Chart\Render\LayoutBox; +use Icinga\Chart\Render\RenderContext; +use Icinga\Chart\Primitive\Canvas; + +class SVGRenderer +{ + /** + * The XML-document + * + * @var DOMDocument + */ + private $document; + + /** + * The SVG-element + * + * @var DOMNode + */ + private $svg; + + /** + * The root layer for all elements + * + * @var DOMNode + */ + private $rootCanvas; + + /** + * The position and dimension of each layer + * + * @var array + */ + private $layerInfo = array(); + + private $width = 100; + + private $height = 100; + + + private function createRootDocument() + { + $implementation = new DOMImplementation(); + $docType = $implementation->createDocumentType( + 'svg', + '-//W3C//DTD SVG 1.1//EN', + 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' + ); + + $this->document = $implementation->createDocument(null, null, $docType); + $this->svg = $this->createOuterBox(); + $this->document->appendChild($this->svg); + + } + + private function setRootCanvas(Canvas $root) { + $this->rootCanvas = $root; + + } + + 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("width", "100%"); + $svg->setAttribute("height", "100%"); + $svg->setAttribute( + 'viewBox', + sprintf( + '0 0 %s %s', $ctx->getNrOfUnitsX(), $ctx->getNrOfUnitsY() + ) + ); + return $svg; + } + + /** + * Initialises the XML-document, SVG-element and this figure's root layer + */ + public function __construct($width, $height) { + $this->width = $width; + $this->height = $height; + $this->setRootCanvas(new Canvas('root', new LayoutBox(0,0))); + } + + /** + * Render the XML-document + * + * @return string The resulting XML structure + */ + public function render() + { + $this->createRootDocument(); + $ctx = $this->createRenderContext(); + $this->svg->appendChild($this->rootCanvas->toSvg($ctx)); + $this->document->formatOutput = true; + return $this->document->saveXML(); + } + + public function createRenderContext() + { + return new RenderContext($this->document, $this->width, $this->height); + } + + + public function getCanvas() + { + return $this->rootCanvas; + } + + /** + * Draw a line + * + * TODO: Arguments + */ + public function line() + { + + } + + /** + * Draw a pie slice + * + * TODO: Arguments + */ + public function slice() + { + + } + + /** + * Draw a bar + * + * TODO: Arguments + */ + public function bar() + { + + } +} diff --git a/library/Icinga/Chart/Unit/AxisUnit.php b/library/Icinga/Chart/Unit/AxisUnit.php new file mode 100644 index 000000000..8cdd0ba94 --- /dev/null +++ b/library/Icinga/Chart/Unit/AxisUnit.php @@ -0,0 +1,37 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Unit; +use Iterator; + +interface AxisUnit extends Iterator { + + function addValues(array $dataset, $id=0); + function transform($value); +} \ No newline at end of file diff --git a/library/Icinga/Chart/Unit/LinearAxis.php b/library/Icinga/Chart/Unit/LinearAxis.php new file mode 100644 index 000000000..ac9225d18 --- /dev/null +++ b/library/Icinga/Chart/Unit/LinearAxis.php @@ -0,0 +1,135 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Chart\Unit; + + +class LinearAxis implements AxisUnit { + + private $min; + private $max; + + private $staticMin = false; + private $staticMax = false; + + private $nrOfTicks = 10; + + private $currentTick = 0; + private $currentValue = 0; + + public function __construct($nrOfTicks = 10) { + $this->min = PHP_INT_MAX; + $this->max = ~PHP_INT_MAX; + $this->nrOfTicks = $nrOfTicks; + } + + public function addValues(array $dataset, $idx=0) + { + + $datapoints = array(); + foreach($dataset['data'] as $points) { + $datapoints[] = $points[$idx]; + } + 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; + return $this; + } + + public function transform($value) + { + if ($value < $this->min) { + return 0; + } else if ($value > $this->max) { + return 100; + } else { + return 100 * ($value - $this->min) / ($this->max - $this->min); + } + } + + public function current() + { + return $this->currentTick; + } + + + public function next() + { + $this->currentTick += (100 / $this->nrOfTicks); + $this->currentValue += (($this->max - $this->min) / $this->nrOfTicks ); + } + + + public function key() + { + return $this->currentValue; + } + + + public function valid() + { + return $this->currentTick >= 0 && $this->currentTick <= 100; + } + + public function rewind() + { + $this->currentTick = 0; + $this->currentValue = $this->min; + } + + /** + * @param int $max + */ + public function setMax($max) + { + if ($max !== null) { + $this->max = $max; + $this->staticMax = true; + } + } + + /** + * @param int $min + */ + public function setMin($min) + { + if ($min !== null) { + $this->min = $min; + $this->staticMin = true; + } + } + +} \ No newline at end of file diff --git a/modules/monitoring/application/controllers/ChartController.php b/modules/monitoring/application/controllers/ChartController.php new file mode 100644 index 000000000..ff6c67cd3 --- /dev/null +++ b/modules/monitoring/application/controllers/ChartController.php @@ -0,0 +1,121 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +use Icinga\Application\Icinga; +use Icinga\Application\Config; +use Icinga\Application\Logger; +use Icinga\Web\Form; +use Icinga\Web\Controller\ActionController; +use Icinga\Chart\SVGRenderer; +use Icinga\Chart\GridChart; +use Icinga\Module\Monitoring\Backend; +use Icinga\Chart\Axis; + +/** + * Class Monitoring_CommandController + * + * Interface to send commands and display forms + */ + +class Monitoring_ChartController extends ActionController +{ + /** + * The backend used for this controller + * + * @var Backend + */ + protected $backend; + /** + * Set to a string containing the compact layout name to use when + * 'compact' is set as the layout parameter, otherwise null + * + * @var string + */ + private $compactView; + + /** + * Retrieve backend and hooks for this controller + * + * @see ActionController::init + */ + public function init() + { + $this->backend = Backend::getInstance($this->_getParam('backend')); + } + + public function testAction() { + $this->chart = new GridChart(); + $this->chart->setAxisLabel("X axis label", "Y axis label") + ->setAxisMin(null, 0); + $data1 = array(); + $data2 = array(); + $data3 = array(); + for ($i=0; $i<25; $i++) { + $data[] = array(1379344218+$i*10, rand(0,12)); + $data2[] = array(1379344218+$i*10, rand(4,30)); + $data3[] = array(1379344218+$i*10, rand(0,30)); + } + $this->chart->drawLines( + array( + 'label' => 'Nr of outtakes', + 'color' => 'red', + 'width' => '5', + + 'data' => $data + ), array( + 'label' => 'Some line', + 'color' => 'blue', + 'width' => '4', + + 'data' => $data3, + 'showPoints' => true + ) + ); + + $this->chart->drawBars( + array( + 'label' => 'Some other line', + 'color' => 'black', + 'data' => $data3, + 'showPoints' => true + ) + ); + + $this->chart->drawLines( + array( + 'label' => 'Nr of outtakes', + 'color' => 'yellow', + 'width' => '5', + + 'data' => $data2 + ) + ); + + $this->view->svg = $this->chart; + } +} \ No newline at end of file diff --git a/modules/monitoring/application/views/scripts/chart/test.phtml b/modules/monitoring/application/views/scripts/chart/test.phtml new file mode 100644 index 000000000..1c2046bdf --- /dev/null +++ b/modules/monitoring/application/views/scripts/chart/test.phtml @@ -0,0 +1,6 @@ +mah +
+render(); +?> +
\ No newline at end of file