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