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_max = null; 99 protected $left_y_units = null; 100 protected $left_y_empty = true; 101 102 protected $right_y_show = false; 103 protected $right_y_min = null; 104 protected $right_y_max = null; 105 protected $right_y_units = null; 106 protected $right_y_empty = true; 107 108 protected $right_y_zero = null; 109 protected $left_y_zero = null; 110 111 protected $x_show; 112 113 protected $offset_bottom; 114 115 /** 116 * Value for graph left offset. Is used as width for left Y axis container. 117 * 118 * @var int 119 */ 120 protected $offset_left = 20; 121 122 /** 123 * Value for graph right offset. Is used as width for right Y axis container. 124 * 125 * @var int 126 */ 127 protected $offset_right = 20; 128 129 /** 130 * Maximum width of container for every Y axis. 131 * 132 * @var int 133 */ 134 protected $max_yaxis_width = 120; 135 136 protected $offset_top; 137 protected $time_from; 138 protected $time_till; 139 140 /** 141 * Height for X axis container. 142 * 143 * @var int 144 */ 145 protected $xaxis_height = 20; 146 147 /** 148 * SVG default size. 149 */ 150 protected $width = 1000; 151 protected $height = 1000; 152 153 public function __construct(array $options) { 154 parent::__construct(); 155 156 // Set colors. 157 $theme = getUserGraphTheme(); 158 $this->text_color = '#' . $theme['textcolor']; 159 $this->grid_color = '#' . $theme['gridcolor']; 160 161 $this 162 ->addClass(ZBX_STYLE_SVG_GRAPH) 163 ->setTimePeriod($options['time_period']['time_from'], $options['time_period']['time_to']) 164 ->setXAxis($options['x_axis']) 165 ->setYAxisLeft($options['left_y_axis']) 166 ->setYAxisRight($options['right_y_axis']); 167 } 168 169 /** 170 * Get graph canvas X offset. 171 * 172 * @return int 173 */ 174 public function getCanvasX() { 175 return $this->canvas_x; 176 } 177 178 /** 179 * Get graph canvas Y offset. 180 * 181 * @return int 182 */ 183 public function getCanvasY() { 184 return $this->canvas_y; 185 } 186 187 /** 188 * Get graph canvas width. 189 * 190 * @return int 191 */ 192 public function getCanvasWidth() { 193 return $this->canvas_width; 194 } 195 196 /** 197 * Get graph canvas height. 198 * 199 * @return int 200 */ 201 public function getCanvasHeight() { 202 return $this->canvas_height; 203 } 204 205 /** 206 * Set problems data for graph. 207 * 208 * @param array $problems Array of problems data. 209 * 210 * @return CSvgGraph 211 */ 212 public function addProblems(array $problems) { 213 $this->problems = $problems; 214 215 return $this; 216 } 217 218 /** 219 * Set metrics data for graph. 220 * 221 * @param array $metrics Array of metrics data. 222 * 223 * @return CSvgGraph 224 */ 225 public function addMetrics(array $metrics = []) { 226 $metrics_for_each_axes = [ 227 GRAPH_YAXIS_SIDE_LEFT => 0, 228 GRAPH_YAXIS_SIDE_RIGHT => 0 229 ]; 230 231 foreach ($metrics as $i => $metric) { 232 $min_value = null; 233 $max_value = null; 234 235 if ($metric['points']) { 236 $metrics_for_each_axes[$metric['options']['axisy']]++; 237 238 foreach ($metric['points'] as $point) { 239 if ($min_value === null || $min_value > $point['value']) { 240 $min_value = $point['value']; 241 } 242 if ($max_value === null || $max_value < $point['value']) { 243 $max_value = $point['value']; 244 } 245 246 $this->points[$i][$point['clock']] = $point['value']; 247 } 248 249 if ($metric['options']['axisy'] == GRAPH_YAXIS_SIDE_LEFT) { 250 if ($this->min_value_left === null || $this->min_value_left > $min_value) { 251 $this->min_value_left = $min_value; 252 } 253 if ($this->max_value_left === null || $this->max_value_left < $max_value) { 254 $this->max_value_left = $max_value; 255 } 256 } 257 else { 258 if ($this->min_value_right === null || $this->min_value_right > $min_value) { 259 $this->min_value_right = $min_value; 260 } 261 if ($this->max_value_right === null || $this->max_value_right < $max_value) { 262 $this->max_value_right = $max_value; 263 } 264 } 265 } 266 267 $this->metrics[$i] = [ 268 'name' => $metric['hosts'][0]['name'].NAME_DELIMITER.$metric['name'], 269 'itemid' => $metric['itemid'], 270 'units' => $metric['units'], 271 'host' => $metric['hosts'][0], 272 'options' => ['order' => $i] + $metric['options'] 273 ]; 274 } 275 276 $this->left_y_empty = ($metrics_for_each_axes[GRAPH_YAXIS_SIDE_LEFT] == 0); 277 $this->right_y_empty = ($metrics_for_each_axes[GRAPH_YAXIS_SIDE_RIGHT] == 0); 278 279 return $this; 280 } 281 282 /** 283 * Set graph time period. 284 * 285 * @param int $time_from Timestamp. 286 * @param int @time_till Timestamp. 287 * 288 * @return CSvgGraph 289 */ 290 public function setTimePeriod($time_from, $time_till) { 291 $this->time_from = $time_from; 292 $this->time_till = $time_till; 293 294 return $this; 295 } 296 297 /** 298 * Set left side Y axis display options. 299 * 300 * @param array $options 301 * @param int $options['show'] 302 * @param string $options['min'] 303 * @param string $options['max'] 304 * @param string $options['units'] 305 * 306 * @return CSvgGraph 307 */ 308 public function setYAxisLeft(array $options) { 309 $this->left_y_show = ($options['show'] == SVG_GRAPH_AXIS_SHOW); 310 311 if ($options['min'] !== '') { 312 $this->left_y_min = $options['min']; 313 } 314 if ($options['max'] !== '') { 315 $this->left_y_max = $options['max']; 316 } 317 if ($options['units'] !== null) { 318 $units = trim(preg_replace('/\s+/', ' ', $options['units'])); 319 $this->left_y_units = htmlspecialchars($units); 320 } 321 322 return $this; 323 } 324 325 /** 326 * Set right side Y axis display options. 327 * 328 * @param array $options 329 * @param int $options['show'] 330 * @param string $options['min'] 331 * @param string $options['max'] 332 * @param string $options['units'] 333 * 334 * @return CSvgGraph 335 */ 336 public function setYAxisRight(array $options) { 337 $this->right_y_show = ($options['show'] == SVG_GRAPH_AXIS_SHOW); 338 339 if ($options['min'] !== '') { 340 $this->right_y_min = $options['min']; 341 } 342 if ($options['max'] !== '') { 343 $this->right_y_max = $options['max']; 344 } 345 if ($options['units'] !== null) { 346 $units = trim(preg_replace('/\s+/', ' ', $options['units'])); 347 $this->right_y_units = htmlspecialchars($units); 348 } 349 350 return $this; 351 } 352 353 /** 354 * Show or hide X axis. 355 * 356 * @param array $options 357 * 358 * @return CSvgGraph 359 */ 360 public function setXAxis(array $options) { 361 $this->x_show = ($options['show'] == SVG_GRAPH_AXIS_SHOW); 362 363 return $this; 364 } 365 366 /** 367 * Return array of horizontal labels with positions. Array key will be position, value will be label. 368 * 369 * @return array 370 */ 371 public function getTimeGridWithPosition() { 372 $period = $this->time_till - $this->time_from; 373 $step = round(bcmul(bcdiv($period, $this->canvas_width), 100)); // Grid cell (100px) in seconds. 374 375 /* 376 * In case if requested time period is so small that it is rounded to zero, we are displaying only two 377 * milestones on X axis - the start and the end of period. 378 */ 379 if ($step == 0) { 380 return [ 381 0 => date('H:i:s', $this->time_from), 382 $this->canvas_width => date('H:i:s', $this->time_till) 383 ]; 384 } 385 386 $start = $this->time_from + $step - $this->time_from % $step; 387 $time_formats = ['Y-n-d', 'n-d', 'n-d H:i','H:i', 'H:i:s']; 388 389 // Search for most appropriate time format. 390 foreach ($time_formats as $fmt) { 391 $grid_values = []; 392 393 for ($clock = $start; $this->time_till >= $clock; $clock += $step) { 394 $relative_pos = round($this->canvas_width - $this->canvas_width * ($this->time_till - $clock) / $period); 395 $grid_values[$relative_pos] = date($fmt, $clock); 396 } 397 398 /** 399 * If at least two calculated time-strings are equal, proceed with next format. Do that as long as each date 400 * is different or there is no more time formats to test. 401 */ 402 if (count(array_flip($grid_values)) == count($grid_values) || $fmt === end($time_formats)) { 403 break; 404 } 405 } 406 407 return $grid_values; 408 } 409 410 /** 411 * Add UI selection box element to graph. 412 * 413 * @return CSvgGraph 414 */ 415 public function addSBox() { 416 $this->addItem([ 417 (new CSvgRect(0, 0, 0, 0))->addClass('svg-graph-selection'), 418 (new CSvgText(0, 0, ''))->addClass('svg-graph-selection-text') 419 ]); 420 421 return $this; 422 } 423 424 /** 425 * Add UI helper line that follows mouse. 426 * 427 * @return CSvgGraph 428 */ 429 public function addHelper() { 430 $this->addItem((new CSvgLine(0, 0, 0, 0))->addClass(CSvgTag::ZBX_STYLE_GRAPH_HELPER)); 431 432 return $this; 433 } 434 435 /** 436 * Render graph. 437 * 438 * @return CSvgGraph 439 */ 440 public function draw() { 441 $this->applyMissingDataFunc(); 442 $this->calculateDimensions(); 443 $this->calculatePaths(); 444 445 $this->drawGrid(); 446 447 if ($this->left_y_show) { 448 $this->drawCanvasLeftYAxis(); 449 } 450 if ($this->right_y_show) { 451 $this->drawCanvasRightYAxis(); 452 } 453 if ($this->x_show) { 454 $this->drawCanvasXAxis(); 455 } 456 457 $this->drawMetricsLine(); 458 $this->drawMetricsPoint(); 459 460 $this->drawProblems(); 461 462 $this->addClipArea(); 463 464 return $this; 465 } 466 467 /** 468 * Add dynamic clip path to hide metric lines and area outside graph canvas. 469 */ 470 protected function addClipArea() { 471 $areaid = uniqid('metric_clip_'); 472 473 // CSS styles. 474 $this->styles['.'.CSvgTag::ZBX_STYLE_GRAPH_AREA]['clip-path'] = 'url(#'.$areaid.')'; 475 $this->styles['[data-metric]']['clip-path'] = 'url(#'.$areaid.')'; 476 477 $this->addItem( 478 (new CsvgTag('clipPath')) 479 ->addItem( 480 (new CSvgPath(implode(' ', [ 481 'M'.$this->canvas_x.','.($this->canvas_y - 3), 482 'H'.($this->canvas_width + $this->canvas_x), 483 'V'.($this->canvas_height + $this->canvas_y), 484 'H'.($this->canvas_x) 485 ]))) 486 ) 487 ->setAttribute('id', $areaid) 488 ); 489 } 490 491 /** 492 * Calculate canvas size, margins and offsets for graph canvas inside SVG element. 493 */ 494 protected function calculateDimensions() { 495 // Canvas height must be specified before call self::getValuesGridWithPosition. 496 $this->offset_top = 10; 497 $this->offset_bottom = self::SVG_GRAPH_X_AXIS_HEIGHT; 498 $this->canvas_height = $this->height - $this->offset_top - $this->offset_bottom; 499 $this->canvas_y = $this->offset_top; 500 501 // Set missing properties for left Y axis. 502 if ($this->left_y_min === null) { 503 $this->left_y_min = $this->min_value_left ? : 0; 504 } 505 if ($this->left_y_max === null) { 506 $this->left_y_max = $this->max_value_left ? : 1; 507 } 508 509 if (bccomp($this->left_y_min, $this->left_y_max) == 0) { 510 $this->left_y_min -= 0.5; 511 $this->left_y_max += 0.5; 512 } 513 elseif (bccomp($this->left_y_min, $this->left_y_max) == 1) { 514 $this->left_y_max = $this->left_y_min + 1; 515 } 516 517 $grid = $this->getValueGrid($this->left_y_min, $this->left_y_max); 518 $this->left_y_min = $grid[0]; 519 $this->left_y_max = end($grid); 520 521 if ($this->left_y_units === null) { 522 $this->left_y_units = ''; 523 foreach ($this->metrics as $metric) { 524 if ($metric['options']['axisy'] == GRAPH_YAXIS_SIDE_LEFT) { 525 $this->left_y_units = $metric['units']; 526 break; 527 } 528 } 529 } 530 531 // Set missing properties for right Y axis. 532 if ($this->right_y_min === null) { 533 $this->right_y_min = $this->min_value_right ? : 0; 534 } 535 if ($this->right_y_max === null) { 536 $this->right_y_max = $this->max_value_right ? : 1; 537 } 538 539 if (bccomp($this->right_y_min, $this->right_y_max) == 0) { 540 $this->right_y_min -= 0.5; 541 $this->right_y_max += 0.5; 542 } 543 elseif (bccomp($this->right_y_min, $this->right_y_max) == 1) { 544 $this->right_y_max = $this->right_y_min + 1; 545 } 546 547 $grid = $this->getValueGrid($this->right_y_min, $this->right_y_max); 548 $this->right_y_min = $grid[0]; 549 $this->right_y_max = end($grid); 550 551 if ($this->right_y_units === null) { 552 $this->right_y_units = ''; 553 foreach ($this->metrics as $metric) { 554 if ($metric['options']['axisy'] == GRAPH_YAXIS_SIDE_RIGHT) { 555 $this->right_y_units = $metric['units']; 556 break; 557 } 558 } 559 } 560 561 // Define canvas dimensions and offsets, except canvas height and bottom offset. 562 $approx_width = 10; 563 564 if ($this->left_y_show) { 565 $values = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_LEFT, $this->left_y_empty); 566 567 if ($values) { 568 $offset_left = max($this->offset_left, max(array_map('strlen', $values)) * $approx_width); 569 $this->offset_left = (int) min($offset_left, $this->max_yaxis_width); 570 } 571 } 572 573 if ($this->right_y_show) { 574 $values = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_RIGHT, $this->right_y_empty); 575 576 if ($values) { 577 $offset_right = max($this->offset_right, max(array_map('strlen', $values)) * $approx_width); 578 $offset_right += self::SVG_GRAPH_Y_AXIS_RIGHT_LABEL_MARGIN; 579 $this->offset_right = (int) min($offset_right, $this->max_yaxis_width); 580 } 581 } 582 583 $this->canvas_width = $this->width - $this->offset_left - $this->offset_right; 584 $this->canvas_x = $this->offset_left; 585 586 // Calculate Y = 0 position. 587 $delta = (($this->right_y_max - $this->right_y_min) ? : 1); 588 $this->right_y_zero = $this->canvas_y + $this->canvas_height * $this->right_y_max / $delta; 589 590 $delta = (($this->left_y_max - $this->left_y_min) ? : 1); 591 $this->left_y_zero = $this->canvas_y + $this->canvas_height * $this->left_y_max / $delta; 592 } 593 594 /** 595 * Get array of X points with labels, for grid and X/Y axes. Array key is Y coordinate for SVG, value is label with 596 * axis units. 597 * 598 * @param int $side Type of X axis: GRAPH_YAXIS_SIDE_RIGHT, GRAPH_YAXIS_SIDE_LEFT 599 * 600 * @return array 601 */ 602 protected function getValuesGridWithPosition($side, $empty_set = false) { 603 if ($empty_set) { 604 $units = ''; 605 } 606 elseif ($side === GRAPH_YAXIS_SIDE_LEFT) { 607 $min_value = $this->left_y_min; 608 $max_value = $this->left_y_max; 609 $units = $this->left_y_units; 610 } 611 else { 612 $min_value = $this->right_y_min; 613 $max_value = $this->right_y_max; 614 $units = $this->right_y_units; 615 } 616 617 $grid = $empty_set ? [0, 1] : $this->getValueGrid($min_value, $max_value); 618 $min_value = $grid[0]; 619 $max_value = end($grid); 620 $delta = ($max_value != $min_value) 621 ? $max_value - $min_value 622 : (count($grid) > 1 ? end($grid) - $grid[0] : 1); 623 $grid_values = []; 624 625 foreach ($grid as $value) { 626 $relative_pos = $this->canvas_height - intval($this->canvas_height * ($max_value - $value) / $delta); 627 628 if ($relative_pos >= 0 && $relative_pos <= $this->canvas_height) { 629 $grid_values[$relative_pos] = convert_units([ 630 'value' => $value, 631 'units' => $units, 632 'convert' => ITEM_CONVERT_NO_UNITS 633 ]); 634 } 635 } 636 637 return $grid_values; 638 } 639 640 /** 641 * Add Y axis with labels to left side of graph. 642 */ 643 protected function drawCanvasLeftYaxis() { 644 $grid_values = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_LEFT, $this->left_y_empty); 645 $this->addItem( 646 (new CSvgGraphAxis($grid_values, GRAPH_YAXIS_SIDE_LEFT)) 647 ->setSize($this->offset_left, $this->canvas_height) 648 ->setPosition($this->canvas_x - $this->offset_left, $this->canvas_y) 649 ->setTextColor($this->text_color) 650 ->setLineColor($this->grid_color) 651 ); 652 } 653 654 /** 655 * Add Y axis with labels to right side of graph. 656 */ 657 protected function drawCanvasRightYAxis() { 658 $grid_values = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_RIGHT, $this->right_y_empty); 659 660 // Do not draw label at the bottom of right Y axis to avoid label averlapping with horizontal axis arrow. 661 if (array_key_exists(0, $grid_values)) { 662 unset($grid_values[0]); 663 } 664 665 $this->addItem( 666 (new CSvgGraphAxis($grid_values, GRAPH_YAXIS_SIDE_RIGHT)) 667 ->setSize($this->offset_right, $this->canvas_height) 668 ->setPosition($this->canvas_x + $this->canvas_width, $this->canvas_y) 669 ->setTextColor($this->text_color) 670 ->setLineColor($this->grid_color) 671 ); 672 } 673 674 /** 675 * Add X axis with labels to graph. 676 */ 677 protected function drawCanvasXAxis() { 678 $this->addItem( 679 (new CSvgGraphAxis($this->getTimeGridWithPosition(), GRAPH_YAXIS_SIDE_BOTTOM)) 680 ->setSize($this->canvas_width, $this->xaxis_height) 681 ->setPosition($this->canvas_x, $this->canvas_y + $this->canvas_height) 682 ->setTextColor($this->text_color) 683 ->setLineColor($this->grid_color) 684 ); 685 } 686 687 /** 688 * Calculate array of points between $min and $max value. 689 * 690 * @param int $min Minimum value. 691 * @param int $max Maximum value. 692 * 693 * @return $array 694 */ 695 protected function getValueGrid($min, $max) { 696 $res = []; 697 698 // If absolute min/max is equal, calculate grid with 4 rows to make 0 centered. 5 rows otherwise. 699 $grid_rows = (abs($min) == abs($max)) ? 4 : 5; 700 $decimals = strlen(substr(strrchr($max, '.'), 1)); 701 $decimals = $decimals > 4 ? 4 : $decimals; 702 $decimals = $decimals < 2 ? 2 : $decimals; 703 for ($base = 10; $base > .01; $base /= 10) { 704 $mul = ($max > 0) ? 1 / pow($base, floor(log10($max))) : 1; 705 $max10 = ceil($mul * $max) / $mul; 706 $min10 = floor($mul * $min) / $mul; 707 $delta = $max10 - $min10; 708 $delta = ceil($mul * $delta) / $mul; 709 710 if ($mul >= 1) { 711 if ($delta) { 712 for ($i = 0; $delta >= $i; $i += $delta / $grid_rows) { 713 $res[] = sprintf('%.'.$decimals.'f', $i + $min10); 714 } 715 } 716 else { 717 $res[] = $min10; 718 } 719 break; 720 } 721 } 722 723 return $res; 724 } 725 726 /** 727 * Add grid to graph. 728 */ 729 protected function drawGrid() { 730 $time_points = $this->x_show ? $this->getTimeGridWithPosition() : []; 731 $value_points = []; 732 733 if ($this->left_y_show) { 734 $value_points = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_LEFT, $this->left_y_empty); 735 736 unset($time_points[0]); 737 } 738 elseif ($this->right_y_show) { 739 $value_points = $this->getValuesGridWithPosition(GRAPH_YAXIS_SIDE_RIGHT, $this->right_y_empty); 740 741 unset($time_points[$this->canvas_width]); 742 } 743 744 if ($this->x_show) { 745 unset($value_points[0]); 746 } 747 748 $this->addItem((new CSvgGraphGrid($value_points, $time_points)) 749 ->setPosition($this->canvas_x, $this->canvas_y) 750 ->setSize($this->canvas_width, $this->canvas_height) 751 ->setColor($this->grid_color) 752 ); 753 } 754 755 /** 756 * Calculate paths for metric elements. 757 */ 758 protected function calculatePaths() { 759 // Metric having very big values of y outside visible area will fail to render. 760 $y_max = pow(2, 16); 761 $y_min = $y_max * -1; 762 763 foreach ($this->metrics as $index => $metric) { 764 if (!array_key_exists($index, $this->points)) { 765 continue; 766 } 767 768 if ($metric['options']['axisy'] == GRAPH_YAXIS_SIDE_RIGHT) { 769 $min_value = $this->right_y_min; 770 $max_value = $this->right_y_max; 771 } 772 else { 773 $min_value = $this->left_y_min; 774 $max_value = $this->left_y_max; 775 } 776 777 $time_range = ($this->time_till - $this->time_from) ? : 1; 778 $value_diff = ($max_value - $min_value) ? : 1; 779 $timeshift = $metric['options']['timeshift']; 780 $paths = []; 781 782 $path_num = 0; 783 foreach ($this->points[$index] as $clock => $point) { 784 // If missing data function is SVG_GRAPH_MISSING_DATA_NONE, path should be split in multiple svg shapes. 785 if ($point === null) { 786 $path_num++; 787 continue; 788 } 789 790 /** 791 * Avoid invisible data point drawing. Data sets of type != SVG_GRAPH_TYPE_POINTS cannot be skipped to 792 * keep shape unchanged. 793 */ 794 $in_range = ($max_value >= $point && $min_value <= $point); 795 if ($in_range || $metric['options']['type'] != SVG_GRAPH_TYPE_POINTS) { 796 $x = $this->canvas_x + $this->canvas_width 797 - $this->canvas_width * ($this->time_till - $clock + $timeshift) / $time_range; 798 $y = $this->canvas_y + $this->canvas_height * ($max_value - $point) / $value_diff; 799 800 if (!$in_range) { 801 $y = ($point > $max_value) ? max($y_min, $y) : min($y_max, $y); 802 } 803 804 $paths[$path_num][] = [$x, ceil($y), convert_units([ 805 'value' => $point, 806 'units' => $metric['units'] 807 ])]; 808 } 809 } 810 811 if ($paths) { 812 $this->paths[$index] = $paths; 813 } 814 } 815 } 816 817 /** 818 * Modifies metric data and Y value range according specified missing data function. 819 */ 820 protected function applyMissingDataFunc() { 821 foreach ($this->metrics as $index => $metric) { 822 /** 823 * - Missing data points are calculated only between existing data points; 824 * - Missing data points are not calculated for SVG_GRAPH_TYPE_POINTS metrics; 825 * - SVG_GRAPH_MISSING_DATA_CONNECTED is default behavior of SVG graphs, so no need to calculate anything 826 * here. 827 */ 828 if (array_key_exists($index, $this->points) 829 && $metric['options']['type'] != SVG_GRAPH_TYPE_POINTS 830 && $metric['options']['missingdatafunc'] != SVG_GRAPH_MISSING_DATA_CONNECTED) { 831 $points = &$this->points[$index]; 832 $missing_data_points = $this->getMissingData($points, $metric['options']['missingdatafunc']); 833 834 // Sort according new clock times (array keys). 835 $points += $missing_data_points; 836 ksort($points); 837 838 // Missing data function can change min value of Y axis. 839 if ($missing_data_points 840 && $metric['options']['missingdatafunc'] == SVG_GRAPH_MISSING_DATA_TREAT_AS_ZERO) { 841 if ($this->min_value_left > 0 && $metric['options']['axisy'] == GRAPH_YAXIS_SIDE_LEFT) { 842 $this->min_value_left = 0; 843 } 844 elseif ($this->min_value_right > 0 && $metric['options']['axisy'] == GRAPH_YAXIS_SIDE_RIGHT) { 845 $this->min_value_right = 0; 846 } 847 } 848 } 849 } 850 } 851 852 /** 853 * Calculate missing data for given set of $points according given $missingdatafunc. 854 * 855 * @param array $points Array of metric points to modify, where key is metric timestamp. 856 * @param int $missingdatafunc Type of function, allowed value: 857 * SVG_GRAPH_MISSING_DATA_TREAT_AS_ZERO, SVG_GRAPH_MISSING_DATA_NONE, 858 * SVG_GRAPH_MISSING_DATA_CONNECTED 859 * 860 * @return array Array of calculated missing data points. 861 */ 862 protected function getMissingData(array $points = [], $missingdatafunc) { 863 // Get average distance between points to detect gaps of missing data. 864 $prev_clock = null; 865 $points_distance = []; 866 foreach ($points as $clock => $point) { 867 if ($prev_clock !== null) { 868 $points_distance[] = $clock - $prev_clock; 869 } 870 $prev_clock = $clock; 871 } 872 873 /** 874 * $threshold is a minimal period of time at what we assume that data point is missed; 875 * $average_distance is an average distance between existing data points; 876 * $gap_interval is a time distance between missing points used to fulfill gaps of missing data. 877 * It's unique for each gap. 878 */ 879 $average_distance = $points_distance ? array_sum($points_distance) / count($points_distance) : 0; 880 $threshold = $points_distance ? $average_distance * 3 : 0; 881 882 // Add missing values. 883 $prev_clock = null; 884 $missing_points = []; 885 foreach ($points as $clock => $point) { 886 if ($prev_clock !== null && ($clock - $prev_clock) > $threshold) { 887 $gap_interval = floor(($clock - $prev_clock) / $threshold); 888 889 if ($missingdatafunc == SVG_GRAPH_MISSING_DATA_NONE) { 890 $missing_points[$prev_clock + $gap_interval] = null; 891 } 892 elseif ($missingdatafunc == SVG_GRAPH_MISSING_DATA_TREAT_AS_ZERO) { 893 $missing_points[$prev_clock + $gap_interval] = 0; 894 $missing_points[$clock - $gap_interval] = 0; 895 } 896 } 897 898 $prev_clock = $clock; 899 } 900 901 return $missing_points; 902 } 903 904 /** 905 * Add fill area to graph for metric of type SVG_GRAPH_TYPE_LINE or SVG_GRAPH_TYPE_STAIRCASE. 906 */ 907 protected function drawMetricArea(array $metric, array $paths) { 908 $y_zero = ($metric['options']['axisy'] == GRAPH_YAXIS_SIDE_RIGHT) ? $this->right_y_zero : $this->left_y_zero; 909 910 foreach ($paths as $path) { 911 if (count($path) > 1) { 912 $this->addItem(new CSvgGraphArea($path, $metric, $y_zero)); 913 } 914 } 915 } 916 917 /** 918 * Add line paths to graph for metric of type SVG_GRAPH_TYPE_LINE or SVG_GRAPH_TYPE_STAIRCASE. 919 */ 920 protected function drawMetricsLine() { 921 foreach ($this->metrics as $index => $metric) { 922 if (array_key_exists($index, $this->paths) && ($metric['options']['type'] == SVG_GRAPH_TYPE_LINE 923 || $metric['options']['type'] == SVG_GRAPH_TYPE_STAIRCASE)) { 924 if ($metric['options']['fill'] > 0) { 925 $this->drawMetricArea($metric, $this->paths[$index]); 926 } 927 928 $this->addItem(new CSvgGraphLineGroup($this->paths[$index], $metric)); 929 } 930 } 931 } 932 933 /** 934 * Add metric of type points to graph. 935 */ 936 protected function drawMetricsPoint() { 937 foreach ($this->metrics as $index => $metric) { 938 if ($metric['options']['type'] == SVG_GRAPH_TYPE_POINTS && array_key_exists($index, $this->paths)) { 939 $this->addItem(new CSvgGraphPoints(reset($this->paths[$index]), $metric)); 940 } 941 } 942 } 943 944 /** 945 * Add problems tooltip data to graph. 946 */ 947 protected function drawProblems() { 948 $today = strtotime('today'); 949 $container = (new CSvgGroup())->addClass(CSvgTag::ZBX_STYLE_GRAPH_PROBLEMS); 950 951 foreach ($this->problems as $problem) { 952 // If problem is never recovered, it will be drown till the end of graph or till current time. 953 $time_to = ($problem['r_clock'] == 0) 954 ? min($this->time_till, time()) 955 : min($this->time_till, $problem['r_clock']); 956 $time_range = $this->time_till - $this->time_from; 957 $x1 = $this->canvas_x + $this->canvas_width 958 - $this->canvas_width * ($this->time_till - $problem['clock']) / $time_range; 959 $x2 = $this->canvas_x + $this->canvas_width 960 - $this->canvas_width * ($this->time_till - $time_to) / $time_range; 961 962 if ($this->canvas_x > $x1) { 963 $x1 = $this->canvas_x; 964 } 965 966 // Make problem info. 967 if ($problem['r_clock'] != 0) { 968 $status_str = _('RESOLVED'); 969 $status_color = ZBX_STYLE_OK_UNACK_FG; 970 } 971 else { 972 $status_str = _('PROBLEM'); 973 $status_color = ZBX_STYLE_PROBLEM_UNACK_FG; 974 975 foreach ($problem['acknowledges'] as $acknowledge) { 976 if ($acknowledge['action'] & ZBX_PROBLEM_UPDATE_CLOSE) { 977 $status_str = _('CLOSING'); 978 $status_color = ZBX_STYLE_OK_UNACK_FG; 979 break; 980 } 981 } 982 } 983 984 $info = [ 985 'name' => $problem['name'], 986 'clock' => ($problem['clock'] >= $today) 987 ? zbx_date2str(TIME_FORMAT_SECONDS, $problem['clock']) 988 : zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['clock']), 989 'r_clock' => ($problem['r_clock'] != 0) 990 ? ($problem['r_clock'] >= $today) 991 ? zbx_date2str(TIME_FORMAT_SECONDS, $problem['r_clock']) 992 : zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['r_clock']) 993 : '', 994 'url' => (new CUrl('tr_events.php')) 995 ->setArgument('triggerid', $problem['objectid']) 996 ->setArgument('eventid', $problem['eventid']) 997 ->getUrl(), 998 'r_eventid' => $problem['r_eventid'], 999 'severity' => getSeverityStyle($problem['severity'], $problem['r_clock'] == 0), 1000 'status' => $status_str, 1001 'status_color' => $status_color 1002 ]; 1003 1004 // At least 3 pixels expected to be occupied to show the range. Show simple anotation otherwise. 1005 $draw_type = ($x2 - $x1) > 2 ? CSvgGraphAnnotation::TYPE_RANGE : CSvgGraphAnnotation::TYPE_SIMPLE; 1006 1007 // Draw border lines. Make them dashed if beginning or ending of highlighted zone is visible in graph. 1008 if ($problem['clock'] > $this->time_from) { 1009 $draw_type |= CSvgGraphAnnotation::DASH_LINE_START; 1010 } 1011 1012 if ($this->time_till > $time_to) { 1013 $draw_type |= CSvgGraphAnnotation::DASH_LINE_END; 1014 } 1015 1016 $container->addItem( 1017 (new CSvgGraphAnnotation($draw_type)) 1018 ->setInformation(CJs::encodeJson($info)) 1019 ->setSize(min($x2 - $x1, $this->canvas_width), $this->canvas_height) 1020 ->setPosition(max($x1, $this->canvas_x), $this->canvas_y) 1021 ->setColor($this->color_annotation) 1022 ); 1023 } 1024 1025 $this->addItem($container); 1026 } 1027} 1028