{% extends "/base.html" %}
{% block html_attr %} ng-app="TaskApp"{% endblock %}
{% block title_text %}Rally Task Report{% endblock %}
{% block libs %}
{% if include_libs %}
{% else %}
{% endif %}
{% endblock %}
{% block js_before %}
{% raw %}
"use strict";
if (typeof angular === "object") { angular.module("TaskApp", []).controller(
"TaskController", ["$scope", "$location", function($scope, $location) {
{% endraw %}
$scope.source = {{ source }};
$scope.scenarios = {{ data }};
{% raw %}
$scope.location = {
/* #/path/hash/sub/div */
normalize: function(str) {
/* Remove unwanted characters from string */
if (typeof str !== "string") { return "" }
return str.replace(/[^\w\-\.]/g, "")
},
uri: function(obj) {
/* Getter/Setter */
if (! obj) {
var uri = {path: "", hash: "", sub: "", div: ""};
var arr = ["div", "sub", "hash", "path"];
angular.forEach($location.url().split("/"), function(value){
var v = $scope.location.normalize(value);
if (v) { var k = arr.pop(); if (k) { this[k] = v }}
}, uri);
return uri
}
var arr = [obj.path, obj.hash, obj.sub, obj.div], res = [];
for (var i in arr) { if (! arr[i]) { break }; res.push(arr[i]) }
return $location.url("/" + res.join("/"))
},
path: function(path, hash) {
/* Getter/Setter */
if (path === "") { return this.uri({}) }
path = this.normalize(path);
var uri = this.uri();
if (! path) { return uri.path }
uri.path = path;
var _hash = this.normalize(hash);
if (_hash || hash === "") { uri.hash = _hash }
return this.uri(uri)
},
hash: function(hash) {
/* Getter/Setter */
if (hash) { this.uri({path:this.uri().path, hash:hash}) }
return this.uri().hash
}
}
/* Dispatch */
$scope.route = function(uri) {
if (! $scope.scenarios_map) { return }
if (uri.path in $scope.scenarios_map) {
$scope.view = {is_scenario:true};
$scope.scenario = $scope.scenarios_map[uri.path];
$scope.nav_idx = $scope.nav_map[uri.path];
if ($scope.scenario.iterations.histogram.views.length) {
$scope.mainHistogram = $scope.scenario.iterations.histogram.views[0]
}
if ($scope.scenario.atomic.histogram.views.length) {
$scope.atomicHistogram = $scope.scenario.atomic.histogram.views[0]
}
$scope.outputIteration = 0;
$scope.showTab(uri);
} else {
$scope.scenario = null;
if (uri.path === "source") {
$scope.view = {is_source:true}
} else {
$scope.view = {is_main:true}
}
}
}
$scope.$on("$locationChangeSuccess", function (event, newUrl, oldUrl) {
$scope.route($scope.location.uri())
});
$scope.showNav = function(nav_idx) { $scope.nav_idx = nav_idx }
/* Tabs */
$scope.tabs = [
{
id: "overview",
name: "Overview",
visible: function(){ return !! $scope.scenario.iterations.pie.length }
},{
id: "details",
name: "Details",
visible: function(){ return !! $scope.scenario.atomic.pie.length }
},{
id: "output",
name: "Scenario Data",
visible: function(){ return $scope.scenario.output.length }
},{
id: "failures",
name: "Failures",
visible: function(){ return !! $scope.scenario.errors.length }
},{
id: "task",
name: "Input task",
visible: function(){ return !! $scope.scenario.config }
}
];
$scope.tabs_map = {};
angular.forEach($scope.tabs,
function(tab){ this[tab.id] = tab }, $scope.tabs_map);
$scope.showTab = function(uri) {
$scope.tab = uri.hash in $scope.tabs_map ? uri.hash : "overview";
if (! $scope.scenario.output) {
var has_additive = !! $scope.scenario.additive_output.length;
var has_complete = !! ($scope.scenario.complete_output.length
&& $scope.scenario.complete_output[0].length);
$scope.scenario.output = {
has_additive: has_additive,
has_complete: has_complete,
length: has_additive + has_complete,
active: has_additive ? "additive" : (has_complete ? "complete" : "")
}
}
if (uri.hash === "output") {
if (uri.sub && $scope.scenario.output["has_" + uri.sub]) {
$scope.scenario.output.active = uri.sub
}
}
}
for (var i in $scope.tabs) {
if ($scope.tabs[i].id === $scope.location.hash()) {
$scope.tab = $scope.tabs[i].id
}
$scope.tabs[i].isVisible = function() {
if ($scope.scenario) {
if (this.visible()) { return true }
/* If tab should be hidden but is selected - show another one */
if (this.id === $scope.location.hash()) {
for (var i in $scope.tabs) {
var tab = $scope.tabs[i];
if (tab.id != this.id && tab.visible()) {
$scope.tab = tab.id;
return false
}
}
}
}
return false
}
}
$scope.showError = function(message) {
return (function (e) {
e.style.display = "block";
e.textContent = message
})(document.getElementById("page-error"))
}
/* Initialization */
angular.element(document).ready(function(){
if (! $scope.scenarios.length) {
return $scope.showError("No data...")
}
/* Compose data mapping */
$scope.nav = [];
$scope.nav_map = {};
$scope.scenarios_map = {};
var met = [], itr = 0, cls_idx = 0;
var prev_cls, prev_met;
for (var idx in $scope.scenarios) {
var sc = $scope.scenarios[idx];
if (! prev_cls) {
prev_cls = sc.cls
}
else if (prev_cls !== sc.cls) {
$scope.nav.push({cls:prev_cls, met:met, idx:cls_idx});
prev_cls = sc.cls;
met = [];
itr = 1;
cls_idx += 1
}
if (prev_met !== sc.met) { itr = 1 };
sc.ref = $scope.location.normalize(sc.cls+"."+sc.met+(itr > 1 ? "-"+itr : ""));
$scope.scenarios_map[sc.ref] = sc;
$scope.nav_map[sc.ref] = cls_idx;
met.push({name:sc.name, itr:itr, idx:idx, ref:sc.ref});
prev_met = sc.met;
itr += 1;
}
if (met.length) {
$scope.nav.push({cls:prev_cls, met:met, idx:cls_idx})
}
/* Start */
var uri = $scope.location.uri();
uri.path = $scope.location.path();
$scope.route(uri);
$scope.$digest()
});
}])
.directive("widget", function($compile) {
var Chart = {
_render: function(node, data, chart, do_after){
nv.addGraph(function() {
d3.select(node)
.datum(data).transition().duration(0)
.call(chart);
if (typeof do_after === "function") {
do_after(node, chart)
}
nv.utils.windowResize(chart.update);
})
},
/* NOTE(amaretskiy): this is actually a result of
d3.scale.category20().range(), excluding red color (#d62728)
which is reserved for errors */
_colors: ["#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c",
"#98df8a", "#ff9896", "#9467bd", "#c5b0d5", "#8c564b",
"#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", "#c7c7c7",
"#bcbd22", "#dbdb8d", "#17becf", "#9edae5"],
_widgets: {
Pie: "pie",
StackedArea: "stack",
Lines: "lines",
Histogram: "histogram"
},
get_chart: function(widget) {
if (widget in this._widgets) {
var name = this._widgets[widget];
return Chart[name]
}
return function() { console.log("Error: unexpected widget:", widget) }
},
pie: function(node, data, opts, do_after) {
var chart = nv.models.pieChart()
.x(function(d) { return d.key })
.y(function(d) { return d.values })
.showLabels(true)
.labelType("percent")
.donut(true)
.donutRatio(0.25)
.donutLabelsOutside(true)
.color(function(d){
if (d.data && d.data.color) { return d.data.color }
});
var data_ = [], colors = [], colors_map = {errors: "#d62728"};
for (var i in data) {
var key = data[i][0];
if (! (key in colors_map)) {
if (! colors.length) { colors = Chart._colors.slice() }
colors_map[key] = colors.shift()
}
data_.push({key:key, values:data[i][1], color:colors_map[key]})
}
Chart._render(node, data_, chart)
},
stack: function(node, data, opts, do_after) {
var chart = nv.models.stackedAreaChart()
.x(function(d) { return d[0] })
.y(function(d) { return d[1] })
.useInteractiveGuideline(opts.guide)
.showControls(opts.controls)
.clipEdge(true);
chart.xAxis
.tickFormat(d3.format(opts.xformat || "d"))
.axisLabel(opts.xname || "")
.showMaxMin(false);
chart.yAxis
.orient("left")
.tickFormat(d3.format(opts.yformat || ",.3f"));
var data_ = [];
for (var i in data) {
var d = {key:data[i][0], values:data[i][1]};
if (d.key === "failed_duration") {
d.color = "#d62728"
}
data_.push(d);
}
Chart._render(node, data_, chart, do_after);
},
lines: function(node, data, opts, do_after) {
var chart = nv.models.lineChart()
.x(function(d) { return d[0] })
.y(function(d) { return d[1] })
.useInteractiveGuideline(opts.guide)
.clipEdge(true);
chart.xAxis
.tickFormat(d3.format(opts.xformat || "d"))
.axisLabel(opts.xname || "")
.showMaxMin(false);
chart.yAxis
.orient("left")
.tickFormat(d3.format(opts.yformat || ",.3f"));
var data_ = [];
for (var i in data) {
var d = {key:data[i][0], values:data[i][1]};
if (d.key === "failed_duration") {
d.color = "#d62728"
}
data_.push(d)
}
Chart._render(node, data_, chart, do_after)
},
histogram: function(node, data, opts) {
var chart = nv.models.multiBarChart()
.reduceXTicks(true)
.showControls(false)
.transitionDuration(0)
.groupSpacing(0.05);
chart
.legend.radioButtonMode(true);
chart.xAxis
.axisLabel("Duration (seconds)")
.tickFormat(d3.format(",.2f"));
chart.yAxis
.axisLabel("Iterations (frequency)")
.tickFormat(d3.format("d"));
Chart._render(node, data, chart)
}
};
return {
restrict: "A",
scope: { data: "=" },
link: function(scope, element, attrs) {
scope.$watch("data", function(data) {
if (! data) { return console.log("Chart has no data to render!") }
if (attrs.widget === "Table") {
var ng_class = attrs.lastrowClass ? " ng-class='{"+attrs.lastrowClass+":$last}'" : "";
var template = "
" +
"
{{i}}
" +
"
" +
"
" +
"
{{i}}" +
"
" +
"
";
var el = element.empty().append($compile(template)(scope)).children()[0]
} else {
var el_chart = element.addClass("chart").css({display:"block"});
var el = el_chart.html("").children()[0];
var do_after = null;
if (attrs.widget in {StackedArea:0, Lines:0}) {
/* Hide widget if not enough data */
if ((! data.length) || (data[0].length < 1) || (data[0][1].length < 2)) {
return element.empty().css({display:"none"})
}
/* NOTE(amaretskiy): Dirty fix for changing chart width in case
if there are too long Y values that overlaps chart box. */
var do_after = function(node, chart){
var g_box = angular.element(el_chart[0].querySelector(".nv-y.nv-axis"));
if (g_box && g_box[0] && g_box[0].getBBox) {
try {
// 30 is padding aroung graphs
var width = g_box[0].getBBox().width + 30;
} catch (err) {
// This happens sometimes, just skip silently
return
}
// 890 is chart width (set by CSS)
if (typeof width === "number" && width > 890) {
width = (890 * 2) - width;
if (width > 0) {
angular.element(node).css({width:width+"px"});
chart.update()
}
}
}
}
}
else if (attrs.widget === "Pie") {
if (! data.length) {
return element.empty().css({display:"none"})
}
}
var options = {
xname: attrs.nameX || "",
xformat: attrs.formatX || "d",
yformat: attrs.formatY || ",.3f",
controls: attrs.controls === "true",
guide: attrs.guide === "true"
};
Chart.get_chart(attrs.widget)(el, data, options, do_after);
}
if (attrs.nameY) {
/* NOTE(amaretskiy): Dirty fix for displaying Y-axis label correctly.
I believe sometimes NVD3 will allow doing this in normal way */
var label_y = angular.element("
").addClass("chart-label-y").text(attrs.nameY);
angular.element(el).parent().prepend(label_y)
}
if (attrs.description) {
var desc_el = angular.element("
").addClass(attrs.descriptionClass || "h3").text(attrs.description);
angular.element(el).parent().prepend(desc_el)
}
if (attrs.title) {
var title_el = angular.element("