1<?php 2/* 3** Zabbix 4** Copyright (C) 2001-2021 Zabbix SIA 5** 6** This program is free software; you can redistribute it and/or modify 7** it under the terms of the GNU General Public License as published by 8** the Free Software Foundation; either version 2 of the License, or 9** (at your option) any later version. 10** 11** This program is distributed in the hope that it will be useful, 12** but WITHOUT ANY WARRANTY; without even the implied warranty of 13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14** GNU General Public License for more details. 15** 16** You should have received a copy of the GNU General Public License 17** along with this program; if not, write to the Free Software 18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19**/ 20 21 22/** 23 * General class for SVG Graph usage. 24 */ 25class CSvgGraph extends CSvg { 26 27 const SVG_GRAPH_X_AXIS_HEIGHT = 20; 28 const SVG_GRAPH_DEFAULT_COLOR = '#b0af07'; 29 const SVG_GRAPH_DEFAULT_TRANSPARENCY = 5; 30 const SVG_GRAPH_DEFAULT_POINTSIZE = 1; 31 const SVG_GRAPH_DEFAULT_LINE_WIDTH = 1; 32 33 const SVG_GRAPH_X_AXIS_LABEL_MARGIN = 5; 34 const SVG_GRAPH_Y_AXIS_LEFT_LABEL_MARGIN = 5; 35 const SVG_GRAPH_Y_AXIS_RIGHT_LABEL_MARGIN = 12; 36 37 protected $canvas_height; 38 protected $canvas_width; 39 protected $canvas_x; 40 protected $canvas_y; 41 42 /** 43 * Problems annotation labels color. 44 * 45 * @var string 46 */ 47 protected $color_annotation = '#AA4455'; 48 49 /** 50 * Text color. 51 * 52 * @var string 53 */ 54 protected $text_color; 55 56 /** 57 * Grid color. 58 * 59 * @var string 60 */ 61 protected $grid_color; 62 63 /** 64 * Array of graph metrics data. 65 * 66 * @var array 67 */ 68 protected $metrics = []; 69 70 /** 71 * Array of graph points data. Calculated from metrics data. 72 * 73 * @var array 74 */ 75 protected $points = []; 76 77 /** 78 * Array of metric paths. Where key is metric index from $metrics array. 79 * 80 * @var array 81 */ 82 protected $paths = []; 83 84 /** 85 * Array of graph problems to display. 86 * 87 * @var array 88 */ 89 protected $problems = []; 90 91 protected $max_value_left = null; 92 protected $max_value_right = null; 93 protected $min_value_left = null; 94 protected $min_value_right = null; 95 96 protected $left_y_show = false; 97 protected $left_y_min = null; 98 protected $left_y_min_calculated = null; 99 protected $left_y_max = null; 100 protected $left_y_max_calculated = null; 101 protected $left_y_interval = null; 102 protected $left_y_units = null; 103 protected $left_y_is_binary = null; 104 protected $left_y_power = null; 105 protected $left_y_empty = true; 106 107 protected $right_y_show = false; 108 protected $right_y_min = null; 109 protected $right_y_min_calculated = null; 110 protected $right_y_max = null; 111 protected $right_y_max_calculated = null; 112 protected $right_y_interval = null; 113 protected $right_y_units = null; 114 protected $right_y_is_binary = null; 115 protected $right_y_power = null; 116 protected $right_y_empty = true; 117 118 protected $right_y_zero = null; 119 protected $left_y_zero = null; 120 121 protected $x_show; 122 123 protected $offset_bottom; 124 125 /** 126 * Value for graph left offset. Is used as width for left Y axis container. 127 * 128 * @var int 129 */ 130 protected $offset_left = 20; 131 132 /** 133 * Value for graph right offset. Is used as width for right Y axis container. 134 * 135 * @var int 136 */ 137 protected $offset_right = 20; 138 139 /** 140 * Maximum width of container for every Y axis. 141 * 142 * @var int 143 */ 144 protected $max_yaxis_width = 120; 145 146 protected $cell_height_min = 30; 147 148 protected $offset_top; 149 protected $time_from; 150 protected $time_till; 151 152 /** 153 * Height for X axis container. 154 * 155 * @var int 156 */ 157 protected $xaxis_height = 20; 158 159 /** 160 * SVG default size. 161 */ 162 protected $width = 1000; 163 protected $height = 1000; 164 165 public function __construct(array $options) { 166 parent::__construct(); 167 168 // Set colors. 169 $theme = getUserGraphTheme(); 170 $this->text_color = '#' . $theme['textcolor']; 171 $this->grid_color = '#' . $theme['gridcolor']; 172 173 $this 174 ->addClass(ZBX_STYLE_SVG_GRAPH) 175 ->setTimePeriod($options['time_period']['time_from'], $options['time_period']['time_to']) 176 ->setXAxis($options['x_axis']) 177 ->setYAxisLeft($options['left_y_axis']) 178 ->setYAxisRight($options['right_y_axis']); 179 } 180 181 /** 182 * Get graph canvas X offset. 183 * 184 * @return int 185 */ 186 public function getCanvasX() { 187 return $this->canvas_x; 188 } 189 190 /** 191 * Get graph canvas Y offset. 192 * 193 * @return int 194 */ 195 public function getCanvasY() { 196 return $this->canvas_y; 197 } 198 199 /** 200 * Get graph canvas width. 201 * 202 * @return int 203 */ 204 public function getCanvasWidth() { 205 return $this->canvas_width; 206 } 207 208 /** 209 * Get graph canvas height. 210 * 211 * @return int 212 */ 213 public function getCanvasHeight() { 214 return $this->canvas_height; 215 } 216 217 /** 218 * Set problems data for graph. 219 * 220 * @param array $problems Array of problems data. 221 * 222 * @return CSvgGraph 223 */ 224 public function addProblems(array $problems) { 225 $this->problems = $problems; 226 227 return $this; 228 } 229 230 /** 231 * Set metrics data for graph. 232 * 233 * @param array $metrics Array of metrics data. 234 * 235 * @return CSvgGraph 236 */ 237 public function addMetrics(array $metrics = []) { 238 $metrics_for_each_axes = [ 239 GRAPH_YAXIS_SIDE_LEFT => 0, 240 GRAPH_YAXIS_SIDE_RIGHT => 0 241 ]; 242 243 foreach ($metrics as $i => $metric) { 244 $min_value = null; 245 $max_value = null; 246 247 if (array_key_exists('points', $metric)) { 248 $metrics_for_each_axes[$metric['options']['axisy']]++; 249 250 foreach ($metric['points'] as $point) { 251 if ($min_value === null || $min_value > $point['value']) { 252 $min_value = $point['value']; 253 } 254 if ($max_value === null || $max_value < $point['value']) { 255 $max_value = $point['value']; 256 } 257 258 $this->points[$i][$point['clock']] = $point['value']; 259 } 260 261 if ($metric['options']['axisy'] == GRAPH_YAXIS_SIDE_LEFT) { 262 if ($this->min_value_left === null || $this->min_value_left > $min_value) { 263 $this->min_value_left = $min_value; 264 } 265 if ($this->max_value_left === null || $this->max_value_left < $max_value) { 266 $this->max_value_left = $max_value; 267 } 268 } 269 else { 270 if ($this->min_value_right === null || $this->min_value_right > $min_value) { 271 $this->min_value_right = $min_value; 272 } 273 if ($this->max_value_right === null || $this->max_value_right < $max_value) { 274 $this->max_value_right = $max_value; 275 } 276 } 277 } 278 279 $this->metrics[$i] = [ 280 'name' => $metric['name'], 281 'itemid' => $metric['itemid'], 282 'units' => $metric['units'], 283 'host' => $metric['hosts'][0], 284 'options' => ['order' => $i] + $metric['options'] 285 ]; 286 } 287 288 $this->left_y_empty = ($metrics_for_each_axes[GRAPH_YAXIS_SIDE_LEFT] == 0); 289 $this->right_y_empty = ($metrics_for_each_axes[GRAPH_YAXIS_SIDE_RIGHT] == 0); 290 291 return $this; 292 } 293 294 /** 295 * Set graph time period. 296 * 297 * @param int $time_from Timestamp. 298 * @param int @time_till Timestamp. 299 * 300 * @return CSvgGraph 301 */ 302 public function setTimePeriod($time_from, $time_till) { 303 $this->time_from = $time_from; 304 $this->time_till = $time_till; 305 306 return $this; 307 } 308 309 /** 310 * Set left side Y axis display options. 311 * 312 * @param array $options 313 * @param int $options['show'] 314 * @param string $options['min'] 315 * @param string $options['max'] 316 * @param string $options['units'] 317 * 318 * @return CSvgGraph 319 */ 320 public function setYAxisLeft(array $options) { 321 $this->left_y_show = ($options['show'] == SVG_GRAPH_AXIS_SHOW); 322 323 if ($options['min'] !== '') { 324 $this->left_y_min = $options['min']; 325 } 326 if ($options['max'] !== '') { 327 $this->left_y_max = $options['max']; 328 } 329 if ($options['units'] !== null) { 330 $units = trim(preg_replace('/\s+/', ' ', $options['units'])); 331 $this->left_y_units = htmlspecialchars($units); 332 } 333 334 return $this; 335 } 336 337 /** 338 * Set right side Y axis display options. 339 * 340 * @param array $options 341 * @param int $options['show'] 342 * @param string $options['min'] 343 * @param string $options['max'] 344 * @param string $options['units'] 345 * 346 * @return CSvgGraph 347 */ 348 public function setYAxisRight(array $options) { 349 $this->right_y_show = ($options['show'] == SVG_GRAPH_AXIS_SHOW); 350 351 if ($options['min'] !== '') { 352 $this->right_y_min = $options['min']; 353 } 354 if ($options['max'] !== '') { 355 $this->right_y_max = $options['max']; 356 } 357 if ($options['units'] !== null) { 358 $units = trim(preg_replace('/\s+/', ' ', $options['units'])); 359 $this->right_y_units = htmlspecialchars($units); 360 } 361 362 return $this; 363 } 364 365 /** 366 * Show or hide X axis. 367 * 368 * @param array $options 369 * 370 * @return CSvgGraph 371 */ 372 public function setXAxis(array $options) { 373 $this->x_show = ($options['show'] == SVG_GRAPH_AXIS_SHOW); 374 375 return $this; 376 } 377 378 /** 379 * Return array of horizontal labels with positions. Array key will be position, value will be label. 380 * 381 * @return array 382 */ 383 public function getTimeGridWithPosition() { 384 $period = $this->time_till - $this->time_from; 385 $step = round($period / $this->canvas_width * 100); // Grid cell (100px) in seconds. 386 387 /* 388 * In case if requested time period is so small that it is rounded to zero, we are displaying only two 389 * milestones on X axis - the start and the end of period. 390 */ 391 if ($step == 0) { 392 return [ 393 0 => zbx_date2str(TIME_FORMAT_SECONDS, $this->time_from), 394 $this->canvas_width => zbx_date2str(TIME_FORMAT_SECONDS, $this->time_till) 395 ]; 396 } 397 398 $start = $this->time_from + $step - $this->time_from % $step; 399 $time_formats = [ 400 SVG_GRAPH_DATE_FORMAT, 401 SVG_GRAPH_DATE_FORMAT_SHORT, 402 SVG_GRAPH_DATE_TIME_FORMAT_SHORT, 403 TIME_FORMAT, 404 TIME_FORMAT_SECONDS 405 ]; 406 407 // Search for most appropriate time format. 408 foreach ($time_formats as $fmt) { 409 $grid_values = []; 410 411 for ($clock = $start; $this->time_till >= $clock; $clock += $step) { 412 $relative_pos = round($this->canvas_width - $this->canvas_width * ($this->time_till - $clock) / $period); 413 $grid_values[$relative_pos] = zbx_date2str($fmt, $clock); 414 } 415 416 /** 417 * If at least two calculated time-strings are equal, proceed with next format. Do that as long as each date 418 * is different or there is no more time formats to test. 419 */ 420 if (count(array_flip($grid_values)) == count($grid_values) || $fmt === end($time_formats)) { 421 break; 422 } 423 } 424 425 return $grid_values; 426 } 427 428 /** 429 * Add UI selection box element to graph. 430 * 431 * @return CSvgGraph 432 */ 433 public function addSBox() { 434 $this->addItem([ 435 (new CSvgRect(0, 0, 0, 0))->addClass('svg-graph-selection'), 436 (new CSvgText(0, 0, ''))->addClass('svg-graph-selection-text') 437 ]); 438 439 return $this; 440 } 441 442 /** 443 * Add UI helper line that follows mouse. 444 * 445 * @return CSvgGraph 446 */ 447 public function addHelper() { 448 $this->addItem((new CSvgLine(0, 0, 0, 0))->addClass(CSvgTag::ZBX_STYLE_GRAPH_HELPER)); 449 450 return $this; 451 } 452 453 /** 454 * Render graph. 455 * 456 * @return CSvgGraph 457 */ 458 public function draw() { 459 $this->applyMissingDataFunc(); 460 $this->calculateDimensions(); 461 462 if ($this->canvas_width > 0 && $this->canvas_height > 0) { 463 $this->calculatePaths(); 464 465 $this->drawGrid(); 466 467 if ($this->left_y_show) { 468 $this->drawCanvasLeftYAxis(); 469 } 470 if ($this->right_y_show) { 471 $this->drawCanvasRightYAxis(); 472 } 473 if ($this->x_show) { 474 $this->drawCanvasXAxis(); 475 } 476 477 $this->drawMetricsLine(); 478 $this->drawMetricsPoint(); 479 $this->drawMetricsBar(); 480 481 $this->drawProblems(); 482 483 $this->addClipArea(); 484 } 485 486 return $this; 487 } 488 489 /** 490 * Add dynamic clip path to hide metric lines and area outside graph canvas. 491 */ 492 protected function addClipArea() { 493 $areaid = uniqid('metric_clip_'); 494 495 // CSS styles. 496 $this->styles['.'.CSvgTag::ZBX_STYLE_GRAPH_AREA]['clip-path'] = 'url(#'.$areaid.')'; 497 $this->styles['[data-metric]']['clip-path'] = 'url(#'.$areaid.')'; 498 499 $this->addItem( 500 (new CsvgTag('clipPath')) 501 ->addItem( 502 (new CSvgPath(implode(' ', [ 503 'M'.$this->canvas_x.','.($this->canvas_y - 3), 504 'H'.($this->canvas_width + $this->canvas_x), 505 'V'.($this->canvas_height + $this->canvas_y), 506 'H'.($this->canvas_x) 507 ]))) 508 ) 509 ->setAttribute('id', $areaid) 510 ); 511 } 512 513 /** 514 * Calculate canvas size, margins and offsets for graph canvas inside SVG element. 515 */ 516 protected function calculateDimensions() { 517 // Canvas height must be specified before call self::getValuesGridWithPosition. 518 519 $this->offset_top = 10; 520 $this->offset_bottom = self::SVG_GRAPH_X_AXIS_HEIGHT; 521 $this->canvas_height = $this->height - $this->offset_top - $this->offset_bottom; 522 $this->canvas_y = $this->offset_top; 523 524 // Determine units for left side. 525 526 if ($this->left_y_units === null) { 527 $this->left_y_units = ''; 528 foreach ($this->metrics as $metric) { 529 if ($metric['options']['axisy'] == GRAPH_YAXIS_SIDE_LEFT) { 530 $this->left_y_units = $metric['units']; 531 break; 532 } 533 } 534 } 535 536 // Determine units for right side. 537 538 if ($this->right_y_units === null) { 539 $this->right_y_units = ''; 540 foreach ($this->metrics as $metric) { 541 if ($metric['options']['axisy'] == GRAPH_YAXIS_SIDE_RIGHT) { 542 $this->right_y_units = $metric['units']; 543 break; 544 } 545 } 546 } 547 548 // Calculate vertical scale parameters for left side. 549 550 $rows_min = (int) max(1, floor($this->canvas_height / $this->cell_height_min / 1.5)); 551 $rows_max = (int) max(1, floor($this->canvas_height / $this->cell_height_min)); 552 553 $this->left_y_min_calculated = $this->left_y_min === null; 554 $this->left_y_max_calculated = $this->left_y_max === null; 555 556 if ($this->left_y_min_calculated) { 557 $this->left_y_min = $this->min_value_left ? : 0; 558 } 559 if ($this->left_y_max_calculated) { 560 $this->left_y_max = $this->max_value_left ? : 1; 561 } 562 563 $this->left_y_is_binary = $this->left_y_units === 'B' || $this->left_y_units === 'Bps'; 564 565 $result = calculateGraphScaleExtremes($this->left_y_min, $this->left_y_max, $this->left_y_is_binary, 566 $this->left_y_min_calculated, $this->left_y_max_calculated, $rows_min, $rows_max 567 ); 568 569 [ 570 'min' => $this->left_y_min, 571 'max' => $this->left_y_max, 572 'interval' => $this->left_y_interval, 573 'power' => $this->left_y_power 574 ] = $result; 575 576 // Calculate vertical scale parameters for right side. 577 578 if ($this->left_y_min_calculated && $this->left_y_max_calculated) { 579 $rows_min = $rows_max = $result['rows']; 580 } 581 582 $this->right_y_min_calculated = $this->right_y_min === null; 583 $this->right_y_max_calculated = $this->right_y_max === null; 584 585 if ($this->right_y_min_calculated) { 586 $this->right_y_min = $this->min_value_right ? : 0; 587 } 588 if ($this->right_y_max_calculated) { 589 $this->right_y_max = $this->max_value_right ? : 1; 590 } 591 592 $this->right_y_is_binary = $this->right_y_units === 'B' || $this->right_y_units === 'Bps'; 593 594 $result = calculateGraphScaleExtremes($this->right_y_min, $this->right_y_max, $this->right_y_is_binary, 595 $this->right_y_min_calculated, $this->right_y_max_calculated, $rows_min, $rows_max 596 ); 597 598 [ 599 'min' => $this->right_y_min, 600 'max' => $this->right_y_max, 601 'interval' => $this->right_y_interval, 602 'power' => $this->right_y_power 603 ] = $result; 604 605 // Define canvas dimensions and offsets, except canvas height and bottom offset. 606 607 $approx_width = 10; 608 609 if ($this->left_y_show) { 610 $values = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_LEFT, $this->left_y_empty); 611 612 if ($values) { 613 $offset_left = max($this->offset_left, max(array_map('strlen', $values)) * $approx_width); 614 $this->offset_left = (int) min($offset_left, $this->max_yaxis_width); 615 } 616 } 617 618 if ($this->right_y_show) { 619 $values = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_RIGHT, $this->right_y_empty); 620 621 if ($values) { 622 $offset_right = max($this->offset_right, max(array_map('strlen', $values)) * $approx_width); 623 $offset_right += self::SVG_GRAPH_Y_AXIS_RIGHT_LABEL_MARGIN; 624 $this->offset_right = (int) min($offset_right, $this->max_yaxis_width); 625 } 626 } 627 628 $this->canvas_width = $this->width - $this->offset_left - $this->offset_right; 629 $this->canvas_x = $this->offset_left; 630 631 // Calculate vertical zero position. 632 633 if ($this->left_y_max - $this->left_y_min == INF) { 634 $this->left_y_zero = $this->canvas_y + $this->canvas_height 635 * max(0, min(1, $this->left_y_max / 10 / ($this->left_y_max / 10 - $this->left_y_min / 10))); 636 } 637 else { 638 $this->left_y_zero = $this->canvas_y + $this->canvas_height 639 * max(0, min(1, $this->left_y_max / ($this->left_y_max - $this->left_y_min))); 640 } 641 642 if ($this->right_y_max - $this->right_y_min == INF) { 643 $this->right_y_zero = $this->canvas_y + $this->canvas_height 644 * max(0, min(1, $this->right_y_max / 10 / ($this->right_y_max / 10 - $this->right_y_min / 10))); 645 } 646 else { 647 $this->right_y_zero = $this->canvas_y + $this->canvas_height 648 * max(0, min(1, $this->right_y_max / ($this->right_y_max - $this->right_y_min))); 649 } 650 } 651 652 /** 653 * Get array of X points with labels, for grid and X/Y axes. Array key is Y coordinate for SVG, value is label with 654 * axis units. 655 * 656 * @param int $side Type of Y axis: GRAPH_YAXIS_SIDE_RIGHT, GRAPH_YAXIS_SIDE_LEFT 657 * @param bool $empty_set Return defaults for empty side. 658 * 659 * @return array 660 */ 661 protected function getValuesGridWithPosition($side, $empty_set = false) { 662 $min = 0; 663 $max = 1; 664 $min_calculated = true; 665 $max_calculated = true; 666 $interval = 1; 667 $units = ''; 668 $is_binary = false; 669 $power = 0; 670 671 if (!$empty_set) { 672 if ($side === GRAPH_YAXIS_SIDE_LEFT) { 673 $min = $this->left_y_min; 674 $max = $this->left_y_max; 675 $min_calculated = $this->left_y_min_calculated; 676 $max_calculated = $this->left_y_max_calculated; 677 $interval = $this->left_y_interval; 678 $units = $this->left_y_units; 679 $is_binary = $this->left_y_is_binary; 680 $power = $this->left_y_power; 681 } 682 elseif ($side === GRAPH_YAXIS_SIDE_RIGHT) { 683 $min = $this->right_y_min; 684 $max = $this->right_y_max; 685 $min_calculated = $this->right_y_min_calculated; 686 $max_calculated = $this->right_y_max_calculated; 687 $interval = $this->right_y_interval; 688 $units = $this->right_y_units; 689 $is_binary = $this->right_y_is_binary; 690 $power = $this->right_y_power; 691 } 692 } 693 694 $relative_values = calculateGraphScaleValues($min, $max, $min_calculated, $max_calculated, $interval, $units, 695 $is_binary, $power, 14 696 ); 697 698 $absolute_values = []; 699 700 foreach ($relative_values as ['relative_pos' => $relative_pos, 'value' => $value]) { 701 $absolute_values[(int) round($this->canvas_height * $relative_pos)] = $value; 702 } 703 704 return $absolute_values; 705 } 706 707 /** 708 * Add Y axis with labels to left side of graph. 709 */ 710 protected function drawCanvasLeftYaxis() { 711 $grid_values = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_LEFT, $this->left_y_empty); 712 $this->addItem( 713 (new CSvgGraphAxis($grid_values, GRAPH_YAXIS_SIDE_LEFT)) 714 ->setLineColor($this->grid_color) 715 ->setTextColor($this->text_color) 716 ->setSize($this->offset_left, $this->canvas_height) 717 ->setPosition($this->canvas_x - $this->offset_left, $this->canvas_y) 718 ); 719 } 720 721 /** 722 * Add Y axis with labels to right side of graph. 723 */ 724 protected function drawCanvasRightYAxis() { 725 $grid_values = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_RIGHT, $this->right_y_empty); 726 727 // Do not draw label at the bottom of right Y axis to avoid label averlapping with horizontal axis arrow. 728 if (array_key_exists(0, $grid_values)) { 729 unset($grid_values[0]); 730 } 731 732 $this->addItem( 733 (new CSvgGraphAxis($grid_values, GRAPH_YAXIS_SIDE_RIGHT)) 734 ->setLineColor($this->grid_color) 735 ->setTextColor($this->text_color) 736 ->setSize($this->offset_right, $this->canvas_height) 737 ->setPosition($this->canvas_x + $this->canvas_width, $this->canvas_y) 738 ); 739 } 740 741 /** 742 * Add X axis with labels to graph. 743 */ 744 protected function drawCanvasXAxis() { 745 $this->addItem( 746 (new CSvgGraphAxis($this->getTimeGridWithPosition(), GRAPH_YAXIS_SIDE_BOTTOM)) 747 ->setLineColor($this->grid_color) 748 ->setTextColor($this->text_color) 749 ->setSize($this->canvas_width, $this->xaxis_height) 750 ->setPosition($this->canvas_x, $this->canvas_y + $this->canvas_height) 751 ); 752 } 753 754 /** 755 * Add grid to graph. 756 */ 757 protected function drawGrid() { 758 $time_points = $this->x_show ? $this->getTimeGridWithPosition() : []; 759 $value_points = []; 760 761 if ($this->left_y_show) { 762 $value_points = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_LEFT, $this->left_y_empty); 763 764 unset($time_points[0]); 765 } 766 elseif ($this->right_y_show) { 767 $value_points = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_RIGHT, $this->right_y_empty); 768 769 unset($time_points[$this->canvas_width]); 770 } 771 772 if ($this->x_show) { 773 unset($value_points[0]); 774 } 775 776 $this->addItem((new CSvgGraphGrid($value_points, $time_points)) 777 ->setColor($this->grid_color) 778 ->setPosition($this->canvas_x, $this->canvas_y) 779 ->setSize($this->canvas_width, $this->canvas_height) 780 ); 781 } 782 783 /** 784 * Calculate paths for metric elements. 785 */ 786 protected function calculatePaths() { 787 // Metric having very big values of y outside visible area will fail to render. 788 $y_max = pow(2, 16); 789 $y_min = -$y_max; 790 791 foreach ($this->metrics as $index => $metric) { 792 if (!array_key_exists($index, $this->points)) { 793 continue; 794 } 795 796 if ($metric['options']['axisy'] == GRAPH_YAXIS_SIDE_RIGHT) { 797 $min_value = $this->right_y_min; 798 $max_value = $this->right_y_max; 799 } 800 else { 801 $min_value = $this->left_y_min; 802 $max_value = $this->left_y_max; 803 } 804 805 $time_range = ($this->time_till - $this->time_from) ? : 1; 806 $timeshift = $metric['options']['timeshift']; 807 $paths = []; 808 809 $path_num = 0; 810 foreach ($this->points[$index] as $clock => $point) { 811 // If missing data function is SVG_GRAPH_MISSING_DATA_NONE, path should be split in multiple svg shapes. 812 if ($point === null) { 813 $path_num++; 814 continue; 815 } 816 817 /** 818 * Avoid invisible data point drawing. Data sets of type != SVG_GRAPH_TYPE_POINTS cannot be skipped to 819 * keep shape unchanged. 820 */ 821 $in_range = ($max_value >= $point && $min_value <= $point); 822 if ($in_range || $metric['options']['type'] != SVG_GRAPH_TYPE_POINTS) { 823 $x = $this->canvas_x + $this->canvas_width 824 - $this->canvas_width * ($this->time_till - $clock + $timeshift) / $time_range; 825 826 if ($max_value - $min_value == INF) { 827 $y = $this->canvas_y + CMathHelper::safeMul([$this->canvas_height, 828 $max_value / 10 - $point / 10, 1 / ($max_value / 10 - $min_value / 10) 829 ]); 830 } 831 else { 832 $y = $this->canvas_y + CMathHelper::safeMul([$this->canvas_height, 833 $max_value - $point, 1 / ($max_value - $min_value) 834 ]); 835 } 836 837 if (!$in_range) { 838 $y = ($point > $max_value) ? max($y_min, $y) : min($y_max, $y); 839 } 840 841 $paths[$path_num][] = [$x, ceil($y), convertUnits([ 842 'value' => $point, 843 'units' => $metric['units'] 844 ])]; 845 } 846 } 847 848 if ($paths) { 849 $this->paths[$index] = $paths; 850 } 851 } 852 } 853 854 /** 855 * Modifies metric data and Y value range according specified missing data function. 856 */ 857 protected function applyMissingDataFunc() { 858 foreach ($this->metrics as $index => $metric) { 859 /** 860 * - Missing data points are calculated only between existing data points; 861 * - Missing data points are not calculated for SVG_GRAPH_TYPE_POINTS && SVG_GRAPH_TYPE_BAR metrics; 862 * - SVG_GRAPH_MISSING_DATA_CONNECTED is default behavior of SVG graphs, so no need to calculate anything 863 * here. 864 */ 865 if (array_key_exists($index, $this->points) 866 && !in_array($metric['options']['type'], [SVG_GRAPH_TYPE_POINTS, SVG_GRAPH_TYPE_BAR]) 867 && $metric['options']['missingdatafunc'] != SVG_GRAPH_MISSING_DATA_CONNECTED) { 868 $points = &$this->points[$index]; 869 $missing_data_points = $this->getMissingData($points, $metric['options']['missingdatafunc']); 870 871 // Sort according new clock times (array keys). 872 $points += $missing_data_points; 873 ksort($points); 874 875 // Missing data function can change min value of Y axis. 876 if ($missing_data_points 877 && $metric['options']['missingdatafunc'] == SVG_GRAPH_MISSING_DATA_TREAT_AS_ZERO) { 878 if ($this->min_value_left > 0 && $metric['options']['axisy'] == GRAPH_YAXIS_SIDE_LEFT) { 879 $this->min_value_left = 0; 880 } 881 elseif ($this->min_value_right > 0 && $metric['options']['axisy'] == GRAPH_YAXIS_SIDE_RIGHT) { 882 $this->min_value_right = 0; 883 } 884 } 885 } 886 } 887 } 888 889 /** 890 * Calculate missing data for given set of $points according given $missingdatafunc. 891 * 892 * @param array $points Array of metric points to modify, where key is metric timestamp. 893 * @param int $missingdatafunc Type of function, allowed value: 894 * SVG_GRAPH_MISSING_DATA_TREAT_AS_ZERO, SVG_GRAPH_MISSING_DATA_NONE, 895 * SVG_GRAPH_MISSING_DATA_CONNECTED 896 * 897 * @return array Array of calculated missing data points. 898 */ 899 protected function getMissingData(array $points, $missingdatafunc) { 900 // Get average distance between points to detect gaps of missing data. 901 $prev_clock = null; 902 $points_distance = []; 903 foreach ($points as $clock => $point) { 904 if ($prev_clock !== null) { 905 $points_distance[] = $clock - $prev_clock; 906 } 907 $prev_clock = $clock; 908 } 909 910 /** 911 * $threshold is a minimal period of time at what we assume that data point is missed; 912 * $average_distance is an average distance between existing data points; 913 * $gap_interval is a time distance between missing points used to fulfill gaps of missing data. 914 * It's unique for each gap. 915 */ 916 $average_distance = $points_distance ? array_sum($points_distance) / count($points_distance) : 0; 917 $threshold = $points_distance ? $average_distance * 3 : 0; 918 919 // Add missing values. 920 $prev_clock = null; 921 $missing_points = []; 922 foreach ($points as $clock => $point) { 923 if ($prev_clock !== null && ($clock - $prev_clock) > $threshold) { 924 $gap_interval = floor(($clock - $prev_clock) / $threshold); 925 926 if ($missingdatafunc == SVG_GRAPH_MISSING_DATA_NONE) { 927 $missing_points[$prev_clock + $gap_interval] = null; 928 } 929 elseif ($missingdatafunc == SVG_GRAPH_MISSING_DATA_TREAT_AS_ZERO) { 930 $missing_points[$prev_clock + $gap_interval] = 0; 931 $missing_points[$clock - $gap_interval] = 0; 932 } 933 } 934 935 $prev_clock = $clock; 936 } 937 938 return $missing_points; 939 } 940 941 /** 942 * Add fill area to graph for metric of type SVG_GRAPH_TYPE_LINE or SVG_GRAPH_TYPE_STAIRCASE. 943 */ 944 protected function drawMetricArea(array $metric, array $paths) { 945 $y_zero = ($metric['options']['axisy'] == GRAPH_YAXIS_SIDE_RIGHT) ? $this->right_y_zero : $this->left_y_zero; 946 947 foreach ($paths as $path) { 948 if (count($path) > 1) { 949 $this->addItem(new CSvgGraphArea($path, $metric, $y_zero)); 950 } 951 } 952 } 953 954 /** 955 * Add line paths to graph for metric of type SVG_GRAPH_TYPE_LINE or SVG_GRAPH_TYPE_STAIRCASE. 956 */ 957 protected function drawMetricsLine() { 958 foreach ($this->metrics as $index => $metric) { 959 if (array_key_exists($index, $this->paths) && ($metric['options']['type'] == SVG_GRAPH_TYPE_LINE 960 || $metric['options']['type'] == SVG_GRAPH_TYPE_STAIRCASE)) { 961 if ($metric['options']['fill'] > 0) { 962 $this->drawMetricArea($metric, $this->paths[$index]); 963 } 964 965 $this->addItem(new CSvgGraphLineGroup($this->paths[$index], $metric)); 966 } 967 } 968 } 969 970 /** 971 * Add metric of type points to graph. 972 */ 973 protected function drawMetricsPoint() { 974 foreach ($this->metrics as $index => $metric) { 975 if ($metric['options']['type'] == SVG_GRAPH_TYPE_POINTS && array_key_exists($index, $this->paths)) { 976 $this->addItem(new CSvgGraphPoints(reset($this->paths[$index]), $metric)); 977 } 978 } 979 } 980 981 /** 982 * Add metric of type bar to graph. 983 */ 984 protected function drawMetricsBar() { 985 $bar_min_width = [ 986 GRAPH_YAXIS_SIDE_LEFT => $this->canvas_width * .25, 987 GRAPH_YAXIS_SIDE_RIGHT => $this->canvas_width * .25 988 ]; 989 $bar_groups_indexes = []; 990 $bar_groups_position = []; 991 992 foreach ($this->paths as $index => $path) { 993 if ($this->metrics[$index]['options']['type'] == SVG_GRAPH_TYPE_BAR) { 994 // If one second in displayed over multiple pixels, this shows number of px in second. 995 $sec_per_px = ceil(($this->time_till - $this->time_from) / $this->canvas_width); 996 $px_per_sec = ceil($this->canvas_width / ($this->time_till - $this->time_from)); 997 998 $y_axis_side = $this->metrics[$index]['options']['axisy']; 999 $time_points = array_keys($this->points[$index]); 1000 $last_point = 0; 1001 $path = reset($path); 1002 1003 foreach ($path as $point_index => $point) { 1004 $time_point = ($sec_per_px > $px_per_sec) 1005 ? floor($time_points[$point_index] / $sec_per_px) * $sec_per_px 1006 : $time_points[$point_index]; 1007 $bar_groups_indexes[$y_axis_side][$time_point][$index] = $point_index; 1008 $bar_groups_position[$y_axis_side][$time_point][$point_index] = $point[0]; 1009 1010 if ($last_point > 0) { 1011 $bar_min_width[$y_axis_side] = min($point[0] - $last_point, $bar_min_width[$y_axis_side]); 1012 } 1013 $last_point = $point[0]; 1014 } 1015 } 1016 } 1017 1018 foreach ($bar_groups_indexes as $y_axis => $points) { 1019 foreach ($points as $time_point => $paths) { 1020 $group_count = count($paths); 1021 $group_width = $bar_min_width[$y_axis]; 1022 $bar_width = ceil($group_width / $group_count * .75); 1023 $group_index = 0; 1024 foreach ($paths as $path_index => $point_index) { 1025 $group_x = $bar_groups_position[$y_axis][$time_point][$point_index]; 1026 if ($group_count > 1) { 1027 $this->paths[$path_index][0][$point_index][0] = $group_x 1028 // Calculate the leftmost X-coordinate including gap size. 1029 - $group_width * .375 1030 // Calculate the X-offset for the each bar in the group. 1031 + ceil($bar_width * ($group_index + .5)); 1032 $group_index++; 1033 } 1034 $this->paths[$path_index][0][$point_index][3] = max(1, $bar_width); 1035 // X position for bars group. 1036 $this->paths[$path_index][0][$point_index][4] = $group_x - $group_width * .375; 1037 } 1038 } 1039 } 1040 1041 foreach ($this->metrics as $index => $metric) { 1042 if ($metric['options']['type'] == SVG_GRAPH_TYPE_BAR && array_key_exists($index, $this->paths)) { 1043 $metric['options']['y_zero'] = ($metric['options']['axisy'] == GRAPH_YAXIS_SIDE_RIGHT) 1044 ? $this->right_y_zero 1045 : $this->left_y_zero; 1046 $metric['options']['bar_width'] = $bar_min_width[$metric['options']['axisy']]; 1047 1048 $this->addItem(new CSvgGraphBar(reset($this->paths[$index]), $metric)); 1049 } 1050 } 1051 } 1052 1053 /** 1054 * Add problems tooltip data to graph. 1055 */ 1056 protected function drawProblems() { 1057 $today = strtotime('today'); 1058 $container = (new CSvgGroup())->addClass(CSvgTag::ZBX_STYLE_GRAPH_PROBLEMS); 1059 1060 foreach ($this->problems as $problem) { 1061 // If problem is never recovered, it will be drown till the end of graph or till current time. 1062 $time_to = ($problem['r_clock'] == 0) 1063 ? min($this->time_till, time()) 1064 : min($this->time_till, $problem['r_clock']); 1065 $time_range = $this->time_till - $this->time_from; 1066 $x1 = $this->canvas_x + $this->canvas_width 1067 - $this->canvas_width * ($this->time_till - $problem['clock']) / $time_range; 1068 $x2 = $this->canvas_x + $this->canvas_width 1069 - $this->canvas_width * ($this->time_till - $time_to) / $time_range; 1070 1071 if ($this->canvas_x > $x1) { 1072 $x1 = $this->canvas_x; 1073 } 1074 1075 // Make problem info. 1076 if ($problem['r_clock'] != 0) { 1077 $status_str = _('RESOLVED'); 1078 $status_color = ZBX_STYLE_OK_UNACK_FG; 1079 } 1080 else { 1081 $status_str = _('PROBLEM'); 1082 $status_color = ZBX_STYLE_PROBLEM_UNACK_FG; 1083 1084 foreach ($problem['acknowledges'] as $acknowledge) { 1085 if ($acknowledge['action'] & ZBX_PROBLEM_UPDATE_CLOSE) { 1086 $status_str = _('CLOSING'); 1087 $status_color = ZBX_STYLE_OK_UNACK_FG; 1088 break; 1089 } 1090 } 1091 } 1092 1093 $clock_fmt = ($problem['clock'] >= $today) 1094 ? zbx_date2str(TIME_FORMAT_SECONDS, $problem['clock']) 1095 : zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['clock']); 1096 1097 if ($problem['r_clock'] != 0) { 1098 $r_clock_fmt = ($problem['r_clock'] >= $today) 1099 ? zbx_date2str(TIME_FORMAT_SECONDS, $problem['r_clock']) 1100 : zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['r_clock']); 1101 } 1102 else { 1103 $r_clock_fmt = ''; 1104 } 1105 1106 $info = [ 1107 'name' => $problem['name'], 1108 'clock' => $clock_fmt, 1109 'r_clock' => $r_clock_fmt, 1110 'url' => (new CUrl('tr_events.php')) 1111 ->setArgument('triggerid', $problem['objectid']) 1112 ->setArgument('eventid', $problem['eventid']) 1113 ->getUrl(), 1114 'r_eventid' => $problem['r_eventid'], 1115 'severity' => getSeverityStyle($problem['severity'], $problem['r_clock'] == 0), 1116 'status' => $status_str, 1117 'status_color' => $status_color 1118 ]; 1119 1120 // At least 3 pixels expected to be occupied to show the range. Show simple anotation otherwise. 1121 $draw_type = ($x2 - $x1) > 2 ? CSvgGraphAnnotation::TYPE_RANGE : CSvgGraphAnnotation::TYPE_SIMPLE; 1122 1123 // Draw border lines. Make them dashed if beginning or ending of highlighted zone is visible in graph. 1124 if ($problem['clock'] > $this->time_from) { 1125 $draw_type |= CSvgGraphAnnotation::DASH_LINE_START; 1126 } 1127 1128 if ($this->time_till > $time_to) { 1129 $draw_type |= CSvgGraphAnnotation::DASH_LINE_END; 1130 } 1131 1132 $container->addItem( 1133 (new CSvgGraphAnnotation($draw_type)) 1134 ->setInformation(json_encode($info)) 1135 ->setSize(min($x2 - $x1, $this->canvas_width), $this->canvas_height) 1136 ->setPosition(max($x1, $this->canvas_x), $this->canvas_y) 1137 ->setColor($this->color_annotation) 1138 ); 1139 } 1140 1141 $this->addItem($container); 1142 } 1143} 1144