Chromium Code Reviews| Index: chrome/browser/resources/performance_monitor/chart.js |
| diff --git a/chrome/browser/resources/performance_monitor/chart.js b/chrome/browser/resources/performance_monitor/chart.js |
| index 4497f588e10a4d017540819932c605ee7312907e..8109cab5e7ace5b40537e0e4e5acfdcfea422a3d 100644 |
| --- a/chrome/browser/resources/performance_monitor/chart.js |
| +++ b/chrome/browser/resources/performance_monitor/chart.js |
| @@ -4,16 +4,25 @@ |
| 'use strict'; |
| -google.load('visualization', '1', {packages: ['corechart']}); |
| - |
| -function $(criterion) { |
| +function querySelect(criterion) { |
| return document.querySelector(criterion); |
| } |
| + |
| var controller = new function() { |
| - // Tabular setup for various time ranges, giving a descriptive name, time span |
| - // prior to |now|, data point resolution, and time-label frequency and format |
| - // for each. |
| + /** |
| + * @enum {{ |
| + * value: !number, |
| + * name: !string, |
| + * timeSpan: !number, |
| + * resolution: !number, |
| + * labelEvery: !number, |
| + * format: !string |
| + * }} |
| + * |
| + * Enum for time ranges, giving a descriptive name, time span prior to |now|, |
| + * data point resolution, and time-label frequency and format for each. |
| + */ |
| this.TimeRange = { |
| // Prior day, resolution of 2 min, at most 720 points, |
| // Labels at 90 point (3 hour) intervals |
| @@ -36,162 +45,351 @@ var controller = new function() { |
| resolution: 1000 * 3600 * 3, labelEvery: 112, format: 'M/yy'}, |
| }; |
| - // Parent container of all line graphs |
| - this.chartDiv = $('#charts'); |
| + /** @type {Object.<string, { |
| + * divs: !Array.<DOMElement>, |
| + * yAxis: !{max: !number, color: !string}, |
| + * data: ?Array.<{time: !number, value: !number}>, |
| + * description: !string, |
| + * units: !string |
| + * }>} |
| + * |
| + * All metrics have entries, but those not displayed have an empty div list. |
| + * If a div list is not empty, the associated data will be non-null, or |
| + * null but about to be filled by webui response. Any metric with non-empty |
| + * div list but null data is awaiting a data response from the webui. |
|
koz (OOO until 15th September)
2012/07/02 06:14:48
This last sentence seems to be just a repetition o
clintstaley
2012/07/02 19:09:05
I was repeating for clarification. (Teacher's ins
|
| + */ |
| + this.metricMap = { |
| + jankiness: { |
| + divs: [], |
| + yAxis: {max: 100, color: 'rgb(255, 128, 128)'}, |
| + data: null, |
| + description: 'Jankiness', |
| + units: 'milliJanks' |
| + }, |
| + oddness: { |
| + divs: [], |
| + yAxis: {max: 20, color: 'rgb(0, 192, 0)'}, |
| + data: null, |
| + description: 'Oddness', |
| + units: 'kOdds' |
| + } |
| + }; |
| + |
| + /** @type {Object.<string, { |
| + * divs: !Array.<DOMElement>, |
| + * description: !string, |
| + * color: !string, |
| + * data: ?Array.<{time: !number, longDescription: !string}> |
| + * }>} |
| + * |
| + * Similar data for events, though no yAxis info is needed since events |
| + * are simply labelled markers at X locations. Rules regarding null data |
| + * with non-empty div list apply here as for metricMap above. |
| + */ |
| + this.eventMap = { |
| + wampusAttacks: { |
| + divs: [], |
| + description: 'Wampus Attack', |
| + color: 'rgb(0, 0, 255)', |
| + data: null |
| + }, |
| + solarEclipses: { |
| + divs: [], |
| + description: 'Solar Eclipse', |
| + color: 'rgb(255, 0, 255)', |
| + data: null |
| + } |
| + }; |
| + |
| + /** |
| + * @type {!Array.<{start: !number, end: !number}>} |
| + * Time periods in which the browser was active and collecting metrics |
| + * and events. |
| + */ |
| + this.intervals = []; |
| + |
| + /** |
| + * Set up the radio button set to choose time range. Use div#radioTemplate |
| + * as a template. |
| + */ |
| + this.setupTimeRangeChooser = function() { |
| + var timeDiv = querySelect('#chooseTimeRange'); |
| + var radioTemplate = querySelect('#radioTemplate'); |
| + var controller = this; |
| + |
| + for (var time in this.TimeRange) { |
| + var timeRange = this.TimeRange[time]; |
| + var radio = radioTemplate.cloneNode(true); |
| + var input = radio.querySelector('input'); |
| + |
| + input.value = timeRange.value; |
| + input.timeRange = timeRange; |
| + radio.querySelector('span').innerText = timeRange.name; |
| + timeDiv.appendChild(radio); |
| + timeRange.element = input; |
| + } |
| + timeDiv.addEventListener('click', function(e) { |
| + if (!e.target.webkitMatchesSelector('input[type="radio"]')) |
| + return; |
| - // Parent container of checkboxes to choose metrics to display |
| - this.chooseMetricsDiv = $('#chooseMetrics'); |
| + controller.setTimeRange(e.target.timeRange); |
| + }); |
| + }; |
| - // Parent container of checkboxes to choose event types to display |
| - this.chooseEventsDiv = $('#chooseEvents'); |
| + /** |
| + * Generalized function for setting up checkbox blocks for either events |
| + * or metrics. Take a div ID |divId| into which to place the checkboxes, |
| + * and a map |optionMap| with values that each include a property |
| + * |description|. Set up one checkbox for each entry in |optionMap| |
| + * labelled with that description. Arrange callbacks to function |check| |
| + * or |uncheck|, passing them the key of the checked or unchecked option, |
| + * when the relevant checkbox state changes. |
| + * |
| + * @param {!string} divId Id of division into which to put checkboxes |
| + * @param {!Object} optionMap map of metric/event entries |
| + * @param {!function(this:Controller, Object)} check |
| + * function to select an entry (metric or event) |
| + * @param {!function(this:Controller, Object)} uncheck |
| + * function to deselect an entry (metric or event) |
| + */ |
| + |
| + this.setupCheckboxes = function(divId, optionMap, check, uncheck) { |
| + var checkboxTemplate = querySelect('#checkboxTemplate'); |
| + var chooseMetricsDiv = querySelect(divId); |
| + var doCheck = check.bind(this); |
| + var doUncheck = uncheck.bind(this); |
| + |
| + for (var option in optionMap) { |
| + var checkbox = checkboxTemplate.cloneNode(true); |
| + checkbox.querySelector('span').innerText = 'Show ' + |
| + optionMap[option].description; |
| + chooseMetricsDiv.appendChild(checkbox); |
| - // Parent container of radio buttons to select time range |
| - this.timeDiv = $('#chooseTimeRange'); |
| + var input = checkbox.querySelector('input'); |
| + input.option = option; |
| + input.addEventListener('change', function(e) { |
| + if (e.target.checked) |
| + doCheck(e.target.option); |
| + else |
| + doUncheck(e.target.option); |
| + }); |
| + } |
| + }; |
| - this.metricMap = {}; // MetricName => {div, lineChart, dataTable} objects |
| - this.eventMap = {}; // EventName => event point lists, as returned by webui |
| - this.intervals = []; // Array of objects {start, end} |
| + /** |
| + * Set up just one chart in which all metrics will be displayed |
| + * initially. But, the design readily accommodates addition of |
| + * new charts, and movement of metrics into those other charts. |
| + */ |
| + this.setupMainChart = function() { |
| + this.chartParent = querySelect('#charts'); |
| + this.charts = [document.createElement('div')]; |
| + this.charts[0].className = 'chart'; |
| + this.chartParent.appendChild(this.charts[0]); |
| + }; |
| + /** |
| + * Set the time range for which to display metrics and events. For |
| + * now, the time range always ends at "now", but future implementations |
| + * may allow time ranges not so anchored. |
| + * |
| + * @param {!{start: !number, end: !number, resolution: !number}} range |
| + */ |
| this.setTimeRange = function(range) { |
| this.range = range; |
| - this.end = Math.floor(new Date().getTime() / range.resolution) * |
| + this.end = Math.floor(Date.now() / range.resolution) * |
| range.resolution; |
| + |
| + // Take the GMT value of this.end ("now") and subtract from it the |
| + // number of minutes by which we lag GMT in the present timezone, |
| + // X mS/minute. This will show time in the present timezone. |
| + this.end -= new Date().getTimezoneOffset() * 60000; |
| this.start = this.end - range.timeSpan; |
| this.requestIntervals(); |
| - } |
| + }; |
| - // Return mock interval set for testing |
| + /** |
| + * Return mock interval set for testing |
| + * @return {!Array.<{start: !number, end: !number}>} intervals |
| + */ |
| this.getMockIntervals = function() { |
| var interval = this.end - this.start; |
| + |
| return [ |
| - {'start': this.start + interval * .1, |
| - 'end': this.start + interval * .2}, |
| - {'start': this.start + interval * .7, 'end': this.start + interval * .9} |
| + { start: this.start + interval * .1, |
| + end: this.start + interval * .2 |
| + }, |
| + { start: this.start + interval * .7, |
| + end: this.start + interval |
| + } |
| ]; |
| - } |
| - // Request array of objects with start and end fields showing browser |
| - // activity intervals in the specified time range |
| + }; |
| + |
| + /** |
| + * Request activity intervals in the current time range |
| + */ |
| this.requestIntervals = function() { |
| this.onReceiveIntervals(this.getMockIntervals()); |
| // Replace with: chrome.send('getIntervals', this.start, this.end, |
| // this.onReceiveIntervals); |
| - } |
| + }; |
| - // Webui callback delivering response from requestIntervals call. Assumes |
| - // this is a new time range choice, which results in complete refresh of |
| - // all metrics and event types that are currently selected. |
| + /** |
| + * Webui callback delivering response from requestIntervals call. Assumes |
| + * this is a new time range choice, which results in complete refresh of |
| + * all metrics and event types that are currently selected. |
| + * |
| + * @param {!Array.<{start: !number, end: !number}> intervals new intervals |
| + */ |
| this.onReceiveIntervals = function(intervals) { |
| this.intervals = intervals; |
| - for (var metric in this.metricMap) |
| - this.refreshMetric(metric); |
| - for (var eventType in this.eventMap) |
| - this.refreshEvent(eventType); |
| - } |
| - |
| - // Add a new metric, and its associated linegraph. The linegraph for |
| - // each metric has a discrete domain of times. This is not continuous |
| - // because of breaks for each interval of activity. (No point in showing |
| - // a lot of "dead air" when the browser wasn't running.) Column 1 of |
| - // its DataTable is the metric data, and higher numbered columns are added |
| - // in pairs for each event type currently chosen. Each pair gives the |
| - // event occurence points, and mouseover detailed description for one event |
| - // type. |
| - this.addMetric = function(metric) { |
| - if (!(metric in this.metricMap)) { |
| - var div = document.createElement('div'); |
| - div.className = 'chart'; |
| - this.chartDiv.appendChild(div); |
| - |
| - var table = new google.visualization.DataTable(); |
| - var chart = new google.visualization.LineChart(div); |
| - this.metricMap[metric] = {'div': div, 'chart': chart, 'table': table}; |
| - |
| - table.addColumn('string', 'Time'); // Default domain column |
| - table.addColumn('number', 'Value'); // Only numerical range column |
| - for (var event in this.events) |
| - this.addEventColumns(table, event); |
| + for (var metric in this.metricMap) { |
| + var metricValue = this.metricMap[metric]; |
| + if (metricValue.divs.length > 0) // If we're displaying this metric. |
| + this.refreshMetric(metric); |
| + } |
| - this.refreshMetric(metric); |
| + for (var eventType in this.eventMap) { |
| + var eventValue = this.eventMap[eventType]; |
| + if (eventValue.divs.length > 0) |
| + this.refreshEventType(eventType); |
| } |
| - } |
| + }; |
| - // Remove a metric from the UI |
| + /** |
| + * Add a new metric to the main (currently only) chart. |
| + * |
| + * @param {!string} metric Metric to start displaying |
| + */ |
| + this.addMetric = function(metric) { |
| + this.metricMap[metric].divs.push(this.charts[0]); |
| + this.refreshMetric(metric); |
| + }; |
| + |
| + /** |
| + * Remove a metric from the chart(s). |
| + * |
| + * @param {!string} metric Metric to stop displaying |
| + */ |
| this.dropMetric = function(metric) { |
| - if (metric in this.metricMap) { |
| - this.chartDiv.removeChild(this.metricMap[metric].div); |
| - delete this.metricMap[metric]; |
| - } |
| - } |
| + var metricValue = this.metricMap[metric]; |
| + var affectedCharts = metricValue.divs; |
| + metricValue.divs = []; |
| + |
| + affectedCharts.forEach(this.drawChart, this); |
| + }; |
| - // Return mock metric data points for testing |
| - this.getMockDataPoints = function() { |
| + /** |
| + * Return mock metric data points for testing. Give values ranging from |
| + * offset to max-offset. (This let us avoid direct overlap of |
|
koz (OOO until 15th September)
2012/07/02 06:14:48
let -> lets
clintstaley
2012/07/02 19:09:05
Done.
|
| + * different mock data sets in an ugly way that will die in the next |
| + * version anyway.) |
| + * |
| + * @param {!number} max Max data value to return, less offset |
| + * @param {!number} offset Adjustment factor |
| + * @return {!Array.<{time: !number, value: !number}>} |
| + */ |
| + this.getMockDataPoints = function(max, offset) { |
| var dataPoints = []; |
| - for (var x = 0; x < this.intervals.length; x++) { |
| - // Rise from low 0 to high 100 every 20 min |
| - for (var time = this.intervals[x].start; time <= this.intervals[x].end; |
| + for (var i = 0; i < this.intervals.length; i++) { |
| + // Rise from low offset to high max-offset in 100 point steps |
| + for (var time = this.intervals[i].start; time <= this.intervals[i].end; |
| time += this.range.resolution) |
| - dataPoints.push({'time': time, 'value': time % 1000000 / 10000}); |
| + dataPoints.push({time: time, value: offset + time / |
|
koz (OOO until 15th September)
2012/07/02 06:14:48
This expression could do with some extra parenthes
clintstaley
2012/07/02 19:09:05
Done.
|
| + this.range.resolution % 100 * (max - 2 * offset) / 100}); |
| } |
| return dataPoints; |
| - } |
| + }; |
| - // Request new metric data, assuming the metric table and chart already |
| - // exist. |
| + /** |
| + * Request new metric data, assuming the metric table and chart already |
| + * exist. |
| + * |
| + * @param {!string} metric Metric for which to get data |
| + */ |
| this.refreshMetric = function(metric) { |
| - this.onReceiveMetric(metric, this.getMockDataPoints()); |
| + var metricValue = this.metricMap[metric]; |
| + |
| + metricValue.data = null; // Mark metric as awaiting response. |
| + this.onReceiveMetric(metric, |
| + this.getMockDataPoints(metricValue.yAxis.max, 5)); |
| // Replace with: |
| // chrome.send("getMetric", this.range.start, this.range.end, |
| // this.range.resolution, this.onReceiveMetric); |
| - } |
| + }; |
| - // Receive new datapoints for |metric|, and completely refresh the DataTable |
| - // for that metric, redrawing the chart. (We cannot preserve the event |
| - // columns because entirely new rows may be implied by the new metric |
| - // datapoints.) |
| + /** |
| + * Receive new datapoints for |metric|, convert the data to Flot-usable |
| + * form, and redraw all affected charts. |
| + * |
| + * @param {!string} metric Metric to which |points| applies |
| + * @param {!Array.<{time: !number, value: !number}>} points |
| + * new data points |
| + */ |
| this.onReceiveMetric = function(metric, points) { |
| + var metricValue = this.metricMap[metric]; |
| + |
| // Might have been dropped while waiting for data |
| - if (!(metric in this.metricMap)) |
| + if (metricValue.divs.length == 0) |
| return; |
| - var data = this.metricMap[metric].table; |
| + var series = []; |
| + metricValue.data = [series]; |
| - data.removeRows(0, data.getNumberOfRows()); |
| - |
| - // Traverse the points, which are in time order, and the intervals, |
| - // placing each value in the interval (if any) in which it belongs. |
| + // Traverse the points, and the intervals, in parallel. Both are in |
| + // ascending time order. Create a sequence of data "series" (per Flot) |
| + // arrays, with each series comprising all points within a given interval. |
| var interval = this.intervals[0]; |
| var intervalIndex = 0; |
| - var valueIndex = 0; |
| - var value; |
| - while (valueIndex < points.length && |
| + var point; |
| + var pointIndex = 0; |
| + while (pointIndex < points.length && |
| intervalIndex < this.intervals.length) { |
| - value = points[valueIndex++]; |
| - while (value.time > interval.end && |
| + point = points[pointIndex++]; |
| + while (point.time > interval.end && |
| intervalIndex < this.intervals.length) { |
| interval = this.intervals[++intervalIndex]; // Jump to new interval |
| - data.addRow(null, null); // Force gap in line chart |
| + if (series.length > 0) |
| + metricValue.data.push(series = []); |
|
koz (OOO until 15th September)
2012/07/02 06:14:48
Could you put this on two lines, like:
metricValu
clintstaley
2012/07/02 19:09:05
C coder's style; sorry :) Will change.
|
| } |
| - if (intervalIndex < this.intervals.length && value.time > interval.start) |
| - if (data.getNumberOfRows() % this.range.labelEvery == 0) |
| - data.addRow([new Date(value.time).toString(this.range.format), |
| - value.value]); |
| - else |
| - data.addRow(['', value.value]); |
| - } |
| - this.drawChart(metric); |
| - } |
| + if (intervalIndex < this.intervals.length && point.time > interval.start) |
| + series.push([point.time, point.value]); |
| + } |
| - // Add a new event to all line graphs. |
| + metricValue.divs.forEach(this.drawChart, this); |
| + }; |
| + |
| + /** |
| + * Add a new event to the chart(s). |
| + * |
| + * @param {!string} eventType type of event to start displaying |
| + */ |
| this.addEventType = function(eventType) { |
| - if (!(eventType in this.eventMap)) { |
| - this.eventMap[eventType] = []; |
| - this.refreshEventType(eventType); |
| - } |
| - } |
| + this.eventMap[eventType].divs = this.charts; // Events show on all charts |
| + this.refreshEventType(eventType); |
| + }; |
| + |
| + /* |
| + * Remove an event from the chart(s) |
| + * |
| + * @param {!string} eventType type of event to stop displaying |
| + */ |
| + this.dropEventType = function(eventType) { |
| + var eventValue = this.eventMap[eventType]; |
| + var affectedCharts = eventValue.divs; |
| + eventValue.divs = []; |
| + |
| + affectedCharts.forEach(this.drawChart, this); |
| + }; |
| - // Return mock event point for testing |
| + /** |
| + * Return mock event points for testing |
| + * |
| + * @param {!string} eventType type of event to generate mock data for |
| + * @return {!Array.<{time: !number, longDescription: !string}>} |
| + */ |
| this.getMockEventValues = function(eventType) { |
| var mockValues = []; |
| for (var i = 0; i < this.intervals.length; i++) { |
| @@ -199,164 +397,194 @@ var controller = new function() { |
| mockValues.push({ |
| time: interval.start, |
| - shortDescription: eventType, |
| longDescription: eventType + ' at ' + |
| new Date(interval.start) + ' blah, blah blah'}); |
| mockValues.push({ |
| time: (interval.start + interval.end) / 2, |
| - shortDescription: eventType, |
| longDescription: eventType + ' at ' + |
| new Date((interval.start + interval.end) / 2) + ' blah, blah blah'}); |
| mockValues.push({ |
| time: interval.end, |
| - shortDescription: eventType, |
| longDescription: eventType + ' at ' + new Date(interval.end) + |
| ' blah, blah blah'}); |
| } |
| return mockValues; |
| - } |
| + }; |
| - // Request new data for |eventType|, for times in the current range. |
| + /** |
| + * Request new data for |eventType|, for times in the current range. |
| + * |
| + * @param {!string} eventType type of event to get new data for |
| + */ |
| this.refreshEventType = function(eventType) { |
| + this.eventMap[eventType].data = null; // Mark eventType as awaiting response |
| this.onReceiveEventType(eventType, this.getMockEventValues(eventType)); |
| // Replace with: |
| // chrome.send("getEvents", eventType, this.range.start, this.range.end); |
|
koz (OOO until 15th September)
2012/07/02 06:14:48
nit: Missing final callback parameter here?
clintstaley
2012/07/02 19:09:05
Will add if it proves necessary, but the current d
|
| - } |
| - |
| - // Add an event column pair to DataTable |table| for |eventType| |
| - this.addEventColumns = function(table, eventType) { |
| - var annotationCol = table.addColumn({'id': eventType, type: 'string', |
| - role: 'annotation'}); |
| - var rolloverCol = table.addColumn({'id': eventType + 'Tip', type: 'string', |
| - role: 'annotationText'}); |
| - var values = this.eventMap[eventType]; |
| - var interval = this.intervals[0], intervalIndex = 0; |
| - |
| - for (var i = 0; i < values.length; i++) { |
| - var event = values[i]; |
| - var rowIndex = 0; |
| - while (event.time > interval.end && |
| - intervalIndex < this.intervals.length - 1) |
| - { |
| - // Skip interval times, inclusive of interval.end, and of following null |
| - rowIndex += (interval.end - interval.start) / this.range.resolution + 2; |
| - interval = this.intervals[++intervalIndex]; |
| - } |
| - if (event.time >= interval.start && event.time <= interval.end) { |
| - table.setCell(rowIndex + (event.time - interval.start) / |
| - this.range.resolution, annotationCol, event.shortDescription); |
| - table.setCell(rowIndex + (event.time - interval.start) / |
| - this.range.resolution, rolloverCol, event.longDescription); |
| - } |
| - } |
| - } |
| + }; |
| - this.dropEventColumns = function(table, eventType) { |
| - var colIndex, numCols = table.getNumberOfColumns(); |
| + /** |
| + * Receive new data for |eventType|. If the event has been deselected while |
| + * awaiting webui response, do nothing. Otherwise, save the data directly, |
| + * since events are handled differently than metrics when drawing |
| + * (no "series"), and redraw all the affected charts. |
| + * |
| + * @param {!string} eventType type of event the new data applies to |
| + * @param {!Array.<{time: !number, longDescription: !string}>} values |
| + * new event values |
| + */ |
| + this.onReceiveEventType = function(eventType, values) { |
| + var eventValue = this.eventMap[eventType]; |
| - for (colIndex = 0; colIndex < numCols; colIndex++) |
| - if (table.getColumnId(colIndex) == eventType) |
| - break; |
| + if (eventValue.divs.length == 0) |
| + return; |
| - if (colIndex < numCols) { |
| - table.removeColumn(colIndex + 1); |
| - table.removeColumn(colIndex); |
| - } |
| - } |
| + eventValue.data = values; |
| + eventValue.divs.forEach(this.drawChart, this); |
| + }; |
| - // Receive new data for |eventType|. Save this in eventMap for future |
| - // redraws. Then, for each metric linegraph remove any current column pair |
| - // for |eventType| and replace it with a new pair, which will reflect the |
| - // new data. Redraw the linegraph. |
| - this.onReceiveEventType = function(eventType, values) { |
| - this.eventMap[eventType] = values; |
| + /** |
| + * Return an object containing an array of metrics and another of events |
| + * that include |chart| as one of the divs into which they display. |
| + * |
| + * @param {DOMElement} chart div for which to get relevant items |
| + * @return {!{metrics: !Array,<Object>, events: !Array.<Object>}} |
| + */ |
| + this.getChartData = function(chart) { |
| + var result = {metrics: [], events: []}; |
| for (var metric in this.metricMap) { |
| - var table = this.metricMap[metric].table; |
| + var metricValue = this.metricMap[metric]; |
| - this.dropEventColumns(table, eventType); |
| - this.addEventColumns(table, eventType); |
| - this.drawChart(metric); |
| + if (metricValue.divs.indexOf(chart) != -1) |
| + result.metrics.push(metricValue); |
| } |
| - } |
| - this.dropEventType = function(eventType) { |
| - delete this.eventMap[eventType]; |
| + for (var eventType in this.eventMap) { |
| + var eventValue = this.eventMap[eventType]; |
| - for (var metric in this.metricMap) { |
| - var table = this.metricMap[metric].table; |
| - |
| - this.dropEventColumns(table, eventType); |
| - this.drawChart(metric); |
| + if (eventValue.divs.length > 0) |
|
koz (OOO until 15th September)
2012/07/02 06:14:48
Add a comment explaining that events appear on all
clintstaley
2012/07/02 19:09:05
There's one elsewhere, but it's worth repeating he
|
| + result.events.push(eventValue); |
| } |
| - } |
| + return result; |
| + }; |
| - // Redraw the linegraph for |metric|, assuming its DataTable is fully up to |
| - // date. |
| - this.drawChart = function(metric) { |
| - var entry = this.metricMap[metric]; |
| + /** |
| + * Check all entries in an object of the type returned from getChartData, |
| + * above, to see if all events and metrics have completed data (none is |
| + * awaiting an asynchronous webui response to get their current data). |
| + * |
| + * @param {!{metrics: !Array,<Object>, events: !Array.<Object>}} chartData |
| + * event/metric data to check for readiness |
| + * @return {boolean} is data ready? |
| + */ |
| + this.isDataReady = function(chartData) { |
| + for (var i = 0; i < chartData.metrics.length; i++) { |
| + if (chartData.metrics[i].data == null) |
| + return false; |
| + } |
| - entry.chart.draw(entry.table, {title: metric + ' for ' + this.range.name, |
| - hAxis: {showTextEvery: this.range.labelEvery}}); |
| - } |
| + for (var i = 0; i < chartData.events.length; i++) { |
| + if (chartData.events[i].data == null) |
| + return false; |
| + } |
| - this.setupTimeRangeChooser = function() { |
| - var controller = this; |
| - var radioTemplate = $('#radioTemplate'); |
| + return true; |
| + }; |
| - for (var time in this.TimeRange) { |
| - var range = this.TimeRange[time]; |
| - var radio = radioTemplate.cloneNode(true); |
| - var input = radio.querySelector('input'); |
| - |
| - input.value = range.value; |
| - radio.querySelector('span').innerText = range.name; |
| - this.timeDiv.appendChild(radio); |
| - range.element = input; |
| - radio.range = range; |
| - radio.addEventListener('click', function() { |
| - controller.setTimeRange(this.range); |
| - }); |
| + /** |
| + * Create and return an array of "markings" (per Flot), representing |
| + * vertical lines at the event time, in the event's color. Also add |
| + * (not per Flot) a |description| property to each, to be used for hand |
| + * creating description boxes. |
| + * |
| + * @param {!Array.<{ |
| + * description: !string, |
| + * color: !string, |
| + * data: !Array.<{time: !number}> |
| + * }>} eventValues events to make markings for |
| + * @return {!Array.<{ |
| + * color: !string, |
| + * description: !string, |
| + * xaxis: {from: !number, to: !number} |
| + * }>} mark data structure for Flot to use |
| + */ |
| + this.getEventMarks = function(eventValues) { |
| + var markings = []; |
| + |
| + for (var i = 0; i < eventValues.length; i++) { |
| + var eventValue = eventValues[i]; |
| + for (var d = 0; d < eventValue.data.length; d++) { |
| + var point = eventValue.data[d]; |
| + markings.push({ |
| + color: eventValue.color, |
| + description: eventValue.description, |
| + xaxis: {from: point.time, to: point.time} |
| + }); |
| + } |
| } |
| - } |
| - this.setupMetricChooser = function(metricTypes) { |
| - var checkboxTemplate = $('#checkboxTemplate'); |
| + return markings; |
| + }; |
| - metricTypes.forEach(function(metric) { |
| - var checkbox = checkboxTemplate.cloneNode(true); |
| - var input = checkbox.querySelector('input'); |
| - input.addEventListener('change', function() { |
| - if (input.checked) |
| - this.addMetric(metric); |
| - else |
| - this.dropMetric(metric); |
| - }.bind(this)); |
| - checkbox.getElementsByTagName('span')[0].innerText = 'Show ' + metric; |
| - this.chooseMetricsDiv.appendChild(checkbox); |
| - }, this); |
| - } |
| + /** |
| + * Redraw the chart in div |chart|, IF all its dependent data is present. |
| + * Otherwise simply return, and await another call when all data is |
| + * available. |
| + * |
| + * @param {DOMElement} chart div to redraw |
| + */ |
| + this.drawChart = function(chart) { |
| + var chartData = this.getChartData(chart); |
| + |
| + if (!this.isDataReady(chartData)) |
| + return; |
| - this.setupEventChooser = function(eventTypes) { |
| - var checkboxTemplate = $('#checkboxTemplate'); |
| + var seriesSeq = []; |
| + var yAxes = []; |
| + chartData.metrics.forEach(function(value) { |
| + yAxes.push(value.yAxis); |
| + for (var run = 0; run < value.data.length; run++) { |
|
koz (OOO until 15th September)
2012/07/02 06:14:48
i seems like a more idiomatic iterator variable he
clintstaley
2012/07/02 19:09:05
Done.
|
| + seriesSeq.push({ |
| + color: value.yAxis.color, |
| + data: value.data[run], |
| + label: run == 0 ? value.description + '(' + value.label + ')' : null, |
| + yaxis: yAxes.length, // Use just-added Y axis |
| + }); |
| + } |
| + }); |
| + |
| + var markings = this.getEventMarks(chartData.events); |
| + var chart = this.charts[0]; |
| + var plot = $.plot(chart, seriesSeq, { |
| + yaxes: yAxes, |
| + xaxis: {mode: 'time'}, |
| + grid: {markings: markings} |
| + }); |
| + |
| + // For each event in |markings|, create also a label div, with left |
| + // edge colinear with the event vertical-line. Top of label is |
| + // presently a hack-in, putting labels in three tiers of 25px height |
| + // each to avoid overlap. Will need something better. |
| + var labelTemplate = querySelect('#labelTemplate'); |
| + for (var i = 0; i < markings.length; i++) { |
| + var mark = markings[i]; |
| + var point = |
| + plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, yaxis: 1}); |
| + var labelDiv = labelTemplate.cloneNode(true); |
| + labelDiv.innerText = mark.description; |
| + labelDiv.style.left = point.left + 'px'; |
| + labelDiv.style.top = (point.top + 25 * (i % 3)) + 'px'; |
| + chart.appendChild(labelDiv); |
| + } |
| + }; |
| - eventTypes.forEach(function(event) { |
| - var checkbox = checkboxTemplate.cloneNode(true); |
| - var input = checkbox.querySelector('input'); |
| - input.addEventListener('change', function() { |
| - if (input.checked) |
| - this.addEventType(event); |
| - else |
| - this.dropEventType(event); |
| - }.bind(this)); |
| - checkbox.getElementsByTagName('span')[0].innerText = 'Show ' + event; |
| - this.chooseEventsDiv.appendChild(checkbox); |
| - }, this); |
| - } |
| - |
| - this.setupMetricChooser(['Oddness', 'Jankiness']); |
| - this.setupEventChooser(['Wampus Attack', 'Solar Eclipse']); |
| + this.setupCheckboxes( |
| + '#chooseMetrics', this.metricMap, this.addMetric, this.dropMetric); |
| + this.setupCheckboxes( |
| + '#chooseEvents', this.eventMap, this.addEventType, this.dropEventType); |
| this.setupTimeRangeChooser(); |
| + this.setupMainChart(); |
| this.TimeRange.day.element.click(); |
| } |