diff --git a/index.js b/index.js index 0b60ac87..65b202e8 100644 --- a/index.js +++ b/index.js @@ -8,11 +8,19 @@ exports.DataView = require('./lib/DataView'); // Graph3d exports.Graph3d = require('./lib/graph3d/Graph3d'); +exports.graph3d = { + Camera: require('./lib/graph3d/Camera'), + Filter: require('./lib/graph3d/Filter'), + Point2d: require('./lib/graph3d/Point2d'), + Point3d: require('./lib/graph3d/Point3d'), + Slider: require('./lib/graph3d/Slider'), + StepNumber: require('./lib/graph3d/StepNumber') +}; // Timeline exports.Timeline = require('./lib/timeline/Timeline'); exports.Graph2d = require('./lib/timeline/Graph2d'); -exports.timeline= { +exports.timeline = { DataStep: require('./lib/timeline/DataStep'), Range: require('./lib/timeline/Range'), stack: require('./lib/timeline/Stack'), diff --git a/lib/graph3d/Camera.js b/lib/graph3d/Camera.js new file mode 100644 index 00000000..d758f6fc --- /dev/null +++ b/lib/graph3d/Camera.js @@ -0,0 +1,135 @@ +var Point3d = require('./Point3d'); + +/** + * @class Camera + * The camera is mounted on a (virtual) camera arm. The camera arm can rotate + * The camera is always looking in the direction of the origin of the arm. + * This way, the camera always rotates around one fixed point, the location + * of the camera arm. + * + * Documentation: + * http://en.wikipedia.org/wiki/3D_projection + */ +Camera = function () { + this.armLocation = new Point3d(); + this.armRotation = {}; + this.armRotation.horizontal = 0; + this.armRotation.vertical = 0; + this.armLength = 1.7; + + this.cameraLocation = new Point3d(); + this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0); + + this.calculateCameraOrientation(); +}; + +/** + * Set the location (origin) of the arm + * @param {Number} x Normalized value of x + * @param {Number} y Normalized value of y + * @param {Number} z Normalized value of z + */ +Camera.prototype.setArmLocation = function(x, y, z) { + this.armLocation.x = x; + this.armLocation.y = y; + this.armLocation.z = z; + + this.calculateCameraOrientation(); +}; + +/** + * Set the rotation of the camera arm + * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI + * if vertical=0.5*PI, the graph is shown from the + * top. Optional, can be left undefined. + */ +Camera.prototype.setArmRotation = function(horizontal, vertical) { + if (horizontal !== undefined) { + this.armRotation.horizontal = horizontal; + } + + if (vertical !== undefined) { + this.armRotation.vertical = vertical; + if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; + if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI; + } + + if (horizontal !== undefined || vertical !== undefined) { + this.calculateCameraOrientation(); + } +}; + +/** + * Retrieve the current arm rotation + * @return {object} An object with parameters horizontal and vertical + */ +Camera.prototype.getArmRotation = function() { + var rot = {}; + rot.horizontal = this.armRotation.horizontal; + rot.vertical = this.armRotation.vertical; + + return rot; +}; + +/** + * Set the (normalized) length of the camera arm. + * @param {Number} length A length between 0.71 and 5.0 + */ +Camera.prototype.setArmLength = function(length) { + if (length === undefined) + return; + + this.armLength = length; + + // Radius must be larger than the corner of the graph, + // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the + // graph + if (this.armLength < 0.71) this.armLength = 0.71; + if (this.armLength > 5.0) this.armLength = 5.0; + + this.calculateCameraOrientation(); +}; + +/** + * Retrieve the arm length + * @return {Number} length + */ +Camera.prototype.getArmLength = function() { + return this.armLength; +}; + +/** + * Retrieve the camera location + * @return {Point3d} cameraLocation + */ +Camera.prototype.getCameraLocation = function() { + return this.cameraLocation; +}; + +/** + * Retrieve the camera rotation + * @return {Point3d} cameraRotation + */ +Camera.prototype.getCameraRotation = function() { + return this.cameraRotation; +}; + +/** + * Calculate the location and rotation of the camera based on the + * position and orientation of the camera arm + */ +Camera.prototype.calculateCameraOrientation = function() { + // calculate location of the camera + this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); + this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); + this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); + + // calculate rotation of the camera + this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical; + this.cameraRotation.y = 0; + this.cameraRotation.z = -this.armRotation.horizontal; +}; + +module.exports = Camera; \ No newline at end of file diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index bd3c00e1..bbc6bdcd 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -4,7 +4,9 @@ var DataView = require('../DataView'); var util = require('../util'); var Point3d = require('./Point3d'); var Point2d = require('./Point2d'); +var Camera = require('./Camera'); var Filter = require('./Filter'); +var Slider = require('./Slider'); var StepNumber = require('./StepNumber'); /** @@ -49,7 +51,7 @@ function Graph3d(container, data, options) { this.animationInterval = 1000; // milliseconds this.animationPreload = false; - this.camera = new Graph3d.Camera(); + this.camera = new Camera(); this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? this.dataTable = null; // The original data table @@ -98,138 +100,6 @@ function Graph3d(container, data, options) { // Extend Graph3d with an Emitter mixin Emitter(Graph3d.prototype); -/** - * @class Camera - * The camera is mounted on a (virtual) camera arm. The camera arm can rotate - * The camera is always looking in the direction of the origin of the arm. - * This way, the camera always rotates around one fixed point, the location - * of the camera arm. - * - * Documentation: - * http://en.wikipedia.org/wiki/3D_projection - */ -Graph3d.Camera = function () { - this.armLocation = new Point3d(); - this.armRotation = {}; - this.armRotation.horizontal = 0; - this.armRotation.vertical = 0; - this.armLength = 1.7; - - this.cameraLocation = new Point3d(); - this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0); - - this.calculateCameraOrientation(); -}; - -/** - * Set the location (origin) of the arm - * @param {Number} x Normalized value of x - * @param {Number} y Normalized value of y - * @param {Number} z Normalized value of z - */ -Graph3d.Camera.prototype.setArmLocation = function(x, y, z) { - this.armLocation.x = x; - this.armLocation.y = y; - this.armLocation.z = z; - - this.calculateCameraOrientation(); -}; - -/** - * Set the rotation of the camera arm - * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI. - * Optional, can be left undefined. - * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI - * if vertical=0.5*PI, the graph is shown from the - * top. Optional, can be left undefined. - */ -Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) { - if (horizontal !== undefined) { - this.armRotation.horizontal = horizontal; - } - - if (vertical !== undefined) { - this.armRotation.vertical = vertical; - if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; - if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI; - } - - if (horizontal !== undefined || vertical !== undefined) { - this.calculateCameraOrientation(); - } -}; - -/** - * Retrieve the current arm rotation - * @return {object} An object with parameters horizontal and vertical - */ -Graph3d.Camera.prototype.getArmRotation = function() { - var rot = {}; - rot.horizontal = this.armRotation.horizontal; - rot.vertical = this.armRotation.vertical; - - return rot; -}; - -/** - * Set the (normalized) length of the camera arm. - * @param {Number} length A length between 0.71 and 5.0 - */ -Graph3d.Camera.prototype.setArmLength = function(length) { - if (length === undefined) - return; - - this.armLength = length; - - // Radius must be larger than the corner of the graph, - // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the - // graph - if (this.armLength < 0.71) this.armLength = 0.71; - if (this.armLength > 5.0) this.armLength = 5.0; - - this.calculateCameraOrientation(); -}; - -/** - * Retrieve the arm length - * @return {Number} length - */ -Graph3d.Camera.prototype.getArmLength = function() { - return this.armLength; -}; - -/** - * Retrieve the camera location - * @return {Point3d} cameraLocation - */ -Graph3d.Camera.prototype.getCameraLocation = function() { - return this.cameraLocation; -}; - -/** - * Retrieve the camera rotation - * @return {Point3d} cameraRotation - */ -Graph3d.Camera.prototype.getCameraRotation = function() { - return this.cameraRotation; -}; - -/** - * Calculate the location and rotation of the camera based on the - * position and orientation of the camera arm - */ -Graph3d.Camera.prototype.calculateCameraOrientation = function() { - // calculate location of the camera - this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); - this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); - this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); - - // calculate rotation of the camera - this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical; - this.cameraRotation.y = 0; - this.cameraRotation.z = -this.armRotation.horizontal; -}; - /** * Calculate the scaling values, dependent on the range in x, y, and z direction */ @@ -2372,351 +2242,6 @@ Graph3d.prototype._hideTooltip = function () { } }; -/** - * @constructor Slider - * - * An html slider control with start/stop/prev/next buttons - * @param {Element} container The element where the slider will be created - * @param {Object} options Available options: - * {boolean} visible If true (default) the - * slider is visible. - */ -function Slider(container, options) { - if (container === undefined) { - throw 'Error: No container element defined'; - } - this.container = container; - this.visible = (options && options.visible != undefined) ? options.visible : true; - - if (this.visible) { - this.frame = document.createElement('DIV'); - //this.frame.style.backgroundColor = '#E5E5E5'; - this.frame.style.width = '100%'; - this.frame.style.position = 'relative'; - this.container.appendChild(this.frame); - - this.frame.prev = document.createElement('INPUT'); - this.frame.prev.type = 'BUTTON'; - this.frame.prev.value = 'Prev'; - this.frame.appendChild(this.frame.prev); - - this.frame.play = document.createElement('INPUT'); - this.frame.play.type = 'BUTTON'; - this.frame.play.value = 'Play'; - this.frame.appendChild(this.frame.play); - - this.frame.next = document.createElement('INPUT'); - this.frame.next.type = 'BUTTON'; - this.frame.next.value = 'Next'; - this.frame.appendChild(this.frame.next); - - this.frame.bar = document.createElement('INPUT'); - this.frame.bar.type = 'BUTTON'; - this.frame.bar.style.position = 'absolute'; - this.frame.bar.style.border = '1px solid red'; - this.frame.bar.style.width = '100px'; - this.frame.bar.style.height = '6px'; - this.frame.bar.style.borderRadius = '2px'; - this.frame.bar.style.MozBorderRadius = '2px'; - this.frame.bar.style.border = '1px solid #7F7F7F'; - this.frame.bar.style.backgroundColor = '#E5E5E5'; - this.frame.appendChild(this.frame.bar); - - this.frame.slide = document.createElement('INPUT'); - this.frame.slide.type = 'BUTTON'; - this.frame.slide.style.margin = '0px'; - this.frame.slide.value = ' '; - this.frame.slide.style.position = 'relative'; - this.frame.slide.style.left = '-100px'; - this.frame.appendChild(this.frame.slide); - - // create events - var me = this; - this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);}; - this.frame.prev.onclick = function (event) {me.prev(event);}; - this.frame.play.onclick = function (event) {me.togglePlay(event);}; - this.frame.next.onclick = function (event) {me.next(event);}; - } - - this.onChangeCallback = undefined; - - this.values = []; - this.index = undefined; - - this.playTimeout = undefined; - this.playInterval = 1000; // milliseconds - this.playLoop = true; -} - -/** - * Select the previous index - */ -Slider.prototype.prev = function() { - var index = this.getIndex(); - if (index > 0) { - index--; - this.setIndex(index); - } -}; - -/** - * Select the next index - */ -Slider.prototype.next = function() { - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); - } -}; - -/** - * Select the next index - */ -Slider.prototype.playNext = function() { - var start = new Date(); - - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); - } - else if (this.playLoop) { - // jump to the start - index = 0; - this.setIndex(index); - } - - var end = new Date(); - var diff = (end - start); - - // calculate how much time it to to set the index and to execute the callback - // function. - var interval = Math.max(this.playInterval - diff, 0); - // document.title = diff // TODO: cleanup - - var me = this; - this.playTimeout = setTimeout(function() {me.playNext();}, interval); -}; - -/** - * Toggle start or stop playing - */ -Slider.prototype.togglePlay = function() { - if (this.playTimeout === undefined) { - this.play(); - } else { - this.stop(); - } -}; - -/** - * Start playing - */ -Slider.prototype.play = function() { - // Test whether already playing - if (this.playTimeout) return; - - this.playNext(); - - if (this.frame) { - this.frame.play.value = 'Stop'; - } -}; - -/** - * Stop playing - */ -Slider.prototype.stop = function() { - clearInterval(this.playTimeout); - this.playTimeout = undefined; - - if (this.frame) { - this.frame.play.value = 'Play'; - } -}; - -/** - * Set a callback function which will be triggered when the value of the - * slider bar has changed. - */ -Slider.prototype.setOnChangeCallback = function(callback) { - this.onChangeCallback = callback; -}; - -/** - * Set the interval for playing the list - * @param {Number} interval The interval in milliseconds - */ -Slider.prototype.setPlayInterval = function(interval) { - this.playInterval = interval; -}; - -/** - * Retrieve the current play interval - * @return {Number} interval The interval in milliseconds - */ -Slider.prototype.getPlayInterval = function(interval) { - return this.playInterval; -}; - -/** - * Set looping on or off - * @pararm {boolean} doLoop If true, the slider will jump to the start when - * the end is passed, and will jump to the end - * when the start is passed. - */ -Slider.prototype.setPlayLoop = function(doLoop) { - this.playLoop = doLoop; -}; - - -/** - * Execute the onchange callback function - */ -Slider.prototype.onChange = function() { - if (this.onChangeCallback !== undefined) { - this.onChangeCallback(); - } -}; - -/** - * redraw the slider on the correct place - */ -Slider.prototype.redraw = function() { - if (this.frame) { - // resize the bar - this.frame.bar.style.top = (this.frame.clientHeight/2 - - this.frame.bar.offsetHeight/2) + 'px'; - this.frame.bar.style.width = (this.frame.clientWidth - - this.frame.prev.clientWidth - - this.frame.play.clientWidth - - this.frame.next.clientWidth - 30) + 'px'; - - // position the slider button - var left = this.indexToLeft(this.index); - this.frame.slide.style.left = (left) + 'px'; - } -}; - - -/** - * Set the list with values for the slider - * @param {Array} values A javascript array with values (any type) - */ -Slider.prototype.setValues = function(values) { - this.values = values; - - if (this.values.length > 0) - this.setIndex(0); - else - this.index = undefined; -}; - -/** - * Select a value by its index - * @param {Number} index - */ -Slider.prototype.setIndex = function(index) { - if (index < this.values.length) { - this.index = index; - - this.redraw(); - this.onChange(); - } - else { - throw 'Error: index out of range'; - } -}; - -/** - * retrieve the index of the currently selected vaue - * @return {Number} index - */ -Slider.prototype.getIndex = function() { - return this.index; -}; - - -/** - * retrieve the currently selected value - * @return {*} value - */ -Slider.prototype.get = function() { - return this.values[this.index]; -}; - - -Slider.prototype._onMouseDown = function(event) { - // only react on left mouse button down - var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); - if (!leftButtonDown) return; - - this.startClientX = event.clientX; - this.startSlideX = parseFloat(this.frame.slide.style.left); - - this.frame.style.cursor = 'move'; - - // add event listeners to handle moving the contents - // we store the function onmousemove and onmouseup in the graph, so we can - // remove the eventlisteners lateron in the function mouseUp() - var me = this; - this.onmousemove = function (event) {me._onMouseMove(event);}; - this.onmouseup = function (event) {me._onMouseUp(event);}; - util.addEventListener(document, 'mousemove', this.onmousemove); - util.addEventListener(document, 'mouseup', this.onmouseup); - util.preventDefault(event); -}; - - -Slider.prototype.leftToIndex = function (left) { - var width = parseFloat(this.frame.bar.style.width) - - this.frame.slide.clientWidth - 10; - var x = left - 3; - - var index = Math.round(x / width * (this.values.length-1)); - if (index < 0) index = 0; - if (index > this.values.length-1) index = this.values.length-1; - - return index; -}; - -Slider.prototype.indexToLeft = function (index) { - var width = parseFloat(this.frame.bar.style.width) - - this.frame.slide.clientWidth - 10; - - var x = index / (this.values.length-1) * width; - var left = x + 3; - - return left; -}; - - - -Slider.prototype._onMouseMove = function (event) { - var diff = event.clientX - this.startClientX; - var x = this.startSlideX + diff; - - var index = this.leftToIndex(x); - - this.setIndex(index); - - util.preventDefault(); -}; - - -Slider.prototype._onMouseUp = function (event) { - this.frame.style.cursor = 'auto'; - - // remove event listeners - util.removeEventListener(document, 'mousemove', this.onmousemove); - util.removeEventListener(document, 'mouseup', this.onmouseup); - - util.preventDefault(); -}; - - - /**--------------------------------------------------------------------------**/ diff --git a/lib/graph3d/Slider.js b/lib/graph3d/Slider.js new file mode 100644 index 00000000..dd688e50 --- /dev/null +++ b/lib/graph3d/Slider.js @@ -0,0 +1,346 @@ +var util = require('../util'); + +/** + * @constructor Slider + * + * An html slider control with start/stop/prev/next buttons + * @param {Element} container The element where the slider will be created + * @param {Object} options Available options: + * {boolean} visible If true (default) the + * slider is visible. + */ +function Slider(container, options) { + if (container === undefined) { + throw 'Error: No container element defined'; + } + this.container = container; + this.visible = (options && options.visible != undefined) ? options.visible : true; + + if (this.visible) { + this.frame = document.createElement('DIV'); + //this.frame.style.backgroundColor = '#E5E5E5'; + this.frame.style.width = '100%'; + this.frame.style.position = 'relative'; + this.container.appendChild(this.frame); + + this.frame.prev = document.createElement('INPUT'); + this.frame.prev.type = 'BUTTON'; + this.frame.prev.value = 'Prev'; + this.frame.appendChild(this.frame.prev); + + this.frame.play = document.createElement('INPUT'); + this.frame.play.type = 'BUTTON'; + this.frame.play.value = 'Play'; + this.frame.appendChild(this.frame.play); + + this.frame.next = document.createElement('INPUT'); + this.frame.next.type = 'BUTTON'; + this.frame.next.value = 'Next'; + this.frame.appendChild(this.frame.next); + + this.frame.bar = document.createElement('INPUT'); + this.frame.bar.type = 'BUTTON'; + this.frame.bar.style.position = 'absolute'; + this.frame.bar.style.border = '1px solid red'; + this.frame.bar.style.width = '100px'; + this.frame.bar.style.height = '6px'; + this.frame.bar.style.borderRadius = '2px'; + this.frame.bar.style.MozBorderRadius = '2px'; + this.frame.bar.style.border = '1px solid #7F7F7F'; + this.frame.bar.style.backgroundColor = '#E5E5E5'; + this.frame.appendChild(this.frame.bar); + + this.frame.slide = document.createElement('INPUT'); + this.frame.slide.type = 'BUTTON'; + this.frame.slide.style.margin = '0px'; + this.frame.slide.value = ' '; + this.frame.slide.style.position = 'relative'; + this.frame.slide.style.left = '-100px'; + this.frame.appendChild(this.frame.slide); + + // create events + var me = this; + this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);}; + this.frame.prev.onclick = function (event) {me.prev(event);}; + this.frame.play.onclick = function (event) {me.togglePlay(event);}; + this.frame.next.onclick = function (event) {me.next(event);}; + } + + this.onChangeCallback = undefined; + + this.values = []; + this.index = undefined; + + this.playTimeout = undefined; + this.playInterval = 1000; // milliseconds + this.playLoop = true; +} + +/** + * Select the previous index + */ +Slider.prototype.prev = function() { + var index = this.getIndex(); + if (index > 0) { + index--; + this.setIndex(index); + } +}; + +/** + * Select the next index + */ +Slider.prototype.next = function() { + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); + } +}; + +/** + * Select the next index + */ +Slider.prototype.playNext = function() { + var start = new Date(); + + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); + } + else if (this.playLoop) { + // jump to the start + index = 0; + this.setIndex(index); + } + + var end = new Date(); + var diff = (end - start); + + // calculate how much time it to to set the index and to execute the callback + // function. + var interval = Math.max(this.playInterval - diff, 0); + // document.title = diff // TODO: cleanup + + var me = this; + this.playTimeout = setTimeout(function() {me.playNext();}, interval); +}; + +/** + * Toggle start or stop playing + */ +Slider.prototype.togglePlay = function() { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); + } +}; + +/** + * Start playing + */ +Slider.prototype.play = function() { + // Test whether already playing + if (this.playTimeout) return; + + this.playNext(); + + if (this.frame) { + this.frame.play.value = 'Stop'; + } +}; + +/** + * Stop playing + */ +Slider.prototype.stop = function() { + clearInterval(this.playTimeout); + this.playTimeout = undefined; + + if (this.frame) { + this.frame.play.value = 'Play'; + } +}; + +/** + * Set a callback function which will be triggered when the value of the + * slider bar has changed. + */ +Slider.prototype.setOnChangeCallback = function(callback) { + this.onChangeCallback = callback; +}; + +/** + * Set the interval for playing the list + * @param {Number} interval The interval in milliseconds + */ +Slider.prototype.setPlayInterval = function(interval) { + this.playInterval = interval; +}; + +/** + * Retrieve the current play interval + * @return {Number} interval The interval in milliseconds + */ +Slider.prototype.getPlayInterval = function(interval) { + return this.playInterval; +}; + +/** + * Set looping on or off + * @pararm {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ +Slider.prototype.setPlayLoop = function(doLoop) { + this.playLoop = doLoop; +}; + + +/** + * Execute the onchange callback function + */ +Slider.prototype.onChange = function() { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } +}; + +/** + * redraw the slider on the correct place + */ +Slider.prototype.redraw = function() { + if (this.frame) { + // resize the bar + this.frame.bar.style.top = (this.frame.clientHeight/2 - + this.frame.bar.offsetHeight/2) + 'px'; + this.frame.bar.style.width = (this.frame.clientWidth - + this.frame.prev.clientWidth - + this.frame.play.clientWidth - + this.frame.next.clientWidth - 30) + 'px'; + + // position the slider button + var left = this.indexToLeft(this.index); + this.frame.slide.style.left = (left) + 'px'; + } +}; + + +/** + * Set the list with values for the slider + * @param {Array} values A javascript array with values (any type) + */ +Slider.prototype.setValues = function(values) { + this.values = values; + + if (this.values.length > 0) + this.setIndex(0); + else + this.index = undefined; +}; + +/** + * Select a value by its index + * @param {Number} index + */ +Slider.prototype.setIndex = function(index) { + if (index < this.values.length) { + this.index = index; + + this.redraw(); + this.onChange(); + } + else { + throw 'Error: index out of range'; + } +}; + +/** + * retrieve the index of the currently selected vaue + * @return {Number} index + */ +Slider.prototype.getIndex = function() { + return this.index; +}; + + +/** + * retrieve the currently selected value + * @return {*} value + */ +Slider.prototype.get = function() { + return this.values[this.index]; +}; + + +Slider.prototype._onMouseDown = function(event) { + // only react on left mouse button down + var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + if (!leftButtonDown) return; + + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); + + this.frame.style.cursor = 'move'; + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + this.onmousemove = function (event) {me._onMouseMove(event);}; + this.onmouseup = function (event) {me._onMouseUp(event);}; + util.addEventListener(document, 'mousemove', this.onmousemove); + util.addEventListener(document, 'mouseup', this.onmouseup); + util.preventDefault(event); +}; + + +Slider.prototype.leftToIndex = function (left) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + var x = left - 3; + + var index = Math.round(x / width * (this.values.length-1)); + if (index < 0) index = 0; + if (index > this.values.length-1) index = this.values.length-1; + + return index; +}; + +Slider.prototype.indexToLeft = function (index) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + + var x = index / (this.values.length-1) * width; + var left = x + 3; + + return left; +}; + + + +Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; + + var index = this.leftToIndex(x); + + this.setIndex(index); + + util.preventDefault(); +}; + + +Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + + // remove event listeners + util.removeEventListener(document, 'mousemove', this.onmousemove); + util.removeEventListener(document, 'mouseup', this.onmouseup); + + util.preventDefault(); +}; + +module.exports = Slider;