← Back
Editing: graph.js
/* * Copyright (C) 2017 Vsevolod Stakhov <vsevolod@highsecure.ru> * Copyright (C) 2017 Alexander Moisseev */ define(["app/common", "app/tab-utils", "d3evolution", "d3pie", "d3", "tabulator"], (common, tabUtils, D3Evolution, D3Pie, d3, Tabulator) => { "use strict"; const rrd_pie_config = { cornerRadius: 2, size: { canvasWidth: 400, canvasHeight: 180, pieInnerRadius: "50%", pieOuterRadius: "80%" }, labels: { outer: { format: "none" }, inner: { hideWhenLessThanPercentage: 8, offset: 0 }, }, padAngle: 0.02, pieCenterOffset: { x: -120, y: 10, }, total: { enabled: true }, }; const ui = {}; let prevUnit = "msg/s"; const rrdUnitColumns = ["min", "avg", "max", "last"]; function rrdColumnTitle(field, unit) { const titles = {min: "Minimum", avg: "Average", max: "Maximum", last: "Last"}; const label = titles[field]; return label ? label + ", <span class=\"unit\">" + unit + "</span>" : ""; } ui.draw = function (graphs, neighbours, checked_server, type) { const graph_options = { title: "Rspamd throughput", width: 1060, height: 370, yAxisLabel: "Message rate, msg/s", legend: { space: 140, entries: common.chartLegend } }; function initGraph() { const graph = new D3Evolution("graph", { ...graph_options, yScale: common.getSelector("selYScale"), type: common.getSelector("selType"), interpolate: common.getSelector("selInterpolate"), convert: common.getSelector("selConvert"), }); document.getElementById("selYScale").addEventListener("change", (e) => { graph.yScale(e.currentTarget.value); }); document.getElementById("selConvert").addEventListener("change", (e) => { graph.convert(e.currentTarget.value); }); document.getElementById("selType").addEventListener("change", (e) => { graph.type(e.currentTarget.value); }); document.getElementById("selInterpolate").addEventListener("change", (e) => { graph.interpolate(e.currentTarget.value); }); return graph; } function getRrdSummary(json, scaleFactor) { const xExtents = d3.extent(d3.merge(json), (d) => d.x); const timeInterval = xExtents[1] - xExtents[0]; let total = 0; const rows = json.map((curr, i) => { // Time intervals that don't have data are excluded from average calculation as d3.mean()ignores nulls const avg = d3.mean(curr, (d) => d.y); // To find an integral on the whole time interval we need to convert nulls to zeroes // eslint-disable-next-line no-bitwise const value = d3.mean(curr, (d) => Number(d.y)) * timeInterval / scaleFactor ^ 0; const yExtents = d3.extent(curr, (d) => d.y); total += value; return { label: graph_options.legend.entries[i].label, value: value, min: Number((yExtents[0] ?? 0).toFixed(6)), avg: Number((avg ?? 0).toFixed(6)), max: Number((yExtents[1] ?? 0).toFixed(6)), last: Number((curr[curr.length - 1]?.y ?? 0).toFixed(6)), color: graph_options.legend.entries[i].color, }; }, []); return { rows: rows, total: total }; } function initSummaryTable(rows, unit) { common.tables.rrd_summary = new Tabulator("#rrd-table", { layout: "fitColumns", selectable: false, index: "label", data: rows, columns: [ {title: "Action", field: "label", minWidth: 90}, {title: "Messages", field: "value", minWidth: 100}, {title: rrdColumnTitle("min", unit), field: "min", minWidth: 149}, {title: rrdColumnTitle("avg", unit), field: "avg", minWidth: 143}, {title: rrdColumnTitle("max", unit), field: "max", minWidth: 151}, {title: rrdColumnTitle("last", unit), field: "last", minWidth: 121}, ], rowFormatter: function (row) { const c = row.getData().color; if (c) row.getElement().style.setProperty("--rrd-row-color", c); }, }); tabUtils.patchScrollIntoViewOnce(); tabUtils.stripTableholderTabindex("rrd_summary"); } function drawRrdTable(rows, unit) { if (Object.prototype.hasOwnProperty.call(common.tables, "rrd_summary")) { common.tables.rrd_summary.updateData(rows); if (unit !== prevUnit) { rrdUnitColumns.forEach((field) => { common.tables.rrd_summary.updateColumnDefinition(field, { title: rrdColumnTitle(field, unit), }); }); } } else { initSummaryTable(rows, unit); } } function updateWidgets(data) { let rrd_summary = {rows: []}; let unit = "msg/s"; if (data) { // Autoranging let scaleFactor = 1; const yMax = d3.max(d3.merge(data), (d) => d.y); if (yMax < 1) { scaleFactor = 60; unit = "msg/min"; data.forEach((s) => { s.forEach((d) => { if (d.y !== null) { d.y *= scaleFactor; } }); }); } rrd_summary = getRrdSummary(data, scaleFactor); } if (!graphs.rrd_pie) graphs.rrd_pie = new D3Pie("rrd-pie", rrd_pie_config); graphs.rrd_pie.data(rrd_summary.rows); graphs.graph.data(data); if (unit !== prevUnit) { graphs.graph.yAxisLabel("Message rate, " + unit); document.querySelectorAll(".unit").forEach((el) => { el.textContent = unit; }); prevUnit = unit; } drawRrdTable(rrd_summary.rows, unit); document.getElementById("rrd-total-value").innerHTML = rrd_summary.total; } if (!graphs.graph) { graphs.graph = initGraph(); } common.query("graph", { success: function (req_data) { let data = null; const neighbours_data = req_data .filter((d) => d.status) // filter out unavailable neighbours .map((d) => d.data); if (neighbours_data.length === 1) { [data] = neighbours_data; } else { let time_match = true; neighbours_data.reduce((res, curr, _, arr) => { if ((curr[0][0].x !== res[0][0].x) || (curr[0][curr[0].length - 1].x !== res[0][res[0].length - 1].x)) { time_match = false; common.logError({ server: "Multi-server", endpoint: "graph", message: "Neighbours time extents do not match. " + "Check if time is synchronized on all servers.", errorType: "data_inconsistency" }); arr.splice(1); // Break out of .reduce() by mutating the source array } return curr; }); if (time_match) { data = neighbours_data.reduce((res, curr) => curr.map((action, j) => action.map((d, i) => ({ x: d.x, y: (res[j][i].y === null) ? d.y : res[j][i].y + d.y })))); } } updateWidgets(data); }, complete: function () { const refreshBtn = document.getElementById("refresh"); refreshBtn.disabled = false; refreshBtn.classList.remove("disabled"); }, errorMessage: "Cannot receive throughput data", errorOnceId: "alerted_graph_", data: {type: type} }); }; // Handling mouse events on overlapping elements function setZIndex(selector, value) { document.querySelectorAll(selector).forEach((el) => { el.style.zIndex = value; }); } document.getElementById("rrd-pie").addEventListener("mouseover", () => { setZIndex("#rrd-pie, #rrd-pie-tooltip", "200"); setZIndex("#rrd-table_toggle", "300"); }); document.getElementById("rrd-table_toggle").addEventListener("mouseover", () => { setZIndex("#rrd-pie, #rrd-pie-tooltip, #rrd-table_toggle", "0"); }); return ui; });
Save File
Cancel