diff --git a/js/beestat/style.js b/js/beestat/style.js index 4da55b7..3830846 100644 --- a/js/beestat/style.js +++ b/js/beestat/style.js @@ -24,22 +24,22 @@ beestat.style.color = { 'bluegreen': { 'light': '#2bcbba', 'base': '#0fb9b1', - 'dark': '' + 'dark': '#00867e' }, 'lightblue': { 'light': '#45aaf2', 'base': '#2d98da', - 'dark': '' + 'dark': '#147fc1' }, 'blue': { 'light': '#4b7bec', 'base': '#3867d6', - 'dark': '' + 'dark': '#1f4ebd' }, 'purple': { 'light': '#a55eea', 'base': '#8854d0', - 'dark': '' + 'dark': '#6f3bb7' }, 'gray': { 'light': '#d1d8e0', diff --git a/js/component/card/runtime_sensor_detail.js b/js/component/card/runtime_sensor_detail.js index 57f761a..ce7ed62 100644 --- a/js/component/card/runtime_sensor_detail.js +++ b/js/component/card/runtime_sensor_detail.js @@ -109,7 +109,7 @@ beestat.component.card.runtime_sensor_detail.prototype.decorate_contents_ = func } var api_call = new beestat.api(); - Object.values(beestat.cache.sensor).forEach(function(sensor) { + this.get_sensors_().forEach(function(sensor) { if (sensor.thermostat_id === beestat.setting('thermostat_id')) { api_call.add_call( 'runtime_sensor', @@ -276,7 +276,8 @@ beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function() { 'title': this.get_title_(), 'subtitle': this.get_subtitle_(), 'y_min': Infinity, - 'y_max': -Infinity + 'y_max': -Infinity, + 'sensors': null } } }; @@ -289,8 +290,15 @@ beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function() { } }; + var series_codes = []; - Object.values(beestat.cache.sensor).forEach(function(sensor) { + + // Get and sort all the sensors. + var sensors = this.get_sensors_(); + data.metadata.sensors = sensors; + + // Set up the series_codes. + sensors.forEach(function(sensor) { if (sensor.thermostat_id === beestat.setting('thermostat_id')) { series_codes.push('temperature_' + sensor.sensor_id); series_codes.push('occupancy_' + sensor.sensor_id); @@ -350,7 +358,7 @@ beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function() { var moving = []; var moving_count; if (beestat.setting('runtime_sensor_detail_smoothing') === true) { - moving_count = 15; + moving_count = 5; } else { moving_count = 1; } @@ -360,6 +368,11 @@ beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function() { moving.push(runtime_sensors[begin_m.valueOf() + offset]); } + // TODO: Garage sensor is not returned in runtime data until halfway + // through...so the series data never got added early on so it just gets + // slapped on the beginning. It also takes over a previous "j" value and + // pushes everything around. Instead of looping over runtime_sensor I need to loop over sensor and grab the values. + // Loop. var current_m = begin_m; while ( @@ -373,9 +386,16 @@ beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function() { data.metadata.series.dummy.active = true; if (runtime_sensors[current_m.valueOf()] !== undefined) { - runtime_sensors[current_m.valueOf()].forEach(function(runtime_sensor, j) { + sensors.forEach(function(sensor, j) { + var runtime_sensor = runtime_sensors[current_m.valueOf()][sensor.sensor_id]; + if (runtime_sensor === undefined) { + data.series['temperature_' + sensor.sensor_id].push(null); + data.series['occupancy_' + sensor.sensor_id].push(null); + return; + } + var temperature_moving = beestat.temperature( - self.get_average_(moving, j) + self.get_average_(moving, sensor.sensor_id) ); data.series['temperature_' + runtime_sensor.sensor_id].push(temperature_moving); y_min_max(temperature_moving); @@ -384,7 +404,7 @@ beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function() { if (runtime_sensor.occupancy === true) { let swimlane_properties = beestat.component.chart.runtime_sensor_detail.get_swimlane_properties( - runtime_sensors[current_m.valueOf()].length, + sensors.length, j ); @@ -394,7 +414,7 @@ beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function() { if (sequential['occupancy_' + runtime_sensor.sensor_id] > 0) { let swimlane_properties = beestat.component.chart.runtime_sensor_detail.get_swimlane_properties( - runtime_sensors[current_m.valueOf()].length, + sensors.length, j ); @@ -406,7 +426,7 @@ beestat.component.card.runtime_sensor_detail.prototype.get_data_ = function() { } }); } else { - Object.values(beestat.cache.sensor).forEach(function(sensor) { + sensors.forEach(function(sensor) { if (sensor.thermostat_id === beestat.setting('thermostat_id')) { data.series['temperature_' + sensor.sensor_id].push(null); data.series['occupancy_' + sensor.sensor_id].push(null); @@ -439,9 +459,11 @@ beestat.component.card.runtime_sensor_detail.prototype.get_runtime_sensor_by_dat beestat.cache.runtime_sensor.forEach(function(runtime_sensor) { var timestamp = [moment(runtime_sensor.timestamp).valueOf()]; if (runtime_sensors[timestamp] === undefined) { - runtime_sensors[timestamp] = []; + // runtime_sensors[timestamp] = []; + runtime_sensors[timestamp] = {}; } - runtime_sensors[timestamp].push(runtime_sensor); + // runtime_sensors[timestamp].push(runtime_sensor); + runtime_sensors[timestamp][runtime_sensor.sensor_id] = runtime_sensor; }); } return runtime_sensors; @@ -453,19 +475,28 @@ beestat.component.card.runtime_sensor_detail.prototype.get_runtime_sensor_by_dat * moving average. * * @param {array} runtime_sensors - * @param {string} j The index in the sub-array + * @param {string} sensor_id The index in the sub-array * * @return {number} The average. */ -beestat.component.card.runtime_sensor_detail.prototype.get_average_ = function(runtime_sensors, j) { +beestat.component.card.runtime_sensor_detail.prototype.get_average_ = function(runtime_sensors, sensor_id) { var average = 0; var count = 0; for (var i = 0; i < runtime_sensors.length; i++) { - if (runtime_sensors[i] !== undefined) { - average += runtime_sensors[i][j].temperature; + if ( + runtime_sensors[i] !== undefined && + runtime_sensors[i][sensor_id] !== undefined && + runtime_sensors[i][sensor_id].temperature !== null + ) { + average += runtime_sensors[i][sensor_id].temperature; count++; } } + + if (count === 0) { + return null; + } + return average / count; }; @@ -526,3 +557,24 @@ beestat.component.card.runtime_sensor_detail.prototype.data_synced_ = function(r current_sync_end.isSameOrAfter(required_sync_end) ); }; + +/** + * Get a sorted list of all sensors attached to the current thermostat. + * + * @return {array} The sensors. + */ +beestat.component.card.runtime_sensor_detail.prototype.get_sensors_ = function() { + // Get and sort all the sensors. + var sensors = []; + $.values(beestat.cache.sensor).forEach(function(sensor) { + if (sensor.thermostat_id === beestat.setting('thermostat_id')) { + sensors.push(sensor); + } + }); + + sensors.sort(function(a, b) { + return a.name.localeCompare(b.name, 'en', {'sensitivity': 'base'}); + }); + + return sensors; +}; diff --git a/js/component/chart.js b/js/component/chart.js index a13d489..dff6168 100644 --- a/js/component/chart.js +++ b/js/component/chart.js @@ -71,7 +71,19 @@ beestat.component.chart.prototype.get_options_legend_ = function() { 'itemHiddenStyle': { 'color': '#7f8c8d' }, - 'labelFormatter': this.get_options_legend_labelFormatter_() + 'labelFormatter': this.get_options_legend_labelFormatter_(), + + + // 'layout': 'vertical', + // 'align': 'right', + // 'verticalAlign': 'top' + + // 'maxHeight': 1000, // To prevent the navigation thing + // 'floating': true, + // 'verticalAlign': 'top', + // 'y': 50, + // 'borderWidth': 1 + // }; }; @@ -175,10 +187,20 @@ beestat.component.chart.prototype.get_options_chart_ = function() { 'display': 'none' } }, - 'height': this.get_options_chart_height_() + 'height': this.get_options_chart_height_(), + 'events': this.get_options_chart_events_() }; }; +/** + * Get the events list for the chart. + * + * @return {number} The events list for the chart. + */ +beestat.component.chart.prototype.get_options_chart_events_ = function() { + return null; +}; + /** * Get the height of the chart. * @@ -240,7 +262,8 @@ beestat.component.chart.prototype.get_options_exporting_ = function() { beestat.style.size.gutter, beestat.style.size.gutter, beestat.style.size.gutter - ] + ], + 'events': this.get_options_exporting_chart_events_() } } }; @@ -290,6 +313,15 @@ beestat.component.chart.prototype.get_options_exporting_filename_ = function() { return filename.join(' '); }; +/** + * Get the events list for the chart on export. + * + * @return {string} The events list for the chart on export. + */ +beestat.component.chart.prototype.get_options_exporting_chart_events_ = function() { + return null; +}; + /** * Get the credits options. * diff --git a/js/component/chart/runtime_sensor_detail.js b/js/component/chart/runtime_sensor_detail.js index 84bb3d3..0a124ef 100644 --- a/js/component/chart/runtime_sensor_detail.js +++ b/js/component/chart/runtime_sensor_detail.js @@ -62,10 +62,26 @@ beestat.component.chart.runtime_sensor_detail.prototype.get_options_series_ = fu beestat.style.color.orange.base, beestat.style.color.bluegreen.base, beestat.style.color.purple.base, - beestat.style.color.lightblue.base + beestat.style.color.lightblue.base, + beestat.style.color.blue.light, + beestat.style.color.red.light, + beestat.style.color.yellow.light, + beestat.style.color.green.light, + beestat.style.color.orange.light, + beestat.style.color.bluegreen.light, + beestat.style.color.purple.light, + beestat.style.color.lightblue.light, + beestat.style.color.blue.dark, + beestat.style.color.red.dark, + beestat.style.color.yellow.dark, + beestat.style.color.green.dark, + beestat.style.color.orange.dark, + beestat.style.color.bluegreen.dark, + beestat.style.color.purple.dark, + beestat.style.color.lightblue.dark ]; - Object.values(beestat.cache.sensor).forEach(function(sensor, i) { + this.data_.metadata.sensors.forEach(function(sensor, i) { if (sensor.thermostat_id === beestat.setting('thermostat_id')) { series.push({ 'name': 'temperature_' + sensor.sensor_id, @@ -76,7 +92,7 @@ beestat.component.chart.runtime_sensor_detail.prototype.get_options_series_ = fu 'lineWidth': 1 }); - var sensor_count = (Object.keys(self.data_.series).length - 1) / 2; + // var sensor_count = (Object.keys(self.data_.series).length - 1) / 2; series.push({ 'linkedTo': ':previous', @@ -85,7 +101,7 @@ beestat.component.chart.runtime_sensor_detail.prototype.get_options_series_ = fu 'color': colors[i], 'yAxis': 1, 'type': 'line', - 'lineWidth': beestat.component.chart.runtime_sensor_detail.get_swimlane_properties(sensor_count, 1).line_width, + 'lineWidth': beestat.component.chart.runtime_sensor_detail.get_swimlane_properties(self.data_.metadata.sensors.length, 1).line_width, 'linecap': 'square', 'className': 'crisp_edges' }); @@ -114,37 +130,13 @@ beestat.component.chart.runtime_sensor_detail.prototype.get_options_yAxis_ = fun * Highcharts doesn't seem to respect axis behavior well so just overriding * it completely here. */ + var y_min = Math.floor((this.data_.metadata.chart.y_min) / 5) * 5; + var y_max = Math.ceil((this.data_.metadata.chart.y_max) / 5) * 5; - var sensor_count = (Object.keys(this.data_.series).length - 1) / 2; - - var y_min = Math.floor((this.data_.metadata.chart.y_min - 5) / 10) * 10; - var y_max = Math.ceil((this.data_.metadata.chart.y_max + 10) / 10) * 10; - - /** - * This is unfortunate. Axis heights can be done in either pixels or - * percentages. If you use percentages, it's percentage of the plot height - * which includes the y-axis labels and the legend. These heights are - * variable, so setting a 20% height on the swimlane axis means the axis - * height can actually change depending on external factors. When trying to - * accurately position lanes, this variation can mess up pixel-perfect - * spacing. - * - * If you use pixels you can get more exact, but since there's no way to - * determine the available height for the chart (plot area minus y-axis - * labels minus legend), you're left in the dark on how high to make your - * "rest of the space" axis. There's also no way to set the height of one - * axis and have the other axis take the remaining space. - * - * So, as a workaround, setting the swimlane axis to a fixed height and - * having it sit on top of a full height axis works well enough. Adding a - * bit of padding to the primary axis prevents those values from flowing on - * top. It's not perfect because you get the main axis all the way up the - * side but it's not terrible. - */ - y_max += ((sensor_count > 8) ? 20 : 10); + y_max += ((beestat.setting('temperature_unit') === '°F') ? 10 : 4); var tick_positions = []; - var tick_interval = (beestat.setting('temperature_unit') === '°F') ? 10 : 5; + var tick_interval = (beestat.setting('temperature_unit') === '°F') ? 5 : 2; var current_tick_position = Math.floor(y_min / tick_interval) * tick_interval; while (current_tick_position <= y_max) { @@ -152,6 +144,7 @@ beestat.component.chart.runtime_sensor_detail.prototype.get_options_yAxis_ = fun current_tick_position += tick_interval; } + return [ // Temperature { @@ -170,13 +163,20 @@ beestat.component.chart.runtime_sensor_detail.prototype.get_options_yAxis_ = fun // Swimlanes { 'height': 100, - 'top': 15, + // 'top': 0, 'min': 0, 'max': 100, 'reversed': true, 'gridLineWidth': 0, 'title': {'text': null}, - 'labels': {'enabled': false} + 'labels': {'enabled': false}, + 'plotBands': { + 'zIndex': 2, + // 'color': 'red', + 'color': beestat.style.color.bluegray.dark, + 'from': 0, + 'to': 51 + } } ]; }; @@ -193,39 +193,55 @@ beestat.component.chart.runtime_sensor_detail.prototype.get_options_tooltip_form var sections = []; var group = []; + // Get all the point values and index them by series_code for reference. var values = {}; this.points.forEach(function(point) { values[point.series.name] = point.y; }); - this.points.forEach(function(point) { + /** + * Get a couple of other properties and index them by series_code for + * reference. This dives up to the chart itself because the tooltip shows + * all series unless explicitly disabled and those aren't always in the + * points array. + */ + var colors = {}; + var visible = {}; + self.chart_.series.forEach(function(series) { + colors[series.name] = series.color; + visible[series.name] = series.visible; + }); + + for (var series_code in self.data_.series) { var label; var value; var color; - if (point.series.name.includes('temperature') === true) { - label = self.data_.metadata.series[point.series.name].name; - color = point.series.color; - value = beestat.temperature({ - 'temperature': values[point.series.name], - 'convert': false, - 'units': true - }); + if (series_code.includes('temperature') && visible[series_code] === true) { + label = self.data_.metadata.series[series_code].name; + color = colors[series_code]; + if (values[series_code] === undefined) { + value = '-'; + } else { + value = beestat.temperature({ + 'temperature': values[series_code], + 'convert': false, + 'units': true + }); + } - var occupancy_key = point.series.name.replace('temperature', 'occupancy'); + var occupancy_key = series_code.replace('temperature', 'occupancy'); if (values[occupancy_key] !== undefined && values[occupancy_key] !== null) { value += ' ●'; } - } else { - return; - } - group.push({ - 'label': label, - 'value': value, - 'color': color - }); - }); + group.push({ + 'label': label, + 'value': value, + 'color': color + }); + } + } if (group.length === 0) { group.push({ @@ -255,14 +271,17 @@ beestat.component.chart.runtime_sensor_detail.prototype.get_options_tooltip_form * @return {Object} The swimlane line width and y position. */ beestat.component.chart.runtime_sensor_detail.get_swimlane_properties = function(count, i) { + // Available height for all swimlanes var height = 50; + + // Some sensible max height if you have very few sensors. var max_line_width = 16; - // Spacing. - var spacing = 4; + // Spacing. This is arbitrary...spacing decreases to 0 after you hit 15 sensors. + var spacing = Math.floor(15 / count); // Base line width is a percentage height of the container. - var line_width = Math.round(height / count); + var line_width = Math.floor(height / count); // Cap to a max line width. line_width = Math.min(line_width, max_line_width); @@ -274,8 +293,69 @@ beestat.component.chart.runtime_sensor_detail.get_swimlane_properties = function // Make the lines slightly less tall to create space between them. line_width -= spacing; + // Center within the swimlane area. + var occupied_space = (line_width * count) + (spacing * count); + var empty_space = height - occupied_space; + // y += (empty_space / 2); + return { 'line_width': line_width, 'y': y }; }; + +/** + * This is unfortunate. Axis heights can be done in either pixels or + * percentages. If you use percentages, it's percentage of the plot height + * which includes the y-axis labels and the legend. These heights are + * variable, so setting a 20% height on the swimlane axis means the axis + * height can actually change depending on external factors. When trying to + * accurately position lanes, this variation can mess up pixel-perfect + * spacing. + * + * If you use pixels you can get more exact, but since there's no way to + * determine the available height for the chart (plot area minus y-axis labels + * minus legend), you're left in the dark on how high to make your "rest of + * the space" axis. There's also no way to set the height of one axis and have + * the other axis take the remaining space. + * + * So, as a workaround...I simply overlay the swimlanes on the top of a + * full-height temperature chart. Then I draw a rectangle on top of y-axis + * labels I want to hide so it appears to be on it's own. + * + * Helpful: https://www.highcharts.com/demo/renderer + * + * @return {object} The events list for the chart. + */ +beestat.component.chart.runtime_sensor_detail.prototype.get_options_chart_events_ = function() { + return { + 'load': function() { + this.renderer.rect(0, 0, 30, 80) + .attr({ + 'fill': beestat.style.color.bluegray.base, + 'zIndex': 10 + }) + .add(); + } + }; +}; + +/** + * See comment on get_options_chart_events_. This is done separately to + * override the normal load event rectangle draw because on export I also add + * padding and a title which screws up the positioning a bit. + * + * @return {object} The events list for the chart on export. + */ +beestat.component.chart.runtime_sensor_detail.prototype.get_options_exporting_chart_events_ = function() { + return { + 'load': function() { + this.renderer.rect(beestat.style.size.gutter, 60, 30, 60) + .attr({ + 'fill': beestat.style.color.bluegray.base, + 'zIndex': 10 + }) + .add(); + } + }; +};