mirror of
				https://github.com/beestat/app.git
				synced 2025-11-03 18:37:01 -05: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;
 | 
						|
};
 |