You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2246 lines
63 KiB
2246 lines
63 KiB
<?php
|
|
/*
|
|
** Zabbix
|
|
** Copyright (C) 2001-2023 Zabbix SIA
|
|
**
|
|
** This program is free software; you can redistribute it and/or modify
|
|
** it under the terms of the GNU General Public License as published by
|
|
** the Free Software Foundation; either version 2 of the License, or
|
|
** (at your option) any later version.
|
|
**
|
|
** This program is distributed in the hope that it will be useful,
|
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
** GNU General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with this program; if not, write to the Free Software
|
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
**/
|
|
|
|
|
|
class CLineGraphDraw extends CGraphDraw {
|
|
const GRAPH_WIDTH_MIN = 20;
|
|
const GRAPH_HEIGHT_MIN = 20;
|
|
const LEGEND_OFFSET_Y = 90;
|
|
|
|
private $cell_height_min;
|
|
private $cell_width;
|
|
private $drawExLegend;
|
|
private $drawItemsLegend;
|
|
private $intervals;
|
|
private $is_binary;
|
|
private $itemsHost;
|
|
private $outer;
|
|
private $oxy;
|
|
private $percentile;
|
|
private $power;
|
|
private $show_triggers;
|
|
private $show_work_period;
|
|
private $triggers;
|
|
private $unit2px;
|
|
private $yaxis;
|
|
private $yaxismin;
|
|
private $yaxismax;
|
|
private $ymin_itemid;
|
|
private $ymax_itemid;
|
|
private $ymin_type;
|
|
private $ymax_type;
|
|
private $zero;
|
|
|
|
public function __construct($type = GRAPH_TYPE_NORMAL) {
|
|
parent::__construct($type);
|
|
|
|
$this->triggers = [];
|
|
|
|
$this->yaxis = [
|
|
GRAPH_YAXIS_SIDE_LEFT => false,
|
|
GRAPH_YAXIS_SIDE_RIGHT => false
|
|
];
|
|
|
|
$this->ymin_type = GRAPH_YAXIS_TYPE_CALCULATED;
|
|
$this->ymax_type = GRAPH_YAXIS_TYPE_CALCULATED;
|
|
|
|
$this->yaxismin = null;
|
|
$this->yaxismax = null;
|
|
|
|
$this->ymin_itemid = 0;
|
|
$this->ymax_itemid = 0;
|
|
|
|
$this->percentile = [
|
|
GRAPH_YAXIS_SIDE_LEFT => [
|
|
'percent' => 0, // draw percentage line
|
|
'value' => 0 // calculated percentage value left y axis
|
|
],
|
|
GRAPH_YAXIS_SIDE_RIGHT => [
|
|
'percent' => 0, // draw percentage line
|
|
'value' => 0 // calculated percentage value right y axis
|
|
]
|
|
];
|
|
|
|
$this->outer = false;
|
|
|
|
$this->show_work_period = true;
|
|
$this->show_triggers = true;
|
|
|
|
$this->zero = [];
|
|
|
|
$this->cell_width = 30;
|
|
$this->cell_height_min = 30;
|
|
|
|
$this->intervals = [];
|
|
$this->power = [];
|
|
$this->is_binary = [];
|
|
|
|
$this->drawItemsLegend = false; // draw items legend
|
|
$this->drawExLegend = false; // draw percentile and triggers legend
|
|
}
|
|
|
|
public function showWorkPeriod($value) {
|
|
$this->show_work_period = $value;
|
|
}
|
|
|
|
public function showTriggers($value) {
|
|
$this->show_triggers = $value;
|
|
}
|
|
|
|
/**
|
|
* Add single item object to graph. If invalid 'delay' interval passed method will interrupt current request with
|
|
* error message.
|
|
*
|
|
* @param array $graph_item Array of graph item properties.
|
|
* @param string $graph_item['itemid'] Item id.
|
|
* @param string $graph_item['type'] Item type.
|
|
* @param string $graph_item['name'] Item host display name.
|
|
* @param string $graph_item['hostname'] Item hostname.
|
|
* @param string $graph_item['key_'] Item key_ field value.
|
|
* @param string $graph_item['value_type'] Item value type.
|
|
* @param string $graph_item['history'] Item history field value.
|
|
* @param string $graph_item['trends'] Item trends field value.
|
|
* @param string $graph_item['delay'] Item delay.
|
|
* @param string $graph_item['master_itemid'] Master item id for item of type ITEM_TYPE_DEPENDENT.
|
|
* @param string $graph_item['units'] Item units value.
|
|
* @param string $graph_item['hostid'] Item host id.
|
|
* @param string $graph_item['hostname'] Item host name.
|
|
* @param string $graph_item['color'] Item presentation color.
|
|
* @param int $graph_item['drawtype'] Item presentation draw type, could be one of
|
|
* GRAPH_ITEM_DRAWTYPE_* constants.
|
|
* @param int $graph_item['yaxisside'] Item axis side, could be one of GRAPH_YAXIS_SIDE_* constants.
|
|
* @param int $graph_item['calc_fnc'] Item calculation function, could be one of CALC_FNC_* constants.
|
|
* @param int $graph_item['calc_type'] Item graph presentation calculation type, GRAPH_ITEM_SIMPLE or
|
|
* GRAPH_ITEM_SUM.
|
|
*/
|
|
public function addItem(array $graph_item) {
|
|
if ($this->type == GRAPH_TYPE_STACKED) {
|
|
$graph_item['drawtype'] = GRAPH_ITEM_DRAWTYPE_FILLED_REGION;
|
|
}
|
|
$update_interval_parser = new CUpdateIntervalParser(['usermacros' => true]);
|
|
|
|
if ($update_interval_parser->parse($graph_item['delay']) != CParser::PARSE_SUCCESS) {
|
|
show_error_message(_s('Incorrect value for field "%1$s": %2$s.', 'delay', _('invalid delay')));
|
|
exit;
|
|
}
|
|
|
|
// Set graph item safe default values.
|
|
$graph_item += [
|
|
'color' => 'Dark Green',
|
|
'drawtype' => GRAPH_ITEM_DRAWTYPE_LINE,
|
|
'yaxisside' => GRAPH_YAXIS_SIDE_DEFAULT,
|
|
'calc_fnc' => CALC_FNC_AVG,
|
|
'calc_type' => GRAPH_ITEM_SIMPLE
|
|
];
|
|
|
|
$this->items[$this->num] = $graph_item;
|
|
|
|
$this->yaxis[$graph_item['yaxisside']] = true;
|
|
|
|
$this->num++;
|
|
}
|
|
|
|
public function setYMinAxisType($yaxistype) {
|
|
$this->ymin_type = $yaxistype;
|
|
}
|
|
|
|
public function setYMaxAxisType($yaxistype) {
|
|
$this->ymax_type = $yaxistype;
|
|
}
|
|
|
|
public function setYAxisMin($yaxismin) {
|
|
$this->yaxismin = $yaxismin;
|
|
}
|
|
|
|
public function setYAxisMax($yaxismax) {
|
|
$this->yaxismax = $yaxismax;
|
|
}
|
|
|
|
public function setYMinItemId($itemid) {
|
|
$this->ymin_itemid = $itemid;
|
|
}
|
|
|
|
public function setYMaxItemId($itemid) {
|
|
$this->ymax_itemid = $itemid;
|
|
}
|
|
|
|
public function setLeftPercentage($percentile) {
|
|
$this->percentile[GRAPH_YAXIS_SIDE_LEFT]['percent'] = $percentile;
|
|
}
|
|
|
|
public function setRightPercentage($percentile) {
|
|
$this->percentile[GRAPH_YAXIS_SIDE_RIGHT]['percent'] = $percentile;
|
|
}
|
|
|
|
/**
|
|
* Interpret width/height as image size; or as the graph size, if the argument is false.
|
|
*
|
|
* @param bool $outer
|
|
*/
|
|
public function setOuter($outer) {
|
|
$this->outer = $outer;
|
|
}
|
|
|
|
/**
|
|
* Get list of vertical scales in use, starting from the main one.
|
|
*
|
|
* @return array
|
|
*/
|
|
private function getVerticalScalesInUse() {
|
|
return array_keys(array_filter($this->yaxis));
|
|
}
|
|
|
|
protected function selectData() {
|
|
$this->data = [];
|
|
|
|
$time_now = time();
|
|
|
|
if ($this->stime === null) {
|
|
$this->stime = $time_now - $this->period;
|
|
}
|
|
|
|
$this->from_time = $this->stime;
|
|
$this->to_time = $this->stime + $this->period;
|
|
|
|
$this->itemsHost = null;
|
|
|
|
$items = [];
|
|
|
|
for ($i = 0; $i < $this->num; $i++) {
|
|
$item = $this->items[$i];
|
|
|
|
if ($this->itemsHost === null) {
|
|
// Select item host for graph caption.
|
|
$this->itemsHost = $item['hostid'];
|
|
}
|
|
elseif ($this->itemsHost != $item['hostid']) {
|
|
// Do not select any item host for graph caption, if more than one item host.
|
|
$this->itemsHost = false;
|
|
}
|
|
|
|
$to_resolve = [];
|
|
|
|
// Override item history setting with housekeeping settings, if they are enabled in config.
|
|
if (CHousekeepingHelper::get(CHousekeepingHelper::HK_HISTORY_GLOBAL)) {
|
|
$item['history'] = timeUnitToSeconds(CHousekeepingHelper::get(CHousekeepingHelper::HK_HISTORY));
|
|
}
|
|
else {
|
|
$to_resolve[] = 'history';
|
|
}
|
|
|
|
if (CHousekeepingHelper::get(CHousekeepingHelper::HK_TRENDS_GLOBAL)) {
|
|
$item['trends'] = timeUnitToSeconds(CHousekeepingHelper::get(CHousekeepingHelper::HK_TRENDS));
|
|
}
|
|
else {
|
|
$to_resolve[] = 'trends';
|
|
}
|
|
|
|
// Otherwise, resolve user macro and parse the string. If successful, convert to seconds.
|
|
if ($to_resolve) {
|
|
$item = CMacrosResolverHelper::resolveTimeUnitMacros([$item], $to_resolve)[0];
|
|
|
|
$simple_interval_parser = new CSimpleIntervalParser();
|
|
|
|
if (!CHousekeepingHelper::get(CHousekeepingHelper::HK_HISTORY_GLOBAL)) {
|
|
if ($simple_interval_parser->parse($item['history']) != CParser::PARSE_SUCCESS) {
|
|
show_error_message(_s('Incorrect value for field "%1$s": %2$s.', 'history',
|
|
_('invalid history storage period')
|
|
));
|
|
exit;
|
|
}
|
|
$item['history'] = timeUnitToSeconds($item['history']);
|
|
}
|
|
|
|
if (!CHousekeepingHelper::get(CHousekeepingHelper::HK_TRENDS_GLOBAL)) {
|
|
if ($simple_interval_parser->parse($item['trends']) != CParser::PARSE_SUCCESS) {
|
|
show_error_message(_s('Incorrect value for field "%1$s": %2$s.', 'trends',
|
|
_('invalid trend storage period')
|
|
));
|
|
exit;
|
|
}
|
|
$item['trends'] = timeUnitToSeconds($item['trends']);
|
|
}
|
|
}
|
|
|
|
$item['source'] = ($item['trends'] == 0 || (($time_now - $item['history']) < $this->from_time
|
|
&& ($this->period / $this->sizeX) <= (ZBX_MAX_TREND_DIFF / ZBX_GRAPH_MAX_SKIP_CELL)))
|
|
? 'history'
|
|
: 'trends';
|
|
|
|
$this->items[$i]['source'] = $item['source'];
|
|
|
|
$items[] = $item;
|
|
}
|
|
|
|
$results = Manager::History()->getGraphAggregationByWidth($items, $this->from_time, $this->to_time,
|
|
$this->sizeX
|
|
);
|
|
|
|
foreach ($items as $item) {
|
|
$data = [
|
|
'count' => [],
|
|
'min' => [],
|
|
'max' => [],
|
|
'avg' => [],
|
|
'clock' => []
|
|
];
|
|
|
|
if (array_key_exists($item['itemid'], $results)) {
|
|
$result = $results[$item['itemid']];
|
|
|
|
foreach ($result['data'] as $data_row) {
|
|
$idx = $data_row['i'];
|
|
|
|
$data['count'][$idx] = $data_row['count'];
|
|
$data['min'][$idx] = (float) $data_row['min'];
|
|
$data['max'][$idx] = (float) $data_row['max'];
|
|
$data['avg'][$idx] = (float) $data_row['avg'];
|
|
$data['clock'][$idx] = $data_row['clock'];
|
|
$data['shift_min'][$idx] = 0;
|
|
$data['shift_max'][$idx] = 0;
|
|
$data['shift_avg'][$idx] = 0;
|
|
}
|
|
}
|
|
|
|
$data['avg_orig'] = $data['avg'] ? CMathHelper::safeAvg($data['avg']) : null;
|
|
|
|
/*
|
|
first_idx - last existing point
|
|
ci - current index
|
|
cj - count of missed in one go
|
|
dx - offset to first value (count to last existing point)
|
|
*/
|
|
for ($ci = 0, $cj = 0; $ci <= $this->sizeX; $ci++) {
|
|
if (!array_key_exists($ci, $data['count']) || ($data['count'][$ci] == 0)) {
|
|
$data['count'][$ci] = 0;
|
|
$data['shift_min'][$ci] = 0;
|
|
$data['shift_max'][$ci] = 0;
|
|
$data['shift_avg'][$ci] = 0;
|
|
$cj++;
|
|
continue;
|
|
}
|
|
|
|
if ($cj == 0) {
|
|
continue;
|
|
}
|
|
|
|
$dx = $cj + 1;
|
|
$first_idx = $ci - $dx;
|
|
|
|
if ($first_idx < 0) {
|
|
$first_idx = $ci; // if no data from start of graph get current data as first data
|
|
}
|
|
|
|
for(; $cj > 0; $cj--) {
|
|
if ($dx < ($this->sizeX / 20) && $this->type == GRAPH_TYPE_STACKED) {
|
|
$data['count'][$ci - ($dx - $cj)] = 1;
|
|
}
|
|
|
|
foreach (['clock', 'min', 'max', 'avg'] as $var_name) {
|
|
if ($first_idx == $ci && $var_name == 'clock') {
|
|
$data['clock'][$ci - ($dx - $cj)] = $data['clock'][$first_idx] -
|
|
($this->to_time - $this->from_time) / $this->sizeX * ($dx - $cj);
|
|
|
|
continue;
|
|
}
|
|
|
|
$data[$var_name][$ci - ($dx - $cj)] = CMathHelper::safeSum([
|
|
$data[$var_name][$first_idx],
|
|
CMathHelper::safeMul([$cj, 1 / $dx, $data[$var_name][$ci]]),
|
|
CMathHelper::safeMul([$cj, 1 / $dx, -$data[$var_name][$first_idx]])
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($cj > 0 && $ci > $cj) {
|
|
$dx = $cj + 1;
|
|
$first_idx = $ci - $dx;
|
|
|
|
for(; $cj > 0; $cj--) {
|
|
foreach (['clock', 'min', 'max', 'avg'] as $var) {
|
|
$data[$var][$first_idx + ($dx - $cj)] = ($var == 'clock')
|
|
? $data[$var][$first_idx] + ($this->to_time - $this->from_time) / $this->sizeX * ($dx - $cj)
|
|
: $data[$var][$first_idx];
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->data[$item['itemid']] = $data;
|
|
}
|
|
|
|
// calculate shift for stacked graphs
|
|
if ($this->type == GRAPH_TYPE_STACKED) {
|
|
for ($i = 1; $i < $this->num; $i++) {
|
|
$item1 = $this->items[$i];
|
|
|
|
if (!array_key_exists($item1['itemid'], $this->data)) {
|
|
continue;
|
|
}
|
|
|
|
$curr_data = &$this->data[$item1['itemid']];
|
|
|
|
for ($j = $i - 1; $j >= 0; $j--) {
|
|
$item2 = $this->items[$j];
|
|
|
|
if ($item2['yaxisside'] != $item1['yaxisside']) {
|
|
continue;
|
|
}
|
|
|
|
if (!array_key_exists($item2['itemid'], $this->data)) {
|
|
continue;
|
|
}
|
|
|
|
$prev_data = &$this->data[$item2['itemid']];
|
|
|
|
for ($ci = 0; $ci <= $this->sizeX; $ci++) {
|
|
foreach (['min', 'max', 'avg'] as $var_name) {
|
|
$shift_var_name = 'shift_'.$var_name;
|
|
$curr_shift = &$curr_data[$shift_var_name];
|
|
$prev_shift = &$prev_data[$shift_var_name];
|
|
$prev_var = &$prev_data[$var_name];
|
|
|
|
$prev_var_ci = $prev_var ? $prev_var[$ci] : 0;
|
|
$prev_shift_ci = $prev_shift ? $prev_shift[$ci] : 0;
|
|
$curr_shift[$ci] = $prev_var_ci + $prev_shift_ci;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function selectTriggers() {
|
|
$this->triggers = [];
|
|
|
|
if (!$this->show_triggers) {
|
|
return;
|
|
}
|
|
|
|
$number_parser = new CNumberParser(['with_size_suffix' => true, 'with_time_suffix' => true]);
|
|
|
|
$max = 3;
|
|
$cnt = 0;
|
|
|
|
foreach ($this->items as $item) {
|
|
$db_triggers = DBselect(
|
|
'SELECT DISTINCT h.host,tr.description,tr.triggerid,tr.expression,tr.priority,tr.value'.
|
|
' FROM triggers tr,functions f,items i,hosts h'.
|
|
' WHERE tr.triggerid=f.triggerid'.
|
|
" AND f.name IN ('last','min','avg','max')".
|
|
' AND tr.status='.TRIGGER_STATUS_ENABLED.
|
|
' AND i.itemid=f.itemid'.
|
|
' AND h.hostid=i.hostid'.
|
|
' AND f.itemid='.zbx_dbstr($item['itemid']).
|
|
' ORDER BY tr.priority'
|
|
);
|
|
|
|
while (($trigger = DBfetch($db_triggers)) && $cnt < $max) {
|
|
$fnc_cnt = DBfetch(DBselect(
|
|
'SELECT COUNT(*) AS cnt'.
|
|
' FROM functions f'.
|
|
' WHERE f.triggerid='.zbx_dbstr($trigger['triggerid'])
|
|
));
|
|
|
|
if ($fnc_cnt['cnt'] != 1) {
|
|
continue;
|
|
}
|
|
|
|
$trigger['expression'] = CMacrosResolverHelper::resolveTriggerExpressions([$trigger],
|
|
['resolve_usermacros' => true, 'resolve_functionids' => false]
|
|
)[0]['expression'];
|
|
|
|
if (!preg_match('/^\{\d+\}\s*(?<operator>[><]=?|=)\s*(?<constant>.*)$/', $trigger['expression'],
|
|
$matches)) {
|
|
continue;
|
|
}
|
|
|
|
if ($number_parser->parse($matches['constant']) != CParser::PARSE_SUCCESS) {
|
|
continue;
|
|
}
|
|
|
|
$this->triggers[] = [
|
|
'yaxisside' => $item['yaxisside'],
|
|
'val' => $number_parser->calcValue(),
|
|
'color' => CSeverityHelper::getColor((int) $trigger['priority']),
|
|
'description' => _('Trigger').NAME_DELIMITER.CMacrosResolverHelper::resolveTriggerName($trigger),
|
|
'constant' => '['.$matches['operator'].' '.$matches['constant'].']'
|
|
];
|
|
|
|
$cnt++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// calculates percentages for left & right Y axis
|
|
protected function calcPercentile() {
|
|
if ($this->type != GRAPH_TYPE_NORMAL) {
|
|
return;
|
|
}
|
|
|
|
$values = [
|
|
GRAPH_YAXIS_SIDE_LEFT => [],
|
|
GRAPH_YAXIS_SIDE_RIGHT => []
|
|
];
|
|
|
|
$maxX = $this->sizeX;
|
|
|
|
// for each metric
|
|
for ($i = 0; $i < $this->num; $i++) {
|
|
if (!array_key_exists($this->items[$i]['itemid'], $this->data)) {
|
|
continue;
|
|
}
|
|
|
|
$data = &$this->data[$this->items[$i]['itemid']];
|
|
|
|
// for each X
|
|
for ($j = 0; $j < $maxX; $j++) { // new point
|
|
if ($data['count'][$j] == 0) {
|
|
continue;
|
|
}
|
|
|
|
switch ($this->items[$i]['calc_fnc']) {
|
|
case CALC_FNC_MAX:
|
|
$value = $data['max'][$j];
|
|
break;
|
|
case CALC_FNC_MIN:
|
|
$value = $data['min'][$j];
|
|
break;
|
|
case CALC_FNC_ALL:
|
|
case CALC_FNC_AVG:
|
|
default:
|
|
$value = $data['avg'][$j];
|
|
}
|
|
|
|
$values[$this->items[$i]['yaxisside']][] = $value;
|
|
}
|
|
}
|
|
|
|
foreach ($this->percentile as $side => $percentile) {
|
|
if ($percentile['percent'] > 0 && $values[$side]) {
|
|
sort($values[$side]);
|
|
|
|
// Using "Nearest Rank" method.
|
|
$percent = (int) ceil($percentile['percent'] / 100 * count($values[$side]));
|
|
$this->percentile[$side]['value'] = $values[$side][$percent - 1];
|
|
}
|
|
}
|
|
}
|
|
|
|
// calculation of minimum Y of a side (left/right)
|
|
protected function calculateMinY($side) {
|
|
if ($this->ymin_type == GRAPH_YAXIS_TYPE_FIXED) {
|
|
return $this->yaxismin;
|
|
}
|
|
|
|
if ($this->ymin_type == GRAPH_YAXIS_TYPE_ITEM_VALUE && $this->ymin_itemid != 0) {
|
|
$items = API::Item()->get([
|
|
'output' => ['itemid', 'value_type'],
|
|
'itemids' => [$this->ymin_itemid],
|
|
'webitems' => true
|
|
]);
|
|
|
|
if ($items) {
|
|
$history = Manager::History()->getLastValues($items);
|
|
|
|
if ($history) {
|
|
return $history[$items[0]['itemid']][0]['value'];
|
|
}
|
|
}
|
|
else {
|
|
$this->ymin_type = GRAPH_YAXIS_TYPE_CALCULATED;
|
|
}
|
|
}
|
|
|
|
$minY = null;
|
|
for ($i = 0; $i < $this->num; $i++) {
|
|
if ($this->items[$i]['yaxisside'] != $side) {
|
|
continue;
|
|
}
|
|
|
|
if ($this->items[$i]['calc_type'] != GRAPH_ITEM_SIMPLE) {
|
|
continue;
|
|
}
|
|
|
|
if (!array_key_exists($this->items[$i]['itemid'], $this->data)) {
|
|
continue;
|
|
}
|
|
|
|
$data = &$this->data[$this->items[$i]['itemid']];
|
|
|
|
$calc_fnc = $this->items[$i]['calc_fnc'];
|
|
|
|
switch ($calc_fnc) {
|
|
case CALC_FNC_ALL:
|
|
case CALC_FNC_MIN:
|
|
$values = $data['min'];
|
|
$shift_values = $data['shift_min'];
|
|
break;
|
|
case CALC_FNC_MAX:
|
|
$values = $data['max'];
|
|
$shift_values = $data['shift_max'];
|
|
break;
|
|
case CALC_FNC_AVG:
|
|
default:
|
|
$values = $data['avg'];
|
|
$shift_values = $data['shift_avg'];
|
|
}
|
|
|
|
if (!$values) {
|
|
continue;
|
|
}
|
|
|
|
if ($this->type == GRAPH_TYPE_STACKED) {
|
|
foreach ($values as $ci => &$value) {
|
|
if ($data['count'][$ci] == 0) {
|
|
continue;
|
|
}
|
|
$value += $shift_values[$ci];
|
|
}
|
|
unset($value);
|
|
}
|
|
|
|
$minY = ($minY === null) ? min($values) : min($minY, min($values));
|
|
}
|
|
|
|
return $minY;
|
|
}
|
|
|
|
// calculation of maximum Y of a side (left/right)
|
|
protected function calculateMaxY($side) {
|
|
if ($this->ymax_type == GRAPH_YAXIS_TYPE_FIXED) {
|
|
return $this->yaxismax;
|
|
}
|
|
|
|
if ($this->ymax_type == GRAPH_YAXIS_TYPE_ITEM_VALUE && $this->ymax_itemid != 0) {
|
|
$items = API::Item()->get([
|
|
'output' => ['itemid', 'value_type'],
|
|
'itemids' => [$this->ymax_itemid],
|
|
'webitems' => true
|
|
]);
|
|
|
|
if ($items) {
|
|
$history = Manager::History()->getLastValues($items);
|
|
|
|
if ($history) {
|
|
return $history[$items[0]['itemid']][0]['value'];
|
|
}
|
|
}
|
|
else {
|
|
$this->ymax_type = GRAPH_YAXIS_TYPE_CALCULATED;
|
|
}
|
|
}
|
|
|
|
$maxY = null;
|
|
for ($i = 0; $i < $this->num; $i++) {
|
|
if ($this->items[$i]['yaxisside'] != $side) {
|
|
continue;
|
|
}
|
|
|
|
if ($this->items[$i]['calc_type'] != GRAPH_ITEM_SIMPLE) {
|
|
continue;
|
|
}
|
|
|
|
if (!array_key_exists($this->items[$i]['itemid'], $this->data)) {
|
|
continue;
|
|
}
|
|
|
|
$data = &$this->data[$this->items[$i]['itemid']];
|
|
|
|
$calc_fnc = $this->items[$i]['calc_fnc'];
|
|
|
|
switch ($calc_fnc) {
|
|
case CALC_FNC_ALL:
|
|
case CALC_FNC_MAX:
|
|
$values = $data['max'];
|
|
$shift_values = $data['shift_max'];
|
|
break;
|
|
case CALC_FNC_MIN:
|
|
$values = $data['min'];
|
|
$shift_values = $data['shift_min'];
|
|
break;
|
|
case CALC_FNC_AVG:
|
|
default:
|
|
$values = $data['avg'];
|
|
$shift_values = $data['shift_avg'];
|
|
}
|
|
|
|
if (!$values) {
|
|
continue;
|
|
}
|
|
|
|
if ($this->type == GRAPH_TYPE_STACKED) {
|
|
foreach ($values as $ci => &$value) {
|
|
if ($data['count'][$ci] == 0) {
|
|
continue;
|
|
}
|
|
$value += $shift_values[$ci];
|
|
}
|
|
unset($value);
|
|
}
|
|
|
|
$maxY = ($maxY === null) ? max($values) : max($maxY, max($values));
|
|
}
|
|
|
|
return $maxY;
|
|
}
|
|
|
|
protected function calcZero() {
|
|
foreach ($this->getVerticalScalesInUse() as $side) {
|
|
// Expression optimized to avoid overflow.
|
|
$this->unit2px[$side] = $this->m_maxY[$side] / $this->sizeY - $this->m_minY[$side] / $this->sizeY;
|
|
|
|
if ($this->unit2px[$side] == 0) {
|
|
$this->unit2px[$side] = 1;
|
|
}
|
|
|
|
if ($this->m_minY[$side] > 0) {
|
|
$this->zero[$side] = $this->sizeY + $this->shiftY;
|
|
if ($this->m_minY[$side] > $this->m_maxY[$side]) {
|
|
$this->oxy[$side] = $this->m_maxY[$side];
|
|
}
|
|
else {
|
|
$this->oxy[$side] = $this->m_minY[$side];
|
|
}
|
|
}
|
|
elseif ($this->m_maxY[$side] < 0) {
|
|
$this->zero[$side] = $this->shiftY;
|
|
if ($this->m_minY[$side] > $this->m_maxY[$side]) {
|
|
$this->oxy[$side] = $this->m_minY[$side];
|
|
}
|
|
else {
|
|
$this->oxy[$side] = $this->m_maxY[$side];
|
|
}
|
|
}
|
|
else {
|
|
$this->zero[$side] = $this->sizeY + $this->shiftY - abs($this->m_minY[$side] / $this->unit2px[$side]);
|
|
$this->oxy[$side] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw X and Y axis.
|
|
*/
|
|
private function drawXYAxis() {
|
|
$gbColor = $this->getColor($this->graphtheme['gridbordercolor'], 0);
|
|
|
|
if ($this->yaxis[GRAPH_YAXIS_SIDE_LEFT]) {
|
|
zbx_imageline(
|
|
$this->im,
|
|
$this->shiftXleft,
|
|
$this->shiftY - 5,
|
|
$this->shiftXleft,
|
|
$this->sizeY + $this->shiftY + 4,
|
|
$gbColor
|
|
);
|
|
|
|
$points = [
|
|
$this->shiftXleft - 3, $this->shiftY - 5,
|
|
$this->shiftXleft + 3, $this->shiftY - 5,
|
|
$this->shiftXleft, $this->shiftY - 10
|
|
];
|
|
if (PHP_VERSION_ID >= 80100) {
|
|
imagefilledpolygon($this->im, $points, $this->getColor('White'));
|
|
}
|
|
else {
|
|
imagefilledpolygon($this->im, $points, 3, $this->getColor('White'));
|
|
}
|
|
|
|
/* draw left axis triangle */
|
|
zbx_imageline($this->im, $this->shiftXleft - 3, $this->shiftY - 5, $this->shiftXleft + 3, $this->shiftY - 5,
|
|
$gbColor);
|
|
zbx_imagealine($this->im, $this->shiftXleft - 3, $this->shiftY - 5, $this->shiftXleft, $this->shiftY - 10,
|
|
$gbColor);
|
|
zbx_imagealine($this->im, $this->shiftXleft + 3, $this->shiftY - 5, $this->shiftXleft, $this->shiftY - 10,
|
|
$gbColor);
|
|
}
|
|
else {
|
|
dashedLine(
|
|
$this->im,
|
|
$this->shiftXleft,
|
|
$this->shiftY,
|
|
$this->shiftXleft,
|
|
$this->sizeY + $this->shiftY,
|
|
$this->getColor($this->graphtheme['gridcolor'], 0)
|
|
);
|
|
}
|
|
|
|
if ($this->yaxis[GRAPH_YAXIS_SIDE_RIGHT]) {
|
|
zbx_imageline(
|
|
$this->im,
|
|
$this->sizeX + $this->shiftXleft,
|
|
$this->shiftY - 5,
|
|
$this->sizeX + $this->shiftXleft,
|
|
$this->sizeY + $this->shiftY + 4,
|
|
$gbColor
|
|
);
|
|
|
|
$points = [
|
|
$this->sizeX + $this->shiftXleft - 3, $this->shiftY - 5,
|
|
$this->sizeX + $this->shiftXleft + 3, $this->shiftY - 5,
|
|
$this->sizeX + $this->shiftXleft, $this->shiftY - 10
|
|
];
|
|
if (PHP_VERSION_ID >= 80100) {
|
|
imagefilledpolygon($this->im, $points, $this->getColor('White'));
|
|
}
|
|
else {
|
|
imagefilledpolygon($this->im, $points, 3, $this->getColor('White'));
|
|
}
|
|
|
|
/* draw right axis triangle */
|
|
zbx_imageline($this->im, $this->sizeX + $this->shiftXleft - 3, $this->shiftY - 5,
|
|
$this->sizeX + $this->shiftXleft + 3, $this->shiftY - 5, $gbColor);
|
|
zbx_imagealine($this->im, $this->sizeX + $this->shiftXleft + 3, $this->shiftY - 5,
|
|
$this->sizeX + $this->shiftXleft, $this->shiftY - 10, $gbColor);
|
|
zbx_imagealine($this->im, $this->sizeX + $this->shiftXleft - 3, $this->shiftY - 5,
|
|
$this->sizeX + $this->shiftXleft, $this->shiftY - 10, $gbColor);
|
|
}
|
|
else {
|
|
dashedLine(
|
|
$this->im,
|
|
$this->sizeX + $this->shiftXleft,
|
|
$this->shiftY,
|
|
$this->sizeX + $this->shiftXleft,
|
|
$this->sizeY + $this->shiftY,
|
|
$this->getColor($this->graphtheme['gridcolor'], 0)
|
|
);
|
|
}
|
|
|
|
zbx_imageline(
|
|
$this->im,
|
|
$this->shiftXleft - 3,
|
|
$this->sizeY + $this->shiftY + 1,
|
|
$this->sizeX + $this->shiftXleft + 5,
|
|
$this->sizeY + $this->shiftY + 1,
|
|
$gbColor
|
|
);
|
|
|
|
$points = [
|
|
$this->sizeX + $this->shiftXleft + 5, $this->sizeY + $this->shiftY - 2,
|
|
$this->sizeX + $this->shiftXleft + 5, $this->sizeY + $this->shiftY + 4,
|
|
$this->sizeX + $this->shiftXleft + 10, $this->sizeY + $this->shiftY + 1
|
|
];
|
|
if (PHP_VERSION_ID >= 80100) {
|
|
imagefilledpolygon($this->im, $points, $this->getColor('White'));
|
|
}
|
|
else {
|
|
imagefilledpolygon($this->im, $points, 3, $this->getColor('White'));
|
|
}
|
|
|
|
/* draw X axis triangle */
|
|
zbx_imageline($this->im, $this->sizeX + $this->shiftXleft + 5, $this->sizeY + $this->shiftY - 2,
|
|
$this->sizeX + $this->shiftXleft + 5, $this->sizeY + $this->shiftY + 4, $gbColor);
|
|
zbx_imagealine($this->im, $this->sizeX + $this->shiftXleft + 5, $this->sizeY + $this->shiftY + 4,
|
|
$this->sizeX + $this->shiftXleft + 10, $this->sizeY + $this->shiftY + 1, $gbColor);
|
|
zbx_imagealine($this->im, $this->sizeX + $this->shiftXleft + 10, $this->sizeY + $this->shiftY + 1,
|
|
$this->sizeX + $this->shiftXleft + 5, $this->sizeY + $this->shiftY - 2, $gbColor);
|
|
}
|
|
|
|
private function drawTimeGrid() {
|
|
$time_format = (date('Y', $this->stime) != date('Y', $this->to_time))
|
|
? DATE_FORMAT
|
|
: DATE_TIME_FORMAT_SHORT;
|
|
|
|
// Draw start date (and time) label.
|
|
$this->drawStartEndTimePeriod($this->stime, $time_format, 0);
|
|
$this->drawDateTimeIntervals();
|
|
|
|
// Draw end date (and time) label.
|
|
$this->drawStartEndTimePeriod($this->to_time, $time_format, $this->sizeX);
|
|
}
|
|
|
|
/**
|
|
* Draw start or end date (and time) label.
|
|
*
|
|
* @param int $value Unix time.
|
|
* @param string $format Date time format.
|
|
* @param int $position Position on X axis.
|
|
*/
|
|
private function drawStartEndTimePeriod($value, $format, $position) {
|
|
$point = zbx_date2str(_($format), $value);
|
|
$element = imageTextSize(8, 90, $point);
|
|
imageText(
|
|
$this->im,
|
|
8,
|
|
90,
|
|
$this->shiftXleft + $position + round($element['width'] / 2),
|
|
$this->sizeY + $this->shiftY + $element['height'] + 6,
|
|
$this->getColor($this->graphtheme['highlightcolor'], 0),
|
|
$point
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Draw main period label in red color with 8px font size under X axis and a 2px dashed gray vertical line
|
|
* according to that label.
|
|
*
|
|
* @param string $value Readable timestamp.
|
|
* @param int $position Position on X axis.
|
|
*/
|
|
private function drawMainPeriod($value, $position) {
|
|
$dims = imageTextSize(8, 90, $value);
|
|
|
|
imageText(
|
|
$this->im,
|
|
8,
|
|
90,
|
|
$this->shiftXleft + $position + round($dims['width'] / 2),
|
|
$this->sizeY + $this->shiftY + $dims['height'] + 6,
|
|
$this->getColor($this->graphtheme['highlightcolor'], 0),
|
|
$value
|
|
);
|
|
|
|
dashedLine(
|
|
$this->im,
|
|
$this->shiftXleft + $position,
|
|
$this->shiftY,
|
|
$this->shiftXleft + $position,
|
|
$this->sizeY + $this->shiftY,
|
|
$this->getColor($this->graphtheme['maingridcolor'], 0)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Draw main period label in black color with 7px font size under X axis and a 1px dashed gray vertical line
|
|
* according to that label.
|
|
*
|
|
* @param string $value Readable timestamp.
|
|
* @param int $position Position on X axis.
|
|
*/
|
|
private function drawSubPeriod($value, $position) {
|
|
$element = imageTextSize(7, 90, $value);
|
|
|
|
imageText(
|
|
$this->im,
|
|
7,
|
|
90,
|
|
$this->shiftXleft + $position + round($element['width'] / 2),
|
|
$this->sizeY + $this->shiftY + $element['height'] + 6,
|
|
$this->getColor($this->graphtheme['textcolor'], 0),
|
|
$value
|
|
);
|
|
|
|
dashedLine(
|
|
$this->im,
|
|
$this->shiftXleft + $position,
|
|
$this->shiftY,
|
|
$this->shiftXleft + $position,
|
|
$this->sizeY + $this->shiftY,
|
|
$this->getColor($this->graphtheme['gridcolor'], 0)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get best matching X-axis interval specification for the preferred sub-interval.
|
|
*
|
|
* @param int $pref_sub_interval Preferred sub-interval in seconds.
|
|
* @param float $min_sub_interval Preferred minimal sub-interval in seconds (float). Discarded if no matches.
|
|
* @param string $magnitude The highest non-permanent date component (Y, m, d, H, i, s).
|
|
*
|
|
* @return array
|
|
*/
|
|
private function getOptimalDateTimeIntervalSpec(int $pref_sub_interval, float $min_sub_interval,
|
|
string $magnitude): array {
|
|
// Possible X-axis main and sub-intervals.
|
|
$intervals = [
|
|
'PT1M' => ['PT1S', 'PT5S', 'PT10S', 'PT30S'],
|
|
'PT1H' => ['PT1M', 'PT2M', 'PT5M', 'PT15M', 'PT30M'],
|
|
'P1D' => ['PT1H', 'PT3H', 'PT6H', 'PT12H'],
|
|
'P1W' => ['P1D'],
|
|
'P1M' => ['P3D', 'P1W', 'P2W'],
|
|
'P1Y' => ['P1M', 'P3M', 'P4M', 'P6M'],
|
|
'P10Y' => ['P1Y', 'P5Y']
|
|
];
|
|
|
|
// Starting date and time aligners.
|
|
$aligners = [
|
|
'PT1M' => ['trim' => 'Y-m-d H:i:00', 'convert' => null],
|
|
'PT1H' => ['trim' => 'Y-m-d H:00:00', 'convert' => null],
|
|
'P1D' => ['trim' => 'Y-m-d 00:00:00', 'convert' => null],
|
|
'P1W' => ['trim' => 'Y-m-d 00:00:00', 'convert' => 'last Sunday 00:00:00'],
|
|
'P1M' => ['trim' => 'Y-m-01 00:00:00', 'convert' => null],
|
|
'P1Y' => ['trim' => 'Y-01-01 00:00:00', 'convert' => null],
|
|
'P10Y' => ['trim' => '1970-01-01 00:00:00', 'convert' => null]
|
|
];
|
|
|
|
// Date and time label formats.
|
|
$formats = [
|
|
'PT1M' => ['main' => TIME_FORMAT, 'sub' => _('H:i:s')],
|
|
'PT1H' => ['main' => TIME_FORMAT, 'sub' => TIME_FORMAT],
|
|
'P1D' => ['main' => $magnitude === 'Y' ? DATE_FORMAT : _('m-d'), 'sub' => TIME_FORMAT],
|
|
'P1W' => ['main' => $magnitude === 'Y' ? DATE_FORMAT : _('m-d'), 'sub' => _('m-d')],
|
|
'P1M' => ['main' => $magnitude === 'Y' ? DATE_FORMAT : _('m-d'), 'sub' => _('m-d')],
|
|
'P1Y' => ['main' => _x('Y', DATE_FORMAT_CONTEXT), 'sub' => _('M')],
|
|
'P10Y' => ['main' => _x('Y', DATE_FORMAT_CONTEXT), 'sub' => _x('Y', DATE_FORMAT_CONTEXT)]
|
|
];
|
|
|
|
$best_main_interval = null;
|
|
$best_sub_interval = null;
|
|
$best_sub_interval_ts = 0;
|
|
$best_sub_interval_prop = INF;
|
|
|
|
foreach ($intervals as $main_interval => $sub_intervals) {
|
|
foreach ($sub_intervals as $sub_interval) {
|
|
$interval_ts = (new DateTime('@0'))
|
|
->add(new DateInterval($sub_interval))
|
|
->getTimestamp();
|
|
|
|
$interval_prop = max($pref_sub_interval, $interval_ts) / min($pref_sub_interval, $interval_ts);
|
|
|
|
// Search for the best interval preferably but not necessarily matching the $min_sub_interval criteria.
|
|
$is_better = ($best_sub_interval_ts < $min_sub_interval)
|
|
? $interval_ts > $best_sub_interval_ts
|
|
: $interval_prop < $best_sub_interval_prop;
|
|
|
|
if ($is_better) {
|
|
$best_main_interval = $main_interval;
|
|
$best_sub_interval = $sub_interval;
|
|
$best_sub_interval_ts = $interval_ts;
|
|
$best_sub_interval_prop = $interval_prop;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'intervals' => [
|
|
'main' => new DateInterval($best_main_interval),
|
|
'sub' => new DateInterval($best_sub_interval)
|
|
],
|
|
'aligner' => $aligners[$best_main_interval],
|
|
'format' => $formats[$best_main_interval]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get date and time intervals in the given range for the X-axis.
|
|
*
|
|
* @param int $start Range start in seconds.
|
|
* @param int $end Range end in seconds.
|
|
* @param DateInterval $interval Date and time interval.
|
|
*
|
|
* @return array
|
|
*/
|
|
private function getDateTimeIntervals(int $start, int $end, DateInterval $interval): array {
|
|
$intervals = [];
|
|
|
|
$interval_ts = (new DateTime('@0'))
|
|
->add($interval)
|
|
->getTimestamp();
|
|
|
|
// Manage time transitions natively.
|
|
if ($interval_ts >= SEC_PER_DAY) {
|
|
$dt = (new DateTime())->setTimestamp($start);
|
|
|
|
while ($dt->getTimestamp() <= $end) {
|
|
$intervals[] = $dt->getTimestamp();
|
|
$dt->add($interval);
|
|
}
|
|
}
|
|
else {
|
|
$transitions = (new DateTime())->getTimezone()->getTransitions($start, $end);
|
|
if (!$transitions) {
|
|
$transitions = [];
|
|
}
|
|
|
|
$time = $start;
|
|
$transition = 1;
|
|
|
|
while ($time <= $end) {
|
|
$correct_before = 0;
|
|
$correct_after = 0;
|
|
|
|
while ($transition < count($transitions) && $time >= $transitions[$transition]['ts']) {
|
|
$offset_diff = $transitions[$transition]['offset'] - $transitions[$transition - 1]['offset'];
|
|
|
|
if ($interval_ts > abs($offset_diff)) {
|
|
if ($transitions[$transition]['isdst']) {
|
|
if ($time - $transitions[$transition]['ts'] >= $offset_diff) {
|
|
$correct_before -= $offset_diff;
|
|
}
|
|
else {
|
|
$correct_after -= $offset_diff;
|
|
}
|
|
}
|
|
else {
|
|
$correct_before -= $offset_diff;
|
|
}
|
|
}
|
|
|
|
$transition++;
|
|
}
|
|
|
|
$time += $correct_before;
|
|
$intervals[] = $time;
|
|
$time += $correct_after + $interval_ts;
|
|
}
|
|
}
|
|
|
|
return $intervals;
|
|
}
|
|
|
|
/**
|
|
* Draw date and time intervals under the X axis.
|
|
*/
|
|
private function drawDateTimeIntervals() {
|
|
// Calculate standard label width in time units.
|
|
$label_size = imageTextSize(7, 90, 'WWW')['width'] * $this->period / $this->sizeX * 2;
|
|
|
|
$preferred_sub_interval = (int) ($this->period * $this->cell_width / $this->sizeX) ?: 1;
|
|
|
|
foreach (['Y', 'm', 'd', 'H', 'i', 's'] as $magnitude) {
|
|
if (date($magnitude, $this->stime) !== date($magnitude, $this->stime + $this->period)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
$optimal = $this->getOptimalDateTimeIntervalSpec($preferred_sub_interval, $label_size, $magnitude);
|
|
|
|
// Align starting date and time with the interval.
|
|
$start = strtotime(date($optimal['aligner']['trim'], $this->stime));
|
|
if ($optimal['aligner']['convert'] !== null) {
|
|
$start = strtotime($optimal['aligner']['convert'], $start);
|
|
}
|
|
|
|
$end = $this->stime + $this->period;
|
|
|
|
// Draw main intervals.
|
|
$mains = [];
|
|
|
|
foreach ($this->getDateTimeIntervals($start, $end, $optimal['intervals']['main']) as $time) {
|
|
$pos = $time - $this->stime;
|
|
|
|
if ($pos >= $label_size && $pos <= $this->period - $label_size) {
|
|
$this->drawMainPeriod(date($optimal['format']['main'], $time), $pos * $this->sizeX / $this->period);
|
|
}
|
|
|
|
$mains[] = $time;
|
|
}
|
|
|
|
$mains[] = $end;
|
|
|
|
// Draw sub-intervals.
|
|
$sub_interval_ts = (new DateTime('@0'))
|
|
->add($optimal['intervals']['sub'])
|
|
->getTimestamp();
|
|
|
|
// Sub-intervals' margin from the main interval boundaries.
|
|
$main_margin = max($label_size, (int) ($sub_interval_ts * .75));
|
|
|
|
for ($i = 0, $i_max = count($mains) - 2; $i <= $i_max; $i++) {
|
|
$pos_min = $mains[$i] - $this->stime + $main_margin;
|
|
$pos_max = $mains[$i + 1] - $this->stime - $main_margin;
|
|
|
|
foreach ($this->getDateTimeIntervals($mains[$i], $mains[$i + 1], $optimal['intervals']['sub']) as $time) {
|
|
$pos = $time - $this->stime;
|
|
|
|
if ($pos >= max($pos_min, $label_size) && $pos <= min($pos_max, $this->period - $label_size)) {
|
|
$this->drawSubPeriod(date($optimal['format']['sub'], $time), $pos * $this->sizeX / $this->period);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function drawVerticalScale() {
|
|
foreach ($this->getVerticalScalesInUse() as $side_index => $side) {
|
|
$units = null;
|
|
$units_long = '';
|
|
|
|
foreach ($this->items as $item) {
|
|
if ($item['yaxisside'] == $side) {
|
|
if ($units === null) {
|
|
$units = $item['units'];
|
|
}
|
|
elseif ($item['units'] !== $units) {
|
|
$units = '';
|
|
}
|
|
|
|
if ($item['units_long'] !== '') {
|
|
$units_long = $item['units_long'];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($units_long !== '') {
|
|
$dims = imageTextSize(9, 90, $units_long);
|
|
|
|
$tmpY = $this->sizeY / 2 + $this->shiftY + $dims['height'] / 2;
|
|
if ($tmpY < $dims['height']) {
|
|
$tmpY = $dims['height'] + 6;
|
|
}
|
|
|
|
$tmpX = $side == GRAPH_YAXIS_SIDE_LEFT ? $dims['width'] + 8 : $this->fullSizeX - $dims['width'];
|
|
|
|
imageText(
|
|
$this->im,
|
|
9,
|
|
90,
|
|
$tmpX,
|
|
$tmpY,
|
|
$this->getColor($this->graphtheme['textcolor'], 0),
|
|
$units_long
|
|
);
|
|
}
|
|
|
|
$scale_values = calculateGraphScaleValues($this->m_minY[$side], $this->m_maxY[$side],
|
|
$this->ymin_type == GRAPH_YAXIS_TYPE_CALCULATED, $this->ymax_type == GRAPH_YAXIS_TYPE_CALCULATED,
|
|
$this->intervals[$side], $units, $this->is_binary[$side], $this->power[$side], 10
|
|
);
|
|
|
|
$line_color = $this->getColor($this->graphtheme['gridcolor'], 0);
|
|
|
|
foreach ($scale_values as ['relative_pos' => $relative_pos, 'value' => $value]) {
|
|
$pos_X = ($side == GRAPH_YAXIS_SIDE_LEFT)
|
|
? $this->shiftXleft - imageTextSize(8, 0, $value)['width'] - 9
|
|
: $this->sizeX + $this->shiftXleft + 12;
|
|
|
|
$pos_Y = $this->shiftY + $this->sizeY * (1 - $relative_pos);
|
|
|
|
if ($side_index == 0 && $relative_pos > 0) {
|
|
dashedLine($this->im, $this->shiftXleft, $pos_Y, $this->shiftXleft + $this->sizeX, $pos_Y,
|
|
$line_color
|
|
);
|
|
}
|
|
|
|
imageText(
|
|
$this->im,
|
|
8,
|
|
0,
|
|
$pos_X,
|
|
$pos_Y + 4,
|
|
$this->getColor($this->graphtheme['textcolor'], 0),
|
|
$value
|
|
);
|
|
}
|
|
|
|
if ($this->zero[$side] != $this->sizeY + $this->shiftY && $this->zero[$side] != $this->shiftY) {
|
|
zbx_imageline(
|
|
$this->im,
|
|
$this->shiftXleft,
|
|
$this->zero[$side],
|
|
$this->shiftXleft + $this->sizeX,
|
|
$this->zero[$side],
|
|
$this->getColor($side == GRAPH_YAXIS_SIDE_LEFT
|
|
? GRAPH_ZERO_LINE_COLOR_LEFT
|
|
: GRAPH_ZERO_LINE_COLOR_RIGHT
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function drawWorkPeriod() {
|
|
imagefilledrectangle($this->im,
|
|
$this->shiftXleft + 1,
|
|
$this->shiftY,
|
|
$this->sizeX + $this->shiftXleft - 1, // -2 border
|
|
$this->sizeY + $this->shiftY,
|
|
$this->getColor($this->graphtheme['graphcolor'], 0)
|
|
);
|
|
|
|
if (!$this->show_work_period) {
|
|
return;
|
|
}
|
|
|
|
if ($this->period > SEC_PER_MONTH * 3) {
|
|
return;
|
|
}
|
|
|
|
$config = [CSettingsHelper::WORK_PERIOD => CSettingsHelper::get(CSettingsHelper::WORK_PERIOD)];
|
|
$config = CMacrosResolverHelper::resolveTimeUnitMacros([$config], [CSettingsHelper::WORK_PERIOD])[0];
|
|
|
|
$periods = parse_period($config[CSettingsHelper::WORK_PERIOD]);
|
|
if (!$periods) {
|
|
return;
|
|
}
|
|
|
|
imagefilledrectangle(
|
|
$this->im,
|
|
$this->shiftXleft + 1,
|
|
$this->shiftY,
|
|
$this->sizeX + $this->shiftXleft - 1, // -1 border
|
|
$this->sizeY + $this->shiftY,
|
|
$this->getColor($this->graphtheme['nonworktimecolor'], 0)
|
|
);
|
|
|
|
$from = $this->from_time;
|
|
$max_time = $this->to_time;
|
|
|
|
$start = find_period_start($periods, $from);
|
|
$end = -1;
|
|
while ($start < $max_time && $start > 0) {
|
|
$end = find_period_end($periods, $start, $max_time);
|
|
|
|
$x1 = round((($start - $from) * $this->sizeX) / $this->period) + $this->shiftXleft;
|
|
$x2 = ceil((($end - $from) * $this->sizeX) / $this->period) + $this->shiftXleft;
|
|
|
|
// draw rectangle
|
|
imagefilledrectangle(
|
|
$this->im,
|
|
$x1,
|
|
$this->shiftY,
|
|
$x2 - 1, // -1 border
|
|
$this->sizeY + $this->shiftY,
|
|
$this->getColor($this->graphtheme['graphcolor'], 0)
|
|
);
|
|
|
|
$start = find_period_start($periods, $end);
|
|
}
|
|
}
|
|
|
|
protected function drawPercentile() {
|
|
if ($this->type != GRAPH_TYPE_NORMAL) {
|
|
return;
|
|
}
|
|
|
|
foreach ($this->percentile as $side => $percentile) {
|
|
if ($percentile['percent'] > 0 && $percentile['value']) {
|
|
$minY = $this->m_minY[$side];
|
|
$maxY = $this->m_maxY[$side];
|
|
|
|
$color = ($side == GRAPH_YAXIS_SIDE_LEFT)
|
|
? $this->graphtheme['leftpercentilecolor']
|
|
: $this->graphtheme['rightpercentilecolor'];
|
|
|
|
if ($maxY - $minY == INF) {
|
|
$y = $this->sizeY + $this->shiftY - CMathHelper::safeMul([$this->sizeY,
|
|
$percentile['value'] / 10 - $minY / 10, 1 / ($maxY / 10 - $minY / 10)]
|
|
);
|
|
}
|
|
else {
|
|
$y = $this->sizeY + $this->shiftY - CMathHelper::safeMul([$this->sizeY,
|
|
$percentile['value'] - $minY, 1 / ($maxY - $minY)]
|
|
);
|
|
}
|
|
|
|
zbx_imageline(
|
|
$this->im,
|
|
$this->shiftXleft,
|
|
$y,
|
|
$this->sizeX + $this->shiftXleft,
|
|
$y,
|
|
$this->getColor($color)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function drawTriggers() {
|
|
if (!$this->show_triggers) {
|
|
return;
|
|
}
|
|
|
|
$oppColor = $this->getColor(GRAPH_TRIGGER_LINE_OPPOSITE_COLOR);
|
|
|
|
foreach ($this->triggers as $trigger) {
|
|
$minY = $this->m_minY[$trigger['yaxisside']];
|
|
$maxY = $this->m_maxY[$trigger['yaxisside']];
|
|
|
|
if ($minY >= $trigger['val'] || $trigger['val'] >= $maxY) {
|
|
continue;
|
|
}
|
|
|
|
if ($maxY - $minY == INF) {
|
|
$y = $this->sizeY + $this->shiftY - CMathHelper::safeMul([$this->sizeY,
|
|
$trigger['val'] / 10 - $minY / 10, 1 / ($maxY / 10 - $minY / 10)
|
|
]);
|
|
}
|
|
else {
|
|
$y = $this->sizeY + $this->shiftY - CMathHelper::safeMul([$this->sizeY,
|
|
$trigger['val'] - $minY, 1 / ($maxY - $minY)
|
|
]);
|
|
}
|
|
|
|
$triggerColor = $this->getColor($trigger['color']);
|
|
$lineStyle = [$triggerColor, $triggerColor, $triggerColor, $triggerColor, $triggerColor, $oppColor, $oppColor, $oppColor];
|
|
|
|
dashedLine( $this->im, $this->shiftXleft, $y, $this->sizeX + $this->shiftXleft, $y, $lineStyle);
|
|
dashedLine( $this->im, $this->shiftXleft, $y + 1, $this->sizeX + $this->shiftXleft, $y + 1, $lineStyle);
|
|
}
|
|
}
|
|
|
|
private function getLastValue(array $data, int $calc_fnc) {
|
|
for ($i = $this->sizeX; $i >= 0; $i--) {
|
|
if ($data['count'][$i] != 0) {
|
|
switch ($calc_fnc) {
|
|
case CALC_FNC_MIN:
|
|
return $data['min'][$i];
|
|
case CALC_FNC_MAX:
|
|
return $data['max'][$i];
|
|
case CALC_FNC_ALL:
|
|
case CALC_FNC_AVG:
|
|
default:
|
|
return $data['avg'][$i];
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
protected function drawLegend() {
|
|
// if graph is small, we are not drawing legend
|
|
if (!$this->drawItemsLegend) {
|
|
return true;
|
|
}
|
|
|
|
$leftXShift = 15;
|
|
$units = [GRAPH_YAXIS_SIDE_LEFT => 0, GRAPH_YAXIS_SIDE_RIGHT => 0];
|
|
|
|
// draw item legend
|
|
$legend = new CImageTextTable($this->im, $leftXShift - 5, $this->sizeY + $this->shiftY + self::LEGEND_OFFSET_Y);
|
|
$legend->color = $this->getColor($this->graphtheme['textcolor'], 0);
|
|
$legend->rowheight = 14;
|
|
$legend->fontsize = 9;
|
|
|
|
// item legend table header
|
|
$row = [
|
|
['text' => '', 'marginRight' => 5],
|
|
['text' => ''],
|
|
['text' => ''],
|
|
['text' => _('last'), 'align' => 1, 'fontsize' => 9],
|
|
['text' => _('min'), 'align' => 1, 'fontsize' => 9],
|
|
['text' => _('avg'), 'align' => 1, 'fontsize' => 9],
|
|
['text' => _('max'), 'align' => 1, 'fontsize' => 9]
|
|
];
|
|
|
|
$legend->addRow($row);
|
|
$rowNum = $legend->getNumRows();
|
|
|
|
$i = ($this->type == GRAPH_TYPE_STACKED) ? $this->num - 1 : 0;
|
|
while ($i >= 0 && $i < $this->num) {
|
|
$color = $this->getColor($this->items[$i]['color'], GRAPH_STACKED_ALFA);
|
|
switch ($this->items[$i]['calc_fnc']) {
|
|
case CALC_FNC_MIN:
|
|
$fncRealName = _('min');
|
|
break;
|
|
case CALC_FNC_MAX:
|
|
$fncRealName = _('max');
|
|
break;
|
|
case CALC_FNC_ALL:
|
|
$fncRealName = _('all');
|
|
break;
|
|
case CALC_FNC_AVG:
|
|
default:
|
|
$fncRealName = _('avg');
|
|
}
|
|
|
|
// draw color square
|
|
$colorSquare = imagecreatetruecolor(11, 11);
|
|
imagefill($colorSquare, 0, 0, $this->getColor($this->graphtheme['backgroundcolor'], 0));
|
|
imagefilledrectangle($colorSquare, 0, 0, 10, 10, $color);
|
|
imagerectangle($colorSquare, 0, 0, 10, 10, $this->getColor('Black'));
|
|
|
|
// caption
|
|
$itemCaption = $this->itemsHost
|
|
? $this->items[$i]['name']
|
|
: $this->items[$i]['hostname'].NAME_DELIMITER.$this->items[$i]['name'];
|
|
|
|
// draw legend of an item with data
|
|
$data = array_key_exists($this->items[$i]['itemid'], $this->data)
|
|
? $this->data[$this->items[$i]['itemid']]
|
|
: null;
|
|
|
|
if ($data && $data['min']) {
|
|
if ($this->items[$i]['yaxisside'] == GRAPH_YAXIS_SIDE_LEFT) {
|
|
$units[GRAPH_YAXIS_SIDE_LEFT] = $this->items[$i]['units'];
|
|
}
|
|
else {
|
|
$units[GRAPH_YAXIS_SIDE_RIGHT] = $this->items[$i]['units'];
|
|
}
|
|
|
|
$legend->addCell($rowNum, ['image' => $colorSquare, 'marginRight' => 5]);
|
|
$legend->addCell($rowNum, ['text' => $itemCaption]);
|
|
$legend->addCell($rowNum, ['text' => '['.$fncRealName.']']);
|
|
$legend->addCell($rowNum, [
|
|
'text' => convertUnits([
|
|
'value' => $this->getLastValue($data, $this->items[$i]['calc_fnc']),
|
|
'units' => $this->items[$i]['units'],
|
|
'convert' => ITEM_CONVERT_NO_UNITS
|
|
]),
|
|
'align' => 2
|
|
]);
|
|
$legend->addCell($rowNum, [
|
|
'text' => convertUnits([
|
|
'value' => min($data['min']),
|
|
'units' => $this->items[$i]['units'],
|
|
'convert' => ITEM_CONVERT_NO_UNITS
|
|
]),
|
|
'align' => 2
|
|
]);
|
|
$legend->addCell($rowNum, [
|
|
'text' => convertUnits([
|
|
'value' => $data['avg_orig'],
|
|
'units' => $this->items[$i]['units'],
|
|
'convert' => ITEM_CONVERT_NO_UNITS
|
|
]),
|
|
'align' => 2
|
|
]);
|
|
$legend->addCell($rowNum, [
|
|
'text' => convertUnits([
|
|
'value' => max($data['max']),
|
|
'units' => $this->items[$i]['units'],
|
|
'convert' => ITEM_CONVERT_NO_UNITS
|
|
]),
|
|
'align' => 2
|
|
]);
|
|
}
|
|
// draw legend of an item without data
|
|
else {
|
|
$legend->addCell($rowNum, ['image' => $colorSquare, 'marginRight' => 5]);
|
|
$legend->addCell($rowNum, ['text' => $itemCaption]);
|
|
$legend->addCell($rowNum, ['text' => '['._('no data').']']);
|
|
}
|
|
|
|
$rowNum++;
|
|
|
|
// legends for stacked graphs are written in reverse order so that the order of items
|
|
// matches the order of lines on the graphs
|
|
if ($this->type == GRAPH_TYPE_STACKED) {
|
|
$i--;
|
|
}
|
|
else {
|
|
$i++;
|
|
}
|
|
}
|
|
|
|
$legend->draw();
|
|
|
|
// if graph is small, we are not drawing percent line and trigger legends
|
|
if (!$this->drawExLegend) {
|
|
return true;
|
|
}
|
|
|
|
$legend = new CImageTextTable(
|
|
$this->im,
|
|
$leftXShift + 10,
|
|
$this->sizeY + $this->shiftY + 14 * $rowNum + self::LEGEND_OFFSET_Y
|
|
);
|
|
$legend->color = $this->getColor($this->graphtheme['textcolor'], 0);
|
|
$legend->rowheight = 14;
|
|
$legend->fontsize = 9;
|
|
|
|
// draw percentile
|
|
if ($this->type == GRAPH_TYPE_NORMAL) {
|
|
foreach ($this->percentile as $side => $percentile) {
|
|
if ($percentile['percent'] > 0 && $this->yaxis[$side]) {
|
|
$percentile['percent'] = (float) $percentile['percent'];
|
|
$convertedUnit = $percentile['value']
|
|
? convertUnits([
|
|
'value' => $percentile['value'],
|
|
'units' => $units[$side]
|
|
])
|
|
: '-';
|
|
$side_str = ($side == GRAPH_YAXIS_SIDE_LEFT) ? _('left') : _('right');
|
|
$legend->addCell($rowNum, [
|
|
'text' => $percentile['percent'].'th percentile: '.$convertedUnit.' ('.$side_str.')',
|
|
ITEM_CONVERT_NO_UNITS
|
|
]);
|
|
$color = ($side == GRAPH_YAXIS_SIDE_LEFT)
|
|
? $this->graphtheme['leftpercentilecolor']
|
|
: $this->graphtheme['rightpercentilecolor'];
|
|
|
|
$points = [
|
|
$leftXShift + 5, $this->sizeY + $this->shiftY + 14 * $rowNum + self::LEGEND_OFFSET_Y,
|
|
$leftXShift - 5, $this->sizeY + $this->shiftY + 14 * $rowNum + self::LEGEND_OFFSET_Y,
|
|
$leftXShift, $this->sizeY + $this->shiftY + 14 * $rowNum + self::LEGEND_OFFSET_Y - 10
|
|
];
|
|
if (PHP_VERSION_ID >= 80100) {
|
|
imagefilledpolygon($this->im, $points, $this->getColor($color));
|
|
}
|
|
else {
|
|
imagefilledpolygon($this->im, $points, 3, $this->getColor($color));
|
|
}
|
|
|
|
$points = [
|
|
$leftXShift + 5, $this->sizeY + $this->shiftY + 14 * $rowNum + self::LEGEND_OFFSET_Y,
|
|
$leftXShift - 5, $this->sizeY + $this->shiftY + 14 * $rowNum + self::LEGEND_OFFSET_Y,
|
|
$leftXShift, $this->sizeY + $this->shiftY + 14 * $rowNum + self::LEGEND_OFFSET_Y - 10
|
|
];
|
|
if (PHP_VERSION_ID >= 80100) {
|
|
imagepolygon($this->im, $points, $this->getColor('Black No Alpha'));
|
|
}
|
|
else {
|
|
imagepolygon($this->im, $points, 3, $this->getColor('Black No Alpha'));
|
|
}
|
|
|
|
$rowNum++;
|
|
}
|
|
}
|
|
}
|
|
|
|
$legend->draw();
|
|
|
|
$legend = new CImageTextTable(
|
|
$this->im,
|
|
$leftXShift + 10,
|
|
$this->sizeY + $this->shiftY + 14 * $rowNum + self::LEGEND_OFFSET_Y + 5
|
|
);
|
|
$legend->color = $this->getColor($this->graphtheme['textcolor'], 0);
|
|
$legend->rowheight = 14;
|
|
$legend->fontsize = 9;
|
|
|
|
// draw triggers
|
|
foreach ($this->triggers as $trigger) {
|
|
imagefilledellipse(
|
|
$this->im,
|
|
$leftXShift,
|
|
$this->sizeY + $this->shiftY + 14 * $rowNum + self::LEGEND_OFFSET_Y,
|
|
10,
|
|
10,
|
|
$this->getColor($trigger['color'])
|
|
);
|
|
|
|
imageellipse(
|
|
$this->im,
|
|
$leftXShift,
|
|
$this->sizeY + $this->shiftY + 14 * $rowNum + self::LEGEND_OFFSET_Y,
|
|
10,
|
|
10,
|
|
$this->getColor('Black No Alpha')
|
|
);
|
|
|
|
$legend->addRow([
|
|
['text' => $trigger['description']],
|
|
['text' => $trigger['constant']]
|
|
]);
|
|
$rowNum++;
|
|
}
|
|
|
|
$legend->draw();
|
|
}
|
|
|
|
protected function limitToBounds(&$value1, &$value2, $min, $max, $drawtype) {
|
|
// fixes graph out of bounds problem
|
|
if ((($value1 > ($max + $min)) && ($value2 > ($max + $min))) || ($value1 < $min && $value2 < $min)) {
|
|
if (!in_array($drawtype, [GRAPH_ITEM_DRAWTYPE_FILLED_REGION, GRAPH_ITEM_DRAWTYPE_GRADIENT_LINE])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$y_first = $value1 > ($max + $min) || $value1 < $min;
|
|
$y_second = $value2 > ($max + $min) || $value2 < $min;
|
|
|
|
if ($y_first) {
|
|
$value1 = ($value1 > ($max + $min)) ? $max + $min : $min;
|
|
}
|
|
|
|
if ($y_second) {
|
|
$value2 = ($value2 > ($max + $min)) ? $max + $min : $min;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function drawElement(&$data, $from, $to, $drawtype, $max_color, $avg_color, $min_color, $minmax_color, $calc_fnc, $yaxisside) {
|
|
if (!isset($data['max'][$from]) || !isset($data['max'][$to])) {
|
|
return;
|
|
}
|
|
|
|
$oxy = $this->oxy[$yaxisside];
|
|
$zero = $this->zero[$yaxisside];
|
|
$unit2px = $this->unit2px[$yaxisside];
|
|
|
|
$shift_min_from = $shift_min_to = 0;
|
|
$shift_max_from = $shift_max_to = 0;
|
|
$shift_avg_from = $shift_avg_to = 0;
|
|
|
|
if (isset($data['shift_min'][$from])) {
|
|
$shift_min_from = $data['shift_min'][$from];
|
|
}
|
|
if (isset($data['shift_min'][$to])) {
|
|
$shift_min_to = $data['shift_min'][$to];
|
|
}
|
|
|
|
if (isset($data['shift_max'][$from])) {
|
|
$shift_max_from = $data['shift_max'][$from];
|
|
}
|
|
if (isset($data['shift_max'][$to])) {
|
|
$shift_max_to = $data['shift_max'][$to];
|
|
}
|
|
|
|
if (isset($data['shift_avg'][$from])) {
|
|
$shift_avg_from = $data['shift_avg'][$from];
|
|
}
|
|
if (isset($data['shift_avg'][$to])) {
|
|
$shift_avg_to = $data['shift_avg'][$to];
|
|
}
|
|
|
|
$min_from = $data['min'][$from] + $shift_min_from;
|
|
$min_to = $data['min'][$to] + $shift_min_to;
|
|
|
|
$max_from = $data['max'][$from] + $shift_max_from;
|
|
$max_to = $data['max'][$to] + $shift_max_to;
|
|
|
|
$avg_from = $data['avg'][$from] + $shift_avg_from;
|
|
$avg_to = $data['avg'][$to] + $shift_avg_to;
|
|
|
|
$x1 = $from + $this->shiftXleft;
|
|
$x2 = $to + $this->shiftXleft;
|
|
|
|
$y1min = (int) round($zero - ($min_from - $oxy) / $unit2px);
|
|
$y2min = (int) round($zero - ($min_to - $oxy) / $unit2px);
|
|
|
|
$y1max = (int) round($zero - ($max_from - $oxy) / $unit2px);
|
|
$y2max = (int) round($zero - ($max_to - $oxy) / $unit2px);
|
|
|
|
$y1avg = (int) round($zero - ($avg_from - $oxy) / $unit2px);
|
|
$y2avg = (int) round($zero - ($avg_to - $oxy) / $unit2px);
|
|
|
|
switch ($calc_fnc) {
|
|
case CALC_FNC_MAX:
|
|
$y1 = $y1max;
|
|
$y2 = $y2max;
|
|
$shift_from = $shift_max_from;
|
|
$shift_to = $shift_max_to;
|
|
break;
|
|
|
|
case CALC_FNC_MIN:
|
|
$y1 = $y1min;
|
|
$y2 = $y2min;
|
|
$shift_from = $shift_min_from;
|
|
$shift_to = $shift_min_to;
|
|
break;
|
|
|
|
case CALC_FNC_ALL:
|
|
// max
|
|
$y1x = (($y1max > ($this->sizeY + $this->shiftY)) || $y1max < $this->shiftY);
|
|
$y2x = (($y2max > ($this->sizeY + $this->shiftY)) || $y2max < $this->shiftY);
|
|
|
|
if ($y1x) {
|
|
$y1max = ($y1max > ($this->sizeY + $this->shiftY)) ? $this->sizeY + $this->shiftY : $this->shiftY;
|
|
}
|
|
if ($y2x) {
|
|
$y2max = ($y2max > ($this->sizeY + $this->shiftY)) ? $this->sizeY + $this->shiftY : $this->shiftY;
|
|
}
|
|
|
|
// min
|
|
$y1n = (($y1min > ($this->sizeY + $this->shiftY)) || $y1min < $this->shiftY);
|
|
$y2n = (($y2min > ($this->sizeY + $this->shiftY)) || $y2min < $this->shiftY);
|
|
|
|
if ($y1n) {
|
|
$y1min = ($y1min > ($this->sizeY + $this->shiftY)) ? $this->sizeY + $this->shiftY : $this->shiftY;
|
|
}
|
|
if ($y2n) {
|
|
$y2min = ($y2min > ($this->sizeY + $this->shiftY)) ? $this->sizeY + $this->shiftY : $this->shiftY;
|
|
}
|
|
|
|
$a[0] = $x1;
|
|
$a[1] = $y1max;
|
|
$a[2] = $x1;
|
|
$a[3] = $y1min;
|
|
$a[4] = $x2;
|
|
$a[5] = $y2min;
|
|
$a[6] = $x2;
|
|
$a[7] = $y2max;
|
|
|
|
// don't use break, avg must be drawn in this statement
|
|
case CALC_FNC_AVG:
|
|
|
|
// don't use break, avg must be drawn in this statement
|
|
default:
|
|
$y1 = $y1avg;
|
|
$y2 = $y2avg;
|
|
$shift_from = $shift_avg_from;
|
|
$shift_to = $shift_avg_to;
|
|
}
|
|
|
|
$shift_from -= ($shift_from != 0) ? $oxy : 0;
|
|
$shift_to -= ($shift_to != 0) ? $oxy : 0;
|
|
|
|
$y1_shift = $zero - $shift_from / $unit2px;
|
|
$y2_shift = $zero - $shift_to / $unit2px;
|
|
|
|
if (!$this->limitToBounds($y1, $y2, $this->shiftY, $this->sizeY, $drawtype)) {
|
|
return true;
|
|
}
|
|
if (!$this->limitToBounds($y1_shift, $y2_shift, $this->shiftY, $this->sizeY, $drawtype)) {
|
|
return true;
|
|
}
|
|
|
|
// draw main line
|
|
switch ($drawtype) {
|
|
case GRAPH_ITEM_DRAWTYPE_LINE:
|
|
case GRAPH_ITEM_DRAWTYPE_BOLD_LINE:
|
|
$style = $drawtype == GRAPH_ITEM_DRAWTYPE_BOLD_LINE ? LINE_TYPE_BOLD : LINE_TYPE_NORMAL;
|
|
|
|
if ($calc_fnc == CALC_FNC_ALL) {
|
|
if (PHP_VERSION_ID >= 80100) {
|
|
imagefilledpolygon($this->im, $a, $avg_color);
|
|
}
|
|
else {
|
|
imagefilledpolygon($this->im, $a, 4, $minmax_color);
|
|
}
|
|
|
|
if (!$y1x || !$y2x) {
|
|
zbx_imagealine($this->im, $x1, $y1max, $x2, $y2max, $max_color, $style);
|
|
}
|
|
if (!$y1n || !$y2n) {
|
|
zbx_imagealine($this->im, $x1, $y1min, $x2, $y2min, $min_color, $style);
|
|
}
|
|
}
|
|
|
|
zbx_imagealine($this->im, $x1, $y1, $x2, $y2, $avg_color, $style);
|
|
break;
|
|
|
|
case GRAPH_ITEM_DRAWTYPE_DOT:
|
|
imagefilledrectangle($this->im, $x1 - 1, $y1 - 1, $x1, $y1, $avg_color);
|
|
break;
|
|
|
|
case GRAPH_ITEM_DRAWTYPE_BOLD_DOT:
|
|
imagefilledrectangle($this->im, $x2 - 1, $y2 - 1, $x2 + 1, $y2 + 1, $avg_color);
|
|
break;
|
|
|
|
case GRAPH_ITEM_DRAWTYPE_DASHED_LINE:
|
|
imagesetstyle($this->im, [$avg_color, $avg_color, IMG_COLOR_TRANSPARENT, IMG_COLOR_TRANSPARENT]);
|
|
zbx_imageline($this->im, $x1, $y1, $x2, $y2, IMG_COLOR_STYLED);
|
|
break;
|
|
|
|
case GRAPH_ITEM_DRAWTYPE_GRADIENT_LINE:
|
|
case GRAPH_ITEM_DRAWTYPE_FILLED_REGION:
|
|
/*
|
|
* Graphs should be at least 50px in height in order to visually see the gradient. Though 51px would not
|
|
* make any difference either. If graph height is too small to see gradient, use standard solid color
|
|
* filling function instead.
|
|
*/
|
|
if ($drawtype == GRAPH_ITEM_DRAWTYPE_FILLED_REGION
|
|
|| ($drawtype == GRAPH_ITEM_DRAWTYPE_GRADIENT_LINE && $this->sizeY <= 50)) {
|
|
$a[0] = $x1;
|
|
$a[1] = $y1;
|
|
$a[2] = $x1;
|
|
$a[3] = $y1_shift;
|
|
$a[4] = $x2;
|
|
$a[5] = $y2_shift;
|
|
$a[6] = $x2;
|
|
$a[7] = $y2;
|
|
|
|
if (PHP_VERSION_ID >= 80100) {
|
|
imagefilledpolygon($this->im, $a, $avg_color);
|
|
}
|
|
else {
|
|
imagefilledpolygon($this->im, $a, 4, $avg_color);
|
|
}
|
|
}
|
|
else {
|
|
imageLine($this->im, $x1, $y1, $x2, $y2, $avg_color); // draw the initial line
|
|
imageLine($this->im, $x1, $y1 - 1, $x2, $y2 - 1, $avg_color);
|
|
|
|
$bitmask = 255;
|
|
$blue = $avg_color & $bitmask;
|
|
|
|
// $blue_diff = 255 - $blue;
|
|
$bitmask = $bitmask << 8;
|
|
$green = ($avg_color & $bitmask) >> 8;
|
|
|
|
// $green_diff = 255 - $green;
|
|
$bitmask = $bitmask << 8;
|
|
$red = ($avg_color & $bitmask) >> 16;
|
|
// $red_diff = 255 - $red;
|
|
|
|
// note: though gradients on the chart looks ok, the formula used is completely incorrect
|
|
// if you plan to fix something here, it would be better to start from scratch
|
|
$maxAlpha = 110;
|
|
$startAlpha = 50;
|
|
|
|
$alphaRatio = $maxAlpha / ($this->sizeY - $startAlpha);
|
|
|
|
$diffX = $x1 - $x2;
|
|
for ($i = 0; $i <= $diffX; $i++) {
|
|
$Yincr = ($diffX > 0) ? (abs($y2 - $y1) / $diffX) : 0;
|
|
|
|
$gy = ($y1 > $y2) ? ($y2 + $Yincr * $i) : ($y2 - $Yincr * $i);
|
|
$steps = $this->sizeY + $this->shiftY - $gy + 1;
|
|
|
|
for ($j = 0; $j < $steps; $j++) {
|
|
$alpha = ($gy + $j) < ($this->shiftY + $startAlpha)
|
|
? 0
|
|
: 127 - (int) abs(127 - ($alphaRatio * ($gy + $j - $this->shiftY - $startAlpha)));
|
|
|
|
$color = imagecolorexactalpha($this->im, $red, $green, $blue, $alpha);
|
|
imagesetpixel($this->im, $x2 + $i, (int) $gy + $j, $color);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private function calcVerticalScale() {
|
|
$calc_min = $this->ymin_type == GRAPH_YAXIS_TYPE_CALCULATED;
|
|
$calc_max = $this->ymax_type == GRAPH_YAXIS_TYPE_CALCULATED;
|
|
|
|
$rows_min = (int) max(1, floor($this->sizeY / $this->cell_height_min / 1.5));
|
|
$rows_max = (int) max(1, floor($this->sizeY / $this->cell_height_min));
|
|
|
|
foreach ($this->getVerticalScalesInUse() as $side) {
|
|
$min = $this->calculateMinY($side);
|
|
$max = $this->calculateMaxY($side);
|
|
|
|
if ($min === null) {
|
|
$min = 0;
|
|
}
|
|
|
|
if ($max === null) {
|
|
$max = 1;
|
|
}
|
|
|
|
if ($this->type == GRAPH_TYPE_STACKED && $this->ymin_type == GRAPH_YAXIS_TYPE_CALCULATED) {
|
|
$min = min(0, $min);
|
|
}
|
|
|
|
$is_binary = false;
|
|
$calc_power = false;
|
|
|
|
foreach ($this->items as $item) {
|
|
if ($item['yaxisside'] == $side) {
|
|
$is_binary = $is_binary || in_array($item['units'], ['B', 'Bps']);
|
|
$calc_power = $calc_power || $item['units'] === '' || $item['units'][0] !== '!';
|
|
}
|
|
}
|
|
|
|
$result = calculateGraphScaleExtremes($min, $max, $is_binary, $calc_power, $calc_min, $calc_max, $rows_min,
|
|
$rows_max
|
|
);
|
|
|
|
if ($result === null) {
|
|
show_error_message(_('Y axis MAX value must be greater than Y axis MIN value.'));
|
|
exit;
|
|
}
|
|
|
|
[
|
|
'min' => $this->m_minY[$side],
|
|
'max' => $this->m_maxY[$side],
|
|
'interval' => $this->intervals[$side],
|
|
'power' => $this->power[$side]
|
|
] = $result;
|
|
|
|
$this->is_binary[$side] = $is_binary;
|
|
|
|
if ($calc_min && $calc_max) {
|
|
$rows_min = $rows_max = $result['rows'];
|
|
}
|
|
}
|
|
}
|
|
|
|
private function calcDimensions() {
|
|
$this->shiftXleft = $this->yaxis[GRAPH_YAXIS_SIDE_LEFT] ? 85 : 30;
|
|
$this->shiftXright = $this->yaxis[GRAPH_YAXIS_SIDE_RIGHT] ? 85 : 30;
|
|
|
|
// Calculate graph summary padding for both axes.
|
|
$x_offsets = $this->shiftXleft + $this->shiftXright + 1;
|
|
$y_offsets = $this->shiftY + self::LEGEND_OFFSET_Y;
|
|
|
|
if (!$this->with_vertical_padding) {
|
|
$y_offsets -= ($this->show_triggers && count($this->triggers) > 0)
|
|
? static::DEFAULT_TOP_BOTTOM_PADDING / 2
|
|
: static::DEFAULT_TOP_BOTTOM_PADDING;
|
|
}
|
|
|
|
// Actual outer dimensions, regardless $this->outer setting.
|
|
$this->fullSizeX = $this->sizeX;
|
|
$this->fullSizeY = $this->sizeY;
|
|
|
|
if ($this->drawLegend) {
|
|
// Reserve N+1 item rows, last row is used as padding for legend.
|
|
$h_legend_items = 14 * ($this->num + 1);
|
|
$h_legend_triggers = 14 * count($this->triggers);
|
|
$h_legend_percentile = 0;
|
|
|
|
foreach ($this->percentile as $side => $percentile) {
|
|
if ($percentile['percent'] > 0 && $this->yaxis[$side]) {
|
|
$h_legend_percentile += 14;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Normalize dimensions according to which dimensions were initially provided.
|
|
if ($this->outer) {
|
|
// Adjust inner graph dimensions.
|
|
$this->sizeX = $this->fullSizeX - $x_offsets;
|
|
$this->sizeY = $this->fullSizeY - $y_offsets;
|
|
|
|
if ($this->drawLegend) {
|
|
if ($this->sizeY - $h_legend_items >= self::GRAPH_HEIGHT_MIN) {
|
|
$this->sizeY -= $h_legend_items;
|
|
$this->drawItemsLegend = true;
|
|
|
|
if ($this->sizeY - ($h_legend_triggers + $h_legend_percentile) >= self::GRAPH_HEIGHT_MIN) {
|
|
$this->sizeY -= $h_legend_triggers + $h_legend_percentile;
|
|
$this->drawExLegend = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Adjust target image dimensions.
|
|
$this->fullSizeX += $x_offsets;
|
|
$this->fullSizeY += $y_offsets;
|
|
|
|
if ($this->drawLegend) {
|
|
$this->fullSizeY += $h_legend_items;
|
|
$this->drawItemsLegend = true;
|
|
|
|
if ($this->sizeY >= ZBX_GRAPH_LEGEND_HEIGHT) {
|
|
$this->fullSizeY += $h_legend_triggers + $h_legend_percentile;
|
|
$this->drawExLegend = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function getMinDimensions() {
|
|
$min_dimensions = [
|
|
'width' => self::GRAPH_WIDTH_MIN,
|
|
'height' => self::GRAPH_HEIGHT_MIN
|
|
];
|
|
|
|
if ($this->outer) {
|
|
$min_dimensions['width'] += $this->yaxis[GRAPH_YAXIS_SIDE_LEFT] ? 85 : 30;
|
|
$min_dimensions['width'] += $this->yaxis[GRAPH_YAXIS_SIDE_RIGHT] ? 85 : 30;
|
|
$min_dimensions['width']++;
|
|
|
|
$min_dimensions['height'] += $this->shiftY + self::LEGEND_OFFSET_Y;
|
|
}
|
|
|
|
return $min_dimensions;
|
|
}
|
|
|
|
/**
|
|
* Expands graph item objects data: macros in item name, time units, dependent item
|
|
*/
|
|
private function expandItems() {
|
|
$items_cache = zbx_toHash($this->items, 'itemid');
|
|
$items = $this->items;
|
|
|
|
do {
|
|
$master_itemids = [];
|
|
|
|
foreach ($items as $item) {
|
|
if ($item['type'] == ITEM_TYPE_DEPENDENT && !array_key_exists($item['master_itemid'], $items_cache)) {
|
|
$master_itemids[$item['master_itemid']] = true;
|
|
}
|
|
$items_cache[$item['itemid']] = $item;
|
|
}
|
|
$master_itemids = array_keys($master_itemids);
|
|
|
|
$items = API::Item()->get([
|
|
'output' => ['itemid', 'type', 'master_itemid', 'delay'],
|
|
'itemids' => $master_itemids,
|
|
'filter' => [
|
|
'flags' => [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_PROTOTYPE, ZBX_FLAG_DISCOVERY_CREATED]
|
|
]
|
|
]);
|
|
} while ($items);
|
|
|
|
$update_interval_parser = new CUpdateIntervalParser();
|
|
|
|
foreach ($this->items as &$graph_item) {
|
|
if ($graph_item['type'] == ITEM_TYPE_DEPENDENT) {
|
|
$master_item = $graph_item;
|
|
|
|
while ($master_item && $master_item['type'] == ITEM_TYPE_DEPENDENT) {
|
|
$master_item = $items_cache[$master_item['master_itemid']];
|
|
}
|
|
$graph_item['type'] = $master_item['type'];
|
|
$graph_item['delay'] = $master_item['delay'];
|
|
}
|
|
|
|
$graph_item = CMacrosResolverHelper::resolveTimeUnitMacros([$graph_item], ['delay'])[0];
|
|
|
|
$update_interval_parser->parse($graph_item['delay']);
|
|
$graph_item['delay'] = getItemDelay($update_interval_parser->getDelay(),
|
|
$update_interval_parser->getIntervals(ITEM_DELAY_FLEXIBLE)
|
|
);
|
|
|
|
$graph_item['has_scheduling_intervals']
|
|
= (bool) $update_interval_parser->getIntervals(ITEM_DELAY_SCHEDULING);
|
|
|
|
if (strpos($graph_item['units'], ',') === false) {
|
|
$graph_item['units_long'] = '';
|
|
}
|
|
else {
|
|
list($graph_item['units'], $graph_item['units_long']) = explode(',', $graph_item['units'], 2);
|
|
}
|
|
}
|
|
unset($graph_item);
|
|
}
|
|
|
|
/**
|
|
* Calculate graph dimensions and draw 1x1 pixel image placeholder.
|
|
*/
|
|
public function drawDimensions() {
|
|
set_image_header();
|
|
|
|
$this->calculateTopPadding();
|
|
$this->selectTriggers();
|
|
$this->calcDimensions();
|
|
|
|
$this->im = imagecreatetruecolor(1, 1);
|
|
|
|
$this->initColors();
|
|
|
|
imageOut($this->im);
|
|
}
|
|
|
|
public function draw() {
|
|
$debug_mode = CWebUser::getDebugMode();
|
|
if ($debug_mode) {
|
|
$start_time = microtime(true);
|
|
}
|
|
|
|
set_image_header();
|
|
|
|
$this->calculateTopPadding();
|
|
|
|
$this->expandItems();
|
|
$this->selectTriggers();
|
|
$this->calcDimensions();
|
|
|
|
$this->selectData();
|
|
if (hasErrorMessages()) {
|
|
show_messages();
|
|
}
|
|
|
|
$this->calcVerticalScale();
|
|
$this->calcPercentile();
|
|
$this->calcZero();
|
|
|
|
$this->im = imagecreatetruecolor($this->fullSizeX, $this->fullSizeY);
|
|
|
|
$this->initColors();
|
|
$this->drawRectangle();
|
|
$this->drawHeader();
|
|
$this->drawWorkPeriod();
|
|
$this->drawTimeGrid();
|
|
$this->drawVerticalScale();
|
|
$this->drawXYAxis();
|
|
|
|
// Correct item 'delay' field value when graph data requested for trends.
|
|
foreach ($this->items as &$item) {
|
|
if ($item['source'] === 'trends' && (!$item['has_scheduling_intervals'] || $item['delay'] != 0)) {
|
|
$item['delay'] = max($item['delay'], ZBX_MAX_TREND_DIFF);
|
|
}
|
|
}
|
|
unset($item);
|
|
|
|
// for each metric
|
|
for ($item = 0; $item < $this->num; $item++) {
|
|
if (!array_key_exists($this->items[$item]['itemid'], $this->data)) {
|
|
continue;
|
|
}
|
|
|
|
$data = &$this->data[$this->items[$item]['itemid']];
|
|
|
|
$drawtype = $this->items[$item]['drawtype'];
|
|
$max_color = $this->getColor('ValueMax', GRAPH_STACKED_ALFA);
|
|
$avg_color = $this->getColor($this->items[$item]['color'], GRAPH_STACKED_ALFA);
|
|
$min_color = $this->getColor('ValueMin', GRAPH_STACKED_ALFA);
|
|
$minmax_color = $this->getColor('ValueMinMax', GRAPH_STACKED_ALFA);
|
|
|
|
$calc_fnc = $this->items[$item]['calc_fnc'];
|
|
|
|
// for each X
|
|
$prevDraw = true;
|
|
for ($i = 1, $j = 0; $i <= $this->sizeX; $i++) { // new point
|
|
if ($data['count'][$i] == 0 && $i != $this->sizeX) {
|
|
continue;
|
|
}
|
|
|
|
$delay = $this->items[$item]['delay'];
|
|
|
|
if ($this->items[$item]['type'] == ITEM_TYPE_TRAPPER
|
|
|| ($this->items[$item]['type'] == ITEM_TYPE_ZABBIX_ACTIVE
|
|
&& preg_match('/^(event)?log(rt)?\[/', $this->items[$item]['key_']))
|
|
|| ($this->items[$item]['has_scheduling_intervals'] && $delay == 0)) {
|
|
$draw = true;
|
|
}
|
|
else {
|
|
if (!$data['clock']) {
|
|
$diff = 0;
|
|
}
|
|
else {
|
|
$diff = abs($data['clock'][$i] - $data['clock'][$j]);
|
|
}
|
|
|
|
$cell = ($this->to_time - $this->from_time) / $this->sizeX;
|
|
|
|
if ($cell > $delay) {
|
|
$draw = ($diff < (ZBX_GRAPH_MAX_SKIP_CELL * $cell));
|
|
}
|
|
else {
|
|
$draw = ($diff < (ZBX_GRAPH_MAX_SKIP_DELAY * $delay));
|
|
}
|
|
}
|
|
|
|
if (!$draw && !$prevDraw) {
|
|
$draw = true;
|
|
$valueDrawType = GRAPH_ITEM_DRAWTYPE_BOLD_DOT;
|
|
}
|
|
else {
|
|
$valueDrawType = $drawtype;
|
|
$prevDraw = $draw;
|
|
}
|
|
|
|
if ($draw) {
|
|
$this->drawElement(
|
|
$data,
|
|
$i,
|
|
$j,
|
|
$valueDrawType,
|
|
$max_color,
|
|
$avg_color,
|
|
$min_color,
|
|
$minmax_color,
|
|
$calc_fnc,
|
|
$this->items[$item]['yaxisside']
|
|
);
|
|
}
|
|
|
|
$j = $i;
|
|
}
|
|
}
|
|
|
|
if ($this->drawLegend) {
|
|
$this->drawTriggers();
|
|
$this->drawPercentile();
|
|
$this->drawLegend();
|
|
}
|
|
|
|
if ($debug_mode) {
|
|
$data_from = [];
|
|
foreach ($this->items as $item) {
|
|
$data_from[$item['source']] = true;
|
|
}
|
|
ksort($data_from);
|
|
|
|
$str = sprintf('%0.2f', microtime(true) - $start_time);
|
|
imageText($this->im, 6, 90, $this->fullSizeX - 2, $this->fullSizeY - 5, $this->getColor('Gray'),
|
|
_s('Data from %1$s. Generated in %2$s sec.', implode(', ', array_keys($data_from)), $str)
|
|
);
|
|
}
|
|
|
|
unset($this->items, $this->data);
|
|
|
|
imageOut($this->im);
|
|
}
|
|
}
|