mirror of
https://github.com/ntop/ntopng.git
synced 2026-04-29 23:49:33 +00:00
461 lines
15 KiB
JavaScript
461 lines
15 KiB
JavaScript
/**
|
|
(C) 2023 - ntop.org
|
|
*/
|
|
|
|
import dygraphPlotters from './dygraph-plotters.js';
|
|
import dygraphConfig from './dygraph-config.js';
|
|
|
|
|
|
/* ***************************************** */
|
|
|
|
const defaultColors = [
|
|
'#C6D9FD',
|
|
'#90EE90',
|
|
'#EE8434',
|
|
'#C95D63',
|
|
'#AE8799',
|
|
'#717EC3',
|
|
'#496DDB',
|
|
'#5A7ADE',
|
|
'#6986E1',
|
|
'#7791E4',
|
|
'#839BE6',
|
|
'#8EA4E8',
|
|
];
|
|
|
|
/* ***************************************** */
|
|
|
|
const constant_serie_colors = {
|
|
'default_color': '#C6D9FD',
|
|
'95_perc': '#8EA4E8',
|
|
'avg': '#839BE6',
|
|
};
|
|
|
|
/* ***************************************** */
|
|
|
|
function getSerieId(serie) {
|
|
return `${serie.id}`;
|
|
}
|
|
|
|
/* ***************************************** */
|
|
|
|
function formatSerieColors(palette_list) {
|
|
const colors_list = palette_list;
|
|
let count0 = 0; let count1 = 0;
|
|
const colors0 = defaultColors;
|
|
const colors1 = d3v7.schemeCategory10;
|
|
colors_list.forEach((s, index) => {
|
|
if (s.palette == 0) {
|
|
if (palette_list.find((element, j) => (element.color === s.color && j !== index))) {
|
|
palette_list[index] = colors0[count0 % colors0.length];
|
|
} else {
|
|
palette_list[index] = s.color;
|
|
}
|
|
count0 += 1;
|
|
} else if (s.palette == 1) {
|
|
palette_list[index] = colors1[count1 % colors1.length];
|
|
count1 += 1;
|
|
}
|
|
});
|
|
}
|
|
|
|
/* *********************************************** */
|
|
|
|
/* Return the formatted serie name */
|
|
function getSerieName(name, id, tsGroup, useFullName) {
|
|
if (name == null) {
|
|
name = id;
|
|
}
|
|
let name_more_space = '';
|
|
if (name != null) {
|
|
name_more_space = `${name}`;
|
|
}
|
|
if (useFullName == false) {
|
|
return name;
|
|
}
|
|
let source_index = 0;
|
|
const source_def_array = tsGroup.source_type.source_def_array;
|
|
for (let i = 0; i < source_def_array.length; i += 1) {
|
|
const source_def = source_def_array[i];
|
|
if (source_def.main_source_def == true) {
|
|
source_index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const source = tsGroup.source_array[source_index];
|
|
const prefix = `${source.label}`;
|
|
return `${prefix} - ${name_more_space}`;
|
|
}
|
|
|
|
/* *********************************************** */
|
|
|
|
/* Given all the info about a timeserie, return the correct name to be displayed */
|
|
function getName(ts_info, metadata) {
|
|
let name = (metadata.use_serie_name == true) ? ts_info.name : metadata.label;
|
|
|
|
if (ts_info.ext_label) {
|
|
name = ts_info.ext_label;
|
|
}
|
|
if (ts_info.label) {
|
|
name = ts_info.label;
|
|
}
|
|
return {timeserie_name: name, show_full_name: (ts_info.label == null)};
|
|
}
|
|
|
|
/* *********************************************** */
|
|
|
|
/* This function return the plotting function */
|
|
function getPlotter(chart_type) {
|
|
switch (chart_type) {
|
|
case 'bar':
|
|
return dygraphPlotters.barChartPlotter;
|
|
}
|
|
}
|
|
|
|
/* *********************************************** */
|
|
|
|
function addNewSerie(serie_name, chart_type, color, config) {
|
|
config.labels.push(serie_name);
|
|
if (config.properties == null) {
|
|
config.properties = {};
|
|
}
|
|
config.properties[serie_name] = {};
|
|
config.properties[serie_name] = dygraphConfig.formatSerieProperties(chart_type);
|
|
config.colors.push(color);
|
|
}
|
|
|
|
/* *********************************************** */
|
|
|
|
/* This function given a serie, format the array needed */
|
|
function compactSerie(config, ts_info, extra_timeseries, serie, past_serie, scalar, step, epoch_begin, names) {
|
|
const avg_value = ts_info.statistics['average'];
|
|
const perc_value = ts_info.statistics['95th_percentile'];
|
|
let time = epoch_begin;
|
|
|
|
/* Now format the timeserie */
|
|
for (let point = 0; point < serie.length; point++) {
|
|
const serie_point = serie[point];
|
|
/* If the point is inserted for the first time, add the time before everything else */
|
|
if (!config.serie[time]) {
|
|
config.serie[time] = [{value: new Date(time * 1000), name: 'Time'}];
|
|
}
|
|
/* Add the point to the array or NaN if it's null */
|
|
(serie_point !== null) ?
|
|
config.serie[time].push({value: serie_point * scalar, name: names.serie_name}) :
|
|
config.serie[time].push({value: NaN, name: names.serie_name});
|
|
|
|
/* Add extra series, avg, 95th and past timeseries */
|
|
if (extra_timeseries?.avg == true) {
|
|
config.serie[time].push({value: avg_value * scalar, name: names.avg_name});
|
|
}
|
|
if (extra_timeseries?.perc_95 == true) {
|
|
config.serie[time].push({value: perc_value * scalar, name: names.perc_name});
|
|
}
|
|
if (extra_timeseries?.past == true) {
|
|
const past_value = (past_serie) ? past_serie[point] : null;
|
|
(past_value) ?
|
|
config.serie[time].push({value: past_value * scalar, name: names.past_name}) :
|
|
config.serie[time].push({value: NaN, name: names.past_label});
|
|
}
|
|
|
|
/* Increase the time using the step */
|
|
time = time + step;
|
|
}
|
|
}
|
|
|
|
/* *********************************************** */
|
|
|
|
/* This function format the Bound type serie in the correct format */
|
|
function splitBoundSerie(series, timeserie_info) {
|
|
const serie = [];
|
|
let color = {};
|
|
let formatter = null;
|
|
let serie_name = null;
|
|
let properties = {};
|
|
const full_serie = [];
|
|
|
|
/* A bound timeserie should be composed by 3 timeseries:
|
|
* - metric (main)
|
|
* - lower_bound (the lower bound)
|
|
* - upper_bound (the upper bound)
|
|
*/
|
|
series.forEach((ts_info, j) => {
|
|
const ts_id = getSerieId(ts_info);
|
|
const serie = ts_info.data || []; /* Safety check */
|
|
const metadata = timeserie_info.metric.timeseries[ts_id];
|
|
const scalar = (metadata?.invert_direction === true) ? -1 : 1;
|
|
|
|
/* Just add the name, properties, colors, ecc, for the
|
|
* "main" timeserie and not for the bounds ones
|
|
*/
|
|
if (metadata.type == 'metric') {
|
|
serie_name = getSerieName(metadata.label, ts_id, timeserie_info, true);
|
|
properties = dygraphConfig.formatSerieProperties('bounds');
|
|
color = {color: metadata.color, palette: 0};
|
|
formatter = timeserie_info.metric.measure_unit;
|
|
}
|
|
for (let point = 0; point < serie.length; point++) {
|
|
const serie_point = (serie[point] === null) ? NaN : serie[point];
|
|
if (full_serie[point] == null) {
|
|
full_serie[point] = [0, NaN, 0];
|
|
}
|
|
|
|
if (metadata.type == 'lower_bound') {
|
|
full_serie[point][0] = serie_point * scalar;
|
|
} else if (metadata.type == 'metric') {
|
|
full_serie[point][1] = serie_point * scalar;
|
|
} else if (metadata.type == 'upper_bound') {
|
|
full_serie[point][2] = serie_point * scalar;
|
|
}
|
|
}
|
|
});
|
|
|
|
return {serie: full_serie, color: color, formatter: formatter, serie_name: serie_name, properties: properties};
|
|
}
|
|
|
|
/* *********************************************** */
|
|
|
|
/* This function, given a serie format the bounds serie */
|
|
function formatBoundsSerie(timeserie_info, timeserie_options, config) {
|
|
/* By default the chart type is line */
|
|
const chart_type = timeserie_info.metric.chart_type || 'filled';
|
|
const series = timeserie_options.series || [];
|
|
const epoch_begin = timeserie_options.metadata.epoch_begin;
|
|
const step = timeserie_options.metadata.epoch_step;
|
|
const {serie, color, formatter, serie_name, properties} = splitBoundSerie(series, timeserie_info);
|
|
let time = epoch_begin;
|
|
/* TODO: add avg, past, ecc. timeseries to the bounds one */
|
|
|
|
/* Update the config */
|
|
const formatted_name = `${serie_name} ${i18n('lower_value_upper')}`;
|
|
const formatter_found = config.formatters.find((el) => el == formatter);
|
|
if (!formatter_found) {
|
|
config.formatters.push(formatter);
|
|
}
|
|
config.plotter = getPlotter(chart_type);
|
|
config.customBars = true;
|
|
config.colors.push(color);
|
|
config.labels.push(formatted_name);
|
|
config.properties[formatted_name] = properties;
|
|
|
|
/* Update the serie */
|
|
Object.keys(serie).forEach((key) => {
|
|
if (!config.serie[time]) {
|
|
config.serie[time] = [
|
|
{value: new Date(time * 1000), name: 'Time'},
|
|
{value: serie[key], name: formatted_name},
|
|
];
|
|
}
|
|
|
|
time = time + step;
|
|
});
|
|
}
|
|
|
|
/* *********************************************** */
|
|
|
|
function formatStandardSerie(timeserie_info, timeserie_options, config, tsCompare) {
|
|
/* Iterate all the timeseries currently contained inside the single ts:
|
|
* e.g. in the Traffic timeseries we have the Bytes sent and Bytes rcvd
|
|
*/
|
|
const series = timeserie_options.series || [];
|
|
const chart_type = timeserie_info.metric.chart_type || 'filled';
|
|
const epoch_begin = timeserie_options.metadata.epoch_begin;
|
|
const step = timeserie_options.metadata.epoch_step;
|
|
const formatter = timeserie_info.metric.measure_unit;
|
|
const max_value = timeserie_info.metric.max_value || null;
|
|
const min_value = timeserie_info.metric.min_value || null;
|
|
const past_serie = timeserie_options.additional_series;
|
|
let disable_past_ts = false;
|
|
|
|
config.value_range = [min_value, max_value];
|
|
config.plotter = getPlotter(chart_type);
|
|
if (!config.stacked) {
|
|
config.stacked = timeserie_info.metric.draw_stacked || false;
|
|
}
|
|
if (timeserie_info.metric.disable_default_ago_ts) {
|
|
disable_past_ts = timeserie_info.metric.disable_default_ago_ts || true;
|
|
}
|
|
|
|
series.forEach((ts_info, j) => {
|
|
const serie = ts_info.data || []; /* Safety check */
|
|
const extra_timeseries = timeserie_info.timeseries[0]; /* e.g. the Average */
|
|
const ts_id = getSerieId(ts_info);
|
|
const metadata = timeserie_info.metric.timeseries[ts_id];
|
|
const scalar = (metadata.invert_direction === true) ? -1 : 1;
|
|
const {timeserie_name, show_full_name} = getName(ts_info, metadata);
|
|
/* Check if show_full_name is null or undefined */
|
|
const serie_name = getSerieName(timeserie_name, ts_id, timeserie_info, (show_full_name !== null || show_full_name !== undefined) ? (config.use_full_name && show_full_name) : config.use_full_name);
|
|
const avg_name = getSerieName(timeserie_name + ' Avg', ts_id, timeserie_info, config.use_full_name);
|
|
const perc_name = getSerieName(timeserie_name + ' 95th Perc', ts_id, timeserie_info, config.use_full_name);
|
|
const past_name = getSerieName(timeserie_name + ' ' + tsCompare + ' Ago', ts_id, timeserie_info, config.use_full_name);
|
|
const past_value = (past_serie) ? past_serie[`${tsCompare}_ago`]?.series[j]?.data : null;
|
|
/* An option used to not display a timeserie */
|
|
if (metadata.hidden) {
|
|
return;
|
|
}
|
|
|
|
/* Search for the formatter in the array, if not found, add it. */
|
|
const formatter_found = config.formatters.find((el) => el == formatter);
|
|
if (!formatter_found) {
|
|
config.formatters.push(formatter);
|
|
}
|
|
|
|
/* Add the serie */
|
|
addNewSerie(serie_name, chart_type, {color: metadata.color, palette: 0}, config);
|
|
|
|
/* Adding the extra timeseries, 30m ago, avg and 95th */
|
|
if (extra_timeseries?.avg == true) {
|
|
addNewSerie(avg_name, 'point', {color: constant_serie_colors['avg'], palette: 1}, config);
|
|
}
|
|
if (extra_timeseries?.perc_95 == true) {
|
|
addNewSerie(perc_name, 'point', {color: constant_serie_colors['perc_95'], palette: 1}, config);
|
|
}
|
|
if (extra_timeseries?.past == true && !disable_past_ts) {
|
|
addNewSerie(past_name, 'dash', {color: constant_serie_colors['past'], palette: 1}, config);
|
|
}
|
|
|
|
/* ************************************** */
|
|
|
|
compactSerie(config, ts_info, extra_timeseries, serie, past_value, scalar, step, epoch_begin, {
|
|
serie_name: serie_name,
|
|
avg_name: avg_name,
|
|
perc_name: perc_name,
|
|
past_name: past_name,
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ************************************** */
|
|
|
|
/* This function finally format the timeseries and compact it togheter */
|
|
function formatFullSerie(config) {
|
|
const full_serie = [];
|
|
const serie_keys = Object.keys(config.serie);
|
|
|
|
/* Iterate the serie and for each label, get the value and set to null in case it does not exists */
|
|
serie_keys.forEach((key, index) => {
|
|
full_serie[index] = [];
|
|
config.labels.forEach((label) => {
|
|
let found = false;
|
|
for (let j = 0; j < config.serie[key].length; j++) {
|
|
if (config.serie[key][j].name == label) {
|
|
full_serie[index].push(config.serie[key][j].value);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Push null if no value is found */
|
|
if (found == false) {
|
|
full_serie[index].push(null);
|
|
}
|
|
});
|
|
});
|
|
config.serie = full_serie;
|
|
}
|
|
|
|
/* ************************************** */
|
|
|
|
function formatSingleSerie(timeserie_info, timeserie_options, tsCompare, config) {
|
|
if (timeserie_info.source_type.f_map_ts_options != null) {
|
|
const f_map_ts_options = timeserie_info.source_type.f_map_ts_options;
|
|
timeserie_options = f_map_ts_options(timeserie_options, timeserie_info);
|
|
}
|
|
|
|
/* Format the data */
|
|
|
|
/* the data in Dygraphs should be formatted as follow:
|
|
* { [ time_1, serie1_1, serie2_1 ], [ time_2, serie1_2, serie2_2 ] }
|
|
*/
|
|
const bounds = timeserie_info.metric.bounds || false;
|
|
|
|
/* The serie can possibly have multiple timeseries, like for the
|
|
* bytes, we have sent and rcvd, so compact them
|
|
*/
|
|
if (bounds == true) {
|
|
formatBoundsSerie(timeserie_info, timeserie_options, config);
|
|
} else {
|
|
formatStandardSerie(timeserie_info, timeserie_options, config, tsCompare);
|
|
}
|
|
}
|
|
|
|
/* *********************************************** */
|
|
|
|
function formatSimpleSerie(data, serie_name, chart_type, formatters, value_range) {
|
|
let counter = 1;
|
|
const tmp_serie = [];
|
|
data.serie.forEach((value) => {
|
|
tmp_serie.push([counter, value]);
|
|
counter++;
|
|
});
|
|
|
|
/* To not have an error, just add a null value */
|
|
if (tmp_serie.length == 0) {
|
|
tmp_serie.push([1, null]);
|
|
}
|
|
|
|
const config = {
|
|
serie: tmp_serie,
|
|
formatters: formatters,
|
|
labels: ['index'],
|
|
colors: [],
|
|
stacked: false,
|
|
customBars: false,
|
|
use_full_name: false,
|
|
plotter: getPlotter(chart_type),
|
|
value_range: value_range,
|
|
disable_ts_list: true,
|
|
};
|
|
|
|
if (typeof (serie_name) === 'string') {
|
|
addNewSerie(serie_name, chart_type, {color: constant_serie_colors['default_color'], palette: 0}, config);
|
|
} else {
|
|
serie_name.forEach((el) => {
|
|
addNewSerie(el, chart_type, {color: constant_serie_colors['default_color'], palette: 0}, config);
|
|
});
|
|
}
|
|
formatSerieColors(config.colors);
|
|
return dygraphConfig.buildChartOptions(config);
|
|
}
|
|
|
|
/* *********************************************** */
|
|
|
|
function formatSerie(tsOptionsArray, tsGroupsArray, tsCompare, useFullName) {
|
|
const config = {
|
|
serie: [],
|
|
formatters: [],
|
|
labels: ['Time'],
|
|
colors: [],
|
|
properties: [],
|
|
stacked: false,
|
|
customBars: false,
|
|
use_full_name: (useFullName != null) ? useFullName : false,
|
|
};
|
|
|
|
/* Go throught each serie */
|
|
tsOptionsArray.forEach((tsOptions, i) => {
|
|
formatSingleSerie(tsGroupsArray[i], tsOptions, tsCompare, config);
|
|
});
|
|
|
|
/* Need to finally format the serie as requested by Dygraph, with
|
|
NULL as value in case the serie has NOT THAT POINT (e.g. with a 5 minutes frequency, the user
|
|
is confronting a chart with 1 minute frequency, there are 4 minutes with no existing points)
|
|
*/
|
|
formatFullSerie(config);
|
|
formatSerieColors(config.colors);
|
|
return dygraphConfig.buildChartOptions(config);
|
|
}
|
|
|
|
/* *********************************************** */
|
|
|
|
const dygraphFormat = function() {
|
|
return {
|
|
formatSerie,
|
|
formatSimpleSerie,
|
|
getSerieId,
|
|
getSerieName,
|
|
};
|
|
}();
|
|
|
|
export default dygraphFormat;
|