
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
299 lines
7.8 KiB
JavaScript
299 lines
7.8 KiB
JavaScript
'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);
|