
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
174 lines
5.0 KiB
JavaScript
174 lines
5.0 KiB
JavaScript
'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);
|