<?php declare(strict_types = 0); /* ** Copyright (C) 2001-2025 Zabbix SIA ** ** This program is free software: you can redistribute it and/or modify it under the terms of ** the GNU Affero General Public License as published by the Free Software Foundation, version 3. ** ** 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 Affero General Public License for more details. ** ** You should have received a copy of the GNU Affero General Public License along with this program. ** If not, see <https://www.gnu.org/licenses/>. **/ namespace Widgets\PieChart\Actions; use API, CAggFunctionData, CArrayHelper, CControllerDashboardWidgetView, CControllerResponseData, CItemHelper, Manager; use Widgets\PieChart\Includes\{ CWidgetFieldDataSet, WidgetForm }; class WidgetView extends CControllerDashboardWidgetView { private const LEGEND_AGGREGATION_ON = 1; private const LEGEND_VALUE_ON = 1; private const MERGE_SECTORS_ON = 1; private const SHOW_TOTAL_ON = 1; private const SHOW_UNITS_ON = 1; private const VALUE_BOLD_ON = 1; protected function init(): void { parent::init(); $this->addValidationRules([ 'has_custom_time_period' => 'in 1', 'with_config' => 'in 1' ]); } protected function doAction(): void { $pie_chart_options = [ 'data_sets' => array_values($this->fields_values['ds']), 'data_source' => $this->fields_values['source'], 'time_period' => [ 'time_from' => $this->fields_values['time_period']['from_ts'], 'time_to' => $this->fields_values['time_period']['to_ts'] ], 'templateid' => $this->getInput('templateid', ''), 'override_hostid' => $this->fields_values['override_hostid'] ? $this->fields_values['override_hostid'][0] : '', 'merge_sectors' => [ 'merge' => $this->fields_values['merge'], 'percent' => $this->fields_values['merge'] == self::MERGE_SECTORS_ON ? $this->fields_values['merge_percent'] : null, 'color' => $this->fields_values['merge'] == self::MERGE_SECTORS_ON ? '#'.$this->fields_values['merge_color'] : null ], 'total_value' => [ 'total_show' => $this->fields_values['total_show'], 'decimal_places' => $this->fields_values['total_show'] == self::SHOW_TOTAL_ON ? $this->fields_values['decimal_places'] : null ], 'units' => [ 'units_show' => $this->fields_values['units_show'], 'units_value' => $this->fields_values['units_show'] == self::SHOW_UNITS_ON ? $this->fields_values['units'] : null ], 'legend_aggregation_show' => $this->fields_values['legend_aggregation'] == self::LEGEND_AGGREGATION_ON ]; $data = [ 'name' => $this->getInput('name', $this->widget->getDefaultName()), 'info' => $this->makeWidgetInfo(), 'user' => [ 'debug_mode' => $this->getDebugMode() ], 'vars' => [] ]; $metrics = $this->getData($pie_chart_options); $svg_data = $this->getSVGData($metrics['sectors'], $metrics['total_value']); $data['vars']['sectors'] = $svg_data['svg_sectors']; $data['vars']['all_sectorids'] = $metrics['all_sectorids']; $data['vars']['total_value'] = $svg_data['svg_total_value']; $data['vars']['legend'] = $this->getLegend($metrics['sectors']); if ($this->hasInput('with_config')) { $data['vars']['config'] = $this->getConfig(); } $this->setResponse(new CControllerResponseData($data)); } private function getData($options): array { $metrics = []; $total_value = []; $all_sectorids = []; self::getItems($metrics, $options['data_sets'], $options['templateid'], $options['override_hostid']); self::getChartDataSource($metrics, $options['data_source'], $options['time_period']['time_from']); self::getMetricsData($metrics, $options['time_period'], $options['legend_aggregation_show'], $options['templateid'], $options['override_hostid']); self::getSectorsData($metrics, $total_value, $options['merge_sectors'], $options['total_value'], $options['units'], $options['templateid'], $options['override_hostid'], $all_sectorids); return [ 'sectors' => $metrics, 'all_sectorids' => $all_sectorids, 'total_value' => $total_value ]; } private function makeWidgetInfo(): array { $info = []; if ($this->hasInput('has_custom_time_period')) { $info[] = [ 'icon' => ZBX_ICON_TIME_PERIOD, 'hint' => relativeDateToText($this->fields_values['time_period']['from'], $this->fields_values['time_period']['to'] ) ]; } return $info; } private static function getItems(array &$metrics, array $data_sets, string $templateid, string $override_hostid): void { $metrics = []; $max_metrics = 50; foreach ($data_sets as $index => $data_set) { if ($max_metrics === 0) { break; } if ($data_set['dataset_type'] == CWidgetFieldDataSet::DATASET_TYPE_SINGLE_ITEM) { $ds_metrics = self::getMetricsSingleItemDS($data_set, $max_metrics, $templateid, $override_hostid); } else { $ds_metrics = self::getMetricsPatternItemDS($data_set, $max_metrics, $templateid, $override_hostid); } foreach ($ds_metrics as $ds_metric) { $ds_metric['data_set'] = $index; $metrics[] = $ds_metric; $max_metrics--; } } } private static function getMetricsSingleItemDS(array $data_set, int $max_metrics, string $templateid, string $override_hostid): array { $metrics = []; $ds_items = []; if (!$data_set['itemids'] || count($data_set['color']) !== count($data_set['itemids']) || count($data_set['type']) !== count($data_set['itemids'])) { return $metrics; } foreach ($data_set['itemids'] as $key => $item) { $ds_items[$item] = [ 'color' => $data_set['color'][$key], 'type' => $data_set['type'][$key] ]; } unset($data_set['itemids'], $data_set['color'], $data_set['type']); if ($override_hostid !== '') { // Host dashboard (view). $tmp_items = API::Item()->get([ 'output' => ['itemid', 'key_'], 'itemids' => array_keys($ds_items), 'webitems' => true ]); if ($tmp_items) { $items = API::Item()->get([ 'output' => ['itemid', 'key_'], 'hostids' => [$override_hostid], 'webitems' => true, 'filter' => [ 'key_' => array_column($tmp_items, 'key_') ] ]); if (!$items) { $ds_items = []; } else { $old_item_keys = array_combine(array_column($tmp_items, 'itemid'), array_column($tmp_items, 'key_')); $new_itemids = array_combine(array_column($items, 'key_'), array_column($items, 'itemid')); foreach ($ds_items as $key => $item) { unset($ds_items[$key]); $new_id = $new_itemids[$old_item_keys[$key]]; $ds_items[$new_id] = $item; } } } } $resolve_macros = $templateid === '' || $override_hostid !== ''; $db_items = API::Item()->get([ 'output' => ['itemid', 'hostid', $resolve_macros ? 'name_resolved' : 'name', 'history', 'trends', 'units', 'value_type' ], 'selectHosts' => ['name'], 'webitems' => true, 'filter' => [ 'value_type' => [ITEM_VALUE_TYPE_UINT64, ITEM_VALUE_TYPE_FLOAT] ], 'itemids' => array_keys($ds_items), 'preservekeys' => true, 'limit' => $max_metrics ]); if ($resolve_macros) { $db_items = CArrayHelper::renameObjectsKeys($db_items, ['name_resolved' => 'name']); } foreach ($ds_items as $itemid => $ds_item) { if (array_key_exists($itemid, $db_items)) { $data_set['color'] = '#' . $ds_item['color']; $data_set['type'] = $ds_item['type']; $metrics[] = $db_items[$itemid] + ['options' => $data_set]; } } return $metrics; } private static function getMetricsPatternItemDS(array $data_set, int $max_metrics, string $templateid, string $override_hostid): array { $metrics = []; if (($templateid === '' && (!$data_set['hosts'] || !$data_set['items'])) || ($templateid !== '' && !$data_set['items']) ) { return $metrics; } if ($override_hostid === '' && $templateid === '') { $hosts = API::Host()->get([ 'output' => [], 'search' => [ 'name' => self::processPattern($data_set['hosts']) ], 'searchWildcardsEnabled' => true, 'searchByAny' => true, 'preservekeys' => true ]); $hostids = array_keys($hosts); } else { $hostids = $override_hostid !== '' ? [$override_hostid] : [$templateid]; } if (!$hostids) { return $metrics; } $options = [ 'output' => ['itemid', 'hostid', 'history', 'trends', 'units', 'value_type'], 'selectHosts' => ['name'], 'webitems' => true, 'hostids' => $hostids, 'filter' => [ 'value_type' => [ITEM_VALUE_TYPE_UINT64, ITEM_VALUE_TYPE_FLOAT] ], 'searchWildcardsEnabled' => true, 'searchByAny' => true, 'sortfield' => 'name', 'sortorder' => ZBX_SORT_UP, 'limit' => $max_metrics ]; $resolve_macros = $templateid === '' || $override_hostid !== ''; if ($resolve_macros) { $options['output'][] = 'name_resolved'; if ($templateid === '') { $options['search']['name_resolved'] = self::processPattern($data_set['items']); } else { $options['search']['name'] = self::processPattern($data_set['items']); } } else { $options['output'][] = 'name'; $options['search']['name'] = self::processPattern($data_set['items']); } $items = API::Item()->get($options); if ($resolve_macros) { $items = CArrayHelper::renameObjectsKeys($items, ['name_resolved' => 'name']); } $colors = getColorVariations('#'.$data_set['color'], count($items)); unset($data_set['hosts'], $data_set['items'], $data_set['color']); foreach ($items as $item) { $data_set['color'] = array_shift($colors); $metrics[] = $item + ['options' => $data_set]; } return $metrics; } private static function getChartDataSource(array &$metrics, int $data_source, int $time): void { if ($data_source == WidgetForm::DATA_SOURCE_AUTO) { $metrics = CItemHelper::addDataSource($metrics, $time); } else { foreach ($metrics as &$metric) { $metric['source'] = $data_source == WidgetForm::DATA_SOURCE_TRENDS ? 'trends' : 'history'; } unset($metric); } } private static function getMetricsData(array &$metrics, array $time_period, bool $legend_aggregation_show, string $templateid, string $override_hostid): void { $dataset_metrics = []; foreach ($metrics as $metric_num => &$metric) { $dataset_num = $metric['data_set']; if ($metric['options']['dataset_aggregation'] == AGGREGATE_NONE) { if ($legend_aggregation_show) { $name = CItemHelper::getAggregateFunctionName($metric['options']['aggregate_function']). '('.$metric['hosts'][0]['name'].NAME_DELIMITER.$metric['name'].')'; } else { $name = $metric['hosts'][0]['name'].NAME_DELIMITER.$metric['name']; } } else { $name = $metric['options']['data_set_label'] !== '' ? $metric['options']['data_set_label'] : _('Data set').' #'.($dataset_num + 1); } $item = [ 'itemid' => $metric['itemid'], 'value_type' => $metric['value_type'], 'source' => $metric['source'] ]; if (!array_key_exists($dataset_num, $dataset_metrics)) { $metric['name'] = $name; $metric['items'] = [$item]; $metric['value'] = null; if ($metric['options']['dataset_aggregation'] != AGGREGATE_NONE) { $dataset_metrics[$dataset_num] = $metric_num; } } else { $metrics[$dataset_metrics[$dataset_num]]['items'][] = $item; unset($metrics[$metric_num]); } } unset($metric); foreach ($metrics as &$metric) { if ($templateid !== '' && $override_hostid === '') { continue; } $values = Manager::History()->getAggregatedValues($metric['items'], $metric['options']['aggregate_function'], $time_period['time_from'], $time_period['time_to'] ); if (!$values) { continue; } $values = array_column($values, 'value'); switch($metric['options']['dataset_aggregation']) { case AGGREGATE_MAX: $metric['value'] = max($values); break; case AGGREGATE_MIN: $metric['value'] = min($values); break; case AGGREGATE_AVG: $metric['value'] = array_sum($values) / count($values); break; case AGGREGATE_COUNT: $metric['value'] = count($values); break; case AGGREGATE_SUM: $metric['value'] = array_sum($values); break; default: $metric['value'] = $values[0]; } } } private static function getSectorsData(array &$metrics, array &$total_value, array $merge_sectors, array $total_config, array $units_config, string $templateid, string $override_hostid, array &$all_sectorids): void { $has_total = false; $chart_units = null; $raw_total_value = null; $others_value = 0; $below_threshold_sectors = []; $sectors = []; if ($templateid !== '' && $override_hostid === '') { foreach ($metrics as $metric) { $is_total = ($metric['options']['dataset_aggregation'] == AGGREGATE_NONE && $metric['options']['type'] == CWidgetFieldDataSet::ITEM_TYPE_TOTAL); $sectors[] = [ 'id' => $metric['data_set'].'_'.$metric['itemid'], 'name' => $metric['name'], 'color' => $metric['options']['color'], 'value' => null, 'units' => '', 'is_total' => $is_total ]; $all_sectorids[] = $metric['data_set'].'_'.$metric['itemid']; } } else { foreach ($metrics as &$metric) { $is_total = ($metric['options']['dataset_aggregation'] == AGGREGATE_NONE && $metric['options']['type'] == CWidgetFieldDataSet::ITEM_TYPE_TOTAL); if ($is_total) { $raw_total_value = $metric['value'] !== null ? abs($metric['value']) : null; $has_total = true; } elseif (!$has_total && $metric['value'] !== null) { if ($raw_total_value === null) { $raw_total_value = 0; } $raw_total_value += abs($metric['value']); } if ($units_config['units_show'] == self::SHOW_UNITS_ON && $units_config['units_value'] !== '') { $metric['units'] = $units_config['units_value']; } elseif (!CAggFunctionData::preservesUnits($metric['options']['aggregate_function'])) { $metric['units'] = ''; } if ($chart_units === null || $is_total) { $chart_units = $metric['units']; } $sectors[] = [ 'id' => $metric['data_set'].'_'.$metric['itemid'], 'name' => $metric['name'], 'color' => $metric['options']['color'], 'value' => $metric['value'], 'units' => $metric['units'], 'is_total' => $is_total ]; $all_sectorids[] = $metric['data_set'].'_'.$metric['itemid']; } unset($metric); if ($merge_sectors['merge'] == self::MERGE_SECTORS_ON) { $all_sectorids[] = 'other'; } foreach ($sectors as $key => $sector) { if ($sector['value'] == 0 || $raw_total_value == 0) { $percentage = 0; } else { $percentage = (abs($sector['value']) / $raw_total_value) * 100; } if ($merge_sectors['merge'] == self::MERGE_SECTORS_ON && $percentage < $merge_sectors['percent']) { $others_value += abs($sector['value'] ?? 0); $below_threshold_sectors[] = $key; } } if (count($below_threshold_sectors) >= 2) { foreach ($below_threshold_sectors as $sector_key) { unset($sectors[$sector_key]); } $sectors[] = [ 'id' => 'other', 'name' => _('Other'), 'color' => $merge_sectors['color'], 'value' => $others_value, 'units' => $chart_units, 'is_total' => false ]; } } foreach ($sectors as &$sector) { $formatted_value = convertUnitsRaw([ 'value' => $sector['value'], 'units' => $sector['units'], 'zero_as_zero' => false ]); unset($formatted_value['is_numeric']); $sector['formatted_value'] = $formatted_value; } unset($sector); $metrics = $sectors; $formatted_total_value = convertUnitsRaw([ 'value' => $raw_total_value, 'units' => $chart_units, 'power' => $units_config['units_show'] == self::SHOW_UNITS_ON ? null : 0, 'decimals' => $total_config['decimal_places'], 'decimals_exact' => true, 'small_scientific' => false, 'zero_as_zero' => false ]); unset($formatted_total_value['is_numeric']); $total_value['value'] = $raw_total_value; if ($raw_total_value !== null) { $total_value['formatted_value'] = $formatted_total_value; } } /** * Prepare an array to be used for hosts/items filtering. * * @param array $patterns Array of strings containing hosts/items patterns. * * @return array|mixed Returns array of patterns. * Returns NULL if array contains '*' (so any possible host/item search matches). */ private static function processPattern(array $patterns): ?array { return in_array('*', $patterns, true) ? null : $patterns; } private function getConfig(): array { $config = [ 'draw_type' => $this->fields_values['draw_type'], 'space' => $this->fields_values['space'] ]; if ($this->fields_values['draw_type'] == WidgetForm::DRAW_TYPE_DOUGHNUT) { $config['width'] = $this->fields_values['width']; $config['stroke'] = $this->fields_values['stroke']; if ($this->fields_values['total_show'] == self::SHOW_TOTAL_ON) { $config['total_value'] = [ 'show' => true, 'is_custom_size' => $this->fields_values['value_size_type'] == WidgetForm::VALUE_SIZE_CUSTOM, 'is_bold' => $this->fields_values['value_bold'] == self::VALUE_BOLD_ON, 'color' => '#'.$this->fields_values['value_color'], 'units_show' => $this->fields_values['units_show'] == self::SHOW_UNITS_ON ]; if ($this->fields_values['value_size_type'] == WidgetForm::VALUE_SIZE_CUSTOM) { $config['total_value']['size'] = $this->fields_values['value_size']; } } else { $config['total_value'] = [ 'show' => false ]; } } return $config; } private function getLegend(array $sectors): array { $legend['data'] = []; foreach ($sectors as $sector) { $legend['data'][] = [ 'id' => $sector['id'], 'name' => $sector['name'], 'value' => convertUnits([ 'value' => $sector['value'], 'units' => $sector['units'], 'convert' => ITEM_CONVERT_NO_UNITS ]), 'color' => $sector['color'], 'is_total' => $sector['is_total'] ]; } if ($this->fields_values['legend'] == WidgetForm::LEGEND_ON) { $legend['show'] = true; $legend['value_show'] = $this->fields_values['legend_value'] === self::LEGEND_VALUE_ON; $legend['lines_mode'] = $this->fields_values['legend_lines_mode']; $legend['lines'] = $this->fields_values['legend_lines']; if (!$legend['value_show']) { $legend['columns'] = $this->fields_values['legend_columns']; } } else { $legend['show'] = false; } return $legend; } private function getSVGData(array $sectors, array $total_value): array { if ($total_value['value'] === null) { return [ 'svg_sectors' => [], 'svg_total_value' => $total_value ]; } $sector_total_value = 0; $non_total_sectors = []; $has_total_item = false; $sectors = array_filter($sectors, static function ($sector) { return $sector['value'] !== null; }); // Move total sector to the end. foreach ($sectors as $key => $sector) { if ($sector['is_total']) { $has_total_item = true; $sectors[] = $sector; unset($sectors[$key]); break; } } $svg_sectors = array_values($sectors); foreach ($svg_sectors as &$sector) { if ($total_value['value']) { $sector['percent_of_total'] = (abs($sector['value']) / $total_value['value']) * 100; } else { $sector['percent_of_total'] = 0; } if (!$sector['is_total']) { $sector_total_value += abs($sector['value']); $non_total_sectors[] = $sector; } } unset($sector); if ($has_total_item) { if ($sector_total_value === $total_value['value']) { // Sectors use the full total value, no remaining space for the total sector. array_pop($svg_sectors); } elseif ($sector_total_value < $total_value['value']) { // Sectors use less than total value, the remaining of the total sector will be displayed. $svg_sectors[count($svg_sectors) - 1]['percent_of_total'] = ($total_value['value'] - $sector_total_value) * 100 / $total_value['value']; } else { // Sectors use more than total value. $current_value = 0; $remaining_value = $total_value['value']; $sectors_to_keep = []; foreach ($non_total_sectors as &$sector) { if (($current_value + abs($sector['value'])) <= $remaining_value) { // There is enough space for this sector. $sectors_to_keep[] = $sector; $current_value += abs($sector['value']); $remaining_value -= abs($sector['value']); } elseif (abs($sector['value']) >= $remaining_value && $current_value < $total_value['value']) { // This sector needs to be cut, to fit. $sector['percent_of_total'] = ($remaining_value / $total_value['value']) * 100; $sectors_to_keep[] = $sector; break; } else { // This sector doesn't fit. break; } } unset($sector); $svg_sectors = $sectors_to_keep; } } foreach($svg_sectors as &$sector) { unset($sector['value']); } unset($sector); return [ 'svg_sectors' => $svg_sectors, 'svg_total_value' => $total_value ]; } }