-
David Hoese authoredDavid Hoese authored
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;