diff --git a/codespeed/static/css/main.css b/codespeed/static/css/main.css
index 361ffa09..a0c538ff 100644
--- a/codespeed/static/css/main.css
+++ b/codespeed/static/css/main.css
@@ -234,6 +234,7 @@ div.about_content { text-align: left; }
.boxbody input { margin-left: 0; vertical-align: top; }
#options li { margin-bottom: 0.8em; }
.seriescolor { float: right; margin-top: 2px; height: 13px; width: 14px; }
+.envlinecolor { float: right; font-size: 12px; line-height: 18px; color: #555; }
.compplot-wrap { display: inline-block; }
a.togglefold { font-size: normal; color: #000000; }
a.togglefold::before {
diff --git a/codespeed/static/js/changes.js b/codespeed/static/js/changes.js
index 21e5895a..2c93f153 100644
--- a/codespeed/static/js/changes.js
+++ b/codespeed/static/js/changes.js
@@ -93,6 +93,9 @@ function updateRevisionMarkers(env_id) {
});
$("#revision").html(options);
$("#revision").val(current);
+ if (!$("#revision").val()) {
+ $("#revision").prop('selectedIndex', 0);
+ }
}
function refreshContent() {
@@ -123,8 +126,19 @@ function changeRevisions() {
selected_project = projectmatrix[executable];
if (selected_project !== currentproject) {
+ var oldRevs = revisiondata[currentproject] || [];
+ var oldEntry = oldRevs.find(function(r) { return r[1] === $("#revision").val(); });
+ var oldDate = oldEntry ? new Date(oldEntry[0].slice(0, 19)) : null;
currentproject = selected_project;
updateRevisionMarkers($("input[name='environment']:checked").val());
+ if (oldDate) {
+ var newRevs = revisiondata[currentproject] || [];
+ var best = newRevs.reduce(function(bi, r, i) {
+ return Math.abs(new Date(r[0].slice(0, 19)) - oldDate) <
+ Math.abs(new Date(newRevs[bi][0].slice(0, 19)) - oldDate) ? i : bi;
+ }, 0);
+ $("#revision").prop('selectedIndex', best);
+ }
//Give visual cue that the select box has changed
var bgc = $("#revision").parent().parent().css("backgroundColor");
diff --git a/codespeed/static/js/timeline.js b/codespeed/static/js/timeline.js
index 45c40834..6f921856 100644
--- a/codespeed/static/js/timeline.js
+++ b/codespeed/static/js/timeline.js
@@ -21,6 +21,13 @@ function getColor(exe_id) {
return $("#executable > div.boxbody").find("input[value='" + exe_id + "']").data('color');
}
+function updateExeColors() {
+ $("#executable div.boxbody > ul > ul > li > input").each(function() {
+ var $sq = $(this).parent().find("div.seriescolor");
+ $sq.css("background-color", $(this).is(':checked') ? getColor($(this).attr("id").slice(10)) : '');
+ });
+}
+
function shouldPlotEquidistant() { return $("#equidistant").is(':checked'); }
function shouldPlotQuartiles() { return $("#show_quartile_bands").is(':checked'); }
function shouldPlotExtrema() { return $("#show_extrema_bands").is(':checked'); }
@@ -30,7 +37,7 @@ function getConfiguration() {
exe: readCheckbox("input[name='executable']:checked"),
base: $("#baseline option:selected").val(),
ben: $("input[name='benchmark']:checked").val(),
- env: $("input[name='environments']:checked").val(),
+ env: readCheckbox("input[name='environments']:checked"),
revs: $("#revisions option:selected").val(),
equid: shouldPlotEquidistant() ? "on" : "off",
quarts: shouldPlotQuartiles() ? "on" : "off",
@@ -61,23 +68,28 @@ function updateUrl() {
function destroyPlots() {
if (plotInstance) { plotInstance.destroy(); plotInstance = null; }
$("#plot").off("mousemove.tooltip");
+ $("#plot-legend").remove();
miniplotInstances.forEach(function(g) { g.destroy(); });
miniplotInstances = [];
}
// Build dygraphs-ready data from the branches dict.
-// Returns {labels, colors, data, commitMap, sortedDateKeys, seriesIds}
-// Uses customBars: data rows are [x, [low,mid,high], [low,mid,high], ...]
-function buildGraphData(branches, median, equidistant) {
- var seriesIds = [];
- var seriesRaw = {}; // exe_id -> dateKey -> {low, mid, high, commit, tag}
+// Returns {labels, colors, data, commitMap, sortedDateKeys, seriesIds, envIdToIndex}
+// Series keys are "exe_id:env_id". Uses customBars: [x, [low,mid,high], ...]
+function buildGraphData(branches, environments, median, equidistant) {
+ var envIdToIndex = {};
+ (environments || []).forEach(function(env, i) { envIdToIndex[String(env.id)] = i; });
+ var multiEnv = (environments || []).length > 1;
+
+ var seriesIds = []; // composite "exe_id:env_id"
+ var seriesRaw = {}; // composite -> dateKey -> {low, mid, high, commit, tag}
var dateIndex = {}; // dateKey -> Date
for (var branch in branches) {
- for (var exe_id in branches[branch]) {
- seriesIds.push(exe_id);
- seriesRaw[exe_id] = {};
- var pts = branches[branch][exe_id];
+ for (var compositeKey in branches[branch]) {
+ seriesIds.push(compositeKey);
+ seriesRaw[compositeKey] = {};
+ var pts = branches[branch][compositeKey];
for (var i = 0; i < pts.length; i++) {
var pt = pts[i];
var dateKey = pt[0];
@@ -108,10 +120,10 @@ function buildGraphData(branches, median, equidistant) {
}
dateIndex[dateKey] = new Date(dateKey.trim());
- seriesRaw[exe_id][dateKey] = {low: low, mid: mid, high: high,
- commit: commit, tag: tag,
- suite_version: suite_version,
- dateKey: dateKey};
+ seriesRaw[compositeKey][dateKey] = {low: low, mid: mid, high: high,
+ commit: commit, tag: tag,
+ suite_version: suite_version,
+ dateKey: dateKey};
}
}
}
@@ -123,23 +135,33 @@ function buildGraphData(branches, median, equidistant) {
var labels = ['Date'];
var colors = [];
for (var k = 0; k < seriesIds.length; k++) {
- var id = seriesIds[k];
- labels.push($("label[for*='executable" + id + "']").text().trim());
- colors.push(getColor(id));
+ var ckey = seriesIds[k];
+ var kparts = ckey.split(':');
+ var exeId = kparts[0];
+ var envId = kparts[1] || null;
+ var exeLabel = $("label[for*='executable" + exeId + "']").text().trim();
+ if (multiEnv && envId) {
+ var envObj = (environments || []).reduce(function(found, e) {
+ return found || (String(e.id) === envId ? e : null);
+ }, null);
+ exeLabel = exeLabel + ' (' + (envObj ? envObj.name : envId) + ')';
+ }
+ labels.push(exeLabel);
+ colors.push(getColor(exeId));
}
- // commitMap[dateKey][exe_id] = {commit, tag}
+ // commitMap[dateKey][compositeKey] = {commit, tag, low, high, suite_version}
var commitMap = {};
- var tsMap = {}; // timestamp (ms) -> dateKey, for legend lookup
+ var tsMap = {}; // timestamp (ms) -> dateKey, for tooltip lookup
sortedDateKeys.forEach(function(dk) {
tsMap[dateIndex[dk].getTime()] = dk;
commitMap[dk] = {};
- seriesIds.forEach(function(id) {
- if (seriesRaw[id][dk]) {
- var pt = seriesRaw[id][dk];
- commitMap[dk][id] = {commit: pt.commit, tag: pt.tag,
- low: pt.low, high: pt.high,
- suite_version: pt.suite_version};
+ seriesIds.forEach(function(ckey) {
+ if (seriesRaw[ckey][dk]) {
+ var pt = seriesRaw[ckey][dk];
+ commitMap[dk][ckey] = {commit: pt.commit, tag: pt.tag,
+ low: pt.low, high: pt.high,
+ suite_version: pt.suite_version};
}
});
});
@@ -147,8 +169,8 @@ function buildGraphData(branches, median, equidistant) {
var graphData = sortedDateKeys.map(function(dk, idx) {
var xval = equidistant ? idx : dateIndex[dk];
var row = [xval];
- seriesIds.forEach(function(id) {
- var pt = seriesRaw[id][dk];
+ seriesIds.forEach(function(ckey) {
+ var pt = seriesRaw[ckey][dk];
row.push(pt ? [pt.low, pt.mid, pt.high] : null);
});
return row;
@@ -156,7 +178,8 @@ function buildGraphData(branches, median, equidistant) {
return {labels: labels, colors: colors, data: graphData,
commitMap: commitMap, sortedDateKeys: sortedDateKeys,
- seriesIds: seriesIds, tsMap: tsMap, dateIndex: dateIndex};
+ seriesIds: seriesIds, tsMap: tsMap, dateIndex: dateIndex,
+ envIdToIndex: envIdToIndex};
}
function renderPlot(data) {
@@ -173,7 +196,7 @@ function renderPlot(data) {
$("span.options.median").css("display", "inline");
}
- var built = buildGraphData(data.branches, median, equidistant);
+ var built = buildGraphData(data.branches, data.environments || [], median, equidistant);
if (built.data.length === 0) {
$("#plot").html(getLoadText("No data available", 420));
return;
@@ -196,7 +219,6 @@ function renderPlot(data) {
var seriesIds = built.seriesIds;
var tsMap = built.tsMap;
var dateIndex = built.dateIndex;
- var env = $("input[name='environments']:checked").val();
// Find x-positions where suite_version changes between adjacent points
var seen = {};
@@ -204,9 +226,9 @@ function renderPlot(data) {
sortedDateKeys.forEach(function(dk, idx) {
if (idx === 0) { return; }
var prevDk = sortedDateKeys[idx - 1];
- seriesIds.forEach(function(id) {
- var curr = commitMap[dk] && commitMap[dk][id];
- var prev = commitMap[prevDk] && commitMap[prevDk][id];
+ seriesIds.forEach(function(ckey) {
+ var curr = commitMap[dk] && commitMap[dk][ckey];
+ var prev = commitMap[prevDk] && commitMap[prevDk][ckey];
if (curr && prev && curr.suite_version && prev.suite_version &&
curr.suite_version !== prev.suite_version && !seen[dk]) {
seen[dk] = true;
@@ -215,12 +237,12 @@ function renderPlot(data) {
});
});
- // Map label -> color and label -> exeId for tooltip
+ // Map label -> color and label -> composite series key for tooltip
var colorMap = {};
- var labelToExeId = {};
+ var labelToSeriesKey = {};
built.labels.slice(1).forEach(function(lbl, i) {
colorMap[lbl] = built.colors[i];
- labelToExeId[lbl] = seriesIds[i];
+ labelToSeriesKey[lbl] = seriesIds[i];
});
function lookupDateKey(x) {
@@ -239,40 +261,61 @@ function renderPlot(data) {
function legendFormatter(ld) {
if (ld.x === undefined) { return ''; }
var dk = lookupDateKey(ld.x);
- var html = '
' +
- (dk ? dk.slice(0, 16) : '') + '
';
+
+ // Collect commit info from the first visible series with data
+ var commitInfo = null;
+ ld.series.forEach(function(s) {
+ if (commitInfo || !s.isVisible || s.y === undefined) { return; }
+ var seriesKey = labelToSeriesKey[s.labelHTML] || labelToSeriesKey[s.name];
+ if (dk && commitMap[dk] && commitMap[dk][seriesKey]) {
+ commitInfo = commitMap[dk][seriesKey];
+ }
+ });
+
+ var header = '' +
+ (dk ? dk.slice(0, 16) : '');
+ if (commitInfo) {
+ header += ' commit: ' + commitInfo.commit;
+ if (commitInfo.tag) { header += ' tag: ' + commitInfo.tag; }
+ if (versionBoundaries.length > 0 && commitInfo.suite_version) { header += ' suite: ' + commitInfo.suite_version; }
+ header += '';
+ }
+ header += '
';
+
+ var html = header;
ld.series.forEach(function(s) {
if (!s.isVisible || s.y === undefined) { return; }
- var exeId = labelToExeId[s.labelHTML] || labelToExeId[s.name];
+ var seriesKey = labelToSeriesKey[s.labelHTML] || labelToSeriesKey[s.name];
+ if (!seriesKey) { return; } // skip baseline
var color = colorMap[s.labelHTML] || colorMap[s.name] || '#333';
html += '' +
'' + s.labelHTML + ': ' + s.y.toPrecision(4) + '';
- if (dk && commitMap[dk] && commitMap[dk][exeId]) {
- var info = commitMap[dk][exeId];
+ if (dk && commitMap[dk] && commitMap[dk][seriesKey]) {
+ var info = commitMap[dk][seriesKey];
if (info.low !== info.high) {
html += ' [' +
info.low.toPrecision(3) + ' \u2013 ' +
info.high.toPrecision(3) + ']';
}
- html += '
' +
- 'commit: ' + info.commit;
- if (info.tag) { html += ' tag: ' + info.tag; }
- if (versionBoundaries.length > 0 && info.suite_version) { html += ' suite: ' + info.suite_version; }
- html += '';
}
html += '
';
});
return html;
}
- // Per-series options: baseline gets thinner line, no points
+ // Per-series options: baseline gets thinner line; 2nd env gets dashed
var seriesOpts = {};
built.labels.slice(1).forEach(function(lbl, i) {
var isBase = hasBaseline && i === built.labels.length - 2;
+ var ckey = built.seriesIds[i] || '';
+ var envId = ckey.split(':')[1] || null;
+ var envIdx = envId ? (built.envIdToIndex[envId] || 0) : 0;
seriesOpts[lbl] = {
strokeWidth: isBase ? 1.5 : 2,
+ strokePattern: (!isBase && envIdx === 1) ? [6, 3] : null,
drawPoints: !isBase,
pointSize: 3,
+ highlightCircleSize: isBase ? 0 : 5,
color: built.colors[i]
};
});
@@ -363,16 +406,38 @@ function renderPlot(data) {
}, sortedDateKeys[0]);
}
if (!dk || !commitMap[dk]) { return; }
- var id = seriesIds[0];
- var info = commitMap[dk][id];
+ var highlightedLabel = plotInstance && plotInstance.getHighlightSeries && plotInstance.getHighlightSeries();
+ var ckey = (highlightedLabel && labelToSeriesKey[highlightedLabel]) || seriesIds[0];
+ var info = commitMap[dk][ckey];
if (info) {
+ var parts = ckey.split(':');
+ var navEnv = parts[1] || $("input[name='environments']:checked").first().val();
window.location = CHANGES_URL + "?rev=" + info.commit +
- "&exe=" + id + "&env=" + env;
+ "&exe=" + parts[0] + "&env=" + navEnv;
}
}
}
);
+ // Persistent legend below the plot
+ (function() {
+ var $legend = $('');
+ built.labels.slice(1).forEach(function(lbl, i) {
+ var isBase = hasBaseline && i === built.labels.length - 2;
+ var ckey = built.seriesIds[i] || '';
+ var envId = ckey.split(':')[1] || null;
+ var envIdx = envId ? (built.envIdToIndex[envId] || 0) : 0;
+ var color = built.colors[i];
+ var dashed = !isBase && envIdx === 1;
+ var svg = '';
+ $legend.append('' +
+ svg + '' + lbl + '');
+ });
+ $("#plot").after($legend);
+ }());
+
$("#plot").on("mousemove.tooltip", function(e) {
var $tip = $("#dygraph-tooltip");
if (!lastHighlightPoints) { $tip.hide(); return; }
@@ -408,7 +473,7 @@ function renderPlot(data) {
function renderMiniplot(plotid, data) {
var median = data['data_type'] === 'M';
- var built = buildGraphData(data.branches, median, false);
+ var built = buildGraphData(data.branches, data.environments || [], median, false);
if (built.data.length === 0) { return; }
if (data.baseline !== "None") {
@@ -427,6 +492,18 @@ function renderMiniplot(plotid, data) {
: [mxv - 1, mxv + 1];
}
+ var miniSeriesOpts = {};
+ built.labels.slice(1).forEach(function(lbl, i) {
+ var ckey = built.seriesIds[i] || '';
+ var envId = ckey.split(':')[1] || null;
+ var envIdx = envId ? (built.envIdToIndex[envId] || 0) : 0;
+ miniSeriesOpts[lbl] = {
+ strokeWidth: 1.5,
+ strokePattern: envIdx === 1 ? [4, 3] : null,
+ color: built.colors[i]
+ };
+ });
+
var g = new Dygraph(
document.getElementById(plotid),
built.data,
@@ -436,6 +513,7 @@ function renderMiniplot(plotid, data) {
labels: built.labels,
colors: built.colors,
customBars: true,
+ series: miniSeriesOpts,
legend: 'never',
axes: {
x: { drawAxis: false, drawGrid: false },
@@ -476,7 +554,7 @@ function render(data) {
config.nextBenchmarks = data.nextBenchmarks;
$.getJSON("json/", config, render);
}
- $("#revisions").attr("disabled", true);
+ $("#revisions").val(15).attr("disabled", true);
$("#equidistant").attr("disabled", true);
for (var i = 0; i < data.timelines.length; i++) {
var tl = data.timelines[i];
@@ -529,13 +607,14 @@ function setValuesOfInputFields(params) {
var benchmark = valueOrDefault(params.ben, defaults.benchmark);
$("input:radio[name='benchmark']").filter("[value='" + benchmark + "']").prop('checked', true);
- var environment = valueOrDefault(params.env, defaults.environment);
- $("input:radio[name='environments']").filter("[value='" + environment + "']").prop('checked', true);
-
- $("#executable div.boxbody > ul > ul > li > input").each(function() {
- $(this).parent().find("div.seriescolor")
- .css("background-color", getColor($(this).attr("id").slice(10)));
+ var envDefault = (defaults.environments || []).map(String).join(',');
+ var envIds = valueOrDefault(params.env, envDefault).split(',').filter(Boolean);
+ var envSel = $("input[name='environments']");
+ envIds.forEach(function(envId) {
+ envSel.filter("[value='" + envId.trim() + "']").prop('checked', true);
});
+ updateEnvIndicators();
+ updateExeColors();
$("#baselinecolor").css("background-color", baselineColor);
$("#equidistant").prop('checked', valueOrDefault(params.equid, defaults.equidistant) === "on");
@@ -543,18 +622,36 @@ function setValuesOfInputFields(params) {
$("#show_extrema_bands").prop('checked', valueOrDefault(params.extr, defaults.extrema) === "on");
}
+function updateEnvIndicators() {
+ $("input[name='environments']").each(function() {
+ $(this).siblings('.envlinecolor').text('');
+ });
+ $("input[name='environments']:checked").each(function(i) {
+ $(this).siblings('.envlinecolor').text(i === 0 ? '───' : '╌╌╌');
+ });
+}
+
function initializeSite() {
var params = getUrlParams();
- setValuesOfInputFields(params);
setExeColors();
+ setValuesOfInputFields(params);
var onChange = function() { updateUrl(); refreshContent(); };
$("#revisions, #baseline").change(onChange);
- $("input[name='executable'], input[name='branch'], input[name='benchmark']," +
- "input[name='environments'], #equidistant, #show_quartile_bands, #show_extrema_bands"
+ $("input[name='branch'], input[name='benchmark']," +
+ "#equidistant, #show_quartile_bands, #show_extrema_bands"
).change(onChange);
- $('.checkall, .uncheckall').click(onChange);
+ $("input[name='executable']").change(function() { updateExeColors(); onChange(); });
+ $("input[name='environments']").change(function() {
+ if ($("input[name='environments']:checked").length > 2) {
+ $(this).prop('checked', false);
+ return;
+ }
+ updateEnvIndicators();
+ onChange();
+ });
+ $('.checkall, .uncheckall').click(function() { updateExeColors(); onChange(); });
$("#permalink").click(function() {
window.location = "?" + $.param(getConfiguration());
@@ -570,6 +667,9 @@ function initializeSite() {
function init(def) {
defaults = def;
+ if (!defaults.environments) {
+ defaults.environments = defaults.environment ? [defaults.environment] : [];
+ }
$.ajaxSetup({cache: false});
initializeSite();
}
diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html
index cadfa506..71fa46a0 100644
--- a/codespeed/templates/codespeed/timeline.html
+++ b/codespeed/templates/codespeed/timeline.html
@@ -23,8 +23,8 @@
@@ -77,7 +77,7 @@
-
+
Show the last