strong-arc
Version:
A visual suite for the StrongLoop API Platform
450 lines (360 loc) • 13.4 kB
JavaScript
ApiAnalytics.directive('slApiAnalyticsChart', [
'$log',
'$interpolate',
function ($log, $interpolate) {
var pageSize = 10;
function custom(scope, elem){
var margin = {top: 70, right: 120, bottom: 0, left: 270},
width = 960 - margin.left - margin.right,
height = 550 - margin.top - margin.bottom;
var x = d3.scale.linear()
.range([0, width]);
var tip;
var barHeight = 20;
var color = d3.scale.ordinal()
.range(["steelblue"]);
var duration = 750,
delay = 25;
var partition = d3.layout.partition()
.value(function(d) { return d.size; });
var xAxis = d3.svg.axis()
.scale(x)
.ticks(6)
.orient("top");
var svgParent = d3.select(elem.find('.svg-chart')[0]).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom + 40);
var svg = svgParent
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", up);
svg.append("g")
.attr("class", "x axis");
svg.append("g")
.attr("class", "y axis")
.append("line")
.attr("y1", "100%");
function toggleTip(e, tip){
scope.showToolTip = !scope.showToolTip;
if ( scope.showToolTip ) {
tip.show(e);
} else {
tip.hide(e);
}
}
function sortByTime(a, b){
var aTime = new Date(a.orig.timeStamp).getTime();
var bTime = new Date(b.orig.timeStamp).getTime();
return aTime - bTime;
}
function drawAxisLabels(){
var xLabel;
var yLabel;
switch(scope.chartDepth){
case 0:
xLabel = 'Number of requests';
yLabel = 'End point path';
break;
case 1:
xLabel = 'Number of requests';
yLabel = 'Hour of day';
break;
case 2:
xLabel = 'Response time';
yLabel = 'End point path';
break;
}
svg.select('.x.label').remove();
svg.select('.y.label').remove();
//x-axis label
svg.append("text")
.attr("class", "x label")
.attr("text-anchor", "middle")
.attr("x", width/2)
.attr("y", -40)
.text(xLabel);
//y-axis label
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("font-size", "36px")
.attr("x", -100)
.attr("y", 60)
.attr("dy", -60)
.attr("transform", "rotate(0)")
.text(yLabel);
}
function processData(d, i) {
if ( !d.children ) return;
scope.responseMax = d.rangeMax;
var end = duration + d.children.length * delay;
if ( scope.chartDepth === 1 ) {
d.children = d.children.sort(function(a, b){
var aTime = new Date(a.orig.timeStamp).getTime();
var bTime = new Date(b.orig.timeStamp).getTime();
return aTime - bTime;
})
}
drawAxisLabels();
// Mark any currently-displayed bars as exiting.
var exit = svg.selectAll(".enter")
.attr("class", "exit");
// Entering nodes immediately obscure the clicked-on bar, so hide it.
exit.selectAll("rect").filter(function(p) { return p === d; })
.style("fill-opacity", 1e-6);
// Enter the new bars for the clicked-on data.
// Per above, entering bars are immediately visible.
var enter = bar(d)
.attr("transform", stack(i))
.style("opacity", 1);
// Have the text fade-in, even though the bars are visible.
// Color the bars as parents; they will fade to children if appropriate.
enter.select("text").style("fill-opacity", 1e-6);
enter.select("rect").style("fill", color(true));
// Update the x-scale domain.
if (scope.chartDepth === 2 && scope.responseMax) {
x.domain([0, scope.responseMax]).nice();
}
else {
x.domain([0, d3.max(d.children, function(d) { return d.value; })]).nice();
}
// Update the x-axis.
svg.selectAll(".x.axis").transition()
.duration(duration)
.call(xAxis);
//show tooltip for chart 3
if ( scope.chartDepth === 1 || scope.chartDepth === 2 ) {
tip = d3.tip().attr('class', 'd3-tip endpoint').direction('se').html(function(d) {
var list = '';
//show proper data for current chart
if ( scope.chartDepth === 2 ) {
var keys = 'requestMethod statusCode responseDuration responseSize clientDetail'.split(' ');
} else if ( scope.chartDepth === 1 ) {
var keys = 'GET POST DELETE PUT'.split(' ');
}
keys.forEach(function(key){
var val = d.orig[key];
var tmpl = $interpolate('<li>{{key}}: {{val}}</li>');
if ( key === 'responseDuration' ) {
val += 'ms';
}
if ( key === 'responseSize' ){
val += 'kb';
}
if ( key === 'clientDetail' ){
var tmpl = $interpolate('<li>{{key}}: <textarea class="data" onfocus="this.select()">{{val}}</textarea></li>');
val = JSON.stringify(val);
}
list += tmpl({ key: key, val: val });
});
var icon = (
'<span class="ui-icon"><svg version="1.1" viewBox="0 0 30 30" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="arrow-popover">' +
'<g>' +
'<path d="M3.55271368e-15,0 L30,15 L-3.55271368e-15,30"></path>' +
'</g>' +
'</svg></span>'
);
var listTmpl = $interpolate(icon+'<h3>{{name}}</h3><ul>{{list}}</ul>');
return listTmpl({ name: d.name, list: list });
});
svg.call(tip);
svg.selectAll('g.enter g text')
.on('click', function(e){
toggleTip(e, tip);
});
if (scope.chartDepth === 2) {
svg.selectAll('g.enter g rect')
.on('click', function(e){
toggleTip(e, tip);
});
}
}
// Transition entering bars to their new position.
var enterTransition = enter.transition()
.duration(duration)
.delay(function(d, i) { return i * delay; })
.attr("transform", function(d, i) { return "translate(0," + barHeight * i * 1.2 + ")"; });
// Transition entering text.
enterTransition.select("text")
.style("fill-opacity", 1);
// Transition entering rects to the new x-scale.
enterTransition.select("rect")
.attr("width", function(d) { return x(d.value); })
.style("fill", function(d) { return color(true); });
// Transition exiting bars to fade out.
var exitTransition = exit.transition()
.duration(duration)
.style("opacity", 1e-6)
.remove();
// Transition exiting bars to the new x-scale.
exitTransition.selectAll("rect")
.attr("width", function(d) { return x(d.value); });
// Rebind the current node to the background.
svg.select(".background")
.datum(d)
.transition()
.duration(end);
d.index = i;
}
scope.$watch('page', function(newVal, oldVal){
if ( !scope.chart.allData || newVal < 0 ) return; //at first page
var root = scope.chart.data;
var allData = scope.chart.allData;
var nodeIdx = scope.chart.nodeIdx;
var start = (newVal-1)*pageSize;
var end = start+pageSize;
//check if we are on last page or not
if ( allData.children && allData.children.length > start ) {
root.children = allData.children.slice(start, end);
processData(root, nodeIdx);
}
});
scope.$watch('chart', function(newVal, oldVal){
if ( !newVal || !newVal.data ) {
//reset everything if load button is clicked
scope.crumbs = [{ name: 'Past 24 Hours', orig: null }];
scope.chartStack = [];
scope.chartDepth = 0;
return;
}
var root = newVal.data;
var allData = newVal.allData;
var node = newVal.node;
var nodeIdx = newVal.nodeIdx;
scope.page = 1; //reset pagination on new charts
scope.maxPages = Math.ceil(allData.children.length/pageSize); //used by view
partition.nodes(root);
x.domain([0, root.value]).nice();
if ( scope.navDirection === 'down' ) {
if ( Object.keys(oldVal).length ){
//scope.prevChart = oldVal;
scope.chartStack.push(oldVal);
var crumbNode = oldVal.allData.children[newVal.nodeIdx];
scope.crumbs.push({ name: crumbNode.name, orig: crumbNode.orig });
$log.log('stack', scope.chartStack);
}
}
var start = (scope.page-1)*pageSize;
var end = start+pageSize;
//sort by time for chart 2
if ( scope.chartDepth === 1 ) {
root.children = root.children.sort(sortByTime);
allData.children = allData.children.sort(sortByTime);
}
if ( allData.children.length > start ) {
root.children = allData.children.slice(start, end);
}
processData(root, nodeIdx);
});
function down(d, i) {
$log.log('down');
scope.chartDepth += 1;
scope.navDirection = 'down';
//chart only goes three deep
if (this.__transition__ || scope.chartDepth > 2 ) return;
if ( scope.chartDepth === 1 ) {
scope.initialModel = d.name;
}
if ( scope.chartDepth > 0 ) {
scope.getData({ d: d, i: i, depth: scope.chartDepth, initialModel: scope.initialModel })
.then(function(chart){
scope.chart = chart;
})
.catch(function(error) {
$log.warn('bad getData for api analytics chart ', error);
throw error;
});
}
}
function up(d, i, skipCrumb) {
if ( tip ) {
scope.showToolTip = false;
tip.hide();
}
if (this.__transition__) return;
if ( !scope.chartStack.length ) return;
$log.log('up');
scope.chartDepth = scope.chartDepth > 0 ? scope.chartDepth - 1 : 0;
scope.navDirection = 'up';
scope.chart = scope.chartStack.pop();
if ( !skipCrumb ) {
scope.crumbs.pop();
}
$log.log(scope.chart);
}
// Creates a set of bars for the given data node, at the specified index.
function bar(d) {
var bar = svg.insert("g", ".y.axis")
.attr("class", "enter")
.attr("transform", "translate(0,5)")
.selectAll("g")
.data(d.children)
.enter().append("g")
.style("cursor", "pointer");
//.on("click", down);
var calculatedHeight = barHeight*d.children.length;
var minHeight = 100;
var chartHeight = calculatedHeight < minHeight ? minHeight : calculatedHeight;
svgParent.attr('height', chartHeight + margin.top - margin.bottom);
bar.append("text")
.attr("x", -6)
.attr("y", barHeight / 2)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d.name; });
bar.append("rect")
.attr("width", function(d) {
return x(d.value);
})
.attr("height", barHeight)
.on('click', down);
return bar;
}
// A stateful closure for stacking bars horizontally.
function stack(i) {
var x0 = 0;
return function(d) {
var tx = "translate(" + x0 + "," + barHeight * i * 1.2 + ")";
x0 += x(d.value);
return tx;
};
}
}
return {
restrict: "E",
replace: true,
scope: {
chart: '=',
getData: '&getdata'
},
templateUrl: './scripts/modules/api-analytics/templates/api.analytics.chart.html',
link: function(scope, elem, attrs){
scope.chartDepth = 0;
scope.initialModel = null;
scope.chartStack = [];
scope.crumbs = [];
scope.navDirection = 'down';
scope.showToolTip = false;
scope.page = 1;
//example(scope, elem);
custom(scope, elem);
scope.onClickCrumb = function(i, crumb, len){
scope.navDirection = 'up';
scope.chartDepth = i;
scope.chartStack.splice(i+1, len);
scope.chart = scope.chartStack.pop();
};
scope.onClickPrevPage = function(){
scope.page--;
};
scope.onClickNextPage = function(){
scope.page++;
};
}
};
}
]);