diff --git a/library/Icinga/Chart/Chart.php b/library/Icinga/Chart/Chart.php index 13b0114e6..842bb56e8 100644 --- a/library/Icinga/Chart/Chart.php +++ b/library/Icinga/Chart/Chart.php @@ -38,11 +38,25 @@ abstract class Chart implements Drawable */ protected $palette; + /** + * The title of this chart, used for providing accessibility features + * + * @var string + */ + public $title; + + /** + * The description for this chart, mandatory for providing accessibility features + * + * @var string + */ + public $description; + /** * Create a new chart object and create internal objects * * If you want to extend this class use the init() method as an extension point, - * as this will be called at the end o fthe construct call + * as this will be called at the end of the construct call */ public function __construct() { @@ -86,7 +100,6 @@ abstract class Chart implements Drawable } /** - * * Render this graph and return the created SVG * * @return string The SVG created by the SvgRenderer @@ -105,6 +118,11 @@ abstract class Chart implements Drawable $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(); } diff --git a/library/Icinga/Chart/Graph/BarGraph.php b/library/Icinga/Chart/Graph/BarGraph.php index 2d20f8f22..dacc12878 100644 --- a/library/Icinga/Chart/Graph/BarGraph.php +++ b/library/Icinga/Chart/Graph/BarGraph.php @@ -65,7 +65,13 @@ class BarGraph extends Styleable implements Drawable ) { $this->order = $order; $this->dataSet = $dataSet; + $this->tooltips = $tooltips; + foreach ($this->tooltips as $value) { + $ts[] = $value; + } + $this->tooltips = $ts; + $this->graphs = $graphs; } diff --git a/library/Icinga/Chart/Graph/LineGraph.php b/library/Icinga/Chart/Graph/LineGraph.php index 630223968..7b831d625 100644 --- a/library/Icinga/Chart/Graph/LineGraph.php +++ b/library/Icinga/Chart/Graph/LineGraph.php @@ -38,6 +38,13 @@ class LineGraph extends Styleable implements Drawable */ private $isDiscrete = false; + /** + * The tooltips + * + * @var + */ + private $tooltips; + /** * The default stroke width * @var int @@ -56,10 +63,22 @@ class LineGraph extends Styleable implements Drawable * * @param array $dataset An array of [x, y] arrays to display */ - public function __construct(array $dataset) - { + public function __construct( + array $dataset, + array &$graphs, + $order, + array $tooltips = null + ) { usort($dataset, array($this, 'sortByX')); $this->dataset = $dataset; + $this->graphs = $graphs; + + $this->tooltips = $tooltips; + foreach ($this->tooltips as $value) { + $ts[] = $value; + } + $this->tooltips = $ts; + $this->order = $order; } /** @@ -142,14 +161,41 @@ class LineGraph extends Styleable implements Drawable $path->setAdditionalStyle('clip-path: url(#clip);'); $path->setId($this->id); $group = $path->toSvg($ctx); - if ($this->showDataPoints === true) { - foreach ($this->dataset as $point) { - $dot = new Circle($point[0], $point[1], $this->dotWith); - $dot->setFill($this->strokeColor); - $group->appendChild($dot->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 + $invisible = new Circle($point[0], $point[1], 20); + $invisible->setFill($this->strokeColor); + $invisible->setAdditionalStyle('opacity: 0.0;'); + $invisible->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; + $invisible->setAttribute( + 'title', + $this->tooltips[$x]->renderNoHtml($this->order, $data, $format) + ); + $invisible->setAttribute( + 'data-title-rich', + $this->tooltips[$x]->render($this->order, $data, $format) + ); + } + $group->appendChild($invisible->toSvg($ctx)); } - } + return $group; } } diff --git a/library/Icinga/Chart/Graph/Tooltip.php b/library/Icinga/Chart/Graph/Tooltip.php index 5fa5c3608..8b2efa613 100644 --- a/library/Icinga/Chart/Graph/Tooltip.php +++ b/library/Icinga/Chart/Graph/Tooltip.php @@ -66,7 +66,7 @@ class Tooltip */ public function __construct ( $data = array(), - $format = '{title}
{value} of {sum} {label}' + $format = '{title}: {value} {label}' ) { $this->data = array_merge($this->data, $data); $this->defaultFormat = $format; diff --git a/library/Icinga/Chart/GridChart.php b/library/Icinga/Chart/GridChart.php index 7f2794282..9f867ac66 100644 --- a/library/Icinga/Chart/GridChart.php +++ b/library/Icinga/Chart/GridChart.php @@ -84,6 +84,13 @@ class GridChart extends Chart */ 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. * @@ -395,7 +402,12 @@ class GridChart extends Chart ); break; case self::TYPE_LINE: - $graphObj = new LineGraph($axis->transform($graph['data'])); + $graphObj = new LineGraph( + $axis->transform($graph['data']), + $graphs, + $dataset, + $this->tooltips + ); break; default: continue; diff --git a/library/Icinga/Chart/PieChart.php b/library/Icinga/Chart/PieChart.php index 33ce32927..6b19969c8 100644 --- a/library/Icinga/Chart/PieChart.php +++ b/library/Icinga/Chart/PieChart.php @@ -50,6 +50,13 @@ class PieChart extends Chart */ private $noCaption = false; + public function __construct() + { + $this->title = t('Pie Chart'); + $this->description = t('Contains data in a pie chart.'); + parent::__construct(); + } + /** * Test if the given pies have the correct format * diff --git a/library/Icinga/Chart/Primitive/Canvas.php b/library/Icinga/Chart/Primitive/Canvas.php index c5617fa35..88e247f02 100644 --- a/library/Icinga/Chart/Primitive/Canvas.php +++ b/library/Icinga/Chart/Primitive/Canvas.php @@ -43,6 +43,13 @@ class Canvas implements Drawable */ private $rect; + /** + * The aria role used to describe this canvas' purpose in the accessibility tree + * + * @var string + */ + private $ariaRole; + /** * Create this canvas * @@ -111,6 +118,23 @@ class Canvas implements Drawable $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/SVGRenderer.php b/library/Icinga/Chart/SVGRenderer.php index 5cc223388..98eaa86d6 100644 --- a/library/Icinga/Chart/SVGRenderer.php +++ b/library/Icinga/Chart/SVGRenderer.php @@ -48,6 +48,27 @@ class SVGRenderer */ private $svg; + /** + * The description of this SVG, useful for screen readers + * + * @var string + */ + private $ariaDescription; + + /** + * The title of this SVG, useful for screen readers + * + * @var string + */ + private $ariaTitle; + + /** + * The aria role used by this svg element + * + * @var string + */ + private $ariaRole = 'img'; + /** * The root layer for all elements * @@ -126,6 +147,7 @@ class SVGRenderer $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( @@ -150,6 +172,42 @@ class SVGRenderer 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 * @@ -172,6 +230,7 @@ class SVGRenderer { $this->createRootDocument(); $ctx = $this->createRenderContext(); + $this->addAriaDescription($this->svg, $this->ariaTitle, $this->ariaDescription); $this->svg->appendChild($this->rootCanvas->toSvg($ctx)); $this->document->formatOutput = true; return $this->document->saveXML(); @@ -232,4 +291,40 @@ class SVGRenderer { $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/Web/Widget/Chart/InlinePie.php b/library/Icinga/Web/Widget/Chart/InlinePie.php index 341322714..0bddc906d 100644 --- a/library/Icinga/Web/Widget/Chart/InlinePie.php +++ b/library/Icinga/Web/Widget/Chart/InlinePie.php @@ -44,7 +44,6 @@ EOD; EOD; - /** * @var Url */ diff --git a/modules/monitoring/application/controllers/AlertsummaryController.php b/modules/monitoring/application/controllers/AlertsummaryController.php index b36f7ac3e..2c884e60f 100644 --- a/modules/monitoring/application/controllers/AlertsummaryController.php +++ b/modules/monitoring/application/controllers/AlertsummaryController.php @@ -343,6 +343,8 @@ class Monitoring_AlertsummaryController extends Controller public function createHealingChart() { $gridChart = new GridChart(); + $gridChart->title = t('Healing Chart'); + $gridChart->description = t('Notifications and average reaction time per hour.'); $gridChart->alignTopLeft(); $gridChart->setAxisLabel($this->createPeriodDescription(), mt('monitoring', 'Notifications')) @@ -446,7 +448,8 @@ class Monitoring_AlertsummaryController extends Controller 'label' => $this->translate('Notifications'), 'color' => '#07C0D9', 'data' => $notifications, - 'showPoints' => true + 'showPoints' => true, + 'tooltip' => '{title}: {value} {label}' ) ); @@ -455,7 +458,8 @@ class Monitoring_AlertsummaryController extends Controller 'label' => $this->translate('Avg (min)'), 'color' => '#ffaa44', 'data' => $dAvg, - 'showPoints' => true + 'showPoints' => true, + 'tooltip' => t('{title}: {value}m min. reaction time') ) ); @@ -464,7 +468,8 @@ class Monitoring_AlertsummaryController extends Controller 'label' => $this->translate('Max (min)'), 'color' => '#ff5566', 'data' => $dMax, - 'showPoints' => true + 'showPoints' => true, + 'tooltip' => t('{title}: {value}m max. reaction time') ) ); @@ -479,6 +484,8 @@ class Monitoring_AlertsummaryController extends Controller public function createDefectImage() { $gridChart = new GridChart(); + $gridChart->title = t('Defect Chart'); + $gridChart->description = t('Notifications and defects per hour'); $gridChart->alignTopLeft(); $gridChart->setAxisLabel($this->createPeriodDescription(), mt('monitoring', 'Notifications')) @@ -491,7 +498,8 @@ class Monitoring_AlertsummaryController extends Controller 'label' => $this->translate('Notifications'), 'color' => '#07C0D9', 'data' => $this->notificationData, - 'showPoints' => true + 'showPoints' => true, + 'tooltip' => '{title}: {value} {label}' ) ); @@ -500,7 +508,8 @@ class Monitoring_AlertsummaryController extends Controller 'label' => $this->translate('Defects'), 'color' => '#ff5566', 'data' => $this->problemData, - 'showPoints' => true + 'showPoints' => true, + 'tooltip' => '{title}: {value} {label}' ) ); diff --git a/modules/monitoring/application/controllers/ChartController.php b/modules/monitoring/application/controllers/ChartController.php index b01702c04..f19d3f458 100644 --- a/modules/monitoring/application/controllers/ChartController.php +++ b/modules/monitoring/application/controllers/ChartController.php @@ -236,6 +236,9 @@ class Monitoring_ChartController extends Controller $unknownBars[] = array($servicegroup->servicegroup, $servicegroup->services_unknown_unhandled); } $this->view->chart = new GridChart(); + $this->view->chart->title = t('Service Group Chart'); + $this->view->chart->description = t('Contains service states for each service group.'); + $this->view->chart->alignTopLeft(); $this->view->chart->setAxisLabel('', mt('monitoring', 'Services')) ->setXAxis(new StaticAxis()) @@ -292,6 +295,9 @@ class Monitoring_ChartController extends Controller } $tooltip = mt('monitoring', '{title}:
{value} of {sum} hosts are {label}'); $this->view->chart = new GridChart(); + $this->view->chart->title = t('Host Group Chart'); + $this->view->chart->description = t('Contains host states of each service group.'); + $this->view->chart->alignTopLeft(); $this->view->chart->setAxisLabel('', mt('monitoring', 'Hosts')) ->setXAxis(new StaticAxis()) diff --git a/modules/monitoring/application/forms/Config/BackendConfigForm.php b/modules/monitoring/application/forms/Config/BackendConfigForm.php index e0f8e3e0d..265ab33bc 100644 --- a/modules/monitoring/application/forms/Config/BackendConfigForm.php +++ b/modules/monitoring/application/forms/Config/BackendConfigForm.php @@ -43,8 +43,11 @@ class BackendConfigForm extends ConfigForm { $resources = array(); foreach ($resourceConfig as $name => $resource) { - if ($resource->type === 'db' || $resource->type === 'livestatus') { - $resources[$resource->type === 'db' ? 'ido' : 'livestatus'][$name] = $name; +// if ($resource->type === 'db' || $resource->type === 'livestatus') { +// $resources[$resource->type === 'db' ? 'ido' : 'livestatus'][$name] = $name; +// } + if ($resource->type === 'db') { + $resources['ido'][$name] = $name; } } @@ -183,13 +186,19 @@ class BackendConfigForm extends ConfigForm { $resourceType = isset($formData['type']) ? $formData['type'] : key($this->resources); + if ($resourceType === 'livestatus') { + throw new ConfigurationError( + 'We\'ve disabled livestatus support for now because it\'s not feature complete yet' + ); + } + $resourceTypes = array(); if ($resourceType === 'ido' || array_key_exists('ido', $this->resources)) { $resourceTypes['ido'] = 'IDO Backend'; } - if ($resourceType === 'livestatus' || array_key_exists('livestatus', $this->resources)) { - $resourceTypes['livestatus'] = 'Livestatus'; - } +// if ($resourceType === 'livestatus' || array_key_exists('livestatus', $this->resources)) { +// $resourceTypes['livestatus'] = 'Livestatus'; +// } $this->addElement( 'checkbox', diff --git a/modules/monitoring/application/forms/Setup/BackendPage.php b/modules/monitoring/application/forms/Setup/BackendPage.php index d8d4c7fd0..f2df42a1f 100644 --- a/modules/monitoring/application/forms/Setup/BackendPage.php +++ b/modules/monitoring/application/forms/Setup/BackendPage.php @@ -51,7 +51,7 @@ class BackendPage extends Form if (Platform::hasMysqlSupport() || Platform::hasPostgresqlSupport()) { $resourceTypes['ido'] = 'IDO'; } - $resourceTypes['livestatus'] = 'Livestatus'; + // $resourceTypes['livestatus'] = 'Livestatus'; $this->addElement( 'select', diff --git a/public/js/icinga/behavior/tooltip.js b/public/js/icinga/behavior/tooltip.js index b08b1f3c1..c9a64257f 100644 --- a/public/js/icinga/behavior/tooltip.js +++ b/public/js/icinga/behavior/tooltip.js @@ -27,7 +27,7 @@ var $el = $(this); $el.attr('title', $el.data('title-rich') || $el.attr('title')); }); - $('svg rect.chart-data[title]', el).tipsy({ gravity: 'se', html: true }); + $('svg .chart-data', el).tipsy({ gravity: 'se', 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 });