687 lines
21 KiB
JavaScript
687 lines
21 KiB
JavaScript
// vim: ts=2 sw=2
|
|
(function () {
|
|
d3.timeline = function() {
|
|
var DISPLAY_TYPES = ["circle", "rect"];
|
|
|
|
var hover = function () {},
|
|
mouseover = function () {},
|
|
mouseout = function () {},
|
|
click = function () {},
|
|
scroll = function () {},
|
|
labelFunction = function(label) { return label; },
|
|
navigateLeft = function () {},
|
|
navigateRight = function () {},
|
|
orient = "bottom",
|
|
width = null,
|
|
height = null,
|
|
rowSeparatorsColor = null,
|
|
backgroundColor = null,
|
|
tickFormat = { format: d3.time.format("%I %p"),
|
|
tickTime: d3.time.hours,
|
|
tickInterval: 1,
|
|
tickSize: 6,
|
|
tickValues: null
|
|
},
|
|
colorCycle = d3.scale.category20(),
|
|
colorPropertyName = null,
|
|
display = "rect",
|
|
beginning = 0,
|
|
labelMargin = 0,
|
|
ending = 0,
|
|
margin = {left: 30, right:30, top: 30, bottom:30},
|
|
stacked = false,
|
|
rotateTicks = false,
|
|
timeIsRelative = false,
|
|
fullLengthBackgrounds = false,
|
|
itemHeight = 20,
|
|
itemMargin = 5,
|
|
navMargin = 60,
|
|
showTimeAxis = true,
|
|
showAxisTop = false,
|
|
showTodayLine = false,
|
|
timeAxisTick = false,
|
|
timeAxisTickFormat = {stroke: "stroke-dasharray", spacing: "4 10"},
|
|
showTodayFormat = {marginTop: 25, marginBottom: 0, width: 1, color: colorCycle},
|
|
showBorderLine = false,
|
|
showBorderFormat = {marginTop: 25, marginBottom: 0, width: 1, color: colorCycle},
|
|
showAxisHeaderBackground = false,
|
|
showAxisNav = false,
|
|
showAxisCalendarYear = false,
|
|
axisBgColor = "white",
|
|
chartData = {}
|
|
;
|
|
|
|
var appendTimeAxis = function(g, xAxis, yPosition) {
|
|
|
|
if(showAxisHeaderBackground){ appendAxisHeaderBackground(g, 0, 0); }
|
|
|
|
if(showAxisNav){ appendTimeAxisNav(g) };
|
|
|
|
var axis = g.append("g")
|
|
.attr("class", "axis")
|
|
.attr("transform", "translate(" + 0 + "," + yPosition + ")")
|
|
.call(xAxis);
|
|
};
|
|
|
|
var appendTimeAxisCalendarYear = function (nav) {
|
|
var calendarLabel = beginning.getFullYear();
|
|
|
|
if (beginning.getFullYear() != ending.getFullYear()) {
|
|
calendarLabel = beginning.getFullYear() + "-" + ending.getFullYear()
|
|
}
|
|
|
|
nav.append("text")
|
|
.attr("transform", "translate(" + 20 + ", 0)")
|
|
.attr("x", 0)
|
|
.attr("y", 14)
|
|
.attr("class", "calendarYear")
|
|
.text(calendarLabel)
|
|
;
|
|
};
|
|
var appendTimeAxisNav = function (g) {
|
|
var timelineBlocks = 6;
|
|
var leftNavMargin = (margin.left - navMargin);
|
|
var incrementValue = (width - margin.left)/timelineBlocks;
|
|
var rightNavMargin = (width - margin.right - incrementValue + navMargin);
|
|
|
|
var nav = g.append('g')
|
|
.attr("class", "axis")
|
|
.attr("transform", "translate(0, 20)")
|
|
;
|
|
|
|
if(showAxisCalendarYear) { appendTimeAxisCalendarYear(nav) };
|
|
|
|
nav.append("text")
|
|
.attr("transform", "translate(" + leftNavMargin + ", 0)")
|
|
.attr("x", 0)
|
|
.attr("y", 14)
|
|
.attr("class", "chevron")
|
|
.text("<")
|
|
.on("click", function () {
|
|
return navigateLeft(beginning, chartData);
|
|
})
|
|
;
|
|
|
|
nav.append("text")
|
|
.attr("transform", "translate(" + rightNavMargin + ", 0)")
|
|
.attr("x", 0)
|
|
.attr("y", 14)
|
|
.attr("class", "chevron")
|
|
.text(">")
|
|
.on("click", function () {
|
|
return navigateRight(ending, chartData);
|
|
})
|
|
;
|
|
};
|
|
|
|
var appendAxisHeaderBackground = function (g, xAxis, yAxis) {
|
|
g.insert("rect")
|
|
.attr("class", "row-green-bar")
|
|
.attr("x", xAxis)
|
|
.attr("width", width)
|
|
.attr("y", yAxis)
|
|
.attr("height", itemHeight)
|
|
.attr("fill", axisBgColor);
|
|
};
|
|
|
|
var appendTimeAxisTick = function(g, xAxis, maxStack) {
|
|
g.append("g")
|
|
.attr("class", "axis")
|
|
.attr("transform", "translate(" + 0 + "," + (margin.top + (itemHeight + itemMargin) * maxStack) + ")")
|
|
.attr(timeAxisTickFormat.stroke, timeAxisTickFormat.spacing)
|
|
.call(xAxis.tickFormat("").tickSize(-(margin.top + (itemHeight + itemMargin) * (maxStack - 1) + 3), 0, 0));
|
|
};
|
|
|
|
var appendBackgroundBar = function (yAxisMapping, index, g, data, datum) {
|
|
var greenbarYAxis = ((itemHeight + itemMargin) * yAxisMapping[index]) + margin.top;
|
|
g.selectAll("svg").data(data).enter()
|
|
.insert("rect")
|
|
.attr("class", "row-green-bar")
|
|
.attr("x", fullLengthBackgrounds ? 0 : margin.left)
|
|
.attr("width", fullLengthBackgrounds ? width : (width - margin.right - margin.left))
|
|
.attr("y", greenbarYAxis)
|
|
.attr("height", itemHeight)
|
|
.attr("fill", backgroundColor instanceof Function ? backgroundColor(datum, index) : backgroundColor)
|
|
;
|
|
};
|
|
|
|
var appendLabel = function (gParent, yAxisMapping, index, hasLabel, datum) {
|
|
var fullItemHeight = itemHeight + itemMargin;
|
|
var rowsDown = margin.top + (fullItemHeight/2) + fullItemHeight * (yAxisMapping[index] || 1);
|
|
|
|
gParent.append("text")
|
|
.attr("class", "timeline-label")
|
|
.attr("transform", "translate(" + labelMargin + "," + rowsDown + ")")
|
|
.text(hasLabel ? labelFunction(datum.label) : datum.id)
|
|
.on("click", function (d, i) { click(d, index, datum); });
|
|
};
|
|
|
|
function timeline (gParent) {
|
|
var g = gParent.append("g");
|
|
var gParentSize = gParent[0][0].getBoundingClientRect();
|
|
|
|
var gParentItem = d3.select(gParent[0][0]);
|
|
|
|
var yAxisMapping = {},
|
|
maxStack = 1,
|
|
minTime = 0,
|
|
maxTime = 0;
|
|
|
|
setWidth();
|
|
|
|
// check if the user wants relative time
|
|
// if so, substract the first timestamp from each subsequent timestamps
|
|
if(timeIsRelative){
|
|
g.each(function (d, i) {
|
|
d.forEach(function (datum, index) {
|
|
datum.times.forEach(function (time, j) {
|
|
if(index === 0 && j === 0){
|
|
originTime = time.starting_time; //Store the timestamp that will serve as origin
|
|
time.starting_time = 0; //Set the origin
|
|
time.ending_time = time.ending_time - originTime; //Store the relative time (millis)
|
|
}else{
|
|
time.starting_time = time.starting_time - originTime;
|
|
time.ending_time = time.ending_time - originTime;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// check how many stacks we're gonna need
|
|
// do this here so that we can draw the axis before the graph
|
|
if (stacked || ending === 0 || beginning === 0) {
|
|
g.each(function (d, i) {
|
|
d.forEach(function (datum, index) {
|
|
|
|
// create y mapping for stacked graph
|
|
if (stacked && Object.keys(yAxisMapping).indexOf(index) == -1) {
|
|
yAxisMapping[index] = maxStack;
|
|
maxStack++;
|
|
}
|
|
|
|
// figure out beginning and ending times if they are unspecified
|
|
datum.times.forEach(function (time, i) {
|
|
if(beginning === 0)
|
|
if (time.starting_time < minTime || (minTime === 0 && timeIsRelative === false))
|
|
minTime = time.starting_time;
|
|
if(ending === 0)
|
|
if (time.ending_time > maxTime)
|
|
maxTime = time.ending_time;
|
|
});
|
|
});
|
|
});
|
|
|
|
if (ending === 0) {
|
|
ending = maxTime;
|
|
}
|
|
if (beginning === 0) {
|
|
beginning = minTime;
|
|
}
|
|
}
|
|
|
|
var scaleFactor = (1/(ending - beginning)) * (width - margin.left - margin.right);
|
|
|
|
// draw the axis
|
|
var xScale = d3.time.scale()
|
|
.domain([beginning, ending])
|
|
.range([margin.left, width - margin.right]);
|
|
|
|
var xAxis = d3.svg.axis()
|
|
.scale(xScale)
|
|
.orient(orient)
|
|
.tickFormat(tickFormat.format)
|
|
.tickSize(tickFormat.tickSize);
|
|
|
|
if (tickFormat.tickValues != null) {
|
|
xAxis.tickValues(tickFormat.tickValues);
|
|
} else {
|
|
xAxis.ticks(tickFormat.numTicks || tickFormat.tickTime, tickFormat.tickInterval);
|
|
}
|
|
|
|
// draw the chart
|
|
g.each(function(d, i) {
|
|
chartData = d;
|
|
d.forEach( function(datum, index){
|
|
var data = datum.times;
|
|
var hasLabel = (typeof(datum.label) != "undefined");
|
|
|
|
// issue warning about using id per data set. Ids should be individual to data elements
|
|
if (typeof(datum.id) != "undefined") {
|
|
console.warn("d3Timeline Warning: Ids per dataset is deprecated in favor of a 'class' key. Ids are now per data element.");
|
|
}
|
|
|
|
if (backgroundColor) { appendBackgroundBar(yAxisMapping, index, g, data, datum); }
|
|
|
|
g.selectAll("svg").data(data).enter()
|
|
.append(function(d, i) {
|
|
return document.createElementNS(d3.ns.prefix.svg, "display" in d? d.display:display);
|
|
})
|
|
.attr("x", getXPos)
|
|
.attr("y", getStackPosition)
|
|
.attr("width", function (d, i) {
|
|
return (d.ending_time - d.starting_time) * scaleFactor;
|
|
})
|
|
.attr("cy", function(d, i) {
|
|
return getStackPosition(d, i) + itemHeight/2;
|
|
})
|
|
.attr("cx", getXPos)
|
|
.attr("r", itemHeight / 2)
|
|
.attr("height", itemHeight)
|
|
.style("fill", function(d, i){
|
|
var dColorPropName;
|
|
if (d.color) return d.color;
|
|
if( colorPropertyName ){
|
|
dColorPropName = d[colorPropertyName];
|
|
if ( dColorPropName ) {
|
|
return colorCycle( dColorPropName );
|
|
} else {
|
|
return colorCycle( datum[colorPropertyName] );
|
|
}
|
|
}
|
|
return colorCycle(index);
|
|
})
|
|
.on("mousemove", function (d, i) {
|
|
hover(d, index, datum);
|
|
})
|
|
.on("mouseover", function (d, i) {
|
|
mouseover(d, i, datum);
|
|
})
|
|
.on("mouseout", function (d, i) {
|
|
mouseout(d, i, datum);
|
|
})
|
|
.on("click", function (d, i) {
|
|
click(d, index, datum);
|
|
})
|
|
.attr("class", function (d, i) {
|
|
return datum.class ? "timelineSeries_"+datum.class : "timelineSeries_"+index;
|
|
})
|
|
.attr("id", function(d, i) {
|
|
// use deprecated id field
|
|
if (datum.id && !d.id) {
|
|
return 'timelineItem_'+datum.id;
|
|
}
|
|
|
|
return d.id ? d.id : "timelineItem_"+index+"_"+i;
|
|
})
|
|
;
|
|
|
|
g.selectAll("svg").data(data).enter()
|
|
.append("text")
|
|
.attr("x", getXTextPos)
|
|
.attr("y", getStackTextPosition)
|
|
.text(function(d) {
|
|
return d.label;
|
|
})
|
|
;
|
|
|
|
if (rowSeparatorsColor) {
|
|
var lineYAxis = ( itemHeight + itemMargin / 2 + margin.top + (itemHeight + itemMargin) * yAxisMapping[index]);
|
|
gParent.append("svg:line")
|
|
.attr("class", "row-separator")
|
|
.attr("x1", 0 + margin.left)
|
|
.attr("x2", width - margin.right)
|
|
.attr("y1", lineYAxis)
|
|
.attr("y2", lineYAxis)
|
|
.attr("stroke-width", 1)
|
|
.attr("stroke", rowSeparatorsColor);
|
|
}
|
|
|
|
// add the label
|
|
if (hasLabel) { appendLabel(gParent, yAxisMapping, index, hasLabel, datum); }
|
|
|
|
if (typeof(datum.icon) !== "undefined") {
|
|
gParent.append("image")
|
|
.attr("class", "timeline-label")
|
|
.attr("transform", "translate("+ 0 +","+ (margin.top + (itemHeight + itemMargin) * yAxisMapping[index])+")")
|
|
.attr("xlink:href", datum.icon)
|
|
.attr("width", margin.left)
|
|
.attr("height", itemHeight);
|
|
}
|
|
|
|
function getStackPosition(d, i) {
|
|
if (stacked) {
|
|
return margin.top + (itemHeight + itemMargin) * yAxisMapping[index];
|
|
}
|
|
return margin.top;
|
|
}
|
|
function getStackTextPosition(d, i) {
|
|
if (stacked) {
|
|
return margin.top + (itemHeight + itemMargin) * yAxisMapping[index] + itemHeight * 0.75;
|
|
}
|
|
return margin.top + itemHeight * 0.75;
|
|
}
|
|
});
|
|
});
|
|
|
|
var belowLastItem = (margin.top + (itemHeight + itemMargin) * maxStack);
|
|
var aboveFirstItem = margin.top;
|
|
var timeAxisYPosition = showAxisTop ? aboveFirstItem : belowLastItem;
|
|
if (showTimeAxis) { appendTimeAxis(g, xAxis, timeAxisYPosition); }
|
|
if (timeAxisTick) { appendTimeAxisTick(g, xAxis, maxStack); }
|
|
|
|
if (width > gParentSize.width) {
|
|
var move = function() {
|
|
var x = Math.min(0, Math.max(gParentSize.width - width, d3.event.translate[0]));
|
|
zoom.translate([x, 0]);
|
|
g.attr("transform", "translate(" + x + ",0)");
|
|
scroll(x*scaleFactor, xScale);
|
|
};
|
|
|
|
var zoom = d3.behavior.zoom().x(xScale).on("zoom", move);
|
|
|
|
gParent
|
|
.attr("class", "scrollable")
|
|
.call(zoom);
|
|
}
|
|
|
|
if (rotateTicks) {
|
|
g.selectAll(".tick text")
|
|
.attr("transform", function(d) {
|
|
return "rotate(" + rotateTicks + ")translate("
|
|
+ (this.getBBox().width / 2 + 10) + "," // TODO: change this 10
|
|
+ this.getBBox().height / 2 + ")";
|
|
});
|
|
}
|
|
|
|
var gSize = g[0][0].getBoundingClientRect();
|
|
setHeight();
|
|
|
|
if (showBorderLine) {
|
|
g.each(function (d, i) {
|
|
d.forEach(function (datum) {
|
|
var times = datum.times;
|
|
times.forEach(function (time) {
|
|
appendLine(xScale(time.starting_time), showBorderFormat);
|
|
appendLine(xScale(time.ending_time), showBorderFormat);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
if (showTodayLine) {
|
|
var todayLine = xScale(new Date());
|
|
appendLine(todayLine, showTodayFormat);
|
|
}
|
|
|
|
function getXPos(d, i) {
|
|
return margin.left + (d.starting_time - beginning) * scaleFactor;
|
|
}
|
|
|
|
function getXTextPos(d, i) {
|
|
return margin.left + (d.starting_time - beginning) * scaleFactor + 5;
|
|
}
|
|
|
|
function setHeight() {
|
|
if (!height && !gParentItem.attr("height")) {
|
|
if (itemHeight) {
|
|
// set height based off of item height
|
|
height = gSize.height + gSize.top - gParentSize.top;
|
|
// set bounding rectangle height
|
|
d3.select(gParent[0][0]).attr("height", height);
|
|
} else {
|
|
throw "height of the timeline is not set";
|
|
}
|
|
} else {
|
|
if (!height) {
|
|
height = gParentItem.attr("height");
|
|
} else {
|
|
gParentItem.attr("height", height);
|
|
}
|
|
}
|
|
}
|
|
|
|
function setWidth() {
|
|
if (!width && !gParentSize.width) {
|
|
try {
|
|
width = gParentItem.attr("width");
|
|
if (!width) {
|
|
throw "width of the timeline is not set. As of Firefox 27, timeline().with(x) needs to be explicitly set in order to render";
|
|
}
|
|
} catch (err) {
|
|
console.log( err );
|
|
}
|
|
} else if (!(width && gParentSize.width)) {
|
|
try {
|
|
width = gParentItem.attr("width");
|
|
} catch (err) {
|
|
console.log( err );
|
|
}
|
|
}
|
|
// if both are set, do nothing
|
|
}
|
|
|
|
function appendLine(lineScale, lineFormat) {
|
|
gParent.append("svg:line")
|
|
.attr("x1", lineScale)
|
|
.attr("y1", lineFormat.marginTop)
|
|
.attr("x2", lineScale)
|
|
.attr("y2", height - lineFormat.marginBottom)
|
|
.style("stroke", lineFormat.color)//"rgb(6,120,155)")
|
|
.style("stroke-width", lineFormat.width);
|
|
}
|
|
|
|
}
|
|
|
|
// SETTINGS
|
|
|
|
timeline.margin = function (p) {
|
|
if (!arguments.length) return margin;
|
|
margin = p;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.orient = function (orientation) {
|
|
if (!arguments.length) return orient;
|
|
orient = orientation;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.itemHeight = function (h) {
|
|
if (!arguments.length) return itemHeight;
|
|
itemHeight = h;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.itemMargin = function (h) {
|
|
if (!arguments.length) return itemMargin;
|
|
itemMargin = h;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.navMargin = function (h) {
|
|
if (!arguments.length) return navMargin;
|
|
navMargin = h;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.height = function (h) {
|
|
if (!arguments.length) return height;
|
|
height = h;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.width = function (w) {
|
|
if (!arguments.length) return width;
|
|
width = w;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.display = function (displayType) {
|
|
if (!arguments.length || (DISPLAY_TYPES.indexOf(displayType) == -1)) return display;
|
|
display = displayType;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.labelFormat = function(f) {
|
|
if (!arguments.length) return labelFunction;
|
|
labelFunction = f;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.tickFormat = function (format) {
|
|
if (!arguments.length) return tickFormat;
|
|
tickFormat = format;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.hover = function (hoverFunc) {
|
|
if (!arguments.length) return hover;
|
|
hover = hoverFunc;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.mouseover = function (mouseoverFunc) {
|
|
if (!arguments.length) return mouseover;
|
|
mouseover = mouseoverFunc;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.mouseout = function (mouseoutFunc) {
|
|
if (!arguments.length) return mouseout;
|
|
mouseout = mouseoutFunc;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.click = function (clickFunc) {
|
|
if (!arguments.length) return click;
|
|
click = clickFunc;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.scroll = function (scrollFunc) {
|
|
if (!arguments.length) return scroll;
|
|
scroll = scrollFunc;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.colors = function (colorFormat) {
|
|
if (!arguments.length) return colorCycle;
|
|
colorCycle = colorFormat;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.beginning = function (b) {
|
|
if (!arguments.length) return beginning;
|
|
beginning = b;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.ending = function (e) {
|
|
if (!arguments.length) return ending;
|
|
ending = e;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.labelMargin = function (m) {
|
|
if (!arguments.length) return labelMargin;
|
|
labelMargin = m;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.rotateTicks = function (degrees) {
|
|
if (!arguments.length) return rotateTicks;
|
|
rotateTicks = degrees;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.stack = function () {
|
|
stacked = !stacked;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.relativeTime = function() {
|
|
timeIsRelative = !timeIsRelative;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.showBorderLine = function () {
|
|
showBorderLine = !showBorderLine;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.showBorderFormat = function(borderFormat) {
|
|
if (!arguments.length) return showBorderFormat;
|
|
showBorderFormat = borderFormat;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.showToday = function () {
|
|
showTodayLine = !showTodayLine;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.showTodayFormat = function(todayFormat) {
|
|
if (!arguments.length) return showTodayFormat;
|
|
showTodayFormat = todayFormat;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.colorProperty = function(colorProp) {
|
|
if (!arguments.length) return colorPropertyName;
|
|
colorPropertyName = colorProp;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.rowSeparators = function (color) {
|
|
if (!arguments.length) return rowSeparatorsColor;
|
|
rowSeparatorsColor = color;
|
|
return timeline;
|
|
|
|
};
|
|
|
|
timeline.background = function (color) {
|
|
if (!arguments.length) return backgroundColor;
|
|
backgroundColor = color;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.showTimeAxis = function () {
|
|
showTimeAxis = !showTimeAxis;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.showAxisTop = function () {
|
|
showAxisTop = !showAxisTop;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.showAxisCalendarYear = function () {
|
|
showAxisCalendarYear = !showAxisCalendarYear;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.showTimeAxisTick = function () {
|
|
timeAxisTick = !timeAxisTick;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.fullLengthBackgrounds = function () {
|
|
fullLengthBackgrounds = !fullLengthBackgrounds;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.showTimeAxisTickFormat = function(format) {
|
|
if (!arguments.length) return timeAxisTickFormat;
|
|
timeAxisTickFormat = format;
|
|
return timeline;
|
|
};
|
|
|
|
timeline.showAxisHeaderBackground = function(bgColor) {
|
|
showAxisHeaderBackground = !showAxisHeaderBackground;
|
|
if(bgColor) { (axisBgColor = bgColor) };
|
|
return timeline;
|
|
};
|
|
|
|
timeline.navigate = function (navigateBackwards, navigateForwards) {
|
|
if (!arguments.length) return [navigateLeft, navigateRight];
|
|
navigateLeft = navigateBackwards;
|
|
navigateRight = navigateForwards;
|
|
showAxisNav = !showAxisNav;
|
|
return timeline;
|
|
};
|
|
|
|
return timeline;
|
|
};
|
|
})();
|