1
0
mirror of https://github.com/beestat/app.git synced 2025-05-24 02:14:03 -04:00
beestat/js/component/card/temperature_profiles.js
2019-10-16 20:46:41 -04:00

416 lines
12 KiB
JavaScript

/**
* Temperature profiles.
*/
beestat.component.card.temperature_profiles = function() {
var self = this;
beestat.dispatcher.addEventListener('cache.data.comparison_temperature_profile', function() {
self.rerender();
});
beestat.component.card.apply(this, arguments);
this.layer_.register_loader(beestat.generate_temperature_profile);
};
beestat.extend(beestat.component.card.temperature_profiles, beestat.component.card);
/**
* Decorate card.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.temperature_profiles.prototype.decorate_contents_ = function(parent) {
var self = this;
this.chart_ = new beestat.component.chart();
this.chart_.options.chart.height = 300;
if (
beestat.cache.data.comparison_temperature_profile === undefined
) {
this.chart_.render(parent);
this.show_loading_('Calculating');
} else {
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
// var x_categories = [];
var trendlines = {};
var raw = {};
// Global x range.
var x_min = Infinity;
var x_max = -Infinity;
var y_min = Infinity;
var y_max = -Infinity;
for (var type in beestat.cache.data.comparison_temperature_profile) {
// Cloned because I mutate this data for temperature conversions.
var profile = beestat.clone(
beestat.cache.data.comparison_temperature_profile[type]
);
if (profile !== null) {
// Convert the data to Celsius if necessary
var deltas_converted = {};
for (var key in profile.deltas) {
deltas_converted[beestat.temperature({'temperature': (key / 10)})] =
beestat.temperature({
'temperature': (profile.deltas[key] / 10),
'delta': true,
'round': 3
});
}
profile.deltas = deltas_converted;
var linear_trendline = this.get_linear_trendline_(profile.deltas);
var min_max_keys = Object.keys(profile.deltas);
// This specific trendline x range.
var this_x_min = Math.min.apply(null, min_max_keys);
var this_x_max = Math.max.apply(null, min_max_keys);
// Global x range.
x_min = Math.min(x_min, this_x_min);
x_max = Math.max(x_max, this_x_max);
trendlines[type] = [];
raw[type] = [];
/**
* Data is stored internally as °F with 1 value per degree. That data
* gets converted to °C which then requires additional precision
* (increment).
*
* The additional precision introduces floating point error, so
* convert the x value to a fixed string.
*
* The string then needs converted to a number for highcharts, so
* later on use parseFloat to get back to that.
*
* Stupid Celsius.
*/
var increment;
var fixed;
if (thermostat.temperature_unit === '°F') {
increment = 1;
fixed = 0;
} else {
increment = 0.1;
fixed = 1;
}
for (var x = this_x_min; x <= this_x_max; x += increment) {
var x_fixed = x.toFixed(fixed);
var y = (linear_trendline.slope * x_fixed) +
linear_trendline.intercept;
trendlines[type].push([
parseFloat(x_fixed),
y
]);
if (profile.deltas[x_fixed] !== undefined) {
raw[type].push([
parseFloat(x_fixed),
profile.deltas[x_fixed]
]);
y_min = Math.min(y_min, profile.deltas[x_fixed]);
y_max = Math.max(y_max, profile.deltas[x_fixed]);
}
}
}
}
/*
* Set y_min and y_max to be equal but opposite so the graph is always
* centered.
*/
var absolute_y_max = Math.max(Math.abs(y_min), Math.abs(y_max));
y_min = absolute_y_max * -1;
y_max = absolute_y_max;
// Chart
this.chart_.options.exporting.chartOptions.title.text = this.get_title_();
this.chart_.options.exporting.chartOptions.subtitle.text = this.get_subtitle_();
this.chart_.options.chart.backgroundColor = beestat.style.color.bluegray.base;
this.chart_.options.exporting.filename = 'Temperature Profiles';
this.chart_.options.chart.zoomType = null;
this.chart_.options.plotOptions.series.connectNulls = true;
this.chart_.options.legend = {'enabled': false};
this.chart_.options.xAxis = {
'lineWidth': 0,
'tickLength': 0,
'tickInterval': 5,
'gridLineWidth': 1,
'gridLineColor': beestat.style.color.bluegray.light,
'gridLineDashStyle': 'longdash',
'labels': {
'style': {'color': beestat.style.color.gray.base},
'formatter': function() {
return this.value + thermostat.temperature_unit;
}
},
'plotLines': [
{
'color': beestat.series.outdoor_temperature.color,
'dashStyle': 'ShortDash',
'width': 1,
'value': beestat.temperature(thermostat.weather.temperature),
'zIndex': 2
}
]
};
this.chart_.options.yAxis = [
{
'alignTicks': false,
'gridLineColor': beestat.style.color.bluegray.light,
'gridLineDashStyle': 'longdash',
'title': {'text': null},
'labels': {
'style': {'color': beestat.style.color.gray.base},
'formatter': function() {
return this.value + thermostat.temperature_unit;
}
},
'min': y_min,
'max': y_max,
'plotLines': [
{
'color': beestat.style.color.bluegray.light,
'dashStyle': 'solid',
'width': 3,
'value': 0,
'zIndex': 1
}
]
}
];
this.chart_.options.tooltip = {
'shared': true,
'useHTML': true,
'borderWidth': 0,
'shadow': false,
'backgroundColor': null,
'followPointer': true,
'crosshairs': {
'width': 1,
'zIndex': 100,
'color': beestat.style.color.gray.light,
'dashStyle': 'shortDot',
'snap': false
},
'positioner': function(tooltip_width, tooltip_height, point) {
return beestat.component.chart.tooltip_positioner(
self.chart_.get_chart(),
tooltip_width,
tooltip_height,
point
);
},
'formatter': function() {
var sections = [];
var section = [];
this.points.forEach(function(point) {
var series = point.series;
var value = beestat.temperature({
'temperature': point.y,
'units': true,
'convert': false,
'delta': true,
'type': 'string'
}) + ' / hour';
if (series.name.indexOf('Raw') === -1) {
section.push({
'label': series.name,
'value': value,
'color': series.color
});
}
});
sections.push(section);
return beestat.component.chart.tooltip_formatter(
'Outdoor Temp: ' +
beestat.temperature({
'temperature': this.x,
'round': 0,
'units': true,
'convert': false
}),
sections
);
}
};
this.chart_.options.series = [];
// Trendline data
this.chart_.options.series.push({
'data': trendlines.heat,
'name': 'Indoor Heat Δ',
'color': beestat.series.compressor_heat_1.color,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'type': 'line',
'lineWidth': 2,
'states': {'hover': {'lineWidthPlus': 0}}
});
// Trendline data
this.chart_.options.series.push({
'data': trendlines.cool,
'name': 'Indoor Cool Δ',
'color': beestat.series.compressor_cool_1.color,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'type': 'line',
'lineWidth': 2,
'states': {'hover': {'lineWidthPlus': 0}}
});
// Trendline data
this.chart_.options.series.push({
'data': trendlines.resist,
'name': 'Indoor Δ',
'color': beestat.style.color.gray.dark,
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'type': 'line',
'lineWidth': 2,
'states': {'hover': {'lineWidthPlus': 0}}
});
// Raw data
this.chart_.options.series.push({
'data': raw.heat,
'name': 'Heat Raw',
'color': beestat.series.compressor_heat_1.color,
'dashStyle': 'ShortDot',
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'type': 'spline',
'lineWidth': 1,
'states': {'hover': {'lineWidthPlus': 0}}
});
// Raw data
this.chart_.options.series.push({
'data': raw.cool,
'name': 'Cool Raw',
'color': beestat.series.compressor_cool_1.color,
'dashStyle': 'ShortDot',
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'type': 'spline',
'lineWidth': 1,
'states': {'hover': {'lineWidthPlus': 0}}
});
// Raw data
this.chart_.options.series.push({
'data': raw.resist,
'name': 'Resist Raw',
'color': beestat.style.color.gray.dark,
'dashStyle': 'ShortDot',
'marker': {
'enabled': false,
'states': {'hover': {'enabled': false}}
},
'type': 'spline',
'lineWidth': 1,
'states': {'hover': {'lineWidthPlus': 0}}
});
this.chart_.render(parent);
}
};
/**
* Get a linear trendline from a set of data.
*
* @param {Object} data The data; at least two points required.
*
* @return {Object} The slope and intercept of the trendline.
*/
beestat.component.card.temperature_profiles.prototype.get_linear_trendline_ = function(data) {
// Requires at least two points.
if (Object.keys(data).length < 2) {
return null;
}
var sum_x = 0;
var sum_y = 0;
var sum_xy = 0;
var sum_x_squared = 0;
var n = 0;
for (var x in data) {
x = parseFloat(x);
var y = parseFloat(data[x]);
sum_x += x;
sum_y += y;
sum_xy += (x * y);
sum_x_squared += Math.pow(x, 2);
n++;
}
var slope = ((n * sum_xy) - (sum_x * sum_y)) /
((n * sum_x_squared) - (Math.pow(sum_x, 2)));
var intercept = ((sum_y) - (slope * sum_x)) / (n);
return {
'slope': slope,
'intercept': intercept
};
};
/**
* Get the title of the card.
*
* @return {string} The title.
*/
beestat.component.card.temperature_profiles.prototype.get_title_ = function() {
return 'Temperature Profiles';
};
/**
* Decorate the menu.
*
* @param {rocket.Elements} parent
*/
beestat.component.card.temperature_profiles.prototype.decorate_top_right_ = function(parent) {
var self = this;
var menu = (new beestat.component.menu()).render(parent);
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Download Chart')
.set_icon('download')
.set_callback(function() {
self.chart_.get_chart().exportChartLocal();
}));
menu.add_menu_item(new beestat.component.menu_item()
.set_text('Help')
.set_icon('help_circle')
.set_callback(function() {
(new beestat.component.modal.help_temperature_profiles()).render();
}));
};