@ -6,8 +6,9 @@ var Component = require('./Component');
var DataAxis = require ( './DataAxis' ) ;
var GraphGroup = require ( './GraphGroup' ) ;
var Legend = require ( './Legend' ) ;
var BarFunctions = require ( './graph2d_types/bar' ) ;
var LineFunctions = require ( './graph2d_types/line' ) ;
var Bars = require ( './graph2d_types/bar' ) ;
var Lines = require ( './graph2d_types/line' ) ;
var Points = require ( './graph2d_types/points' ) ;
var UNGROUPED = '__ungrouped__' ; // reserved group id for ungrouped items
@ -27,11 +28,11 @@ function LineGraph(body, options) {
defaultGroup : 'default' ,
sort : true ,
sampling : true ,
stack : false ,
stack : false ,
graphHeight : '400px' ,
shaded : {
enabled : false ,
orientation : 'bottom' // top, bottom
orientation : 'bottom' // top, bottom, zero
} ,
style : 'line' , // line, bar
barChart : {
@ -56,15 +57,19 @@ function LineGraph(body, options) {
width : '40px' ,
visible : true ,
alignZeros : true ,
left : {
range : { min : undefined , max : undefined } ,
format : function ( value ) { return value ; } ,
title : { text : undefined , style : undefined }
left : {
range : { min : undefined , max : undefined } ,
format : function ( value ) {
return value ;
} ,
title : { text : undefined , style : undefined }
} ,
right : {
range : { min : undefined , max : undefined } ,
format : function ( value ) { return value ; } ,
title : { text : undefined , style : undefined }
right : {
range : { min : undefined , max : undefined } ,
format : function ( value ) {
return value ;
} ,
title : { text : undefined , style : undefined }
}
} ,
legend : {
@ -133,10 +138,10 @@ function LineGraph(body, options) {
this . setOptions ( options ) ;
this . groupsUsingDefaultStyles = [ 0 ] ;
this . COUNTER = 0 ;
this . body . emitter . on ( 'rangechanged' , function ( ) {
this . body . emitter . on ( 'rangechanged' , function ( ) {
me . lastStart = me . body . range . start ;
me . svg . style . left = util . option . asSize ( - me . props . width ) ;
me . redraw . call ( me , true ) ;
me . redraw . call ( me , true ) ;
} ) ;
// create the HTML DOM
@ -151,15 +156,15 @@ LineGraph.prototype = new Component();
/ * *
* Create the HTML DOM for the ItemSet
* /
LineGraph . prototype . _create = function ( ) {
LineGraph . prototype . _create = function ( ) {
var frame = document . createElement ( 'div' ) ;
frame . className = 'vis-line-graph' ;
this . dom . frame = frame ;
// create svg element for graph drawing.
this . svg = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'svg' ) ;
this . svg = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'svg' ) ;
this . svg . style . position = 'relative' ;
this . svg . style . height = ( '' + this . options . graphHeight ) . replace ( 'px' , '' ) + 'px' ;
this . svg . style . height = ( '' + this . options . graphHeight ) . replace ( 'px' , '' ) + 'px' ;
this . svg . style . display = 'block' ;
frame . appendChild ( this . svg ) ;
@ -182,23 +187,23 @@ LineGraph.prototype._create = function(){
* set the options of the LineGraph . the mergeOptions is used for subObjects that have an enabled element .
* @ param { object } options
* /
LineGraph . prototype . setOptions = function ( options ) {
LineGraph . prototype . setOptions = function ( options ) {
if ( options ) {
var fields = [ 'sampling' , 'defaultGroup' , 'stack' , 'height' , 'graphHeight' , 'yAxisOrientation' , 'style' , 'barChart' , 'dataAxis' , 'sort' , 'groups' ] ;
var fields = [ 'sampling' , 'defaultGroup' , 'stack' , 'height' , 'graphHeight' , 'yAxisOrientation' , 'style' , 'barChart' , 'dataAxis' , 'sort' , 'groups' ] ;
if ( options . graphHeight === undefined && options . height !== undefined && this . body . domProps . centerContainer . height !== undefined ) {
this . updateSVGheight = true ;
this . updateSVGheightOnResize = true ;
}
else if ( this . body . domProps . centerContainer . height !== undefined && options . graphHeight !== undefined ) {
if ( parseInt ( ( options . graphHeight + '' ) . replace ( "px" , '' ) ) < this . body . domProps . centerContainer . height ) {
if ( parseInt ( ( options . graphHeight + '' ) . replace ( "px" , '' ) ) < this . body . domProps . centerContainer . height ) {
this . updateSVGheight = true ;
}
}
util . selectiveDeepExtend ( fields , this . options , options ) ;
util . mergeOptions ( this . options , options , 'interpolation' ) ;
util . mergeOptions ( this . options , options , 'drawPoints' ) ;
util . mergeOptions ( this . options , options , 'shaded' ) ;
util . mergeOptions ( this . options , options , 'legend' ) ;
util . mergeOptions ( this . options , options , 'interpolation' ) ;
util . mergeOptions ( this . options , options , 'drawPoints' ) ;
util . mergeOptions ( this . options , options , 'shaded' ) ;
util . mergeOptions ( this . options , options , 'legend' ) ;
if ( options . interpolation ) {
if ( typeof options . interpolation == 'object' ) {
@ -245,7 +250,7 @@ LineGraph.prototype.setOptions = function(options) {
/ * *
* Hide the component from the DOM
* /
LineGraph . prototype . hide = function ( ) {
LineGraph . prototype . hide = function ( ) {
// remove the frame containing the items
if ( this . dom . frame . parentNode ) {
this . dom . frame . parentNode . removeChild ( this . dom . frame ) ;
@ -257,7 +262,7 @@ LineGraph.prototype.hide = function() {
* Show the component in the DOM ( when not already visible ) .
* @ return { Boolean } changed
* /
LineGraph . prototype . show = function ( ) {
LineGraph . prototype . show = function ( ) {
// show frame containing the items
if ( ! this . dom . frame . parentNode ) {
this . body . dom . center . appendChild ( this . dom . frame ) ;
@ -269,7 +274,7 @@ LineGraph.prototype.show = function() {
* Set items
* @ param { vis . DataSet | null } items
* /
LineGraph . prototype . setItems = function ( items ) {
LineGraph . prototype . setItems = function ( items ) {
var me = this ,
ids ,
oldItemsData = this . itemsData ;
@ -315,7 +320,7 @@ LineGraph.prototype.setItems = function(items) {
* Set groups
* @ param { vis . DataSet } groups
* /
LineGraph . prototype . setGroups = function ( groups ) {
LineGraph . prototype . setGroups = function ( groups ) {
var me = this ;
var ids ;
@ -362,20 +367,23 @@ LineGraph.prototype.setGroups = function(groups) {
* @ param [ ids ]
* @ private
* /
LineGraph . prototype . _onUpdate = function ( ids ) {
LineGraph . prototype . _onUpdate = function ( ids ) {
this . _updateAllGroupData ( ) ;
this . redraw ( true ) ;
} ;
LineGraph . prototype . _onAdd = function ( ids ) { this . _onUpdate ( ids ) ; } ;
LineGraph . prototype . _onRemove = function ( ids ) { this . _onUpdate ( ids ) ; } ;
LineGraph . prototype . _onUpdateGroups = function ( groupIds ) {
for ( var i = 0 ; i < groupIds . length ; i ++ ) {
var group = this . groupsData . get ( groupIds [ i ] ) ;
this . _updateGroup ( group , groupIds [ i ] ) ;
}
LineGraph . prototype . _onAdd = function ( ids ) {
this . _onUpdate ( ids ) ;
} ;
LineGraph . prototype . _onRemove = function ( ids ) {
this . _onUpdate ( ids ) ;
} ;
LineGraph . prototype . _onUpdateGroups = function ( groupIds ) {
this . _updateAllGroupData ( ) ;
this . redraw ( true ) ;
} ;
LineGraph . prototype . _onAddGroups = function ( groupIds ) { this . _onUpdateGroups ( groupIds ) ; } ;
LineGraph . prototype . _onAddGroups = function ( groupIds ) {
this . _onUpdateGroups ( groupIds ) ;
} ;
/ * *
@ -446,7 +454,9 @@ LineGraph.prototype._updateGroup = function (group, groupId) {
LineGraph . prototype . _updateAllGroupData = function ( ) {
if ( this . itemsData != null ) {
var groupsContent = { } ;
this . itemsData . get ( ) . forEach ( function ( item ) {
var items = this . itemsData . get ( ) ;
for ( var i = 0 ; i < items . length ; i ++ ) {
var item = items [ i ] ;
var groupId = item . group ;
if ( groupId === null || groupId === undefined ) {
groupId = UNGROUPED ;
@ -454,11 +464,13 @@ LineGraph.prototype._updateAllGroupData = function () {
if ( groupsContent [ groupId ] === undefined ) {
groupsContent [ groupId ] = [ ] ;
}
var extended = Object . create ( item ) ;
var extended = util . bridgeObject ( item ) ;
extended . x = util . convert ( item . x , 'Date' ) ;
extended . orginalY = item . y ; //real Y
extended . y = item . y ;
groupsContent [ groupId ] . push ( extended ) ;
} ) ;
//Update legendas and axis
}
//Update legendas, style and axis
for ( var groupId in groupsContent ) {
if ( groupsContent . hasOwnProperty ( groupId ) ) {
if ( groupsContent [ groupId ] . length == 0 ) {
@ -466,13 +478,11 @@ LineGraph.prototype._updateAllGroupData = function () {
this . _onRemoveGroups ( [ groupId ] ) ;
}
} else {
if ( ! this . groups . hasOwnProperty ( groupId ) ) {
var group = { id : groupId , content : this . options . defaultGroup } ;
if ( this . groupsData && this . groupsData . hasOwnProperty ( groupId ) ) {
group = this . groupsData [ groupId ] ;
}
this . _updateGroup ( group , groupId ) ;
var group = this . groupsData . get ( groupId ) ;
if ( group === undefined ) {
group = { id : groupId , content : this . options . defaultGroup + groupId } ;
}
this . _updateGroup ( group , groupId ) ;
this . groups [ groupId ] . setItems ( groupsContent [ groupId ] ) ;
}
}
@ -484,14 +494,14 @@ LineGraph.prototype._updateAllGroupData = function () {
* Redraw the component , mandatory function
* @ return { boolean } Returns true if the component is resized
* /
LineGraph . prototype . redraw = function ( forceGraphUpdate ) {
LineGraph . prototype . redraw = function ( forceGraphUpdate ) {
var resized = false ;
// calculate actual size and position
this . props . width = this . dom . frame . offsetWidth ;
this . props . height = this . body . domProps . centerContainer . height
- this . body . domProps . border . top
- this . body . domProps . border . bottom ;
- this . body . domProps . border . top
- this . body . domProps . border . bottom ;
// update the graph if there is no lastWidth or with, used for the initial draw
if ( this . lastWidth === undefined && this . props . width ) {
@ -510,7 +520,7 @@ LineGraph.prototype.redraw = function(forceGraphUpdate) {
// the svg element is three times as big as the width, this allows for fully dragging left and right
// without reloading the graph. the controls for this are bound to events in the constructor
if ( resized == true ) {
this . svg . style . width = util . option . asSize ( 3 * this . props . width ) ;
this . svg . style . width = util . option . asSize ( 3 * this . props . width ) ;
this . svg . style . left = util . option . asSize ( - this . props . width ) ;
// if the height of the graph is set as proportional, change the height of the svg
@ -528,7 +538,7 @@ LineGraph.prototype.redraw = function(forceGraphUpdate) {
this . updateSVGheight = false ;
}
else {
this . svg . style . height = ( '' + this . options . graphHeight ) . replace ( 'px' , '' ) + 'px' ;
this . svg . style . height = ( '' + this . options . graphHeight ) . replace ( 'px' , '' ) + 'px' ;
}
// zoomed is here to ensure that animations are shown correctly.
@ -541,7 +551,7 @@ LineGraph.prototype.redraw = function(forceGraphUpdate) {
var offset = this . body . range . start - this . lastStart ;
var range = this . body . range . end - this . body . range . start ;
if ( this . props . width != 0 ) {
var rangePerPixelInv = this . props . width / range ;
var rangePerPixelInv = this . props . width / range ;
var xOffset = offset * rangePerPixelInv ;
this . svg . style . left = ( - this . props . width - xOffset ) + 'px' ;
}
@ -583,6 +593,7 @@ LineGraph.prototype._updateGraph = function () {
var minDate = this . body . util . toGlobalTime ( - this . body . domProps . root . width ) ;
var maxDate = this . body . util . toGlobalTime ( 2 * this . body . domProps . root . width ) ;
var groupsData = { } ;
// fill groups data, this only loads the data we require based on the timewindow
this . _getRelevantData ( groupIds , groupsData , minDate , maxDate ) ;
@ -618,17 +629,64 @@ LineGraph.prototype._updateGraph = function () {
// With the yAxis scaled correctly, use this to get the Y values of the points.
for ( i = 0 ; i < groupIds . length ; i ++ ) {
group = this . groups [ groupIds [ i ] ] ;
if ( this . options . stack === true && this . options . style === 'line' && i > 0 ) {
this . _stack ( groupsData [ groupIds [ i ] ] , groupsData [ groupIds [ i - 1 ] ] ) ;
}
processedGroupData [ groupIds [ i ] ] = this . _convertYcoordinates ( groupsData [ groupIds [ i ] ] , group ) ;
}
// draw the groups
BarFunctions . draw ( groupIds , processedGroupData , this . framework ) ;
//Precalculate paths and draw shading if appropriate. This will make sure the shading is always behind any lines.
var paths = { } ;
for ( i = 0 ; i < groupIds . length ; i ++ ) {
group = this . groups [ groupIds [ i ] ] ;
if ( group . options . style != 'bar' ) { // bar needs to be drawn enmasse
group . draw ( processedGroupData [ groupIds [ i ] ] , group , this . framework ) ;
if ( group . options . style === 'line' && group . options . shaded . enabled == true ) {
var dataset = processedGroupData [ groupIds [ i ] ] ;
if ( ! paths . hasOwnProperty ( groupIds [ i ] ) ) {
paths [ groupIds [ i ] ] = Lines . calcPath ( dataset , group ) ;
}
if ( group . options . shaded . orientation === "group" ) {
var subGroupId = group . options . shaded . groupId ;
if ( groupIds . indexOf ( subGroupId ) === - 1 ) {
console . log ( "Unknown shading group target given:" + subGroupId ) ;
continue ;
}
if ( ! paths . hasOwnProperty ( subGroupId ) ) {
paths [ subGroupId ] = Lines . calcPath ( processedGroupData [ subGroupId ] , this . groups [ subGroupId ] ) ;
}
Lines . drawShading ( paths [ groupIds [ i ] ] , group , paths [ subGroupId ] , this . framework ) ;
}
else {
Lines . drawShading ( paths [ groupIds [ i ] ] , group , undefined , this . framework ) ;
}
}
}
// draw the groups, calculating paths if still necessary.
Bars . draw ( groupIds , processedGroupData , this . framework ) ;
for ( i = 0 ; i < groupIds . length ; i ++ ) {
group = this . groups [ groupIds [ i ] ] ;
if ( processedGroupData [ groupIds [ i ] ] . length > 0 ) {
switch ( group . options . style ) {
case "line" :
if ( ! paths . hasOwnProperty ( groupIds [ i ] ) ) {
paths [ groupIds [ i ] ] = Lines . calcPath ( processedGroupData [ groupIds [ i ] ] , group ) ;
}
Lines . draw ( paths [ groupIds [ i ] ] , group , this . framework ) ;
//explicit no break;
case "points" :
if ( group . options . style == "points" || group . options . drawPoints . enabled == true ) {
Points . draw ( processedGroupData [ groupIds [ i ] ] , group , this . framework ) ;
}
break ;
case "bar" :
// bar needs to be drawn enmasse
//explicit no break
default :
//do nothing...
}
}
}
}
}
}
@ -638,6 +696,51 @@ LineGraph.prototype._updateGraph = function () {
return false ;
} ;
LineGraph . prototype . _stack = function ( data , subData ) {
var index , dx , dy , subPrevPoint , subNextPoint ;
index = 0 ;
// for each data point we look for a matching on in the set below
for ( var j = 0 ; j < data . length ; j ++ ) {
subPrevPoint = undefined ;
subNextPoint = undefined ;
// we look for time matches or a before-after point
for ( var k = index ; k < subData . length ; k ++ ) {
// if times match exactly
if ( subData [ k ] . x === data [ j ] . x ) {
subPrevPoint = subData [ k ] ;
subNextPoint = subData [ k ] ;
index = k ;
break ;
}
else if ( subData [ k ] . x > data [ j ] . x ) { // overshoot
subNextPoint = subData [ k ] ;
if ( k == 0 ) {
subPrevPoint = subNextPoint ;
}
else {
subPrevPoint = subData [ k - 1 ] ;
}
index = k ;
break ;
}
}
// in case the last data point has been used, we assume it stays like this.
if ( subNextPoint === undefined ) {
subPrevPoint = subData [ subData . length - 1 ] ;
subNextPoint = subData [ subData . length - 1 ] ;
}
// linear interpolation
dx = subNextPoint . x - subPrevPoint . x ;
dy = subNextPoint . y - subPrevPoint . y ;
if ( dx == 0 ) {
data [ j ] . y = data [ j ] . orginalY + subNextPoint . y ;
}
else {
data [ j ] . y = data [ j ] . orginalY + ( dy / dx ) * ( data [ j ] . x - subPrevPoint . x ) + subPrevPoint . y ; // ax + b where b is data[j].y
}
}
}
/ * *
* first select and preprocess the data from the datasets .
@ -658,32 +761,21 @@ LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate,
for ( i = 0 ; i < groupIds . length ; i ++ ) {
group = this . groups [ groupIds [ i ] ] ;
groupsData [ groupIds [ i ] ] = [ ] ;
var dataContainer = groupsData [ groupIds [ i ] ] ;
// optimization for sorted data
if ( group . options . sort == true ) {
var dataContainer = groupsData [ groupIds [ i ] ] ;
var guess = Math . max ( 0 , util . binarySearchValue ( group . itemsData , minDate , 'x' , 'before' ) ) ;
for ( j = guess ; j < group . itemsData . length ; j ++ ) {
item = group . itemsData [ j ] ;
if ( item !== undefined ) {
if ( item . x > maxDate ) {
dataContainer . push ( item ) ;
break ;
}
else {
dataContainer . push ( item ) ;
}
dataContainer . push ( item ) ;
if ( item . x > maxDate ) {
break ;
}
}
}
else {
for ( j = 0 ; j < group . itemsData . length ; j ++ ) {
item = group . itemsData [ j ] ;
if ( item !== undefined ) {
if ( item . x > minDate && item . x < maxDate ) {
dataContainer . push ( item ) ;
}
}
}
// If unsorted data, all data is relevant, just returning entire structure
groupsData [ groupIds [ i ] ] = group . itemsData ;
}
}
}
@ -747,18 +839,22 @@ LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) {
group = this . groups [ groupIds [ i ] ] ;
// if bar graphs are stacked, their range need to be handled differently and accumulated over all groups.
if ( options . stack === true && options . style === 'bar' ) {
if ( options . yAxisOrientation === 'left' ) { combinedDataLeft = combinedDataLeft . concat ( group . getData ( groupData ) ) ; }
else { combinedDataRight = combinedDataRight . concat ( group . getData ( groupData ) ) ; }
if ( options . yAxisOrientation === 'left' ) {
combinedDataLeft = combinedDataLeft . concat ( group . getData ( groupData ) ) ;
}
else {
combinedDataRight = combinedDataRight . concat ( group . getData ( groupData ) ) ;
}
}
else {
groupRanges [ groupIds [ i ] ] = group . getYRange ( groupData , groupIds [ i ] ) ;
groupRanges [ groupIds [ i ] ] = group . getYRange ( groupData , groupIds [ i ] ) ;
}
}
}
// if bar graphs are stacked, their range need to be handled differently and accumulated over all groups.
BarFunction s . getStackedYRange ( combinedDataLeft , groupRanges , groupIds , '__barStackLeft' , 'left' ) ;
BarFunction s . getStackedYRange ( combinedDataRight , groupRanges , groupIds , '__barStackRight' , 'right' ) ;
Bars . getStackedYRange ( combinedDataLeft , groupRanges , groupIds , '__barStackLeft' , 'left' ) ;
Bars . getStackedYRange ( combinedDataRight , groupRanges , groupIds , '__barStackRight' , 'right' ) ;
// if line graphs are stacked, their range need to be handled differently and accumulated over all groups.
//LineFunctions.getStackedYRange(combinedDataLeft , groupRanges, groupIds, '__lineStackLeft' , 'left' );
//LineFunctions.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__lineStackRight', 'right');
@ -823,7 +919,7 @@ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
this . yAxisRight . setRange ( minRight , maxRight ) ;
}
}
resized = this . _toggleAxisVisiblity ( yAxisLeftUsed , this . yAxisLeft ) || resized ;
resized = this . _toggleAxisVisiblity ( yAxisLeftUsed , this . yAxisLeft ) || resized ;
resized = this . _toggleAxisVisiblity ( yAxisRightUsed , this . yAxisRight ) || resized ;
if ( yAxisRightUsed == true && yAxisLeftUsed == true ) {
@ -836,8 +932,12 @@ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
}
this . yAxisRight . master = ! yAxisLeftUsed ;
if ( this . yAxisRight . master == false ) {
if ( yAxisRightUsed == true ) { this . yAxisLeft . lineOffset = this . yAxisRight . width ; }
else { this . yAxisLeft . lineOffset = 0 ; }
if ( yAxisRightUsed == true ) {
this . yAxisLeft . lineOffset = this . yAxisRight . width ;
}
else {
this . yAxisLeft . lineOffset = 0 ;
}
resized = this . yAxisLeft . redraw ( ) || resized ;
this . yAxisRight . stepPixels = this . yAxisLeft . stepPixels ;
@ -850,9 +950,11 @@ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
}
// clean the accumulated lists
var tempGroups = [ '__barStackLeft' , '__barStackRight' , '__lineStackLeft' , '__lineStackRight' ] ;
var tempGroups = [ '__barStackLeft' , '__barStackRight' , '__lineStackLeft' , '__lineStackRight' ] ;
for ( var i = 0 ; i < tempGroups . length ; i ++ ) {
if ( groupIds . indexOf ( tempGroups [ i ] ) != - 1 ) { groupIds . splice ( groupIds . indexOf ( tempGroups [ i ] ) , 1 ) ; }
if ( groupIds . indexOf ( tempGroups [ i ] ) != - 1 ) {
groupIds . splice ( groupIds . indexOf ( tempGroups [ i ] ) , 1 ) ;
}
}
return resized ;
@ -924,7 +1026,7 @@ LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
var xValue , yValue ;
var toScreen = this . body . util . toScreen ;
var axis = this . yAxisLeft ;
var svgHeight = Number ( this . svg . style . height . replace ( 'px' , '' ) ) ;
var svgHeight = Number ( this . svg . style . height . replace ( 'px' , '' ) ) ;
if ( group . options . yAxisOrientation == 'right' ) {
axis = this . yAxisRight ;
}
@ -933,7 +1035,7 @@ LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
var labelValue = datapoints [ i ] . label ? datapoints [ i ] . label : null ;
xValue = toScreen ( datapoints [ i ] . x ) + this . props . width ;
yValue = Math . round ( axis . convertValue ( datapoints [ i ] . y ) ) ;
extractedData . push ( { x : xValue , y : yValue , label : labelValue } ) ;
extractedData . push ( { x : xValue , y : yValue , label : labelValue } ) ;
}
group . setZeroPosition ( Math . min ( svgHeight , axis . convertValue ( 0 ) ) ) ;