You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
927 lines
26 KiB
927 lines
26 KiB
/*
|
|
** Zabbix
|
|
** Copyright (C) 2001-2023 Zabbix SIA
|
|
**
|
|
** This program is free software; you can redistribute it and/or modify
|
|
** it under the terms of the GNU General Public License as published by
|
|
** the Free Software Foundation; either version 2 of the License, or
|
|
** (at your option) any later version.
|
|
**
|
|
** This program is distributed in the hope that it will be useful,
|
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
** GNU General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with this program; if not, write to the Free Software
|
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
**/
|
|
|
|
class CSVGGauge {
|
|
|
|
static ZBX_STYLE_CLASS = 'svg-gauge';
|
|
static ZBX_STYLE_DESCRIPTION = 'svg-gauge-description';
|
|
static ZBX_STYLE_ARCS = 'svg-gauge-arcs';
|
|
static ZBX_STYLE_THRESHOLDS_ARC_SECTOR = 'svg-gauge-thresholds-arc-sector';
|
|
static ZBX_STYLE_VALUE_ARC_SECTOR = 'svg-gauge-value-arc-sector';
|
|
static ZBX_STYLE_EMPTY_ARC_SECTOR = 'svg-gauge-empty-arc-sector';
|
|
static ZBX_STYLE_NEEDLE = 'svg-gauge-needle';
|
|
static ZBX_STYLE_NEEDLE_LIGHT = 'svg-gauge-needle-light';
|
|
static ZBX_STYLE_NEEDLE_DARK = 'svg-gauge-needle-dark';
|
|
static ZBX_STYLE_LABEL = 'svg-gauge-label';
|
|
static ZBX_STYLE_LABEL_LEFT = 'svg-gauge-label-left';
|
|
static ZBX_STYLE_LABEL_RIGHT = 'svg-gauge-label-right';
|
|
static ZBX_STYLE_LABEL_CENTER = 'svg-gauge-label-center';
|
|
static ZBX_STYLE_VALUE_AND_UNITS = 'svg-gauge-value-and-units';
|
|
static ZBX_STYLE_VALUE = 'svg-gauge-value';
|
|
static ZBX_STYLE_UNITS = 'svg-gauge-units';
|
|
static ZBX_STYLE_NO_DATA = 'svg-gauge-no-data';
|
|
|
|
static SVG_NS = 'http://www.w3.org/2000/svg';
|
|
|
|
static LINE_HEIGHT = 1.14;
|
|
static TEXT_BASELINE = 0.8;
|
|
static CAPITAL_HEIGHT = 0.72;
|
|
|
|
static DESC_V_POSITION_TOP = 0;
|
|
static DESC_V_POSITION_BOTTOM = 1;
|
|
|
|
static UNITS_POSITION_BEFORE = 0;
|
|
static UNITS_POSITION_ABOVE = 1;
|
|
static UNITS_POSITION_AFTER = 2;
|
|
static UNITS_POSITION_BELOW = 3;
|
|
|
|
static SCALE_SIZE_DEFAULT = 10;
|
|
|
|
static ARCS_GAP = 2;
|
|
|
|
static DESCRIPTION_GAP = 4;
|
|
|
|
static LABEL_GAP = 40;
|
|
|
|
static NEEDLE_RADIUS = 6.5;
|
|
|
|
static NEEDLE_GAP = 20;
|
|
|
|
static ANIMATE_DURATION = 500;
|
|
|
|
static ID_COUNTER = 0;
|
|
|
|
/**
|
|
* Widget configuration.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
#config;
|
|
|
|
/**
|
|
* Inner padding of the root SVG element.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
#padding;
|
|
|
|
/**
|
|
* Root SVG element.
|
|
*
|
|
* @type {SVGSVGElement}
|
|
*/
|
|
#svg;
|
|
|
|
/**
|
|
* SVG group element implementing padding inside the root SVG element.
|
|
*
|
|
* @type {SVGGElement}
|
|
*/
|
|
#g;
|
|
|
|
/**
|
|
* SVG rect element implementing clipping path of the visible area of the root SVG element.
|
|
*
|
|
* @type {SVGRectElement}
|
|
*/
|
|
#g_clip_rect;
|
|
|
|
/**
|
|
* SVG group element implementing scaling and fitting of its contents inside the root SVG element.
|
|
*
|
|
* @type {SVGGElement}
|
|
*/
|
|
#g_scalable;
|
|
|
|
/**
|
|
* Created SVG child elements and related data.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
#elements = {};
|
|
|
|
/**
|
|
* Usable width of widget without padding.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
#width;
|
|
|
|
/**
|
|
* Usable height of widget without padding.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
#height;
|
|
|
|
/**
|
|
* Current needle (and value arc) position in 0..1 range.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
#pos_current = 0;
|
|
|
|
/**
|
|
* @param {HTMLElement} container HTML container to append the root SVG element to.
|
|
*
|
|
* @param {Object} padding Inner padding of the root SVG element.
|
|
* {number} padding.horizontal
|
|
* {number} padding.vertical
|
|
*
|
|
* @param {Object} config Widget configuration.
|
|
*/
|
|
constructor(container, padding, config) {
|
|
this.#config = config;
|
|
this.#padding = padding;
|
|
|
|
this.#svg = document.createElementNS(CSVGGauge.SVG_NS, 'svg');
|
|
|
|
container.appendChild(this.#svg);
|
|
|
|
this.#svg.classList.add(CSVGGauge.ZBX_STYLE_CLASS);
|
|
|
|
if (this.#config.bg_color !== '') {
|
|
this.#svg.style.backgroundColor = `#${this.#config.bg_color}`;
|
|
}
|
|
|
|
const g_clip_path = document.createElementNS(CSVGGauge.SVG_NS, 'clipPath');
|
|
|
|
this.#svg.appendChild(g_clip_path);
|
|
|
|
g_clip_path.id = CSVGGauge.#getUniqueId();
|
|
|
|
this.#g_clip_rect = document.createElementNS(CSVGGauge.SVG_NS, 'rect');
|
|
|
|
this.#g_clip_rect.setAttribute('x', '0');
|
|
this.#g_clip_rect.setAttribute('y', '0');
|
|
|
|
g_clip_path.appendChild(this.#g_clip_rect);
|
|
|
|
this.#g = document.createElementNS(CSVGGauge.SVG_NS, 'g');
|
|
|
|
this.#svg.appendChild(this.#g);
|
|
|
|
this.#g.setAttribute('transform', `translate(${this.#padding.horizontal} ${this.#padding.vertical})`);
|
|
this.#g.setAttribute('clip-path', `url(#${g_clip_path.id})`);
|
|
|
|
this.#g_scalable = document.createElementNS(CSVGGauge.SVG_NS, 'g');
|
|
|
|
this.#g.appendChild(this.#g_scalable);
|
|
|
|
this.#createDescription();
|
|
|
|
if (this.#config.thresholds.arc.show || this.#config.value.arc.show) {
|
|
this.#createArcs();
|
|
|
|
if (this.#config.needle.show) {
|
|
this.#createNeedle();
|
|
}
|
|
|
|
if (this.#config.scale.show || this.#config.thresholds.show_labels) {
|
|
this.#createLabels();
|
|
}
|
|
}
|
|
|
|
this.#createValueAndUnits();
|
|
this.#createNoData();
|
|
}
|
|
|
|
/**
|
|
* Get the root SVG element.
|
|
*
|
|
* @returns {SVGSVGElement}
|
|
*/
|
|
getSVGElement() {
|
|
return this.#svg;
|
|
}
|
|
|
|
/**
|
|
* Set size of the root SVG element and re-position the elements.
|
|
*
|
|
* @param {number} width
|
|
* @param {number} height
|
|
*/
|
|
setSize({width, height}) {
|
|
this.#svg.setAttribute('width', `${width}`);
|
|
this.#svg.setAttribute('height', `${height}`);
|
|
|
|
this.#width = width - this.#padding.horizontal * 2;
|
|
this.#height = height - this.#padding.vertical * 2;
|
|
|
|
this.#g_clip_rect.setAttribute('width', `${this.#width}`);
|
|
this.#g_clip_rect.setAttribute('height', `${this.#height}`);
|
|
|
|
this.#drawDescription();
|
|
|
|
const arcs_height = ((this.#config.thresholds.arc.show || this.#config.value.arc.show)
|
|
&& this.#config.angle === 270)
|
|
? 1 + Math.sqrt(2) / 2
|
|
: 1;
|
|
|
|
const description_gap = this.#height * CSVGGauge.DESCRIPTION_GAP / 100;
|
|
const description_bbox = this.#elements.description.container.getBBox();
|
|
|
|
const max_width = this.#width;
|
|
const max_height = Math.max(0, this.#height - description_bbox.height - description_gap);
|
|
|
|
// Fix imprecise calculation of "this.#g_scalable" dimensions.
|
|
this.#g_scalable.setAttribute('transform', `translate(0 0) scale(1000)`);
|
|
|
|
const scalable_bbox = this.#getScalableBBox();
|
|
const box_width = Math.max(1, -scalable_bbox.x, scalable_bbox.width + scalable_bbox.x) * 2;
|
|
const box_height = Math.max(arcs_height, scalable_bbox.height);
|
|
|
|
const scale = Math.min(max_width / box_width, max_height / box_height);
|
|
|
|
const x = max_width / 2;
|
|
const y = (max_height - scalable_bbox.height * scale) / 2 - scalable_bbox.y * scale
|
|
+ (this.#config.description.position === CSVGGauge.DESC_V_POSITION_TOP
|
|
? description_bbox.height + description_gap
|
|
: 0);
|
|
|
|
this.#g_scalable.setAttribute('transform', `translate(${x} ${y}) scale(${scale})`);
|
|
}
|
|
|
|
/**
|
|
* Set value of the gauge. Null value will reset the needle to the min position.
|
|
*
|
|
* @param {number|null} value Numeric value of the gauge.
|
|
* @param {string} value_text Text representation of the value.
|
|
* @param {string} units_text Text representation of the units of the value.
|
|
*/
|
|
setValue({value, value_text, units_text}) {
|
|
this.#elements.value_and_units.value.container.textContent = value !== null ? value_text : '';
|
|
|
|
if (this.#config.units.show) {
|
|
this.#elements.value_and_units.units.container.textContent = value !== null ? units_text : '';
|
|
}
|
|
|
|
this.#elements.no_data.container.textContent = value === null ? value_text : '';
|
|
|
|
if (this.#config.value.arc.show || this.#config.needle.show) {
|
|
let pos_new = 0;
|
|
|
|
if (value !== null) {
|
|
const value_in_range = Math.min(this.#config.max, Math.max(this.#config.min, value));
|
|
|
|
pos_new = (value_in_range - this.#config.min) / (this.#config.max - this.#config.min);
|
|
}
|
|
|
|
let arc_color_new = this.#config.value.arc.color;
|
|
let needle_color_new = '';
|
|
let threshold_pos_start = 0;
|
|
|
|
for (const {color: color_next, value} of this.#config.thresholds.data) {
|
|
const threshold_pos_end = (value - this.#config.min) / (this.#config.max - this.#config.min);
|
|
|
|
if (pos_new >= threshold_pos_start && pos_new < threshold_pos_end) {
|
|
break;
|
|
}
|
|
|
|
threshold_pos_start = threshold_pos_end;
|
|
arc_color_new = color_next;
|
|
needle_color_new = color_next;
|
|
}
|
|
|
|
if (this.#config.value.arc.show) {
|
|
this.#elements.value_arcs.value_arc.style.fill = arc_color_new !== '' ? `#${arc_color_new}` : '';
|
|
}
|
|
|
|
if (this.#config.needle.show && this.#config.needle.color === '') {
|
|
this.#elements.needle.container.style.fill = needle_color_new !== '' ? `#${needle_color_new}` : '';
|
|
|
|
if (needle_color_new !== '') {
|
|
const hsl = convertRGBToHSL(
|
|
parseInt(needle_color_new.slice(0, 2), 16) / 255,
|
|
parseInt(needle_color_new.slice(2, 4), 16) / 255,
|
|
parseInt(needle_color_new.slice(4, 6), 16) / 255
|
|
);
|
|
|
|
this.#elements.needle.container.classList.toggle(CSVGGauge.ZBX_STYLE_NEEDLE_LIGHT, hsl[2] > 0.25);
|
|
this.#elements.needle.container.classList.toggle(CSVGGauge.ZBX_STYLE_NEEDLE_DARK, hsl[2] <= 0.25);
|
|
}
|
|
else {
|
|
this.#elements.needle.container.style.stroke = '';
|
|
this.#elements.needle.container.classList.remove(CSVGGauge.ZBX_STYLE_NEEDLE_LIGHT,
|
|
CSVGGauge.ZBX_STYLE_NEEDLE_DARK
|
|
);
|
|
}
|
|
}
|
|
|
|
this.#animate(this.#pos_current, pos_new,
|
|
(pos) => {
|
|
const angle = (pos - 0.5) * this.#config.angle;
|
|
|
|
if (this.#config.value.arc.show) {
|
|
this.#elements.value_arcs.value_arc.setAttribute('d',
|
|
this.#defineArc(-this.#config.angle / 2, angle, this.#elements.value_arcs.data.radius,
|
|
this.#elements.value_arcs.data.size
|
|
)
|
|
);
|
|
|
|
this.#elements.value_arcs.empty_arc.setAttribute('d',
|
|
this.#defineArc(angle, this.#config.angle / 2, this.#elements.value_arcs.data.radius,
|
|
this.#elements.value_arcs.data.size
|
|
)
|
|
);
|
|
}
|
|
|
|
if (this.#config.needle.show) {
|
|
this.#elements.needle.container.setAttribute('transform', `rotate(${angle}, 0, 1)`);
|
|
}
|
|
}
|
|
);
|
|
|
|
this.#pos_current = pos_new;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove created SVG element from the container.
|
|
*/
|
|
destroy() {
|
|
this.#svg.remove();
|
|
}
|
|
|
|
/**
|
|
* Create multi-line description.
|
|
*/
|
|
#createDescription() {
|
|
const container = document.createElementNS(CSVGGauge.SVG_NS, 'text');
|
|
|
|
this.#g.appendChild(container);
|
|
|
|
container.classList.add(CSVGGauge.ZBX_STYLE_DESCRIPTION);
|
|
|
|
if (this.#config.description.is_bold) {
|
|
container.style.fontWeight = 'bold';
|
|
}
|
|
|
|
if (this.#config.description.color !== '') {
|
|
container.style.fill = `#${this.#config.description.color}`;
|
|
}
|
|
|
|
const lines_data = [];
|
|
|
|
for (const text of this.#config.description.text.split('\r\n')) {
|
|
let line = null;
|
|
|
|
if (text !== '') {
|
|
line = document.createElementNS(CSVGGauge.SVG_NS, 'tspan');
|
|
|
|
container.appendChild(line);
|
|
}
|
|
|
|
lines_data.push({line, text});
|
|
}
|
|
|
|
this.#elements.description = {container, lines_data};
|
|
}
|
|
|
|
/**
|
|
* Create threshold arc, value arc or both whichever required by the widget configuration.
|
|
*/
|
|
#createArcs() {
|
|
const container = document.createElementNS(CSVGGauge.SVG_NS, 'g');
|
|
|
|
this.#g_scalable.appendChild(container);
|
|
|
|
container.classList.add(CSVGGauge.ZBX_STYLE_ARCS);
|
|
|
|
if (this.#config.thresholds.arc.show) {
|
|
const radius = 1;
|
|
const size = this.#config.thresholds.arc.size / 100;
|
|
|
|
const thresholds_arc_sectors = [];
|
|
|
|
let pos_start = 0;
|
|
let color = this.#config.empty_color;
|
|
|
|
for (const {color: color_next, value} of this.#config.thresholds.data) {
|
|
const pos_end = (value - this.#config.min) / (this.#config.max - this.#config.min);
|
|
|
|
thresholds_arc_sectors.push({pos_start, pos_end, color});
|
|
|
|
pos_start = pos_end;
|
|
color = color_next;
|
|
}
|
|
|
|
if (pos_start < 1) {
|
|
const pos_end = 1;
|
|
|
|
thresholds_arc_sectors.push({pos_start, pos_end, color});
|
|
}
|
|
|
|
for (const {pos_start, pos_end, color} of thresholds_arc_sectors) {
|
|
const angle_start = (pos_start - 0.5) * this.#config.angle;
|
|
const angle_end = (pos_end - 0.5) * this.#config.angle;
|
|
|
|
const arc = document.createElementNS(CSVGGauge.SVG_NS, 'path');
|
|
|
|
container.appendChild(arc);
|
|
|
|
arc.classList.add(CSVGGauge.ZBX_STYLE_THRESHOLDS_ARC_SECTOR);
|
|
|
|
arc.setAttribute('d', this.#defineArc(angle_start, angle_end, radius, size));
|
|
|
|
if (color !== '') {
|
|
arc.style.fill = `#${color}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.#config.value.arc.show) {
|
|
const radius = this.#config.thresholds.arc.show
|
|
? Math.max(0, 1 - (this.#config.thresholds.arc.size + CSVGGauge.ARCS_GAP) / 100)
|
|
: 1;
|
|
|
|
const size = Math.min(radius, this.#config.value.arc.size / 100);
|
|
|
|
const value_arc_sectors = [
|
|
{pos_start: 0, pos_end: 0, class_name: CSVGGauge.ZBX_STYLE_VALUE_ARC_SECTOR,
|
|
color: this.#config.value.arc.color
|
|
},
|
|
{pos_start: 0, pos_end: 1, class_name: CSVGGauge.ZBX_STYLE_EMPTY_ARC_SECTOR,
|
|
color: this.#config.empty_color
|
|
}
|
|
];
|
|
|
|
const value_arcs = [];
|
|
|
|
for (const {pos_start, pos_end, class_name, color} of value_arc_sectors) {
|
|
const angle_start = (pos_start - 0.5) * this.#config.angle;
|
|
const angle_end = (pos_end - 0.5) * this.#config.angle;
|
|
|
|
const arc = document.createElementNS(CSVGGauge.SVG_NS, 'path');
|
|
|
|
container.appendChild(arc);
|
|
|
|
arc.classList.add(class_name);
|
|
arc.setAttribute('d', this.#defineArc(angle_start, angle_end, radius, size));
|
|
|
|
if (color !== '') {
|
|
arc.style.fill = `#${color}`;
|
|
}
|
|
|
|
value_arcs.push(arc);
|
|
}
|
|
|
|
this.#elements.value_arcs = {value_arc: value_arcs[0], empty_arc: value_arcs[1], data: {radius, size}};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create and position needle, and point it to the min position.
|
|
*/
|
|
#createNeedle() {
|
|
const radius = CSVGGauge.NEEDLE_RADIUS / 100;
|
|
|
|
const length = this.#config.thresholds.arc.show
|
|
? 1 - this.#config.thresholds.arc.size / 2 / 100
|
|
: 1 - this.#config.value.arc.size / 2 / 100;
|
|
|
|
const container = document.createElementNS(CSVGGauge.SVG_NS, 'path');
|
|
|
|
this.#g_scalable.appendChild(container);
|
|
|
|
container.classList.add(CSVGGauge.ZBX_STYLE_NEEDLE);
|
|
|
|
container.setAttribute('d', [
|
|
'M', radius, 1,
|
|
'A', radius, radius, 0, 0, 1, -radius, 1,
|
|
'L', 0, 1 - length,
|
|
'Z'
|
|
].join(' '));
|
|
|
|
if (this.#config.needle.color !== '') {
|
|
container.style.fill = `#${this.#config.needle.color}`;
|
|
container.style.stroke = `#${this.#config.needle.color}`;
|
|
}
|
|
|
|
container.setAttribute('transform', `rotate(${-this.#config.angle / 2}, 0, 1)`);
|
|
|
|
this.#elements.needle = {container, data: {pos: 0}};
|
|
}
|
|
|
|
/**
|
|
* Create and position min/max and threshold labels.
|
|
*/
|
|
#createLabels() {
|
|
const scale_size = this.#config.scale.show ? this.#config.scale.size : CSVGGauge.SCALE_SIZE_DEFAULT;
|
|
const font_size = scale_size / 100;
|
|
const radius = 1 + font_size * CSVGGauge.LABEL_GAP / 100;
|
|
|
|
const labels_data = this.#config.thresholds.show_labels ? [...this.#config.thresholds.data] : [];
|
|
|
|
if (this.#config.scale.show) {
|
|
const do_add_min = labels_data.length === 0 || this.#config.min < labels_data[0].value;
|
|
const do_add_max = labels_data.length === 0 || this.#config.max > labels_data[labels_data.length - 1].value;
|
|
|
|
if (do_add_min) {
|
|
labels_data.push({value: this.#config.min, text: this.#config.scale.min_text});
|
|
}
|
|
|
|
if (do_add_max) {
|
|
labels_data.push({value: this.#config.max, text: this.#config.scale.max_text});
|
|
}
|
|
}
|
|
|
|
for (const {value, text} of labels_data) {
|
|
const pos = (value - this.#config.min) / (this.#config.max - this.#config.min);
|
|
const angle = Math.round((pos - 0.5) * this.#config.angle * 100) / 100;
|
|
|
|
const container = document.createElementNS(CSVGGauge.SVG_NS, 'text');
|
|
|
|
this.#g_scalable.appendChild(container);
|
|
|
|
container.classList.add(CSVGGauge.ZBX_STYLE_LABEL);
|
|
|
|
container.textContent = text;
|
|
container.style.fontSize = `${font_size}px`;
|
|
|
|
let {x, y} = this.#polarToCartesian(radius, angle);
|
|
|
|
if (this.#config.angle === 270 && Math.abs(angle) > 90) {
|
|
y += font_size * CSVGGauge.CAPITAL_HEIGHT;
|
|
|
|
const arcs_height = 1 + Math.sqrt(2) / 2;
|
|
|
|
if (y > arcs_height) {
|
|
x = Math.sqrt(radius ** 2 - (arcs_height - 1 - font_size * CSVGGauge.CAPITAL_HEIGHT) ** 2)
|
|
* Math.sign(angle);
|
|
y = arcs_height;
|
|
}
|
|
}
|
|
|
|
container.setAttribute('x', `${x}`);
|
|
container.setAttribute('y', `${y}`);
|
|
|
|
if (Math.abs(angle) <= 1) {
|
|
container.classList.add(CSVGGauge.ZBX_STYLE_LABEL_CENTER);
|
|
}
|
|
if (angle < -1) {
|
|
container.classList.add(CSVGGauge.ZBX_STYLE_LABEL_LEFT);
|
|
}
|
|
else {
|
|
container.classList.add(CSVGGauge.ZBX_STYLE_LABEL_RIGHT);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create and position containers for value and units.
|
|
*/
|
|
#createValueAndUnits() {
|
|
const container = document.createElementNS(CSVGGauge.SVG_NS, 'text');
|
|
|
|
this.#g_scalable.appendChild(container);
|
|
|
|
container.classList.add(CSVGGauge.ZBX_STYLE_VALUE_AND_UNITS);
|
|
|
|
const arcs_height = ((this.#config.thresholds.arc.show || this.#config.value.arc.show)
|
|
&& this.#config.angle === 270)
|
|
? 1 + Math.sqrt(2) / 2
|
|
: 1;
|
|
|
|
const is_aligned_to_bottom = (this.#config.thresholds.arc.show || this.#config.value.arc.show)
|
|
&& (this.#config.angle === 270 || !this.#config.needle.show);
|
|
|
|
const value_font_size = this.#config.value.size / 100;
|
|
const value_container = document.createElementNS(CSVGGauge.SVG_NS, 'tspan');
|
|
|
|
value_container.classList.add(CSVGGauge.ZBX_STYLE_VALUE);
|
|
value_container.style.fontSize = `${value_font_size}px`;
|
|
|
|
if (this.#config.value.is_bold) {
|
|
value_container.style.fontWeight = 'bold';
|
|
}
|
|
|
|
if (this.#config.value.color) {
|
|
value_container.style.fill = `#${this.#config.value.color}`;
|
|
}
|
|
|
|
this.#elements.value_and_units = {container, value: {container: value_container}};
|
|
|
|
if (this.#config.units.show) {
|
|
const units_font_size = this.#config.units.size / 100;
|
|
const units_container = document.createElementNS(CSVGGauge.SVG_NS, 'tspan');
|
|
|
|
units_container.classList.add(CSVGGauge.ZBX_STYLE_UNITS);
|
|
units_container.style.fontSize = `${units_font_size}px`;
|
|
|
|
if (this.#config.units.is_bold) {
|
|
units_container.style.fontWeight = 'bold';
|
|
}
|
|
|
|
if (this.#config.units.color) {
|
|
units_container.style.fill = `#${this.#config.units.color}`;
|
|
}
|
|
|
|
switch (this.#config.units.position) {
|
|
case CSVGGauge.UNITS_POSITION_BEFORE:
|
|
case CSVGGauge.UNITS_POSITION_AFTER:
|
|
const space_font_size = Math.min(value_font_size, units_font_size);
|
|
const space_container = document.createElementNS(CSVGGauge.SVG_NS, 'tspan');
|
|
|
|
space_container.style.fontSize = `${space_font_size}px`;
|
|
space_container.textContent = ' ';
|
|
|
|
if (this.#config.units.position === CSVGGauge.UNITS_POSITION_BEFORE) {
|
|
container.appendChild(units_container);
|
|
container.appendChild(space_container);
|
|
container.appendChild(value_container);
|
|
}
|
|
else {
|
|
container.appendChild(value_container);
|
|
container.appendChild(space_container);
|
|
container.appendChild(units_container);
|
|
}
|
|
|
|
if (is_aligned_to_bottom) {
|
|
container.setAttribute('y', `${arcs_height}`);
|
|
}
|
|
else {
|
|
const max_font_size = Math.max(value_font_size, units_font_size);
|
|
|
|
container.setAttribute('y', `${arcs_height + CSVGGauge.NEEDLE_RADIUS / 100 * 2
|
|
+ max_font_size * (CSVGGauge.CAPITAL_HEIGHT + CSVGGauge.NEEDLE_GAP / 100)
|
|
}`);
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
const parts = this.#config.units.position === CSVGGauge.UNITS_POSITION_BELOW
|
|
? [value_container, units_container]
|
|
: [units_container, value_container];
|
|
|
|
const parts_font_size = this.#config.units.position === CSVGGauge.UNITS_POSITION_BELOW
|
|
? [value_font_size, units_font_size]
|
|
: [units_font_size, value_font_size];
|
|
|
|
container.appendChild(parts[0]);
|
|
container.appendChild(parts[1]);
|
|
|
|
parts[1].setAttribute('x', '0');
|
|
|
|
if (is_aligned_to_bottom) {
|
|
parts[0].setAttribute('y', `${arcs_height
|
|
- parts_font_size[1] * CSVGGauge.CAPITAL_HEIGHT
|
|
- parts_font_size[1] * (1 - CSVGGauge.TEXT_BASELINE)
|
|
- parts_font_size[0] * (1 - CSVGGauge.TEXT_BASELINE)
|
|
}`);
|
|
parts[1].setAttribute('y', `${arcs_height}`);
|
|
}
|
|
else {
|
|
const y_top = arcs_height + CSVGGauge.NEEDLE_RADIUS / 100 * 2
|
|
+ parts_font_size[0] * (CSVGGauge.CAPITAL_HEIGHT + CSVGGauge.NEEDLE_GAP / 100);
|
|
|
|
parts[0].setAttribute('y', `${y_top}`);
|
|
parts[1].setAttribute('y', `${y_top
|
|
+ parts_font_size[1] * CSVGGauge.CAPITAL_HEIGHT
|
|
+ parts_font_size[1] * (1 - CSVGGauge.TEXT_BASELINE)
|
|
+ parts_font_size[0] * (1 - CSVGGauge.TEXT_BASELINE)
|
|
}`);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
this.#elements.value_and_units.units = {container: units_container};
|
|
}
|
|
else {
|
|
container.appendChild(value_container);
|
|
|
|
if (is_aligned_to_bottom) {
|
|
container.setAttribute('y', `${arcs_height}`);
|
|
}
|
|
else {
|
|
container.setAttribute('y', `${arcs_height + CSVGGauge.NEEDLE_RADIUS / 100 * 2
|
|
+ value_font_size * (CSVGGauge.CAPITAL_HEIGHT + CSVGGauge.NEEDLE_GAP / 100)
|
|
}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create and position "No data" container.
|
|
*/
|
|
#createNoData() {
|
|
const container = document.createElementNS(CSVGGauge.SVG_NS, 'text');
|
|
|
|
this.#g_scalable.appendChild(container);
|
|
|
|
container.classList.add(CSVGGauge.ZBX_STYLE_NO_DATA);
|
|
|
|
const font_size = this.#config.value.size / 100;
|
|
|
|
container.style.fontSize = `${font_size}px`;
|
|
|
|
if (this.#config.value.is_bold) {
|
|
container.style.fontWeight = 'bold';
|
|
}
|
|
|
|
const arcs_height = ((this.#config.thresholds.arc.show || this.#config.value.arc.show)
|
|
&& this.#config.angle === 270)
|
|
? 1 + Math.sqrt(2) / 2
|
|
: 1;
|
|
|
|
const is_aligned_to_bottom = (this.#config.thresholds.arc.show || this.#config.value.arc.show)
|
|
&& (this.#config.angle === 270 || !this.#config.needle.show);
|
|
|
|
if (is_aligned_to_bottom) {
|
|
container.setAttribute('y', `${arcs_height}`);
|
|
}
|
|
else {
|
|
container.setAttribute('y', `${arcs_height + CSVGGauge.NEEDLE_RADIUS / 100 * 2
|
|
+ font_size * (CSVGGauge.CAPITAL_HEIGHT + CSVGGauge.NEEDLE_GAP / 100)
|
|
}`);
|
|
}
|
|
|
|
this.#elements.no_data = {container};
|
|
}
|
|
|
|
/**
|
|
* Get bounding box of the scalable group.
|
|
*
|
|
* @returns {SVGRect}
|
|
*/
|
|
#getScalableBBox() {
|
|
const value_text = this.#elements.value_and_units.value.container.textContent;
|
|
|
|
const units_text = this.#config.units.show
|
|
? this.#elements.value_and_units.units.container.textContent
|
|
: null;
|
|
|
|
const no_data_text = this.#elements.no_data.container.textContent;
|
|
|
|
this.#elements.value_and_units.value.container.innerHTML = '█';
|
|
|
|
if (this.#config.units.show) {
|
|
this.#elements.value_and_units.units.container.innerHTML = '█';
|
|
}
|
|
|
|
this.#elements.no_data.container.innerHTML = '█';
|
|
|
|
const scalable_bbox = this.#g_scalable.getBBox();
|
|
|
|
this.#elements.value_and_units.value.container.textContent = value_text;
|
|
|
|
if (this.#config.units.show) {
|
|
this.#elements.value_and_units.units.container.textContent = units_text;
|
|
}
|
|
|
|
this.#elements.no_data.container.textContent = no_data_text;
|
|
|
|
return scalable_bbox;
|
|
}
|
|
|
|
/**
|
|
* Define arc path.
|
|
*
|
|
* @param {number} angle_start Start angle in degrees, zero pointing to the top.
|
|
* @param {number} angle_end Start angle in degrees, zero pointing to the top.
|
|
* @param {number} radius Arc outer radius.
|
|
* @param {number} size Arc size (thickness).
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
#defineArc(angle_start, angle_end, radius, size) {
|
|
const inner_start = this.#polarToCartesian(radius - size, angle_end);
|
|
const inner_end = this.#polarToCartesian(radius - size, angle_start);
|
|
const outer_start = this.#polarToCartesian(radius, angle_end);
|
|
const outer_end = this.#polarToCartesian(radius, angle_start);
|
|
|
|
const large_arc_flag = angle_end - angle_start <= 180 ? 0 : 1;
|
|
|
|
return [
|
|
'M', outer_start.x, outer_start.y,
|
|
'A', radius, radius, 0, large_arc_flag, 0, outer_end.x, outer_end.y,
|
|
'L', inner_end.x, inner_end.y,
|
|
'A', radius - size, radius - size, 0, large_arc_flag, 1, inner_start.x, inner_start.y,
|
|
'Z'
|
|
].join(' ');
|
|
}
|
|
|
|
/**
|
|
* Get X, Y coordinates out of radius and angle in degrees.
|
|
*
|
|
* @param {number} radius
|
|
* @param {number} angle_in_degrees Zero pointing to the top.
|
|
*
|
|
* @returns {{x: number, y: number}}
|
|
*/
|
|
#polarToCartesian(radius, angle_in_degrees) {
|
|
const angle_in_radians = this.#degreesToRadians(angle_in_degrees);
|
|
|
|
return {
|
|
x: radius * Math.cos(angle_in_radians),
|
|
y: 1 + radius * Math.sin(angle_in_radians)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get radians out of degrees.
|
|
*
|
|
* @param {number} degrees
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
#degreesToRadians(degrees) {
|
|
return (degrees - 90) * Math.PI / 180;
|
|
}
|
|
|
|
/**
|
|
* Position description according to the size of widget and truncate the text matching the available width.
|
|
*/
|
|
#drawDescription() {
|
|
const {container, lines_data} = this.#elements.description;
|
|
|
|
const line_height = this.#height * this.#config.description.size / 100;
|
|
const font_size = line_height / CSVGGauge.LINE_HEIGHT;
|
|
|
|
container.style.fontSize = `${font_size}px`;
|
|
|
|
let offset = 0;
|
|
|
|
for (const {line, text} of lines_data) {
|
|
if (text === '') {
|
|
offset++;
|
|
|
|
continue;
|
|
}
|
|
|
|
line.setAttribute('x', `${this.#width / 2}`);
|
|
line.setAttribute('dy', `${offset * line_height}`);
|
|
|
|
line.textContent = text;
|
|
|
|
while (line.getComputedTextLength() > this.#width && line.textContent.length >= 4) {
|
|
line.textContent = `${line.textContent.slice(0, -4)}...`;
|
|
}
|
|
|
|
offset = 1;
|
|
}
|
|
|
|
container.setAttribute('y', this.#config.description.position === CSVGGauge.DESC_V_POSITION_TOP
|
|
? `${font_size * CSVGGauge.TEXT_BASELINE + (line_height - font_size) / 2}`
|
|
: `${this.#height + font_size * (CSVGGauge.TEXT_BASELINE - 1/2) + line_height * (1/2 - lines_data.length)}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Animate numeric value smoothly within the defined time period, within the given interval.
|
|
*
|
|
* @param {number} from
|
|
* @param {number} to
|
|
* @param {function} callback Callback function to be called with value transitioning within the interval.
|
|
*/
|
|
#animate(from, to, callback) {
|
|
const start_time = Date.now();
|
|
const end_time = start_time + CSVGGauge.ANIMATE_DURATION;
|
|
|
|
const animate = () => {
|
|
const time = Date.now();
|
|
|
|
if (time <= end_time) {
|
|
const progress = (time - start_time) / (end_time - start_time);
|
|
const smooth_progress = 0.5 + Math.sin(Math.PI * (progress - 0.5)) / 2;
|
|
|
|
callback(from + (to - from) * smooth_progress);
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
else {
|
|
callback(to);
|
|
}
|
|
};
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
/**
|
|
* Get unique ID.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
static #getUniqueId() {
|
|
return `CSVGGauge-${this.ID_COUNTER++}`;
|
|
}
|
|
}
|