Merge branch 'feature/svg-chart-tooltips-7024'
This commit is contained in:
commit
64b9c89692
|
@ -238,6 +238,77 @@ the labels to show you can use the 'disableLegend()' call on the GridChart objec
|
|||
|
||||
![Various Line Graph Options][graph7]
|
||||
|
||||
|
||||
### Tooltips
|
||||
|
||||
It is possible to specify custom tooltip format strings when creating bar charts.
|
||||
Tooltips provide information about the points of each bar chart column, by aggregating
|
||||
the values of all data sets with the same x-coordinate.
|
||||
|
||||
When no custom format string is given, a sane default format string is used, but its usually
|
||||
clearer for the user to describe the data of each chart more accurately with a custom one.
|
||||
|
||||
|
||||
**Example #9.1: Bar Charts with custom tooltips**
|
||||
|
||||
$this->chart->drawBars(
|
||||
array(
|
||||
'label' => 'Hosts critical',
|
||||
'palette' => Palette::PROBLEM,
|
||||
'stack' => 'stack1',
|
||||
'data' => $data2,
|
||||
'tooltip' => '{title}<br/> {value} of {sum} hosts are ok.'
|
||||
),
|
||||
array(
|
||||
'label' => 'Hosts warning',
|
||||
'stack' => 'stack1',
|
||||
'palette' => Palette::WARNING,
|
||||
'data' => $data,
|
||||
'tooltip' => '{title}<br/> Oh no, {value} of {sum} hosts are down!'
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
As you can see, you can specify a format string for each data set, which allows you to
|
||||
pass a custom message for all "down" hosts, one custom message for all "Ok" hosts and so on.
|
||||
In contrast to that, the aggregation of values works on a column basis and will give you the
|
||||
sum of all y-values with the same x-coordinate and not the aggregation of all values of the data set.
|
||||
|
||||
#### Rich Tooltips
|
||||
|
||||
It is also possible to use HTML in the tooltip strings to create rich tooltip markups, which can
|
||||
be useful to provide extended output that spans over multiple lines. Please keep in mind that
|
||||
users without JavaScript will see the tooltip with all of its html-tags stripped.
|
||||
|
||||
![Various Line Graph Options][graph7.1]
|
||||
|
||||
#### Available replacements
|
||||
|
||||
The available replacements depend on the used chart type, since the tooltip data is
|
||||
instantiated and populated by the chart. All bar graphs have the following replacements available:
|
||||
|
||||
Aggregated values, are calculated from the data points of each column:
|
||||
|
||||
- sum: The amount of all Y-values of the current column
|
||||
- max: The biggest occurring Y-value of the current column
|
||||
- min: The smallest occurring Y-value of the current column
|
||||
|
||||
|
||||
Column values are also defined by the current column, but are not
|
||||
the product of any aggregation
|
||||
|
||||
- title: The x-value of the current column
|
||||
|
||||
|
||||
Row values are defined by the properties the current data set, and are only useful for rendering the
|
||||
generic tooltip correctly, since you could also just directly write
|
||||
those values into your custom tooltip.
|
||||
|
||||
- label: The name of the current data set
|
||||
- color: The color of this data set
|
||||
|
||||
|
||||
|
||||
## Pie Charts
|
||||
|
||||
### The PieChart Object
|
||||
|
@ -317,5 +388,6 @@ Rendering is straightforward, assuming $svg is the PieChart/GridChart object, yo
|
|||
[graph5]: res/GraphExample#5.png
|
||||
[graph6]: res/GraphExample#6.png
|
||||
[graph7]: res/GraphExample#7.png
|
||||
[graph7.1]: res/GraphExample#7.1.png
|
||||
[graph8]: res/GraphExample#8.png
|
||||
[graph9]: res/GraphExample#9.png
|
||||
[graph9]: res/GraphExample#9.png
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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']));
|
||||
|
|
|
@ -25,12 +25,14 @@ class JavaScript
|
|||
|
||||
protected static $vendorFiles = array(
|
||||
'js/vendor/jquery-2.1.0',
|
||||
'js/vendor/jquery.sparkline'
|
||||
'js/vendor/jquery.sparkline',
|
||||
'js/vendor/jquery.tipsy'
|
||||
);
|
||||
|
||||
protected static $ie8VendorFiles = array(
|
||||
'js/vendor/jquery-1.11.0',
|
||||
'js/vendor/jquery.sparkline'
|
||||
'js/vendor/jquery.sparkline',
|
||||
'js/vendor/jquery.tipsy'
|
||||
);
|
||||
|
||||
public static function listModuleFiles()
|
||||
|
|
|
@ -24,6 +24,8 @@ class StyleSheet
|
|||
'css/icinga/monitoring-colors.less',
|
||||
'css/icinga/selection-toolbar.less',
|
||||
'css/icinga/login.less',
|
||||
'css/icinga/charts.less',
|
||||
'css/vendor/tipsy.css'
|
||||
);
|
||||
|
||||
public static function compileForPdf()
|
||||
|
|
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
|
||||
/* Add hover effects to chart data */
|
||||
.chart-data:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.pie-data:hover {
|
||||
opacity: 0.6;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/* tipsy, facebook style tooltips for jquery
|
||||
version 1.0.0a
|
||||
(c) 2008-2010 jason frame [jason@onehackoranother.com]
|
||||
released under the MIT license */
|
||||
|
||||
.tipsy { font-size: 14px; position: absolute; padding: 5px; z-index: 100000; }
|
||||
.tipsy-inner { background-color: #000; color: #FFF; max-width: 200px; padding: 5px 8px 4px 8px; text-align: center; }
|
||||
|
||||
/* Rounded corners */
|
||||
.tipsy-inner { border-radius: 3px; -moz-border-radius: 3px; -webkit-border-radius: 3px; }
|
||||
|
||||
/* Uncomment for shadow */
|
||||
/*.tipsy-inner { box-shadow: 0 0 5px #000000; -webkit-box-shadow: 0 0 5px #000000; -moz-box-shadow: 0 0 5px #000000; }*/
|
||||
|
||||
.tipsy-arrow { position: absolute; width: 0; height: 0; line-height: 0; border: 5px dashed #000; }
|
||||
|
||||
/* Rules to colour arrows */
|
||||
.tipsy-arrow-n { border-bottom-color: #000; }
|
||||
.tipsy-arrow-s { border-top-color: #000; }
|
||||
.tipsy-arrow-e { border-left-color: #000; }
|
||||
.tipsy-arrow-w { border-right-color: #000; }
|
||||
|
||||
.tipsy-n .tipsy-arrow { top: 0px; left: 50%; margin-left: -5px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent; }
|
||||
.tipsy-nw .tipsy-arrow { top: 0; left: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;}
|
||||
.tipsy-ne .tipsy-arrow { top: 0; right: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;}
|
||||
.tipsy-s .tipsy-arrow { bottom: 0; left: 50%; margin-left: -5px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; }
|
||||
.tipsy-sw .tipsy-arrow { bottom: 0; left: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; }
|
||||
.tipsy-se .tipsy-arrow { bottom: 0; right: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; }
|
||||
.tipsy-e .tipsy-arrow { right: 0; top: 50%; margin-top: -5px; border-left-style: solid; border-right: none; border-top-color: transparent; border-bottom-color: transparent; }
|
||||
.tipsy-w .tipsy-arrow { left: 0; top: 50%; margin-top: -5px; border-right-style: solid; border-left: none; border-top-color: transparent; border-bottom-color: transparent; }
|
|
@ -109,6 +109,62 @@
|
|||
if (searchField.length && searchField.val().length) {
|
||||
this.searchValue = searchField.val();
|
||||
}
|
||||
|
||||
$('[title]').each(function () {
|
||||
var $el = $(this);
|
||||
$el.attr('title', $el.attr('title-rich') || $el.attr('title'));
|
||||
// $el.attr('title', null);
|
||||
});
|
||||
|
||||
$('svg rect.chart-data[title]', el).tipsy({ gravity: 'e', html: true });
|
||||
$('.historycolorgrid a[title]', el).tipsy({ gravity: 's', offset: 2 });
|
||||
$('img.icon[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, offset: 2 });
|
||||
$('[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, delayIn: 500 });
|
||||
|
||||
// Rescue or remove all orphaned tooltips
|
||||
$('.tipsy').each(function () {
|
||||
function isElementInDOM(ele) {
|
||||
while (ele = ele.parentNode) {
|
||||
if (ele == document) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
var arrow = $('.tipsy-arrow', this)[0];
|
||||
if (!icinga.utils.elementsOverlap(arrow, $('#main')[0])) {
|
||||
$(this).remove();
|
||||
return;
|
||||
}
|
||||
// all tooltips are direct children of the body
|
||||
// so we need find out whether the tooltip belongs applied area,
|
||||
// by checking if both areas overlap
|
||||
if (!icinga.utils.elementsOverlap(arrow, el)) {
|
||||
// tooltip does not belong to this area
|
||||
return;
|
||||
}
|
||||
|
||||
var pointee = $.data(this, 'tipsy-pointee');
|
||||
if (!pointee || !isElementInDOM(pointee)) {
|
||||
var orphan = this;
|
||||
var oldTitle = $(this).find('.tipsy-inner').html();
|
||||
var migrated = false;
|
||||
// try to find an element with the same title
|
||||
$('[original-title="' + oldTitle + '"]').each(function() {
|
||||
// get stored instance of Tipsy from newly created element
|
||||
// point it to the orphaned tooltip
|
||||
var tipsy = $.data(this, 'tipsy');
|
||||
tipsy.$tip = $(orphan);
|
||||
$.data(this, 'tipsy', tipsy);
|
||||
|
||||
// orphaned tooltip has the new element as pointee
|
||||
$.data(orphan, 'tipsy-pointee', this);
|
||||
migrated = true;
|
||||
});
|
||||
if (!migrated) {
|
||||
$(orphan).remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -162,6 +218,13 @@
|
|||
// $(document).on('keyup', 'form.auto input', this.formChangeDelayed);
|
||||
// $(document).on('change', 'form.auto input', this.formChanged);
|
||||
// $(document).on('change', 'form.auto select', this.submitForm);
|
||||
|
||||
$(document).on('mouseenter', '[title-original]', { gravity: 'n' }, function(event) {
|
||||
icinga.ui.hoverTooltip (this, event.data);
|
||||
});
|
||||
$(document).on('mouseleave', '[title-original]', {}, function() {
|
||||
icinga.ui.unhoverTooltip (this);
|
||||
});
|
||||
},
|
||||
|
||||
menuTitleHovered: function (event) {
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
// The currently hovered tooltip
|
||||
var tooltip = null;
|
||||
|
||||
// Stores the icinga-data-url of the last focused table.
|
||||
var focusedTableDataUrl = null;
|
||||
|
||||
|
@ -767,7 +770,6 @@
|
|||
this.debugTimer = null;
|
||||
this.timeCounterTimer = null;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}(Icinga, jQuery));
|
||||
|
|
|
@ -200,6 +200,40 @@
|
|||
return params;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether two HTMLElements overlap
|
||||
*
|
||||
* @param a {HTMLElement}
|
||||
* @param b {HTMLElement}
|
||||
*
|
||||
* @returns {Boolean} whether elements overlap, will return false when one
|
||||
* element is not in the DOM
|
||||
*/
|
||||
elementsOverlap: function(a, b)
|
||||
{
|
||||
// a bounds
|
||||
var aoff = $(a).offset();
|
||||
if (!aoff) {
|
||||
return false;
|
||||
}
|
||||
var at = aoff.top;
|
||||
var ah = a.offsetHeight;
|
||||
var al = aoff.left;
|
||||
var aw = a.offsetWidth;
|
||||
|
||||
// b bounds
|
||||
var boff = $(b).offset();
|
||||
if (!boff) {
|
||||
return false;
|
||||
}
|
||||
var bt = boff.top;
|
||||
var bh = b.offsetHeight;
|
||||
var bl = boff.left;
|
||||
var bw = b.offsetWidth;
|
||||
|
||||
return !(at > (bt + bh) || bt > (at + ah)) && !(bl > (al + aw) || al > (bl + bw));
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
jquery.tipsy.js SOURCE
|
||||
======================
|
||||
|
||||
This file contains information about how to acquire and install the source files for jquery.tipsy
|
||||
|
||||
|
||||
# version
|
||||
|
||||
1.0.0a
|
||||
|
||||
|
||||
# license
|
||||
|
||||
MIT license
|
||||
|
||||
|
||||
# used files
|
||||
|
||||
src/javascript/tipsy.css
|
||||
src/javascript/jquery.tipsy.js
|
||||
|
||||
|
||||
# source
|
||||
|
||||
https://github.com/jaz303/tipsy.git
|
||||
|
||||
|
||||
# installation
|
||||
|
||||
|
||||
mv src/javascript/tipsy.css ICINGAWEB/public/css/vendor/tipsy.css
|
||||
mv src/javascript/jquery.tipsy.js ICINGAWEB/public/js/vendor/jquery.tipsy.js
|
||||
uglifyjs src/javascript/jquery.tipsy.js ICINGAWEB/public/js/vendor/jquery.tipsy.min.js
|
|
@ -0,0 +1,262 @@
|
|||
// tipsy, facebook style tooltips for jquery
|
||||
// version 1.0.0a
|
||||
// (c) 2008-2010 jason frame [jason@onehackoranother.com]
|
||||
// released under the MIT license
|
||||
|
||||
(function($) {
|
||||
|
||||
function maybeCall(thing, ctx) {
|
||||
return (typeof thing == 'function') ? (thing.call(ctx)) : thing;
|
||||
};
|
||||
|
||||
function isElementInDOM(ele) {
|
||||
while (ele = ele.parentNode) {
|
||||
if (ele == document) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
function Tipsy(element, options) {
|
||||
this.$element = $(element);
|
||||
this.options = options;
|
||||
this.enabled = true;
|
||||
this.fixTitle();
|
||||
};
|
||||
|
||||
Tipsy.prototype = {
|
||||
show: function() {
|
||||
var title = this.getTitle();
|
||||
if (title && this.enabled) {
|
||||
var $tip = this.tip();
|
||||
|
||||
$tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title);
|
||||
$tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity
|
||||
$tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body);
|
||||
|
||||
var pos = $.extend({}, this.$element.offset(), {
|
||||
width: this.$element[0].offsetWidth,
|
||||
height: this.$element[0].offsetHeight
|
||||
});
|
||||
|
||||
var actualWidth = $tip[0].offsetWidth,
|
||||
actualHeight = $tip[0].offsetHeight,
|
||||
gravity = maybeCall(this.options.gravity, this.$element[0]);
|
||||
|
||||
var tp;
|
||||
switch (gravity.charAt(0)) {
|
||||
case 'n':
|
||||
tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
|
||||
break;
|
||||
case 's':
|
||||
tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
|
||||
break;
|
||||
case 'e':
|
||||
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset};
|
||||
break;
|
||||
case 'w':
|
||||
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset};
|
||||
break;
|
||||
}
|
||||
|
||||
if (gravity.length == 2) {
|
||||
if (gravity.charAt(1) == 'w') {
|
||||
tp.left = pos.left + pos.width / 2 - 15;
|
||||
} else {
|
||||
tp.left = pos.left + pos.width / 2 - actualWidth + 15;
|
||||
}
|
||||
}
|
||||
|
||||
$tip.css(tp).addClass('tipsy-' + gravity);
|
||||
$tip.find('.tipsy-arrow')[0].className = 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0);
|
||||
if (this.options.className) {
|
||||
$tip.addClass(maybeCall(this.options.className, this.$element[0]));
|
||||
}
|
||||
|
||||
if (this.options.fade) {
|
||||
$tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity});
|
||||
} else {
|
||||
$tip.css({visibility: 'visible', opacity: this.options.opacity});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
if (this.options.fade) {
|
||||
this.tip().stop().fadeOut(function() { $(this).remove(); });
|
||||
} else {
|
||||
this.tip().remove();
|
||||
}
|
||||
},
|
||||
|
||||
fixTitle: function() {
|
||||
var $e = this.$element;
|
||||
if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') {
|
||||
$e.attr('original-title', $e.attr('title') || '').removeAttr('title');
|
||||
}
|
||||
},
|
||||
|
||||
getTitle: function() {
|
||||
var title, $e = this.$element, o = this.options;
|
||||
this.fixTitle();
|
||||
var title, o = this.options;
|
||||
if (typeof o.title == 'string') {
|
||||
title = $e.attr(o.title == 'title' ? 'original-title' : o.title);
|
||||
} else if (typeof o.title == 'function') {
|
||||
title = o.title.call($e[0]);
|
||||
}
|
||||
title = ('' + title).replace(/(^\s*|\s*$)/, "");
|
||||
return title || o.fallback;
|
||||
},
|
||||
|
||||
tip: function() {
|
||||
if (!this.$tip) {
|
||||
this.$tip = $('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>');
|
||||
this.$tip.data('tipsy-pointee', this.$element[0]);
|
||||
}
|
||||
return this.$tip;
|
||||
},
|
||||
|
||||
validate: function() {
|
||||
if (!this.$element[0].parentNode) {
|
||||
this.hide();
|
||||
this.$element = null;
|
||||
this.options = null;
|
||||
}
|
||||
},
|
||||
|
||||
enable: function() { this.enabled = true; },
|
||||
disable: function() { this.enabled = false; },
|
||||
toggleEnabled: function() { this.enabled = !this.enabled; }
|
||||
};
|
||||
|
||||
$.fn.tipsy = function(options) {
|
||||
|
||||
if (options === true) {
|
||||
return this.data('tipsy');
|
||||
} else if (typeof options == 'string') {
|
||||
var tipsy = this.data('tipsy');
|
||||
if (tipsy) tipsy[options]();
|
||||
return this;
|
||||
}
|
||||
|
||||
options = $.extend({}, $.fn.tipsy.defaults, options);
|
||||
|
||||
function get(ele) {
|
||||
var tipsy = $.data(ele, 'tipsy');
|
||||
if (!tipsy) {
|
||||
tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options));
|
||||
$.data(ele, 'tipsy', tipsy);
|
||||
}
|
||||
return tipsy;
|
||||
}
|
||||
|
||||
function enter() {
|
||||
var tipsy = get(this);
|
||||
var ele = this;
|
||||
tipsy.hoverState = 'in';
|
||||
if (options.delayIn == 0) {
|
||||
tipsy.show();
|
||||
} else {
|
||||
tipsy.fixTitle();
|
||||
setTimeout(function() {
|
||||
if (tipsy.hoverState == 'in' && ele && isElementInDOM(ele))
|
||||
tipsy.show();
|
||||
}, options.delayIn);
|
||||
}
|
||||
};
|
||||
|
||||
function leave() {
|
||||
var tipsy = get(this);
|
||||
tipsy.hoverState = 'out';
|
||||
if (options.delayOut == 0) {
|
||||
tipsy.hide();
|
||||
} else {
|
||||
setTimeout(function() { if (tipsy.hoverState == 'out') tipsy.hide(); }, options.delayOut);
|
||||
}
|
||||
};
|
||||
|
||||
if (!options.live) this.each(function() { get(this); });
|
||||
|
||||
if (options.trigger != 'manual') {
|
||||
var binder = options.live ? 'live' : 'bind',
|
||||
eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus',
|
||||
eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur';
|
||||
this[binder](eventIn, enter)[binder](eventOut, leave);
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
};
|
||||
|
||||
$.fn.tipsy.defaults = {
|
||||
className: null,
|
||||
delayIn: 0,
|
||||
delayOut: 0,
|
||||
fade: false,
|
||||
fallback: '',
|
||||
gravity: 'n',
|
||||
html: false,
|
||||
live: false,
|
||||
offset: 0,
|
||||
opacity: 0.8,
|
||||
title: 'title',
|
||||
trigger: 'hover'
|
||||
};
|
||||
|
||||
$.fn.tipsy.revalidate = function() {
|
||||
$('.tipsy').each(function() {
|
||||
var pointee = $.data(this, 'tipsy-pointee');
|
||||
if (!pointee || !isElementInDOM(pointee)) {
|
||||
$(this).remove();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Overwrite this method to provide options on a per-element basis.
|
||||
// For example, you could store the gravity in a 'tipsy-gravity' attribute:
|
||||
// return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' });
|
||||
// (remember - do not modify 'options' in place!)
|
||||
$.fn.tipsy.elementOptions = function(ele, options) {
|
||||
return $.metadata ? $.extend({}, options, $(ele).metadata()) : options;
|
||||
};
|
||||
|
||||
$.fn.tipsy.autoNS = function() {
|
||||
return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n';
|
||||
};
|
||||
|
||||
$.fn.tipsy.autoWE = function() {
|
||||
return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w';
|
||||
};
|
||||
|
||||
/**
|
||||
* yields a closure of the supplied parameters, producing a function that takes
|
||||
* no arguments and is suitable for use as an autogravity function like so:
|
||||
*
|
||||
* @param margin (int) - distance from the viewable region edge that an
|
||||
* element should be before setting its tooltip's gravity to be away
|
||||
* from that edge.
|
||||
* @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer
|
||||
* if there are no viewable region edges effecting the tooltip's
|
||||
* gravity. It will try to vary from this minimally, for example,
|
||||
* if 'sw' is preferred and an element is near the right viewable
|
||||
* region edge, but not the top edge, it will set the gravity for
|
||||
* that element's tooltip to be 'se', preserving the southern
|
||||
* component.
|
||||
*/
|
||||
$.fn.tipsy.autoBounds = function(margin, prefer) {
|
||||
return function() {
|
||||
var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)},
|
||||
boundTop = $(document).scrollTop() + margin,
|
||||
boundLeft = $(document).scrollLeft() + margin,
|
||||
$this = $(this);
|
||||
|
||||
if ($this.offset().top < boundTop) dir.ns = 'n';
|
||||
if ($this.offset().left < boundLeft) dir.ew = 'w';
|
||||
if ($(window).width() + $(document).scrollLeft() - $this.offset().left < margin) dir.ew = 'e';
|
||||
if ($(window).height() + $(document).scrollTop() - $this.offset().top < margin) dir.ns = 's';
|
||||
|
||||
return dir.ns + (dir.ew ? dir.ew : '');
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery);
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue