Refactor the timeline directive.
Presently the timeline directive is a single gigantic directive containing all of the timeline code. Given the amount of code involved, this complicates maintenance and makes it excessively difficult to improve. This refactors the single timeline directive into four separate collaborating directives: a controller, an overview, a viewport, and dstat charts. Each child directive is functionally separate from the others, but can communicate with other chart components via the controller. This separation should greatly improve future maintenance, and has already led to several (included) bug fixes. Change-Id: Id11d15a34466e42c5ebc9808717d52b468245e3a
This commit is contained in:
parent
fe6f75a271
commit
e7473f7f01
@ -8,6 +8,7 @@ var controllersModule = require('./_index');
|
||||
function MainCtrl($window, $scope) {
|
||||
$window.addEventListener('resize', function () {
|
||||
$scope.$broadcast('windowResize');
|
||||
$scope.$apply();
|
||||
});
|
||||
}
|
||||
|
||||
|
202
app/js/directives/timeline-dstat.js
Normal file
202
app/js/directives/timeline-dstat.js
Normal file
@ -0,0 +1,202 @@
|
||||
'use strict';
|
||||
|
||||
var directivesModule = require('./_index.js');
|
||||
|
||||
var arrayUtil = require('../util/array-util');
|
||||
var parseDstat = require('../util/dstat-parse');
|
||||
var d3 = require('d3');
|
||||
|
||||
var getDstatLanes = function(data, mins, maxes) {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var row = data[0];
|
||||
var lanes = [];
|
||||
|
||||
if ('total_cpu_usage_usr' in row && 'total_cpu_usage_sys' in row) {
|
||||
lanes.push([{
|
||||
scale: d3.scale.linear().domain([0, 100]),
|
||||
value: function(d) {
|
||||
return d.total_cpu_usage_wai;
|
||||
},
|
||||
color: "rgba(224, 188, 188, 1)",
|
||||
text: "CPU wait"
|
||||
}, {
|
||||
scale: d3.scale.linear().domain([0, 100]),
|
||||
value: function(d) {
|
||||
return d.total_cpu_usage_usr + d.total_cpu_usage_sys;
|
||||
},
|
||||
color: "rgba(102, 140, 178, 0.75)",
|
||||
text: "CPU (user+sys)"
|
||||
}]);
|
||||
}
|
||||
|
||||
if ('memory_usage_used' in row) {
|
||||
lanes.push([{
|
||||
scale: d3.scale.linear().domain([0, maxes.memory_usage_used]),
|
||||
value: function(d) { return d.memory_usage_used; },
|
||||
color: "rgba(102, 140, 178, 0.75)",
|
||||
text: "Memory"
|
||||
}]);
|
||||
}
|
||||
|
||||
if ('net_total_recv' in row && 'net_total_send' in row) {
|
||||
lanes.push([{
|
||||
scale: d3.scale.linear().domain([0, maxes.net_total_recv]),
|
||||
value: function(d) { return d.net_total_recv; },
|
||||
color: "rgba(224, 188, 188, 1)",
|
||||
text: "Net Down"
|
||||
}, {
|
||||
scale: d3.scale.linear().domain([0, maxes.net_total_send]),
|
||||
value: function(d) { return d.net_total_send; },
|
||||
color: "rgba(102, 140, 178, 0.75)",
|
||||
text: "Net Up",
|
||||
type: "line"
|
||||
}]);
|
||||
}
|
||||
|
||||
if ('dsk_total_read' in row && 'dsk_total_writ' in row) {
|
||||
lanes.push([{
|
||||
scale: d3.scale.linear().domain([0, maxes.dsk_total_read]),
|
||||
value: function(d) { return d.dsk_total_read; },
|
||||
color: "rgba(224, 188, 188, 1)",
|
||||
text: "Disk Read",
|
||||
type: "line"
|
||||
}, {
|
||||
scale: d3.scale.linear().domain([0, maxes.dsk_total_writ]),
|
||||
value: function(d) { return d.dsk_total_writ; },
|
||||
color: "rgba(102, 140, 178, 0.75)",
|
||||
text: "Disk Write",
|
||||
type: "line"
|
||||
}]);
|
||||
}
|
||||
|
||||
return lanes;
|
||||
};
|
||||
|
||||
function timelineDstat() {
|
||||
var link = function(scope, el, attrs, timelineController) {
|
||||
var margin = timelineController.margin;
|
||||
var height = 140;
|
||||
var lanes = [];
|
||||
var laneHeight = 30;
|
||||
|
||||
var chart = d3.select(el[0])
|
||||
.append('svg')
|
||||
.attr('width', timelineController.width + margin.left + margin.right)
|
||||
.attr('height', height);
|
||||
|
||||
var main = chart.append('g')
|
||||
.attr('transform', 'translate(' + margin.left + ',0)')
|
||||
.attr('width', timelineController.width);
|
||||
|
||||
var xSelected = timelineController.axes.selection;
|
||||
var y = d3.scale.linear();
|
||||
|
||||
var update = function() {
|
||||
if (lanes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var extent = timelineController.viewExtents;
|
||||
var minExtent = extent[0];
|
||||
var maxExtent = extent[1];
|
||||
|
||||
var entries = timelineController.dstat.entries;
|
||||
var timeFunc = function(d) { return d.system_time; };
|
||||
|
||||
var visibleEntries = entries.slice(
|
||||
arrayUtil.binaryMinIndex(minExtent, entries, timeFunc),
|
||||
arrayUtil.binaryMaxIndex(maxExtent, entries, timeFunc)
|
||||
);
|
||||
|
||||
// apply the current dataset (visibleEntries) to each dstat path
|
||||
lanes.forEach(function(lane) {
|
||||
lane.forEach(function(pathDef) {
|
||||
pathDef.path
|
||||
.datum(visibleEntries)
|
||||
.attr("d", pathDef.area);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var initLane = function(lane, i) {
|
||||
var laneGroup = main.append('g');
|
||||
|
||||
var text = laneGroup.append('text')
|
||||
.attr('y', y(i + 0.5))
|
||||
.attr('dy', '0.5ex')
|
||||
.attr('text-anchor', 'end')
|
||||
.style('font', '10px sans-serif');
|
||||
|
||||
var dy = 0;
|
||||
|
||||
lane.forEach(function(pathDef) {
|
||||
var laneHeight = 0.8 * y(1);
|
||||
pathDef.scale.range([laneHeight, 0]);
|
||||
|
||||
if ('text' in pathDef) {
|
||||
text.append('tspan')
|
||||
.attr('x', -margin.right)
|
||||
.attr('dy', dy)
|
||||
.text(pathDef.text)
|
||||
.attr('fill', pathDef.color);
|
||||
|
||||
dy += 10;
|
||||
}
|
||||
|
||||
pathDef.path = laneGroup.append('path');
|
||||
if (pathDef.type === 'line') {
|
||||
pathDef.area = d3.svg.line()
|
||||
.x(function(d) { return xSelected(d.system_time); })
|
||||
.y(function(d) {
|
||||
return y(i) + pathDef.scale(pathDef.value(d));
|
||||
});
|
||||
|
||||
pathDef.path
|
||||
.style('stroke', pathDef.color)
|
||||
.style('stroke-width', '1.5px')
|
||||
.style('fill', 'none');
|
||||
} else {
|
||||
pathDef.area = d3.svg.area()
|
||||
.x(function(d) { return xSelected(d.system_time); })
|
||||
.y0(y(i) + laneHeight)
|
||||
.y1(function(d) {
|
||||
return y(i) + pathDef.scale(pathDef.value(d));
|
||||
});
|
||||
|
||||
pathDef.path.style('fill', pathDef.color);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
scope.$on('dstatLoaded', function(event, dstat) {
|
||||
lanes = getDstatLanes(dstat.entries, dstat.minimums, dstat.maximums);
|
||||
laneHeight = height / (lanes.length + 1);
|
||||
|
||||
y.domain([0, lanes.length]).range([0, height]);
|
||||
|
||||
lanes.forEach(initLane);
|
||||
});
|
||||
|
||||
scope.$on('update', function() {
|
||||
chart.style('width', timelineController.width + margin.left + margin.right);
|
||||
main.style('width', timelineController.width);
|
||||
update(timelineController.dstat);
|
||||
});
|
||||
|
||||
scope.$on('updateView', function() {
|
||||
update(timelineController.dstat);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
require: '^timeline',
|
||||
scope: true,
|
||||
link: link
|
||||
};
|
||||
}
|
||||
|
||||
directivesModule.directive('timelineDstat', timelineDstat);
|
173
app/js/directives/timeline-overview.js
Normal file
173
app/js/directives/timeline-overview.js
Normal file
@ -0,0 +1,173 @@
|
||||
'use strict';
|
||||
|
||||
var directivesModule = require('./_index.js');
|
||||
|
||||
var d3 = require('d3');
|
||||
|
||||
function timelineOverview() {
|
||||
var link = function(scope, el, attrs, timelineController) {
|
||||
var margin = timelineController.margin;
|
||||
var height = 80;
|
||||
var laneHeight = 10;
|
||||
|
||||
var x = timelineController.axes.x;
|
||||
var y = d3.scale.linear();
|
||||
|
||||
var brush = null;
|
||||
|
||||
var chart = d3.select(el[0])
|
||||
.append('svg')
|
||||
.attr('height', height)
|
||||
.style('position', 'relative')
|
||||
.style('width', timelineController.width)
|
||||
.style('left', margin.left)
|
||||
.style('right', margin.right);
|
||||
|
||||
var groups = chart.append('g');
|
||||
|
||||
var updateBrush = function() {
|
||||
timelineController.setViewExtents(brush.extent());
|
||||
};
|
||||
|
||||
var updateItems = function(data) {
|
||||
var lanes = groups
|
||||
.selectAll('g')
|
||||
.data(data, function(d) { return d.key; });
|
||||
|
||||
lanes.enter().append('g');
|
||||
|
||||
var rects = lanes.selectAll('rect').data(
|
||||
function(d) { return d.values; },
|
||||
function(d) { return d.name; });
|
||||
|
||||
rects.enter().append('rect')
|
||||
.attr('y', function(d) { return y(d.worker + 0.5) - 5; })
|
||||
.attr('height', laneHeight);
|
||||
|
||||
rects.attr('x', function(d) { return x(d.startDate); })
|
||||
.attr('width', function(d) { return x(d.endDate) - x(d.startDate); })
|
||||
.attr('stroke', 'rgba(100, 100, 100, 0.25)')
|
||||
.attr('fill', function(d) {
|
||||
return timelineController.statusColorMap[d.status];
|
||||
});
|
||||
|
||||
rects.exit().remove();
|
||||
lanes.exit().remove();
|
||||
};
|
||||
|
||||
var centerViewport = function(date) {
|
||||
// explicitly center the viewport on a date
|
||||
var timeExtents = timelineController.timeExtents;
|
||||
var start = timeExtents[0];
|
||||
var end = timeExtents[1];
|
||||
|
||||
if (date < start || date > end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var viewExtents = timelineController.viewExtents;
|
||||
var size = viewExtents[1] - viewExtents[0];
|
||||
|
||||
var targetStart = math.max(start.getTime(), date - (size / 2));
|
||||
targetStart = Math.min(targetStart, end.getTime() - size);
|
||||
var targetEnd = begin + extentSize;
|
||||
|
||||
brush.extent([targetStart, targetEnd]);
|
||||
chart.select('.brush').call(brush);
|
||||
updateBrush();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
var shiftViewport = function(item) {
|
||||
// shift the viewport left/right to fit an item
|
||||
// unlike centerViewport() this only moves the view extents far enough to
|
||||
// make an item fit entirely in the view, but will not center it
|
||||
// if the item is already fully contained in the view, this does nothing
|
||||
var timeExtents = timelineController.timeExtents;
|
||||
var start = timeExtents[0];
|
||||
var end = timeExtents[1];
|
||||
|
||||
var viewExtents = timelineController.viewExtents;
|
||||
var viewStart = viewExtents[0];
|
||||
var viewEnd = viewExtents[1];
|
||||
if (item.startDate >= viewStart && item.endDate <= viewEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var size = viewEnd - viewStart;
|
||||
var currentMid = viewStart.getTime() + (size / 2);
|
||||
var targetMid = item.startDate.getTime() + (item.endDate - item.startDate) / 2;
|
||||
|
||||
var targetStart, targetEnd;
|
||||
if (targetMid > currentMid) {
|
||||
// move right - anchor item end to view right
|
||||
targetEnd = item.endDate.getTime();
|
||||
targetStart = Math.max(start.getTime(), targetEnd - size);
|
||||
} else if (targetMid < currentMid) {
|
||||
// move left - anchor item start to view left
|
||||
targetStart = item.startDate.getTime();
|
||||
targetEnd = Math.min(end.getTime(), targetStart + size);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
brush.extent([targetStart, targetEnd]);
|
||||
chart.select('.brush').call(brush);
|
||||
updateBrush();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
scope.$on('dataLoaded', function(event, data) {
|
||||
laneHeight = height / (data.length + 1);
|
||||
|
||||
var timeExtents = timelineController.timeExtents;
|
||||
var start = timeExtents[0];
|
||||
var end = timeExtents[1];
|
||||
var reducedEnd = new Date(start.getTime() + (end - start) / 8);
|
||||
|
||||
y.domain([0, data.length]).range([0, height]);
|
||||
|
||||
brush = d3.svg.brush()
|
||||
.x(timelineController.axes.x)
|
||||
.extent([start, reducedEnd])
|
||||
.on('brush', updateBrush);
|
||||
|
||||
var brushElement = chart.append('g')
|
||||
.attr('class', 'brush')
|
||||
.call(brush)
|
||||
.selectAll('rect')
|
||||
.attr('y', 1)
|
||||
.attr('fill', 'dodgerblue')
|
||||
.attr('fill-opacity', 0.365)
|
||||
.attr('height', height - 1);
|
||||
|
||||
timelineController.setViewExtents(brush.extent());
|
||||
});
|
||||
|
||||
scope.$on('update', function() {
|
||||
chart.style('width', timelineController.width);
|
||||
updateItems(timelineController.data);
|
||||
});
|
||||
|
||||
scope.$on('updateView', function() {
|
||||
updateItems(timelineController.data);
|
||||
});
|
||||
|
||||
scope.$on('select', function(event, selection) {
|
||||
if (selection) {
|
||||
shiftViewport(selection.item);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
require: '^timeline',
|
||||
scope: true,
|
||||
link: link
|
||||
};
|
||||
}
|
||||
|
||||
directivesModule.directive('timelineOverview', timelineOverview);
|
298
app/js/directives/timeline-viewport.js
Normal file
298
app/js/directives/timeline-viewport.js
Normal file
@ -0,0 +1,298 @@
|
||||
'use strict';
|
||||
|
||||
var directivesModule = require('./_index.js');
|
||||
|
||||
var d3 = require('d3');
|
||||
|
||||
/**
|
||||
* @ngInject
|
||||
*/
|
||||
function timelineViewport($document) {
|
||||
var link = function(scope, el, attrs, timelineController) {
|
||||
var margin = timelineController.margin;
|
||||
var height = 200;
|
||||
var loaded = false;
|
||||
|
||||
var y = d3.scale.linear();
|
||||
var xSelected = timelineController.axes.selection;
|
||||
|
||||
var statusColorMap = timelineController.statusColorMap;
|
||||
|
||||
var chart = d3.select(el[0])
|
||||
.append('svg')
|
||||
.attr('width', timelineController.width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom);
|
||||
|
||||
var defs = chart.append('defs')
|
||||
.append('clipPath')
|
||||
.attr('id', 'clip')
|
||||
.append('rect')
|
||||
.attr('width', timelineController.width);
|
||||
|
||||
var main = chart.append('g')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
|
||||
.attr('width', timelineController.width);
|
||||
|
||||
var laneLines = main.append('g');
|
||||
var laneLabels = main.append('g');
|
||||
|
||||
var itemGroups = main.append('g');
|
||||
|
||||
var cursorGroup = main.append('g')
|
||||
.style('opacity', 0)
|
||||
.style('pointer-events', 'none');
|
||||
|
||||
var cursor = cursorGroup.append('line')
|
||||
.attr('x1', 0)
|
||||
.attr('x2', 0)
|
||||
.attr('stroke', 'blue');
|
||||
|
||||
var cursorText = cursorGroup.append('text')
|
||||
.attr('x', 0)
|
||||
.attr('y', -10)
|
||||
.attr('dy', '-.5ex')
|
||||
.style('text-anchor', 'middle')
|
||||
.style('font', '9px sans-serif');
|
||||
|
||||
var selectedRect = null;
|
||||
|
||||
var color = function(rect, color) {
|
||||
if (!rect.attr('data-old-fill')) {
|
||||
rect.attr('data-old-fill', rect.attr('fill'));
|
||||
}
|
||||
|
||||
rect.attr('fill', color);
|
||||
};
|
||||
|
||||
var uncolor = function(rect) {
|
||||
if (!$document[0].contains(rect[0][0])) {
|
||||
// we load the original colored rect so we can't unset its color,
|
||||
// force a full reload
|
||||
updateItems(timelineController.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rect.attr('data-old-fill')) {
|
||||
rect.attr('fill', rect.attr('data-old-fill'));
|
||||
rect.attr('data-old-fill', null);
|
||||
}
|
||||
};
|
||||
|
||||
var rectMouseOver = function(d) {
|
||||
if (selectedRect !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
timelineController.setHover(d);
|
||||
scope.$apply();
|
||||
|
||||
color(d3.select(this), statusColorMap.hover);
|
||||
};
|
||||
|
||||
var rectMouseOut = function(d) {
|
||||
if (selectedRect !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
timelineController.clearHover();
|
||||
scope.$apply();
|
||||
|
||||
var self = d3.select(this);
|
||||
uncolor(d3.select(this));
|
||||
};
|
||||
|
||||
var rectClick = function(d) {
|
||||
timelineController.selectItem(d);
|
||||
scope.$apply();
|
||||
};
|
||||
|
||||
var updateLanes = function(data) {
|
||||
var lines = laneLines.selectAll('.laneLine')
|
||||
.data(data, function(d) { return d.key; });
|
||||
|
||||
lines.enter().append('line')
|
||||
.attr('x1', 0)
|
||||
.attr('x2', timelineController.width)
|
||||
.attr('stroke', 'lightgray')
|
||||
.attr('class', 'laneLine');
|
||||
|
||||
lines.attr('y1', function(d, i) { return y(i - 0.1); })
|
||||
.attr('y2', function(d, i) { return y(i - 0.1); });
|
||||
|
||||
lines.exit().remove();
|
||||
|
||||
var labels = laneLabels.selectAll('.laneLabel')
|
||||
.data(data, function(d) { return d.key; });
|
||||
|
||||
labels.enter().append('text')
|
||||
.text(function(d) { return 'Worker #' + d.key; })
|
||||
.attr('x', -margin.right)
|
||||
.attr('dy', '.5ex')
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('class', 'laneLabel');
|
||||
|
||||
labels.attr('y', function(d, i) { return y(i + 0.5); });
|
||||
labels.exit().remove();
|
||||
|
||||
cursor.attr('y2', y(data.length - 0.1));
|
||||
};
|
||||
|
||||
var updateItems = function(data) {
|
||||
var extent = timelineController.viewExtents;
|
||||
var minExtent = extent[0];
|
||||
var maxExtent = extent[1];
|
||||
|
||||
// filter visible items to include only those within the current extent
|
||||
// additionally prune extremely small values to improve performance
|
||||
var visibleItems = data.map(function(group) {
|
||||
return {
|
||||
key: group.key,
|
||||
values: group.values.filter(function(e) {
|
||||
if (xSelected(e.endDate) - xSelected(e.startDate) < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.startDate > maxExtent || e.endDate < minExtent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
var groups = itemGroups.selectAll("g")
|
||||
.data(visibleItems, function(d) { return d.key; });
|
||||
|
||||
groups.enter().append("g");
|
||||
|
||||
var rects = groups.selectAll("rect")
|
||||
.data(function(d) { return d.values; }, function(d) { return d.name; });
|
||||
|
||||
rects.enter().append("rect")
|
||||
.attr('y', function(d) { return y(d.worker); })
|
||||
.attr('height', 0.8 * y(1))
|
||||
.attr('stroke', 'rgba(100, 100, 100, 0.25)')
|
||||
.attr('clip-path', 'url(#clip)');
|
||||
|
||||
rects
|
||||
.attr('x', function(d) {
|
||||
return xSelected(d.startDate);
|
||||
})
|
||||
.attr('width', function(d) {
|
||||
return xSelected(d.endDate) - xSelected(d.startDate);
|
||||
})
|
||||
.attr('fill', function(d) {
|
||||
if (timelineController.selectionName === d.name) {
|
||||
return statusColorMap.selected;
|
||||
} else {
|
||||
return statusColorMap[d.status];
|
||||
}
|
||||
})
|
||||
.attr('data-old-fill', function(d) {
|
||||
if (timelineController.selectionName === d.name) {
|
||||
return statusColorMap[d.status];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.on("mouseover", rectMouseOver)
|
||||
.on('mouseout', rectMouseOut)
|
||||
.on('click', rectClick);
|
||||
|
||||
rects.exit().remove();
|
||||
groups.exit().remove();
|
||||
};
|
||||
|
||||
var update = function(data) {
|
||||
updateItems(timelineController.data);
|
||||
updateLanes(timelineController.data);
|
||||
};
|
||||
|
||||
var select = function(rect) {
|
||||
if (selectedRect) {
|
||||
uncolor(selectedRect);
|
||||
}
|
||||
|
||||
selectedRect = rect;
|
||||
|
||||
if (rect !== null) {
|
||||
color(rect, statusColorMap.selected);
|
||||
}
|
||||
};
|
||||
|
||||
chart.on('mouseout', function() {
|
||||
cursorGroup.style('opacity', 0);
|
||||
});
|
||||
|
||||
chart.on('mousemove', function() {
|
||||
var pos = d3.mouse(this);
|
||||
var px = pos[0];
|
||||
var py = pos[1];
|
||||
|
||||
if (px >= margin.left && px < (timelineController.width + margin.left) &&
|
||||
py > margin.top && py < (height + margin.top)) {
|
||||
var relX = px - margin.left;
|
||||
var currentTime = new Date(xSelected.invert(relX));
|
||||
|
||||
cursorGroup
|
||||
.style('opacity', '0.5')
|
||||
.attr('transform', 'translate(' + relX + ', 0)');
|
||||
|
||||
cursorText.text(d3.time.format('%X')(currentTime));
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('dataLoaded', function(event, data) {
|
||||
y.domain([0, data.length]).range([0, height]);
|
||||
|
||||
defs.attr('height', height);
|
||||
main.attr('height', height);
|
||||
cursor.attr('y1', y(-0.1));
|
||||
|
||||
loaded = true;
|
||||
});
|
||||
|
||||
scope.$on('update', function() {
|
||||
if (!loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
chart.style('width', timelineController.width + margin.left + margin.right);
|
||||
defs.attr('width', timelineController.width);
|
||||
main.attr('width', timelineController.width);
|
||||
|
||||
update(timelineController.data);
|
||||
});
|
||||
|
||||
scope.$on('updateView', function() {
|
||||
if (!loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
update(timelineController.data);
|
||||
});
|
||||
|
||||
scope.$on('postSelect', function(event, selection) {
|
||||
if (selection) {
|
||||
// iterate over all rects to find match
|
||||
itemGroups.selectAll('rect').each(function(d) {
|
||||
if (d.name === selection.item.name) {
|
||||
select(d3.select(this));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
select(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
require: '^timeline',
|
||||
scope: true,
|
||||
link: link
|
||||
};
|
||||
}
|
||||
|
||||
directivesModule.directive('timelineViewport', timelineViewport);
|
@ -7,485 +7,140 @@ var parseDstat = require('../util/dstat-parse');
|
||||
var d3 = require('d3');
|
||||
|
||||
var statusColorMap = {
|
||||
"success": "LightGreen",
|
||||
"fail": "Crimson",
|
||||
"skip": "DodgerBlue"
|
||||
'success': 'LightGreen',
|
||||
'fail': 'Crimson',
|
||||
'skip': 'DodgerBlue',
|
||||
'selected': 'GoldenRod',
|
||||
'hover': 'DarkTurquoise'
|
||||
};
|
||||
|
||||
var parseWorker = function(tags) {
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
if (!tags[i].startsWith("worker")) {
|
||||
if (!tags[i].startsWith('worker')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return parseInt(tags[i].split("-")[1]);
|
||||
return parseInt(tags[i].split('-')[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
var getDstatLanes = function(data, mins, maxes) {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var row = data[0];
|
||||
var lanes = [];
|
||||
|
||||
if ('total_cpu_usage_usr' in row && 'total_cpu_usage_sys' in row) {
|
||||
lanes.push([{
|
||||
scale: d3.scale.linear().domain([0, 100]),
|
||||
value: function(d) {
|
||||
return d.total_cpu_usage_wai;
|
||||
},
|
||||
color: "rgba(224, 188, 188, 1)",
|
||||
text: "CPU wait"
|
||||
}, {
|
||||
scale: d3.scale.linear().domain([0, 100]),
|
||||
value: function(d) {
|
||||
return d.total_cpu_usage_usr + d.total_cpu_usage_sys;
|
||||
},
|
||||
color: "rgba(102, 140, 178, 0.75)",
|
||||
text: "CPU (user+sys)"
|
||||
}]);
|
||||
}
|
||||
|
||||
if ('memory_usage_used' in row) {
|
||||
lanes.push([{
|
||||
scale: d3.scale.linear().domain([0, maxes.memory_usage_used]),
|
||||
value: function(d) { return d.memory_usage_used; },
|
||||
color: "rgba(102, 140, 178, 0.75)",
|
||||
text: "Memory"
|
||||
}]);
|
||||
}
|
||||
|
||||
if ('net_total_recv' in row && 'net_total_send' in row) {
|
||||
lanes.push([{
|
||||
scale: d3.scale.linear().domain([0, maxes.net_total_recv]),
|
||||
value: function(d) { return d.net_total_recv; },
|
||||
color: "rgba(224, 188, 188, 1)",
|
||||
text: "Net Down"
|
||||
}, {
|
||||
scale: d3.scale.linear().domain([0, maxes.net_total_send]),
|
||||
value: function(d) { return d.net_total_send; },
|
||||
color: "rgba(102, 140, 178, 0.75)",
|
||||
text: "Net Up",
|
||||
type: "line"
|
||||
}]);
|
||||
}
|
||||
|
||||
if ('dsk_total_read' in row && 'dsk_total_writ' in row) {
|
||||
lanes.push([{
|
||||
scale: d3.scale.linear().domain([0, maxes.dsk_total_read]),
|
||||
value: function(d) { return d.dsk_total_read; },
|
||||
color: "rgba(224, 188, 188, 1)",
|
||||
text: "Disk Read",
|
||||
type: "line"
|
||||
}, {
|
||||
scale: d3.scale.linear().domain([0, maxes.dsk_total_writ]),
|
||||
value: function(d) { return d.dsk_total_writ; },
|
||||
color: "rgba(102, 140, 178, 0.75)",
|
||||
text: "Disk Write",
|
||||
type: "line"
|
||||
}]);
|
||||
}
|
||||
|
||||
return lanes;
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngInject
|
||||
*/
|
||||
function timeline($log, datasetService) {
|
||||
var link = function(scope, el, attrs) {
|
||||
var data = [];
|
||||
var dstat = {};
|
||||
var timeExtents = [];
|
||||
var controller = function($scope) {
|
||||
var self = this;
|
||||
self.statusColorMap = statusColorMap;
|
||||
|
||||
var margin = { top: 20, right: 10, bottom: 10, left: 80 };
|
||||
var width = el.parent()[0].clientWidth - margin.left - margin.right;
|
||||
var height = 550 - margin.top - margin.bottom;
|
||||
self.data = [];
|
||||
self.dataRaw = [];
|
||||
self.dstat = [];
|
||||
|
||||
var miniHeight = 0;
|
||||
var dstatHeight = 0;
|
||||
var mainHeight = 0;
|
||||
self.margin = { top: 20, right: 10, bottom: 10, left: 80 };
|
||||
self.width = 0;
|
||||
self.height = 550 - this.margin.top - this.margin.bottom;
|
||||
|
||||
// primary x axis, maps time -> screen x
|
||||
var x = d3.time.scale().range([0, width]);
|
||||
|
||||
// secondary x axis, maps time (in selected range) -> screen x
|
||||
var xSelected = d3.scale.linear().range([0, width]);
|
||||
|
||||
// y axis for lane positions within main
|
||||
var yMain = d3.scale.linear();
|
||||
|
||||
// y axis for dstat lane positions
|
||||
var yDstat = d3.scale.linear();
|
||||
|
||||
// y axis for lane positions within mini
|
||||
var yMini = d3.scale.linear();
|
||||
|
||||
var chart = d3.select(el[0])
|
||||
.append('svg')
|
||||
.attr('width', '100%')
|
||||
.attr('height', height + margin.top + margin.bottom);
|
||||
|
||||
var defs = chart.append('defs')
|
||||
.append('clipPath')
|
||||
.attr('id', 'clip')
|
||||
.append('rect')
|
||||
.attr('width', width);
|
||||
|
||||
var main = chart.append('g')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
|
||||
.attr('width', width);
|
||||
|
||||
var laneLines = main.append('g');
|
||||
var laneLabels = main.append('g');
|
||||
|
||||
var itemGroups = main.append('g');
|
||||
|
||||
var dstatLanes = [];
|
||||
var dstatGroup = chart.append('g').attr('width', width);
|
||||
|
||||
var mini = chart.append('g').attr('width', width);
|
||||
var miniGroups = mini.append('g');
|
||||
|
||||
// delay init of the brush until we know the extents, otherwise it won't
|
||||
// init properly
|
||||
var brush = null;
|
||||
|
||||
var selectedRect = null;
|
||||
|
||||
var cursorGroup = main.append('g')
|
||||
.style('opacity', 0)
|
||||
.style('pointer-events', 'none');
|
||||
|
||||
var cursor = cursorGroup.append('line')
|
||||
.attr('x1', 0)
|
||||
.attr('x2', 0)
|
||||
.attr('stroke', 'blue');
|
||||
|
||||
var cursorText = cursorGroup.append('text')
|
||||
.attr('x', 0)
|
||||
.attr('y', -10)
|
||||
.attr('dy', '-.5ex')
|
||||
.style('text-anchor', 'middle')
|
||||
.style('font', '9px sans-serif');
|
||||
|
||||
var updateLanes = function() {
|
||||
var lines = laneLines.selectAll('.laneLine')
|
||||
.data(data, function(d) { return d.key; });
|
||||
|
||||
lines.enter().append('line')
|
||||
.attr('x1', 0)
|
||||
.attr('x2', width)
|
||||
.attr('stroke', 'lightgray')
|
||||
.attr('class', 'laneLine');
|
||||
|
||||
lines.attr('y1', function(d, i) { return yMain(i - 0.1); })
|
||||
.attr('y2', function(d, i) { return yMain(i - 0.1); });
|
||||
|
||||
lines.exit().remove();
|
||||
|
||||
var labels = laneLabels.selectAll('.laneLabel')
|
||||
.data(data, function(d) { return d.key; });
|
||||
|
||||
labels.enter().append('text')
|
||||
.text(function(d) { return 'Worker #' + d.key; })
|
||||
.attr('x', -margin.right)
|
||||
.attr('dy', '.5ex')
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('class', 'laneLabel');
|
||||
|
||||
labels.attr('y', function(d, i) { return yMain(i + 0.5); });
|
||||
labels.exit().remove();
|
||||
|
||||
cursor.attr('y2', yMain(data.length - 0.1));
|
||||
self.timeExtents = [0, 0];
|
||||
self.viewExtents = [0, 0];
|
||||
self.axes = {
|
||||
x: d3.time.scale(),
|
||||
selection: d3.scale.linear()
|
||||
};
|
||||
|
||||
var updateItems = function() {
|
||||
var minExtent = brush.extent()[0];
|
||||
var maxExtent = brush.extent()[1];
|
||||
self.selectionName = null;
|
||||
self.selection = null;
|
||||
self.hover = null;
|
||||
|
||||
// filter visible items to include only those within the current extent
|
||||
// additionally prune extremely small values to improve performance
|
||||
var visibleItems = data.map(function(group) {
|
||||
return {
|
||||
key: group.key,
|
||||
values: group.values.filter(function(e) {
|
||||
if (xSelected(e.endDate) - xSelected(e.startDate) < 2) {
|
||||
return false;
|
||||
}
|
||||
self.setViewExtents = function(extents) {
|
||||
if (angular.isNumber(extents[0])) {
|
||||
extents[0] = new Date(extents[0]);
|
||||
}
|
||||
|
||||
if (e.startDate > maxExtent || e.endDate < minExtent) {
|
||||
return false;
|
||||
}
|
||||
if (angular.isNumber(extents[1])) {
|
||||
extents[1] = new Date(extents[1]);
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
self.viewExtents = extents;
|
||||
self.axes.selection.domain(extents);
|
||||
|
||||
$scope.$broadcast('updateView');
|
||||
};
|
||||
|
||||
self.setHover = function(item) {
|
||||
self.hover = item;
|
||||
$scope.hoveredItem = item;
|
||||
};
|
||||
|
||||
self.clearHover = function() {
|
||||
self.hover = null;
|
||||
$scope.hoveredItem = null;
|
||||
};
|
||||
|
||||
self.setSelection = function(index, item) {
|
||||
if (self.selection && self.selection.item.name === item.name) {
|
||||
self.selectionName = null;
|
||||
self.selection = null;
|
||||
$scope.selectedItem = null;
|
||||
} else {
|
||||
self.selectionName = item.name;
|
||||
self.selection = {
|
||||
item: item,
|
||||
index: index
|
||||
};
|
||||
});
|
||||
|
||||
var groups = itemGroups.selectAll("g")
|
||||
.data(visibleItems, function(d) { return d.key; });
|
||||
|
||||
groups.enter().append("g");
|
||||
|
||||
var rects = groups.selectAll("rect")
|
||||
.data(function(d) { return d.values; }, function(d) { return d.name; });
|
||||
|
||||
rects.enter().append("rect")
|
||||
.attr('y', function(d) { return yMain(parseWorker(d.tags)); })
|
||||
.attr('height', 0.8 * yMain(1))
|
||||
.attr('stroke', 'rgba(100, 100, 100, 0.25)')
|
||||
.attr('clip-path', 'url(#clip)');
|
||||
|
||||
rects
|
||||
.attr('x', function(d) {
|
||||
return xSelected(d.startDate);
|
||||
})
|
||||
.attr('width', function(d) {
|
||||
return xSelected(d.endDate) - xSelected(d.startDate);
|
||||
})
|
||||
.attr('fill', function(d) { return statusColorMap[d.status]; })
|
||||
.on("mouseover", function(d) {
|
||||
if (selectedRect !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
scope.hoveredItem = d;
|
||||
scope.$apply();
|
||||
|
||||
var self = d3.select(this);
|
||||
if (!self.attr('data-old-fill')) {
|
||||
self.attr('data-old-fill', self.attr('fill'));
|
||||
}
|
||||
|
||||
self.attr('fill', 'darkturquoise');
|
||||
})
|
||||
.on('mouseout', function(d) {
|
||||
if (selectedRect !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
scope.hoveredItem = null;
|
||||
scope.$apply();
|
||||
|
||||
var self = d3.select(this);
|
||||
if (self.attr('data-old-fill')) {
|
||||
self.attr('fill', self.attr('data-old-fill'));
|
||||
self.attr('data-old-fill', null);
|
||||
}
|
||||
})
|
||||
.on('click', function(d) {
|
||||
var self = d3.select(this);
|
||||
selectItem(self, d);
|
||||
scope.$apply();
|
||||
});
|
||||
|
||||
rects.exit().remove();
|
||||
groups.exit().remove();
|
||||
};
|
||||
|
||||
var updateDstat = function() {
|
||||
if (dstatLanes.length === 0) {
|
||||
return;
|
||||
$scope.selectedItem = item;
|
||||
}
|
||||
|
||||
var minExtent = brush.extent()[0];
|
||||
var maxExtent = brush.extent()[1];
|
||||
|
||||
var timeFunc = function(d) { return d.system_time; };
|
||||
|
||||
var visibleEntries = dstat.entries.slice(
|
||||
arrayUtil.binaryMinIndex(minExtent, dstat.entries, timeFunc),
|
||||
arrayUtil.binaryMaxIndex(maxExtent, dstat.entries, timeFunc)
|
||||
);
|
||||
|
||||
// apply the current dataset (visibleEntries) to each dstat path
|
||||
dstatLanes.forEach(function(lane) {
|
||||
lane.forEach(function(pathDef) {
|
||||
pathDef.path
|
||||
.datum(visibleEntries)
|
||||
.attr("d", pathDef.area);
|
||||
});
|
||||
});
|
||||
// selection in the viewport depends on the overview setting the view
|
||||
// extents & makings sure there is a visible rect to select
|
||||
// the postSelect event makes sure that this is handled in the correct
|
||||
// sequence
|
||||
$scope.$broadcast('select', self.selection);
|
||||
$scope.$broadcast('postSelect', self.selection);
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
self.selectItem = function(item) {
|
||||
var workerItems = self.data[item.worker].values;
|
||||
var index = -1;
|
||||
|
||||
xSelected.domain(brush.extent());
|
||||
|
||||
updateLanes();
|
||||
updateItems();
|
||||
updateDstat();
|
||||
};
|
||||
|
||||
var updateMiniItems = function() {
|
||||
var groups = miniGroups.selectAll("g")
|
||||
.data(data, function(d) { return d.key; });
|
||||
|
||||
groups.enter().append("g");
|
||||
|
||||
var rects = groups.selectAll("rect").data(
|
||||
function(d) { return d.values; },
|
||||
function(d) { return d.name; });
|
||||
|
||||
rects.enter().append("rect")
|
||||
.attr("y", function(d) { return yMini(parseWorker(d.tags) + 0.5) - 5; })
|
||||
.attr("height", 10);
|
||||
|
||||
rects.attr("x", function(d) { return x(d.startDate); })
|
||||
.attr("width", function(d) { return x(d.endDate) - x(d.startDate); })
|
||||
.attr("stroke", 'rgba(100, 100, 100, 0.25)')
|
||||
.attr("fill", function(d) { return statusColorMap[d.status]; });
|
||||
|
||||
rects.exit().remove();
|
||||
groups.exit().remove();
|
||||
};
|
||||
|
||||
var selectItem = function(element, datum) {
|
||||
if (selectedRect) {
|
||||
if (selectedRect.attr('data-old-fill')) {
|
||||
selectedRect.attr('fill', selectedRect.attr('data-old-fill'));
|
||||
selectedRect.attr('data-old-fill', null);
|
||||
workerItems.forEach(function(d, i) {
|
||||
if (d.name === item.name) {
|
||||
index = i;
|
||||
}
|
||||
|
||||
if (scope.selectedItem.name === datum.name) {
|
||||
scope.selectedItem = null;
|
||||
selectedRect = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
scope.selectedItem = datum;
|
||||
selectedRect = element;
|
||||
|
||||
if (!element.attr('data-old-fill')) {
|
||||
element.attr('data-old-fill', element.attr('fill'));
|
||||
}
|
||||
|
||||
element.attr('fill', 'goldenrod');
|
||||
};
|
||||
|
||||
var initChart = function() {
|
||||
// determine lanes available based on current data
|
||||
dstatLanes = getDstatLanes(dstat.entries, dstat.minimums, dstat.maximums);
|
||||
|
||||
// determine region sizes that depend on available datasets
|
||||
miniHeight = data.length * 12 + 30;
|
||||
dstatHeight = dstatLanes.length * 30 + 30;
|
||||
mainHeight = height - miniHeight - dstatHeight - 10;
|
||||
|
||||
// update scales based on data and calculated heights
|
||||
x.domain(timeExtents);
|
||||
yMain.domain([0, data.length]).range([0, mainHeight]);
|
||||
yDstat.domain([0, dstatLanes.length]).range([0, dstatHeight]);
|
||||
yMini.domain([0, data.length]).range([0, miniHeight]);
|
||||
|
||||
// apply calculated heights to group sizes and transforms
|
||||
defs.attr('height', mainHeight);
|
||||
main.attr('height', mainHeight);
|
||||
cursor.attr('y1', yMain(-0.1));
|
||||
|
||||
var dstatOffset = margin.top + mainHeight;
|
||||
dstatGroup
|
||||
.attr('height', dstatHeight)
|
||||
.attr('transform', 'translate(' + margin.left + ',' + dstatOffset + ')');
|
||||
|
||||
var miniOffset = margin.top + mainHeight + dstatHeight;
|
||||
mini.attr('height', mainHeight)
|
||||
.attr('transform', 'translate(' + margin.left + ',' + miniOffset + ')');
|
||||
|
||||
// set initial selection extents to 1/8 the total size
|
||||
// this helps with performance issues in some browsers when displaying
|
||||
// large datasets (e.g. recent Firefox on Linux)
|
||||
var start = timeExtents[0];
|
||||
var end = timeExtents[1];
|
||||
var reducedEnd = new Date(start.getTime() + (end - start) / 8);
|
||||
|
||||
brush = d3.svg.brush()
|
||||
.x(x)
|
||||
.extent([start, reducedEnd])
|
||||
.on('brush', update);
|
||||
|
||||
var brushElement = mini.append('g')
|
||||
.attr('class', 'brush')
|
||||
.call(brush)
|
||||
.selectAll('rect')
|
||||
.attr('y', 1)
|
||||
.attr('fill', 'dodgerblue')
|
||||
.attr('fill-opacity', 0.365);
|
||||
|
||||
brushElement.attr('height', miniHeight - 1);
|
||||
|
||||
// init dstat lanes
|
||||
dstatLanes.forEach(function(lane, i) {
|
||||
var laneGroup = dstatGroup.append('g');
|
||||
|
||||
var text = laneGroup.append('text')
|
||||
.attr('y', yDstat(i + 0.5))
|
||||
.attr('dy', '0.5ex')
|
||||
.attr('text-anchor', 'end')
|
||||
.style('font', '10px sans-serif');
|
||||
|
||||
var dy = 0;
|
||||
|
||||
lane.forEach(function(pathDef) {
|
||||
var laneHeight = 0.8 * yDstat(1);
|
||||
pathDef.scale.range([laneHeight, 0]);
|
||||
|
||||
if ('text' in pathDef) {
|
||||
text.append('tspan')
|
||||
.attr('x', -margin.right)
|
||||
.attr('dy', dy)
|
||||
.text(pathDef.text)
|
||||
.attr('fill', pathDef.color);
|
||||
|
||||
dy += 10;
|
||||
}
|
||||
|
||||
pathDef.path = laneGroup.append('path');
|
||||
if (pathDef.type === 'line') {
|
||||
pathDef.area = d3.svg.line()
|
||||
.x(function(d) { return xSelected(d.system_time); })
|
||||
.y(function(d) {
|
||||
return yDstat(i) + pathDef.scale(pathDef.value(d));
|
||||
});
|
||||
|
||||
pathDef.path
|
||||
.style('stroke', pathDef.color)
|
||||
.style('stroke-width', '1.5px')
|
||||
.style('fill', 'none');
|
||||
} else {
|
||||
pathDef.area = d3.svg.area()
|
||||
.x(function(d) { return xSelected(d.system_time); })
|
||||
.y0(yDstat(i) + laneHeight)
|
||||
.y1(function(d) {
|
||||
return yDstat(i) + pathDef.scale(pathDef.value(d));
|
||||
});
|
||||
|
||||
pathDef.path.style('fill', pathDef.color);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// finalize chart init
|
||||
updateMiniItems();
|
||||
update();
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.setSelection(index, item);
|
||||
return true;
|
||||
};
|
||||
|
||||
var initData = function(raw, dstatRaw) {
|
||||
// find data extents
|
||||
self.selectIndex = function(worker, index) {
|
||||
var item = self.data[worker].values[index];
|
||||
|
||||
self.setSelection(index, item);
|
||||
return true;
|
||||
};
|
||||
|
||||
self.clearSelection = function() {
|
||||
self.selection = null;
|
||||
$scope.$broadcast('select', null);
|
||||
};
|
||||
|
||||
var initData = function(raw) {
|
||||
self.dataRaw = raw;
|
||||
|
||||
var minStart = null;
|
||||
var maxEnd = null;
|
||||
var preselect = null;
|
||||
|
||||
// parse date strings and determine extents
|
||||
raw.forEach(function(d) {
|
||||
d.worker = parseWorker(d.tags);
|
||||
|
||||
d.startDate = new Date(d.timestamps[0]);
|
||||
if (minStart === null || d.startDate < minStart) {
|
||||
minStart = d.startDate;
|
||||
@ -496,127 +151,93 @@ function timeline($log, datasetService) {
|
||||
maxEnd = d.endDate;
|
||||
}
|
||||
|
||||
if (scope.preselect && d.name === scope.preselect) {
|
||||
if ($scope.preselect && d.name === $scope.preselect) {
|
||||
preselect = d;
|
||||
}
|
||||
});
|
||||
|
||||
// define a nested data structure with groups by worker, and fill using
|
||||
// entries w/ duration > 0
|
||||
data = d3.nest()
|
||||
.key(function(d) { return parseWorker(d.tags); })
|
||||
self.timeExtents = [ minStart, maxEnd ];
|
||||
|
||||
self.data = d3.nest()
|
||||
.key(function(d) { return d.worker; })
|
||||
.sortKeys(d3.ascending)
|
||||
.entries(raw.filter(function(d) { return d.duration > 0; }));
|
||||
|
||||
var accessor = function(d) { return d.system_time; };
|
||||
var minIndex = arrayUtil.binaryMinIndex(minStart, dstatRaw.entries, accessor);
|
||||
var maxIndex = arrayUtil.binaryMaxIndex(maxEnd, dstatRaw.entries, accessor);
|
||||
self.axes.x.domain(self.timeExtents);
|
||||
|
||||
dstat = {
|
||||
entries: dstatRaw.entries.slice(minIndex, maxIndex),
|
||||
minimums: dstatRaw.minimums,
|
||||
maximums: dstatRaw.maximums
|
||||
};
|
||||
timeExtents = [ minStart, maxEnd ];
|
||||
|
||||
initChart();
|
||||
$scope.$broadcast('dataLoaded', self.data);
|
||||
|
||||
if (preselect) {
|
||||
// determine desired viewport and item sizes to center view on
|
||||
var extentSize = (maxEnd - minStart) / 8;
|
||||
var itemLength = (preselect.endDate - preselect.startDate);
|
||||
var itemMid = preselect.startDate.getTime() + (itemLength / 2);
|
||||
|
||||
// find real begin, end values, but don't exceed min/max time extents
|
||||
var begin = Math.max(minStart.getTime(), itemMid - (extentSize / 2));
|
||||
begin = Math.min(begin, maxEnd.getTime() - extentSize);
|
||||
var end = begin + extentSize;
|
||||
|
||||
// update the brush extent and redraw
|
||||
brush.extent([ begin, end ]);
|
||||
mini.select('.brush').call(brush);
|
||||
|
||||
// update items to reflect the new viewport bounds
|
||||
update();
|
||||
|
||||
// find + select the actual dom element
|
||||
itemGroups.selectAll('rect').each(function(d) {
|
||||
if (d.name === preselect.name) {
|
||||
selectItem(d3.select(this), d);
|
||||
}
|
||||
});
|
||||
self.selectItem(preselect);
|
||||
}
|
||||
};
|
||||
|
||||
chart.on('mouseout', function() {
|
||||
cursorGroup.style('opacity', 0);
|
||||
});
|
||||
var initDstat = function(raw) {
|
||||
var min = self.timeExtents[0];
|
||||
var max = self.timeExtents[1];
|
||||
|
||||
chart.on('mousemove', function() {
|
||||
var pos = d3.mouse(this);
|
||||
var px = pos[0];
|
||||
var py = pos[1];
|
||||
var accessor = function(d) { return d.system_time; };
|
||||
var minIndex = arrayUtil.binaryMinIndex(min, raw.entries, accessor);
|
||||
var maxIndex = arrayUtil.binaryMaxIndex(max, raw.entries, accessor);
|
||||
|
||||
if (px >= margin.left && px < (width + margin.left) &&
|
||||
py > margin.top && py < (mainHeight + margin.top)) {
|
||||
var relX = px - margin.left;
|
||||
var currentTime = new Date(xSelected.invert(relX));
|
||||
self.dstat = {
|
||||
entries: raw.entries.slice(minIndex, maxIndex),
|
||||
minimums: raw.minimums,
|
||||
maximums: raw.maximums
|
||||
};
|
||||
|
||||
cursorGroup
|
||||
.style('opacity', '0.5')
|
||||
.attr('transform', 'translate(' + relX + ', 0)');
|
||||
$scope.$broadcast('dstatLoaded', self.dstat);
|
||||
};
|
||||
|
||||
cursorText.text(d3.time.format('%X')(currentTime));
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('windowResize', function() {
|
||||
var extent = brush.extent();
|
||||
|
||||
width = el.parent()[0].clientWidth - margin.left - margin.right;
|
||||
x.range([0, width]);
|
||||
xSelected.range([0, width]);
|
||||
|
||||
chart.attr('width', el.parent()[0].clientWidth);
|
||||
defs.attr('width', width);
|
||||
main.attr('width', width);
|
||||
mini.attr('width', width);
|
||||
|
||||
laneLines.selectAll('.laneLine').attr('x2', width);
|
||||
|
||||
brush.extent(extent);
|
||||
|
||||
updateMiniItems();
|
||||
update();
|
||||
});
|
||||
|
||||
scope.$watch('dataset', function(dataset) {
|
||||
$scope.$watch('dataset', function(dataset) {
|
||||
if (!dataset) {
|
||||
return;
|
||||
}
|
||||
|
||||
var raw = null;
|
||||
var dstat = null;
|
||||
|
||||
// load both datasets
|
||||
// load dataset details (raw log entries and dstat) sequentially
|
||||
// we need to determine the initial date from the subunit data to parse
|
||||
// dstat
|
||||
datasetService.raw(dataset).then(function(response) {
|
||||
raw = response.data;
|
||||
initData(response.data);
|
||||
|
||||
return datasetService.dstat(dataset);
|
||||
}).then(function(response) {
|
||||
var firstDate = new Date(raw[0].timestamps[0]);
|
||||
dstat = parseDstat(response.data, firstDate.getYear());
|
||||
}).finally(function() {
|
||||
// display as much as we were able to load
|
||||
// (dstat may not exist, but that's okay)
|
||||
initData(raw, dstat);
|
||||
var firstDate = new Date(self.dataRaw[0].timestamps[0]);
|
||||
|
||||
var raw = parseDstat(response.data, firstDate.getYear());
|
||||
initDstat(raw);
|
||||
|
||||
$scope.$broadcast('update');
|
||||
}).catch(function(ex) {
|
||||
$log.error(ex);
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch(function() { return self.width; }, function(width) {
|
||||
self.axes.x.range([0, width]);
|
||||
self.axes.selection.range([0, width]);
|
||||
|
||||
$scope.$broadcast('update');
|
||||
});
|
||||
};
|
||||
|
||||
var link = function(scope, el, attrs, ctrl) {
|
||||
var updateWidth = function() {
|
||||
ctrl.width = el.parent()[0].clientWidth -
|
||||
ctrl.margin.left -
|
||||
ctrl.margin.right;
|
||||
};
|
||||
|
||||
scope.$on('windowResize', updateWidth);
|
||||
updateWidth();
|
||||
};
|
||||
|
||||
return {
|
||||
controller: controller,
|
||||
controllerAs: 'timeline',
|
||||
restrict: 'EA',
|
||||
transclude: true,
|
||||
template: '<ng-transclude></ng-transclude>',
|
||||
scope: {
|
||||
'dataset': '=',
|
||||
'hoveredItem': '=',
|
||||
|
@ -26,7 +26,11 @@
|
||||
dataset="timeline.dataset"
|
||||
hovered-item="timeline.hoveredItem"
|
||||
selected-item="timeline.selectedItem"
|
||||
preselect="timeline.preselect"></timeline>
|
||||
preselect="timeline.preselect">
|
||||
<timeline-viewport></timeline-viewport>
|
||||
<timeline-dstat></timeline-dstat>
|
||||
<timeline-overview></timeline-overview>
|
||||
</timeline>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
|
Loading…
x
Reference in New Issue
Block a user