mirror of
https://github.com/beestat/app.git
synced 2025-05-24 02:14:03 -04:00
422 lines
11 KiB
JavaScript
422 lines
11 KiB
JavaScript
/**
|
|
* Generic customizable metric.
|
|
*/
|
|
beestat.component.metric = function() {
|
|
beestat.component.apply(this, arguments);
|
|
};
|
|
beestat.extend(beestat.component.metric, beestat.component);
|
|
|
|
/**
|
|
* Whether or not this is a temperature value. If so, do the appropriate
|
|
* conversion on display.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
beestat.component.metric.prototype.is_temperature_ = false;
|
|
|
|
/**
|
|
* Whether or not this temperature value is a delta instead of an absolute
|
|
* value. If so, do the appropriate conversion on display.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
beestat.component.metric.prototype.is_temperature_delta_ = false;
|
|
|
|
/**
|
|
* Decorate
|
|
*
|
|
* @param {rocket.Elements} parent
|
|
*/
|
|
beestat.component.metric.prototype.decorate_ = function(parent) {
|
|
const self = this;
|
|
const metric = this.get_metric_();
|
|
|
|
// Construct the table
|
|
var table = $.createElement('table').style('width', '100%');
|
|
table.setAttribute({
|
|
'cellpadding': '0',
|
|
'cellspacing': '0'
|
|
});
|
|
parent.appendChild(table);
|
|
|
|
var tr = $.createElement('tr');
|
|
table.appendChild(tr);
|
|
var td_icon = $.createElement('td')
|
|
.style({
|
|
'width': '36px',
|
|
'text-align': 'center',
|
|
'background': this.get_color_()
|
|
});
|
|
tr.appendChild(td_icon);
|
|
|
|
var td_title = $.createElement('td')
|
|
.style({
|
|
'width': '100px',
|
|
'background': this.get_color_(),
|
|
'color': '#fff'
|
|
});
|
|
tr.appendChild(td_title);
|
|
|
|
var td_chart = $.createElement('td')
|
|
.style({
|
|
'text-align': 'right'
|
|
});
|
|
tr.appendChild(td_chart);
|
|
|
|
// Fill in the content.
|
|
(new beestat.component.icon(this.get_icon_()))
|
|
.set_color('#fff')
|
|
.render(td_icon);
|
|
|
|
td_title.appendChild($.createElement('div').innerText(this.get_title_() + ' '));
|
|
|
|
td_title.appendChild(
|
|
$.createElement('div')
|
|
.innerText(this.get_histogram_sum_().toLocaleString() + ' others')
|
|
);
|
|
|
|
var chart_container = $.createElement('div').style({
|
|
'position': 'relative',
|
|
'height': '60px',
|
|
'user-select': 'none'
|
|
});
|
|
td_chart.appendChild(chart_container);
|
|
|
|
var formatter = this.get_formatter_();
|
|
|
|
const chart_height = 60;
|
|
const chart_padding = 20;
|
|
const chart = $.createElement('div').style({
|
|
'height': chart_height + 'px',
|
|
'padding-top': chart_padding + 'px',
|
|
'position': 'relative'
|
|
});
|
|
chart_container.appendChild(chart);
|
|
|
|
var histogram = this.get_histogram_();
|
|
var histogram_mode = this.get_histogram_mode_();
|
|
var column_width = (100 / histogram.length);
|
|
|
|
let my_column_index;
|
|
let my_column_height;
|
|
let sum_less = 0;
|
|
let sum_more = 0;
|
|
histogram.forEach(function(data, i) {
|
|
const height = (data.count / histogram_mode * 100);
|
|
const column = $.createElement('div').style({
|
|
'display': 'inline-block',
|
|
'width': column_width + '%',
|
|
'height': height + '%'
|
|
});
|
|
|
|
const value = self.get_value_();
|
|
if (
|
|
value >= data.value &&
|
|
value < data.value + metric.interval
|
|
) {
|
|
column.style({
|
|
'background': '#516169',
|
|
'position': 'relative',
|
|
|
|
// Makes it visible even if it's 0px high.
|
|
'border-bottom': '2px solid #516169'
|
|
});
|
|
my_column_index = i;
|
|
my_column_height = height;
|
|
} else {
|
|
if (my_column_index === undefined) {
|
|
sum_less += data.count;
|
|
} else {
|
|
sum_more += data.count;
|
|
}
|
|
column.style({
|
|
'background': beestat.style.color.bluegray.light,
|
|
'border-bottom': '2px solid ' + beestat.style.color.bluegray.light
|
|
});
|
|
}
|
|
|
|
chart.appendChild(column);
|
|
});
|
|
|
|
const label_height = 16;
|
|
const label_bottom = Math.max(20, ((chart_height - chart_padding) * my_column_height / 100));
|
|
var label = $.createElement('div')
|
|
.innerText(formatter(this.get_value_(), this.get_precision_()))
|
|
.style({
|
|
'position': 'absolute',
|
|
'bottom': label_bottom + 'px',
|
|
'left': Math.min(85, ((my_column_index * column_width) + (column_width / 2))) + '%',
|
|
'width': '60px',
|
|
'height': label_height + 'px',
|
|
'line-height': label_height + 'px',
|
|
'margin-left': '-30px',
|
|
'text-align': 'center',
|
|
'font-weight': beestat.style.font_weight.bold,
|
|
'text-shadow': '1px 1px 1px rgba(0, 0, 0, 0.5)'
|
|
});
|
|
chart.appendChild(label);
|
|
|
|
// Min & Max
|
|
chart.appendChild(
|
|
$.createElement('div')
|
|
.style({
|
|
'position': 'absolute',
|
|
'left': '4px',
|
|
'bottom': '2px',
|
|
'color': 'rgba(255, 255, 255, 0.5)',
|
|
'font-size': '11px'
|
|
})
|
|
.innerText(formatter(this.get_min_(), this.get_precision_()))
|
|
);
|
|
chart.appendChild(
|
|
$.createElement('div')
|
|
.style({
|
|
'position': 'absolute',
|
|
'right': '4px',
|
|
'bottom': '2px',
|
|
'color': 'rgba(255, 255, 255, 0.5)',
|
|
'font-size': '11px'
|
|
})
|
|
.innerText(formatter(this.get_max_(), this.get_precision_()))
|
|
);
|
|
|
|
// Greater or less than % label
|
|
const percentage_label = $.createElement('div');
|
|
|
|
let percentage;
|
|
let symbol;
|
|
if (sum_less >= sum_more) {
|
|
symbol = '>';
|
|
percentage = sum_less / this.get_histogram_sum_();
|
|
} else {
|
|
symbol = '<';
|
|
percentage = sum_more / this.get_histogram_sum_();
|
|
}
|
|
|
|
percentage_label.innerText(
|
|
symbol + ' ' + (percentage * 100).toFixed(0) + '% homes'
|
|
);
|
|
td_title.appendChild(percentage_label);
|
|
};
|
|
|
|
/**
|
|
* Get the largest histogram count.
|
|
*
|
|
* @return {number} The largest histogram count.
|
|
*/
|
|
beestat.component.metric.prototype.get_histogram_mode_ = function() {
|
|
let mode = -Infinity;
|
|
this.get_histogram_().forEach(function(data) {
|
|
mode = Math.max(mode, data.count);
|
|
});
|
|
return mode;
|
|
};
|
|
|
|
/**
|
|
* Get the sum of the histogram counts.
|
|
*
|
|
* @return {number} The sum of the histogram counts.
|
|
*/
|
|
beestat.component.metric.prototype.get_histogram_sum_ = function() {
|
|
let sum = 0;
|
|
this.get_histogram_().forEach(function(data) {
|
|
sum += data.count;
|
|
});
|
|
return sum;
|
|
};
|
|
|
|
/**
|
|
* Get the unit string to append to the end of the value.
|
|
*
|
|
* @return {mixed} The unit string.
|
|
*/
|
|
beestat.component.metric.prototype.get_units_ = function() {
|
|
return '';
|
|
};
|
|
|
|
/**
|
|
* Get the a formatter function that applies a transformation to the value.
|
|
*
|
|
* @return {mixed} A function that formats the string.
|
|
*/
|
|
beestat.component.metric.prototype.get_formatter_ = function() {
|
|
var self = this;
|
|
|
|
return function(value) {
|
|
let return_value = value;
|
|
if (self.is_temperature_ === true) {
|
|
return_value = beestat.temperature({
|
|
'temperature': value,
|
|
'delta': self.is_temperature_delta_
|
|
});
|
|
}
|
|
return return_value.toFixed(self.get_precision_()) + self.get_units_();
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Get the minimum value of this metric (within two standard deviations). Then
|
|
* make it go to the min of that and the actual value.
|
|
*
|
|
* @return {mixed} The minimum value of this metric.
|
|
*/
|
|
beestat.component.metric.prototype.get_min_ = function() {
|
|
const metric = this.get_metric_();
|
|
const cutoff_min = this.get_cutoff_min_();
|
|
|
|
// Median minus 2 * standard deviation
|
|
let min = (metric.median - (metric.standard_deviation * 2));
|
|
|
|
// If lower than the cutoff, place at the cutoff
|
|
if (cutoff_min !== null) {
|
|
min = Math.max(min, cutoff_min);
|
|
}
|
|
|
|
// Unless the thermostat value is lower than the cutoff, then go there
|
|
min = Math.min(min, this.get_value_());
|
|
|
|
// Round down to the nearest interval
|
|
min = this.round_(min, 'floor');
|
|
|
|
return min;
|
|
};
|
|
|
|
/**
|
|
* Get the maximum value of this metric (within two standard deviations). Then
|
|
* make it go to the max of that and the actual value.
|
|
*
|
|
* @return {mixed} The maximum value of this metric.
|
|
*/
|
|
beestat.component.metric.prototype.get_max_ = function() {
|
|
const metric = this.get_metric_();
|
|
const cutoff_max = this.get_cutoff_max_();
|
|
|
|
// Median plus 2 * standard deviation
|
|
let max = (metric.median + (metric.standard_deviation * 2));
|
|
|
|
// If higher than the cutoff, place at the cutoff
|
|
if (cutoff_max !== null) {
|
|
max = Math.min(max, cutoff_max);
|
|
}
|
|
|
|
// Unless the thermostat value is higher than the cutoff, then go there
|
|
max = Math.max(max, this.get_value_());
|
|
|
|
// Round up to the nearest interval
|
|
max = this.round_(max, 'ceil');
|
|
|
|
return max;
|
|
};
|
|
|
|
/**
|
|
* Get max cutoff. This is used to set the chart min to max(median - 2 *
|
|
* stddev, max cutoff).
|
|
*
|
|
* @return {object} The cutoff value.
|
|
*/
|
|
beestat.component.metric.prototype.get_cutoff_min_ = function() {
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Get max cutoff. This is used to set the chart max to min(median + 2 *
|
|
* stddev, max cutoff).
|
|
*
|
|
* @return {object} The cutoff value.
|
|
*/
|
|
beestat.component.metric.prototype.get_cutoff_max_ = function() {
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Get the value of this metric.
|
|
*
|
|
* @return {mixed} The value of this metric.
|
|
*/
|
|
beestat.component.metric.prototype.get_value_ = function() {
|
|
const thermostat = beestat.cache.thermostat[this.thermostat_id_];
|
|
|
|
if (this.child_metric_name_ !== undefined) {
|
|
return this.round_(
|
|
thermostat.profile[this.parent_metric_name_][this.child_metric_name_]
|
|
);
|
|
}
|
|
|
|
return this.round_(thermostat.profile[this.parent_metric_name_]);
|
|
};
|
|
|
|
/**
|
|
* Get the actual metric object as returned from thermostat->get_metrics().
|
|
*
|
|
* @return {array} THe metric object.
|
|
*/
|
|
beestat.component.metric.prototype.get_metric_ = function() {
|
|
if (this.child_metric_name_ !== undefined) {
|
|
return beestat.cache.data.metrics[this.parent_metric_name_][this.child_metric_name_];
|
|
}
|
|
|
|
return beestat.cache.data.metrics[this.parent_metric_name_];
|
|
};
|
|
|
|
/**
|
|
* Take the histogram returned from the API, fill in missing values, and
|
|
* remove anything outside the min and max.
|
|
*
|
|
* @return {array} Histogram data.
|
|
*/
|
|
beestat.component.metric.prototype.get_histogram_ = function() {
|
|
const metric = this.get_metric_();
|
|
|
|
const min = this.get_min_();
|
|
const max = this.get_max_();
|
|
|
|
const my_value = this.get_value_();
|
|
|
|
var histogram = [];
|
|
for (let value = min; value <= max; value += metric.interval) {
|
|
let count = metric.histogram[value.toFixed(this.get_precision_())] || 0;
|
|
|
|
// The API call does not include me in the histogram; add it here.
|
|
if (value.toFixed(this.get_precision_()) === my_value.toFixed(this.get_precision_())) {
|
|
count++;
|
|
}
|
|
|
|
histogram.push({
|
|
'value': value,
|
|
'count': count
|
|
});
|
|
}
|
|
|
|
return histogram;
|
|
};
|
|
|
|
/**
|
|
* Based on the interval, get the precision.
|
|
*
|
|
* @return {number} The precision.
|
|
*/
|
|
beestat.component.metric.prototype.get_precision_ = function() {
|
|
const metric = this.get_metric_();
|
|
if (Math.floor(metric.interval) === metric.interval) {
|
|
return 0;
|
|
}
|
|
return metric.interval.toString().split('.')[1].length || 0;
|
|
};
|
|
|
|
/**
|
|
* Round a number to the precision that this metric supports. Useful, for
|
|
* example, because the profile is sometimes a higher precision than the
|
|
* metric uses for display purposes.
|
|
*
|
|
* @param {number} value The value to round.
|
|
* @param {string} mode The math function to use when rounding. Default round,
|
|
* can also choose floor or ceil.
|
|
*
|
|
* @return {number} The rounded value.
|
|
*/
|
|
beestat.component.metric.prototype.round_ = function(value, mode) {
|
|
const metric = this.get_metric_();
|
|
const math_function = (mode === undefined) ? 'round' : mode;
|
|
return Math[math_function](value / metric.interval) * metric.interval;
|
|
};
|