'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);