/*
* niViz -- snow profiles visualization
* Copyright (C) 2015 WSL/SLF - Fluelastrasse 11 - 7260 Davos Dorf - Switzerland.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
(function (niviz, moment) {
'use strict';
// --- Module Dependencies ---
var defprops = Object.defineProperties;
var Graph = niviz.Graph;
var Config = niviz.Config;
var Common = niviz.Common;
var Feature = niviz.Feature;
var Gradient = niviz.Gradient;
var Axis = niviz.Axis;
var round = niviz.util.round;
var header = niviz.Header;
var hsgrid = niviz.Grid.hsgrid;
var t = niviz.Translate.gettext;
var extend = niviz.util.extend;
/** @module niviz */
/**
* Visualization of a time line of snow profiles.
*
* @class Timeline
* @constructor
* @extends Rangegraph
*
* @param {Station} data
* @param {Object} canvas A svg element that will be used as SnapSVG paper
* @param {Object} properties
*/
function Timeline (data, canvas, properties) {
Timeline.uber.call(this, data, canvas, properties);
extend(this.properties, Timeline.defaults.values);
this.setProperties(properties);
this.draw();
}
Graph.implement(Timeline, 'Timeline', 'timeline', 'Rangegraph');
/**
* A list of parameters that are available for display.
*
* @property parameters
* @type Array<String>
* @static
*/
Timeline.parameters = [
'temperature', 'grainshape', 'grainsize', 'density',
'wetness', 'sphericity', 'dendricity'
];
Timeline.defaults = new Config('Timeline', [
{ name: 'timeline', type: 'timelineparams',
values: [ 'bluemultired', 'whitemultiblue', 'bluered', 'whiteblue', 'whiteblack' ],
default: [
{ name: 'temperature', min: -20, max: 0, steps: 5,
palette: 'bluemultired', editable: false },
{ name: 'grainshape', color: 'own', gradient: 'grainshape',
hide: true, editable: false },
{ name: 'grainsize', min: 0, max: 4, steps: 5,
palette: 'whitemultiblue', editable: false },
{ name: 'density', min: 0, max: 600, steps: 4,
palette: 'whitemultiblue', editable: false },
{ name: 'wetness', min: 0, max: 4, steps: 5,
palette: 'whitemultiblue', editable: false },
{ name: 'sphericity', min: 0, max: 1, steps: 5, palette: 'bluered', editable: false },
{ name: 'dendricity', min: 0, max: 1, steps: 5, palette: 'whiteblue', editable: false }
]
}
]);
Timeline.defaults.load();
Timeline.get = function (name) {
var i, ii = Timeline.defaults.values.timeline.length;
for (i = 0; i < ii; ++i) {
if (Timeline.defaults.values.timeline[i].name === name)
return Timeline.defaults.values.timeline[i];
}
return null;
};
/**
* Remove a parameter from the timeline Config Timeline.defaults. This method is
* invoked by the settings modal when a user removes a parameter from the pro parser
* parameters section.
*
* @method remove
* @static
* @param {String} name Paramter name (e. g. ramm, density)
*/
Timeline.remove = function (name) {
var i, ii = Timeline.defaults.timeline.length - 1, current;
for (i = ii; i >= 0; --i) {
current = Timeline.defaults.timeline[i].name;
if (current === name || current === '') Timeline.defaults.timeline.splice(i, 1);
}
};
defprops(Timeline.prototype, {
/**
* Get all selectable parameters.
*
* @property parameters
* @type Array<Object>
*/
parameters: {
get: function () {
return Timeline.defaults.values.timeline.map(function (value) {
if (Feature.type[value.name])
return {lookup: value.name, name: t(Feature.type[value.name].name)};
});
}
}
});
/**
* Overwrite current properties with the ones passed as parameter. If a new
* parameter is set to display: emit the 'parameter' event and retrieve the
* gradient and color settings for the parameter.
*
* @method setProperties
* @param {Object} properties
*/
Timeline.prototype.setProperties = function (properties) {
if (this.properties.parameter !== properties.parameter)
this.data.emitter.emit('parameter', properties.parameter);
extend(this.properties, properties);
};
/**
* Setup the dimensions of the underlying Rangegraph and draw header,
* horizontal grid and legend.
*
* @method setup
* @private
* @chainable
*/
Timeline.prototype.setup = function () {
var p = this.properties, pconfig = Timeline.get(p.parameter);
if (!pconfig) { // Revert to temperature if selected parameter doesn't exist anymore
p.parameter = 'temperature';
pconfig = Timeline.get(p.parameter);
}
this.remove('hs', this.elements);
p.margin = 20;
p.unit = 5;
p.tl.x = p.tl.x + 1.5 * p.fontsize + 2 * p.margin;
p.br.x = p.br.x - 2 * p.fontsize;
p.length.x = p.br.x - p.tl.x;
this.gradient = new Gradient(pconfig);
return this.reconfigure().header().legend();
};
/**
* Reconfigure gridy, i.e. snow height axis, according to zoom area and draw it
*
* @method reconfigure
* @private
* @chainable
*/
Timeline.prototype.reconfigure = function () {
var p = this.properties;
this.hsgrid = hsgrid(this.data, null, null, Common.defaults.autoscale);
if (p.hsmin !== undefined || p.hsmax !== undefined) { // hsmin, hsmax may be set by the user
var max = (p.hsmax || p.hsmax === 0) ? p.hsmax : this.hsgrid.max,
min = (p.hsmin || p.hsmin === 0) ? p.hsmin : this.hsgrid.min;
this.hsgrid = niviz.Grid.hsfixed(min, max);
}
// Code with vertical zoom:
var r = Math.abs(this.hsgrid.min - this.hsgrid.max);
var max = this.hsgrid.max - r * this.yzoom.min / 100;
var min = this.hsgrid.max - r * this.yzoom.max / 100;
this.hsgrid = hsgrid({bottom: min, top: max}, null, null, true);
this.axisy = new Axis(min, max, p.br.y, p.tl.y);
// END code for vertical zoom
return this.gridy();
};
/**
* Draw the grain shape legend and hook it up with the mousemove event. Hovering
* on a grain shape color will display a popup with a description of the grain shape.
*
* @method gtlegend
* @private
*/
Timeline.prototype.gtlegend = function (set) {
var paper = this.paper, p = this.properties, gradient = this.gradient.gradient,
self = this, rect = paper.rect(p.tl.x - 2 * p.margin, p.tl.y, p.margin, p.length.y)
.attr({ fill: 'l(1,1,1,0)' + gradient.gradient, strokeWidth: 1, stroke: p.grid_color })
.transform('t0.5,0.5'), top, i, mfcr = paper.g(),
inset = p.tl.y + p.length.y - p.length.y / 10 / 2 - p.fontsize / 2 - 2;
var x, y = Math.ceil(p.tl.y + p.length.y/11 * 7);
for (i = 1; i < 4; ++i) {
x = Math.round(p.tl.x - p.margin - i * 5 + 1);
mfcr.add(paper.line(x, y, x, Math.ceil(y + p.length.y / 11))
.attr({stroke: p.grid_color, strokeWidth: 1.5, pointerEvents: 'none', strokeOpacity: 0.6}));
}
rect.mousemove(function (e) {
clearTimeout(self.legendtimer);
self.legendtimer = setTimeout(function () {
top = self.canvas.offset().top + p.tl.y;
var index = 10 - Math.min(Math.floor(Math.abs(e.pageY - top) / p.length.y * 11), 10);
if (self.popupindex === index) return;
self.popupindex = index;
if (self.popup) self.popup.remove();
self.popup = self.paper.g();
var lbl = paper.text(0, p.fontsize, t(gradient.lbls[index], 'grainshape'))
.attr(p.font).attr({ textAnchor: 'start' }),
bbox = lbl.getBBox();
var box = self.paper.rect(bbox.x - 5, bbox.y - 7, bbox.width + 10,
bbox.height + 14).attr({
stroke: p.grid_color,
fill: 'white',
fillOpacity: 0.7
});
self.popup.add(box);
self.popup.add(lbl);
self.popup[1].transform('t12,0');
self.popup[0].transform('t12,0');
self.popup.transform('t' + (p.tl.x - p.margin + 2) + ','
+ (inset - index * p.length.y / 11) + ' s0', lbl);
self.popup.animate({
transform: 't' + (p.tl.x - p.margin + 2) + ','
+ (inset - index * p.length.y / 11) + ' s1'
}, 100, mina.easein);
}, 10);
});
rect.mouseout(function () {
self.popupindex = null;
clearTimeout(self.legendtimer);
if (self.popup) self.popup.remove();
});
set.add(rect);
set.add(mfcr);
};
/**
* Draw the legend on the left side of the graph. The legend consists of a bar filled
* with the color gradient for the current parameter and a few labels to denote important
* values on the bar.
*
* @method legend
* @private
* @chainable
*/
Timeline.prototype.legend = function () {
var paper = this.paper, p = this.properties, gradient = this.gradient.gradient,
set = paper.g(), mfcrpos;
this.remove('legend', this.elements);
if (!gradient) return this;
var path = paper.g(), i, ii, font = extend({}, p.font);
if (p.parameter !== 'grainshape') {
set.add(
paper.rect(p.tl.x - 2 * p.margin, p.tl.y, p.margin, p.length.y)
.attr({ 'fill': 'l(1,1,1,0)' + gradient.gradient,
strokeWidth: 1, stroke: p.grid_color })
.transform('t0.5,0.5')
);
} else {
font.fontSize = '18px';
font.fontFamily = 'snowsymbolsiacs';
if (p.length.y < 250) font.fontSize = '14px';
this.gtlegend(set); // Fix issue 450 here
}
for (i = 0, ii = gradient.labels.length; i < ii; ++i) {
var left = p.tl.x - 2 * p.margin - p.unit;
if (gradient.labels[i] === 'O') left = p.tl.x - 2 * p.margin - p.unit - font.fontSize.replace('px', '');
set.add(
paper.text(left,
p.br.y - p.length.y * gradient.positions[i] + p.fontsize / 2 - 2,
gradient.labels[i] + '')
.attr(font).attr({'text-anchor': 'end'}).attr({ opacity: 0.9 })
);
//The gutter
if (gradient.nogutter) continue;
if (i > 0 && i < (ii - 1)) {
var y = p.br.y - p.length.y * gradient.positions[i];
path.add(paper.line(p.tl.x - 2 * p.margin, y, p.tl.x - 2 * p.margin + p.unit, y));
}
}
var config = Feature.type[p.parameter];
var unit = config.unit ? ' [' + config.unit + ']' : '';
set.add(
paper.text(p.fontsize * 1.5, p.br.y - p.length.y / 2, t(config.name) + unit)
.attr(p.font).attr({ opacity: 0.9, 'text-anchor': 'middle' })
.transform('r270,' + (p.fontsize * 1.5) + ',' + (p.br.y - p.length.y / 2))
);
set.add(path
.attr({stroke: p.grid_color})
.transform('t0.5,0.5')); // to make lines thinner
this.elements['legend'] = set;
return this;
};
/**
* Draw the ordinate legend and grid for the snow height
*
* @method gridy
* @private
* @chainable
*/
Timeline.prototype.gridy = function () {
var p = this.properties, paper = this.paper, i, ii, path = paper.g();
this.remove('gridy', this.elements);
var set = this.elements['gridy'] = paper.g();
for (i = 0, ii = this.hsgrid.heights.length; i < ii; ++i) {
var height = round(this.hsgrid.heights[i], 2),
y = Math.round(this.axisy.pixel(height)) || 0;
if (i && i !== (ii - 1))
path.add(paper.line(p.tl.x - 5, y, p.br.x + 5, y));
if (y > p.br.y || y < p.tl.y) continue;
set.add(paper.text(p.br.x + 7, y + p.fontsize / 2 - 2, height + '')
.attr(p.font).attr({ textAnchor: 'start', opacity: 0.9 }));
}
var label = (this.data.bottom < 0 ? t('Height') : t('Snow height')) + ' [cm]';
var x = this.canvas.width() - p.fontsize, y = p.tl.y + p.length.y / 2;
set.add(paper.text(x, y, label)
.attr(p.font).attr({ opacity: 0.9, 'text-anchor': 'middle' })
.transform('r270,' + x + ',' + y));
set.add(path.attr({
stroke: p.grid_color,
strokeOpacity: 0.2,
strokeDasharray: '2,2',
pointerEvents: 'none'
}).transform('t0.5,0.5'));
return this;
};
/**
* Draw a header at the left top of the graph. The header information is clickable
* and links to a map revealing the exact location.
*
* @method header
* @private
* @chainable
*/
Timeline.prototype.header = function () {
var p = this.properties, paper = this.paper, meteo = this.data, text1 = '', text2 = '',
position = meteo.position, avgstep = this.data.avgstep('minutes');
this.remove('header', this.elements);
if (meteo.name) text1 += meteo.name;
if (position) {
if (position.link) {
text1 += (' (' + round(position.latitude, 4) + '° N '
+ round(position.longitude, 4) + '° E)');
}
if (position.altitude) text1 += (', ' + position.altitude + position.uom);
if (position.angle !== '') text2 += (t('Slope') + ': ' + position.angle + '°');
if (position.azimuth !== '')
text2 += (', ' + t('Azimuth') + ': ' + position.azimuth + '°');
if (avgstep) text2 += (', ' + t('Avg. timestep') + ': ' + avgstep + ' min');
}
var el = paper.g().add(paper.text(p.tl.x, p.tl.y - 1.5 * p.fontsize - 2, text1))
.add(paper.text(p.tl.x, p.tl.y - .5 * p.fontsize - 2, text2));
el.attr(p.font).attr({
textAnchor: 'start',
opacity: 0.9
});
header.link(el, position.link, p.font_color);
this.elements['header'] = el;
return this;
};
/**
* Method to be called when mouseout event is detected. Delete the currently
* displayed date label (unhighlight) and display the date label for the
* current indicator position, if the indicator is set.
*
* @method mouseout
* @protected
*/
Timeline.prototype.mouseout = function () {
this.unhighlight();
if (this.clicked) { // If there is an indicator show the date
this.emit('mousemove', this.data.current.date);
this.highlight(this.data.current.index, true);
}
};
/**
* Method to be called when mouse events are turned off.
*
* @method mouseout
* @protected
*/
Timeline.prototype.mouseoff = function () {
clearTimeout(this.timer);
};
/**
* Method to be called when mousemove event is detected. It calls the method
* Timeline:highlight in turn, which displays the date label.
*
* @method mousemove
* @private
* @param {Object} e Mousemove DOM event object
* @param {Object} offset canvas offset object
*/
Timeline.prototype.mousemove = function (currentdate, e, offset) {
var axisy = this.axisy, self = this, current;
clearTimeout(this.timer);
this.timer = setTimeout(function () {
if (e) self.hs(Math.round(axisy.coord(e.pageY - self.canvas.offset().top)));
self.startdragdate = moment.utc(currentdate);
current = self.data.index(currentdate);
self.highlight(current, self.clicked);
}, 5);
};
/**
* Display the snow height at the current cursor position.
*
* @method hs
* @protected
* @param {Number} snowheight The height of snow in cm
*/
Timeline.prototype.hs = function (snowheight) {
var element = this.elements['hs'], text = snowheight + ' cm';
if (element) {
element.attr('text', text);
} else {
var p = this.properties,
label = this.paper.text(p.br.x, p.tl.y - p.fontsize - 2, text)
.attr(p.font).attr('text-anchor', 'end');
this.elements['hs'] = label;
}
};
/**
* Draw the indicator line in case the user desires to lock onto a certain profile.
*
* @method draw_indicator
* @private
* @param {Number} current Index of profile in this.data.profiles
*/
Timeline.prototype.draw_indicator = function (current) {
var p = this.properties;
this.remove('indicator');
if (current === undefined
|| (current < this.$indices.start || current > this.$indices.end)) {
this.clicked = false;
return;
}
var currentdate = moment.utc(this.data.profiles[current].date), pos;
//if (this.data.length > 1) currentdate.add(this.data.avgstep('minutes') / 2, 'minutes');
pos = this.coordx(currentdate);
if (pos < p.tl.x) pos = p.tl.x;
if (pos > p.br.x) pos = p.br.x;
this.indicator = this.paper.line(pos, p.tl.y, pos, p.br.y).transform('t0.5,0.5');
this.indicator.attr({ stroke: '#000' });
this.emit('mousemove', currentdate);
};
/**
* In case an indicator is present, move one profile to the right.
* @method next
*/
Timeline.prototype.next = function () {
if (!this.data.current || !this.indicator) return;
var current = this.data.current.index + 1;
if (current <= this.$indices.end) {
this.draw_indicator(current);
this.highlight(current);
}
};
/**
* In case an indicator is present, move one profile to the left.
* @method previous
*/
Timeline.prototype.previous = function () {
if (!this.data.current || !this.indicator) return;
var current = this.data.current.index - 1;
if (current >= this.$indices.start) {
this.draw_indicator(current);
this.highlight(current);
}
};
/**
* Method to be called during dragging on top of cover.
*
* @method drag
* @protected
* @param {Moment} Date at current drag position
*/
Timeline.prototype.drag = function (current) {
current = this.data.index(current);
this.highlight(current, true);
};
/**
* Method to be called at the start of dragging on top of cover.
*
* @method dragstart
* @protected
* @param {Moment} current Date at current drag position
*/
Timeline.prototype.dragstart = function (current) {
this.startdragdate = moment.utc(current);
};
/**
* Method to be called at the end of dragging on top of cover.
*
* @method dragend
* @protected
* @param {Moment} current Date at current drag position
* @param {Number} ms Duration of drag in ms
*/
Timeline.prototype.dragend = function (current, ms) {
var startdate, enddate;
if (ms > 200 || ms === undefined) { // click at least 200ms long
startdate = moment.utc(this.startdragdate);
enddate = moment.utc(current);
if (enddate.isBefore(startdate)) enddate = [startdate, startdate = enddate][0];
this.clip(startdate, enddate);
this.unhighlight();
// removes the indicator if set and not in zoomed range
this.draw_indicator(this.clicked ? this.data.current.index : undefined);
} else {
this.clicked = !this.clicked;
if (this.clicked) {
current = this.data.index(this.startdragdate);
this.draw_indicator(current);
this.highlight(current);
} else {
this.draw_indicator(); // removes the indicator if set
}
}
};
/**
* Highlight the current cursor position by drawing the date label and
* displaying the current snow height.
*
* @method highlight
* @private
* @param {Number} current Index in this.data.profiles
* @param {Boolean} noemit Whether to emit the 'profile' event
*/
Timeline.prototype.highlight = function (current, noemit) {
var profile = this.data.profiles[current], self = this;
this.datelabel(profile.date);
setTimeout(function () {
if (!noemit && (!self.data.current || current !== self.data.current.index)) {
self.data.emitter.emit('profile', profile);
self.data.current = profile;
self.data.current.index = current;
}
}, 5);
};
/**
* Remove all highlights (date and snow height labels).
* @method unhighlight
* @private
*/
Timeline.prototype.unhighlight = function () {
this.datelabel();
this.remove('hs', this.elements);
};
/**
* Show a part (or all) of the data within a start and an end date.
*
* @method clip
* @private
* @param {Moment} start Start date
* @param {Moment} end End date
*/
Timeline.prototype.clip = function (start, end) {
this.reconfigure(); // set snow height axis according to zoom
if (start && end) {
this.$range = { start: moment.utc(start), end: moment.utc(end) };
} else if (this.$range) {
start = this.$range.start;
end = this.$range.end;
}
this.range(start, end);
this.indices(start, end);
};
/**
* Show all the data from start to end and remove the 'Show all' button.
* @method reset
* @private
*/
Timeline.prototype.reset = function () {
this.yzoom = { min: 0, max: 100 };
this.config();
this.setup();
this.emit('dragstart', this.data.profiles[0].date);
this.emit('dragend', moment.utc(this.data.profiles[this.data.length - 1].date)
.add(this.data.avgstep('minutes'), 'minutes'));
if (this.data && this.data.length)
this.clip(this.data.profiles[0].date,
moment.utc(this.data.profiles[this.data.length - 1].date)
.add(this.data.avgstep('minutes'), 'minutes'));
this.draw_indicator();
this.resetbutton(false);
};
/**
* Given a start and end index (for this.data.profiles) draw the current parameter.
* In case the start index is not 0 and the end index is not the last index show
* the resetbutton.
*
* @method indices
* @private
* @param {Moment} startdate Start date of range
* @param {Moment} enddate End date of range
*/
Timeline.prototype.indices = function (startdate, enddate) {
var start = this.data.index(startdate), end = this.data.index(enddate);
this.resetbutton(start !== 0 || end !== this.data.length - 1, this.reset);
if (this.data.profiles[start].date.isAfter(this.$range.start)) start--;
if (this.data.profiles[end].date.isAfter(this.$range.end)) end--;
this.$indices = { start: Math.max(start, 0), end: Math.max(end, 0) };
this.drawPNG();
};
/**
* Draw the PNG image for the currently selected parameter. The method creates a canvas
* element to use as drawing board. Once the graph is drawn onto the canvas, the pastePNG
* method is called which tranforms the image into a base64 encoded dataURI and embeds it
* into an SVG image tag.
*
* @method drawPNG
* @private
*/
Timeline.prototype.drawPNG = function () {
var p = this.properties, profiles = this.data.profiles, start = this.$indices.start,
end = this.$indices.end, range = end - start + 1, increment = p.length.x / range,
ii, canvas = document.createElement('canvas'), ctx = canvas.getContext('2d'),
gradient = this.gradient;
canvas.width = p.length.x;
canvas.height = p.length.y;
if (start < 0 || end < 0) {
this.pastePNG(p, canvas);
canvas = null;
return;
}
var coord = { x: this.coordx(profiles[start].date), y: this.coordy(this.data.bottom) };
var oldx = -1;
var width = Math.ceil(increment);
var step = this.coordx(
moment.utc(profiles[start].date).add(this.data.avgstep('minutes'), 'minutes')
);
width = Math.ceil(Math.ceil(step) - Math.floor(coord.x)) + 1;
for (ii = start; ii <= end; ++ii) {
var current = profiles[ii], parameter = current[p.parameter];
if (parameter) {
coord.x = this.coordx(current.date);
if (oldx === coord.x) continue;
oldx = coord.x;
var layers = parameter.layers.length, jj, newy;
coord.y = p.br.y - p.tl.y;
for (jj = 0; jj < layers; ++jj) {
var layer = parameter.layers[jj];
this.setFillStyle(ctx, layer);
newy = this.coordy(layer.top);
coord.y = this.coordy(layer.bottom);
ctx.fillRect(coord.x - p.tl.x, round(newy - p.tl.y, 0),
width, Math.max(coord.y - newy, 2));
}
if (p.parameter === 'grainshape' && current.sh) { //for legacy PRO files
ctx.fillStyle = '#ff00ff';
ctx.fillRect(coord.x - p.tl.x, round(newy - 2 - p.tl.y, 0), width, 2);
}
}
}
this.pastePNG(p, canvas);
canvas = null;
};
Timeline.prototype.setFillStyle = function (ctx, layer) {
if (layer.value.primary === 'MFcr') {
ctx.fillStyle = ctx.createPattern(Gradient.patternMFcr, 'repeat');
} else {
ctx.fillStyle = this.gradient.color(layer);
}
};
/**
* Converts the image on the canvas into a base64 encoded dataURI and embeds it in
* a SVG image tag.
*
* @method pastePNG
* @private
*/
Timeline.prototype.pastePNG = function (p, canvas) {
var image = this.elements['image'];
if (!image) {
this.elements['image'] = this.paper.image(canvas.toDataURL('image/png'),
p.tl.x, p.tl.y, p.length.x, p.length.y);
this.elements['image'].prependTo(this.paper);
} else {
image.attr({ href: canvas.toDataURL('image/png'), x: p.tl.x, y: p.tl.y,
width: p.length.x, height: p.length.y });
}
};
/**
* Get the rounded x-coordinate for a given date.
*
* @method coordx
* @private
* @param {Moment} date
* @return {Number} x-coordinate
*/
Timeline.prototype.coordx = function (date) {
return Math.round(this.axisx.pixel(date.format('X')));
};
/**
* Get the rounded y-coordinate for a given (snow) height.
*
* @method coordy
* @private
* @param {Number} height
* @return {Number} y-coordinate
*/
Timeline.prototype.coordy = function (height) {
return Math.round(this.axisy.pixel(height));
};
/**
* Set the date to display explicitly (may be used externally)
* @method setDate
*/
Timeline.prototype.setDate = function (date) {
this.hs(0);
var current = this.data.index(date);
this.highlight(current, this.clicked);
};
/**
* Draw the Timeline and set up the mouse events.
* @method draw
*/
Timeline.prototype.draw = function () {
this.config();
this.setup();
this.mouseon(false);
if (!this.$range) {
this.reset();
} else {
this.clip();
}
if (this.clicked) this.draw_indicator(this.data.current.index);
this.mouseon(true);
this.draggable(this.cover);
};
// --- Helpers ---
// --- Module Exports ---
niviz.Timeline = Timeline;
}(niviz, moment));