/** * Generic customizable metric. */ beestat.component.metric = function() { beestat.component.apply(this, arguments); }; beestat.extend(beestat.component.metric, beestat.component); /** * Whether or not this is an area value. If so, do the appropriate conversion * on display. * * @type {boolean} */ beestat.component.metric.prototype.is_area_ = false; /** * 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_ }); } else if (self.is_area_ === true) { return_value = beestat.area({ 'area': value }); } 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; };