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*(?[><]=?|=)\s*(?.*)$/', $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); } }