From fa0f1c94b14cd3de6a28b30f200ca712740e974e Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Mon, 17 Jul 2023 10:24:46 +0000 Subject: [PATCH] Switch to new weather database backend. --- dashboard/__init__.py | 49 ++- dashboard/environment_json.py | 403 ------------------------ dashboard/templates/environment.html | 24 +- dashboard/templates/infrastructure.html | 38 +-- static/environment.css | 5 +- static/environment.js | 358 +++++++++++---------- 6 files changed, 250 insertions(+), 627 deletions(-) delete mode 100755 dashboard/environment_json.py diff --git a/dashboard/__init__.py b/dashboard/__init__.py index 8b046b1..82d3b75 100644 --- a/dashboard/__init__.py +++ b/dashboard/__init__.py @@ -20,6 +20,8 @@ import pymysql import requests from requests.auth import HTTPDigestAuth +from astropy.time import Time +import astropy.units as u from flask import abort from flask import Flask @@ -31,10 +33,8 @@ from flask import session from flask import url_for from flask_github import GitHub - from warwick.observatory.common import daemons - -from dashboard import environment_json +from werkzeug.exceptions import NotFound # pylint: disable=missing-docstring @@ -80,7 +80,7 @@ } -app = Flask(__name__) +app = Flask(__name__, static_folder='../static') # Stop Flask from telling the browser to cache dynamic files app.config['SEND_FILE_MAX_AGE_DEFAULT'] = -1 @@ -622,24 +622,43 @@ def infrastructure_log(): abort(404) -@app.route('/data/environment') -def environment_data(): - date = request.args['date'] if 'date' in request.args else None - data, start, end = environment_json.environment_json(date) +def environment_json(base): + now = Time.now() + today = Time(now.datetime.strftime('%Y-%m-%d'), format='isot', scale='utc') + 12 * u.hour + if today > now: + today -= 1 * u.day + + path = 'latest.json.gz' + if 'date' in request.args: + # Map today's date to today.json + # HACK: use .datetime to work around missing strftime on ancient astropy + if today.strftime('%Y-%m-%d') == request.args['date']: + path = 'today.json.gz' + else: + # Validate that it is a well-formed date + date = Time(request.args['date'], format='isot', scale='utc') + path = date.datetime.strftime('%Y/%Y-%m-%d.json.gz') + + try: + response = send_from_directory(GENERATED_DATA_DIR, os.path.join(base, path)) + response.headers['Content-Encoding'] = 'gzip' + except NotFound: + start = today.unix * 1000, + end = (today + 1 * u.day).unix * 1000, + response = jsonify(data={}, start=start, end=end) - response = jsonify(data=data, start=start, end=end) response.headers['Access-Control-Allow-Origin'] = '*' return response +@app.route('/data/environment') +def environment_data(): + return environment_json('environment') + + @app.route('/data/infrastructure') def infrastructure_data(): - date = request.args['date'] if 'date' in request.args else None - data, start, end = environment_json.infrastructure_json(date) - - response = jsonify(data=data, start=start, end=end) - response.headers['Access-Control-Allow-Origin'] = '*' - return response + return environment_json('infrastructure') @app.route('/data/w1m/') diff --git a/dashboard/environment_json.py b/dashboard/environment_json.py deleted file mode 100755 index 7e208c7..0000000 --- a/dashboard/environment_json.py +++ /dev/null @@ -1,403 +0,0 @@ -# -# onemetre-dashboard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# onemetre-dashboard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with onemetre-dashboard. If not, see . - -"""Helper functions for querying data for the Environment and Infrastructure graphs""" - -# pylint: disable=invalid-name -# pylint: disable=broad-except - -import datetime -import pymysql - -DATABASE_DB = 'ops' -DATABASE_USER = 'ops' - -ONEMETRE_VAISALA = { - 'temperature': ('W1m', 'wexttemp', '#009DDC'), - 'relative_humidity': ('W1m', 'wexthumid', '#009DDC'), - 'wind_direction': ('W1m', 'wwinddir', '#009DDC'), - 'wind_speed': ('W1m', 'wwindspeed', '#009DDC'), - 'pressure': ('W1m', 'wpressure', '#009DDC'), - 'rain_intensity': ('W1m', 'wrainint', '#009DDC'), - 'accumulated_rain': ('W1m', 'wrain', '#009DDC'), - 'dew_point_delta': ('W1m', 'wdewdelta', '#009DDC') -} - -ROOMALERT = { - 'internal_temp': ('W1m', 'winttemp', '#009DDC'), - 'internal_humidity': ('W1m', 'winthumid', '#009DDC'), -} - -# NOTE: internal/rack probes connected backwards in hardware -NITES_ROOMALERT = { - 'rack_temperature': ('NITES', 'ninttemp', '#DE0D92'), - 'rack_humidity': ('NITES', 'ninthumid', '#DE0D92'), -} - -GOTO_VAISALA = { - 'temperature': ('GOTO', 'gexttemp', '#22cc44'), - 'relative_humidity': ('GOTO', 'gexthumid', '#22cc44'), - 'wind_direction': ('GOTO', 'gwinddir', '#22cc44'), - 'wind_speed': ('GOTO', 'gwindspeed', '#22cc44'), - 'pressure': ('GOTO', 'gpressure', '#22cc44'), - 'rain_intensity': ('GOTO', 'grainint', '#22cc44'), - 'accumulated_rain': ('GOTO', 'grain', '#22cc44'), - 'dew_point_delta': ('GOTO', 'gdewdelta', '#22cc44') -} - -GOTO_ROOMALERT = { - 'internal_temp': ('GOTO 1', 'ginttemp', '#22cc44'), - 'internal_humidity': ('GOTO 1', 'ginthumid', '#22cc44'), -} - -GOTO_DOME2_ROOMALERT = { - 'internal_temp': ('GOTO 2', 'g2inttemp', '#FDE74C'), - 'internal_humidity': ('GOTO 2', 'g2inthumid', '#FDE74C') -} - -SUPERWASP = { - 'ext_temperature': ('SWASP', 'swtemp', '#F26430'), - 'ext_humidity': ('SWASP', 'swhumid', '#F26430'), - 'wind_speed': ('SWASP', 'swwindspeed', '#F26430'), - 'wind_direction': ('SWASP', 'swwinddir', '#F26430'), - 'dew_point_delta': ('SWASP', 'swdewdelta', '#F26430'), -} - -ONEMETRE_RAINDETECTOR = { - 'unsafe_boards': ('W1m', 'rdboards', '#009DDC') -} - -ONEMETRE_UPS = { - 'main_ups_battery_remaining': ('W1m', 'mupsbat', '#009DDC'), - 'dome_ups_battery_remaining': ('NITES', 'dupsbat', '#DE0D92'), -} - -RASA_UPS = { - 'ups_battery_remaining': ('RASA', 'rupsbat', '#FDE74C'), -} - -GOTO_UPS = { - 'main_ups_battery_remaining': ('GOTO Main', 'goto-mupsbat', '#22CC44'), - 'dome_ups_battery_remaining': ('GOTO Dome', 'goto-dupsbat', '#22CC44'), -} - -SUPERWASP_UPS = { - 'ups1_battery_remaining': ('SWASP UPS1', 'swasp-ups1bat', '#F26430'), - 'ups2_battery_remaining': ('SWASP UPS2', 'swasp-ups2bat', '#F26430'), - 'ups3_battery_remaining': ('SWASP UPS3', 'swasp-ups3bat', '#F26430'), - 'roofbattery': ('SWASP Roof Battery', 'swroofbat', '#F26430'), -} - -SUPERWASP_ROOMALERT = { - 'comp_room_temp': ('SWComp', 'swcomptemp', '#FF6699'), - 'comp_room_humidity': ('SWComp', 'swcomphumid', '#FF6699'), - 'cam_room_temp': ('SWCam', 'swcamtemp', '#F26430'), - 'cam_room_humidity': ('SWCam', 'swcamhumid', '#F26430'), -} - -SUPERWASP_AURORA = { - 'clarity': ('SWASP', 'swskyclarity', '#F26430'), - 'light_intensity': ('SWASP', 'swlightintensity', '#F26430'), - 'rain_intensity': ('SWASP', 'swrainintensity', '#F26430'), -} - -NETWORK = { - 'ngtshead': ('Warwick', 'pingngts', '#FDE74C'), - 'google': ('Google', 'pinggoogle', '#009DDC'), - 'onemetre': ('1m', 'pingint1m', '#009DDC'), - 'goto': ('GOTO', 'pingintgoto', '#22CC44'), - 'nites': ('NITES', 'pingintnites', '#DE0D92'), - 'swasp': ('SWASP', 'pingintswasp', '#F26430'), - 'swasp_gateway': ('WHT', 'pingintwht', '#CC0000'), -} - -TNG_SEEING = { - 'seeing': ('TNG', 'tngseeing', '#F26430') -} - -ROBODIMM_SEEING = { - 'seeing': ('RoboDIMM', 'roboseeing', '#FDE74C') -} - -# Bodge for sources that were added to an already existing table -CHANNEL_START_DATES = { - 'swasp-ups3bat': datetime.datetime(2020, 11, 7, 3, 10), - 'swcamtemp': datetime.datetime(2020, 11, 6, 17, 50), - 'swcamhumid': datetime.datetime(2020, 11, 6, 17, 50) -} - - -def environment_json(date=None): - """Queries the data to be rendered on the "Environment" dashboard page - If date is specified, returns data for the specified night (UTC times: 12 through 12) - If date is not specified, returns data for the last 6 hours. - - Returns a tuple of (, - , ) - """ - try: - start = datetime.datetime.strptime(date, '%Y-%m-%d') \ - + datetime.timedelta(hours=11, minutes=54) - end = start + datetime.timedelta(hours=24, minutes=12) - except Exception: - end = datetime.datetime.utcnow() - start = end - datetime.timedelta(hours=6, minutes=6) - - start_str = start.isoformat() - end_str = end.isoformat() - start_js = int(start.replace(tzinfo=datetime.timezone.utc).timestamp() * 1000) - end_js = int(end.replace(tzinfo=datetime.timezone.utc).timestamp() * 1000) - - db = pymysql.connect(db=DATABASE_DB, user=DATABASE_USER) - data = __vaisala_json(db, 'weather_onemetre_vaisala', ONEMETRE_VAISALA, 'wwindrange', start_str, end_str, - wind_range_offset=-30000) - data.update(__sensor_json(db, 'weather_onemetre_roomalert', ROOMALERT, start_str, end_str)) - data.update(__superwasp_json(db, 'weather_superwasp', SUPERWASP, start_str, end_str)) - data.update(__sensor_json(db, 'weather_onemetre_raindetector', ONEMETRE_RAINDETECTOR, start_str, - end_str)) - data.update(__sensor_json(db, 'weather_nites_roomalert', NITES_ROOMALERT, start_str, end_str)) - data.update(__vaisala_json(db, 'weather_goto_vaisala', GOTO_VAISALA, 'gwindrange', start_str, end_str, - wind_range_offset=30000)) - data.update(__sensor_json(db, 'weather_goto_roomalert', GOTO_ROOMALERT, start_str, end_str)) - data.update(__sensor_json(db, 'weather_goto_dome2_roomalert', GOTO_DOME2_ROOMALERT, start_str, end_str)) - data.update(__sensor_json(db, 'weather_superwasp_roomalert', SUPERWASP_ROOMALERT, start_str, end_str)) - data.update(__sensor_json(db, 'weather_superwasp_aurora', SUPERWASP_AURORA, start_str, end_str)) - data.update(__sensor_json(db, 'weather_tng_seeing', TNG_SEEING, start_str, end_str)) - data.update(__sensor_json(db, 'weather_robodimm_seeing', ROBODIMM_SEEING, start_str, end_str)) - - db.close() - - return data, start_js, end_js - - -def infrastructure_json(date=None): - """Queries the data to be rendered on the "Infrastructure" dashboard page - If date is specified, returns data for the specified full day (UTC times) - If date is not specified, returns data for the last 6 hours. - - Returns a tuple of (, - , ) - """ - try: - start = datetime.datetime.strptime(date, '%Y-%m-%d') - datetime.timedelta(minutes=6) - end = start + datetime.timedelta(hours=24, minutes=6) - except Exception: - end = datetime.datetime.utcnow() - start = end - datetime.timedelta(hours=6, minutes=6) - - start_str = start.isoformat() - end_str = end.isoformat() - start_js = int(start.replace(tzinfo=datetime.timezone.utc).timestamp() * 1000) - end_js = int(end.replace(tzinfo=datetime.timezone.utc).timestamp() * 1000) - - db = pymysql.connect(db=DATABASE_DB, user=DATABASE_USER) - data = __sensor_json(db, 'weather_onemetre_ups', ONEMETRE_UPS, start_str, end_str) - data.update(__sensor_json(db, 'weather_rasa_ups', RASA_UPS, start_str, end_str)) - data.update(__sensor_json(db, 'weather_goto_ups', GOTO_UPS, start_str, end_str)) - data.update(__sensor_json(db, 'weather_superwasp_ups', SUPERWASP_UPS, start_str, end_str)) - data.update(__ping_json(db, 'weather_network', NETWORK, start_str, end_str)) - db.close() - - return data, start_js, end_js - - -def __query_weather_data(db, table, columns, start, end): - """Query columns from a weather database table. - Results are returned as a dictionary keyed by columns + date - with values as arrays of data between start and end - """ - query = 'SELECT `date`, `' + '`, `'.join(columns) \ - + '` from `' + table + '` WHERE `date` > ' + db.escape(start) \ - + ' AND `date` <= ' + db.escape(end) + ' ORDER BY `date` DESC;' - - results = { - 'date': [] - } - - for c in columns: - results[c] = [] - - with db.cursor() as cur: - cur.execute(query) - for r in cur: - results['date'].append(r[0]) - for i, column in enumerate(columns): - results[column].append(r[i + 1]) - - return results - - -def __generate_plot_data(label, color, date, series, data_break=360): - """Creates a plot data object suitable for plotting via javascript using flot. - date and series should be given in reverse chronological order - Lines are broken if there is a gap more than data_break seconds between points - """ - c = { - 'label': label, - 'color': color, - 'data': [], - 'max': 0, - 'min': 0 - } - - next_ts = None - for x, y in zip(date, series): - ts = x.replace(tzinfo=datetime.timezone.utc).timestamp() * 1000 - c['max'] = max(c['max'], y) - c['min'] = max(c['min'], y) - - # Insert a break in the plot line if there is a break between points - if next_ts is not None and next_ts - ts > data_break * 1000: - c['data'].append(None) - - c['data'].append((int(ts), y)) - next_ts = ts - - return c - - -def __sensor_json(db, table, channels, start, end, data_break=360): - """Queries data for an general-case sensor""" - results = __query_weather_data(db, table, list(channels.keys()), start, end) - data = {} - for key in channels: - label, name, color = channels[key] - - if name in CHANNEL_START_DATES: - filtered_date = [] - filtered = [] - channel_start = CHANNEL_START_DATES[name] - for d, value in zip(results['date'], results[key]): - if d > channel_start: - filtered_date.append(d) - filtered.append(value) - else: - filtered_date = results['date'] - filtered = results[key] - - data[name] = __generate_plot_data(label, color, filtered_date, filtered, - data_break=data_break) - - return data - - -def __superwasp_json(db, table, channels, start, end): - """Hacky workaround for bogus SuperWASP wind speed measurements""" - results = __query_weather_data(db, table, list(channels.keys()), start, end) - - # Filter bogus wind measurements - wind_date = [] - wind_speed = [] - wind_direction = [] - for date, speed, direction in zip(results['date'], - results['wind_speed'], - results['wind_direction']): - if speed < 200: - wind_date.append(date) - wind_speed.append(speed) - wind_direction.append(direction) - - data = {} - for key in channels: - label, name, color = channels[key] - if key == 'wind_speed': - data[name] = __generate_plot_data(label, color, wind_date, wind_speed) - elif key == 'wind_direction': - data[name] = __generate_plot_data(label, color, wind_date, wind_direction) - else: - data[name] = __generate_plot_data(label, color, results['date'], results[key]) - - return data - - -def __ping_json(db, table, channels, start, end): - """Queries data for the network ping graph, applying the -1 = invalid point filter""" - results = __query_weather_data(db, table, list(channels.keys()), start, end) - data = {} - for key in channels: - label, name, color = channels[key] - - date = [] - filtered = [] - for d, value in zip(results['date'], results[key]): - if value > 0: - date.append(d) - filtered.append(value) - - data[name] = __generate_plot_data(label, color, date, filtered) - - return data - - -def __vaisala_json(db, table, channels, wind_range_key, start, end, wind_range_offset=0): - """Queries data for the vaisala graphs, applying the _valid == 0 = invalid point filter""" - columns = list(channels.keys()) + [c + '_valid' for c in channels] - if wind_range_key and 'wind_speed' in columns: - columns += ['wind_gust', 'wind_gust_valid', 'wind_lull', 'wind_lull_valid'] - - results = __query_weather_data(db, table, columns, start, end) - - data = {} - for key in channels: - label, name, color = channels[key] - - date = [] - filtered = [] - for d, value, valid in zip(results['date'], results[key], results[key + '_valid']): - if valid: - date.append(d) - filtered.append(value) - - data[name] = __generate_plot_data(label, color, date, filtered) - - if wind_range_key and 'wind_gust' in columns: - minmax_data = [] - min_lull = None - max_gust = 0 - - for i in range(len(results['wind_gust'])): - if not results['wind_gust_valid'][i] or not results['wind_lull_valid'][i]: - continue - - ts = results['date'][i].replace(tzinfo=datetime.timezone.utc).timestamp() * 1000 - - mid = (results['wind_gust'][i] + results['wind_lull'][i]) / 2 - delta = (results['wind_gust'][i] - results['wind_lull'][i]) / 2 - - if min_lull is None: - min_lull = results['wind_lull'][i] - else: - min_lull = min(min_lull, results['wind_lull'][i]) - - if max_gust is None: - max_gust = results['wind_gust'][i] - else: - max_gust = max(max_gust, results['wind_gust'][i]) - - minmax_data.append((int(ts) + wind_range_offset, mid, delta)) - - data[wind_range_key] = { - 'label': '', - 'color': channels['wind_speed'][2], - 'points': { - 'radius': 0, - 'errorbars': 'y', - 'yerr': {'show': True}, - }, - 'data': minmax_data, - 'max': max_gust, - 'min': min_lull - } - - return data diff --git a/dashboard/templates/environment.html b/dashboard/templates/environment.html index 8733f07..b11aecb 100644 --- a/dashboard/templates/environment.html +++ b/dashboard/templates/environment.html @@ -7,7 +7,7 @@

{{ title|safe }}

-

Loading...

+

Loading...

@@ -25,41 +25,41 @@

Loading...

-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/dashboard/templates/infrastructure.html b/dashboard/templates/infrastructure.html index 278f80b..9ec5ff1 100644 --- a/dashboard/templates/infrastructure.html +++ b/dashboard/templates/infrastructure.html @@ -5,7 +5,7 @@

{{ title|safe }}

-

Loading...

+

Loading...

@@ -22,40 +22,22 @@

Loading...

-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
+
-
+
-
+
@@ -84,10 +66,12 @@
Authentication Required
{% endblock %} diff --git a/static/environment.css b/static/environment.css index 53d8326..e0b1d46 100644 --- a/static/environment.css +++ b/static/environment.css @@ -58,9 +58,8 @@ #windspeed-container { margin-top: -20px; } #wind-container { height: 320px; } -#main-ups-battery-container, #dome-ups-battery-container, #rasa-ups-battery-container, #goto-main-ups-battery-container, #goto-dome-ups-battery-container, #swasp-ups1-battery-container, #swasp-ups2-battery-container { height: 52px; } -#swasp-ups3-battery-container { height: 87px; margin-top: -5px; } -#dome-ups-battery-container, #rasa-ups-battery-container, #goto-main-ups-battery-container, #goto-main-ups-battery-container, #goto-dome-ups-battery-container, #swasp-ups1-battery-container, #swasp-ups2-battery-container { margin-top: -5px; } +#ups-battery-container, #ups-load-container { height: 193px; } +#ups-load-container { height: 228px; margin-top: -5px; } #network-internal-container { height: 130px; margin-top: 0px; } #network-external-container { height: 130px; margin-top: -5px; } #wasp-roofbattery-container { height: 166px; margin-top: -5px; } diff --git a/static/environment.js b/static/environment.js index 66e5557..df7ed5d 100755 --- a/static/environment.js +++ b/static/environment.js @@ -1,4 +1,5 @@ -var data = {}; +let data = {}; + function colorSeriesLabel(label, series) { return '' + label + ''; @@ -8,17 +9,17 @@ function redrawPlot() { if (!data.data) return; - var d = []; - var plot = $(this); + let d = []; + const plot = $(this); - var axis = 0; - if (plot.data('column') == 'right') + let axis = 0; + if (plot.data('column') === 'right') axis = 1; - var series = plot.data('series'); - var range = [undefined, undefined]; - for (var s in series) { - var sensor = data.data[series[s]]; + const type = plot.data('type'); + let range = [undefined, undefined]; + for (let sensor_name in data.data[type]) { + const sensor = data.data[type][sensor_name]; if (sensor) { d.push(sensor); range[0] = range[0] !== undefined ? Math.min(range[0], sensor['min']) : sensor['min']; @@ -26,47 +27,72 @@ function redrawPlot() { } } - var regenerateLinkedPlots = function(plot, options) { - regenerateBindings = true; - window.setTimeout(function() { + let no_data = false; + if (d.length === 0) { + no_data = true; + d.push({ + 'color': 'red', + 'label': 'NO DATA', + 'data': [] + }) + } + + let regenerateLinkedPlots = function (plot, options) { + let regenerateBindings = true; + window.setTimeout(function () { if (!regenerateBindings) return; - var plots = {}; - var getPlot = function(id) { return plots[id]; }; + let plots = {}; + let getPlot = function (id) { + return plots[id]; + }; - $('.weather-plot').each(function() { plots[$(this).attr('id')] = $(this).data('plot'); }); + $('.weather-plot').each(function () { + plots[$(this).attr('id')] = $(this).data('plot'); + }); // Link the plot hover together only after all plots have been created - for (var key in plots) { - var plot = plots[key]; - var linkedPlots = plot.getPlaceholder().data('linkedplots'); + for (let key in plots) { + let plot = plots[key]; + let linkedPlots = plot.getPlaceholder().data('linkedplots'); if (linkedPlots !== undefined) plot.getOptions().linkedplots = linkedPlots.map(getPlot); - }; + } regenerateBindings = false; }); } - var options = { - series: { shadowSize: 0 }, - axisLabels: { show: true }, - xaxis: { mode: 'time', minTickSize: [1, 'minute'], timeformat:'', min: data.start, max: data.end }, - grid: { margin: { left: axis == 0 ? 0 : 15, top: 0, right: axis == 1 ? 0 : 15, bottom: 0}, hoverable: true, autoHighlight: false }, - crosshair: { mode: "x", color: '#545454' }, - yaxis: { axisLabel: plot.data('axislabel'), axisLabelPadding: 9, labelWidth: 20 }, - legend: { noColumns: 6, units: plot.data('labelunits'), backgroundColor: '#252830', backgroundOpacity: 0.5, margin: 1, labelFormatter: colorSeriesLabel }, + let options = { + series: {shadowSize: 0}, + axisLabels: {show: true}, + xaxis: {mode: 'time', minTickSize: [1, 'minute'], timeformat: '', min: data.start, max: data.end}, + grid: { + margin: {left: axis === 0 ? 0 : 15, top: 0, right: axis === 1 ? 0 : 15, bottom: 0}, + hoverable: true, + autoHighlight: false + }, + crosshair: {mode: "x", color: '#545454'}, + yaxis: {axisLabel: plot.data('axislabel'), axisLabelPadding: 9, labelWidth: 20}, + legend: { + noColumns: 6, + units: plot.data('labelunits'), + backgroundColor: '#252830', + backgroundOpacity: 0.5, + margin: 1, + labelFormatter: colorSeriesLabel + }, linkedplots: [], - hooks: { bindEvents: bindHoverHooks, processOptions: regenerateLinkedPlots } + hooks: {bindEvents: bindHoverHooks, processOptions: regenerateLinkedPlots} }; if (plot.data('points') !== undefined) { - options.lines = { show: false }; - options.points = { show: true, fill: 1, radius: 2, lineWidth: 0, fillColor: false }; + options.lines = {show: false}; + options.points = {show: true, fill: 1, radius: 2, lineWidth: 0, fillColor: false}; } else { - options.lines = { show: true, lineWidth: 1 }; - options.points = { show: false }; + options.lines = {show: true, lineWidth: 1}; + options.points = {show: false}; } if (plot.data('ydecimals') !== undefined) @@ -75,15 +101,18 @@ function redrawPlot() { if (plot.data('labelfudge') !== undefined) options.yaxis.labelFudge = plot.data('labelfudge'); - if (plot.data('min') !== undefined) + if (plot.data('min') !== undefined && !no_data) options.yaxis.min = plot.data('min'); - if (plot.data('max')) + if (plot.data('max') !== undefined && !no_data) options.yaxis.max = plot.data('max'); else options.yaxis.max = range[0] + 1.5 * (range[1] - range[0]); - if (axis == 1) + if (no_data) + options.crosshair.mode = null; + + if (axis === 1) options.yaxis.position = 'right'; if (plot.data('hidetime') === undefined) { @@ -95,19 +124,19 @@ function redrawPlot() { } function setHoverXPosition(plot, offsetX) { - var axes = plot.getAxes(); - var offset = plot.getPlotOffset(); - var dataset = plot.getData(); - var options = plot.getOptions(); - var legend = plot.getPlaceholder().find('.legendLabel :first-child'); + let axes = plot.getAxes(); + let offset = plot.getPlotOffset(); + let dataset = plot.getData(); + let options = plot.getOptions(); + let legend = plot.getPlaceholder().find('.legendLabel :first-child'); - var start = axes.xaxis.p2c(axes.xaxis.min); - var end = axes.xaxis.p2c(axes.xaxis.max); - var fractionalPos = (offsetX - offset.left) / (end - start); + let start = axes.xaxis.p2c(axes.xaxis.min); + let end = axes.xaxis.p2c(axes.xaxis.max); + let fractionalPos = (offsetX - offset.left) / (end - start); if (fractionalPos < 0 || fractionalPos > 1) { // Clear labels - for (var i = 0; i < dataset.length; i++) + for (let i = 0; i < dataset.length; i++) $(legend.eq(i)).html(dataset[i].label); // Clear crosshair @@ -115,92 +144,86 @@ function setHoverXPosition(plot, offsetX) { return; } - var x = axes.xaxis.min + (axes.xaxis.max - axes.xaxis.min) * fractionalPos; - plot.setCrosshair({ x: x }); - for (var i = 0; i < dataset.length; i++) { - var series = dataset[i]; + let x = axes.xaxis.min + (axes.xaxis.max - axes.xaxis.min) * fractionalPos; + plot.setCrosshair({x: x}); + for (let i = 0; i < dataset.length; i++) { + let series = dataset[i]; - var j = 0; + let j = 0; for (; j < series.data.length; j++) - if (series.data[j] !== null && series.data[j][0] < x) + if (series.data[j] !== null && series.data[j][0] > x) break; - var p1 = series.data[j - 1]; - var p2 = series.data[j]; - + let p1 = series.data[j - 1]; + let p2 = series.data[j]; if (p1 != null && p2 != null) { - var y = (p1[1] + (p2[1] - p1[1]) * (x - p1[0]) / (p2[0] - p1[0])).toFixed(2); + let y = (p1[1] + (p2[1] - p1[1]) * (x - p1[0]) / (p2[0] - p1[0])).toFixed(2); $(legend.eq(i)).text(y + options.legend.units); - } - else + } else $(legend.eq(i)).html(dataset[i].label); } } function bindHoverHooks(plot, eventHolder) { - var axes = plot.getAxes(); - var offset = plot.getPlotOffset(); - var dataset = plot.getData(); - var options = plot.getOptions(); - var start = axes.xaxis.p2c(axes.xaxis.min); - var end = axes.xaxis.p2c(axes.xaxis.max); - - var linkedPlots = []; - eventHolder.mousemove(function(e) { + let options = plot.getOptions(); + + eventHolder.mousemove(function (e) { setHoverXPosition(plot, e.offsetX); - for (var i = 0; i < options.linkedplots.length; i++) - setHoverXPosition(options.linkedplots[i], e.offsetX); + for (let i = 0; i < options.linkedplots.length; i++) + setHoverXPosition(options.linkedplots[i], e.offsetX); }); - eventHolder.mouseout(function(e) { + eventHolder.mouseout(function (_) { setHoverXPosition(plot, -1); - for (var i = 0; i < options.linkedplots.length; i++) - setHoverXPosition(options.linkedplots[i], -1); + for (let i = 0; i < options.linkedplots.length; i++) + setHoverXPosition(options.linkedplots[i], -1); }); } function redrawWindPlot() { - var plot = $(this); - var speeds = [data.data.wwindspeed, data.data.gwindspeed, data.data.swwindspeed]; - - var getMaxSpeed = function(a,b) { - if (a === null && b === null) - return 0; - else if (a === null) - return b; - else if (b === null) - return a; - - return Math.max(a, b[1]); - }; + let plot = $(this); + + const type = plot.data('type'); + let max = undefined; + let d = [] + for (let sensor_name in data.data[type]) { + const sensor = data.data[type][sensor_name]; + d.push(sensor); + max = max !== undefined ? Math.max(max, sensor['max']) : sensor['max']; + } - var maxVaisala = data.data.wwindspeed.data.reduce(getMaxSpeed, 0); - var maxSWASP = data.data.swwindspeed.data.reduce(getMaxSpeed, 0); - var maxGOTO = data.data.gwindspeed.data.reduce(getMaxSpeed, 0); - var maxRadius = 1.1 * Math.max(maxVaisala, maxSWASP, maxGOTO, 15 / 1.1); + if (d.length === 0) { + d.push({ + 'color': 'red', + 'label': 'NO DATA', + 'data': [] + }) + } + + let maxRadius = 1.1 * Math.max(max, 15 / 1.1); function drawPoints(plot, ctx) { - var axes = plot.getAxes(); - var offset = plot.getPlotOffset(); + let axes = plot.getAxes(); + let offset = plot.getPlotOffset(); // Clip to plot area to prevent overdraw - var plotWidth = ctx.canvas.width - offset.right - offset.left; - var plotHeight = ctx.canvas.height - offset.top - offset.bottom; - var yScale = 1 / maxRadius; - var xScale = yScale * plotHeight / plotWidth; + let plotWidth = ctx.canvas.width - offset.right - offset.left; + let plotHeight = ctx.canvas.height - offset.top - offset.bottom; + let yScale = 1 / maxRadius; + let xScale = yScale * plotHeight / plotWidth; ctx.save(); ctx.rect(offset.left, offset.top, plot.width(), plot.height()); ctx.clip(); // Background axes - var dl = offset.left + axes.xaxis.p2c(-maxRadius * xScale); - var dt = offset.top + axes.yaxis.p2c(maxRadius * yScale); - var dr = offset.left + axes.xaxis.p2c(maxRadius * xScale); - var db = offset.top + axes.yaxis.p2c(-maxRadius * yScale); + let dl = offset.left + axes.xaxis.p2c(-maxRadius * xScale); + let dt = offset.top + axes.yaxis.p2c(maxRadius * yScale); + let dr = offset.left + axes.xaxis.p2c(maxRadius * xScale); + let db = offset.top + axes.yaxis.p2c(-maxRadius * yScale); - var hCenter = offset.left + axes.xaxis.p2c(0); - var vCenter = offset.top + axes.yaxis.p2c(0); + let hCenter = offset.left + axes.xaxis.p2c(0); + let vCenter = offset.top + axes.yaxis.p2c(0); ctx.strokeStyle = '#545454'; @@ -221,8 +244,9 @@ function redrawWindPlot() { // Background radial curves and tick labels $('#wind-plot .wind-labels').remove(); - for (var r = 0; r < maxRadius * 1.414 * plotWidth / plotHeight; r += 10) { - var cr = axes.yaxis.p2c(-yScale * r) - axes.yaxis.p2c(0); + let wind_plot = $('#wind-plot'); + for (let r = 0; r < maxRadius * 1.414 * plotWidth / plotHeight; r += 10) { + let cr = axes.yaxis.p2c(-yScale * r) - axes.yaxis.p2c(0); if (r > 0) { ctx.beginPath(); @@ -231,50 +255,45 @@ function redrawWindPlot() { } if (r * xScale < 0.975) { - var label = r == 0 ? '   ' + r: r; - var o = plot.pointOffset({ x: r * xScale, y: 0}); - $('#wind-plot').append('
' + label + '
'); + let label = r === 0 ? '   ' + r : r; + let o = plot.pointOffset({x: r * xScale, y: 0}); + wind_plot.append('
' + label + '
'); if (r > 0) { - o = plot.pointOffset({ x: -r * xScale, y: 0}); - $('#wind-plot').append('
' + label + '
'); + o = plot.pointOffset({x: -r * xScale, y: 0}); + wind_plot.append('
' + label + '
'); } } } // Background compass indicators - var labelHCenter = offset.left + plot.width() / 2; - var labelVCenter = offset.top + plot.height() / 2; - var labelPlotWidth = plot.width(); - var labelPlotHeight = plot.height(); - $('#wind-plot').append('
N
'); - $('#wind-plot').append('
E
'); - $('#wind-plot').append('
S
'); - $('#wind-plot').append('
W
'); + let labelHCenter = offset.left + plot.width() / 2; + let labelVCenter = offset.top + plot.height() / 2; + let labelPlotWidth = plot.width(); + let labelPlotHeight = plot.height(); + wind_plot.append('
N
'); + wind_plot.append('
E
'); + wind_plot.append('
S
'); + wind_plot.append('
W
'); // Historical data is drawn with constant opacity // This will be adjusted per-point if we are drawing live data ctx.globalAlpha = 0.4; // Data points - var series = plot.getData(); - for (var i = 0; i < series.length; i++) { - var dir = series[i]; - var speed = speeds[i]; - var r = series[i].points.radius; - ctx.fillStyle = series[i].color; - - for (var j = dir.data.length - 1; j >= 0; j--) { - if (speed.data[j] === null) - continue; + let series = plot.getData(); + for (let i = 0; i < series.length; i++) { + let s = series[i]; + let r = s.points.radius; + ctx.fillStyle = s.color; - var dy = speed.data[j][1] * Math.cos(dir.data[j][1] * Math.PI / 180); - var dx = speed.data[j][1] * Math.sin(dir.data[j][1] * Math.PI / 180); - var x = offset.left + axes.xaxis.p2c(xScale * dx); - var y = offset.top + axes.yaxis.p2c(yScale * dy); + for (let j = s.data.length - 1; j >= 0; j--) { + + let x = offset.left + axes.xaxis.p2c(xScale * s.data[j][1]); + let y = offset.top + axes.yaxis.p2c(yScale * s.data[j][2]); // Opacity scales from full at 0 age to 1 at 1 hour, then stays constant if (!dateString) - ctx.globalAlpha = Math.min(Math.max(0.1, 1 - (data.end - dir.data[j][0]) / 3600000), 1); + ctx.globalAlpha = Math.min(Math.max(0.1, 1 - (data.end - s.data[j][0]) / 3600000), 1); ctx.beginPath(); ctx.arc(x, y, r, 0, 2 * Math.PI, true); @@ -282,23 +301,18 @@ function redrawWindPlot() { } } - // Add a border around the most recent point - ctx.globalAlpha = 1.0; - if (!dateString) { ctx.strokeStyle = '#fff'; - for (var i = 0; i < series.length; i++) { - var dir = series[i]; - var speed = speeds[i]; - if (speed.data[0] === undefined) + for (let i = 0; i < series.length; i++) { + let s = series[i]; + if (s.data[0] === undefined) continue; - var dy = speed.data[0][1] * Math.cos(dir.data[0][1] * Math.PI / 180); - var dx = speed.data[0][1] * Math.sin(dir.data[0][1] * Math.PI / 180); - var x = offset.left + axes.xaxis.p2c(xScale * dx); - var y = offset.top + axes.yaxis.p2c(yScale * dy); + let x = offset.left + axes.xaxis.p2c(xScale * s.data[0][1]); + let y = offset.top + axes.yaxis.p2c(yScale * s.data[0][2]); + ctx.beginPath(); - ctx.arc(x, y, r, 0, 2 * Math.PI, true); + ctx.arc(x, y, s.points.radius, 0, 2 * Math.PI, true); ctx.stroke(); } } @@ -308,29 +322,39 @@ function redrawWindPlot() { ctx.beginPath(); ctx.closePath(); ctx.restore(); - }; + } - var axis = 0; - if (plot.data('column') == 'right') + let axis = 0; + if (plot.data('column') === 'right') axis = 1; - var options = { - lines: { show: false }, - xaxis: { min: -1, max: 1, tickLength: 0, axisLabel: ' ', axisLabelPadding: 0, ticks: [] }, - yaxis: { min: -1, max: 1, tickLength: 0, axisLabel: 'Wind (km/h)', axisLabelPadding: 29, ticks: [] }, - legend: { noColumns: 0, backgroundColor: '#252830', backgroundOpacity: 0.5, margin: 1, labelFormatter: colorSeriesLabel }, - grid: { margin: { left: axis == 0 ? 0 : 15, top: 0, right: axis == 1 ? 0 : 15, bottom: 0}, hoverable: true, autoHighlight: false }, - points: { show: false }, - hooks: { draw: [drawPoints] }, + let options = { + lines: {show: false}, + xaxis: {min: -1, max: 1, tickLength: 0, axisLabel: ' ', axisLabelPadding: 0, ticks: []}, + yaxis: {min: -1, max: 1, tickLength: 0, axisLabel: 'Wind (km/h)', axisLabelPadding: 29, ticks: []}, + legend: { + noColumns: 0, + backgroundColor: '#252830', + backgroundOpacity: 0.5, + margin: 1, + labelFormatter: colorSeriesLabel + }, + grid: { + margin: {left: axis === 0 ? 0 : 15, top: 0, right: axis === 1 ? 0 : 15, bottom: 0}, + hoverable: true, + autoHighlight: false + }, + points: {show: false}, + hooks: {draw: [drawPoints]}, }; - if (axis == 1) + if (axis === 1) options.yaxis.position = 'right'; - $.plot(this, [data.data.wwinddir, data.data.gwinddir, data.data.swwinddir], options); + $.plot(this, d, options); } -var queryUpdate; +let queryUpdate; var dateString = null; function queryData() { @@ -338,25 +362,26 @@ function queryData() { if (queryUpdate) window.clearTimeout(queryUpdate); - var url = dataURL; + let url = dataURL; if (dateString) url += '?date=' + dateString; - $('#headerdesc').text('Loading...'); + $('#data-updated').text('Loading...'); + $.ajax({ url: url, type: 'GET', dataType: 'json', - success: function(json) { + success: function (json) { data = json; $('.weather-plot').each(redrawPlot); $('.wind-plot').each(redrawWindPlot); if (dateString) - $('#headerdesc').text('Archived data for night of ' + dateString); + $('#data-updated').text('Archived data for night of ' + dateString); else - $('#headerdesc').text('Live data (updated ' + formatUTCDate(new Date(data.end)) + ' UTC)'); + $('#data-updated').text('Live data (updated ' + formatUTCDate(new Date(data.end)) + ' UTC)'); } }); @@ -366,12 +391,12 @@ function queryData() { } function setup() { - var init = true; + let init = true; // Automatically switch label side based on column layout $('.weather-plot').each(function() { var plot = $(this); - var redraw = $(this).attr('id') == 'wind-plot' ? redrawWindPlot : redrawPlot; + var redraw = $(this).attr('id') === 'wind-plot' ? redrawWindPlot : redrawPlot; var sensor = $(this).data('sidesensor'); if (!sensor) return; @@ -379,11 +404,11 @@ function setup() { var onResize = function() { var float = $('#'+sensor).css('float'); var updated = false; - if (float == 'none' && plot.data('column') != 'left') { + if (float === 'none' && plot.data('column') !== 'left') { plot.data('column', 'left'); updated = true; } - else if (float == 'left' && plot.data('column') != 'right') { + else if (float === 'left' && plot.data('column') !== 'right') { plot.data('column', 'right'); updated = true; } @@ -396,7 +421,7 @@ function setup() { onResize(); }); - var picker = $('#datepicker'); + var picker = $('#datepicker'); if (picker.length) { var setDataSource = function() { // e.date is in local time, but we need UTC - fetch it directly from the picker. @@ -435,5 +460,4 @@ function setup() { init = false; queryData(); } -}; - +}