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