Merge "Refactor the timeline directive."
This commit is contained in:
commit
408168bd60
@ -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