mirror of
https://github.com/beestat/app.git
synced 2025-06-01 04:37:12 -04:00
630 lines
18 KiB
JavaScript
630 lines
18 KiB
JavaScript
/**
|
|
* Parent score card.
|
|
*/
|
|
beestat.component.card.score = function() {
|
|
var self = this;
|
|
|
|
/*
|
|
* Debounce so that multiple setting changes don't re-trigger the same
|
|
* event. This fires on the trailing edge so that all changes are accounted
|
|
* for when rerendering.
|
|
*/
|
|
var data_change_function = beestat.debounce(function() {
|
|
self.rerender();
|
|
}, 10);
|
|
|
|
beestat.dispatcher.addEventListener(
|
|
[
|
|
'cache.data.comparison_temperature_profile',
|
|
'cache.data.comparison_scores_' + this.type_
|
|
],
|
|
data_change_function
|
|
);
|
|
|
|
beestat.component.card.apply(this, arguments);
|
|
|
|
this.layer_.register_loader(beestat.generate_temperature_profile);
|
|
this.layer_.register_loader(beestat.get_comparison_scores);
|
|
};
|
|
beestat.extend(beestat.component.card.score, beestat.component.card);
|
|
|
|
/**
|
|
* Decorate
|
|
*
|
|
* @param {rocket.Elements} parent
|
|
*/
|
|
beestat.component.card.score.prototype.decorate_contents_ = function(parent) {
|
|
// this.view_detail_ = true;
|
|
|
|
if (this.view_detail_ === true) {
|
|
this.decorate_detail_(parent);
|
|
} else {
|
|
this.decorate_score_(parent);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Decorate the score with the circle.
|
|
*
|
|
* @param {rocket.Elements} parent
|
|
*/
|
|
beestat.component.card.score.prototype.decorate_score_ = function(parent) {
|
|
var thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')];
|
|
var thermostat_group = beestat.cache.thermostat_group[
|
|
thermostat.thermostat_group_id
|
|
];
|
|
|
|
if (
|
|
beestat.cache.data.comparison_temperature_profile === undefined ||
|
|
beestat.cache.data['comparison_scores_' + this.type_] === undefined
|
|
) {
|
|
// Height buffer so the cards don't resize after they load.
|
|
parent.appendChild($.createElement('div')
|
|
.style('height', '166px')
|
|
.innerHTML(' '));
|
|
this.show_loading_('Calculating');
|
|
} else {
|
|
var percentile;
|
|
if (
|
|
thermostat_group.temperature_profile[this.type_] !== undefined &&
|
|
beestat.cache.data['comparison_scores_' + this.type_].length > 2 &&
|
|
beestat.cache.data.comparison_temperature_profile[this.type_] !== null
|
|
) {
|
|
percentile = this.get_percentile_(
|
|
beestat.cache.data.comparison_temperature_profile[this.type_].score,
|
|
beestat.cache.data['comparison_scores_' + this.type_]
|
|
);
|
|
} else {
|
|
percentile = null;
|
|
}
|
|
|
|
var color;
|
|
if (percentile > 70) {
|
|
color = beestat.style.color.green.base;
|
|
} else if (percentile > 50) {
|
|
color = beestat.style.color.yellow.base;
|
|
} else if (percentile > 25) {
|
|
color = beestat.style.color.orange.base;
|
|
} else if (percentile !== null) {
|
|
color = beestat.style.color.red.base;
|
|
} else {
|
|
color = '#fff';
|
|
}
|
|
|
|
var container = $.createElement('div')
|
|
.style({
|
|
'text-align': 'center',
|
|
'position': 'relative',
|
|
'margin-top': beestat.style.size.gutter
|
|
});
|
|
parent.appendChild(container);
|
|
|
|
var percentile_text;
|
|
var percentile_font_size;
|
|
var percentile_color;
|
|
if (percentile !== null) {
|
|
percentile_text = '';
|
|
percentile_font_size = 48;
|
|
percentile_color = '#fff';
|
|
} else if (
|
|
thermostat_group['system_type_' + this.type_] === null ||
|
|
thermostat_group['system_type_' + this.type_] === 'none'
|
|
) {
|
|
percentile_text = 'None';
|
|
percentile_font_size = 16;
|
|
percentile_color = beestat.style.color.gray.base;
|
|
} else {
|
|
percentile_text = 'Insufficient data';
|
|
percentile_font_size = 16;
|
|
percentile_color = beestat.style.color.yellow.base;
|
|
}
|
|
|
|
var percentile_div = $.createElement('div')
|
|
.innerText(percentile_text)
|
|
.style({
|
|
'position': 'absolute',
|
|
'top': '50%',
|
|
'left': '50%',
|
|
'transform': 'translate(-50%, -50%)',
|
|
'font-size': percentile_font_size,
|
|
'font-weight': beestat.style.font_weight.light,
|
|
'color': percentile_color
|
|
});
|
|
container.appendChild(percentile_div);
|
|
|
|
var stroke = 3;
|
|
var size = 150;
|
|
var diameter = size - stroke;
|
|
var radius = diameter / 2;
|
|
var circumference = Math.PI * diameter;
|
|
|
|
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
svg.setAttribute('height', size);
|
|
svg.setAttribute('width', size);
|
|
svg.style.transform = 'rotate(-90deg)';
|
|
container.appendChild(svg);
|
|
|
|
var background = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
background.setAttribute('cx', (size / 2));
|
|
background.setAttribute('cy', (size / 2));
|
|
background.setAttribute('r', radius);
|
|
background.setAttribute('stroke', beestat.style.color.bluegray.dark);
|
|
background.setAttribute('stroke-width', stroke);
|
|
background.setAttribute('fill', 'none');
|
|
svg.appendChild(background);
|
|
|
|
var stroke_dasharray = circumference;
|
|
var stroke_dashoffset_initial = stroke_dasharray;
|
|
var stroke_dashoffset_final = stroke_dasharray * (1 - (percentile / 100));
|
|
|
|
var foreground = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
foreground.style.transition = 'stroke-dashoffset 1s ease';
|
|
foreground.setAttribute('cx', (size / 2));
|
|
foreground.setAttribute('cy', (size / 2));
|
|
foreground.setAttribute('r', radius);
|
|
foreground.setAttribute('stroke', color);
|
|
foreground.setAttribute('stroke-width', stroke);
|
|
foreground.setAttribute('stroke-linecap', 'round');
|
|
foreground.setAttribute('stroke-dasharray', stroke_dasharray);
|
|
foreground.setAttribute('stroke-dashoffset', stroke_dashoffset_initial);
|
|
foreground.setAttribute('fill', 'none');
|
|
svg.appendChild(foreground);
|
|
|
|
/*
|
|
* For some reason the render event (which is timeout 0) doesn't work well
|
|
* here.
|
|
*/
|
|
setTimeout(function() {
|
|
foreground.setAttribute('stroke-dashoffset', stroke_dashoffset_final);
|
|
|
|
if (percentile !== null) {
|
|
$.step(
|
|
function(percentage, sine) {
|
|
var calculated_percentile = Math.round(percentile * sine);
|
|
percentile_div.innerText(calculated_percentile);
|
|
},
|
|
1000,
|
|
null,
|
|
30
|
|
);
|
|
}
|
|
}, 100);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Decorate the detail bell curve.
|
|
*
|
|
* @param {rocket.Elements} parent
|
|
*/
|
|
beestat.component.card.score.prototype.decorate_detail_ = function(parent) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// var self = this;
|
|
|
|
this.chart_ = new beestat.component.chart();
|
|
this.chart_.options.chart.height = 166;
|
|
|
|
// 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) {
|
|
var profile = 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})] =
|
|
beestat.temperature({
|
|
'temperature': profile.deltas[key],
|
|
'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;*/
|
|
|
|
// y_min = -5;
|
|
// y_max = 5;
|
|
// x_min = Math.min(x_min, 0);
|
|
// x_max = Math.max(x_max, 100);
|
|
|
|
// 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 = this.get_title_();
|
|
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;
|
|
}
|
|
}
|
|
};*/
|
|
|
|
|
|
this.chart_.options.xAxis = {
|
|
'title': { 'text': null },
|
|
'plotLines': [
|
|
{
|
|
'color': 'white',
|
|
'width': 2,
|
|
'value': beestat.cache.data.comparison_temperature_profile[this.type_].score
|
|
|
|
}
|
|
]
|
|
// alignTicks: false
|
|
};
|
|
this.chart_.options.yAxis = {
|
|
'title': { 'text': null }
|
|
};
|
|
|
|
/* 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
|
|
);
|
|
}
|
|
};*/
|
|
|
|
// beestat.cache.data['comparison_scores_' + this.type_] = [ 0.4, 0.6, 0.6, 0.7, 0.8, 0.8, 0.8, 0.9, 0.9, 1, 1, 1, 1, 1, 1.1, 1.1, 1.1, 1.1, 1.2, 1.2, 1.2, 1.4, 1.4, 1.5, 1.5, 1.5, 1.5, 1.5, 1.6, 1.7, 1.8, 1.9, 2.3, 2.6, 2.7, 3.3, 3.3, 3.6, 5.9]
|
|
|
|
|
|
console.log(beestat.cache.data['comparison_scores_' + this.type_]);
|
|
|
|
var color = this.type_ === 'resist' ? beestat.style.color.gray.base : beestat.series['compressor_' + this.type_ + '_1'].color;
|
|
|
|
this.chart_.options.series = [
|
|
|
|
|
|
{
|
|
// 'data': trendlines.heat,
|
|
// 'name': 'Indoor Heat Δ',
|
|
// 'color': beestat.series.compressor_heat_1.color,
|
|
// 'marker': {
|
|
// 'enabled': false,
|
|
// 'states': {'hover': {'enabled': false}}
|
|
// },
|
|
'type': 'bellcurve',
|
|
'baseSeries': 1,
|
|
'color': color,
|
|
|
|
// Histogram
|
|
// 'type': 'histogram',
|
|
// 'binWidth': 0.1,
|
|
// 'borderWidth': 0,
|
|
|
|
// 'data': beestat.cache.data['comparison_scores_' + this.type_]
|
|
// 'lineWidth': 2,
|
|
// 'states': {'hover': {'lineWidthPlus': 0}}
|
|
},
|
|
{
|
|
'data': beestat.cache.data['comparison_scores_' + this.type_],
|
|
'visible': false
|
|
},
|
|
|
|
];
|
|
|
|
console.log(parent);
|
|
// return;
|
|
|
|
// 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': 'bellcurve',
|
|
// 'data': beestat.cache.data['comparison_scores_' + this.type_]
|
|
// 'lineWidth': 2,
|
|
// 'states': {'hover': {'lineWidthPlus': 0}}
|
|
// });
|
|
|
|
console.log('render chart');
|
|
this.chart_.render(parent);
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
/**
|
|
* Get the percentile rank of a score in a set of scores.
|
|
*
|
|
* @param {number} score
|
|
* @param {array} scores
|
|
*
|
|
* @return {number} The percentile rank.
|
|
*/
|
|
beestat.component.card.score.prototype.get_percentile_ = function(score, scores) {
|
|
var n = scores.length;
|
|
var below = 0;
|
|
scores.forEach(function(s) {
|
|
if (s < score) {
|
|
below++;
|
|
}
|
|
});
|
|
|
|
return Math.round(below / n * 100);
|
|
};
|
|
|
|
/**
|
|
* Decorate the menu.
|
|
*
|
|
* @param {rocket.Elements} parent
|
|
*/
|
|
beestat.component.card.score.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('View Detail')
|
|
// .set_icon('chart_bell_curve')
|
|
// .set_callback(function() {
|
|
// self.view_detail_ = true;
|
|
// self.rerender();
|
|
// }));
|
|
|
|
menu.add_menu_item(new beestat.component.menu_item()
|
|
.set_text('Help')
|
|
.set_icon('help_circle')
|
|
.set_callback(function() {
|
|
(new beestat.component.modal.help_score(self.type_)).render();
|
|
}));
|
|
};
|
|
|
|
/**
|
|
* Get subtitle.
|
|
*
|
|
* @return {string} The subtitle.
|
|
*/
|
|
beestat.component.card.score.prototype.get_subtitle_ = function() {
|
|
if (this.view_detail_ === true) {
|
|
if (
|
|
// beestat.cache.data['comparison_scores_' + this.type_] !== undefined &&
|
|
// beestat.cache.data['comparison_scores_' + this.type_].length > 2 &&
|
|
beestat.cache.data.comparison_temperature_profile !== undefined &&
|
|
beestat.cache.data.comparison_temperature_profile[this.type_] !== null
|
|
) {
|
|
return 'Your raw score: ' + beestat.cache.data.comparison_temperature_profile[this.type_].score;
|
|
}
|
|
|
|
return 'N/A';
|
|
} else {
|
|
if (
|
|
beestat.cache.data['comparison_scores_' + this.type_] !== undefined &&
|
|
beestat.cache.data['comparison_scores_' + this.type_].length > 2 &&
|
|
beestat.cache.data.comparison_temperature_profile !== undefined &&
|
|
beestat.cache.data.comparison_temperature_profile[this.type_] !== null
|
|
) {
|
|
return 'Comparing to ' + Number(beestat.cache.data['comparison_scores_' + this.type_].length).toLocaleString() + ' Homes';
|
|
}
|
|
|
|
return 'N/A';
|
|
}
|
|
|
|
};
|