Skip to content
Snippets Groups Projects
update.js 14.24 KiB
var Plotly = require('Plotly');
var ticktext = require('./ticks.js');
var _ = require('underscore');


function removeOldPoints(dataCache, removeCount) {
    for (var i = 0; i < dataCache.length; i++) {
        dataCache[i].x.splice(0, removeCount);

        if (dataCache[i].hasOwnProperty('z')) {
            for (var j = 0; j < dataCache[i].z.length; j++) {
                dataCache[i].z[j].splice(0, removeCount);
            }
        } else if (dataCache[i].hasOwnProperty('y')) {
            // delete things from y *only* if we don't have a Z
            dataCache[i].y.splice(0, removeCount);
        }
        if (dataCache[i].hasOwnProperty('text')) {
            dataCache[i].text.splice(0, removeCount);
        }
    }
}


function updateHeader(dataCache, unitCount, layoutUpdates) {
   // Note: curr_date is 2 minutes behind the current time.
   var curr_date = new Date(dataCache[0]['x'][dataCache[0]['x'].length - 1]);
   var hour = Number(curr_date.toLocaleString("en-US", {hour: 'numeric', 'hour12': false}));
   // Make the screen go black between 1 and 5 am.
   if (0 < hour && hour < 5) {
       document.getElementById("overlay").style.opacity = 1;
   } else {
       document.getElementById("overlay").style.opacity = 0;
   }
   var date_local = curr_date.toLocaleString("en-US",
       { year: 'numeric', month: 'short', day: 'numeric',
         hour: 'numeric', minute: 'numeric', second: 'numeric' }).replace(/,([^,]*)$/, '$1');
   var utcMonth = Number(curr_date.getUTCMonth());
   utcMonth = utcMonth < 9 ? '0' + (utcMonth + 1) : utcMonth + 1;
   var utcDay = Number(curr_date.getUTCDate()) < 9 ? '0' + curr_date.getUTCDate() : curr_date.getUTCDate();
   var utcMinutes = Number(curr_date.getUTCMinutes()) < 9 ? '0' + curr_date.getUTCMinutes() : curr_date.getUTCMinutes();
   var utcSeconds = Number(curr_date.getUTCSeconds()) < 9 ? '0' + curr_date.getUTCSeconds() : curr_date.getUTCSeconds();
   var date_utc = curr_date.getUTCFullYear() + '-' + utcMonth + '-' + utcDay + ' ' +
       curr_date.getUTCHours() + ':' + utcMinutes + ':' + utcSeconds;
   var elevation = unitCount == 0 ? '327.5 meters' : '1074.5 feet';
   layoutUpdates['annotations.1.text'] = 'Data last recorded at: ' + date_utc + 'Z [' + date_local + ' Local]          RIG Elevation = ' + elevation;
}



function changeCamera() {
    var directions = {'north': 'east', 'east': 'south', 'south': 'west', 'west': 'north',};
    // Gets witch direction the current camera is showing.
    var newDirection = directions[document.getElementById('roofCam').src.split('/')[7]];
    // Makes image update. Image updates every (about) 2 minutes and 15 seconds.
    document.getElementById('roofCam').alt = document.getElementById('dirText').innerHTML = newDirection;
    document.getElementById('roofCam').src = METOBS_API_URL + '/pub/cache/aoss/cameras/' + newDirection +
                                             '/latest_orig.jpg?t=' + new Date().getTime();
}


function changeUnits(dataCache, graphDiv, plotsInfo, unitCount) {
    // Note: unitCount alternates between 0 and 1.
    var plotName;
    var plotInfo;
    var currData;
    var tick_dates;
    // This changes the graphDiv y-axis we are getting.
    var axis_number;
    // This changes the plotInfo y-axis we are getting, which switches between units every time changeUnits is called.
    var yaxis = unitCount == 0 ? 'yaxis' : 'yaxis2';
    var converted_tickvals;
    var tickvals;
    var layoutUpdates = {};
    updateHeader(dataCache, unitCount, layoutUpdates);
    // Update  each subgraph's displayed y-axis and current data values.
    for (var i = 0; i < dataCache.length; i++) {
        axis_number = i == 0 ? '' : i + 1;
        tick_dates = dataCache[i]['x'];
        // Prevents '-6h' from falling off x-axis.
        layoutUpdates['xaxis' + axis_number + '.tickvals'] = [ tick_dates[0], tick_dates[Math.round(tick_dates.length / 2)], tick_dates[tick_dates.length - 1]]
        plotName = dataCache[i].plot_name;
        plotInfo = plotsInfo.plots[plotName];
        // Convert data to the correct units.
        currData = ticktext.getTickText([dataCache[i]['y'][dataCache[i]['y'].length - 1]], plotInfo[yaxis])[0];
        layoutUpdates['annotations[' + (3 + 2 * i) + '].text'] = currData + '' + plotInfo[yaxis]['units']
        tickvals = graphDiv.layout['yaxis' + axis_number]['tickvals']
        if (tickvals != undefined) {
            // Relative Humidity and Wind Direction ticktext and tickvals are static from site_configs_lobby.js.
            if (plotName == 'wind_direction' || plotName == 'rel_hum') {
                converted_tickvals = plotInfo[yaxis]['ticktext'];
            } else {
                converted_tickvals = [];
                for (var j = 0; j < tickvals.length; j++) {
                    converted_tickvals.push(ticktext.getTickText([tickvals[j]], plotInfo[yaxis]))
                }
            }
            layoutUpdates['yaxis' + axis_number + '.ticktext'] = converted_tickvals;
        }
    }
    // Updates everything at once to save time.
    Plotly.relayout(graphDiv, layoutUpdates);
    changeCamera();
}


function fitGraphs(dataCache, data, var_name, layoutUpdates, yaxis, defaultTickVals) {
    // Set the range of each graph if applicable.
    if (var_name != 'aoss.tower.rel_hum' && var_name != 'aoss.tower.wind_direction') {
        // data only contains new data. Thus we must keep track of old mins/maxs.
        if (dataCache['y'].length == 0) {
            // dataCache['y'] gets overridden somewhere.
            dataCache.yMin = Math.min(...data)
            dataCache.yMax = Math.max(...data)
        } else {
            dataCache.yMin = Math.min(...data, dataCache.yMin)
            dataCache.yMax = Math.max(...data, dataCache.yMax)
        }

        var spread = dataCache.yMax - dataCache.yMin;
        var bottom = dataCache.yMin - spread;
        var top = var_name == 'aoss.tower.solar_flux' ? dataCache.yMax : dataCache.yMax + spread;
        // Make graph visible if flat line: Add a margin equal to 10% of the value if non-zero, else scale from 0 to 1.
        if (spread == 0) {
            // Note: top == bottom since spread == 0.
            if (top > 0) {
                top = top * 1.1
                bottom = bottom * .9
            } else if (top < 0) { // Only applies to air_temp and dewpoint.
                top = top * .9
                bottom = bottom * 1.1
            } else { // Mainly for precipitation.
                top = .1
                bottom = -.1
            }
        }
        // Used to lower bottom so that the graph can be seen if it is 0.
        var tick0 = bottom
        // If bottom < 0: Make the bottom 0 since values cannot be negative,
        // and lower the bottom to see 0.
        if (var_name != 'aoss.tower.air_temp' && var_name != 'aoss.tower.dewpoint' && bottom <= 0) {
            bottom = 0;
            tick0 = 0;
        }
        // Shifts the bottom down so that tick0 doesn't overlap xaxis ticks, and graphs near zero can be seen.
        bottom -= .075 * spread + .001;
        layoutUpdates[yaxis + '.tickvals'] = [tick0, tick0 + (top - tick0) / 4, tick0 + 2 * (top - tick0) / 4,
                                              tick0 + 3 * (top - tick0) / 4, top]
        layoutUpdates[yaxis + '.range'] = [bottom, top]
    } else {
        layoutUpdates[yaxis + '.tickvals'] = defaultTickVals
    }
    // layoutUpdates is changed without having to return it.
}


function replaceData(dataCache, graphDiv, plotsInfo, dataObj, update, maxPoints) {
    var dates = dataObj.dates;
    var plotName;
    var plotInfo;
    var i;
    var probeText;
    var plotHandled = [];
    var forceRedraw = false;

    // scatter updates
    var tracesToExtend = [];
    var extendData = {
        x: [],
        y: [],
        text: [],
    };

    // contour updates
    var newContourData;
    var contourTracesToExtend = [];
    var contourExtendData = {
        x: [],
        z: [],
        text: [],
    };

    var traceIndex = 0;

    // Variables used for lobby display.
    var layoutUpdates = {};
    var axis_number;
    /**
     *
     * If we are updating the plots instead of replacing them, then try
     * to use plotly's extendTraces to update the x and y coordinates as well
     * the hovering probe text. This should only add new trace points instead
     * of updating all points.
     *
     * Since we passed 'dataCache' to plotly's 'plot' function during
     * initialization any changes to 'dataCache' should be mirrored in Plotly's
     * plot and vice versa. See the below stackoverflow question for details.
     * https://stackoverflow.com/questions/45759582/plotly-how-to-discard-older-points
     * This is important since we use `.shift()` on the data arrays to remove
     * "old" elements.
     *
     * We have to operate per-plot because some of our plots hover/probe labels
     * use the trace values to come up with one probe box
     * (air temp + dewpoint -> relative humidity).
     *
     */
    for (i = 0; i < dataCache.length; i++) {
        plotName = dataCache[i].plot_name;
        if (plotHandled[plotHandled.length - 1] == plotName) {
            // this happens when there are more than one trace in a plot
            continue;
        }
        plotInfo = plotsInfo.plots[plotName];
        _.each(plotInfo.traces, function (traceInfo, index) {

            if (dataCache[traceIndex + index].type == 'contour') {
                // push new dates for each trace to be extended
                contourExtendData.x.push(dates);
                contourTracesToExtend.push(traceIndex + index);
                // update the Z data instead of the traditional 'y' data
                // assumes Y doesn't update between calls
                newContourData = _.map(traceInfo.var_names, function(var_name) {return dataObj[var_name];});
                dataCache[i].text.push(...ticktext.getProbeText(newContourData, plotInfo, traceInfo, dataObj));
                for (var j = 0; j < traceInfo.var_names.length; j++) {
                    while (j >= dataCache[i].z.length) {
                        dataCache[i].z.push([]);
                    }
                    dataCache[i].z[j].push(...dataObj[traceInfo.var_names[j]]);  // ES6-style push
                }
                contourExtendData.z.push(dataCache[i].z);
                contourExtendData.text.push(dataCache[i].text);
                // If plotly could handle extendTraces in the x-dimension we'd
                // probably do something like this:
                // newContourData = _.map(traceInfo.var_names, function(var_name) {return dataObj[var_name];});
                // contourExtendData.z.push(newContourData);
                // contourExtendData.text.push(
                //     [ticktext.getProbeText(newContourData, plotInfo, traceInfo, dataObj)]
                // );
            } else {
                // push new dates for each trace to be extended
                extendData['x'].push(dates);
                tracesToExtend.push(traceIndex + index);
                // scatter plots, assume the y data is the data being updated
                _.each(traceInfo.var_names, function (var_name) {
                    if (plotsInfo['isLobbyDisplay']) {
                        axis_number = i == 0 ? '': i + 1;
                        fitGraphs(dataCache[i], dataObj[var_name], var_name, layoutUpdates, 'yaxis' + axis_number, plotInfo['yaxis2']['tickvals'])
                    }
                    // push the entire new data array as one element to update
                    extendData.y.push(dataObj[var_name]);

                    // Get new probe text shown when hovering over the traces
                    if ('probe_text' in traceInfo && traceInfo['probe_text'] === null) {
                        // we don't want probe text for this trace
                        probeText = [undefined];
                    } else {
                        probeText = ticktext.getProbeText(dataObj[var_name], plotInfo, traceInfo, dataObj);
                    }
                    extendData.text.push(probeText);
                });
            }
        });
        traceIndex += plotInfo.traces.length;
        plotHandled.push(plotName);
    }

    // Have plotly update the graph with the new points in extendData
    // This will add data to 'dataCache' because of how we originally
    // created the plots.
    if (tracesToExtend.length > 0) {
        Plotly.extendTraces(graphDiv, extendData, tracesToExtend);
    }
    if (contourTracesToExtend.length > 0) {
        Plotly.extendTraces(graphDiv, contourExtendData, contourTracesToExtend);
        // we are currently cheating and re-adding every "row" of data because
        // plotly doesn't support extendTrace'ing in the x-dimension
        // we want to remove the previous "depth" amount of rows
        for (i = 0; i < contourTracesToExtend.length; i++) {
            dataCache[contourTracesToExtend[i]].z.splice(0, dataCache[contourTracesToExtend[i]].y.length);
            dataCache[contourTracesToExtend[i]].text.splice(0, dataCache[contourTracesToExtend[i]].y.length);
        }
    }

    if (maxPoints == 0 && !update) {
        // we aren't doing realtime updates and the caller wants us to remove
        // all previous points and keep all new points
        maxPoints = dataObj.dates.length;
        // we have to force a redraw of the plot because the removed plots
        // will still exist on the the graph otherwise (until the next update)
        forceRedraw = true;
    }
    // If adding these new points gave us more points than we need
    // remove the points at the beginning of our data
    if (maxPoints > 0 && dataCache[0]['x'].length > maxPoints) {
        removeOldPoints(dataCache, dataCache[0].x.length - maxPoints);
    }

    // If lobby display and data to update.
    if (Object.keys(layoutUpdates).length != 0) {
        // Updates everything at once to save time. Should be done after all dataCache manipulations.
        Plotly.relayout(graphDiv, layoutUpdates);
        // The first time through, call changeUnits to display current data.
        if (!update) {
            changeUnits(dataCache, graphDiv, plotsInfo, 0);
        }
    }
    if (forceRedraw) {
        Plotly.redraw(graphDiv);
    }
}


function updatePlot(dataCache, graphDiv, plotsInfo, dataObj, update, maxPoints) {
    replaceData(dataCache, graphDiv, plotsInfo, dataObj, update, maxPoints);
}

module.exports.updatePlot = updatePlot;
module.exports.changeUnits = changeUnits;