
Firefox appears to behave differently than Chrome when computing CSS sizes for SVG elements, causing the timeline elements to have incorrect widths. This modifies layout behavior to use only attribute sizes instead of CSS sizes, which works around the issue. Change-Id: I6af2d9502ba7881c630197494382c0bc7eec8283
296 lines
7.7 KiB
JavaScript
296 lines
7.7 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 + ')');
|
|
|
|
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);
|
|
cursor.attr('y1', y(-0.1));
|
|
|
|
loaded = true;
|
|
});
|
|
|
|
scope.$on('update', function() {
|
|
if (!loaded) {
|
|
return;
|
|
}
|
|
|
|
chart.attr('width', timelineController.width + margin.left + margin.right);
|
|
defs.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);
|