
This switches the codebase to use only specific d3 (version 4) modules rather than the entire d3 (v3) package, resulting in a decent shrink in build size (~75KB reduction). Change-Id: I9f6a5d039d6340cc28115337bdfb891d15af7057
687 lines
22 KiB
JavaScript
687 lines
22 KiB
JavaScript
'use strict';
|
|
|
|
var directivesModule = require('./_index.js');
|
|
var arrayUtil = require('../util/array-util');
|
|
|
|
var d3Ease = require('d3-ease');
|
|
var d3Interpolate = require('d3-interpolate');
|
|
var d3Scale = require('d3-scale');
|
|
var d3TimeFormat = require('d3-time-format');
|
|
|
|
/**
|
|
* @ngInject
|
|
*/
|
|
function timelineViewport($document, $window) {
|
|
var link = function(scope, el, attrs, timelineController) {
|
|
// local display variables
|
|
var margin = timelineController.margin;
|
|
var statusColorMap = timelineController.statusColorMap;
|
|
var height = 200;
|
|
var loaded = false;
|
|
|
|
// axes and timeline-global variables
|
|
var y = d3Scale.scaleLinear();
|
|
var absolute = timelineController.axes.absolute;
|
|
var xSelected = timelineController.axes.selection;
|
|
var cursorTimeFormat = d3TimeFormat.timeFormat('%X');
|
|
var tickFormat = timelineController.axes.x.tickFormat();
|
|
|
|
// animation variables
|
|
var currentViewExtents = null;
|
|
var viewInterpolator = null;
|
|
var easeOutCubic = d3Ease.easeCubicOut;
|
|
var easeStartTimestamp = null;
|
|
var easeDuration = 500;
|
|
|
|
// selection and hover variables
|
|
var mousePoint = null;
|
|
var selection = null;
|
|
var hover = null;
|
|
|
|
// canvases and layers
|
|
var lanes = timelineController.createCanvas();
|
|
var regions = [];
|
|
var cursor = timelineController.createCanvas();
|
|
var main = timelineController.createCanvas(null, null, false);
|
|
el.append(main.canvas);
|
|
|
|
/**
|
|
* Initializes rects from a list of parsed subunit log entries, setting
|
|
* initial sizes and positions based on the current view extents.
|
|
* @param {Array} data A list of parsed subunit log entries
|
|
*/
|
|
function createRects(data) {
|
|
var rects = [];
|
|
|
|
var h = 0.8 * y(1);
|
|
for (var i = 0; i < data.length; i++) {
|
|
var entry = data[i];
|
|
var start = absolute(+entry.startDate);
|
|
rects.push({
|
|
x: start,
|
|
y: y(entry.worker),
|
|
width: absolute(+entry.endDate) - start,
|
|
height: h,
|
|
entry: entry
|
|
});
|
|
}
|
|
|
|
return rects;
|
|
}
|
|
|
|
/**
|
|
* Generate the list of active regions or "chunks". These regions span a
|
|
* fixed area of the timeline's full "virtual" width, and contain only a
|
|
* small subset of data points that fall within the area. This function only
|
|
* initializes a list of regions, but does not actually attempt to draw
|
|
* anything. Drawing can be handled lazily and will only occur when a
|
|
* region's 'dirty' property is set. If a list of regions already exists,
|
|
* it will be thrown away and replaced with a new list; this should occur
|
|
* any time the full "virtual" timeline width changes (such as a extent
|
|
* resize), or if the view extents no longer fall within the generated list
|
|
* of regions.
|
|
*
|
|
* This function will limit the number of generated regions. If this is not
|
|
* sufficient to cover the entire area spanned by the timeline's virtual
|
|
* width, regions will be generated around the user's current viewport.
|
|
*
|
|
* Note that individual data points will exist within multiple regions if
|
|
* they span region borders. In this case, each containing region will have
|
|
* a unique rect instance pointing to the same data point.
|
|
*/
|
|
function createRegions() {
|
|
regions = [];
|
|
|
|
var fullWidth = absolute(timelineController.timeExtents[1]);
|
|
var chunkWidth = 500;
|
|
var chunks = Math.ceil(fullWidth / chunkWidth);
|
|
var offset = 0;
|
|
|
|
// avoid creating lots of chunks - cap and only generate around the
|
|
// current view
|
|
// if we scroll out of bounds of the chunks we *do* have, we can throw
|
|
// away our regions + purge regions in memory
|
|
if (chunks > 30) {
|
|
var startX = absolute(timelineController.viewExtents[0]);
|
|
var endX = absolute(timelineController.viewExtents[1]);
|
|
var midX = startX + (endX - startX) / 2;
|
|
|
|
chunks = 50;
|
|
offset = Math.max(0, midX - (chunkWidth * 15));
|
|
}
|
|
|
|
for (var i = 0; i < chunks; i++) {
|
|
// for each desired chunk, find the bounds and managed data points
|
|
// then, calculate positions for each data point
|
|
var w = Math.min(fullWidth - offset, chunkWidth);
|
|
var min = absolute.invert(offset);
|
|
var max = absolute.invert(offset + w);
|
|
var data = timelineController.dataInBounds(min, max);
|
|
var rects = createRects(data);
|
|
|
|
regions.push({
|
|
x: offset, width: w, min: min, max: max,
|
|
data: data, rects: rects,
|
|
c: null,
|
|
dirty: true,
|
|
index: regions.length
|
|
});
|
|
|
|
offset += w;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Marks all regions as dirty so they can be redrawn for the next frame.
|
|
*/
|
|
function markAllDirty() {
|
|
regions.forEach(function(region) {
|
|
region.dirty = true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Finds all regions falling within the given minimum and maximum absolute
|
|
* x coordinates.
|
|
* @param {number} minX the minimum x coordinate (exclusive)
|
|
* @param {number} maxX the maximum x coording (exclusive)
|
|
* @return {object[]} a list of matching regions
|
|
*/
|
|
function getContainedRegions(minX, maxX) {
|
|
return regions.filter(function(region) {
|
|
return (region.x + region.width) > minX && region.x < maxX;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all regions containing the given data point.
|
|
* @param {object} entry the datapoint
|
|
* @return {object[]} a list of regions containing this entry
|
|
*/
|
|
function getRegionsForEntry(entry) {
|
|
var min = absolute(entry.startDate);
|
|
var max = absolute(entry.endDate);
|
|
return getContainedRegions(min, max);
|
|
}
|
|
|
|
/**
|
|
* Get the rect corresponding to the given entry within a particular region.
|
|
* @param {object} region the region to search in
|
|
* @param {object} entry the entry to search for
|
|
* @return {object|null} the matching rect, if any
|
|
*/
|
|
function getRectForEntry(region, entry) {
|
|
return region.rects.find(function(r) {
|
|
return r.entry === entry;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Find the region managing the given canvas-local X coordinate. If the x
|
|
* value is outside of the actual canvas area where regions are rendered,
|
|
* this may return null. Rarely, null may also be returned if, while
|
|
* animating, the view moves to a position outside of capped view bounds
|
|
* (i.e. when the view extents are small); if this happens, it can be
|
|
* ignored and createRegions() will generate the necessary area when the
|
|
* animation finishes.
|
|
* @param {number} screenX the canvas-local x coordinate
|
|
* @return {object|null} the matching region or null
|
|
*/
|
|
function getRegionAt(screenX) {
|
|
if (screenX < margin.left || screenX > main.canvas.width - margin.right) {
|
|
return null;
|
|
}
|
|
var absX = absolute(currentViewExtents[0]) + (screenX - margin.left);
|
|
return regions.find(function(r) {
|
|
return absX >= r.x && absX <= (r.x + r.width);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the rect at the given canvas-local coordinates, if any exists. This
|
|
* function has the same limitations as getRegionAt() and will return null
|
|
* when the coordinates are out of bounds or rarely when animating, but also
|
|
* returns null when no rect exists at the given coords.
|
|
*
|
|
* Note that this will only search within the region containing the
|
|
* coordinates so it should be fairly performant, though it will only return
|
|
* one of possibly many matching rects.
|
|
* @param {number} screenX the canvas-local x coordinate
|
|
* @param {number} screenY the canvas-local y coordinate
|
|
* @return {object|null} the matching rect in the region or null
|
|
*/
|
|
function getRectAt(screenX, screenY) {
|
|
if (screenY < margin.top || screenY > main.canvas.height - margin.bottom) {
|
|
return null;
|
|
}
|
|
|
|
var region = getRegionAt(screenX);
|
|
if (!region) {
|
|
return null;
|
|
}
|
|
|
|
// find the absolute coords in rect-space
|
|
var absX = absolute(currentViewExtents[0]) + (screenX - margin.left);
|
|
var absY = screenY - margin.top;
|
|
|
|
for (var i = 0; i < region.rects.length; i++) {
|
|
var rect = region.rects[i];
|
|
|
|
if (absX >= rect.x && absX <= (rect.x + rect.width) &&
|
|
absY >= rect.y && absY <= (rect.y + rect.height)) {
|
|
// make sure the point is contained inside the rect
|
|
return rect;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Draw lane lines and their labels into the offscreen lanes canvas.
|
|
*/
|
|
function drawLanes() {
|
|
// make sure the canvas is the correct size and clear it
|
|
lanes.resize(timelineController.width + margin.left + margin.right);
|
|
lanes.ctx.clearRect(0, 0, lanes.canvas.width, lanes.canvas.height);
|
|
|
|
lanes.ctx.strokeStyle = 'lightgray';
|
|
lanes.ctx.textBaseline = 'middle';
|
|
lanes.ctx.font = '14px Arial';
|
|
|
|
// draw lanes for each worker
|
|
var laneHeight = y(1);
|
|
for (var worker = 0; worker < timelineController.data.length; worker++) {
|
|
var yPos = margin.top + y(worker - 0.1);
|
|
|
|
// draw horizontal lines between lanes
|
|
lanes.ctx.beginPath();
|
|
lanes.ctx.moveTo(margin.left, yPos);
|
|
lanes.ctx.lineTo(margin.left + timelineController.width, yPos);
|
|
lanes.ctx.stroke();
|
|
|
|
// draw labels middle-aligned to the left of each lane
|
|
lanes.ctx.fillText(
|
|
'Worker #' + worker,
|
|
5, yPos + (laneHeight / 2),
|
|
margin.left - 10);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw a single rect within a region. This may be called independently of
|
|
* drawRegion() to update only a single rect, if needed.
|
|
* @param {object} region the region to draw within
|
|
* @param {object} rect the rect to draw
|
|
* @param {boolean} [clear] if true, clear the rect first (default: false)
|
|
*/
|
|
function drawSingleRect(region, rect, clear) {
|
|
var ctx = region.c.ctx;
|
|
|
|
if (rect.entry === selection) {
|
|
ctx.fillStyle = statusColorMap.selected;
|
|
} else if (rect.entry === hover) {
|
|
ctx.fillStyle = statusColorMap.hover;
|
|
} else {
|
|
ctx.fillStyle = statusColorMap[rect.entry.status];
|
|
}
|
|
|
|
if (clear) {
|
|
ctx.clearRect(rect.x - region.x, rect.y, rect.width, rect.height);
|
|
}
|
|
|
|
var filter = timelineController.filterFunction;
|
|
if (!filter || filter(rect.entry)) {
|
|
ctx.globalAlpha = 1.0;
|
|
} else {
|
|
ctx.globalAlpha = 0.15;
|
|
}
|
|
|
|
ctx.fillRect(rect.x - region.x, rect.y, rect.width, rect.height);
|
|
ctx.strokeRect(rect.x - region.x, rect.y, rect.width, rect.height);
|
|
}
|
|
|
|
/**
|
|
* Redraw all matching rects among all regions that contain this entry.
|
|
* @param {object} entry the entry to redraw
|
|
*/
|
|
function drawAllForEntry(entry) {
|
|
getRegionsForEntry(entry).forEach(function(region) {
|
|
if (!region.c) {
|
|
return;
|
|
}
|
|
|
|
var r = getRectForEntry(region, entry);
|
|
if (r) {
|
|
drawSingleRect(region, r, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Draw the given region into its own canvas. The region will only be drawn
|
|
* if it is marked as dirty. If its canvas has not yet been created, it will
|
|
* be initialized automatically. Note that this does not actually draw
|
|
* anything to the screen (i.e. main canvas), as this result only populates
|
|
* each region's local offscreen image with content. drawAll() will actually
|
|
* draw to the screen (and implicitly call this function as well.
|
|
* @param {object} region the region to draw
|
|
*/
|
|
function drawRegion(region) {
|
|
if (!region.dirty) {
|
|
// only redraw if dirty
|
|
return;
|
|
}
|
|
|
|
if (!region.c) {
|
|
// create the actual image buffer lazily - don't waste memory if it will
|
|
// never be seen
|
|
region.c = timelineController.createCanvas(
|
|
region.width, height + margin.bottom);
|
|
}
|
|
|
|
var ctx = region.c.ctx;
|
|
ctx.clearRect(0, 0, region.width, height);
|
|
ctx.strokeStyle = 'rgb(175, 175, 175)';
|
|
ctx.lineWidth = 1;
|
|
|
|
for (var i = 0; i < region.rects.length; i++) {
|
|
var rect = region.rects[i];
|
|
drawSingleRect(region, rect);
|
|
}
|
|
|
|
// draw axis ticks + labels
|
|
// main axis line -- offset y by 0.5 to draw crisp lines
|
|
ctx.strokeStyle = 'lightgray';
|
|
ctx.fillStyle = '#888';
|
|
ctx.font = '9px sans-serif';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'top';
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, height + 0.5);
|
|
ctx.lineTo(region.width, height + 0.5);
|
|
ctx.stroke();
|
|
|
|
// make a scale for the position of this region, but shrink it slightly so
|
|
// no labels overlap region boundaries and get cut off
|
|
var tickScale = d3Scale.scaleTime().domain([
|
|
absolute.invert(region.x + 10),
|
|
absolute.invert(region.x + region.width - 10)
|
|
]);
|
|
|
|
// 1 tick per 125px
|
|
var ticks = tickScale.ticks(Math.floor(region.width / 125));
|
|
|
|
for (var tickIndex = 0; tickIndex < ticks.length; tickIndex++) {
|
|
var tick = ticks[tickIndex];
|
|
var tickX = Math.floor(absolute(tick) - region.x) + 0.5;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(tickX, height);
|
|
ctx.lineTo(tickX, height + 6);
|
|
ctx.stroke();
|
|
|
|
ctx.fillText(tickFormat(tick), tickX, height + 7);
|
|
}
|
|
|
|
ctx.strokeStyle = 'rgb(175, 175, 175)';
|
|
region.dirty = false;
|
|
}
|
|
|
|
function drawCursor() {
|
|
if (!mousePoint || !mousePoint.inBounds) {
|
|
return;
|
|
}
|
|
|
|
var r = main.ratio;
|
|
var ctx = main.ctx;
|
|
ctx.scale(main.ratio, main.ratio);
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'top';
|
|
ctx.fillStyle = 'dimgrey';
|
|
ctx.strokeStyle = 'blue';
|
|
|
|
// draw the cursor line
|
|
ctx.beginPath();
|
|
ctx.moveTo(mousePoint.x, margin.top);
|
|
ctx.lineTo(mousePoint.x, margin.top + height);
|
|
ctx.stroke();
|
|
|
|
// draw the time label
|
|
ctx.font = '9px sans-serif';
|
|
var date = new Date(xSelected.invert(mousePoint.x - margin.left));
|
|
ctx.fillText(cursorTimeFormat(date), mousePoint.x, 16);
|
|
|
|
// draw the hovered item info
|
|
if (hover) {
|
|
var leftEdge = margin.left;
|
|
var rightEdge = leftEdge + timelineController.width;
|
|
|
|
ctx.font = 'bold 12px sans-serif';
|
|
var name = hover.name.split('.').pop();
|
|
var tw = ctx.measureText(name).width;
|
|
|
|
var cx = mousePoint.x;
|
|
if (mousePoint.x + (tw / 2) > rightEdge) {
|
|
cx -= mousePoint.x - (rightEdge - tw / 2);
|
|
} else if (mousePoint.x - (tw / 2) < leftEdge) {
|
|
cx += (leftEdge + tw / 2) - mousePoint.x;
|
|
}
|
|
ctx.fillText(name, cx, 1);
|
|
}
|
|
|
|
// reset scale
|
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
}
|
|
|
|
/**
|
|
* Draw all layers and visible regions on the screen.
|
|
*/
|
|
function drawAll() {
|
|
// update size of main canvas
|
|
var w = timelineController.width + margin.left + margin.right;
|
|
var e = angular.element(main.canvas);
|
|
main.resize(w);
|
|
|
|
var s = function(v) {
|
|
return v * main.ratio;
|
|
};
|
|
|
|
main.ctx.clearRect(0, 0, main.canvas.width, main.canvas.height);
|
|
main.ctx.drawImage(lanes.canvas, 0, 0);
|
|
|
|
// draw all visible regions
|
|
var startX = absolute(currentViewExtents[0]);
|
|
var endX = absolute(currentViewExtents[1]);
|
|
var viewRegions = getContainedRegions(startX, endX);
|
|
|
|
var effectiveWidth = 0;
|
|
viewRegions.forEach(function(region) {
|
|
effectiveWidth += region.width;
|
|
});
|
|
|
|
if (effectiveWidth < timelineController.width) {
|
|
// we had to cap the region generation previously, but moved outside of
|
|
// the generated area, so regenerate regions around the current view
|
|
createRegions();
|
|
viewRegions = getContainedRegions(startX, endX);
|
|
}
|
|
|
|
viewRegions.forEach(function(region) {
|
|
drawRegion(region);
|
|
|
|
// calculate the cropping area and offsets needed to place the region
|
|
// in the main canvas
|
|
var sx1 = Math.max(0, startX - region.x);
|
|
var sx2 = Math.min(region.width, endX - region.x);
|
|
var sw = sx2 - sx1;
|
|
var dx = Math.max(0, startX - region.x);
|
|
if (Math.floor(sw) === 0) {
|
|
return;
|
|
}
|
|
|
|
main.ctx.drawImage(
|
|
region.c.canvas,
|
|
s(sx1), 0,
|
|
Math.floor(s(sw)), s(height + margin.bottom),
|
|
s(margin.left + region.x - startX + sx1), s(margin.top),
|
|
s(sw), s(height + margin.bottom));
|
|
});
|
|
|
|
drawCursor();
|
|
}
|
|
|
|
timelineController.animateCallbacks.push(function(timestamp) {
|
|
if (!loaded) {
|
|
return false;
|
|
}
|
|
|
|
if (viewInterpolator) {
|
|
// start the animation
|
|
var currentSize = currentViewExtents[1] - currentViewExtents[0];
|
|
var newSize = timelineController.viewExtents[1] - timelineController.viewExtents[0];
|
|
var diffSize = currentSize - newSize;
|
|
var diffTime = timestamp - easeStartTimestamp;
|
|
var pct = diffTime / easeDuration;
|
|
|
|
// interpolate the current view bounds according to the easing method
|
|
currentViewExtents = viewInterpolator(easeOutCubic(pct));
|
|
|
|
if (Math.abs(diffSize) > 1) {
|
|
// size has changed, recalculate regions
|
|
createRegions();
|
|
}
|
|
|
|
drawAll();
|
|
|
|
if (pct >= 1) {
|
|
// finished, clear the state vars
|
|
easeStartTimestamp = null;
|
|
viewInterpolator = null;
|
|
return false;
|
|
} else {
|
|
// request more frames until finished
|
|
return true;
|
|
}
|
|
} else {
|
|
// if there is no view interpolator function, just do a plain redraw
|
|
drawAll();
|
|
return false;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Gets the canvas-local mouse point for the given mouse event, accounting
|
|
* for all relevant offsets and margins. The returned object will include an
|
|
* additional `inBounds` property indicating whether or not the point falls
|
|
* within the bounds of the main canvas.
|
|
* @param {MouseEvent} evt the mouse event
|
|
* @return {object} a point object
|
|
*/
|
|
function getMousePoint(evt) {
|
|
var r = main.canvas.getBoundingClientRect();
|
|
var ret = {
|
|
xRaw: evt.clientX - r.left,
|
|
x: evt.clientX - r.left,
|
|
y: evt.clientY - r.top
|
|
};
|
|
|
|
ret.inBounds = ret.x > margin.left &&
|
|
ret.x < (margin.left + timelineController.width) &&
|
|
ret.y > margin.top && ret.y < (margin.top + height);
|
|
|
|
return ret;
|
|
}
|
|
|
|
main.canvas.addEventListener('mousedown', function(evt) {
|
|
evt.preventDefault();
|
|
|
|
mousePoint = getMousePoint(evt);
|
|
var rect = getRectAt(mousePoint.x, mousePoint.y);
|
|
if (rect) {
|
|
timelineController.selectItem(rect.entry);
|
|
scope.$apply();
|
|
}
|
|
});
|
|
|
|
main.canvas.addEventListener('mousemove', function(evt) {
|
|
mousePoint = getMousePoint(evt);
|
|
var rect = getRectAt(mousePoint.x, mousePoint.y);
|
|
var oldHover = hover;
|
|
if (rect && rect.entry !== hover) {
|
|
main.canvas.style.cursor = 'pointer';
|
|
hover = rect.entry;
|
|
|
|
drawAllForEntry(rect.entry);
|
|
if (oldHover) {
|
|
drawAllForEntry(oldHover);
|
|
}
|
|
} else if (!rect && hover) {
|
|
main.canvas.style.cursor = 'default';
|
|
hover = null;
|
|
drawAllForEntry(oldHover);
|
|
}
|
|
|
|
timelineController.animate();
|
|
});
|
|
|
|
main.canvas.addEventListener('mouseout', function(evt) {
|
|
mousePoint = null;
|
|
main.canvas.style.cursor = 'default';
|
|
timelineController.animate();
|
|
});
|
|
|
|
scope.$on('dataLoaded', function(event, data) {
|
|
y.domain([0, data.length]).range([0, height]);
|
|
createRegions();
|
|
drawLanes();
|
|
|
|
loaded = true;
|
|
});
|
|
|
|
scope.$on('update', function() {
|
|
if (!loaded) {
|
|
return;
|
|
}
|
|
|
|
createRegions();
|
|
drawLanes();
|
|
timelineController.animate();
|
|
});
|
|
|
|
scope.$on('updateViewSize', function() {
|
|
if (!loaded) {
|
|
return;
|
|
}
|
|
|
|
if (currentViewExtents) {
|
|
// if we know where the view is already, try to animate the transition
|
|
viewInterpolator = d3Interpolate.interpolateArray(
|
|
currentViewExtents,
|
|
timelineController.viewExtents);
|
|
easeStartTimestamp = performance.now();
|
|
} else {
|
|
// otherwise, move directly to the new location/size
|
|
currentViewExtents = timelineController.viewExtents;
|
|
createRegions();
|
|
}
|
|
|
|
timelineController.animate();
|
|
});
|
|
|
|
scope.$on('updateViewPosition', function() {
|
|
if (!loaded) {
|
|
return;
|
|
}
|
|
|
|
if (currentViewExtents) {
|
|
// if we know where the view is already, try to animate the transition
|
|
viewInterpolator = d3Interpolate.interpolateArray(
|
|
currentViewExtents,
|
|
timelineController.viewExtents);
|
|
easeStartTimestamp = performance.now();
|
|
} else {
|
|
// otherwise, move directly to the new location
|
|
currentViewExtents = timelineController.viewExtents;
|
|
}
|
|
|
|
timelineController.animate();
|
|
});
|
|
|
|
scope.$on('postSelect', function(event, newSelection) {
|
|
var old = selection;
|
|
if (newSelection) {
|
|
selection = newSelection.item;
|
|
} else {
|
|
selection = null;
|
|
}
|
|
|
|
if (old) {
|
|
drawAllForEntry(old);
|
|
}
|
|
|
|
if (selection) {
|
|
drawAllForEntry(selection);
|
|
}
|
|
|
|
timelineController.animate();
|
|
});
|
|
|
|
scope.$on('filter', function() {
|
|
if (loaded) {
|
|
markAllDirty();
|
|
timelineController.animate();
|
|
}
|
|
});
|
|
};
|
|
|
|
return {
|
|
restrict: 'E',
|
|
require: '^timeline',
|
|
scope: true,
|
|
link: link
|
|
};
|
|
}
|
|
|
|
directivesModule.directive('timelineViewport', timelineViewport);
|