aboutsummaryrefslogtreecommitdiff
path: root/ui/ui.js
blob: b74a8e2eb3ce6b94951b60b912a393137c1eed5e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
// 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 + '<br />';
}

// 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 += '<tr>';
      tableHtml += '<td>' + name + '</td>';
      tableHtml += '<td>' + meta.owners + '</td>';
      tableHtml += '<td>' + meta.summary + '</td>';
      tableHtml += '</tr>';
    }
    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);
}