From 4b55bcf8b6e86d56aa0ba50faf6166ca0a00e4d6 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch <matthias.jentsch@netways.de> Date: Tue, 2 Sep 2014 12:24:29 +0200 Subject: [PATCH] Add tooltips to bar charts Add a class to format and populate tooltips from graph data sets and implement those tooltips in the ChartController. --- library/Icinga/Chart/Graph/BarGraph.php | 104 ++++++++++--- library/Icinga/Chart/Graph/StackedGraph.php | 4 + library/Icinga/Chart/Graph/Tooltip.php | 144 ++++++++++++++++++ library/Icinga/Chart/GridChart.php | 40 ++++- .../controllers/ChartController.php | 23 ++- 5 files changed, 286 insertions(+), 29 deletions(-) create mode 100644 library/Icinga/Chart/Graph/Tooltip.php diff --git a/library/Icinga/Chart/Graph/BarGraph.php b/library/Icinga/Chart/Graph/BarGraph.php index 968e4c0f7..3d6a5c234 100644 --- a/library/Icinga/Chart/Graph/BarGraph.php +++ b/library/Icinga/Chart/Graph/BarGraph.php @@ -16,6 +16,13 @@ use Icinga\Chart\Render\RenderContext; */ class BarGraph extends Styleable implements Drawable { + /** + * The dataset order + * + * @var int + */ + private $order = 0; + /** * The width of the bars. * @@ -30,14 +37,37 @@ class BarGraph extends Styleable implements Drawable */ private $dataSet; + /** + * The tooltips + * + * @var + */ + private $tooltips; + + /** + * All graphs + * + * @var + */ + private $graphs; + /** * Create a new BarGraph with the given dataset * - * @param array $dataSet An array of datapoints + * @param array $dataSet An array of data points + * @param int $order The graph number displayed by this BarGraph + * @param array $tooltips The tooltips to display for each value */ - public function __construct(array $dataSet) - { + public function __construct( + array $dataSet, + array &$graphs, + $order, + array $tooltips = null + ) { + $this->order = $order; $this->dataSet = $dataSet; + $this->tooltips = $tooltips; + $this->graphs = $graphs; } /** @@ -56,6 +86,30 @@ class BarGraph extends Styleable implements Drawable } } + /** + * Draw a single rectangle + * + * @param array $point The + * @param null $index + * @param string $fill The fill color to use + * @param $strokeWidth + * + * @return Rect + */ + private function drawSingleBar($point, $index = null, $fill, $strokeWidth) + { + $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 * @@ -68,23 +122,33 @@ class BarGraph extends Styleable implements Drawable $doc = $ctx->getDocument(); $group = $doc->createElement('g'); $idx = 0; - 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('black'); - $rect->setAttribute('data-icinga-graph-index', $idx++); - $rect->setAttribute('data-icinga-graph-type', 'bar'); - $rect->setAdditionalStyle('clip-path: url(#clip);'); - /*$rect->setAnimation( - new Animation( - 'y', - $ctx->yToAbsolute(100), - $ctx->yToAbsolute($point[1]), - rand(1, 1.5)/2 - ) - );*/ - $group->appendChild($rect->toSvg($ctx)); + foreach ($this->dataSet as $x => $point) { + // add white background bar, to prevent other bars from altering transparency effects + $bar = $this->drawSingleBar($point, $idx++, 'white', $this->strokeWidth, $idx)->toSvg($ctx); + $group->appendChild($bar); + + // draw actual bar + $bar = $this->drawSingleBar($point, null, $this->fill, $this->strokeWidth, $idx)->toSvg($ctx); + $bar->setAttribute('class', 'chart-data'); + 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; + $bar->setAttribute( + 'title', + $this->tooltips[$x]->renderNoHtml($this->order, $data, $format) + ); + $bar->setAttribute( + 'title-rich', + $this->tooltips[$x]->render($this->order, $data, $format) + ); + } + $group->appendChild($bar); } return $group; } diff --git a/library/Icinga/Chart/Graph/StackedGraph.php b/library/Icinga/Chart/Graph/StackedGraph.php index ae4f593e2..4339b8c8d 100644 --- a/library/Icinga/Chart/Graph/StackedGraph.php +++ b/library/Icinga/Chart/Graph/StackedGraph.php @@ -41,6 +41,10 @@ class StackedGraph implements Drawable if (!isset($this->points[$x])) { $this->points[$x] = 0; } + // store old y-value for displaying the actual (non-aggregated) + // value in the tooltip + $point[2] = $point[1]; + $this->points[$x] += $point[1]; $point[1] = $this->points[$x]; } diff --git a/library/Icinga/Chart/Graph/Tooltip.php b/library/Icinga/Chart/Graph/Tooltip.php new file mode 100644 index 000000000..b630c7c61 --- /dev/null +++ b/library/Icinga/Chart/Graph/Tooltip.php @@ -0,0 +1,144 @@ +<?php +// {{{ICINGA_LICENSE_HEADER}}} +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Chart\Graph; + +/** + * A tooltip that stores and aggregates information about displayed data + * points of a graph and replaces them in a format string to render the description + * for specific data points of the graph. + * + * When render() is called, placeholders for the keys for each data entry will be replaced by + * the current value of this data set and the formatted string will be returned. + * The content of the replaced keys can change for each data set and depends on how the data + * is passed to this class. There are several types of properties: + * + * <ul> + * <li>Global properties</li>: Key-value pairs that stay the same every time render is called, and are + * passed to an instance in the constructor. + * <li>Aggregated properties</li>: Global properties that are created automatically from + * all attached data points. + * <li>Local properties</li>: Key-value pairs that only apply to a single data point and + * are passed to the render-function. + * </ul> + */ +class Tooltip +{ + /** + * The default format string used + * when no other format is specified + * + * @var string + */ + private $defaultFormat; + + /** + * All aggregated points + * + * @var array + */ + private $points = array(); + + /** + * Contains all static replacements + * + * @var array + */ + private $data = array( + 'sum' => 0 + ); + + /** + * Used to format the displayed tooltip. + * + * @var string + */ + protected $tooltipFormat; + + /** + * Create a new tooltip with the specified default format string + * + * Allows you to set the global data for this tooltip, that is displayed every + * time render is called. + * + * @param array $data Map of global properties + * @param string $format The default format string + */ + public function __construct ( + $data = array(), + $format = '<b>{title}</b></b><br> {value} of {sum} {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)); + } +} \ No newline at end of file diff --git a/library/Icinga/Chart/GridChart.php b/library/Icinga/Chart/GridChart.php index ef1682921..190e09bf9 100644 --- a/library/Icinga/Chart/GridChart.php +++ b/library/Icinga/Chart/GridChart.php @@ -10,6 +10,7 @@ use Icinga\Chart\Axis; use Icinga\Chart\Graph\BarGraph; use Icinga\Chart\Graph\LineGraph; use Icinga\Chart\Graph\StackedGraph; +use Icinga\Chart\Graph\Tooltip; use Icinga\Chart\Primitive\Canvas; use Icinga\Chart\Primitive\Rect; use Icinga\Chart\Primitive\Path; @@ -74,6 +75,16 @@ class GridChart extends Chart */ 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(); + /** * Check if the current dataset has the proper structure for this chart. * @@ -169,6 +180,26 @@ class GridChart extends Chart $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); + } + } } /** @@ -353,11 +384,16 @@ class GridChart extends Chart foreach ($this->graphs as $axisName => $graphs) { $axis = $this->axis[$axisName]; $graphObj = null; - foreach ($graphs as $graph) { + 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'])); + $graphObj = new BarGraph( + $axis->transform($graph['data']), + $graphs, + $dataset, + $this->tooltips + ); break; case self::TYPE_LINE: $graphObj = new LineGraph($axis->transform($graph['data'])); diff --git a/modules/monitoring/application/controllers/ChartController.php b/modules/monitoring/application/controllers/ChartController.php index 305ff92bc..af6240259 100644 --- a/modules/monitoring/application/controllers/ChartController.php +++ b/modules/monitoring/application/controllers/ChartController.php @@ -154,30 +154,35 @@ class Monitoring_ChartController extends Controller ->setXAxis(new \Icinga\Chart\Unit\StaticAxis()) ->setAxisMin(null, 0); + $tooltip = t('<b>{title}:</b><br />{value} of {sum} services are {label}'); $this->view->chart->drawBars( array( 'label' => t('Ok'), 'color' => '#44bb77', 'stack' => 'stack1', - 'data' => $okBars + 'data' => $okBars, + 'tooltip' => $tooltip ), array( 'label' => t('Warning'), 'color' => '#ffaa44', 'stack' => 'stack1', - 'data' => $warningBars + 'data' => $warningBars, + 'tooltip' => $tooltip ), array( 'label' => t('Critical'), 'color' => '#ff5566', 'stack' => 'stack1', - 'data' => $critBars + 'data' => $critBars, + 'tooltip' => $tooltip ), array( 'label' => t('Unknown'), 'color' => '#dd66ff', 'stack' => 'stack1', - 'data' => $unknownBars + 'data' => $unknownBars, + 'tooltip' => $tooltip ) ); } @@ -201,6 +206,7 @@ class Monitoring_ChartController extends Controller $hostgroup->hosts_unreachable_unhandled ); } + $tooltip = t('<b>{title}:</b><br /> {value} of {sum} hosts are {label}'); $this->view->chart = new GridChart(); $this->view->chart->alignTopLeft(); $this->view->chart->setAxisLabel('', t('Hosts')) @@ -211,19 +217,22 @@ class Monitoring_ChartController extends Controller 'label' => t('Up'), 'color' => '#44bb77', 'stack' => 'stack1', - 'data' => $upBars + 'data' => $upBars, + 'tooltip' => $tooltip ), array( 'label' => t('Down'), 'color' => '#ff5566', 'stack' => 'stack1', - 'data' => $downBars + 'data' => $downBars, + 'tooltip' => $tooltip ), array( 'label' => t('Unreachable'), 'color' => '#dd66ff', 'stack' => 'stack1', - 'data' => $unreachableBars + 'data' => $unreachableBars, + 'tooltip' => $tooltip ) ); }