diff --git a/lnt/server/ui/static/lnt_regression.js b/lnt/server/ui/static/lnt_regression.js
new file mode 100644
--- /dev/null
+++ b/lnt/server/ui/static/lnt_regression.js
@@ -0,0 +1,463 @@
+/*jslint vars: true, browser: true, devel: true, plusplus: true, unparam: true*/
+/*global $, jQuery, alert, db_name, test_suite_name, init, changes */
+/*global update_graph*/
+// Keep the graph data we download.
+// Each element is a list of graph data points.
+var data_cache = [];
+var is_checked = []; // The current list of lines to plot.
+var normalize = false;
+
+var MAX_TO_DRAW = 10;
+
+var STATE_NAMES = {0: 'Detected',
+ 1: 'Staged',
+ 10: 'Active',
+ 20: 'Not to be Fixed',
+ 21: 'Ignored',
+ 23: 'Verify',
+ 22: 'Fixed'};
+
+var regression_cache = [];
+var lnt_graph = {};
+
+
+// Grab the graph API url for this line.
+function get_api_url(kind, db, ts, mtf) {
+ "use strict";
+ return [lnt_url_base, "api", "db_" + db, "v4", ts, kind, mtf].join('/');
+}
+
+// Grab the URL for a regression by id.
+function get_regression_url(db, ts, regression) {
+ "use strict";
+ return [lnt_url_base, "db_" + db, "v4", ts, "regressions", regression].join('/');
+}
+
+// Grab the URL for a run by id.
+function get_run_url(db, ts, runID) {
+ "use strict";
+ return [lnt_url_base, "db_" + db, "v4", ts, runID].join('/');
+}
+
+// Create a new regression manually URL.
+function get_manual_regression_url(db, ts, url, runID) {
+ "use strict";
+ return [lnt_url_base, "db_" + db, "v4", ts,
+ "regressions/new_from_graph", url, runID].join('/');
+}
+
+
+
+/* Bind events to the zoom bar buttons, so that
+ * the zoom buttons work, then position them
+ * over top of the main graph.
+ */
+function bind_zoom_bar(my_plot) {
+ "use strict";
+ $('#out').click(function (e) {
+ e.preventDefault();
+ my_plot.zoomOut();
+ });
+
+ $('#in').click(function (e) {
+ e.preventDefault();
+ my_plot.zoom();
+ });
+
+ // Now move the bottons onto the graph.
+ $('#graphbox').css('position', 'relative');
+ $('#zoombar').css('position', 'absolute');
+
+ $('#zoombar').css('left', '40px');
+ $('#zoombar').css('top', '15px');
+
+}
+
+
+// Show our overlay tooltip.
+lnt_graph.current_tip_point = null;
+
+function show_tooltip(x, y, item, pos, graph_data) {
+ "use strict";
+ // Given the event handler item, get the graph metadata.
+ function extract_metadata(item) {
+ var index = item.dataIndex;
+ // Graph data is formatted as [x, y, meta_data].
+ var meta_data = item.series.data[index][2];
+ return meta_data;
+ }
+ var data = item.datapoint;
+ var meta_data = extract_metadata(item);
+ var tip_body = '
";
+ var tooltip_div = $(tip_body).css({
+ position: 'absolute',
+ display: 'none',
+ top: y + 5,
+ left: x + 5,
+ border: '1px solid #fdd',
+ padding: '2px',
+ 'background-color': '#fee',
+ opacity: 0.80,
+ 'z-index': 100000
+ }).appendTo("body").fadeIn(200);
+
+ // Now make sure the tool tip is on the graph canvas.
+ var tt_position = tooltip_div.position();
+
+ var graph_div = $("#graph");
+ var graph_position = graph_div.position();
+
+ // The right edge of the graph.
+ var max_width = graph_position.left + graph_div.width();
+ // The right edge of the tool tip.
+ var tt_right = tt_position.left + tooltip_div.width();
+
+ if (tt_right > max_width) {
+ var diff = tt_right - max_width;
+ var GRAPH_BORDER = 10;
+ var VISUAL_APPEAL = 10;
+ tooltip_div.css({'left' : tt_position.left - diff
+ - GRAPH_BORDER - VISUAL_APPEAL});
+ }
+
+}
+
+// Event handler function to update the tooltop.
+function update_tooltip(event, pos, item, show_fn, graph_data) {
+ "use strict";
+ if (!item) {
+ $("#tooltip").fadeOut(200, function () {
+ $("#tooltip").remove();
+ });
+ lnt_graph.current_tip_point = null;
+ return;
+ }
+
+ if (!lnt_graph.current_tip_point || (lnt_graph.current_tip_point[0] !== item.datapoint[0] ||
+ lnt_graph.current_tip_point[1] !== item.datapoint[1])) {
+ $("#tooltip").remove();
+ lnt_graph.current_tip_point = item.datapoint;
+ show_fn(pos.pageX, pos.pageY, item, pos, graph_data);
+ }
+}
+
+// Normalize this data to the element in index
+function normalize_data(data_array, index) {
+ "use strict";
+ var new_data = new Array(data_array.length);
+ var i = 0;
+ var factor = 0;
+ for (i = 0; i < data_array.length; i++) {
+ if (data_array[i][0] == index) {
+ factor = data_array[i][1];
+ break;
+ }
+ }
+ console.assert(factor !== 0, "Did not find the element to normalize on.");
+ for (i = 0; i < data_array.length; i++) {
+ new_data[i] = jQuery.extend({}, data_array[i]);
+ new_data[i][1] = (data_array[i][1] / factor) * 100;
+ }
+ return new_data;
+}
+
+
+function try_normal(data_array, index) {
+ "use strict";
+ if (normalize) {
+ return normalize_data(data_array, index);
+ }
+ return data_array;
+}
+
+
+function make_graph_point_entry(data, color, regression) {
+ "use strict";
+ var radius = 0.25;
+ var fill = true;
+ if (regression) {
+ radius = 5.0;
+ fill = false;
+ color = "red";
+ }
+ var entry = {"color": color,
+ "data": data,
+ "lines": {"show": false},
+ "points": {"fill": fill,
+ "radius": radius,
+ "show": true
+ }
+ };
+ if (regression) {
+ entry.points.symbol = "triangle";
+ }
+ return entry;
+}
+
+var color_codes = ["#4D4D4D",
+ "#5DA5DA",
+ "#FAA43A",
+ "#60BD68",
+ "#F17CB0",
+ "#B2912F",
+ "#B276B2",
+ "#DECF3F",
+ "#F15854",
+ "#1F78B4",
+ "#33A02C",
+ "#E31A1C",
+ "#FF7F00",
+ "#6A3D9A",
+ "#A6CEE3",
+ "#B2DF8A",
+ "#FB9A99",
+ "#FDBF6F",
+ "#CAB2D6"];
+
+function new_graph_data_callback(data, index) {
+ "use strict";
+ data_cache[index] = data;
+ update_graph();
+}
+
+
+function get_regression_id() {
+ "use strict";
+ var path = window.location.pathname.split("/");
+ if (path[path.length - 2] === "regressions") {
+ return parseInt(path[path.length - 1], 10);
+ }
+}
+
+
+function new_graph_regression_callback(data, index, update_func) {
+ "use strict";
+ $.each(data, function (i, d) {
+
+ if (get_regression_id() !== null) {
+ if (get_regression_id() === d.id || d.state === 21) {
+ return;
+ }
+ }
+ if (!(regression_cache[index])) {
+ regression_cache[index] = [];
+ }
+ var metadata = {'label': d.end_point[0],
+ 'title': d.title,
+ 'id': d.id,
+ 'link': get_regression_url(db_name, test_suite_name, d.id),
+ 'state': STATE_NAMES[d.state]};
+ regression_cache[index].push([parseInt(d.end_point[0], 10), d.end_point[1], metadata]);
+ });
+ update_func();
+}
+
+var NOT_DRAWING = '' +
+ 'Too many lines to plot. Limit is ' + MAX_TO_DRAW + "." +
+ '
×' +
+ '
';
+
+
+function update_graph() {
+ "use strict";
+ var to_draw = [];
+ var starts = [];
+ var ends = [];
+ var lines_to_draw = 0;
+ var i = 0;
+ var color = null;
+ var data = null;
+ var regressions = null;
+ // We need to find the x bounds of the data, sine regressions may be
+ // outside that range.
+ var mins = [];
+ var maxs = [];
+ // Data processing.
+ for (i = 0; i < changes.length; i++) {
+ if (is_checked[i] && data_cache[i]) {
+ lines_to_draw++;
+ starts.push(changes[i].start);
+ ends.push(changes[i].end);
+ color = color_codes[i % color_codes.length];
+ data = try_normal(data_cache[i], changes[i].start);
+ // Find local x-axis min and max.
+ var local_min = parseFloat(data[0][0]);
+ var local_max = parseFloat(data[0][0]);
+ for (var j = 0; j < data.length; j++) {
+ var datum = data[j];
+ var d = parseFloat(datum[0]);
+ if (d < local_min) {
+ local_min = d;
+ }
+ if (d > local_max) {
+ local_max = d;
+ }
+ }
+ mins.push(local_min);
+ maxs.push(local_max);
+
+ to_draw.push(make_graph_point_entry(data, color, false));
+ to_draw.push({"color": color, "data": data, "url": changes[i].url});
+ }
+ }
+ // Zoom the graph to only the data sets, not the regressions.
+ var min_x = Math.min.apply(Math, mins);
+ var max_x = Math.max.apply(Math, maxs);
+ // Regressions.
+ for (i = 0; i < changes.length; i++) {
+ if (is_checked[i] && data_cache[i]) {
+ if (regression_cache[i]) {
+ regressions = try_normal(regression_cache[i]);
+ to_draw.push(make_graph_point_entry(regressions, color, true));
+ }
+ }
+ }
+ // Limit the number of lines to plot: the graph gets cluttered and slow.
+ if (lines_to_draw > MAX_TO_DRAW) {
+ $('#errors').empty().prepend(NOT_DRAWING);
+ return;
+ }
+ var lowest_rev = Math.min.apply(Math, starts);
+ var highest_rev = Math.max.apply(Math, ends);
+ init(to_draw, lowest_rev, highest_rev, min_x, max_x);
+}
+
+// To be called by main page. It will fetch data and make graph ready.
+function add_data_to_graph(URL, index, max_samples) {
+ "use strict";
+ $.getJSON(get_api_url("graph", db_name, test_suite_name, URL) + "?limit=" + max_samples, function (data) {
+ new_graph_data_callback(data, index);
+ });
+ $.getJSON(get_api_url("regression", db_name, test_suite_name, URL) + "?limit=" + max_samples, function (data) {
+ new_graph_regression_callback(data, index, update_graph);
+ });
+ is_checked[index] = true;
+}
+
+
+function init_axis() {
+ "use strict";
+ function onlyUnique(value, index, self) {
+ return self.indexOf(value) === index;
+ }
+
+ var metrics = $('.metric').map(function () {
+ return $(this).text();
+ }).get();
+ metrics = metrics.filter(onlyUnique);
+
+ var yaxis_name = metrics.join(", ");
+ yaxis_name = yaxis_name.replace("_", " ");
+
+ $('#yaxis').text(yaxis_name);
+
+ $('#normalize').click(function (e) {
+ normalize = !normalize;
+ if (normalize) {
+ $('#normalize').toggleClass("btn-default btn-primary");
+ $('#normalize').text("x1");
+ $('#yaxis').text("Normalized (%)");
+ } else {
+ $('#normalize').toggleClass("btn-primary btn-default");
+ $('#normalize').text("%");
+ $('#yaxis').text(yaxis_name);
+ }
+ update_graph();
+ });
+
+ $('#xaxis').css('position', 'absolute');
+ $('#xaxis').css('left', '50%');
+ $('#xaxis').css('bottom', '-15px');
+ $('#xaxis').css('width', '100px');
+ $('#xaxis').css('margin-left', '-50px');
+
+ $('#yaxis').css('position', 'absolute');
+ $('#yaxis').css('left', '-55px');
+ $('#yaxis').css('top', '50%');
+ $('#yaxis').css('-webkit-transform', 'rotate(-90deg)');
+ $('#yaxis').css('-moz-transform', 'rotate(-90deg)');
+}
+
+function init(data, start_highlight, end_highlight, x_min, x_max) {
+ "use strict";
+ // First, set up the primary graph.
+ var graph = $("#graph");
+ var graph_plots = data;
+ var line_width = 1;
+ if (data.length > 0 && data[0].data.length < 50) {
+ line_width = 2;
+ }
+ var graph_options = {
+ xaxis: {
+ min: x_min,
+ max: x_max
+ },
+ series : {
+ lines : {lineWidth : line_width},
+ shadowSize : 0
+ },
+ highlight : {
+ range: {"end": [end_highlight], "start": [start_highlight]},
+ alpha: "0.35",
+ stroke: true
+ },
+ zoom : { interactive : false },
+ pan : { interactive : true,
+ frameRate: 60 },
+ grid : {
+ hoverable : true,
+ clickable: true
+ }
+ };
+
+ var main_plot = $.plot("#graph", graph_plots, graph_options);
+
+ // Add tooltips.
+ graph.bind("plotclick", function (e, p, i) {
+ update_tooltip(e, p, i, show_tooltip, graph_plots);
+ });
+
+ bind_zoom_bar(main_plot);
+}
diff --git a/lnt/server/ui/templates/v4_new_regressions.html b/lnt/server/ui/templates/v4_new_regressions.html
--- a/lnt/server/ui/templates/v4_new_regressions.html
+++ b/lnt/server/ui/templates/v4_new_regressions.html
@@ -25,7 +25,7 @@
-
+
{% endblock %}
{% block javascript %}
diff --git a/lnt/server/ui/templates/v4_regression_detail.html b/lnt/server/ui/templates/v4_regression_detail.html
--- a/lnt/server/ui/templates/v4_regression_detail.html
+++ b/lnt/server/ui/templates/v4_regression_detail.html
@@ -27,7 +27,7 @@
-
+
{% endblock %}