728 строки
23 KiB
JavaScript
Executable File
728 строки
23 KiB
JavaScript
Executable File
// vim: set ts=4 sw=4 tw=99 et:
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
"use strict";
|
|
|
|
function Display(awfy, prefix, id, domid, elt)
|
|
{
|
|
this.awfy = awfy;
|
|
this.prefix = prefix;
|
|
this.id = id;
|
|
this.domid = domid;
|
|
this.elt = elt;
|
|
this.attachedTips = [];
|
|
this.plot = null;
|
|
this.hovering = null;
|
|
this.graph = null;
|
|
this.orig_graph = null;
|
|
}
|
|
|
|
Display.prototype.setGraph = function (graph)
|
|
{
|
|
// We keep both the original dataset and the one we send and display to
|
|
// flot, so we can redraw and hide lines. In the future this should be
|
|
// tightened up, so that code working with the display graph uses "graph"
|
|
// and code (if any) working with the original data set uses "orig_graph".
|
|
// And really no code should be accessing "graph" until draw().
|
|
this.orig_graph = graph;
|
|
this.graph = graph;
|
|
}
|
|
|
|
Display.prototype.setup = function (graph)
|
|
{
|
|
this.setGraph(graph);
|
|
|
|
this.selectDelay = null;
|
|
|
|
if (graph.aggregate)
|
|
this.setHistoricalMidpoint();
|
|
else
|
|
this.aggregate = -1;
|
|
|
|
this.zoomInfo = { prev: null,
|
|
level: 'aggregate'
|
|
};
|
|
|
|
this.elt.bind("plothover", this.onHover.bind(this));
|
|
this.elt.bind('plotclick', this.onClick.bind(this));
|
|
this.elt.bind('plotselected', this.plotSelected.bind(this));
|
|
this.elt.bind('dblclick', (function () {
|
|
if (this.zoomInfo.level != 'aggregate')
|
|
this.unzoom();
|
|
}).bind(this));
|
|
}
|
|
|
|
Display.MaxPoints = 50;
|
|
Display.Months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
|
|
Display.prototype.shutdown = function () {
|
|
this.elt.unbind('plothover');
|
|
this.elt.unbind('plotclick');
|
|
this.elt.unbind('plotselected');
|
|
this.elt.unbind('dblclick');
|
|
if (this.hovering) {
|
|
this.hovering.remove();
|
|
this.hovering = null;
|
|
}
|
|
this.detachTips();
|
|
this.plot = null;
|
|
this.setGraph(null);
|
|
}
|
|
|
|
Display.prototype.shouldRefresh = function () {
|
|
if (this.graph) {
|
|
for (var i = 0; i < this.attachedTips.length; i++) {
|
|
var tooltip = this.attachedTips[i];
|
|
if (tooltip.attached())
|
|
return false;
|
|
}
|
|
if (this.zoomInfo.level != 'aggregate')
|
|
return false;
|
|
this.shutdown();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Display.prototype.setHistoricalMidpoint = function () {
|
|
// Find the range of historical points.
|
|
for (var i = 0; i < this.graph.timelist.length; i++) {
|
|
if (this.graph.timelist[i] >= this.graph.earliest)
|
|
break;
|
|
}
|
|
|
|
if (i && i != this.graph.timelist.length)
|
|
this.historical = i;
|
|
}
|
|
|
|
// Copy flot's tick algorithm.
|
|
Display.prototype.tickSize = function (min, max) {
|
|
var noTicks = 0.3 * Math.sqrt(this.elt.width());
|
|
var delta = (max - min) / noTicks;
|
|
var dec = -Math.floor(Math.log(delta) / Math.LN10);
|
|
var magn = Math.pow(10, -dec);
|
|
var norm = delta / magn;
|
|
var size;
|
|
if (norm < 1.5) {
|
|
size = 1;
|
|
} else if (norm < 3) {
|
|
size = 2;
|
|
if (norm > 2.25) {
|
|
size = 2.5;
|
|
++dec;
|
|
}
|
|
} else if (norm < 7.5) {
|
|
size = 5;
|
|
} else {
|
|
size = 10;
|
|
}
|
|
|
|
size *= magn;
|
|
return size;
|
|
};
|
|
|
|
Display.prototype.aggregateTicks = function () {
|
|
// Draw historical ticks at a higher density.
|
|
var ticks = this.tickSize(0, this.graph.timelist.length);
|
|
var list = [];
|
|
|
|
// This is all a bunch of hardcoded hacks for now.
|
|
var preticks, preticklist;
|
|
|
|
if (ticks == 5) {
|
|
preticks = 6;
|
|
preticklist = [2, 4];
|
|
} else if (ticks == 10) {
|
|
preticks = 9;
|
|
preticklist = [3, 6];
|
|
} else {
|
|
preticks = ticks;
|
|
}
|
|
|
|
var last_year = undefined;
|
|
var current_year = (new Date()).getFullYear();
|
|
for (var i = 0; i < this.historical; i += preticks) {
|
|
var d = new Date(this.graph.timelist[i] * 1000);
|
|
var text = Display.Months[d.getMonth()];
|
|
|
|
// Some graphs span over a year, so add in a hint when the year
|
|
// changes.
|
|
if ((i == 0 && d.getFullYear() != current_year) ||
|
|
(last_year && d.getFullYear() != last_year))
|
|
{
|
|
text += " " + d.getFullYear();
|
|
last_year = d.getFullYear();
|
|
}
|
|
|
|
// Add the tick mark, then try to add some more empty ones to
|
|
// make the graph appear denser.
|
|
list.push([i, text]);
|
|
if (preticklist) {
|
|
for (var j = 0; j < preticklist.length; j++) {
|
|
var v = i + preticklist[j];
|
|
if (v >= this.historical)
|
|
break;
|
|
list.push([v, ""]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Figure out where we should start placing sparser lines, since
|
|
// we don't want them too close to the historical lines.
|
|
i = list[list.length - 1][0] + ticks;
|
|
|
|
// If the aggregate graph has both historical and recent points,
|
|
for (; i < this.graph.timelist.length; i += ticks) {
|
|
var d = new Date(this.graph.timelist[Math.floor(i)] * 1000);
|
|
var text = Display.Months[d.getMonth()] + " " + d.getDate();
|
|
list.push([i, text]);
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
Display.prototype.draw = function () {
|
|
var options = { };
|
|
|
|
// We always start out by using the original graph, since we may modify
|
|
// this one locally. Start by stripping out any lines that should be
|
|
// hidden.
|
|
var new_info = [];
|
|
var new_lines = [];
|
|
for (var i = 0; i < this.orig_graph.info.length; i++) {
|
|
var info = this.orig_graph.info[i];
|
|
var mode = AWFYMaster.modes[info.modeid];
|
|
if (!mode)
|
|
continue;
|
|
|
|
// Strip JM+TI, BC
|
|
if (info.modeid == 12 || info.modeid == 15)
|
|
continue;
|
|
|
|
mode.used = true;
|
|
if (mode.hidden)
|
|
continue;
|
|
if (mode.runtime_hidden)
|
|
continue;
|
|
|
|
new_info.push(info);
|
|
new_lines.push(this.orig_graph.lines[i]);
|
|
}
|
|
this.graph = {
|
|
lines: new_lines,
|
|
info: new_info,
|
|
timelist: this.orig_graph.timelist,
|
|
earliest: this.orig_graph.earliest,
|
|
aggregate: this.orig_graph.aggregate,
|
|
direction: this.orig_graph.direction
|
|
};
|
|
|
|
options.lines = { show: true };
|
|
options.points = { fillColor: "#ffffff", show: true };
|
|
options.borderWidth = 1.5;
|
|
options.borderColor = "#BEBEBE";
|
|
options.legend = { show: false };
|
|
options.xaxis = { };
|
|
options.yaxis = { };
|
|
options.grid = { hoverable: true, clickable: true };
|
|
options.selection = { mode: 'x' }
|
|
|
|
// Aggregate view starts from 0. We space things out when zooming in.
|
|
if (this.graph.aggregate && this.awfy.type != 'overview')
|
|
options.yaxis.min = 0;
|
|
|
|
if (this.graph.direction == 1) {
|
|
options.yaxis.transform = function (v) {
|
|
return -v;
|
|
};
|
|
options.yaxis.inverseTransform = function (v) {
|
|
return -v;
|
|
};
|
|
}
|
|
|
|
if (this.graph.aggregate && this.historical) {
|
|
// If the graph has both historical and recent points, indicated by
|
|
// the "historical" midpoint, then we change some graph formatting
|
|
// to reflect that part of the graph has a greater time density.
|
|
//
|
|
// To make this work, we modified flot to pass in its plot variable
|
|
// when invoking this callback, in order to use c2p().
|
|
options.points.symbol = (function (ctx, x, y, radius, shadow, plot) {
|
|
var axis = plot.getAxes();
|
|
var rx = Math.round(axis.xaxis.c2p(x));
|
|
if (this.graph.timelist[rx] < this.graph.earliest) {
|
|
ctx.strokeRect(x - radius / 3, y - radius / 3, radius * 2/3, radius * 2/3);
|
|
// Disable clearRect due to bug in Firefox for Android (bug 936177)
|
|
//ctx.clearRect(x - radius / 4, y - radius / 4, radius / 2, radius / 2);
|
|
} else {
|
|
ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
|
|
}
|
|
}).bind(this);
|
|
|
|
options.xaxis.ticks = this.aggregateTicks();
|
|
options.xaxis.transform = (function (v) {
|
|
if (v < 30)
|
|
return v;
|
|
var total = this.graph.timelist.length - 30;
|
|
return 30 + (v - 30)/total * 30;
|
|
}).bind(this);
|
|
options.xaxis.inverseTransform = (function (v) {
|
|
if (v < 30)
|
|
return v;
|
|
var total = this.graph.timelist.length - 30;
|
|
return 30 + (v - 30)/30 * total;
|
|
}).bind(this);
|
|
}
|
|
|
|
options.yaxis.tickFormatter = function (v, axis) {
|
|
if (Math.round(v) != v)
|
|
return v.toFixed(2);
|
|
return v;
|
|
}
|
|
|
|
if (!options.xaxis.ticks) {
|
|
options.xaxis.tickFormatter = (function (v, axis) {
|
|
v = Math.round(v);
|
|
if (v < 0 || v >= this.graph.timelist.length)
|
|
return '';
|
|
var t = this.graph.timelist[v];
|
|
var d = new Date(t * 1000);
|
|
return Display.Months[d.getMonth()] + " " + d.getDate();
|
|
}).bind(this);
|
|
}
|
|
|
|
this.plot = $.plot(this.elt, this.graph.lines, options);
|
|
|
|
if (this.graph.direction == 1) {
|
|
var yaxisLabel = $("<div class='axisLabel yaxisLabel'></div>")
|
|
.text("Score")
|
|
.appendTo(this.elt);
|
|
} else {
|
|
var yaxisLabel = $("<div class='axisLabel yaxisLabel'></div>")
|
|
.text("Execution Time (ms)")
|
|
.appendTo(this.elt);
|
|
}
|
|
yaxisLabel.css("margin-top", yaxisLabel.width() / 2 - 20);
|
|
}
|
|
|
|
Display.prototype.plotSelected = function (event, ranges) {
|
|
this.selectDelay = new Date();
|
|
|
|
var from_x = Math.floor(ranges.xaxis.from);
|
|
if (from_x == ranges.xaxis.from)
|
|
from_x -= 1;
|
|
if (from_x < 0)
|
|
from_x = 0;
|
|
|
|
var to_x = Math.ceil(ranges.xaxis.to);
|
|
if (to_x == ranges.xaxis.to)
|
|
to_x += 1;
|
|
if (to_x >= this.graph.timelist.length)
|
|
to_x = this.graph.timelist.length - 1;
|
|
|
|
var start = this.graph.timelist[from_x];
|
|
var end = this.graph.timelist[to_x];
|
|
|
|
AWFY.trackZoom(start, end);
|
|
|
|
var prev = this.zoomInfo.prev;
|
|
if (prev && this.zoomInfo.level == 'month') {
|
|
// Estimate the number of datapoints we had in the old range.
|
|
var oldstart = AWFY.findX(prev, this.graph.timelist[0]);
|
|
var oldend = AWFY.findX(prev, this.graph.timelist[this.graph.timelist.length - 1]);
|
|
|
|
// Estimate the number of datapoints we'd have in the new range.
|
|
var newstart = AWFY.findX(prev, start);
|
|
var newend = AWFY.findX(prev, end);
|
|
|
|
// Some heuristics to figure out whether we should fetch more data.
|
|
var zoom = (newend - newstart) / (oldend - oldstart);
|
|
if ((zoom >= 0.8 && (newend - newstart >= Display.MaxPoints * 1.5)) ||
|
|
(newend - newstart >= Display.MaxPoints * 5))
|
|
{
|
|
// Okay! Trim the cached graph, then display.
|
|
var graph = AWFY.trim(prev, newstart, newend);
|
|
this.localZoom(graph);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If we already have the highest level of data, jump right in.
|
|
if (prev && this.zoomInfo.level == 'raw') {
|
|
var oldstart = AWFY.findX(prev, this.graph.timelist[0]);
|
|
var oldend = AWFY.findX(prev, this.graph.timelist[this.graph.timelist.length - 1]);
|
|
|
|
this.plot.clearSelection();
|
|
|
|
// If we can't really zoom in any more, don't bother.
|
|
if (oldend - oldstart < Display.MaxPoints / 2)
|
|
return;
|
|
|
|
// Require at least a few datapoints.
|
|
var newstart = AWFY.findX(prev, start);
|
|
var newend = AWFY.findX(prev, end);
|
|
if (oldend - oldstart <= 3)
|
|
return;
|
|
|
|
var graph = AWFY.trim(prev, newstart, newend);
|
|
this.localZoom(graph);
|
|
return;
|
|
}
|
|
|
|
// Disable further selections since we wait for the XHR to go through.
|
|
this.plot.disableSelection();
|
|
|
|
// Clear the cached graph, since we'll get a new one.
|
|
this.zoomInfo.prev = null;
|
|
|
|
if (this.zoomInfo.level == 'aggregate') {
|
|
this.awfy.requestZoom(this, 'condensed', start, end);
|
|
this.zoomInfo.level = 'month';
|
|
} else {
|
|
this.awfy.requestZoom(this, 'raw', start, end);
|
|
this.zoomInfo.level = 'raw';
|
|
}
|
|
}
|
|
|
|
Display.prototype.localZoom = function (graph) {
|
|
graph = AWFY.condense(graph, Display.MaxPoints);
|
|
this.setGraph(graph);
|
|
this.draw();
|
|
this.plot.enableSelection();
|
|
this.plot.clearSelection();
|
|
this.detachTips();
|
|
}
|
|
|
|
Display.prototype.completeZoom = function (graph, start, end) {
|
|
// Copy properties from the old graph before resetting.
|
|
graph.direction = this.graph.direction;
|
|
|
|
// Cache the original graph in case it's dense enough to zoom in more
|
|
// without fetching more points via XHR.
|
|
if (!this.zoomInfo.prev)
|
|
this.zoomInfo.prev = graph;
|
|
|
|
var first = AWFY.findX(graph, start);
|
|
var last = AWFY.findX(graph, end);
|
|
graph = AWFY.trim(graph, first, last);
|
|
|
|
// If we got a paltry number of datapoints, skip this and zoom in more.
|
|
if (this.zoomInfo.level == 'month' && graph.timelist.length < Display.MaxPoints / 2) {
|
|
this.zoomInfo.prev = null;
|
|
this.awfy.requestZoom(this, 'raw', start, end);
|
|
this.zoomInfo.level = 'raw';
|
|
return;
|
|
}
|
|
|
|
this.localZoom(graph);
|
|
}
|
|
|
|
Display.prototype.cancelZoom = function () {
|
|
this.plot.enableSelection();
|
|
this.plot.clearSelection();
|
|
|
|
// Reset the zoom level we think we have.
|
|
if (!this.zoomInfo.prev) {
|
|
if (this.zoomInfo.level == 'raw')
|
|
this.zoomInfo.level = 'month';
|
|
else if (this.zoomInfo.level == 'month')
|
|
this.zoomInfo.level = 'aggregate';
|
|
}
|
|
}
|
|
|
|
Display.prototype.unzoom = function () {
|
|
this.setGraph(AWFY.aggregate[this.id]);
|
|
this.setHistoricalMidpoint();
|
|
this.draw();
|
|
this.plot.enableSelection();
|
|
this.plot.clearSelection();
|
|
this.detachTips();
|
|
this.zoomInfo.level = 'aggregate';
|
|
|
|
AWFY.trackZoom(null, null);
|
|
}
|
|
|
|
Display.prototype.detachTips = function () {
|
|
for (var i = 0; i < this.attachedTips.length; i++)
|
|
this.attachedTips[i].detach();
|
|
this.attachedTips = [];
|
|
}
|
|
|
|
Display.prototype.createToolTip = function (item, extended) {
|
|
var so = extended ? '<strong>' : '';
|
|
var sc = extended ? '</strong>' : '';
|
|
|
|
// Figure out the line this corresponds to.
|
|
var line = this.graph.info[item.seriesIndex];
|
|
if (!line)
|
|
return;
|
|
|
|
var text = "";
|
|
var x = item.datapoint[0];
|
|
var y = item.datapoint[1];
|
|
|
|
// Show suite version.
|
|
if (line.data[x][3]) {
|
|
var suiteVersion = AWFYMaster.suiteversions[line.data[x][3]]["name"];
|
|
text += so + 'suite: ' + sc + suiteVersion + '<br>';
|
|
}
|
|
|
|
// Show score.
|
|
if (this.graph.direction == -1)
|
|
text += so + 'score: ' + sc + y.toFixed(2) + 'ms<br>';
|
|
else
|
|
text += so + 'score: ' + sc + y.toFixed() + '<br>';
|
|
|
|
// Find the point previous to this one.
|
|
var prev = null;
|
|
for (var i = x - 1; i >= 0; i--) {
|
|
if (line.data[i] && line.data[i][0]) {
|
|
prev = line.data[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (prev) {
|
|
// Compute a difference.
|
|
var diff = Math.round((y - prev[0]) * 10) / 10;
|
|
var perc = -Math.round(((y - prev[0]) / prev[0]) * 1000) / 10;
|
|
var better;
|
|
if ((perc < 0 && this.graph.direction == -1) ||
|
|
(perc > 0 && this.graph.direction == 1))
|
|
{
|
|
better = 'worse';
|
|
} else {
|
|
better = 'better';
|
|
}
|
|
perc = Math.abs(perc);
|
|
|
|
if (diff === diff) {
|
|
if (extended)
|
|
text += so + 'delta' + sc + ': ' + diff;
|
|
else
|
|
text += String.fromCharCode(916) + ': ' + diff;
|
|
if (this.graph.direction == -1)
|
|
text += 'ms';
|
|
text += ' (' + perc + '% ' + better + ')<br>';
|
|
}
|
|
}
|
|
|
|
// Find the vendor.
|
|
var mode = AWFYMaster.modes[line.modeid];
|
|
var vendor;
|
|
if (mode)
|
|
vendor = AWFYMaster.vendors[mode.vendor_id];
|
|
if (vendor) {
|
|
text += so + 'source: ' + sc +
|
|
vendor.browser +
|
|
' (' + mode.name + ')'+
|
|
'<br>';
|
|
}
|
|
|
|
// Find the datapoint.
|
|
var point = line.data[x];
|
|
|
|
if (extended) {
|
|
if (point.length > 1 && point[2] && point[1] != point[2]) {
|
|
if (vendor.rangeURL) {
|
|
var url = vendor.rangeURL
|
|
.replace(/{from}/g, point[1])
|
|
.replace(/{to}/g, point[2]);
|
|
text += so + 'revs: ' + sc +
|
|
'<a href="' + url + '">' + point[1] + " to " + point[2] + '</a>';
|
|
} else {
|
|
text += so + 'revs: ' + sc +
|
|
'<a href="' + vendor.url + point[1] + '">' + point[1] + '</a>' +
|
|
' to ' +
|
|
'<a href="' + vendor.url + point[2] + '">' + point[2] + '</a>';
|
|
}
|
|
} else {
|
|
text += so + 'rev: ' + sc +
|
|
'<a href="' + vendor.url + point[1] + '">' + point[1] + '</a>';
|
|
|
|
if (prev && vendor.rangeURL) {
|
|
var url = vendor.rangeURL
|
|
.replace(/{from}/g, prev[1])
|
|
.replace(/{to}/g, point[1])
|
|
.replace(/{num}/g, point[1] - prev[1]);
|
|
text += ' (<a href="' + url + '">changelog</a>)';
|
|
}
|
|
}
|
|
text += '<br>';
|
|
} else {
|
|
if (point.length > 1 && point[2] && point[1] != point[2]) {
|
|
text += so + 'revs: ' + sc +
|
|
point[1] +
|
|
' to ' +
|
|
point[2] +
|
|
'<br>';
|
|
} else {
|
|
text += so + 'rev: ' + sc + point[1] + '<br>';
|
|
}
|
|
}
|
|
|
|
var pad = function (d) {
|
|
if (d == 0)
|
|
return '00';
|
|
else if (d < 10)
|
|
return '0' + d;
|
|
else
|
|
return '' + d;
|
|
}
|
|
|
|
// Format a year, if we should.
|
|
if (extended) {
|
|
var current_year = (new Date()).getFullYear();
|
|
var datefmt = function (t, forceYear, omitTime) {
|
|
var d = new Date(t * 1000);
|
|
var text = Display.Months[d.getMonth()] + ' ' + d.getDate();
|
|
if (d.getFullYear() != current_year || forceYear)
|
|
text += ', ' + d.getFullYear();
|
|
if (!omitTime && (d.getHours() || d.getMinutes())) {
|
|
text += ' ';
|
|
text += pad(d.getHours()) + ':' +
|
|
pad(d.getMinutes());
|
|
}
|
|
return text;
|
|
}
|
|
|
|
if (point.length > 1 &&
|
|
point[2] &&
|
|
point[1] != point[2] &&
|
|
x < this.graph.timelist.length - 1)
|
|
{
|
|
text += so + 'tested: ' + sc +
|
|
datefmt(this.graph.timelist[x], false, true) + ' to ' +
|
|
datefmt(this.graph.timelist[x + 1], true, true) + '<br>';
|
|
} else {
|
|
text += so + 'tested: ' + sc +
|
|
datefmt(this.graph.timelist[x], false, false) + '<br>';
|
|
}
|
|
} else {
|
|
// Include a short timestamp if we're looking at recent changesets.
|
|
var d = new Date(this.graph.timelist[x] * 1000);
|
|
var now = new Date();
|
|
text += so + 'tested: ' + sc;
|
|
if (this.graph.aggregate && x < this.historical)
|
|
text += 'around ';
|
|
text += Display.Months[d.getMonth()] + ' ' + d.getDate();
|
|
if (now.getFullYear() != d.getFullYear())
|
|
text += ', ' + d.getFullYear() + ' ';
|
|
else
|
|
text += ' ';
|
|
if (!this.graph.aggregate || x >= this.historical)
|
|
text += pad(d.getHours()) + ':' + pad(d.getMinutes()) + '<br>';
|
|
}
|
|
|
|
if (extended && point.length >= 5) {
|
|
if (!point[4]) {
|
|
text += so + 'regression: ' + sc + "zoom in to view regression info." + '<br>';
|
|
} else {
|
|
var id = Math.random();
|
|
while (document.getElementById("update_"+id))
|
|
id = Math.random();
|
|
|
|
text += so + 'regression?: ' + sc + "<span id='update_regression_" + id + "'><img src='loading.gif' height='12px'/></span>" + '<br>';
|
|
text += so + 'extra info: ' + sc + "<span id='update_info_" + id + "'><img src='loading.gif' height='12px'/></span>" + '<br>';
|
|
var subtest = AWFY.isSubtest();
|
|
$.get("data-info.php", {
|
|
subtest: subtest,
|
|
id: point[4]
|
|
}, function(data) {
|
|
var html = data.regression.status;
|
|
if (data.regression.id) {
|
|
html += " <a href='regressions/#regression/"+data.regression.id+"'>(view)</a>"
|
|
} else {
|
|
if (subtest)
|
|
html += " <a href='regressions/#add/subtest/"+point[4]+"'>(report regression)</a>"
|
|
else
|
|
html += " <a href='regressions/#add/"+point[4]+"'>(report regression)</a>"
|
|
}
|
|
document.getElementById("update_regression_"+id).innerHTML = html;
|
|
document.getElementById("update_info_"+id).innerHTML = data.info;
|
|
}, "json");
|
|
}
|
|
}
|
|
|
|
return new ToolTip(item.pageX, item.pageY, item, text);
|
|
}
|
|
|
|
Display.prototype.onClick = function (event, pos, item) {
|
|
// Remove the existing hovertip.
|
|
if (this.hovering) {
|
|
this.hovering.remove();
|
|
this.hovering = null;
|
|
}
|
|
|
|
if (!item)
|
|
return;
|
|
|
|
if (this.selectDelay) {
|
|
// When unselecting a plot, the cursor might be over a point, which
|
|
// will give us annoying extra tooltips. To combat this, we force a
|
|
// small delay.
|
|
var d = new Date();
|
|
if (d - this.selectDelay <= 1000) {
|
|
this.plot.unhighlight();
|
|
return;
|
|
}
|
|
}
|
|
|
|
var tooltip = this.createToolTip(item, true);
|
|
tooltip.drawFloating();
|
|
|
|
this.lastToolTip = tooltip;
|
|
|
|
// The color of the line will be the series color.
|
|
var line = this.graph.info[item.seriesIndex];
|
|
if (!line)
|
|
return;
|
|
var mode = AWFYMaster.modes[line.modeid];
|
|
if (!mode)
|
|
return;
|
|
|
|
tooltip.attachLine(mode.color);
|
|
this.attachedTips.push(tooltip);
|
|
}
|
|
|
|
Display.prototype.areItemsEqual = function (item1, item2) {
|
|
return item1.seriesIndex == item2.seriesIndex &&
|
|
item1.dataIndex == item2.dataIndex &&
|
|
item1.datapoint[0] == item2.datapoint[0];
|
|
}
|
|
|
|
Display.prototype.onHover = function (event, pos, item) {
|
|
// Are we already hovering over something?
|
|
if (this.hovering) {
|
|
// If we're hovering over the same point, don't do anything.
|
|
if (item && this.areItemsEqual(item, this.hovering.item))
|
|
return;
|
|
|
|
// Otherwise, remove the div since we will redraw.
|
|
this.hovering.remove();
|
|
this.hovering = null;
|
|
}
|
|
|
|
// If we have a pinned tooltip that has not been moved yet, don't draw a
|
|
// second tooltip on top of it.
|
|
if (this.lastToolTip && !this.lastToolTip.dragged && !this.lastToolTip.closed)
|
|
return;
|
|
|
|
if (!item)
|
|
return;
|
|
|
|
this.hovering = this.createToolTip(item, false);
|
|
this.hovering.drawBasic();
|
|
}
|
|
|
|
Display.prototype.hideToolTips = function () {
|
|
for (var i = 0; i < this.attachedTips.length; i++)
|
|
this.attachedTips[i].hide();
|
|
}
|
|
|
|
Display.prototype.showToolTips = function () {
|
|
for (var i = 0; i < this.attachedTips.length; i++)
|
|
this.attachedTips[i].show();
|
|
}
|
|
|