// Dashboard UI functions.
//
// This is shared between all HTML pages.
'use strict';
// Append a message to an element. Used for errors.
function appendMessage(elem, msg) {
elem.innerHTML += msg + '
';
}
// jQuery-like AJAX helper, but simpler.
// Requires an element with id "status" to show errors.
//
// Args:
// errElem: optional element to append error messages to. If null, then
// alert() on error.
// success: callback that is passed the xhr object.
function ajaxGet(url, errElem, success) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true /*async*/);
xhr.onreadystatechange = function() {
if (xhr.readyState != 4 /*DONE*/) {
return;
}
if (xhr.status != 200) {
var msg = 'ERROR requesting ' + url + ': ' + xhr.status + ' ' +
xhr.statusText;
if (errElem) {
appendMessage(errElem, msg);
} else {
alert(msg);
}
return;
}
success(xhr);
};
xhr.send();
}
// Load metadata about the metrics.
// metric-metadata.json is just 14 KB, so we load it for every page.
//
// callback:
// on metric page, just pick out the right description.
// on overview page, populate them ALL with tool tips?
// Or create another column?
function loadMetricMetadata(errElem, success) {
// TODO: Should we make metric-metadata.json optional? Some may not have it.
ajaxGet('metric-metadata.json', errElem, function(xhr) {
// TODO: handle parse error
var m = JSON.parse(xhr.responseText);
success(m);
});
}
// for overview.html.
function initOverview(urlHash, tableStates, statusElem) {
ajaxGet('cooked/overview.part.html', statusElem, function(xhr) {
var elem = document.getElementById('overview');
elem.innerHTML = xhr.responseText;
makeTablesSortable(urlHash, [elem], tableStates);
updateTables(urlHash, tableStates, statusElem);
});
loadMetricMetadata(statusElem, function(metadata) {
var elem = document.getElementById('metricMetadata').tBodies[0];
var metrics = metadata.metrics;
// Sort by the metric name
var metricNames = Object.getOwnPropertyNames(metrics);
metricNames.sort();
var tableHtml = '';
for (var i = 0; i < metricNames.length; ++i) {
var name = metricNames[i];
var meta = metrics[name];
tableHtml += '
';
tableHtml += '' + name + ' | ';
tableHtml += '' + meta.owners + ' | ';
tableHtml += '' + meta.summary + ' | ';
tableHtml += '
';
}
elem.innerHTML += tableHtml;
});
}
// for metric.html.
function initMetric(urlHash, tableStates, statusElem, globals) {
var metricName = urlHash.get('metric');
if (metricName === undefined) {
appendMessage(statusElem, "Missing metric name in URL hash.");
return;
}
loadMetricMetadata(statusElem, function(metadata) {
var meta = metadata.metrics[metricName];
if (!meta) {
appendMessage(statusElem, 'Found no metadata for ' + metricName);
return;
}
var descElem = document.getElementById('metricDesc');
descElem.innerHTML = meta.summary;
// TODO: put owners at the bottom of the page somewhere?
});
// Add title and page element
document.title = metricName;
var nameElem = document.getElementById('metricName');
nameElem.innerHTML = metricName;
// Add correct links.
var u = document.getElementById('underlying-status');
u.href = 'cooked/' + metricName + '/status.csv';
var distUrl = 'cooked/' + metricName + '/dist.csv';
var u2 = document.getElementById('underlying-dist');
u2.href = distUrl;
ajaxGet(distUrl, statusElem, function(xhr) {
var csvData = xhr.responseText;
var elem = document.getElementById('proportionsDy');
// Mutate global so we can respond to onclick.
globals.proportionsDygraph = new Dygraph(elem, csvData, {customBars: true});
});
var numReportsUrl = 'cooked/' + metricName + '/num_reports.csv';
ajaxGet(numReportsUrl, statusElem, function(xhr) {
var csvData = xhr.responseText;
var elem = document.getElementById('num-reports-dy');
var g = new Dygraph(elem, csvData);
});
var massUrl = 'cooked/' + metricName + '/mass.csv';
ajaxGet(massUrl, statusElem, function(xhr) {
var csvData = xhr.responseText;
var elem = document.getElementById('mass-dy');
var g = new Dygraph(elem, csvData);
});
var tableUrl = 'cooked/' + metricName + '/status.part.html';
ajaxGet(tableUrl, statusElem, function(xhr) {
var htmlData = xhr.responseText;
var elem = document.getElementById('status_table');
elem.innerHTML = htmlData;
makeTablesSortable(urlHash, [elem], tableStates);
updateTables(urlHash, tableStates, statusElem);
});
}
// NOTE: This was for optional Dygraphs error bars, but it's not hooked up yet.
function onMetricCheckboxClick(checkboxElem, proportionsDygraph) {
var checked = checkboxElem.checked;
if (proportionsDygraph === null) {
console.log('NULL');
}
proportionsDygraph.updateOptions({customBars: checked});
console.log('HANDLED');
}
// for day.html.
function initDay(urlHash, tableStates, statusElem) {
var jobId = urlHash.get('jobId');
var metricName = urlHash.get('metric');
var date = urlHash.get('date');
var err = '';
if (!jobId) {
err = 'jobId missing from hash';
}
if (!metricName) {
err = 'metric missing from hash';
}
if (!date) {
err = 'date missing from hash';
}
if (err) {
appendMessage(statusElem, err);
}
// Add title and page element
var titleStr = metricName + ' on ' + date;
document.title = titleStr;
var mElem = document.getElementById('metricDay');
mElem.innerHTML = titleStr;
// Add correct links.
var u = document.getElementById('underlying');
u.href = '../' + jobId + '/raw/' + metricName + '/' + date +
'/results.csv';
// Add correct links.
var u_res = document.getElementById('residual');
u_res.src = '../' + jobId + '/raw/' + metricName + '/' + date +
'/residual.png';
var url = '../' + jobId + '/cooked/' + metricName + '/' + date + '.part.html';
ajaxGet(url, statusElem, function(xhr) {
var htmlData = xhr.responseText;
var elem = document.getElementById('results_table');
elem.innerHTML = htmlData;
makeTablesSortable(urlHash, [elem], tableStates);
updateTables(urlHash, tableStates, statusElem);
});
}
// for assoc-overview.html.
function initAssocOverview(urlHash, tableStates, statusElem) {
ajaxGet('cooked/assoc-overview.part.html', statusElem, function(xhr) {
var elem = document.getElementById('overview');
elem.innerHTML = xhr.responseText;
makeTablesSortable(urlHash, [elem], tableStates);
updateTables(urlHash, tableStates, statusElem);
});
}
// for assoc-metric.html.
function initAssocMetric(urlHash, tableStates, statusElem) {
var metricName = urlHash.get('metric');
if (metricName === undefined) {
appendMessage(statusElem, "Missing metric name in URL hash.");
return;
}
// Add title and page element
var title = metricName + ': pairs of variables';
document.title = title;
var pageTitleElem = document.getElementById('pageTitle');
pageTitleElem.innerHTML = title;
// Add correct links.
var u = document.getElementById('underlying-status');
u.href = 'cooked/' + metricName + '/metric-status.csv';
var csvPath = 'cooked/' + metricName + '/metric-status.part.html';
ajaxGet(csvPath, statusElem, function(xhr) {
var elem = document.getElementById('metric_table');
elem.innerHTML = xhr.responseText;
makeTablesSortable(urlHash, [elem], tableStates);
updateTables(urlHash, tableStates, statusElem);
});
}
// Function to help us find the *.part.html files.
//
// NOTE: This naming convention matches the one defined in task_spec.py
// AssocTaskSpec.
function formatAssocRelPath(metricName, var1, var2) {
var varDir = var1 + '_X_' + var2.replace('..', '_');
return metricName + '/' + varDir;
}
// for assoc-pair.html
function initAssocPair(urlHash, tableStates, statusElem, globals) {
var metricName = urlHash.get('metric');
if (metricName === undefined) {
appendMessage(statusElem, "Missing metric name in URL hash.");
return;
}
var var1 = urlHash.get('var1');
if (var1 === undefined) {
appendMessage(statusElem, "Missing var1 in URL hash.");
return;
}
var var2 = urlHash.get('var2');
if (var2 === undefined) {
appendMessage(statusElem, "Missing var2 in URL hash.");
return;
}
var relPath = formatAssocRelPath(metricName, var1, var2);
// Add title and page element
var title = metricName + ': ' + var1 + ' vs. ' + var2;
document.title = title;
var pageTitleElem = document.getElementById('pageTitle');
pageTitleElem.innerHTML = title;
// Add correct links.
var u = document.getElementById('underlying-status');
u.href = 'cooked/' + relPath + '/pair-status.csv';
/*
var distUrl = 'cooked/' + metricName + '/dist.csv';
var u2 = document.getElementById('underlying-dist');
u2.href = distUrl;
*/
var tableUrl = 'cooked/' + relPath + '/pair-status.part.html';
ajaxGet(tableUrl, statusElem, function(xhr) {
var htmlData = xhr.responseText;
var elem = document.getElementById('status_table');
elem.innerHTML = htmlData;
makeTablesSortable(urlHash, [elem], tableStates);
updateTables(urlHash, tableStates, statusElem);
});
}
// for assoc-day.html.
function initAssocDay(urlHash, tableStates, statusElem) {
var jobId = urlHash.get('jobId');
var metricName = urlHash.get('metric');
var var1 = urlHash.get('var1');
var var2 = urlHash.get('var2');
var date = urlHash.get('date');
var err = '';
if (!jobId) {
err = 'jobId missing from hash';
}
if (!metricName) {
err = 'metric missing from hash';
}
if (!var1) {
err = 'var1 missing from hash';
}
if (!var2) {
err = 'var2 missing from hash';
}
if (!date) {
err = 'date missing from hash';
}
if (err) {
appendMessage(statusElem, err);
}
// Add title and page element
var titleStr = metricName + ': ' + var1 + ' vs. ' + var2 + ' on ' + date;
document.title = titleStr;
var mElem = document.getElementById('metricDay');
mElem.innerHTML = titleStr;
var relPath = formatAssocRelPath(metricName, var1, var2);
// Add correct links.
var u = document.getElementById('underlying');
u.href = '../' + jobId + '/raw/' + relPath + '/' + date +
'/assoc-results.csv';
var url = '../' + jobId + '/cooked/' + relPath + '/' + date + '.part.html';
ajaxGet(url, statusElem, function(xhr) {
var htmlData = xhr.responseText;
var elem = document.getElementById('results_table');
elem.innerHTML = htmlData;
makeTablesSortable(urlHash, [elem], tableStates);
updateTables(urlHash, tableStates, statusElem);
});
}
// This is the onhashchange handler of *all* HTML files.
function onHashChange(urlHash, tableStates, statusElem) {
updateTables(urlHash, tableStates, statusElem);
}