@ -1,9 +1,12 @@
var Emitter = require ( 'emitter-component' ) ;
var DataSet = require ( '../DataSet' ) ;
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' ) ;
/ * *
@ -48,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
@ -97,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
* /
@ -768,11 +639,11 @@ Graph3d.prototype.create = function () {
var ontooltip = function ( event ) { me . _onTooltip ( event ) ; } ;
// TODO: these events are never cleaned up... can give a 'memory leakage'
G3D addEventListener( this . frame . canvas , 'keydown' , onkeydown ) ;
G3D addEventListener( this . frame . canvas , 'mousedown' , onmousedown ) ;
G3D addEventListener( this . frame . canvas , 'touchstart' , ontouchstart ) ;
G3D addEventListener( this . frame . canvas , 'mousewheel' , onmousewheel ) ;
G3D addEventListener( this . frame . canvas , 'mousemove' , ontooltip ) ;
util . addEventListener ( this . frame . canvas , 'keydown' , onkeydown ) ;
util . addEventListener ( this . frame . canvas , 'mousedown' , onmousedown ) ;
util . addEventListener ( this . frame . canvas , 'touchstart' , ontouchstart ) ;
util . addEventListener ( this . frame . canvas , 'mousewheel' , onmousewheel ) ;
util . addEventListener ( this . frame . canvas , 'mousemove' , ontooltip ) ;
// add the new graph to the container element
this . containerElement . appendChild ( this . frame ) ;
@ -1987,9 +1858,9 @@ Graph3d.prototype._onMouseDown = function(event) {
var me = this ;
this . onmousemove = function ( event ) { me . _onMouseMove ( event ) ; } ;
this . onmouseup = function ( event ) { me . _onMouseUp ( event ) ; } ;
G3D addEventListener( document , 'mousemove' , me . onmousemove ) ;
G3D addEventListener( document , 'mouseup' , me . onmouseup ) ;
G3D preventDefault( event ) ;
util . addEventListener ( document , 'mousemove' , me . onmousemove ) ;
util . addEventListener ( document , 'mouseup' , me . onmouseup ) ;
util . preventDefault ( event ) ;
} ;
@ -2035,7 +1906,7 @@ Graph3d.prototype._onMouseMove = function (event) {
var parameters = this . getCameraPosition ( ) ;
this . emit ( 'cameraPositionChange' , parameters ) ;
G3D preventDefault( event ) ;
util . preventDefault ( event ) ;
} ;
@ -2049,9 +1920,9 @@ Graph3d.prototype._onMouseUp = function (event) {
this . leftButtonDown = false ;
// remove event listeners here
G3D removeEventListener( document , 'mousemove' , this . onmousemove ) ;
G3D removeEventListener( document , 'mouseup' , this . onmouseup ) ;
G3D preventDefault( event ) ;
util . removeEventListener ( document , 'mousemove' , this . onmousemove ) ;
util . removeEventListener ( document , 'mouseup' , this . onmouseup ) ;
util . preventDefault ( event ) ;
} ;
/ * *
@ -2060,8 +1931,8 @@ Graph3d.prototype._onMouseUp = function (event) {
* /
Graph3d . prototype . _onTooltip = function ( event ) {
var delay = 300 ; // ms
var mouseX = getMouseX ( event ) - getAbsoluteLeft ( this . frame ) ;
var mouseY = getMouseY ( event ) - getAbsoluteTop ( this . frame ) ;
var mouseX = getMouseX ( event ) - util . getAbsoluteLeft ( this . frame ) ;
var mouseY = getMouseY ( event ) - util . getAbsoluteTop ( this . frame ) ;
if ( ! this . showTooltip ) {
return ;
@ -2114,8 +1985,8 @@ Graph3d.prototype._onTouchStart = function(event) {
var me = this ;
this . ontouchmove = function ( event ) { me . _onTouchMove ( event ) ; } ;
this . ontouchend = function ( event ) { me . _onTouchEnd ( event ) ; } ;
G3D addEventListener( document , 'touchmove' , me . ontouchmove ) ;
G3D addEventListener( document , 'touchend' , me . ontouchend ) ;
util . addEventListener ( document , 'touchmove' , me . ontouchmove ) ;
util . addEventListener ( document , 'touchend' , me . ontouchend ) ;
this . _onMouseDown ( event ) ;
} ;
@ -2133,8 +2004,8 @@ Graph3d.prototype._onTouchMove = function(event) {
Graph3d . prototype . _onTouchEnd = function ( event ) {
this . touchDown = false ;
G3D removeEventListener( document , 'touchmove' , this . ontouchmove ) ;
G3D removeEventListener( document , 'touchend' , this . ontouchend ) ;
util . removeEventListener ( document , 'touchmove' , this . ontouchmove ) ;
util . removeEventListener ( document , 'touchend' , this . ontouchend ) ;
this . _onMouseUp ( event ) ;
} ;
@ -2179,7 +2050,7 @@ Graph3d.prototype._onWheel = function(event) {
// Prevent default actions caused by mouse wheel.
// That might be ugly, but we handle scrolls somehow
// anyway, so don't bother here..
G3D preventDefault( event ) ;
util . preventDefault ( event ) ;
} ;
/ * *
@ -2371,466 +2242,9 @@ Graph3d.prototype._hideTooltip = function () {
}
} ;
/ * *
* Add and event listener . Works for all browsers
* @ param { Element } element An html element
* @ param { string } action The action , for example 'click' ,
* without the prefix 'on'
* @ param { function } listener The callback function to be executed
* @ param { boolean } useCapture
* /
G3DaddEventListener = function ( element , action , listener , useCapture ) {
if ( element . addEventListener ) {
if ( useCapture === undefined )
useCapture = false ;
if ( action === 'mousewheel' && navigator . userAgent . indexOf ( 'Firefox' ) >= 0 ) {
action = 'DOMMouseScroll' ; // For Firefox
}
element . addEventListener ( action , listener , useCapture ) ;
} else {
element . attachEvent ( 'on' + action , listener ) ; // IE browsers
}
} ;
/ * *
* Remove an event listener from an element
* @ param { Element } element An html dom element
* @ param { string } action The name of the event , for example 'mousedown'
* @ param { function } listener The listener function
* @ param { boolean } useCapture
* /
G3DremoveEventListener = function ( element , action , listener , useCapture ) {
if ( element . removeEventListener ) {
// non-IE browsers
if ( useCapture === undefined )
useCapture = false ;
if ( action === 'mousewheel' && navigator . userAgent . indexOf ( 'Firefox' ) >= 0 ) {
action = 'DOMMouseScroll' ; // For Firefox
}
element . removeEventListener ( action , listener , useCapture ) ;
} else {
// IE browsers
element . detachEvent ( 'on' + action , listener ) ;
}
} ;
/ * *
* Stop event propagation
* /
G3DstopPropagation = function ( event ) {
if ( ! event )
event = window . event ;
if ( event . stopPropagation ) {
event . stopPropagation ( ) ; // non-IE browsers
}
else {
event . cancelBubble = true ; // IE browsers
}
} ;
/ * *
* Cancels the event if it is cancelable , without stopping further propagation of the event .
* /
G3DpreventDefault = function ( event ) {
if ( ! event )
event = window . event ;
if ( event . preventDefault ) {
event . preventDefault ( ) ; // non-IE browsers
}
else {
event . returnValue = false ; // IE browsers
}
} ;
/ * *
* @ 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 ) ; } ;
G3DaddEventListener ( document , 'mousemove' , this . onmousemove ) ;
G3DaddEventListener ( document , 'mouseup' , this . onmouseup ) ;
G3DpreventDefault ( 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 ) ;
G3DpreventDefault ( ) ;
} ;
Slider . prototype . _onMouseUp = function ( event ) {
this . frame . style . cursor = 'auto' ;
// remove event listeners
G3DremoveEventListener ( document , 'mousemove' , this . onmousemove ) ;
G3DremoveEventListener ( document , 'mouseup' , this . onmouseup ) ;
G3DpreventDefault ( ) ;
} ;
/**--------------------------------------------------------------------------**/
/ * *
* Retrieve the absolute left value of a DOM element
* @ param { Element } elem A dom element , for example a div
* @ return { Number } left The absolute left position of this element
* in the browser page .
* /
getAbsoluteLeft = function ( elem ) {
var left = 0 ;
while ( elem !== null ) {
left += elem . offsetLeft ;
left -= elem . scrollLeft ;
elem = elem . offsetParent ;
}
return left ;
} ;
/ * *
* Retrieve the absolute top value of a DOM element
* @ param { Element } elem A dom element , for example a div
* @ return { Number } top The absolute top position of this element
* in the browser page .
* /
getAbsoluteTop = function ( elem ) {
var top = 0 ;
while ( elem !== null ) {
top += elem . offsetTop ;
top -= elem . scrollTop ;
elem = elem . offsetParent ;
}
return top ;
} ;
/ * *
* Get the horizontal mouse position from a mouse event
* @ param { Event } event