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.
zabbix/ui/js/component.z-bar-gauge.js

291 lines
7.1 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.
**/
const BAR_GAUGE_BAR_DEFAULT_COLOR = '#000000';
const BAR_GAUGE_BAR_DEFAULT_MIN = 0;
const BAR_GAUGE_BAR_DEFAULT_MAX = 100;
const BAR_GAUGE_BAR_ITEM_WIDTH = 7;
class ZBarGauge extends HTMLElement {
constructor() {
super();
this._refresh_frame = null;
this._fill = BAR_GAUGE_BAR_DEFAULT_COLOR;
this._solid = false;
this._min = 0;
this._max = 100;
this._value = 0;
const shadow = this.attachShadow({mode: 'open'});
this._canvas = document.createElement('canvas');
this._canvas.setAttribute('part', 'bar');
shadow.appendChild(this._canvas);
this.registerEvents();
}
connectedCallback() {
setTimeout(() => {
this._events.update();
});
}
disconnectedCallback() {
this.unregisterEvents();
}
static get observedAttributes() {
return ['fill', 'max', 'min', 'solid', 'value', 'width'];
}
attributeChangedCallback(name, old_value, new_value) {
if (old_value === new_value) {
return;
}
switch (name) {
case 'fill':
this._fill = (new_value !== null && /^#([0-9A-F]{6})$/i.test(new_value))
? new_value
: BAR_GAUGE_BAR_DEFAULT_COLOR;
return this._events.update();
case 'max':
this._max = new_value !== null && !isNaN(new_value) ? Number(new_value) : BAR_GAUGE_BAR_DEFAULT_MAX;
break
case 'min':
this._min = new_value !== null && !isNaN(new_value) ? Number(new_value) : BAR_GAUGE_BAR_DEFAULT_MIN;
break;
case 'solid':
this._solid = new_value !== null;
break;
case 'value':
if (new_value !== null) {
this._value = Number(new_value);
this.dispatchEvent(new Event('change', {bubbles: true}));
}
break;
case 'width':
if (new_value === null) {
this.style.width = '';
}
else if (!isNaN(new_value)) {
this.style.width = `${new_value}px`;
}
else {
this.style.width = new_value;
}
break;
default:
return;
}
this._refresh();
}
get max() {
return this.getAttribute('max');
}
set max(max) {
this.setAttribute('max', max);
}
get min() {
return this.getAttribute('min');
}
set min(min) {
this.setAttribute('min', min);
}
get value() {
return this.getAttribute('value');
}
set value(value) {
this.setAttribute('value', value);
}
get width() {
return this.getAttribute('width');
}
set width(width) {
this.setAttribute('width', width);
}
addThreshold(value, fill) {
const threshold = document.createElement('threshold');
threshold.setAttribute('value', value);
threshold.setAttribute('fill', fill);
this.appendChild(threshold);
}
_refresh() {
if (this._refresh_frame !== null || this._thresholds === undefined) {
return;
}
this._refresh_frame = window.requestAnimationFrame(() => {
this._refresh_frame = null;
const ctx = this._canvas.getContext("2d");
const width = this.offsetWidth - 2;
this._canvas.height = this.offsetHeight - 2;
const value = Math.max(this._min, Math.min(this._max, this._value));
if (this._solid) {
const bar_size = value > this._min
? Math.max(Math.floor(width / (this._max - this._min) * (value - this._min)), 2)
: 0;
this._canvas.width = width + 2;
this._drawCell(ctx, 1, bar_size, this._getThresholdColorByValue(this._value), 1);
}
else {
const cell_count = Math.floor(width / BAR_GAUGE_BAR_ITEM_WIDTH);
const cell_interval = (this._max - this._min) / cell_count;
this._canvas.width = cell_count * BAR_GAUGE_BAR_ITEM_WIDTH + 1;
for (let i = 0; i < cell_count; i++) {
const alpha = (value - this._min) / cell_interval > i ? 1 : .25;
this._drawCell(ctx, i * BAR_GAUGE_BAR_ITEM_WIDTH + 1, BAR_GAUGE_BAR_ITEM_WIDTH - 1,
this._getThresholdColorByValue(i * cell_interval + this._min), alpha
);
}
}
});
}
_drawCell(ctx, x, width, color, alpha) {
const rgb = this._hexToRgb(color);
const rgb_lighten = this._colorLightenDarken(rgb, .5);
const rgb_darken = this._colorLightenDarken(rgb, -.3);
const fill = ctx.createLinearGradient(x, 1, x, this._canvas.height - 2);
fill.addColorStop(0, `rgba(${rgb_lighten.r}, ${rgb_lighten.g}, ${rgb_lighten.b}, ${alpha}`);
fill.addColorStop(.3, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha}`);
fill.addColorStop(.8, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha}`);
fill.addColorStop(1, `rgba(${rgb_darken.r}, ${rgb_darken.g}, ${rgb_darken.b}, ${alpha}`);
ctx.fillStyle = fill;
this._roundRect(ctx, x, 1, width, this._canvas.height - 2, 3);
ctx.fill();
}
_roundRect(ctx, x, y, width, height, radius) {
radius = Math.min(width / 2, radius);
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + width, y, x + width, y + height, radius);
ctx.arcTo(x + width, y + height, x, y + height, radius);
ctx.arcTo(x, y + height, x, y, radius);
ctx.arcTo(x, y, x + width, y, radius);
ctx.closePath();
}
_getThresholdColorByValue(value) {
let color = this._fill;
for (const threshold of Object.keys(this._thresholds).sort((a, b) => (a - b))) {
if (threshold <= value) {
color = this._thresholds[threshold];
}
}
return color;
}
_hexToRgb(hex) {
const rgb = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return {
r: rgb ? parseInt(rgb[1], 16) : 0,
g: rgb ? parseInt(rgb[2], 16) : 0,
b: rgb ? parseInt(rgb[3], 16) : 0
};
}
_colorLightenDarken(rgb, amount) {
return {
r: Math.max(0, Math.min(255, rgb.r + amount * (amount > 0 ? 255 - rgb.r : rgb.r))),
g: Math.max(0, Math.min(255, rgb.g + amount * (amount > 0 ? 255 - rgb.g : rgb.g))),
b: Math.max(0, Math.min(255, rgb.b + amount * (amount > 0 ? 255 - rgb.b : rgb.b)))
};
}
registerEvents() {
this._events = {
resize: () => {
this._refresh();
},
update: () => {
this._thresholds = {};
for (const threshold of this.querySelectorAll('threshold')) {
if (threshold.hasAttribute('fill') && threshold.hasAttribute('value')) {
this._thresholds[threshold.getAttribute('value')] = threshold.getAttribute('fill');
}
}
this._refresh();
}
}
this._events_data = {
resize_observer: new ResizeObserver(this._events.resize),
mutation_observer: new MutationObserver(this._events.update)
}
this._events_data.resize_observer.observe(this);
this._events_data.mutation_observer.observe(this, {childList: true});
}
unregisterEvents() {
this._events_data.resize_observer.disconnect();
this._events_data.mutation_observer.disconnect();
cancelAnimationFrame(this._refresh_frame);
}
}
customElements.define('z-bar-gauge', ZBarGauge);