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
22class CLineGraphDraw extends CGraphDraw {
23	const GRAPH_WIDTH_MIN = 20;
24	const GRAPH_HEIGHT_MIN = 20;
25	const legendOffsetY = 90;
26
27	public function __construct($type = GRAPH_TYPE_NORMAL) {
28		parent::__construct($type);
29		$this->yaxismin = null;
30		$this->yaxismax = null;
31		$this->triggers = [];
32		$this->ymin_type = GRAPH_YAXIS_TYPE_CALCULATED;
33		$this->ymax_type = GRAPH_YAXIS_TYPE_CALCULATED;
34		$this->yaxis = [
35			GRAPH_YAXIS_SIDE_LEFT => false,
36			GRAPH_YAXIS_SIDE_RIGHT => false
37		];
38		$this->skipLeftScale = 0; // in case if left axis should be drawn but doesn't contain any data
39		$this->skipRightScale = 0; // in case if right axis should be drawn but doesn't contain any data
40		$this->ymin_itemid = 0;
41		$this->ymax_itemid = 0;
42		$this->percentile = [
43			GRAPH_YAXIS_SIDE_LEFT => [
44				'percent' => 0, // draw percentage line
45				'value' => 0 // calculated percentage value left y axis
46			],
47			GRAPH_YAXIS_SIDE_RIGHT => [
48				'percent' => 0, // draw percentage line
49				'value' => 0 // calculated percentage value right y axis
50			]
51		];
52		$this->outer = false;
53		$this->m_showWorkPeriod = 1;
54		$this->m_showTriggers = 1;
55		$this->zero = [];
56		$this->graphOrientation = [
57			GRAPH_YAXIS_SIDE_LEFT => '',
58			GRAPH_YAXIS_SIDE_RIGHT => ''
59		];
60		$this->gridLinesCount = []; // How many grids to draw
61		$this->gridStep = []; // grid step
62		$this->gridPixels = 30; // optimal grid size
63		$this->gridPixelsVert = 40;
64		$this->axis_valuetype = []; // overal items type (int/float)
65		$this->drawItemsLegend = false; // draw items legend
66		$this->drawExLegend = false; // draw percentile and triggers legend
67	}
68
69	/********************************************************************************************************/
70	// PRE CONFIG:	ADD / SET / APPLY
71	/********************************************************************************************************/
72	public function showWorkPeriod($value) {
73		$this->m_showWorkPeriod = ($value == 1) ? 1 : 0;
74	}
75
76	public function showTriggers($value) {
77		$this->m_showTriggers = ($value == 1) ? 1 : 0;
78	}
79
80	/**
81	 * Add single item object to graph. If invalid 'delay' interval passed method will interrupt current request with
82	 * error message.
83	 *
84	 * @param array  $item                  Array of graph item properties.
85	 * @param string $item['itemid']        Item id.
86	 * @param string $item['type']          Item type.
87	 * @param string $item['name']          Item host display name.
88	 * @param string $item['hostname']      Item hostname.
89	 * @param string $item['key_']          Item key_ field value.
90	 * @param string $item['value_type']    Item value type.
91	 * @param string $item['history']       Item history field value.
92	 * @param string $item['trends']        Item trends field value.
93	 * @param string $item['delay']         Item delay.
94	 * @param string $item['master_itemid'] Master item id for item of type ITEM_TYPE_DEPENDENT.
95	 * @param string $item['units']         Item units value.
96	 * @param string $item['hostid']        Item host id.
97	 * @param string $item['hostname']      Item host name.
98	 * @param string $item['color']         Item presentation color.
99	 * @param int    $item['drawtype']      Item presentation draw type, could be one of GRAPH_ITEM_DRAWTYPE_* constants.
100	 * @param int    $item['yaxisside']     Item axis side, could be one of GRAPH_YAXIS_SIDE_* constants.
101	 * @param int    $item['calc_fnc']      Item calculation function, could be one of CALC_FNC_* constants.
102	 * @param int    $item['calc_type']     Item graph presentation calculation type, GRAPH_ITEM_SIMPLE or GRAPH_ITEM_SUM.
103	 *
104	 */
105	public function addItem(array $graph_item) {
106		if ($this->type == GRAPH_TYPE_STACKED) {
107			$graph_item['drawtype'] = GRAPH_ITEM_DRAWTYPE_FILLED_REGION;
108		}
109		$update_interval_parser = new CUpdateIntervalParser(['usermacros' => true]);
110
111		if ($update_interval_parser->parse($graph_item['delay']) != CParser::PARSE_SUCCESS) {
112			show_error_message(_s('Incorrect value for field "%1$s": %2$s.', 'delay', _('invalid delay')));
113			exit;
114		}
115
116		// Set graph item safe default values.
117		$graph_item += [
118			'color' => 'Dark Green',
119			'drawtype' => GRAPH_ITEM_DRAWTYPE_LINE,
120			'yaxisside' => GRAPH_YAXIS_SIDE_DEFAULT,
121			'calc_fnc' => CALC_FNC_AVG,
122			'calc_type' => GRAPH_ITEM_SIMPLE
123		];
124		$this->items[$this->num] = $graph_item;
125
126		$this->yaxis[$graph_item['yaxisside']] = true;
127
128		$this->num++;
129	}
130
131	public function setGraphOrientation($value, $yaxisside) {
132		if ($value < 0) {
133			$this->graphOrientation[$yaxisside] = '-';
134		}
135		elseif (zbx_empty($this->graphOrientation[$yaxisside]) && $value > 0) {
136			$this->graphOrientation[$yaxisside] = '+';
137		}
138		return $this->graphOrientation[$yaxisside];
139	}
140
141	public function setYMinAxisType($yaxistype) {
142		$this->ymin_type = $yaxistype;
143	}
144
145	public function setYMaxAxisType($yaxistype) {
146		$this->ymax_type = $yaxistype;
147	}
148
149	public function setYAxisMin($yaxismin) {
150		$this->yaxismin = $yaxismin;
151	}
152
153	public function setYAxisMax($yaxismax) {
154		$this->yaxismax = $yaxismax;
155	}
156
157	public function setYMinItemId($itemid) {
158		$this->ymin_itemid = $itemid;
159	}
160
161	public function setYMaxItemId($itemid) {
162		$this->ymax_itemid = $itemid;
163	}
164
165	public function setLeftPercentage($percentile) {
166		$this->percentile[GRAPH_YAXIS_SIDE_LEFT]['percent'] = $percentile;
167	}
168
169	public function setRightPercentage($percentile) {
170		$this->percentile[GRAPH_YAXIS_SIDE_RIGHT]['percent'] = $percentile;
171	}
172
173	public function setOuter($outer) {
174		$this->outer = $outer;
175	}
176
177	protected function selectData() {
178		$this->data = [];
179		$now = time();
180
181		if (!isset($this->stime)) {
182			$this->stime = $now - $this->period;
183		}
184
185		$this->diffTZ = (date('Z', $this->stime) - date('Z', $this->stime + $this->period));
186		$this->from_time = $this->stime; // + timeZone offset
187		$this->to_time = $this->stime + $this->period; // + timeZone offset
188
189		$p = $this->to_time - $this->from_time; // graph size in time
190		$x = $this->sizeX; // graph size in px
191
192		$this->itemsHost = null;
193
194		$config = select_config();
195		$items = [];
196
197		for ($i = 0; $i < $this->num; $i++) {
198			$item = $this->items[$i];
199
200			if ($this->itemsHost === null) {
201				$this->itemsHost = $item['hostid'];
202			}
203			elseif ($this->itemsHost != $item['hostid']) {
204				$this->itemsHost = false;
205			}
206
207			if (!isset($this->axis_valuetype[$item['yaxisside']])) {
208				$this->axis_valuetype[$item['yaxisside']] = $item['value_type'];
209			}
210			elseif ($this->axis_valuetype[$item['yaxisside']] != $item['value_type']) {
211				$this->axis_valuetype[$item['yaxisside']] = ITEM_VALUE_TYPE_FLOAT;
212			}
213
214			$type = $item['calc_type'];
215			$to_resolve = [];
216
217			// Override item history setting with housekeeping settings, if they are enabled in config.
218			if ($config['hk_history_global']) {
219				$item['history'] = timeUnitToSeconds($config['hk_history']);
220			}
221			else {
222				$to_resolve[] = 'history';
223			}
224
225			if ($config['hk_trends_global']) {
226				$item['trends'] = timeUnitToSeconds($config['hk_trends']);
227			}
228			else {
229				$to_resolve[] = 'trends';
230			}
231
232			// Otherwise, resolve user macro and parse the string. If successful, convert to seconds.
233			if ($to_resolve) {
234				$item = CMacrosResolverHelper::resolveTimeUnitMacros([$item], $to_resolve)[0];
235
236				$simple_interval_parser = new CSimpleIntervalParser();
237
238				if (!$config['hk_history_global']) {
239					if ($simple_interval_parser->parse($item['history']) != CParser::PARSE_SUCCESS) {
240						show_error_message(_s('Incorrect value for field "%1$s": %2$s.', 'history',
241							_('invalid history storage period')
242						));
243						exit;
244					}
245					$item['history'] = timeUnitToSeconds($item['history']);
246				}
247
248				if (!$config['hk_trends_global']) {
249					if ($simple_interval_parser->parse($item['trends']) != CParser::PARSE_SUCCESS) {
250						show_error_message(_s('Incorrect value for field "%1$s": %2$s.', 'trends',
251							_('invalid trend storage period')
252						));
253						exit;
254					}
255					$item['trends'] = timeUnitToSeconds($item['trends']);
256				}
257			}
258
259			$item['source'] = ($item['trends'] == 0 || ($item['history'] > time() - ($this->from_time + $this->period / 2)
260					&& $this->period / $this->sizeX <= ZBX_MAX_TREND_DIFF / ZBX_GRAPH_MAX_SKIP_CELL))
261					? 'history' : 'trends';
262
263			$items[] = $item;
264		}
265
266		$results = Manager::History()->getGraphAggregation($items, $this->from_time, $this->to_time, $x);
267
268		foreach ($items as $item) {
269			if (!array_key_exists($item['itemid'], $this->data)) {
270				$this->data[$item['itemid']] = [];
271			}
272
273			if (!array_key_exists($type, $this->data[$item['itemid']])) {
274				$this->data[$item['itemid']][$type] = [];
275			}
276
277			$curr_data = &$this->data[$item['itemid']][$type];
278
279			$curr_data['count'] = null;
280			$curr_data['min'] = null;
281			$curr_data['max'] = null;
282			$curr_data['avg'] = null;
283			$curr_data['clock'] = null;
284
285			if (array_key_exists($item['itemid'], $results)) {
286				$result = $results[$item['itemid']];
287				$this->dataFrom = $result['source'];
288
289				foreach ($result['data'] as $row) {
290					$idx = $row['i'] - 1;
291					if ($idx < 0) {
292						continue;
293					}
294
295					/* --------------------------------------------------
296						We are taking graph on 1px more than we need,
297						and here we are skipping first px, because of MOD (in SELECT),
298						it combines prelast point (it would be last point if not that 1px in beginning)
299						and first point, but we still losing prelast point :(
300						but now we've got the first point.
301					--------------------------------------------------*/
302					$curr_data['count'][$idx] = $row['count'];
303					$curr_data['min'][$idx] = $row['min'];
304					$curr_data['max'][$idx] = $row['max'];
305					$curr_data['avg'][$idx] = $row['avg'];
306					$curr_data['clock'][$idx] = $row['clock'];
307					$curr_data['shift_min'][$idx] = 0;
308					$curr_data['shift_max'][$idx] = 0;
309					$curr_data['shift_avg'][$idx] = 0;
310				}
311
312				unset($result);
313			}
314			else {
315				$this->dataFrom = $item['source'];
316			}
317
318			$loc_min = is_array($curr_data['min']) ? min($curr_data['min']) : null;
319			$this->setGraphOrientation($loc_min, $item['yaxisside']);
320
321			$curr_data['avg_orig'] = is_array($curr_data['avg']) ? zbx_avg($curr_data['avg']) : null;
322
323			// calculate missed points
324			$first_idx = 0;
325
326			/*
327				first_idx - last existing point
328				ci - current index
329				cj - count of missed in one go
330				dx - offset to first value (count to last existing point)
331			*/
332			for ($ci = 0, $cj = 0; $ci < $this->sizeX; $ci++) {
333				if (!isset($curr_data['count'][$ci]) || ($curr_data['count'][$ci] == 0)) {
334					$curr_data['count'][$ci] = 0;
335					$curr_data['shift_min'][$ci] = 0;
336					$curr_data['shift_max'][$ci] = 0;
337					$curr_data['shift_avg'][$ci] = 0;
338					$cj++;
339					continue;
340				}
341
342				if ($cj == 0) {
343					continue;
344				}
345
346				$dx = $cj + 1;
347				$first_idx = $ci - $dx;
348
349				if ($first_idx < 0) {
350					$first_idx = $ci; // if no data from start of graph get current data as first data
351				}
352
353				for(; $cj > 0; $cj--) {
354					if ($dx < ($this->sizeX / 20) && $this->type == GRAPH_TYPE_STACKED) {
355						$curr_data['count'][$ci - ($dx - $cj)] = 1;
356					}
357
358					foreach (['clock', 'min', 'max', 'avg'] as $var_name) {
359						$var = &$curr_data[$var_name];
360
361						if ($first_idx == $ci && $var_name == 'clock') {
362							$var[$ci - ($dx - $cj)] = $var[$first_idx] - (($p / $this->sizeX) * ($dx - $cj));
363							continue;
364						}
365
366						$dy = $var[$ci] - $var[$first_idx];
367						$var[$ci - ($dx - $cj)] = bcadd($var[$first_idx] , bcdiv(($cj * $dy) , $dx));
368					}
369				}
370			}
371
372			if ($cj > 0 && $ci > $cj) {
373				$dx = $cj + 1;
374				$first_idx = $ci - $dx;
375
376				for(;$cj > 0; $cj--) {
377					foreach (['clock', 'min', 'max', 'avg'] as $var_name) {
378						$var = &$curr_data[$var_name];
379
380						if ($var_name == 'clock') {
381							$var[$first_idx + ($dx - $cj)] = $var[$first_idx] + (($p / $this->sizeX) * ($dx - $cj));
382							continue;
383						}
384						$var[$first_idx + ($dx - $cj)] = $var[$first_idx];
385					}
386				}
387			}
388		}
389
390		unset($items);
391		unset($results);
392
393		// calculate shift for stacked graphs
394		if ($this->type == GRAPH_TYPE_STACKED) {
395			for ($i = 1; $i < $this->num; $i++) {
396				$item1 = $this->items[$i];
397
398				$curr_data = &$this->data[$item1['itemid']][$item1['calc_type']];
399
400				if (!isset($curr_data)) {
401					continue;
402				}
403
404				for ($j = $i - 1; $j >= 0; $j--) {
405					$item2 = $this->items[$j];
406
407					if ($item2['yaxisside'] != $item1['yaxisside']) {
408						continue;
409					}
410
411					$prev_data = &$this->data[$item2['itemid']][$item2['calc_type']];
412
413					if (!isset($prev_data)) {
414						continue;
415					}
416
417					for ($ci = 0; $ci < $this->sizeX; $ci++) {
418						foreach (['min', 'max', 'avg'] as $var_name) {
419							$shift_var_name = 'shift_'.$var_name;
420							$curr_shift = &$curr_data[$shift_var_name];
421							$prev_shift = &$prev_data[$shift_var_name];
422							$prev_var = &$prev_data[$var_name];
423
424							$prev_var_ci = ($prev_var === null) ? 0 : $prev_var[$ci];
425							$prev_shift_ci = ($prev_shift === null) ? 0 : $prev_shift[$ci];
426							$curr_shift[$ci] = $prev_var_ci + $prev_shift_ci;
427						}
428					}
429					break;
430				}
431			}
432		}
433	}
434
435	protected function selectTriggers() {
436		$this->triggers = [];
437		if ($this->m_showTriggers != 1) {
438			return;
439		}
440
441		$max = 3;
442		$cnt = 0;
443
444		foreach ($this->items as $item) {
445			$db_triggers = DBselect(
446				'SELECT DISTINCT h.host,tr.description,tr.triggerid,tr.expression,tr.priority,tr.value'.
447				' FROM triggers tr,functions f,items i,hosts h'.
448				' WHERE tr.triggerid=f.triggerid'.
449					" AND f.name IN ('last','min','avg','max')".
450					' AND tr.status='.TRIGGER_STATUS_ENABLED.
451					' AND i.itemid=f.itemid'.
452					' AND h.hostid=i.hostid'.
453					' AND f.itemid='.zbx_dbstr($item['itemid']).
454				' ORDER BY tr.priority'
455			);
456			while (($trigger = DBfetch($db_triggers)) && $cnt < $max) {
457				$db_fnc_cnt = DBselect(
458					'SELECT COUNT(*) AS cnt'.
459					' FROM functions f'.
460					' WHERE f.triggerid='.zbx_dbstr($trigger['triggerid'])
461				);
462				$fnc_cnt = DBfetch($db_fnc_cnt);
463
464				if ($fnc_cnt['cnt'] != 1) {
465					continue;
466				}
467
468				$trigger['expression'] = CMacrosResolverHelper::resolveTriggerExpressionUserMacro($trigger);
469
470				if (!preg_match(
471					'/^\{([0-9]+)\}\s*?([<>=]|[<>][=])\s*?([\-0-9\.]+)(['.ZBX_BYTE_SUFFIXES.ZBX_TIME_SUFFIXES.']?)$/',
472						$trigger['expression'], $arr)) {
473					continue;
474				}
475
476				$constant = $arr[3].$arr[4];
477
478				$this->triggers[] = [
479					'yaxisside' => $item['yaxisside'],
480					'val' => convert($constant),
481					'color' => getSeverityColor($trigger['priority']),
482					'description' => _('Trigger').NAME_DELIMITER.CMacrosResolverHelper::resolveTriggerName($trigger),
483					'constant' => '['.$arr[2].' '.$constant.']'
484				];
485				++$cnt;
486			}
487		}
488	}
489
490	/********************************************************************************************************/
491	// CALCULATIONS
492	/********************************************************************************************************/
493	// calculates percentages for left & right Y axis
494	protected function calcPercentile() {
495		if ($this->type != GRAPH_TYPE_NORMAL) {
496			return ;
497		}
498
499		$values = [
500			GRAPH_YAXIS_SIDE_LEFT => [],
501			GRAPH_YAXIS_SIDE_RIGHT=> []
502		];
503
504		$maxX = $this->sizeX;
505
506		// for each metric
507		for ($item = 0; $item < $this->num; $item++) {
508			$data = &$this->data[$this->items[$item]['itemid']][$this->items[$item]['calc_type']];
509
510			if (!isset($data)) {
511				continue;
512			}
513
514			// for each X
515			for ($i = 0; $i < $maxX; $i++) { // new point
516				if ($data['count'][$i] == 0) {
517					continue;
518				}
519
520				$min = $data['min'][$i];
521				$max = $data['max'][$i];
522				$avg = $data['avg'][$i];
523
524				switch ($this->items[$item]['calc_fnc']) {
525					case CALC_FNC_MAX:
526						$value = $max;
527						break;
528					case CALC_FNC_MIN:
529						$value = $min;
530						break;
531					case CALC_FNC_ALL:
532					case CALC_FNC_AVG:
533					default:
534						$value = $avg;
535				}
536
537				$values[$this->items[$item]['yaxisside']][] = $value;
538			}
539		}
540
541		foreach ($this->percentile as $side => $percentile) {
542			if ($percentile['percent'] > 0 && $values[$side]) {
543				sort($values[$side]);
544				// Using "Nearest Rank" method: http://en.wikipedia.org/wiki/Percentile#Definition_of_the_Nearest_Rank_method
545				$percent = (int) ceil($percentile['percent'] / 100 * count($values[$side]));
546				$this->percentile[$side]['value'] = $values[$side][$percent - 1];
547			}
548		}
549	}
550
551	// calculation of minimum Y axis
552	protected function calculateMinY($side) {
553		if ($this->ymin_type == GRAPH_YAXIS_TYPE_FIXED) {
554			return $this->yaxismin;
555		}
556
557		if ($this->ymin_type == GRAPH_YAXIS_TYPE_ITEM_VALUE && $this->ymin_itemid != 0) {
558			$item = get_item_by_itemid($this->ymin_itemid);
559			if ($item) {
560				$history = Manager::History()->getLastValues([$item]);
561				if (isset($history[$item['itemid']])) {
562					return $history[$item['itemid']][0]['value'];
563				}
564			}
565		}
566
567		$minY = null;
568		for ($i = 0; $i < $this->num; $i++) {
569			if ($this->items[$i]['yaxisside'] != $side) {
570				continue;
571			}
572
573			if (!isset($this->data[$this->items[$i]['itemid']][GRAPH_ITEM_SIMPLE])) {
574				continue;
575			}
576
577			$data = &$this->data[$this->items[$i]['itemid']][GRAPH_ITEM_SIMPLE];
578
579			if (!isset($data)) {
580				continue;
581			}
582
583			$calc_fnc = $this->items[$i]['calc_fnc'];
584
585			switch ($calc_fnc) {
586				case CALC_FNC_ALL:
587				case CALC_FNC_MIN:
588					$val = $data['min'];
589					$shift_val = $data['shift_min'];
590					break;
591				case CALC_FNC_MAX:
592					$val = $data['max'];
593					$shift_val = $data['shift_max'];
594					break;
595				case CALC_FNC_AVG:
596				default:
597					$val = $data['avg'];
598					$shift_val = $data['shift_avg'];
599			}
600
601			if (!isset($val)) {
602				continue;
603			}
604
605			if ($this->type == GRAPH_TYPE_STACKED) {
606				$min_val_shift = min(count($val), count($shift_val));
607				for ($ci = 0; $ci < $min_val_shift; $ci++) {
608					if ($shift_val[$ci] < 0) {
609						$val[$ci] += bcadd($shift_val[$ci], $val[$ci]);
610					}
611				}
612			}
613
614			if (!isset($minY)) {
615				if (isset($val) && count($val) > 0) {
616					$minY = min($val);
617				}
618			}
619			else {
620				$minY = min($minY, min($val));
621			}
622		}
623
624		return $minY;
625	}
626
627	// calculation of maximum Y of a side (left/right)
628	protected function calculateMaxY($side) {
629		if ($this->ymax_type == GRAPH_YAXIS_TYPE_FIXED) {
630			return $this->yaxismax;
631		}
632
633		if ($this->ymax_type == GRAPH_YAXIS_TYPE_ITEM_VALUE && $this->ymax_itemid != 0) {
634			$item = get_item_by_itemid($this->ymax_itemid);
635			if ($item) {
636				$history = Manager::History()->getLastValues([$item]);
637				if (isset($history[$item['itemid']])) {
638					return $history[$item['itemid']][0]['value'];
639				}
640			}
641		}
642
643		$maxY = null;
644		for ($i = 0; $i < $this->num; $i++) {
645			if ($this->items[$i]['yaxisside'] != $side) {
646				continue;
647			}
648
649			if (!isset($this->data[$this->items[$i]['itemid']][GRAPH_ITEM_SIMPLE])) {
650				continue;
651			}
652
653			$data = &$this->data[$this->items[$i]['itemid']][GRAPH_ITEM_SIMPLE];
654
655			if (!isset($data)) {
656				continue;
657			}
658
659			$calc_fnc = $this->items[$i]['calc_fnc'];
660
661			switch ($calc_fnc) {
662				case CALC_FNC_ALL:
663				case CALC_FNC_MAX:
664					$val = $data['max'];
665					$shift_val = $data['shift_max'];
666					break;
667				case CALC_FNC_MIN:
668					$val = $data['min'];
669					$shift_val = $data['shift_min'];
670					break;
671				case CALC_FNC_AVG:
672				default:
673					$val = $data['avg'];
674					$shift_val = $data['shift_avg'];
675			}
676
677			if (!isset($val)) {
678				continue;
679			}
680
681			for ($ci = 0; $ci < min(count($val), count($shift_val)); $ci++) {
682				if ($data['count'][$ci] == 0) {
683					continue;
684				}
685
686				$val[$ci] = bcadd($shift_val[$ci], $val[$ci]);
687			}
688
689			if (!isset($maxY)) {
690				if (isset($val) && count($val) > 0) {
691					$maxY = max($val);
692				}
693			}
694			else {
695				$maxY = max($maxY, max($val));
696			}
697		}
698
699		return $maxY;
700	}
701
702	/**
703	 * Check if Y axis min value is larger than Y axis max value. Show error instead of graph if true.
704	 *
705	 * @param float $min		Y axis min value
706	 * @param float $max		Y axis max value
707	 */
708	protected function validateMinMax($min, $max) {
709		if (bccomp($min, $max) == 0 || bccomp($min, $max) == 1) {
710			show_error_message(_('Y axis MAX value must be greater than Y axis MIN value.'));
711			exit;
712		}
713	}
714
715	protected function calcZero() {
716		if (isset($this->axis_valuetype[GRAPH_YAXIS_SIDE_RIGHT])) {
717			$sides[] = GRAPH_YAXIS_SIDE_RIGHT;
718		}
719
720		if (isset($this->axis_valuetype[GRAPH_YAXIS_SIDE_LEFT]) || !isset($sides)) {
721			$sides[] = GRAPH_YAXIS_SIDE_LEFT;
722		}
723
724		foreach ($sides as $num => $side) {
725			$this->unit2px[$side] = ($this->m_maxY[$side] - $this->m_minY[$side]) / $this->sizeY;
726			if ($this->unit2px[$side] == 0) {
727				$this->unit2px[$side] = 1;
728			}
729
730			if ($this->m_minY[$side] > 0) {
731				$this->zero[$side] = $this->sizeY + $this->shiftY;
732				if (bccomp($this->m_minY[$side], $this->m_maxY[$side]) == 1) {
733					$this->oxy[$side] = $this->m_maxY[$side];
734				}
735				else {
736					$this->oxy[$side] = $this->m_minY[$side];
737				}
738			}
739			elseif ($this->m_maxY[$side] < 0) {
740				$this->zero[$side] = $this->shiftY;
741				if (bccomp($this->m_minY[$side], $this->m_maxY[$side]) == 1) {
742					$this->oxy[$side] = $this->m_minY[$side];
743				}
744				else {
745					$this->oxy[$side] = $this->m_maxY[$side];
746				}
747			}
748			else {
749				$this->zero[$side] = $this->sizeY + $this->shiftY - abs(bcdiv($this->m_minY[$side],
750					$this->unit2px[$side]
751				));
752				$this->oxy[$side] = 0;
753			}
754		}
755	}
756
757	protected function calcMinMaxInterval() {
758		$intervals = [];
759		foreach ([1, 2, 3, 4] as $num) {
760			$dec = pow(0.1, $num);
761			foreach ([1, 2, 5] as $int) {
762				$intervals[] = bcmul($int, $dec);
763			}
764		}
765
766		// Check if items use B or Bps units.
767		$leftBase1024 = false;
768		$rightBase1024 = false;
769
770		for ($item = 0; $item < $this->num; $item++) {
771			if ($this->items[$item]['units'] == 'B' || $this->items[$item]['units'] == 'Bps') {
772				if ($this->items[$item]['yaxisside'] == GRAPH_YAXIS_SIDE_LEFT) {
773					$leftBase1024 = true;
774				}
775				else {
776					$rightBase1024 = true;
777				}
778			}
779		}
780
781		foreach ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] as $num) {
782			$dec = bcpow(10, $num);
783			foreach ([1, 2, 5] as $int) {
784				$intervals[] = bcmul($int, $dec);
785			}
786		}
787
788		if (isset($this->axis_valuetype[GRAPH_YAXIS_SIDE_RIGHT])) {
789			$sides[] = GRAPH_YAXIS_SIDE_RIGHT;
790		}
791
792		if (isset($this->axis_valuetype[GRAPH_YAXIS_SIDE_LEFT]) || !isset($sides)) {
793			$sides[] = GRAPH_YAXIS_SIDE_LEFT;
794		}
795
796		foreach ($sides as $snum => $side) {
797			if (!isset($this->axis_valuetype[$side])) {
798				continue;
799			}
800
801			if (($this->ymin_type != GRAPH_YAXIS_TYPE_FIXED || $this->ymax_type != GRAPH_YAXIS_TYPE_CALCULATED)
802					&& $this->type == GRAPH_TYPE_STACKED) {
803				$this->m_minY[$side] = min($this->m_minY[$side], 0);
804				$this->validateMinMax($this->m_minY[$side], $this->m_maxY[$side]);
805
806				continue;
807			}
808
809			if ($this->ymax_type == GRAPH_YAXIS_TYPE_FIXED) {
810				$this->m_maxY[$side] = $this->yaxismax;
811				if ($this->ymin_type == GRAPH_YAXIS_TYPE_CALCULATED
812						&& ($this->m_minY[$side] == null || bccomp($this->m_maxY[$side], $this->m_minY[$side]) == 0
813								|| bccomp($this->m_maxY[$side], $this->m_minY[$side]) == -1)) {
814					if ($this->m_maxY[$side] == 0) {
815						$this->m_minY[$side] = -1;
816					}
817					elseif ($this->m_maxY[$side] > 0) {
818						$this->m_minY[$side] = bcmul($this->m_maxY[$side], 0.8);
819					}
820					else {
821						$this->m_minY[$side] = bcmul($this->m_maxY[$side], 1.2);
822					}
823				}
824			}
825
826			if ($this->ymin_type == GRAPH_YAXIS_TYPE_FIXED) {
827				$this->m_minY[$side] = $this->yaxismin;
828				if ($this->ymax_type == GRAPH_YAXIS_TYPE_CALCULATED
829						&& ($this->m_maxY[$side] == null || bccomp($this->m_maxY[$side], $this->m_minY[$side]) == 0
830								|| bccomp($this->m_maxY[$side], $this->m_minY[$side]) == -1)) {
831					if ($this->m_minY[$side] > 0) {
832						$this->m_maxY[$side] = bcmul($this->m_minY[$side], 1.2);
833					}
834					else {
835						$this->m_maxY[$side] = bcmul($this->m_minY[$side], 0.8);
836					}
837				}
838			}
839
840			$this->validateMinMax($this->m_minY[$side], $this->m_maxY[$side]);
841		}
842
843		$side = GRAPH_YAXIS_SIDE_LEFT;
844		$other_side = GRAPH_YAXIS_SIDE_RIGHT;
845
846		// Invert sides and its bases, if left side doesn't exist.
847		if (!isset($this->axis_valuetype[GRAPH_YAXIS_SIDE_LEFT])) {
848			$side = GRAPH_YAXIS_SIDE_RIGHT;
849			$other_side = GRAPH_YAXIS_SIDE_LEFT;
850			$tempBase = $leftBase1024;
851			$leftBase1024 = $rightBase1024;
852			$rightBase1024 = $tempBase;
853		}
854
855		if (!isset($this->m_minY[$side])) {
856			$this->m_minY[$side] = 0;
857		}
858		if (!isset($this->m_maxY[$side])) {
859			$this->m_maxY[$side] = 0;
860		}
861
862		if (!isset($this->m_minY[$other_side])) {
863			$this->m_minY[$other_side] = 0;
864		}
865		if (!isset($this->m_maxY[$other_side])) {
866			$this->m_maxY[$other_side] = 0;
867		}
868
869		$tmp_minY = $this->m_minY;
870		$tmp_maxY = $this->m_maxY;
871
872		// Calculate interval.
873		$columnInterval = bcdiv(bcmul($this->gridPixelsVert, (bcsub($this->m_maxY[$side], $this->m_minY[$side]))), $this->sizeY);
874
875		$dist = bcmul(5, bcpow(10, 18));
876
877		$interval = 0;
878		foreach ($intervals as $int) {
879			// We must get a positive number.
880			if (bccomp($int, $columnInterval) == -1) {
881				$t = bcsub($columnInterval, $int);
882			}
883			else {
884				$t = bcsub($int, $columnInterval);
885			}
886
887			if (bccomp($t, $dist) == -1) {
888				$dist = $t;
889				$interval = $int;
890			}
891		}
892
893		// Calculate interval, if left side use B or Bps.
894		if ($leftBase1024) {
895			$interval = getBase1024Interval($interval, $this->m_minY[$side], $this->m_maxY[$side]);
896		}
897
898		$columnInterval = bcdiv(bcmul($this->gridPixelsVert, bcsub($this->m_maxY[$other_side], $this->m_minY[$other_side])), $this->sizeY);
899
900		$dist = bcmul(5, bcpow(10, 18));
901
902		$interval_other_side = 0;
903		foreach ($intervals as $int) {
904			// We must get a positive number.
905			if (bccomp($int, $columnInterval) == -1) {
906				$t = bcsub($columnInterval, $int);
907			}
908			else {
909				$t = bcsub($int, $columnInterval);
910			}
911
912			if (bccomp($t,$dist) == -1) {
913				$dist = $t;
914				$interval_other_side = $int;
915			}
916		}
917
918		// Calculate interval, if right side use B or Bps.
919		if ($rightBase1024) {
920			$interval_other_side = getBase1024Interval($interval_other_side, $this->m_minY[$other_side],
921				$this->m_maxY[$other_side]);
922		}
923
924		// Save original min and max items values.
925		foreach ($sides as $graphSide) {
926			$minY[$graphSide] = $this->m_minY[$graphSide];
927			$maxY[$graphSide] = $this->m_maxY[$graphSide];
928		}
929
930		if (!isset($minY[$side])) {
931			$minY[$side] = 0;
932		}
933		if (!isset($maxY[$side])) {
934			$maxY[$side] = 0;
935		}
936
937		// Correcting MIN & MAX.
938		$this->m_minY[$side] = bcmul(bcfloor(bcdiv($this->m_minY[$side], $interval)), $interval);
939		$this->m_maxY[$side] = bcmul(bcceil(bcdiv($this->m_maxY[$side], $interval)), $interval);
940		$this->m_minY[$other_side] = bcmul(bcfloor(bcdiv($this->m_minY[$other_side], $interval_other_side)), $interval_other_side);
941		$this->m_maxY[$other_side] = bcmul(bcceil(bcdiv($this->m_maxY[$other_side], $interval_other_side)), $interval_other_side);
942
943		// Add intervals so min/max Y wouldn't be too close to graph's top/bottom edges.
944		foreach ($sides as $graphSide) {
945			if ($graphSide == $side) {
946				$tmpInterval = $interval;
947			}
948			else {
949				$tmpInterval = $interval_other_side;
950			}
951
952			if (bccomp($this->m_minY[$graphSide], $minY[$side]) == 0
953					&& $this->m_minY[$graphSide] != null && $this->m_minY[$graphSide] != 0) {
954				$this->m_minY[$graphSide] = bcsub($this->m_minY[$graphSide], $tmpInterval);
955			}
956
957			if (bccomp($this->m_maxY[$graphSide], $maxY[$graphSide]) == 0
958					&& $this->m_maxY[$graphSide] != null && $this->m_maxY[$graphSide] != 0) {
959				$this->m_maxY[$graphSide] = bcadd($this->m_maxY[$graphSide], $tmpInterval);
960			}
961		}
962
963		// Calculate interval count for main and other side.
964		$this->gridLinesCount[$side] = bcceil(bcdiv(bcsub($this->m_maxY[$side], $this->m_minY[$side]), $interval));
965		$this->gridLinesCount[$other_side] = bcceil(bcdiv(bcsub($this->m_maxY[$other_side], $this->m_minY[$other_side]), $interval_other_side));
966
967		$this->m_maxY[$side] = bcadd($this->m_minY[$side], bcmul($interval, $this->gridLinesCount[$side]));
968		$this->gridStep[$side] = $interval;
969
970		if (isset($this->axis_valuetype[$other_side])) {
971			// Other side correction.
972			$dist = bcsub($this->m_maxY[$other_side], $this->m_minY[$other_side]);
973			$interval = 1;
974
975			foreach ($intervals as $int) {
976				if (bccomp($dist, bcmul($this->gridLinesCount[$side], $int)) == -1) {
977					$interval = $int;
978					break;
979				}
980			}
981
982			// Correcting MIN & MAX on other side Y axis.
983			$this->m_minY[$other_side] = bcmul(bcfloor(bcdiv($this->m_minY[$other_side], $interval)), $interval);
984			$this->m_maxY[$other_side] = bcmul(bcceil(bcdiv($this->m_maxY[$other_side], $interval)), $interval);
985
986			// Do recalculation in case if calculated min value is greater than calculated max value.
987			if (bccomp($tmp_maxY[$other_side], $this->m_maxY[$other_side]) == 1 || bccomp($tmp_minY[$other_side], $this->m_minY[$other_side]) == -1) {
988				$dist = bcsub($this->m_maxY[$other_side], $this->m_minY[$other_side]);
989				$interval = 0;
990				foreach ($intervals as $int) {
991					if (bccomp($dist, bcmul($this->gridLinesCount[$side], $int)) == -1) {
992						$interval = $int;
993						break;
994					}
995				}
996
997				// Correcting MIN & MAX values on other side Y axis.
998				$this->m_minY[$other_side] = bcmul(bcfloor(bcdiv($this->m_minY[$other_side], $interval)), $interval);
999				$this->m_maxY[$other_side] = bcmul(bcceil(bcdiv($this->m_maxY[$other_side], $interval)), $interval);
1000			}
1001
1002			// Calculate interval, if right side use B or Bps.
1003			if (isset($rightBase1024)) {
1004				$interval = getBase1024Interval($interval, $this->m_minY[$side], $this->m_maxY[$side]);
1005				// Correcting MIN & MAX values on other side Y axis.
1006				$this->m_minY[$other_side] = bcmul(bcfloor(bcdiv($this->m_minY[$other_side], $interval)), $interval);
1007				$this->m_maxY[$other_side] = bcmul(bcceil(bcdiv($this->m_maxY[$other_side], $interval)), $interval);
1008			}
1009
1010			$this->gridLinesCount[$other_side] = $this->gridLinesCount[$side];
1011			$this->m_maxY[$other_side] = bcadd($this->m_minY[$other_side], bcmul($interval, $this->gridLinesCount[$other_side]));
1012			$this->gridStep[$other_side] = $interval;
1013		}
1014
1015		foreach ($sides as $graphSide) {
1016			if (!isset($this->axis_valuetype[$graphSide])) {
1017				continue;
1018			}
1019
1020			if ($this->type == GRAPH_TYPE_STACKED) {
1021				$this->m_minY[$graphSide] = bccomp($tmp_minY[GRAPH_YAXIS_SIDE_LEFT], 0) == -1 ? $tmp_minY[GRAPH_YAXIS_SIDE_LEFT] : 0;
1022			}
1023
1024			if ($this->ymax_type == GRAPH_YAXIS_TYPE_FIXED) {
1025				$this->m_maxY[$graphSide] = $this->yaxismax;
1026			}
1027			elseif ($this->ymax_type == GRAPH_YAXIS_TYPE_ITEM_VALUE) {
1028				$this->m_maxY[$graphSide] = $tmp_maxY[$graphSide];
1029			}
1030
1031			if ($this->ymin_type == GRAPH_YAXIS_TYPE_FIXED) {
1032				$this->m_minY[$graphSide] = $this->yaxismin;
1033			}
1034			elseif ($this->ymin_type == GRAPH_YAXIS_TYPE_ITEM_VALUE) {
1035				$this->m_minY[$graphSide] = $tmp_minY[$graphSide];
1036			}
1037
1038			$this->validateMinMax($this->m_minY[$graphSide], $this->m_maxY[$graphSide]);
1039		}
1040
1041		// Get diff between min/max Y values and fix potential division by zero.
1042		$diff_val = bcsub($this->m_maxY[$side], $this->m_minY[$side]);
1043		if (bccomp($diff_val, 0) == 0) {
1044			$diff_val = 1;
1045		}
1046
1047		$this->gridStepX[$side] = bcdiv(bcmul($this->gridStep[$side], $this->sizeY), $diff_val);
1048
1049		if (isset($this->axis_valuetype[$other_side])) {
1050			$diff_val = bcsub($this->m_maxY[$other_side], $this->m_minY[$other_side]);
1051			if (bccomp($diff_val, 0) == 0) {
1052				$diff_val = 1;
1053			}
1054			$this->gridStepX[$other_side] = bcdiv(bcmul($this->gridStep[$other_side], $this->sizeY), $diff_val);
1055		}
1056	}
1057
1058	/********************************************************************************************************/
1059	// DRAW ELEMENTS
1060	/********************************************************************************************************/
1061	public function drawXYAxisScale() {
1062		$gbColor = $this->getColor($this->graphtheme['gridbordercolor'], 0);
1063
1064		if ($this->yaxis[GRAPH_YAXIS_SIDE_LEFT]) {
1065			zbx_imageline(
1066				$this->im,
1067				$this->shiftXleft + $this->shiftXCaption,
1068				$this->shiftY - 5,
1069				$this->shiftXleft + $this->shiftXCaption,
1070				$this->sizeY + $this->shiftY + 4,
1071				$gbColor
1072			);
1073
1074			imagefilledpolygon(
1075				$this->im,
1076				[
1077					$this->shiftXleft + $this->shiftXCaption - 3, $this->shiftY - 5,
1078					$this->shiftXleft + $this->shiftXCaption + 3, $this->shiftY - 5,
1079					$this->shiftXleft + $this->shiftXCaption, $this->shiftY - 10
1080				],
1081				3,
1082				$this->getColor('White')
1083			);
1084
1085			/* draw left axis triangle */
1086			zbx_imageline($this->im, $this->shiftXleft + $this->shiftXCaption - 3, $this->shiftY - 5,
1087					$this->shiftXleft + $this->shiftXCaption + 3, $this->shiftY - 5,
1088					$gbColor);
1089			zbx_imagealine($this->im, $this->shiftXleft + $this->shiftXCaption - 3, $this->shiftY - 5,
1090					$this->shiftXleft + $this->shiftXCaption, $this->shiftY - 10,
1091					$gbColor);
1092			zbx_imagealine($this->im, $this->shiftXleft + $this->shiftXCaption + 3, $this->shiftY - 5,
1093					$this->shiftXleft + $this->shiftXCaption, $this->shiftY - 10,
1094					$gbColor);
1095		}
1096		else {
1097			dashedLine(
1098				$this->im,
1099				$this->shiftXleft + $this->shiftXCaption,
1100				$this->shiftY,
1101				$this->shiftXleft + $this->shiftXCaption,
1102				$this->sizeY + $this->shiftY,
1103				$this->getColor($this->graphtheme['gridcolor'], 0)
1104			);
1105		}
1106
1107		if ($this->yaxis[GRAPH_YAXIS_SIDE_RIGHT]) {
1108			zbx_imageline(
1109				$this->im,
1110				$this->sizeX + $this->shiftXleft + $this->shiftXCaption,
1111				$this->shiftY - 5,
1112				$this->sizeX + $this->shiftXleft + $this->shiftXCaption,
1113				$this->sizeY + $this->shiftY + 4,
1114				$gbColor
1115			);
1116
1117			imagefilledpolygon(
1118				$this->im,
1119				[
1120					$this->sizeX + $this->shiftXleft + $this->shiftXCaption - 3, $this->shiftY - 5,
1121					$this->sizeX + $this->shiftXleft + $this->shiftXCaption + 3, $this->shiftY - 5,
1122					$this->sizeX + $this->shiftXleft + $this->shiftXCaption, $this->shiftY - 10
1123				],
1124				3,
1125				$this->getColor('White')
1126			);
1127
1128			/* draw right axis triangle */
1129			zbx_imageline($this->im, $this->sizeX + $this->shiftXleft + $this->shiftXCaption - 3, $this->shiftY - 5,
1130				$this->sizeX + $this->shiftXleft + $this->shiftXCaption + 3, $this->shiftY - 5,
1131				$gbColor);
1132			zbx_imagealine($this->im, $this->sizeX + $this->shiftXleft + $this->shiftXCaption + 3, $this->shiftY - 5,
1133				$this->sizeX + $this->shiftXleft + $this->shiftXCaption, $this->shiftY - 10,
1134				$gbColor);
1135			zbx_imagealine($this->im, $this->sizeX + $this->shiftXleft + $this->shiftXCaption - 3, $this->shiftY - 5,
1136				$this->sizeX + $this->shiftXleft + $this->shiftXCaption, $this->shiftY - 10,
1137				$gbColor);
1138		}
1139		else {
1140			dashedLine(
1141				$this->im,
1142				$this->sizeX + $this->shiftXleft + $this->shiftXCaption,
1143				$this->shiftY,
1144				$this->sizeX + $this->shiftXleft + $this->shiftXCaption,
1145				$this->sizeY + $this->shiftY,
1146				$this->getColor($this->graphtheme['gridcolor'], 0)
1147			);
1148		}
1149
1150		zbx_imageline(
1151			$this->im,
1152			$this->shiftXleft + $this->shiftXCaption - 3,
1153			$this->sizeY + $this->shiftY + 1,
1154			$this->sizeX + $this->shiftXleft + $this->shiftXCaption + 5,
1155			$this->sizeY + $this->shiftY + 1,
1156			$gbColor
1157		);
1158
1159		imagefilledpolygon(
1160			$this->im,
1161			[
1162				$this->sizeX + $this->shiftXleft + $this->shiftXCaption + 5, $this->sizeY + $this->shiftY - 2,
1163				$this->sizeX + $this->shiftXleft + $this->shiftXCaption + 5, $this->sizeY + $this->shiftY + 4,
1164				$this->sizeX + $this->shiftXleft + $this->shiftXCaption + 10, $this->sizeY + $this->shiftY + 1
1165			],
1166			3,
1167			$this->getColor('White')
1168		);
1169
1170		/* draw X axis triangle */
1171		zbx_imageline($this->im, $this->sizeX + $this->shiftXleft + $this->shiftXCaption + 5, $this->sizeY + $this->shiftY - 2,
1172			$this->sizeX + $this->shiftXleft + $this->shiftXCaption + 5, $this->sizeY + $this->shiftY + 4,
1173			$gbColor);
1174		zbx_imagealine($this->im, $this->sizeX + $this->shiftXleft + $this->shiftXCaption + 5, $this->sizeY + $this->shiftY + 4,
1175			$this->sizeX + $this->shiftXleft + $this->shiftXCaption + 10, $this->sizeY + $this->shiftY + 1,
1176			$gbColor);
1177		zbx_imagealine($this->im, $this->sizeX + $this->shiftXleft + $this->shiftXCaption + 10, $this->sizeY + $this->shiftY + 1,
1178			$this->sizeX + $this->shiftXleft + $this->shiftXCaption + 5, $this->sizeY + $this->shiftY - 2,
1179			$gbColor);
1180	}
1181
1182	/**
1183	 * Draws Y scale grid.
1184	 */
1185	private function drawHorizontalGrid() {
1186		$yAxis = $this->yaxis[GRAPH_YAXIS_SIDE_LEFT] ? GRAPH_YAXIS_SIDE_LEFT : GRAPH_YAXIS_SIDE_RIGHT;
1187
1188		$stepY = $this->gridStepX[$yAxis];
1189
1190		if ($this->gridLinesCount[$yAxis] < round($this->sizeY / $this->gridPixels)) {
1191			$stepY = $stepY / 2;
1192		}
1193
1194		$xLeft = $this->shiftXleft;
1195		$xRight = $this->shiftXleft + $this->sizeX;
1196		$lineColor = $this->getColor($this->graphtheme['gridcolor'], 0);
1197
1198		for ($y = $this->shiftY + $this->sizeY - $stepY; $y > $this->shiftY; $y -= $stepY) {
1199			dashedLine($this->im, $xLeft, $y, $xRight, $y, $lineColor);
1200		}
1201	}
1202
1203	private function drawTimeGrid() {
1204		$time_format = (date('Y', $this->stime) != date('Y', $this->to_time))
1205			? DATE_FORMAT
1206			: DATE_TIME_FORMAT_SHORT;
1207
1208		// Draw start date (and time) label.
1209		$this->drawStartEndTimePeriod($this->stime, $time_format, 0);
1210		$this->drawDateTimeIntervals();
1211
1212		// Draw end date (and time) label.
1213		$this->drawStartEndTimePeriod($this->to_time, $time_format, $this->sizeX);
1214	}
1215
1216	/**
1217	 * Draw start or end date (and time) label.
1218	 *
1219	 * @param int $value        Unix time.
1220	 * @param string $format    Date time format.
1221	 * @param int $position     Position on X axis.
1222	 */
1223	private function drawStartEndTimePeriod($value, $format, $position) {
1224		$point = zbx_date2str(_($format), $value);
1225		$element = imageTextSize(8, 90, $point);
1226		imageText(
1227			$this->im,
1228			8,
1229			90,
1230			$this->shiftXleft + $position + round($element['width'] / 2),
1231			$this->sizeY + $this->shiftY + $element['height'] + 6,
1232			$this->getColor($this->graphtheme['highlightcolor'], 0),
1233			$point
1234		);
1235	}
1236
1237	/**
1238	 * Draw main period label in red color with 8px font size under X axis and a 2px dashed gray vertical line
1239	 * according to that label.
1240	 *
1241	 * @param string $value     Readable timestamp.
1242	 * @param int    $position  Position on X axis.
1243	 */
1244	private function drawMainPeriod($value, $position) {
1245		$dims = imageTextSize(8, 90, $value);
1246
1247		imageText(
1248			$this->im,
1249			8,
1250			90,
1251			$this->shiftXleft + $position + round($dims['width'] / 2),
1252			$this->sizeY + $this->shiftY + $dims['height'] + 6,
1253			$this->getColor($this->graphtheme['highlightcolor'], 0),
1254			$value
1255		);
1256
1257		dashedLine(
1258			$this->im,
1259			$this->shiftXleft + $position,
1260			$this->shiftY,
1261			$this->shiftXleft + $position,
1262			$this->sizeY + $this->shiftY,
1263			$this->getColor($this->graphtheme['maingridcolor'], 0)
1264		);
1265	}
1266
1267	/**
1268	 * Draw main period label in black color with 7px font size under X axis and a 1px dashed gray vertical line
1269	 * according to that label.
1270	 *
1271	 * @param strimg $value     Readable timestamp.
1272	 * @param int    $position  Position on X axis.
1273	 */
1274	private function drawSubPeriod($value, $position) {
1275		$element = imageTextSize(7, 90, $value);
1276
1277		imageText(
1278			$this->im,
1279			7,
1280			90,
1281			$this->shiftXleft + $position + round($element['width'] / 2),
1282			$this->sizeY + $this->shiftY + $element['height'] + 6,
1283			$this->getColor($this->graphtheme['textcolor'], 0),
1284			$value
1285		);
1286
1287		dashedLine(
1288			$this->im,
1289			$this->shiftXleft + $position,
1290			$this->shiftY,
1291			$this->shiftXleft + $position,
1292			$this->sizeY + $this->shiftY,
1293			$this->getColor($this->graphtheme['gridcolor'], 0)
1294		);
1295	}
1296
1297	/**
1298	 * Get best matching X-axis interval specification for the preferred sub-interval.
1299	 *
1300	 * @param int   $pref_sub_interval  Preferred sub-interval in seconds.
1301	 * @param float $min_sub_interval   Preferred minimal sub-interval in seconds (float). Discarded if no matches.
1302	 *
1303	 * @return array
1304	 */
1305	private function getOptimalDateTimeIntervalSpec($pref_sub_interval, $min_sub_interval) {
1306		// Possible X-axis main and sub-intervals.
1307		$intervals = [
1308			'PT1M' => ['PT1S', 'PT5S', 'PT10S', 'PT30S'],
1309			'PT1H' => ['PT1M', 'PT2M', 'PT5M', 'PT15M', 'PT30M'],
1310			'P1D' => ['PT1H', 'PT3H', 'PT6H', 'PT12H'],
1311			'P1W' => ['P1D'],
1312			'P1M' => ['P3D', 'P1W', 'P2W'],
1313			'P1Y' => ['P1M', 'P3M', 'P4M', 'P6M'],
1314			'P10Y' => ['P1Y', 'P5Y']
1315		];
1316
1317		// Starting date and time aligners.
1318		$aligners = [
1319			'PT1M' => ['trim' => 'Y-m-d H:i:00', 'convert' => null],
1320			'PT1H' => ['trim' => 'Y-m-d H:00:00', 'convert' => null],
1321			'P1D' => ['trim' => 'Y-m-d 00:00:00', 'convert' => null],
1322			'P1W' => ['trim' => 'Y-m-d 00:00:00', 'convert' => 'last Sunday 00:00:00'],
1323			'P1M' => ['trim' => 'Y-m-01 00:00:00', 'convert' => null],
1324			'P1Y' => ['trim' => 'Y-01-01 00:00:00', 'convert' => null],
1325			'P10Y' => ['trim' => '1970-01-01 00:00:00', 'convert' => null]
1326		];
1327
1328		// Date and time label formats.
1329		$formats = [
1330			'PT1M' => ['main' => TIME_FORMAT, 'sub' => _('H:i:s')],
1331			'PT1H' => ['main' => TIME_FORMAT, 'sub' => TIME_FORMAT],
1332			'P1D' => ['main' => _('m-d'), 'sub' => TIME_FORMAT],
1333			'P1W' => ['main' => _('m-d'), 'sub' => _('m-d')],
1334			'P1M' => ['main' => _('m-d'), 'sub' => _('m-d')],
1335			'P1Y' => ['main' => _x('Y', DATE_FORMAT_CONTEXT), 'sub' => _('M')],
1336			'P10Y' => ['main' => _x('Y', DATE_FORMAT_CONTEXT), 'sub' => _x('Y', DATE_FORMAT_CONTEXT)]
1337		];
1338
1339		$best_main_interval = null;
1340		$best_sub_interval = null;
1341		$best_sub_interval_ts = 0;
1342		$best_sub_interval_prop = INF;
1343
1344		foreach ($intervals as $main_interval => $sub_intervals) {
1345			foreach ($sub_intervals as $sub_interval) {
1346				$interval_ts = (new DateTime('@0'))
1347					->add(new DateInterval($sub_interval))
1348					->getTimestamp();
1349
1350				$interval_prop = max($pref_sub_interval, $interval_ts) / min($pref_sub_interval, $interval_ts);
1351
1352				// Search for the best interval preferably but not necessarily matching the $min_sub_interval criteria.
1353				$is_better = ($best_sub_interval_ts < $min_sub_interval)
1354					? $interval_ts > $best_sub_interval_ts
1355					: $interval_prop < $best_sub_interval_prop;
1356
1357				if ($is_better) {
1358					$best_main_interval = $main_interval;
1359					$best_sub_interval = $sub_interval;
1360					$best_sub_interval_ts = $interval_ts;
1361					$best_sub_interval_prop = $interval_prop;
1362				}
1363			}
1364		}
1365
1366		return [
1367			'intervals' => [
1368				'main' => new DateInterval($best_main_interval),
1369				'sub' => new DateInterval($best_sub_interval)
1370			],
1371			'aligner' => $aligners[$best_main_interval],
1372			'format' => $formats[$best_main_interval]
1373		];
1374	}
1375
1376	/**
1377	 * Get date and time intervals in the given range for the X-axis.
1378	 *
1379	 * @param int          $start     Range start in seconds.
1380	 * @param int          $end       Range end in seconds.
1381	 * @param DateInterval $interval  Date and time interval.
1382	 *
1383	 * @return array
1384	 */
1385	private function getDateTimeIntervals($start, $end, $interval) {
1386		$intervals = [];
1387
1388		$interval_ts = (new DateTime('@0'))
1389			->add($interval)
1390			->getTimestamp();
1391
1392		// Manage time transitions natively.
1393		if ($interval_ts >= SEC_PER_DAY) {
1394			$dt = (new DateTime())->setTimestamp($start);
1395
1396			while ($dt->getTimestamp() <= $end) {
1397				$intervals[] = $dt->getTimestamp();
1398				$dt->add($interval);
1399			}
1400		}
1401		else {
1402			$transitions = (new DateTime())->getTimezone()->getTransitions($start, $end);
1403			if (!$transitions) {
1404				$transitions = [];
1405			}
1406
1407			$time = $start;
1408			$transition = 1;
1409
1410			while ($time <= $end) {
1411				$correct_before = 0;
1412				$correct_after = 0;
1413
1414				while ($transition < count($transitions) && $time >= $transitions[$transition]['ts']) {
1415					$offset_diff = $transitions[$transition]['offset'] - $transitions[$transition - 1]['offset'];
1416
1417					if ($interval_ts > abs($offset_diff)) {
1418						if ($transitions[$transition]['isdst']) {
1419							if ($time - $transitions[$transition]['ts'] >= $offset_diff) {
1420								$correct_before -= $offset_diff;
1421							}
1422							else {
1423								$correct_after -= $offset_diff;
1424							}
1425						}
1426						else {
1427							$correct_before -= $offset_diff;
1428						}
1429					}
1430
1431					$transition++;
1432				}
1433
1434				$time += $correct_before;
1435				$intervals[] = $time;
1436				$time += $correct_after + $interval_ts;
1437			}
1438		}
1439
1440		return $intervals;
1441	}
1442
1443	/**
1444	 * Draw date and time intervals under the X axis.
1445	 */
1446	private function drawDateTimeIntervals() {
1447		// Calculate standard label width in time units.
1448		$label_size = imageTextSize(7, 90, 'WWW')['width'] * $this->period / $this->sizeX * 2;
1449
1450		$preferred_sub_interval = (int) ($this->period * $this->gridPixels / $this->sizeX);
1451
1452		$optimal = $this->getOptimalDateTimeIntervalSpec($preferred_sub_interval, $label_size);
1453
1454		// Align starting date and time with the interval.
1455		$start = strtotime(date($optimal['aligner']['trim'], $this->stime));
1456		if ($optimal['aligner']['convert'] !== null) {
1457			$start = strtotime($optimal['aligner']['convert'], $start);
1458		}
1459
1460		$end = $this->stime + $this->period;
1461
1462		// Draw main intervals.
1463		$mains = [];
1464
1465		foreach ($this->getDateTimeIntervals($start, $end, $optimal['intervals']['main']) as $time) {
1466			$pos = $time - $this->stime;
1467
1468			if ($pos >= $label_size && $pos <= $this->period - $label_size) {
1469				$this->drawMainPeriod(date($optimal['format']['main'], $time), $pos * $this->sizeX / $this->period);
1470			}
1471
1472			$mains[] = $time;
1473		}
1474
1475		$mains[] = $end;
1476
1477		// Draw sub-intervals.
1478		$sub_interval_ts = (new DateTime('@0'))
1479			->add($optimal['intervals']['sub'])
1480			->getTimestamp();
1481
1482		// Sub-intervals' margin from the main interval boundaries.
1483		$main_margin = max($label_size, (int) ($sub_interval_ts * .75));
1484
1485		for ($i = 0, $i_max = count($mains) - 2; $i <= $i_max; $i++) {
1486			$pos_min = $mains[$i] - $this->stime + $main_margin;
1487			$pos_max = $mains[$i + 1] - $this->stime - $main_margin;
1488
1489			foreach ($this->getDateTimeIntervals($mains[$i], $mains[$i + 1], $optimal['intervals']['sub']) as $time) {
1490				$pos = $time - $this->stime;
1491
1492				if ($pos >= max($pos_min, $label_size) && $pos <= min($pos_max, $this->period - $label_size)) {
1493					$this->drawSubPeriod(date($optimal['format']['sub'], $time), $pos * $this->sizeX / $this->period);
1494				}
1495			}
1496		}
1497	}
1498
1499	private function drawSides() {
1500		if (isset($this->axis_valuetype[GRAPH_YAXIS_SIDE_RIGHT])
1501				&& ($this->yaxis[GRAPH_YAXIS_SIDE_RIGHT] || $this->skipRightScale != 1)) {
1502			$sides[] = GRAPH_YAXIS_SIDE_RIGHT;
1503		}
1504
1505		if (((isset($this->axis_valuetype[GRAPH_YAXIS_SIDE_LEFT]))
1506				&& ($this->yaxis[GRAPH_YAXIS_SIDE_LEFT] || $this->skipLeftScale != 1)) || !isset($sides)) {
1507			$sides[] = GRAPH_YAXIS_SIDE_LEFT;
1508		}
1509
1510		foreach ($sides as $side) {
1511			$minY = $this->m_minY[$side];
1512			$maxY = $this->m_maxY[$side];
1513			$units = null;
1514			$unitsLong = null;
1515			$byteStep = false;
1516
1517			for ($item = 0; $item < $this->num; $item++) {
1518				if ($this->items[$item]['yaxisside'] == $side) {
1519					// check if items use B or Bps units
1520					if ($this->items[$item]['units'] == 'B' || $this->items[$item]['units'] == 'Bps') {
1521						$byteStep = true;
1522					}
1523					if (is_null($units)) {
1524						$units = $this->items[$item]['units'];
1525					}
1526					elseif ($this->items[$item]['units'] != $units) {
1527						$units = '';
1528					}
1529				}
1530			}
1531
1532			if (is_null($units) || $units === false) {
1533				$units = '';
1534			}
1535			else {
1536				for ($item = 0; $item < $this->num; $item++) {
1537					if ($this->items[$item]['yaxisside'] == $side && !empty($this->items[$item]['unitsLong'])) {
1538						$unitsLong = $this->items[$item]['unitsLong'];
1539						break;
1540					}
1541				}
1542			}
1543
1544			if (!empty($unitsLong)) {
1545				$dims = imageTextSize(9, 90, $unitsLong);
1546
1547				$tmpY = $this->sizeY / 2 + $this->shiftY + $dims['height'] / 2;
1548				if ($tmpY < $dims['height']) {
1549					$tmpY = $dims['height'] + 6;
1550				}
1551
1552				$tmpX = $side == GRAPH_YAXIS_SIDE_LEFT ? $dims['width'] + 8 : $this->fullSizeX - $dims['width'];
1553
1554				imageText(
1555					$this->im,
1556					9,
1557					90,
1558					$tmpX,
1559					$tmpY,
1560					$this->getColor($this->graphtheme['textcolor'], 0),
1561					$unitsLong
1562				);
1563			}
1564
1565			$step = $this->gridStep[$side];
1566			$hstr_count = $this->gridLinesCount[$side];
1567
1568			// ignore milliseconds if  -1 <= maxY => 1 or -1 <= minY => 1
1569			$ignoreMillisec = (bccomp($maxY, -1) <= 0 || bccomp($maxY, 1) >= 0
1570					|| bccomp($minY, -1) <= 0 || bccomp($minY, 1) >= 0);
1571
1572			$newPow = false;
1573			if ($byteStep) {
1574				$maxYPow = convertToBase1024($maxY, ZBX_KIBIBYTE);
1575				$minYPow = convertToBase1024($minY, ZBX_KIBIBYTE);
1576				$powStep = ZBX_KIBIBYTE;
1577			} else {
1578				$maxYPow = convertToBase1024($maxY);
1579				$minYPow = convertToBase1024($minY);
1580				$powStep = 1000;
1581			}
1582
1583			if (abs($maxYPow['pow']) > abs($minYPow['pow']) && $maxYPow['value'] != 0) {
1584				$newPow = $maxYPow['pow'];
1585				if (abs(bcdiv($minYPow['value'], bcpow($powStep, $maxYPow['pow']))) > 1000) {
1586					$newPow = $minYPow['pow'];
1587				}
1588			}
1589			if (abs($maxYPow['pow']) < abs($minYPow['pow']) && $minYPow['value'] != 0) {
1590				$newPow = $minYPow['pow'];
1591				if (abs(bcdiv($maxYPow['value'], bcpow($powStep, $minYPow['pow']))) > 1000) {
1592					$newPow = $maxYPow['pow'];
1593				}
1594			}
1595			if ($maxYPow['pow'] == $minYPow['pow']) {
1596				$newPow = $maxYPow['pow'];
1597			}
1598
1599			$maxLength = false;
1600			// get all values in y-axis if units != 's'
1601			if ($units != 's') {
1602				$calcValues = [];
1603				for ($i = 0; $i <= $hstr_count; $i++) {
1604					$hstr_count = ($hstr_count == 0) ? 1 : $hstr_count;
1605
1606					$val = bcadd(bcmul($i, $step), $minY);
1607
1608					if (bccomp(bcadd($val, bcdiv($step,2)), $maxY) == 1) {
1609						continue;
1610					}
1611
1612					$calcValues[] = convert_units([
1613						'value' => $val,
1614						'convert' => ITEM_CONVERT_NO_UNITS,
1615						'byteStep' => $byteStep,
1616						'pow' => $newPow
1617					]);
1618				}
1619
1620				$calcValues[] = convert_units([
1621					'value' => $maxY,
1622					'convert' => ITEM_CONVERT_NO_UNITS,
1623					'byteStep' => $byteStep,
1624					'pow' => $newPow
1625				]);
1626
1627				$maxLength = calcMaxLengthAfterDot($calcValues);
1628			}
1629
1630			for ($i = 0; $i <= $hstr_count; $i++) {
1631				$hstr_count = ($hstr_count == 0) ? 1 : $hstr_count;
1632
1633				$val = bcadd(bcmul($i, $step), $minY);
1634
1635				if (bccomp(bcadd($val, bcdiv($step, 2)), $maxY) == 1) {
1636					continue;
1637				}
1638
1639				$str = convert_units([
1640					'value' => $val,
1641					'units' => $units,
1642					'convert' => ITEM_CONVERT_NO_UNITS,
1643					'byteStep' => $byteStep,
1644					'pow' => $newPow,
1645					'ignoreMillisec' => $ignoreMillisec,
1646					'length' => $maxLength
1647				]);
1648
1649				if ($side == GRAPH_YAXIS_SIDE_LEFT) {
1650					$dims = imageTextSize(8, 0, $str);
1651					$posX = $this->shiftXleft - $dims['width'] - 9;
1652				}
1653				else {
1654					$posX = $this->sizeX + $this->shiftXleft + 12;
1655				}
1656
1657				// marker Y coordinate
1658				$posY = $this->sizeY + $this->shiftY - $this->gridStepX[$side] * $i + 4;
1659
1660				imageText(
1661					$this->im,
1662					8,
1663					0,
1664					$posX,
1665					$posY,
1666					$this->getColor($this->graphtheme['textcolor'], 0),
1667					$str
1668				);
1669			}
1670
1671			$str = convert_units([
1672				'value' => $maxY,
1673				'units' => $units,
1674				'convert' => ITEM_CONVERT_NO_UNITS,
1675				'byteStep' => $byteStep,
1676				'pow' => $newPow,
1677				'ignoreMillisec' => $ignoreMillisec,
1678				'length' => $maxLength
1679			]);
1680
1681			if ($side == GRAPH_YAXIS_SIDE_LEFT) {
1682				$dims = imageTextSize(8, 0, $str);
1683				$posX = $this->shiftXleft - $dims['width'] - 9;
1684				$color = $this->getColor(GRAPH_ZERO_LINE_COLOR_LEFT);
1685			}
1686			else {
1687				$posX = $this->sizeX + $this->shiftXleft + 12;
1688				$color = $this->getColor(GRAPH_ZERO_LINE_COLOR_RIGHT);
1689			}
1690
1691			imageText(
1692				$this->im,
1693				8,
1694				0,
1695				$posX,
1696				$this->shiftY + 4,
1697				$this->getColor($this->graphtheme['textcolor'], 0),
1698				$str
1699			);
1700
1701			if ($this->zero[$side] != $this->sizeY + $this->shiftY && $this->zero[$side] != $this->shiftY) {
1702				zbx_imageline(
1703					$this->im,
1704					$this->shiftXleft,
1705					$this->zero[$side],
1706					$this->shiftXleft + $this->sizeX,
1707					$this->zero[$side],
1708					$color
1709				);
1710			}
1711		}
1712	}
1713
1714	protected function drawWorkPeriod() {
1715		imagefilledrectangle($this->im,
1716			$this->shiftXleft + 1,
1717			$this->shiftY,
1718			$this->sizeX + $this->shiftXleft - 1, // -2 border
1719			$this->sizeY + $this->shiftY,
1720			$this->getColor($this->graphtheme['graphcolor'], 0)
1721		);
1722
1723		if ($this->m_showWorkPeriod != 1) {
1724			return;
1725		}
1726		if ($this->period > 8035200) { // 31*24*3600*3 (3*month*3)
1727			return;
1728		}
1729
1730		$config = select_config();
1731		$config = CMacrosResolverHelper::resolveTimeUnitMacros([$config], ['work_period'])[0];
1732
1733		$periods = parse_period($config['work_period']);
1734		if (!$periods) {
1735			return;
1736		}
1737
1738		imagefilledrectangle(
1739			$this->im,
1740			$this->shiftXleft + 1,
1741			$this->shiftY,
1742			$this->sizeX + $this->shiftXleft - 1, // -1 border
1743			$this->sizeY + $this->shiftY,
1744			$this->getColor($this->graphtheme['nonworktimecolor'], 0)
1745		);
1746
1747		$now = time();
1748		if (isset($this->stime)) {
1749			$this->from_time = $this->stime;
1750			$this->to_time = $this->stime + $this->period;
1751		}
1752		else {
1753			$this->to_time = $now;
1754			$this->from_time = $this->to_time - $this->period;
1755		}
1756
1757		$from = $this->from_time;
1758		$max_time = $this->to_time;
1759
1760		$start = find_period_start($periods, $from);
1761		$end = -1;
1762		while ($start < $max_time && $start > 0) {
1763			$end = find_period_end($periods, $start, $max_time);
1764
1765			$x1 = round((($start - $from) * $this->sizeX) / $this->period) + $this->shiftXleft;
1766			$x2 = ceil((($end - $from) * $this->sizeX) / $this->period) + $this->shiftXleft;
1767
1768			// draw rectangle
1769			imagefilledrectangle(
1770				$this->im,
1771				$x1,
1772				$this->shiftY,
1773				$x2 - 1, // -1 border
1774				$this->sizeY + $this->shiftY,
1775				$this->getColor($this->graphtheme['graphcolor'], 0)
1776			);
1777
1778			$start = find_period_start($periods, $end);
1779		}
1780	}
1781
1782	protected function drawPercentile() {
1783		if ($this->type != GRAPH_TYPE_NORMAL) {
1784			return;
1785		}
1786
1787		foreach ($this->percentile as $side => $percentile) {
1788			if ($percentile['percent'] > 0 && $percentile['value']) {
1789				$minY = $this->m_minY[$side];
1790				$maxY = $this->m_maxY[$side];
1791
1792				$color = ($side == GRAPH_YAXIS_SIDE_LEFT)
1793					? $this->graphtheme['leftpercentilecolor']
1794					: $this->graphtheme['rightpercentilecolor'];
1795
1796				$y = $this->sizeY - (($percentile['value'] - $minY) / ($maxY - $minY)) * $this->sizeY + $this->shiftY;
1797				zbx_imageline(
1798					$this->im,
1799					$this->shiftXleft,
1800					$y,
1801					$this->sizeX + $this->shiftXleft,
1802					$y,
1803					$this->getColor($color)
1804				);
1805			}
1806		}
1807	}
1808
1809	protected function drawTriggers() {
1810		if ($this->m_showTriggers != 1) {
1811			return;
1812		}
1813
1814		$oppColor = $this->getColor(GRAPH_TRIGGER_LINE_OPPOSITE_COLOR);
1815
1816		foreach ($this->triggers as $trigger) {
1817			$minY = $this->m_minY[$trigger['yaxisside']];
1818			$maxY = $this->m_maxY[$trigger['yaxisside']];
1819
1820			if ($minY >= $trigger['val'] || $trigger['val'] >= $maxY) {
1821				continue;
1822			}
1823
1824			$y = $this->sizeY - (($trigger['val'] - $minY) / ($maxY - $minY)) * $this->sizeY + $this->shiftY;
1825			$triggerColor = $this->getColor($trigger['color']);
1826			$lineStyle = [$triggerColor, $triggerColor, $triggerColor, $triggerColor, $triggerColor, $oppColor, $oppColor, $oppColor];
1827
1828			dashedLine( $this->im, $this->shiftXleft, $y, $this->sizeX + $this->shiftXleft, $y, $lineStyle);
1829			dashedLine( $this->im, $this->shiftXleft, $y + 1, $this->sizeX + $this->shiftXleft, $y + 1, $lineStyle);
1830		}
1831	}
1832
1833	protected function drawLegend() {
1834		// if graph is small, we are not drawing legend
1835		if (!$this->drawItemsLegend) {
1836			return true;
1837		}
1838
1839		$leftXShift = 15;
1840		$units = [GRAPH_YAXIS_SIDE_LEFT => 0, GRAPH_YAXIS_SIDE_RIGHT => 0];
1841
1842		// draw item legend
1843		$legend = new CImageTextTable($this->im, $leftXShift - 5, $this->sizeY + $this->shiftY + self::legendOffsetY);
1844		$legend->color = $this->getColor($this->graphtheme['textcolor'], 0);
1845		$legend->rowheight = 14;
1846		$legend->fontsize = 9;
1847
1848		// item legend table header
1849		$row = [
1850			['text' => '', 'marginRight' => 5],
1851			['text' => ''],
1852			['text' => ''],
1853			['text' => _('last'), 'align' => 1, 'fontsize' => 9],
1854			['text' => _('min'), 'align' => 1, 'fontsize' => 9],
1855			['text' => _('avg'), 'align' => 1, 'fontsize' => 9],
1856			['text' => _('max'), 'align' => 1, 'fontsize' => 9]
1857		];
1858
1859		$legend->addRow($row);
1860		$rowNum = $legend->getNumRows();
1861
1862		$i = ($this->type == GRAPH_TYPE_STACKED) ? $this->num - 1 : 0;
1863		while ($i >= 0 && $i < $this->num) {
1864			$color = $this->getColor($this->items[$i]['color'], GRAPH_STACKED_ALFA);
1865			switch ($this->items[$i]['calc_fnc']) {
1866				case CALC_FNC_MIN:
1867					$fncRealName = _('min');
1868					break;
1869				case CALC_FNC_MAX:
1870					$fncRealName = _('max');
1871					break;
1872				case CALC_FNC_ALL:
1873					$fncRealName = _('all');
1874					break;
1875				case CALC_FNC_AVG:
1876				default:
1877					$fncRealName = _('avg');
1878			}
1879
1880			$data = &$this->data[$this->items[$i]['itemid']][$this->items[$i]['calc_type']];
1881
1882			// draw color square
1883			if (function_exists('imagecolorexactalpha') && function_exists('imagecreatetruecolor') && @imagecreatetruecolor(1, 1)) {
1884				$colorSquare = imagecreatetruecolor(11, 11);
1885			}
1886			else {
1887				$colorSquare = imagecreate(11, 11);
1888			}
1889
1890			imagefill($colorSquare, 0, 0, $this->getColor($this->graphtheme['backgroundcolor'], 0));
1891			imagefilledrectangle($colorSquare, 0, 0, 10, 10, $color);
1892			imagerectangle($colorSquare, 0, 0, 10, 10, $this->getColor('Black'));
1893
1894			// caption
1895			$itemCaption = $this->itemsHost
1896				? $this->items[$i]['name_expanded']
1897				: $this->items[$i]['hostname'].NAME_DELIMITER.$this->items[$i]['name_expanded'];
1898
1899			// draw legend of an item with data
1900			if (isset($data) && isset($data['min'])) {
1901				if ($this->items[$i]['yaxisside'] == GRAPH_YAXIS_SIDE_LEFT) {
1902					$units[GRAPH_YAXIS_SIDE_LEFT] = $this->items[$i]['units'];
1903				}
1904				else {
1905					$units[GRAPH_YAXIS_SIDE_RIGHT] = $this->items[$i]['units'];
1906				}
1907
1908				$legend->addCell($rowNum, ['image' => $colorSquare, 'marginRight' => 5]);
1909				$legend->addCell($rowNum, ['text' => $itemCaption]);
1910				$legend->addCell($rowNum, ['text' => '['.$fncRealName.']']);
1911				$legend->addCell($rowNum, [
1912					'text' => convert_units([
1913						'value' => $this->getLastValue($i),
1914						'units' => $this->items[$i]['units'],
1915						'convert' => ITEM_CONVERT_NO_UNITS
1916					]),
1917					'align' => 2
1918				]);
1919				$legend->addCell($rowNum, [
1920					'text' => convert_units([
1921						'value' => min($data['min']),
1922						'units' => $this->items[$i]['units'],
1923						'convert' => ITEM_CONVERT_NO_UNITS
1924					]),
1925					'align' => 2
1926				]);
1927				$legend->addCell($rowNum, [
1928					'text' => convert_units([
1929						'value' => $data['avg_orig'],
1930						'units' => $this->items[$i]['units'],
1931						'convert' => ITEM_CONVERT_NO_UNITS
1932					]),
1933					'align' => 2
1934				]);
1935				$legend->addCell($rowNum, [
1936					'text' => convert_units([
1937						'value' => max($data['max']),
1938						'units' => $this->items[$i]['units'],
1939						'convert' => ITEM_CONVERT_NO_UNITS
1940					]),
1941					'align' => 2
1942				]);
1943			}
1944			// draw legend of an item without data
1945			else {
1946				$legend->addCell($rowNum, ['image' => $colorSquare, 'marginRight' => 5]);
1947				$legend->addCell($rowNum, ['text' => $itemCaption]);
1948				$legend->addCell($rowNum, ['text' => '['._('no data').']']);
1949			}
1950
1951			$rowNum++;
1952
1953			// legends for stacked graphs are written in reverse order so that the order of items
1954			// matches the order of lines on the graphs
1955			if ($this->type == GRAPH_TYPE_STACKED) {
1956				$i--;
1957			}
1958			else {
1959				$i++;
1960			}
1961		}
1962
1963		$legend->draw();
1964
1965		// if graph is small, we are not drawing percent line and trigger legends
1966		if (!$this->drawExLegend) {
1967			return true;
1968		}
1969
1970		$legend = new CImageTextTable(
1971			$this->im,
1972			$leftXShift + 10,
1973			$this->sizeY + $this->shiftY + 14 * $rowNum + self::legendOffsetY
1974		);
1975		$legend->color = $this->getColor($this->graphtheme['textcolor'], 0);
1976		$legend->rowheight = 14;
1977		$legend->fontsize = 9;
1978
1979		// draw percentile
1980		if ($this->type == GRAPH_TYPE_NORMAL) {
1981			foreach ($this->percentile as $side => $percentile) {
1982				if ($percentile['percent'] > 0 && $this->yaxis[$side]) {
1983					$percentile['percent'] = (float) $percentile['percent'];
1984					$convertedUnit = $percentile['value']
1985						? convert_units([
1986							'value' => $percentile['value'],
1987							'units' => $units[$side]
1988						])
1989						: '-';
1990					$side_str = ($side == GRAPH_YAXIS_SIDE_LEFT) ? _('left') : _('right');
1991					$legend->addCell($rowNum, [
1992						'text' => $percentile['percent'].'th percentile: '.$convertedUnit.' ('.$side_str.')',
1993						ITEM_CONVERT_NO_UNITS
1994					]);
1995					$color = ($side == GRAPH_YAXIS_SIDE_LEFT)
1996						? $this->graphtheme['leftpercentilecolor']
1997						: $this->graphtheme['rightpercentilecolor'];
1998
1999					imagefilledpolygon(
2000						$this->im,
2001						[
2002							$leftXShift + 5, $this->sizeY + $this->shiftY + 14 * $rowNum + self::legendOffsetY,
2003							$leftXShift - 5, $this->sizeY + $this->shiftY + 14 * $rowNum + self::legendOffsetY,
2004							$leftXShift, $this->sizeY + $this->shiftY + 14 * $rowNum + self::legendOffsetY - 10
2005						],
2006						3,
2007						$this->getColor($color)
2008					);
2009
2010					imagepolygon(
2011						$this->im,
2012						[
2013							$leftXShift + 5, $this->sizeY + $this->shiftY + 14 * $rowNum + self::legendOffsetY,
2014							$leftXShift - 5, $this->sizeY + $this->shiftY + 14 * $rowNum + self::legendOffsetY,
2015							$leftXShift, $this->sizeY + $this->shiftY + 14 * $rowNum + self::legendOffsetY - 10
2016						],
2017						3,
2018						$this->getColor('Black No Alpha')
2019					);
2020					$rowNum++;
2021				}
2022			}
2023		}
2024
2025		$legend->draw();
2026
2027		$legend = new CImageTextTable(
2028			$this->im,
2029			$leftXShift + 10,
2030			$this->sizeY + $this->shiftY + 14 * $rowNum + self::legendOffsetY + 5
2031		);
2032		$legend->color = $this->getColor($this->graphtheme['textcolor'], 0);
2033		$legend->rowheight = 14;
2034		$legend->fontsize = 9;
2035
2036		// draw triggers
2037		foreach ($this->triggers as $trigger) {
2038			imagefilledellipse(
2039				$this->im,
2040				$leftXShift,
2041				$this->sizeY + $this->shiftY + 14 * $rowNum + self::legendOffsetY,
2042				10,
2043				10,
2044				$this->getColor($trigger['color'])
2045			);
2046
2047			imageellipse(
2048				$this->im,
2049				$leftXShift,
2050				$this->sizeY + $this->shiftY + 14 * $rowNum + self::legendOffsetY,
2051				10,
2052				10,
2053				$this->getColor('Black No Alpha')
2054			);
2055
2056			$legend->addRow([
2057				['text' => $trigger['description']],
2058				['text' => $trigger['constant']]
2059			]);
2060			$rowNum++;
2061		}
2062
2063		$legend->draw();
2064	}
2065
2066	protected function limitToBounds(&$value1, &$value2, $min, $max, $drawtype) {
2067		// fixes graph out of bounds problem
2068		if ((($value1 > ($max + $min)) && ($value2 > ($max + $min))) || ($value1 < $min && $value2 < $min)) {
2069			if (!in_array($drawtype, [GRAPH_ITEM_DRAWTYPE_FILLED_REGION, GRAPH_ITEM_DRAWTYPE_GRADIENT_LINE])) {
2070				return false;
2071			}
2072		}
2073
2074		$y_first = $value1 > ($max + $min) || $value1 < $min;
2075		$y_second = $value2 > ($max + $min) || $value2 < $min;
2076
2077		if ($y_first) {
2078			$value1 = ($value1 > ($max + $min)) ? $max + $min : $min;
2079		}
2080
2081		if ($y_second) {
2082			$value2 = ($value2 > ($max + $min)) ? $max + $min : $min;
2083		}
2084
2085		return true;
2086	}
2087
2088	protected function drawElement(&$data, $from, $to, $minX, $maxX, $minY, $maxY, $drawtype, $max_color, $avg_color, $min_color, $minmax_color, $calc_fnc, $yaxisside) {
2089		if (!isset($data['max'][$from]) || !isset($data['max'][$to])) {
2090			return;
2091		}
2092
2093		$oxy = $this->oxy[$yaxisside];
2094		$zero = $this->zero[$yaxisside];
2095		$unit2px = $this->unit2px[$yaxisside];
2096
2097		$shift_min_from = $shift_min_to = 0;
2098		$shift_max_from = $shift_max_to = 0;
2099		$shift_avg_from = $shift_avg_to = 0;
2100
2101		if (isset($data['shift_min'][$from])) {
2102			$shift_min_from = $data['shift_min'][$from];
2103		}
2104		if (isset($data['shift_min'][$to])) {
2105			$shift_min_to = $data['shift_min'][$to];
2106		}
2107
2108		if (isset($data['shift_max'][$from])) {
2109			$shift_max_from = $data['shift_max'][$from];
2110		}
2111		if (isset($data['shift_max'][$to])) {
2112			$shift_max_to = $data['shift_max'][$to];
2113		}
2114
2115		if (isset($data['shift_avg'][$from])) {
2116			$shift_avg_from = $data['shift_avg'][$from];
2117		}
2118		if (isset($data['shift_avg'][$to])) {
2119			$shift_avg_to = $data['shift_avg'][$to];
2120		}
2121
2122		$min_from = $data['min'][$from] + $shift_min_from;
2123		$min_to = $data['min'][$to] + $shift_min_to;
2124
2125		$max_from = $data['max'][$from] + $shift_max_from;
2126		$max_to = $data['max'][$to] + $shift_max_to;
2127
2128		$avg_from = $data['avg'][$from] + $shift_avg_from;
2129		$avg_to = $data['avg'][$to] + $shift_avg_to;
2130
2131		$x1 = $from + $this->shiftXleft - 1;
2132		$x2 = $to + $this->shiftXleft;
2133
2134		$y1min = $zero - ($min_from - $oxy) / $unit2px;
2135		$y2min = $zero - ($min_to - $oxy) / $unit2px;
2136
2137		$y1max = $zero - ($max_from - $oxy) / $unit2px;
2138		$y2max = $zero - ($max_to - $oxy) / $unit2px;
2139
2140		$y1avg = $zero - ($avg_from - $oxy) / $unit2px;
2141		$y2avg = $zero - ($avg_to - $oxy) / $unit2px;
2142
2143		switch ($calc_fnc) {
2144			case CALC_FNC_MAX:
2145				$y1 = $y1max;
2146				$y2 = $y2max;
2147				$shift_from = $shift_max_from;
2148				$shift_to = $shift_max_to;
2149				break;
2150			case CALC_FNC_MIN:
2151				$y1 = $y1min;
2152				$y2 = $y2min;
2153				$shift_from = $shift_min_from;
2154				$shift_to = $shift_min_to;
2155				break;
2156			case CALC_FNC_ALL:
2157				// max
2158				$y1x = (($y1max > ($this->sizeY + $this->shiftY)) || $y1max < $this->shiftY);
2159				$y2x = (($y2max > ($this->sizeY + $this->shiftY)) || $y2max < $this->shiftY);
2160
2161				if ($y1x) {
2162					$y1max = ($y1max > ($this->sizeY + $this->shiftY)) ? $this->sizeY + $this->shiftY : $this->shiftY;
2163				}
2164				if ($y2x) {
2165					$y2max = ($y2max > ($this->sizeY + $this->shiftY)) ? $this->sizeY + $this->shiftY : $this->shiftY;
2166				}
2167
2168				// min
2169				$y1n = (($y1min > ($this->sizeY + $this->shiftY)) || $y1min < $this->shiftY);
2170				$y2n = (($y2min > ($this->sizeY + $this->shiftY)) || $y2min < $this->shiftY);
2171
2172				if ($y1n) {
2173					$y1min = ($y1min > ($this->sizeY + $this->shiftY)) ? $this->sizeY + $this->shiftY : $this->shiftY;
2174				}
2175				if ($y2n) {
2176					$y2min = ($y2min > ($this->sizeY + $this->shiftY)) ? $this->sizeY + $this->shiftY : $this->shiftY;
2177				}
2178
2179				$a[0] = $x1;
2180				$a[1] = $y1max;
2181				$a[2] = $x1;
2182				$a[3] = $y1min;
2183				$a[4] = $x2;
2184				$a[5] = $y2min;
2185				$a[6] = $x2;
2186				$a[7] = $y2max;
2187
2188			// don't use break, avg must be drawn in this statement
2189			case CALC_FNC_AVG:
2190
2191			// don't use break, avg must be drawn in this statement
2192			default:
2193				$y1 = $y1avg;
2194				$y2 = $y2avg;
2195				$shift_from = $shift_avg_from ;
2196				$shift_to = $shift_avg_to;
2197		}
2198
2199		$shift_from -= ($shift_from != 0) ? $oxy : 0;
2200		$shift_to -= ($shift_to != 0) ? $oxy : 0;
2201
2202		$y1_shift = $zero - $shift_from / $unit2px;
2203		$y2_shift = $zero - $shift_to / $unit2px;
2204
2205		if (!$this->limitToBounds($y1, $y2, $this->shiftY, $this->sizeY, $drawtype)) {
2206			return true;
2207		}
2208		if (!$this->limitToBounds($y1_shift, $y2_shift, $this->shiftY, $this->sizeY, $drawtype)) {
2209			return true;
2210		}
2211
2212		// draw main line
2213		switch ($drawtype) {
2214			case GRAPH_ITEM_DRAWTYPE_BOLD_LINE:
2215				if ($calc_fnc == CALC_FNC_ALL) {
2216					imagefilledpolygon($this->im, $a, 4, $minmax_color);
2217					if (!$y1x || !$y2x) {
2218						zbx_imagealine($this->im, $x1, $y1max, $x2, $y2max, $max_color, LINE_TYPE_BOLD);
2219					}
2220
2221					if (!$y1n || !$y2n) {
2222						zbx_imagealine($this->im, $x1, $y1min, $x2, $y2min, $min_color, LINE_TYPE_BOLD);
2223					}
2224				}
2225
2226				zbx_imagealine($this->im, $x1, $y1, $x2, $y2, $avg_color, LINE_TYPE_BOLD);
2227				break;
2228			case GRAPH_ITEM_DRAWTYPE_LINE:
2229				if ($calc_fnc == CALC_FNC_ALL) {
2230					imagefilledpolygon($this->im, $a, 4, $minmax_color);
2231					if (!$y1x || !$y2x) {
2232						zbx_imagealine($this->im, $x1, $y1max, $x2, $y2max, $max_color);
2233					}
2234					if (!$y1n || !$y2n) {
2235						zbx_imagealine($this->im, $x1, $y1min, $x2, $y2min, $min_color);
2236					}
2237				}
2238
2239				zbx_imagealine($this->im, $x1, $y1, $x2, $y2, $avg_color);
2240				break;
2241			case GRAPH_ITEM_DRAWTYPE_FILLED_REGION:
2242				$a[0] = $x1;
2243				$a[1] = $y1;
2244				$a[2] = $x1;
2245				$a[3] = $y1_shift;
2246				$a[4] = $x2;
2247				$a[5] = $y2_shift;
2248				$a[6] = $x2;
2249				$a[7] = $y2;
2250
2251				imagefilledpolygon($this->im, $a, 4, $avg_color);
2252				break;
2253			case GRAPH_ITEM_DRAWTYPE_DOT:
2254				imagefilledrectangle($this->im, $x1 - 1, $y1 - 1, $x1, $y1, $avg_color);
2255				break;
2256			case GRAPH_ITEM_DRAWTYPE_BOLD_DOT:
2257				imagefilledrectangle($this->im, $x2 - 1, $y2 - 1, $x2 + 1, $y2 + 1, $avg_color);
2258				break;
2259			case GRAPH_ITEM_DRAWTYPE_DASHED_LINE:
2260				if (function_exists('imagesetstyle')) {
2261					// use imagesetstyle+imageline instead of bugged imagedashedline
2262					$style = [$avg_color, $avg_color, IMG_COLOR_TRANSPARENT, IMG_COLOR_TRANSPARENT];
2263					imagesetstyle($this->im, $style);
2264					zbx_imageline($this->im, $x1, $y1, $x2, $y2, IMG_COLOR_STYLED);
2265				}
2266				else {
2267					imagedashedline($this->im, $x1, $y1, $x2, $y2, $avg_color);
2268				}
2269				break;
2270			case GRAPH_ITEM_DRAWTYPE_GRADIENT_LINE:
2271				imageLine($this->im, $x1, $y1, $x2, $y2, $avg_color); // draw the initial line
2272				imageLine($this->im, $x1, $y1 - 1, $x2, $y2 - 1, $avg_color);
2273
2274				$bitmask = 255;
2275				$blue = $avg_color & $bitmask;
2276
2277				// $blue_diff = 255 - $blue;
2278				$bitmask = $bitmask << 8;
2279				$green = ($avg_color & $bitmask) >> 8;
2280
2281				// $green_diff = 255 - $green;
2282				$bitmask = $bitmask << 8;
2283				$red = ($avg_color & $bitmask) >> 16;
2284				// $red_diff = 255 - $red;
2285
2286				// note: though gradients on the chart looks ok, the formula used is completely incorrect
2287				// if you plan to fix something here, it would be better to start from scratch
2288				$maxAlpha = 110;
2289				$startAlpha = 50;
2290				$alphaRatio = $maxAlpha / ($this->sizeY - $startAlpha);
2291
2292				$diffX = $x1 - $x2;
2293				for ($i = 0; $i <= $diffX; $i++) {
2294					$Yincr = ($diffX > 0) ? (abs($y2 - $y1) / $diffX) : 0;
2295
2296					$gy = ($y1 > $y2) ? ($y2 + $Yincr * $i) : ($y2 - $Yincr * $i);
2297					$steps = $this->sizeY + $this->shiftY - $gy + 1;
2298
2299					for ($j = 0; $j < $steps; $j++) {
2300						if (($gy + $j) < ($this->shiftY + $startAlpha)) {
2301							$alpha = 0;
2302						}
2303						else {
2304							$alpha = 127 - abs(127 - ($alphaRatio * ($gy + $j - $this->shiftY - $startAlpha)));
2305						}
2306
2307						$color = imagecolorexactalpha($this->im, $red, $green, $blue, $alpha);
2308						imagesetpixel($this->im, $x2 + $i, $gy + $j, $color);
2309					}
2310				}
2311			break;
2312		}
2313	}
2314
2315	private function calcSides() {
2316		$sides = [];
2317
2318		if (array_key_exists(GRAPH_YAXIS_SIDE_RIGHT, $this->axis_valuetype)) {
2319			$sides[] = GRAPH_YAXIS_SIDE_RIGHT;
2320		}
2321		if (array_key_exists(GRAPH_YAXIS_SIDE_LEFT, $this->axis_valuetype) || !$sides) {
2322			$sides[] = GRAPH_YAXIS_SIDE_LEFT;
2323		}
2324
2325		foreach ($sides as $side) {
2326			$this->m_minY[$side] = $this->calculateMinY($side);
2327			$this->m_maxY[$side] = $this->calculateMaxY($side);
2328
2329			if ($this->m_minY[$side] === null) {
2330				$this->m_minY[$side] = 0;
2331			}
2332			if ($this->m_maxY[$side] === null) {
2333				$this->m_maxY[$side] = 1;
2334			}
2335
2336			if ($this->m_minY[$side] == $this->m_maxY[$side]) {
2337				if ($this->graphOrientation[$side] == '-') {
2338					$this->m_maxY[$side] = 0;
2339				}
2340				elseif ($this->m_minY[$side] == 0) {
2341					$this->m_maxY[$side] = 1;
2342				}
2343				else {
2344					$this->m_minY[$side] = 0;
2345				}
2346			}
2347			elseif ($this->m_minY[$side] > $this->m_maxY[$side]) {
2348				if ($this->graphOrientation[$side] == '-') {
2349					$this->m_minY[$side] = bcmul($this->m_maxY[$side], 0.2);
2350				}
2351				else {
2352					$this->m_minY[$side] = 0;
2353				}
2354			}
2355
2356			// If max Y-scale bigger min Y-scale only for 10% or less, then we don't allow Y-scale duplicate
2357			if ($this->m_maxY[$side] && $this->m_minY[$side]) {
2358				if ($this->m_minY[$side] < 0) {
2359					$absMinY = bcmul($this->m_minY[$side], '-1');
2360				}
2361				else {
2362					$absMinY = $this->m_minY[$side];
2363				}
2364				if ($this->m_maxY[$side] < 0) {
2365					$absMaxY = bcmul($this->m_maxY[$side], '-1');
2366				}
2367				else {
2368					$absMaxY = $this->m_maxY[$side];
2369				}
2370
2371				if ($absMaxY < $absMinY) {
2372					$oldAbMaxY = $absMaxY;
2373					$absMaxY = $absMinY;
2374					$absMinY = $oldAbMaxY;
2375				}
2376			}
2377		}
2378	}
2379
2380	private function calcDimentions() {
2381		$this->shiftXleft = $this->yaxis[GRAPH_YAXIS_SIDE_LEFT] ? 85 : 30;
2382		$this->shiftXright = $this->yaxis[GRAPH_YAXIS_SIDE_RIGHT] ? 85 : 30;
2383
2384		$x_offsets = $this->shiftXleft + $this->shiftXright + 1;
2385		$y_offsets = $this->shiftY + self::legendOffsetY;
2386
2387		if (!$this->with_vertical_padding) {
2388			$y_offsets -= ($this->m_showTriggers && count($this->triggers) > 0)
2389				? static::DEFAULT_TOP_BOTTOM_PADDING / 2
2390				: static::DEFAULT_TOP_BOTTOM_PADDING;
2391		}
2392
2393		$this->fullSizeX = $this->sizeX;
2394		$this->fullSizeY = $this->sizeY;
2395
2396		if ($this->drawLegend) {
2397			// Reserve N+1 item rows, last row is used as padding for legend.
2398			$h_legend_items = 14 * $this->num + 14;
2399			$h_legend_triggers = 14 * count($this->triggers);
2400			$h_legend_percentile = 0;
2401
2402			foreach ($this->percentile as $side => $percentile) {
2403				if ($percentile['percent'] > 0 && $this->yaxis[$side]) {
2404					$h_legend_percentile += 14;
2405				}
2406			}
2407		}
2408
2409		if ($this->outer) {
2410			$this->sizeX = $this->fullSizeX - $x_offsets;
2411			$this->sizeY = $this->fullSizeY - $y_offsets;
2412
2413			if ($this->drawLegend) {
2414				if ($this->sizeY - $h_legend_items >= self::GRAPH_HEIGHT_MIN) {
2415					$this->sizeY -= $h_legend_items;
2416					$this->drawItemsLegend = true;
2417
2418					if ($this->sizeY - $h_legend_triggers - $h_legend_percentile >= self::GRAPH_HEIGHT_MIN) {
2419						$this->sizeY -= $h_legend_triggers + $h_legend_percentile;
2420						$this->drawExLegend = true;
2421					}
2422				}
2423			}
2424		}
2425		else {
2426			$this->fullSizeX += $x_offsets;
2427			$this->fullSizeY += $y_offsets;
2428
2429			if ($this->drawLegend) {
2430				$this->fullSizeY += $h_legend_items;
2431				$this->drawItemsLegend = true;
2432
2433				if ($this->sizeY >= ZBX_GRAPH_LEGEND_HEIGHT) {
2434					$this->fullSizeY += $h_legend_triggers + $h_legend_percentile;
2435					$this->drawExLegend = true;
2436				}
2437			}
2438		}
2439	}
2440
2441	public function getMinDimensions() {
2442		$min_dimentions = [
2443			'width' => self::GRAPH_WIDTH_MIN,
2444			'height' => self::GRAPH_HEIGHT_MIN
2445		];
2446
2447		if ($this->outer) {
2448			$min_dimentions['width'] += $this->yaxis[GRAPH_YAXIS_SIDE_LEFT] ? 85 : 30;
2449			$min_dimentions['width'] += $this->yaxis[GRAPH_YAXIS_SIDE_RIGHT] ? 85 : 30;
2450			$min_dimentions['width'] ++;
2451			$min_dimentions['height'] += $this->shiftY + self::legendOffsetY;
2452		}
2453
2454		return $min_dimentions;
2455	}
2456
2457	/**
2458	 * Expands graph item objects data: macros in item name, time units, dependent item
2459	 *
2460	 */
2461	private function expandItems() {
2462		$items_cache = zbx_toHash($this->items, 'itemid');
2463		$items = $this->items;
2464
2465		do {
2466			$master_itemids = [];
2467
2468			foreach ($items as $item) {
2469				if ($item['type'] == ITEM_TYPE_DEPENDENT && !array_key_exists($item['master_itemid'], $items_cache)) {
2470					$master_itemids[$item['master_itemid']] = true;
2471				}
2472				$items_cache[$item['itemid']] = $item;
2473			}
2474			$master_itemids = array_keys($master_itemids);
2475
2476			$items = API::Item()->get([
2477				'output' => ['itemid', 'type', 'master_itemid', 'delay'],
2478				'itemids' => $master_itemids
2479			]);
2480		} while ($items);
2481
2482		$update_interval_parser = new CUpdateIntervalParser();
2483
2484		foreach ($this->items as &$graph_item) {
2485			if ($graph_item['type'] == ITEM_TYPE_DEPENDENT) {
2486				$master_item = $graph_item;
2487
2488				while ($master_item && $master_item['type'] == ITEM_TYPE_DEPENDENT) {
2489					$master_item = $items_cache[$master_item['master_itemid']];
2490				}
2491				$graph_item['type'] = $master_item['type'];
2492				$graph_item['delay'] = $master_item['delay'];
2493			}
2494
2495			$graph_items = CMacrosResolverHelper::resolveItemNames([$graph_item]);
2496			$graph_items = CMacrosResolverHelper::resolveTimeUnitMacros($graph_items, ['delay']);
2497			$graph_item = reset($graph_items);
2498			$graph_item['name'] = $graph_item['name_expanded'];
2499			// getItemDelay will internally convert delay and flexible delay to seconds.
2500			$update_interval_parser->parse($graph_item['delay']);
2501			$graph_item['delay'] = getItemDelay($update_interval_parser->getDelay(),
2502				$update_interval_parser->getIntervals(ITEM_DELAY_FLEXIBLE)
2503			);
2504			$graph_item['has_scheduling_intervals']
2505				= (bool) $update_interval_parser->getIntervals(ITEM_DELAY_SCHEDULING);
2506
2507			if (strpos($graph_item['units'], ',') === false) {
2508				$graph_item['unitsLong'] = '';
2509			}
2510			else {
2511				list($graph_item['units'], $graph_item['unitsLong']) = explode(',', $graph_item['units']);
2512			}
2513		}
2514		unset($graph_item);
2515	}
2516
2517	/**
2518	 * Calculate graph dimensions and draw 1x1 pixel image placeholder.
2519	 */
2520	public function drawDimensions() {
2521		set_image_header();
2522
2523		$this->calculateTopPadding();
2524		$this->selectTriggers();
2525		$this->calcDimentions();
2526
2527		if (function_exists('imagecolorexactalpha') && function_exists('imagecreatetruecolor')
2528				&& @imagecreatetruecolor(1, 1)
2529		) {
2530			$this->im = imagecreatetruecolor(1, 1);
2531		}
2532		else {
2533			$this->im = imagecreate(1, 1);
2534		}
2535
2536		$this->initColors();
2537
2538		imageOut($this->im);
2539	}
2540
2541	public function draw() {
2542		$debug_mode = CWebUser::getDebugMode();
2543		if ($debug_mode) {
2544			$start_time = microtime(true);
2545		}
2546
2547		set_image_header();
2548		$this->calculateTopPadding();
2549
2550		// $this->sizeX is required for selectData() method
2551		$this->expandItems();
2552		$this->selectTriggers();
2553		$this->calcDimentions();
2554		$this->selectData();
2555
2556		$this->calcSides();
2557		$this->calcPercentile();
2558		$this->calcMinMaxInterval();
2559		$this->calcZero();
2560
2561		if (function_exists('imagecolorexactalpha') && function_exists('imagecreatetruecolor') && @imagecreatetruecolor(1, 1)) {
2562			$this->im = imagecreatetruecolor($this->fullSizeX, $this->fullSizeY);
2563		}
2564		else {
2565			$this->im = imagecreate($this->fullSizeX, $this->fullSizeY);
2566		}
2567
2568		$this->initColors();
2569		$this->drawRectangle();
2570		$this->drawHeader();
2571		$this->drawWorkPeriod();
2572		$this->drawTimeGrid();
2573		$this->drawHorizontalGrid();
2574		$this->drawXYAxisScale();
2575
2576		$maxX = $this->sizeX;
2577
2578		if ($this->dataFrom === 'trends') {
2579			// Correct item 'delay' field value when graph data requested for trends.
2580			foreach ($this->items as &$item) {
2581				if (!$item['has_scheduling_intervals'] || $item['delay'] != 0) {
2582					$item['delay'] = max($item['delay'], SEC_PER_HOUR);
2583				}
2584			}
2585			unset($item);
2586		}
2587
2588		// for each metric
2589		for ($item = 0; $item < $this->num; $item++) {
2590			$minY = $this->m_minY[$this->items[$item]['yaxisside']];
2591			$maxY = $this->m_maxY[$this->items[$item]['yaxisside']];
2592
2593			$data = &$this->data[$this->items[$item]['itemid']][$this->items[$item]['calc_type']];
2594
2595			if (!isset($data)) {
2596				continue;
2597			}
2598
2599			if ($this->type == GRAPH_TYPE_STACKED) {
2600				$drawtype = $this->items[$item]['drawtype'];
2601				$max_color = $this->getColor('ValueMax', GRAPH_STACKED_ALFA);
2602				$avg_color = $this->getColor($this->items[$item]['color'], GRAPH_STACKED_ALFA);
2603				$min_color = $this->getColor('ValueMin', GRAPH_STACKED_ALFA);
2604				$minmax_color = $this->getColor('ValueMinMax', GRAPH_STACKED_ALFA);
2605
2606				$calc_fnc = $this->items[$item]['calc_fnc'];
2607			}
2608			else {
2609				$drawtype = $this->items[$item]['drawtype'];
2610				$max_color = $this->getColor('ValueMax', GRAPH_STACKED_ALFA);
2611				$avg_color = $this->getColor($this->items[$item]['color'], GRAPH_STACKED_ALFA);
2612				$min_color = $this->getColor('ValueMin', GRAPH_STACKED_ALFA);
2613				$minmax_color = $this->getColor('ValueMinMax', GRAPH_STACKED_ALFA);
2614
2615				$calc_fnc = $this->items[$item]['calc_fnc'];
2616			}
2617
2618			// for each X
2619			$prevDraw = true;
2620			for ($i = 1, $j = 0; $i < $maxX; $i++) { // new point
2621				if ($data['count'][$i] == 0 && $i != ($maxX - 1)) {
2622					continue;
2623				}
2624
2625				$delay = $this->items[$item]['delay'];
2626
2627				if ($this->items[$item]['type'] == ITEM_TYPE_TRAPPER
2628						|| ($this->items[$item]['type'] == ITEM_TYPE_ZABBIX_ACTIVE
2629							&& preg_match('/^(event)?log(rt)?\[/', $this->items[$item]['key_']))
2630						|| ($this->items[$item]['has_scheduling_intervals'] && $delay == 0)) {
2631					$draw = true;
2632				}
2633				else {
2634					if ($data['clock'] === null) {
2635						$diff = 0;
2636					}
2637					else {
2638						$diff = abs($data['clock'][$i] - $data['clock'][$j]);
2639					}
2640
2641					$cell = ($this->to_time - $this->from_time) / $this->sizeX;
2642
2643					if ($cell > $delay) {
2644						$draw = ($diff < (ZBX_GRAPH_MAX_SKIP_CELL * $cell));
2645					}
2646					else {
2647						$draw = ($diff < (ZBX_GRAPH_MAX_SKIP_DELAY * $delay));
2648					}
2649				}
2650
2651				if (!$draw && !$prevDraw) {
2652					$draw = true;
2653					$valueDrawType = GRAPH_ITEM_DRAWTYPE_BOLD_DOT;
2654				}
2655				else {
2656					$valueDrawType = $drawtype;
2657					$prevDraw = $draw;
2658				}
2659
2660				if ($draw) {
2661					$this->drawElement(
2662						$data,
2663						$i,
2664						$j,
2665						0,
2666						$this->sizeX,
2667						$minY,
2668						$maxY,
2669						$valueDrawType,
2670						$max_color,
2671						$avg_color,
2672						$min_color,
2673						$minmax_color,
2674						$calc_fnc,
2675						$this->items[$item]['yaxisside']
2676					);
2677				}
2678
2679				$j = $i;
2680			}
2681		}
2682
2683		$this->drawSides();
2684
2685		if ($this->drawLegend) {
2686			$this->drawTriggers();
2687			$this->drawPercentile();
2688			$this->drawLegend();
2689		}
2690
2691		if ($debug_mode) {
2692			$str = sprintf('%0.2f', microtime(true) - $start_time);
2693			imageText($this->im, 6, 90, $this->fullSizeX - 2, $this->fullSizeY - 5, $this->getColor('Gray'),
2694				_s('Data from %1$s. Generated in %2$s sec.', $this->dataFrom, $str)
2695			);
2696		}
2697
2698		unset($this->items, $this->data);
2699
2700		imageOut($this->im);
2701	}
2702}
2703