1 /** 2 * @file graph.js 3 * 4 * @brief 5 * The Graph is an interactive visualization chart to draw (measurement) data 6 * in time. You can freely move and zoom in the graph by dragging and scrolling 7 * in the window. The time scale on the axis is adjusted automatically, and 8 * supports scales ranging from milliseconds to years. 9 * 10 * Graph is part of the CHAP Links library. 11 * 12 * Graph is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and 13 * Internet Explorer 6+. 14 * 15 * @license 16 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 17 * use this file except in compliance with the License. You may obtain a copy 18 * of the License at 19 * 20 * http://www.apache.org/licenses/LICENSE-2.0 21 * 22 * Unless required by applicable law or agreed to in writing, software 23 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 24 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 25 * License for the specific language governing permissions and limitations under 26 * the License. 27 * 28 * Copyright (C) 2010-2013 Almende B.V. 29 * 30 * @author Jos de Jong, <jos@almende.org> 31 * @date 2013-08-20 32 * @version 1.3.2 33 */ 34 35 /** 36 * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library, 37 * "links" 38 */ 39 if (typeof links === 'undefined') { 40 links = {}; 41 // important: do not use var, as "var links = {};" will overwrite 42 // the existing links variable value with undefined in IE8, IE7. 43 } 44 45 46 /** 47 * Ensure the variable google exists 48 */ 49 if (typeof google === 'undefined') { 50 google = undefined; 51 // important: do not use var, as "var google = undefined;" will overwrite 52 // the existing google variable value with undefined in IE8, IE7. 53 } 54 55 56 /** 57 * @constructor links.Graph 58 * The Graph is a visualization Graphs on a time line 59 * 60 * Graph is developed in javascript as a Google Visualization Chart. 61 * 62 * @param {Element} container The DOM element in which the Graph will 63 * be created. Normally a div element. 64 */ 65 links.Graph = function(container) { 66 // create variables and set default values 67 this.containerElement = container; 68 this.width = "100%"; 69 this.height = "300px"; 70 this.start = null; 71 this.end = null; 72 this.autoDataStep = true; 73 this.moveable = true; 74 this.zoomable = true; 75 this.showTooltip = true; 76 77 this.redrawWhileMoving = true; 78 79 this.legend = undefined; 80 this.line = {}; // object default style for all lines 81 this.lines = []; // array containing specific line styles, colors, etc. 82 /* 83 this.defaultColors = ["red", "green", "blue", "magenta", 84 "purple", "orange", "lime", "darkgreen", "darkblue", 85 "turquoise", "gray", "darkgray", "darkred", "chocolate", 86 "plum", "#808000"]; 87 */ 88 /* 89 this.defaultColors = ["red", "#008000", "#0000FF", "#FF00FF", 90 "#800080", "#FFA500", "#00FF00", "#006400", "#00008B", 91 "#40E0D0", "#808080", "#A9A9A9", "#8B0000", "#D2691E", 92 "#DDA0DD", "#808000"]; 93 */ 94 this.defaultColors = [ 95 "#3366CC", "#DC3912", "#FF9900", "#109618", 96 "#990099", "#0099C6", "#DD4477", "#66AA00", 97 "#B82E2E", "#316395", "#994499", "#22AA99", 98 "#AAAA11", "#6633CC", "#E67300", "#8B0707"]; 99 100 // The axis is drawn from -axisMargin to frame.width+axisMargin. When making 101 // axisMargin smaller, drawing the axis is faster as the axis is shorter. 102 // this makes scrolling faster. But when moving the Graph, the Graph 103 // needs to be redrawn more often, which makes movement less smooth. 104 //this.axisMargin = document.body.clientWidth; // in pixels 105 this.axisMargin = 800; // in pixels 106 107 this.mainPadding = 8; // pixels. Todo: make option? 108 109 // create a default, empty array 110 this.data = []; 111 112 // create a frame and canvas 113 this._create(); 114 }; 115 116 117 /** 118 * Main drawing logic. This is the function that needs to be called 119 * in the html page, to draw the Graph. 120 * 121 * A data table with the events must be provided, and an options table. 122 * Available options: 123 * - width Width for the Graph in pixels or percentage. 124 * - height Height for the Graph in pixels or percentage. 125 * - start A Date object with the start date of the visible range 126 * - end A Date object with the end date of the visible range 127 * TODO: describe all options 128 * 129 * All options are optional. 130 * 131 * @param {google.visualization.DataTable | Array} data 132 * The data containing the events for the Graph. 133 * Object DataTable is defined in 134 * google.visualization.DataTable. 135 * @param {Object} options A name/value map containing settings for the 136 * Graph. 137 */ 138 links.Graph.prototype.draw = function(data, options) { 139 this._readData(data); 140 141 if (options != undefined) { 142 // retrieve parameter values 143 if (options.width != undefined) this.width = options.width; 144 if (options.height != undefined) this.height = options.height; 145 146 if (options.start != undefined) this.start = options.start; 147 if (options.end != undefined) this.end = options.end; 148 if (options.min != undefined) this.min = options.min; 149 if (options.max != undefined) this.max = options.max; 150 if (options.zoomMin != undefined) this.zoomMin = options.zoomMin; 151 if (options.zoomMax != undefined) this.zoomMax = options.zoomMax; 152 if (options.scale != undefined) this.scale = options.scale; 153 if (options.step != undefined) this.step = options.step; 154 if (options.autoDataStep != undefined) this.autoDataStep = options.autoDataStep; 155 156 if (options.moveable != undefined) this.moveable = options.moveable; 157 if (options.zoomable != undefined) this.zoomable = options.zoomable; 158 159 if (options.line != undefined) this.line = options.line; 160 if (options.lines != undefined) this.lines = options.lines; 161 162 if (options.vStart != undefined) this.vStart = options.vStart; 163 if (options.vEnd != undefined) this.vEnd = options.vEnd; 164 if (options.vMin != undefined) this.vMinFixed = options.vMin; 165 if (options.vMax != undefined) this.vMaxFixed = options.vMax; 166 if (options.vStep != undefined) this.vStepSize = options.vStep; 167 if (options.vPrettyStep != undefined) this.vPrettyStep = options.vPrettyStep; 168 if (options.vAreas != undefined) this.vAreas = options.vAreas; 169 170 if (options.legend != undefined) this.legend = options.legend; // can contain legend.width 171 if (options.tooltip != undefined) { 172 this.showTooltip = (options.tooltip != false); 173 if (typeof options.tooltip === 'function') { 174 this.tooltipFormatter = options.tooltip; 175 } 176 } 177 178 // check for deprecated options 179 if (options.intervalMin != undefined) { 180 this.zoomMin = options.intervalMin; 181 console.log('WARNING: Option intervalMin is deprecated. Use zoomMin instead'); 182 } 183 if (options.intervalMax != undefined) { 184 this.zoomMax = options.intervalMax; 185 console.log('WARNING: Option intervalMax is deprecated. Use zoomMax instead'); 186 } 187 188 // TODO: add options to set the horizontal and vertical range 189 } 190 191 // apply size and time range 192 var redrawNow = false; 193 this.setSize(this.width, this.height); 194 195 this.setVisibleChartRange(this.start, this.end, redrawNow); 196 if (this.scale && this.step) { 197 this.hStep.setScale(this.scale, this.step); 198 } 199 200 // draw the Graph 201 this.redraw(); 202 203 this.trigger('ready'); 204 }; 205 206 /** 207 * fire an event 208 * @param {String} event The name of an event, for example "rangechange" or "edit" 209 * @param {Object} params Optional parameters 210 */ 211 links.Graph.prototype.trigger = function (event, params) { 212 // fire event via the links event bus 213 links.events.trigger(this, event, params); 214 215 // fire the ready event 216 if (google && google.visualization && google.visualization.events) { 217 google.visualization.events.trigger(this, event, params); 218 } 219 }; 220 221 222 /** 223 * Read data into the graph 224 */ 225 links.Graph.prototype._readData = function(data) { 226 if (google && google.visualization && google.visualization.DataTable && 227 data instanceof google.visualization.DataTable) { 228 // read a Google DataTable 229 this.data = []; 230 231 for (var col = 1, cols = data.getNumberOfColumns(); col < cols; col++) { 232 var dataset = []; 233 for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { 234 dataset.push({"date" : data.getValue(row, 0), "value" : data.getValue(row, col)} ); 235 } 236 237 var graph = { 238 "label": data.getColumnLabel(col), 239 "type": undefined, 240 "dataRange": undefined, 241 "rowRange": undefined, 242 "visibleRowRange": undefined, 243 "data": dataset 244 }; 245 this.data.push(graph); 246 247 // TODO: sort by date, and remove redundant null values 248 } 249 } 250 else { 251 // parse Javascipt array 252 this.data = data || []; 253 } 254 255 // calculate date and value ranges 256 for (var i = 0, len = this.data.length; i < len; i++) { 257 var graph = this.data[i]; 258 259 var fields; 260 if (graph.type == 'area') { 261 fields = ['start', 'end']; // area 262 } 263 else { 264 fields = ['date']; // 'line' or 'event' 265 } 266 267 graph.dataRange = this._getDataRange(graph.data); 268 graph.rowRange = this._getRowRange(graph.data, fields); 269 } 270 }; 271 272 /** 273 * @constructor links.Graph.StepDate 274 * The class StepDate is an iterator for dates. You provide a start date and an 275 * end date. The class itself determines the best scale (step size) based on the 276 * provided start Date, end Date, and minimumStep. 277 * 278 * If minimumStep is provided, the step size is chosen as close as possible 279 * to the minimumStep but larger than minimumStep. If minimumStep is not 280 * provided, the scale is set to 1 DAY. 281 * The minimumStep should correspond with the onscreen size of about 6 characters 282 * 283 * Alternatively, you can set a scale by hand. 284 * After creation, you can initialize the class by executing start(). Then you 285 * can iterate from the start date to the end date via next(). You can check if 286 * the end date is reached with the function end(). After each step, you can 287 * retrieve the current date via get(). 288 * The class step has scales ranging from milliseconds, seconds, minutes, hours, 289 * days, to years. 290 * 291 * Version: 1.2 292 * 293 * @param {Date} start The start date, for example new Date(2010, 9, 21) 294 * or new Date(2010, 9, 21, 23, 45, 00) 295 * @param {Date} end The end date 296 * @param {Number} minimumStep Optional. Minimum step size in milliseconds 297 */ 298 links.Graph.StepDate = function(start, end, minimumStep) { 299 300 // variables 301 this.current = new Date(); 302 this._start = new Date(); 303 this._end = new Date(); 304 305 this.autoScale = true; 306 this.scale = links.Graph.StepDate.SCALE.DAY; 307 this.step = 1; 308 309 // initialize the range 310 this.setRange(start, end, minimumStep); 311 }; 312 313 /// enum scale 314 links.Graph.StepDate.SCALE = { 315 MILLISECOND: 1, 316 SECOND: 2, 317 MINUTE: 3, 318 HOUR: 4, 319 DAY: 5, 320 WEEKDAY: 6, 321 MONTH: 7, 322 YEAR: 8 323 }; 324 325 326 /** 327 * Set a new range 328 * If minimumStep is provided, the step size is chosen as close as possible 329 * to the minimumStep but larger than minimumStep. If minimumStep is not 330 * provided, the scale is set to 1 DAY. 331 * The minimumStep should correspond with the onscreen size of about 6 characters 332 * @param {Date} start The start date and time. 333 * @param {Date} end The end date and time. 334 * @param {int} minimumStep Optional. Minimum step size in milliseconds 335 */ 336 links.Graph.StepDate.prototype.setRange = function(start, end, minimumStep) { 337 if (!(start instanceof Date) || !(end instanceof Date)) { 338 //throw "No legal start or end date in method setRange"; 339 return; 340 } 341 342 this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); 343 this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); 344 345 if (this.autoScale) { 346 this.setMinimumStep(minimumStep); 347 } 348 }; 349 350 /** 351 * Set the step iterator to the start date. 352 */ 353 links.Graph.StepDate.prototype.start = function() { 354 this.current = new Date(this._start.valueOf()); 355 this.roundToMinor(); 356 }; 357 358 /** 359 * Round the current date to the first minor date value 360 * This must be executed once when the current date is set to start Date 361 */ 362 links.Graph.StepDate.prototype.roundToMinor = function() { 363 // round to floor 364 // IMPORTANT: we have no breaks in this switch! (this is no bug) 365 //noinspection FallthroughInSwitchStatementJS 366 switch (this.scale) { 367 case links.Graph.StepDate.SCALE.YEAR: 368 this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); 369 this.current.setMonth(0); 370 case links.Graph.StepDate.SCALE.MONTH: this.current.setDate(1); 371 case links.Graph.StepDate.SCALE.DAY: // intentional fall through 372 case links.Graph.StepDate.SCALE.WEEKDAY: this.current.setHours(0); 373 case links.Graph.StepDate.SCALE.HOUR: this.current.setMinutes(0); 374 case links.Graph.StepDate.SCALE.MINUTE: this.current.setSeconds(0); 375 case links.Graph.StepDate.SCALE.SECOND: this.current.setMilliseconds(0); 376 //case links.Graph.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds 377 } 378 379 if (this.step != 1) { 380 // round down to the first minor value that is a multiple of the current step size 381 switch (this.scale) { 382 case links.Graph.StepDate.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; 383 case links.Graph.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; 384 case links.Graph.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; 385 case links.Graph.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; 386 case links.Graph.StepDate.SCALE.WEEKDAY: // intentional fall through 387 case links.Graph.StepDate.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; 388 case links.Graph.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; 389 case links.Graph.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; 390 default: break; 391 } 392 } 393 }; 394 395 /** 396 * Check if the end date is reached 397 * @return {boolean} true if the current date has passed the end date 398 */ 399 links.Graph.StepDate.prototype.end = function () { 400 return (this.current.valueOf() > this._end.valueOf()); 401 }; 402 403 /** 404 * Do the next step 405 */ 406 links.Graph.StepDate.prototype.next = function() { 407 var prev = this.current.valueOf(); 408 409 // Two cases, needed to prevent issues with switching daylight savings 410 // (end of March and end of October) 411 if (this.current.getMonth() < 6) { 412 switch (this.scale) { 413 case links.Graph.StepDate.SCALE.MILLISECOND: 414 415 this.current = new Date(this.current.valueOf() + this.step); break; 416 case links.Graph.StepDate.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; 417 case links.Graph.StepDate.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; 418 case links.Graph.StepDate.SCALE.HOUR: 419 this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); 420 // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) 421 var h = this.current.getHours(); 422 this.current.setHours(h - (h % this.step)); 423 break; 424 case links.Graph.StepDate.SCALE.WEEKDAY: // intentional fall through 425 case links.Graph.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; 426 case links.Graph.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; 427 case links.Graph.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; 428 default: break; 429 } 430 } 431 else { 432 switch (this.scale) { 433 case links.Graph.StepDate.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; 434 case links.Graph.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; 435 case links.Graph.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; 436 case links.Graph.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; 437 case links.Graph.StepDate.SCALE.WEEKDAY: // intentional fall through 438 case links.Graph.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; 439 case links.Graph.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; 440 case links.Graph.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; 441 default: break; 442 } 443 } 444 445 if (this.step != 1) { 446 // round down to the correct major value 447 switch (this.scale) { 448 case links.Graph.StepDate.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; 449 case links.Graph.StepDate.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; 450 case links.Graph.StepDate.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; 451 case links.Graph.StepDate.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; 452 case links.Graph.StepDate.SCALE.WEEKDAY: // intentional fall through 453 case links.Graph.StepDate.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; 454 case links.Graph.StepDate.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; 455 case links.Graph.StepDate.SCALE.YEAR: break; // nothing to do for year 456 default: break; 457 } 458 } 459 460 // safety mechanism: if current time is still unchanged, move to the end 461 if (this.current.valueOf() == prev) { 462 this.current = new Date(this._end.valueOf()); 463 } 464 }; 465 466 467 /** 468 * Get the current datetime 469 * @return {Date} current The current date 470 */ 471 links.Graph.StepDate.prototype.getCurrent = function() { 472 return this.current; 473 }; 474 475 /** 476 * Set a custom scale. Autoscaling will be disabled. 477 * For example setScale(SCALE.MINUTES, 5) will result 478 * in minor steps of 5 minutes, and major steps of an hour. 479 * 480 * @param {links.Graph.StepDate.SCALE} newScale 481 * A scale. Choose from SCALE.MILLISECOND, 482 * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, 483 * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, 484 * SCALE.YEAR. 485 * @param {Number} newStep A step size, by default 1. Choose for 486 * example 1, 2, 5, or 10. 487 */ 488 links.Graph.StepDate.prototype.setScale = function(newScale, newStep) { 489 this.scale = newScale; 490 491 if (newStep > 0) { 492 this.step = newStep; 493 } 494 495 this.autoScale = false; 496 }; 497 498 /** 499 * Enable or disable autoscaling 500 * @param {boolean} enable If true, autoascaling is set true 501 */ 502 links.Graph.StepDate.prototype.setAutoScale = function (enable) { 503 this.autoScale = enable; 504 }; 505 506 507 /** 508 * Automatically determine the scale that bests fits the provided minimum step 509 * @param {Number} minimumStep The minimum step size in milliseconds 510 */ 511 links.Graph.StepDate.prototype.setMinimumStep = function(minimumStep) { 512 if (minimumStep == undefined) { 513 return; 514 } 515 516 var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); 517 var stepMonth = (1000 * 60 * 60 * 24 * 30); 518 var stepDay = (1000 * 60 * 60 * 24); 519 var stepHour = (1000 * 60 * 60); 520 var stepMinute = (1000 * 60); 521 var stepSecond = (1000); 522 var stepMillisecond= (1); 523 524 // find the smallest step that is larger than the provided minimumStep 525 if (stepYear*1000 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.YEAR; this.step = 1000;} 526 if (stepYear*500 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.YEAR; this.step = 500;} 527 if (stepYear*100 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.YEAR; this.step = 100;} 528 if (stepYear*50 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.YEAR; this.step = 50;} 529 if (stepYear*10 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.YEAR; this.step = 10;} 530 if (stepYear*5 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.YEAR; this.step = 5;} 531 if (stepYear > minimumStep) {this.scale = links.Graph.StepDate.SCALE.YEAR; this.step = 1;} 532 if (stepMonth*3 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MONTH; this.step = 3;} 533 if (stepMonth > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MONTH; this.step = 1;} 534 if (stepDay*5 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.DAY; this.step = 5;} 535 if (stepDay*2 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.DAY; this.step = 2;} 536 if (stepDay > minimumStep) {this.scale = links.Graph.StepDate.SCALE.DAY; this.step = 1;} 537 if (stepDay/2 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.WEEKDAY; this.step = 1;} 538 if (stepHour*4 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.HOUR; this.step = 4;} 539 if (stepHour > minimumStep) {this.scale = links.Graph.StepDate.SCALE.HOUR; this.step = 1;} 540 if (stepMinute*15 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MINUTE; this.step = 15;} 541 if (stepMinute*10 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MINUTE; this.step = 10;} 542 if (stepMinute*5 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MINUTE; this.step = 5;} 543 if (stepMinute > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MINUTE; this.step = 1;} 544 if (stepSecond*15 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.SECOND; this.step = 15;} 545 if (stepSecond*10 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.SECOND; this.step = 10;} 546 if (stepSecond*5 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.SECOND; this.step = 5;} 547 if (stepSecond > minimumStep) {this.scale = links.Graph.StepDate.SCALE.SECOND; this.step = 1;} 548 if (stepMillisecond*200 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 200;} 549 if (stepMillisecond*100 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 100;} 550 if (stepMillisecond*50 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 50;} 551 if (stepMillisecond*10 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 10;} 552 if (stepMillisecond*5 > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 5;} 553 if (stepMillisecond > minimumStep) {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 1;} 554 }; 555 556 /** 557 * Snap a date to a rounded value. The snap intervals are dependent on the 558 * current scale and step. 559 * @param {Date} date the date to be snapped 560 */ 561 links.Graph.StepDate.prototype.snap = function(date) { 562 if (this.scale == links.Graph.StepDate.SCALE.YEAR) { 563 var year = date.getFullYear() + Math.round(date.getMonth() / 12); 564 date.setFullYear(Math.round(year / this.step) * this.step); 565 date.setMonth(0); 566 date.setDate(0); 567 date.setHours(0); 568 date.setMinutes(0); 569 date.setSeconds(0); 570 date.setMilliseconds(0); 571 } 572 else if (this.scale == links.Graph.StepDate.SCALE.MONTH) { 573 if (date.getDate() > 15) { 574 date.setDate(1); 575 date.setMonth(date.getMonth() + 1); 576 // important: first set Date to 1, after that change the month. 577 } 578 else { 579 date.setDate(1); 580 } 581 582 date.setHours(0); 583 date.setMinutes(0); 584 date.setSeconds(0); 585 date.setMilliseconds(0); 586 } 587 else if (this.scale == links.Graph.StepDate.SCALE.DAY || 588 this.scale == links.Graph.StepDate.SCALE.WEEKDAY) { 589 switch (this.step) { 590 case 5: 591 case 2: 592 date.setHours(Math.round(date.getHours() / 24) * 24); break; 593 default: 594 date.setHours(Math.round(date.getHours() / 12) * 12); break; 595 } 596 date.setMinutes(0); 597 date.setSeconds(0); 598 date.setMilliseconds(0); 599 } 600 else if (this.scale == links.Graph.StepDate.SCALE.HOUR) { 601 switch (this.step) { 602 case 4: 603 date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break; 604 default: 605 date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break; 606 } 607 date.setSeconds(0); 608 date.setMilliseconds(0); 609 } else if (this.scale == links.Graph.StepDate.SCALE.MINUTE) { 610 switch (this.step) { 611 case 15: 612 case 10: 613 date.setMinutes(Math.round(date.getMinutes() / 5) * 5); 614 date.setSeconds(0); 615 break; 616 case 5: 617 date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break; 618 default: 619 date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break; 620 } 621 date.setMilliseconds(0); 622 } 623 else if (this.scale == links.Graph.StepDate.SCALE.SECOND) { 624 switch (this.step) { 625 case 15: 626 case 10: 627 date.setSeconds(Math.round(date.getSeconds() / 5) * 5); 628 date.setMilliseconds(0); 629 break; 630 case 5: 631 date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break; 632 default: 633 date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break; 634 } 635 } 636 else if (this.scale == links.Graph.StepDate.SCALE.MILLISECOND) { 637 var step = this.step > 5 ? this.step / 2 : 1; 638 date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); 639 } 640 }; 641 642 /** 643 * Check if the current step is a major step (for example when the step 644 * is DAY, a major step is each first day of the MONTH) 645 * @return {boolean} true if current date is major, else false. 646 */ 647 links.Graph.StepDate.prototype.isMajor = function() { 648 switch (this.scale) { 649 case links.Graph.StepDate.SCALE.MILLISECOND: 650 return (this.current.getMilliseconds() == 0); 651 case links.Graph.StepDate.SCALE.SECOND: 652 return (this.current.getSeconds() == 0); 653 case links.Graph.StepDate.SCALE.MINUTE: 654 return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); 655 // Note: this is no bug. Major label is equal for both minute and hour scale 656 case links.Graph.StepDate.SCALE.HOUR: 657 return (this.current.getHours() == 0); 658 case links.Graph.StepDate.SCALE.WEEKDAY: // intentional fall through 659 case links.Graph.StepDate.SCALE.DAY: 660 return (this.current.getDate() == 1); 661 case links.Graph.StepDate.SCALE.MONTH: 662 return (this.current.getMonth() == 0); 663 case links.Graph.StepDate.SCALE.YEAR: 664 return false; 665 default: 666 return false; 667 } 668 }; 669 670 671 /** 672 * Returns formatted text for the minor axislabel, depending on the current 673 * date and the scale. For example when scale is MINUTE, the current time is 674 * formatted as "hh:mm". 675 * @param {Date} [date] custom date. if not provided, current date is taken 676 */ 677 links.Graph.StepDate.prototype.getLabelMinor = function(date) { 678 var MONTHS_SHORT = ["Jan", "Feb", "Mar", 679 "Apr", "May", "Jun", 680 "Jul", "Aug", "Sep", 681 "Oct", "Nov", "Dec"]; 682 var DAYS_SHORT = ["Sun", "Mon", "Tue", 683 "Wed", "Thu", "Fri", "Sat"]; 684 685 if (date == undefined) { 686 date = this.current; 687 } 688 689 switch (this.scale) { 690 case links.Graph.StepDate.SCALE.MILLISECOND: return String(date.getMilliseconds()); 691 case links.Graph.StepDate.SCALE.SECOND: return String(date.getSeconds()); 692 case links.Graph.StepDate.SCALE.MINUTE: 693 return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2); 694 case links.Graph.StepDate.SCALE.HOUR: 695 return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2); 696 case links.Graph.StepDate.SCALE.WEEKDAY: return DAYS_SHORT[date.getDay()] + ' ' + date.getDate(); 697 case links.Graph.StepDate.SCALE.DAY: return String(date.getDate()); 698 case links.Graph.StepDate.SCALE.MONTH: return MONTHS_SHORT[date.getMonth()]; // month is zero based 699 case links.Graph.StepDate.SCALE.YEAR: return String(date.getFullYear()); 700 default: return ""; 701 } 702 }; 703 704 705 /** 706 * Returns formatted text for the major axislabel, depending on the current 707 * date and the scale. For example when scale is MINUTE, the major scale is 708 * hours, and the hour will be formatted as "hh". 709 * @param {Date} [date] custom date. if not provided, current date is taken 710 */ 711 links.Graph.StepDate.prototype.getLabelMajor = function(date) { 712 var MONTHS = ["January", "February", "March", 713 "April", "May", "June", 714 "July", "August", "September", 715 "October", "November", "December"]; 716 var DAYS = ["Sunday", "Monday", "Tuesday", 717 "Wednesday", "Thursday", "Friday", "Saturday"]; 718 719 if (date == undefined) { 720 date = this.current; 721 } 722 723 switch (this.scale) { 724 case links.Graph.StepDate.SCALE.MILLISECOND: 725 return this.addZeros(date.getHours(), 2) + ":" + 726 this.addZeros(date.getMinutes(), 2) + ":" + 727 this.addZeros(date.getSeconds(), 2); 728 case links.Graph.StepDate.SCALE.SECOND: 729 return date.getDate() + " " + 730 MONTHS[date.getMonth()] + " " + 731 this.addZeros(date.getHours(), 2) + ":" + 732 this.addZeros(date.getMinutes(), 2); 733 case links.Graph.StepDate.SCALE.MINUTE: 734 return DAYS[date.getDay()] + " " + 735 date.getDate() + " " + 736 MONTHS[date.getMonth()] + " " + 737 date.getFullYear(); 738 case links.Graph.StepDate.SCALE.HOUR: 739 return DAYS[date.getDay()] + " " + 740 date.getDate() + " " + 741 MONTHS[date.getMonth()] + " " + 742 date.getFullYear(); 743 case links.Graph.StepDate.SCALE.WEEKDAY: 744 case links.Graph.StepDate.SCALE.DAY: 745 return MONTHS[date.getMonth()] + " " + 746 date.getFullYear(); 747 case links.Graph.StepDate.SCALE.MONTH: 748 return String(date.getFullYear()); 749 default: 750 return ""; 751 } 752 }; 753 754 /** 755 * Add leading zeros to the given value to match the desired length. 756 * For example addZeros(123, 5) returns "00123" 757 * @param {int} value A value 758 * @param {int} len Desired final length 759 * @return {string} value with leading zeros 760 */ 761 links.Graph.StepDate.prototype.addZeros = function(value, len) { 762 var str = "" + value; 763 while (str.length < len) { 764 str = "0" + str; 765 } 766 return str; 767 }; 768 769 770 771 /** 772 * @class StepNumber 773 * The class StepNumber is an iterator for numbers. You provide a start and end 774 * value, and a best step size. StepNumber itself rounds to fixed values and 775 * a finds the step that best fits the provided step. 776 * 777 * If prettyStep is true, the step size is chosen as close as possible to the 778 * provided step, but being a round value like 1, 2, 5, 10, 20, 50, .... 779 * 780 * Example usage: 781 * var step = new links.Graph.StepNumber(0, 10, 2.5, true); 782 * step.start(); 783 * while (!step.end()) { 784 * alert(step.getCurrent()); 785 * step.next(); 786 * } 787 * 788 * Version: 1.0 789 * 790 * @param {number} start The start value 791 * @param {number} end The end value 792 * @param {number} step Optional. Step size. Must be a positive value. 793 * @param {boolean} prettyStep Optional. If true, the step size is rounded 794 * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) 795 */ 796 links.Graph.StepNumber = function (start, end, step, prettyStep) { 797 this._start = 0; 798 this._end = 0; 799 this._step = 1; 800 this.prettyStep = true; 801 this.precision = 5; 802 803 this._current = 0; 804 this._setRange(start, end, step, prettyStep); 805 }; 806 807 /** 808 * Set a new range: start, end and step. 809 * 810 * @param {number} start The start value 811 * @param {number} end The end value 812 * @param {number} step Optional. Step size. Must be a positive value. 813 * @param {boolean} prettyStep Optional. If true, the step size is rounded 814 * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) 815 */ 816 links.Graph.StepNumber.prototype._setRange = function(start, end, step, prettyStep) { 817 this._start = start ? start : 0; 818 this._end = end ? end : 0; 819 820 this.setStep(step, prettyStep); 821 }; 822 823 /** 824 * Set a new step size 825 * @param {number} step New step size. Must be a positive value 826 * @param {boolean} prettyStep Optional. If true, the provided step is rounded 827 * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...) 828 */ 829 links.Graph.StepNumber.prototype.setStep = function(step, prettyStep) { 830 if (step == undefined || step <= 0) 831 return; 832 833 this.prettyStep = prettyStep; 834 if (this.prettyStep == true) 835 this._step = links.Graph.StepNumber._calculatePrettyStep(step); 836 else 837 this._step = step; 838 839 840 if (this._end / this._step > Math.pow(10, this.precision)) { 841 this.precision = undefined; 842 } 843 }; 844 845 /** 846 * Calculate a nice step size, closest to the desired step size. 847 * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an 848 * integer number. For example 1, 2, 5, 10, 20, 50, etc... 849 * @param {number} step Desired step size 850 * @return {number} Nice step size 851 */ 852 links.Graph.StepNumber._calculatePrettyStep = function (step) { 853 log10 = function (x) {return Math.log(x) / Math.LN10;}; 854 855 // try three steps (multiple of 1, 2, or 5 856 var step1 = 1 * Math.pow(10, Math.round(log10(step / 1))); 857 var step2 = 2 * Math.pow(10, Math.round(log10(step / 2))); 858 var step5 = 5 * Math.pow(10, Math.round(log10(step / 5))); 859 860 // choose the best step (closest to minimum step) 861 var prettyStep = step1; 862 if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; 863 if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; 864 865 // for safety 866 if (prettyStep <= 0) { 867 prettyStep = 1; 868 } 869 870 return prettyStep; 871 }; 872 873 /** 874 * returns the current value of the step 875 * @return {number} current value 876 */ 877 links.Graph.StepNumber.prototype.getCurrent = function () { 878 if (this.precision) { 879 return Number((this._current).toPrecision(this.precision)); 880 } 881 else { 882 return this._current; 883 } 884 }; 885 886 /** 887 * returns the current step size 888 * @return {number} current step size 889 */ 890 links.Graph.StepNumber.prototype.getStep = function () { 891 return this._step; 892 }; 893 894 /** 895 * Set the current value to the largest value smaller than start, which 896 * is a multiple of the step size 897 */ 898 links.Graph.StepNumber.prototype.start = function() { 899 if (this.prettyStep) 900 this._current = this._start - this._start % this._step; 901 else 902 this._current = this._start; 903 }; 904 905 /** 906 * Do a step, add the step size to the current value 907 */ 908 links.Graph.StepNumber.prototype.next = function () { 909 this._current += this._step; 910 }; 911 912 /** 913 * Returns true whether the end is reached 914 * @return {boolean} True if the current value has passed the end value. 915 */ 916 links.Graph.StepNumber.prototype.end = function () { 917 return (this._current > this._end); 918 }; 919 920 921 /** 922 * Set a custom scale. Autoscaling will be disabled. 923 * For example setScale(SCALE.MINUTES, 5) will result 924 * in minor steps of 5 minutes, and major steps of an hour. 925 * 926 * @param {links.Graph.StepDate.SCALE} scale 927 * A scale. Choose from SCALE.MILLISECOND, 928 * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, 929 * SCALE.DAY, SCALE.MONTH, SCALE.YEAR. 930 * @param {int} step A step size, by default 1. Choose for 931 * example 1, 2, 5, or 10. 932 */ 933 links.Graph.prototype.setScale = function(scale, step) { 934 this.hStep.setScale(scale, step); 935 this.redraw(); 936 }; 937 938 /** 939 * Enable or disable autoscaling 940 * @param {boolean} enable If true or not defined, autoscaling is enabled. 941 * If false, autoscaling is disabled. 942 */ 943 links.Graph.prototype.setAutoScale = function(enable) { 944 this.hStep.setAutoScale(enable); 945 this.redraw(); 946 }; 947 948 949 /** 950 * Append suffix "px" to provided value x 951 * @param {int} x An integer value 952 * @return {string} the string value of x, followed by the suffix "px" 953 */ 954 links.Graph.px = function(x) { 955 return Math.round(x) + "px"; 956 }; 957 958 959 /** 960 * Calculate the factor and offset to convert a position on screen to the 961 * corresponding date and vice versa. 962 * After the method calcConversionFactor is executed once, the methods screenToTime and 963 * timeToScreen can be used. 964 */ 965 links.Graph.prototype._calcConversionFactor = function() { 966 this.ttsOffset = this.start.valueOf(); 967 this.ttsFactor = this.frame.clientWidth / 968 (this.end.valueOf() - this.start.valueOf()); 969 }; 970 971 972 /** 973 * Convert a position on screen (pixels) to a datetime 974 * Before this method can be used, the method calcConversionFactor must be 975 * executed once. 976 * @param {int} x Position on the screen in pixels 977 * @return {Date} time The datetime the corresponds with given position x 978 */ 979 links.Graph.prototype._screenToTime = function(x) { 980 return new Date(x / this.ttsFactor + this.ttsOffset); 981 }; 982 983 /** 984 * Convert a datetime (Date object) into a position on the screen 985 * Before this method can be used, the method calcConversionFactor must be 986 * executed once. 987 * @param {Date} time A date 988 * @return {int} x The position on the screen in pixels which corresponds 989 * with the given date. 990 */ 991 links.Graph.prototype.timeToScreen = function(time) { 992 return (time.valueOf() - this.ttsOffset) * this.ttsFactor || null; 993 }; 994 995 /** 996 * Create the main frame for the Graph. 997 * This function is executed once when a Graph object is created. The frame 998 * contains a canvas, and this canvas contains all objects like the axis and 999 * events. 1000 */ 1001 links.Graph.prototype._create = function () { 1002 // remove all elements from the container element. 1003 while (this.containerElement.hasChildNodes()) { 1004 this.containerElement.removeChild(this.containerElement.firstChild); 1005 } 1006 1007 this.main = document.createElement("DIV"); 1008 this.main.className = "graph-frame"; 1009 this.main.style.position = "relative"; 1010 this.main.style.overflow = "hidden"; 1011 this.containerElement.appendChild(this.main); 1012 1013 // create the main box where the Graph will be created 1014 this.frame = document.createElement("DIV"); 1015 this.frame.style.overflow = "hidden"; 1016 this.frame.style.position = "relative"; 1017 this.frame.style.height = "200px"; // height MUST be initialized. 1018 // Width and height will be set via setSize(); 1019 //this.containerElement.appendChild(this.frame); 1020 this.main.appendChild(this.frame); 1021 1022 // create a canvas background, which can be used to give the canvas a colored background 1023 this.frame.background = document.createElement("DIV"); 1024 this.frame.background.className = "graph-canvas"; 1025 this.frame.background.style.position = "relative"; 1026 this.frame.background.style.left = links.Graph.px(0); 1027 this.frame.background.style.top = links.Graph.px(0); 1028 this.frame.background.style.width = "100%"; 1029 this.frame.appendChild(this.frame.background); 1030 1031 // create a div to contain the grid lines of the vertical axis 1032 this.frame.vgrid = document.createElement("DIV"); 1033 this.frame.vgrid.className = "graph-axis-grid"; 1034 this.frame.vgrid.style.position = "absolute"; 1035 this.frame.vgrid.style.left = links.Graph.px(0); 1036 this.frame.vgrid.style.top = links.Graph.px(0); 1037 this.frame.vgrid.style.width = "100%"; 1038 this.frame.appendChild(this.frame.vgrid); 1039 1040 // create the canvas inside the frame. all elements will be added to this 1041 // canvas 1042 this.frame.canvas = document.createElement("DIV"); 1043 //this.frame.canvas.className = "graph-canvas"; 1044 this.frame.canvas.style.position = "absolute"; 1045 this.frame.canvas.style.left = links.Graph.px(0); 1046 this.frame.canvas.style.top = links.Graph.px(0); 1047 this.frame.appendChild(this.frame.canvas); 1048 // Width and height will be set via setSize(); 1049 1050 // inside the canvas, create a DOM element "axis" to store all axis related elements 1051 this.frame.canvas.axis = document.createElement("DIV"); 1052 this.frame.canvas.axis.style.position = "relative"; 1053 this.frame.canvas.axis.style.left = links.Graph.px(0); 1054 this.frame.canvas.axis.style.top = links.Graph.px(0); 1055 this.frame.canvas.appendChild(this.frame.canvas.axis); 1056 this.majorLabels = []; 1057 1058 // create the graph canvas (HTML canvas element) 1059 this.frame.canvas.graph = document.createElement( "canvas" ); 1060 this.frame.canvas.graph.style.position = "absolute"; 1061 this.frame.canvas.graph.style.left = links.Graph.px(0); 1062 this.frame.canvas.graph.style.top = links.Graph.px(0); 1063 //this.frame.canvas.graph.width = "800"; // width is adjusted lateron 1064 //this.frame.canvas.graph.height = "200"; // height is adjusted lateron 1065 this.frame.canvas.appendChild(this.frame.canvas.graph); 1066 1067 isIE = (/MSIE/.test(navigator.userAgent) && !window.opera); 1068 if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) { 1069 this.frame.canvas.graph = G_vmlCanvasManager.initElement(this.frame.canvas.graph); 1070 } 1071 1072 // add event listeners to handle moving and zooming the contents 1073 var me = this; 1074 var onmousedown = function (event) {me._onMouseDown(event);}; 1075 var onmousewheel = function (event) {me._onWheel(event);}; 1076 var ontouchstart = function (event) {me._onTouchStart(event);}; 1077 if (this.showTooltip) { 1078 var onmouseout = function (event) {me._onMouseOut(event);}; 1079 var onmousehover = function (event) {me._onMouseHover(event);}; 1080 } 1081 1082 // TODO: these events are never cleaned up... can give a "memory leakage"? 1083 links.Graph.addEventListener(this.frame, "mousedown", onmousedown); 1084 links.Graph.addEventListener(this.frame, "mousemove", onmousehover); 1085 links.Graph.addEventListener(this.frame, "mouseout", onmouseout); 1086 links.Graph.addEventListener(this.frame, "mousewheel", onmousewheel); 1087 links.Graph.addEventListener(this.frame, "touchstart", ontouchstart); 1088 links.Graph.addEventListener(this.frame, "mousedown", function() {me._checkSize();}); 1089 1090 // create a step for drawing the horizontal and vertical axis 1091 this.hStep = new links.Graph.StepDate(); // TODO: rename step to hStep 1092 this.vStep = new links.Graph.StepNumber(); 1093 1094 // the array events contains pointers to all data events. It is used 1095 // to sort and stack the events. 1096 this.eventsSorted = []; 1097 }; 1098 1099 1100 /** 1101 * Set a new size for the graph 1102 * @param {string} width Width in pixels or percentage (for example "800px" 1103 * or "50%") 1104 * @param {string} height Height in pixels or percentage (for example "400px" 1105 * or "30%") 1106 */ 1107 links.Graph.prototype.setSize = function(width, height) { 1108 // TODO: test if this solves the width as percentage problem in EXT-GWT 1109 this.containerElement.style.width = width; 1110 this.containerElement.style.height = height; 1111 1112 this.main.style.width = width; 1113 this.main.style.height = height; 1114 1115 this.frame.style.width = links.Graph.px(this.main.clientWidth); 1116 this.frame.style.height = links.Graph.px(this.main.clientHeight); 1117 1118 this.frame.canvas.style.width = links.Graph.px(this.frame.clientWidth); 1119 this.frame.canvas.style.height = links.Graph.px(this.frame.clientHeight); 1120 }; 1121 1122 /** 1123 * Zoom the graph the given zoomfactor in or out. Start and end date will 1124 * be adjusted, and the graph will be redrawn. You can optionally give a 1125 * date around which to zoom. 1126 * For example, try zoomfactor = 0.1 or -0.1 1127 * @param {Number} zoomFactor Zooming amount. Positive value will zoom in, 1128 * negative value will zoom out 1129 * @param {Date} zoomAroundDate Date around which will be zoomed. Optional 1130 */ 1131 links.Graph.prototype._zoom = function(zoomFactor, zoomAroundDate) { 1132 // if zoomAroundDate is not provided, take it half between start Date and end Date 1133 if (zoomAroundDate == undefined) 1134 zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2); 1135 1136 // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will 1137 // result in a start>=end ) 1138 if (zoomFactor >= 1) zoomFactor = 0.9; 1139 if (zoomFactor <= -1) zoomFactor = -0.9; 1140 1141 // adjust a negative factor such that zooming in with 0.1 equals zooming 1142 // out with a factor -0.1 1143 if (zoomFactor < 0) { 1144 zoomFactor = zoomFactor / (1 + zoomFactor); 1145 } 1146 1147 // zoom start Date and end Date relative to the zoomAroundDate 1148 var startDiff = parseFloat(this.start.valueOf() - zoomAroundDate.valueOf()); 1149 var endDiff = parseFloat(this.end.valueOf() - zoomAroundDate.valueOf()); 1150 1151 // calculate new dates 1152 var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor); 1153 var newEnd = new Date(this.end.valueOf() - endDiff * zoomFactor); 1154 1155 /* TODO: cleanup 1156 // prevent scale of less than 10 milliseconds 1157 // TODO: IE has problems with milliseconds 1158 if (zoomFactor > 0 && (newEnd.valueOf() - newStart.valueOf()) < 10) 1159 return; 1160 1161 // prevent scale of mroe than than 10 thousand years 1162 if (zoomFactor < 0 && (newEnd.getFullYear() - newStart.getFullYear()) > 10000) 1163 return; 1164 1165 // apply new dates 1166 this.start = newStart; 1167 this.end = newEnd; 1168 */ 1169 1170 var interval = (newEnd.valueOf() - newStart.valueOf()); 1171 var zoomMin = Number(this.zoomMin) || 10; 1172 if (zoomMin < 10) { 1173 zoomMin = 10; 1174 } 1175 if (interval >= zoomMin) { 1176 // apply new dates 1177 this._applyRange(newStart, newEnd, zoomAroundDate); 1178 1179 this._redrawHorizontalAxis(); 1180 this._redrawData(); 1181 this._redrawDataTooltip(); 1182 } 1183 }; 1184 1185 /** 1186 * Move the graph the given movefactor to the left or right. Start and end 1187 * date will be adjusted, and the graph will be redrawn. 1188 * For example, try moveFactor = 0.1 or -0.1 1189 * @param {Number} moveFactor Moving amount. Positive value will move right, 1190 * negative value will move left 1191 */ 1192 links.Graph.prototype._move = function(moveFactor) { 1193 // TODO: test this function again 1194 // zoom start Date and end Date relative to the zoomAroundDate 1195 var diff = parseFloat(this.end.valueOf() - this.start.valueOf()); 1196 1197 // apply new dates 1198 var newStart = new Date(this.start.valueOf() + diff * moveFactor); 1199 var newEnd = new Date(this.end.valueOf() + diff * moveFactor); 1200 1201 this._applyRange(newStart, newEnd); 1202 1203 // redraw 1204 this._redrawHorizontalAxis(); 1205 this._redrawData(); 1206 }; 1207 1208 1209 /** 1210 * Apply a visible range. The range is limited to feasible maximum and minimum 1211 * range. 1212 * @param {Date} start 1213 * @param {Date} end 1214 * @param {Date} zoomAroundDate Optional. Date around which will be zoomed 1215 * When needed to satisfy a min/max zoom level 1216 * or range. 1217 */ 1218 links.Graph.prototype._applyRange = function (start, end, zoomAroundDate) { 1219 // calculate new start and end value 1220 var startValue = start.valueOf(); 1221 var endValue = end.valueOf(); 1222 var interval = (endValue - startValue); 1223 1224 // determine maximum and minimum interval 1225 var year = 1000 * 60 * 60 * 24 * 365; 1226 var zoomMin = Number(this.zoomMin) || 10; 1227 if (zoomMin < 10) { 1228 zoomMin = 10; 1229 } 1230 var zoomMax = Number(this.zoomMax) || 10000 * year; 1231 if (zoomMax > 10000 * year) { 1232 zoomMax = 10000 * year; 1233 } 1234 if (zoomMax < zoomMin) { 1235 zoomMax = zoomMin; 1236 } 1237 1238 // determine min and max date value 1239 var min = this.min ? this.min.valueOf() : undefined; 1240 var max = this.max ? this.max.valueOf() : undefined; 1241 if (min && max) { 1242 if (min >= max) { 1243 // empty range 1244 var day = 1000 * 60 * 60 * 24; 1245 max = min + day; 1246 } 1247 if (zoomMax > (max - min)) { 1248 zoomMax = (max - min); 1249 } 1250 if (zoomMin > (max - min)) { 1251 zoomMin = (max - min); 1252 } 1253 } 1254 1255 // prevent empty interval 1256 if (startValue >= endValue) { 1257 endValue += 1000 * 60 * 60 * 24; 1258 } 1259 1260 // prevent too small scale 1261 // TODO: IE has problems with milliseconds 1262 if (interval < zoomMin) { 1263 var diff = (zoomMin - interval); 1264 var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5; 1265 startValue -= Math.round(diff * f); 1266 endValue += Math.round(diff * (1 - f)); 1267 } 1268 1269 // prevent too large scale 1270 if (interval > zoomMax) { 1271 var diff = (interval - zoomMax); 1272 var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5; 1273 startValue += Math.round(diff * f); 1274 endValue -= Math.round(diff * (1 - f)); 1275 } 1276 1277 // prevent to small start date 1278 if (min) { 1279 var diff = (startValue - min); 1280 if (diff < 0) { 1281 startValue -= diff; 1282 endValue -= diff; 1283 } 1284 } 1285 1286 // prevent to large end date 1287 if (max) { 1288 var diff = (max - endValue); 1289 if (diff < 0) { 1290 startValue += diff; 1291 endValue += diff; 1292 } 1293 } 1294 1295 // apply new dates 1296 this.start = new Date(startValue); 1297 this.end = new Date(endValue); 1298 }; 1299 1300 1301 /** 1302 * Zoom the graph vertically. The vertical range will be adjusted, and the graph 1303 * will be redrawn. You can optionally give a value around which to zoom. 1304 * For example, try zoomfactor = 0.1 or -0.1 1305 * @param {Number} zoomFactor Zooming amount. Positive value will zoom in, 1306 * negative value will zoom out 1307 * @param {Date} zoomAroundValue Value around which will be zoomed. Optional 1308 */ 1309 links.Graph.prototype._zoomVertical = function(zoomFactor, zoomAroundValue) { 1310 if (zoomAroundValue == undefined) 1311 zoomAroundValue = (this.vStart + this.vEnd) / 2; 1312 1313 // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will 1314 // result in a start>=end ) 1315 if (zoomFactor >= 1) zoomFactor = 0.9; 1316 if (zoomFactor <= -1) zoomFactor = -0.9; 1317 1318 // adjust a negative factor such that zooming in with 0.1 equals zooming 1319 // out with a factor -0.1 1320 if (zoomFactor < 0) { 1321 zoomFactor = zoomFactor / (1 + zoomFactor); 1322 } 1323 1324 // zoom start Date and end Date relative to the zoomAroundDate 1325 var startDiff = (this.vStart - zoomAroundValue); 1326 var endDiff = (this.vEnd - zoomAroundValue); 1327 1328 // calculate start and end 1329 var newStart = (this.vStart - startDiff * zoomFactor); 1330 var newEnd = (this.vEnd - endDiff * zoomFactor); 1331 1332 // prevent empty range 1333 if (newStart >= newEnd) { 1334 return; 1335 } 1336 1337 // prevent range larger than the available range 1338 if (newStart < this.vMin) { 1339 newStart = this.vMin; 1340 } 1341 if (newEnd > this.vMax) { 1342 newEnd = this.vMax; 1343 } 1344 /* TODO: allow start and end larger than the value range? 1345 if (newStart < this.vMin && newStart < this.vStart) { 1346 newStart = (this.vStart > this.vMin) ? this.vMin : this.vStart; 1347 } 1348 if (newEnd > this.vMax && newEnd > this.vEnd) { 1349 newEnd = (this.vEnd < this.vMax) ? this.vMax : this.vEnd; 1350 } 1351 */ 1352 1353 // apply new range 1354 this.vStart = newStart; 1355 this.vEnd = newEnd; 1356 1357 // redraw 1358 this._redrawVerticalAxis(); 1359 this._redrawHorizontalAxis(); // -> width of the vertical axis can be changed 1360 this._redrawData(); 1361 this._redrawDataTooltip(); 1362 }; 1363 1364 1365 /** 1366 * Redraw the Graph. This needs to be executed after the start and/or 1367 * end time are changed, or when data is added or removed dynamically. 1368 */ 1369 links.Graph.prototype.redraw = function() { 1370 this._initSize(); 1371 1372 // Note: the order of drawing is important! 1373 this._redrawLegend(); 1374 this._redrawVerticalAxis(); 1375 this._redrawHorizontalAxis(); 1376 this._redrawData(); 1377 this._redrawDataTooltip(); 1378 1379 // store the current width and height. This is needed to detect when the frame 1380 // was resized (externally). 1381 this.lastMainWidth = this.main.clientWidth; 1382 this.lastMainHeight = this.main.clientHeight; 1383 }; 1384 1385 /** 1386 * Initialize size, range, location of axis 1387 * Execute this method when the data or options are changed, before redrawing 1388 * the graph. 1389 */ 1390 links.Graph.prototype._initSize = function() { 1391 // calculate the width and height of a single character 1392 // this is used to calculate the step size, and also the positioning of the 1393 // axis 1394 var charText = document.createTextNode("0"); 1395 var charDiv = document.createElement("DIV"); 1396 charDiv.className = "graph-axis-text"; 1397 charDiv.appendChild(charText); 1398 charDiv.style.position = "absolute"; 1399 charDiv.style.visibility = "hidden"; 1400 charDiv.style.padding = "0px"; 1401 this.frame.canvas.axis.appendChild(charDiv); 1402 this.axisCharWidth = parseInt(charDiv.clientWidth); 1403 this.axisCharHeight = parseInt(charDiv.clientHeight); 1404 charDiv.style.padding = ""; 1405 charDiv.className = "graph-axis-text graph-axis-text-minor"; 1406 this.axisTextMinorHeight = parseInt(charDiv.offsetHeight); 1407 charDiv.className = "graph-axis-text graph-axis-text-major"; 1408 this.axisTextMajorHeight = parseInt(charDiv.offsetHeight); 1409 this.frame.canvas.axis.removeChild(charDiv); // TODO: When using .redraw() via the browser event onresize, this gives an error in Chrome 1410 1411 // calculate the position of the axis 1412 this.axisOffset = this.main.clientHeight - 1413 this.axisTextMinorHeight - 1414 this.axisTextMajorHeight - 1415 2 * this.mainPadding; 1416 1417 // TODO: do not retrieve datarange here? -> initSize is executed during each redraw() 1418 // retrieve the data range (this can take some time for large amounts of data) 1419 //this.dataRange = this._getDataRange(this.data[0]); // TODO 1420 if (this.data.length > 0) { 1421 var verticalRange = null; 1422 for (var i = 0, imax = this.data.length; i < imax; i++) { 1423 var dataRange = this.data[i].dataRange; 1424 if (dataRange) { 1425 if (verticalRange) { 1426 verticalRange.min = Math.min(verticalRange.min, dataRange.min); 1427 verticalRange.max = Math.max(verticalRange.max, dataRange.max); 1428 } 1429 else { 1430 verticalRange = { 1431 min: dataRange.min, 1432 max: dataRange.max 1433 }; 1434 } 1435 } 1436 } 1437 this.verticalRange = verticalRange || {"min" : -10, "max" : 10}; 1438 } 1439 else { 1440 this.verticalRange = {"min" : -10, "max" : 10}; 1441 } 1442 1443 // get the minimum and maximum data values, and add 5 percent 1444 // so there is always some whitespace above and below the drawn data 1445 var range = this.verticalRange.max - this.verticalRange.min; 1446 if (range <= 0) { 1447 range = 1; 1448 } 1449 var avg = (this.verticalRange.max + this.verticalRange.min) / 2; 1450 this.vMin = this.vMinFixed != undefined ? this.vMinFixed : avg - range / 2 * 1.05; 1451 this.vMax = this.vMaxFixed != undefined ? this.vMaxFixed : avg + range / 2 * 1.05; 1452 if (this.vMax <= this.vMin) { 1453 this.vMax = this.vMin + 1; 1454 } 1455 }; 1456 1457 /** 1458 * Draw the horizontal axis in the graph, containing grid, axis, minor and 1459 * major labels 1460 */ 1461 links.Graph.prototype._redrawHorizontalAxis = function () { 1462 var startTime = new Date(); // TODO: cleanup 1463 1464 // clear any existing data 1465 while (this.frame.canvas.axis.hasChildNodes()) { 1466 this.frame.canvas.axis.removeChild(this.frame.canvas.axis.lastChild); 1467 } 1468 this.majorLabels = []; 1469 1470 // resize the horizontal axis 1471 this.frame.style.left = links.Graph.px(this.main.axisLeft.clientWidth + this.mainPadding); 1472 this.frame.style.top = links.Graph.px(this.mainPadding); 1473 this.frame.style.height = links.Graph.px(this.main.clientHeight - 2 * this.mainPadding ); 1474 this.frame.style.width = links.Graph.px(this.main.clientWidth - 1475 this.main.axisLeft.clientWidth - 1476 this.legendWidth - 1477 2 * this.mainPadding - 2); 1478 1479 this.frame.canvas.style.width = links.Graph.px(this.frame.clientWidth); 1480 this.frame.canvas.style.height = links.Graph.px(this.axisOffset); 1481 this.frame.background.style.height = links.Graph.px(this.axisOffset); 1482 1483 this._calcConversionFactor(); 1484 1485 // the drawn axis is more wide than the actual visual part, such that 1486 // the axis can be dragged without having to redraw it each time again. 1487 var start = this._screenToTime(-this.axisMargin); 1488 var end = this._screenToTime(this.frame.clientWidth + this.axisMargin); 1489 var width = this.frame.clientWidth + 2*this.axisMargin; 1490 1491 var yvalueMinor = this.axisOffset; 1492 var yvalueMajor = this.axisOffset + this.axisTextMinorHeight; 1493 1494 // calculate minimum step (in milliseconds) based on character size 1495 this.minimumStep = this._screenToTime(this.axisCharWidth * 6).valueOf() - 1496 this._screenToTime(0).valueOf(); 1497 1498 this.hStep.setRange(start, end, this.minimumStep); 1499 1500 // create a left major label 1501 if (this.leftMajorLabel) { 1502 this.frame.canvas.removeChild(this.leftMajorLabel); 1503 this.leftMajorLabel = undefined; 1504 } 1505 var leftDate = this.hStep.getLabelMajor(this._screenToTime(0)); 1506 var content = document.createTextNode(leftDate); 1507 this.leftMajorLabel = document.createElement("DIV"); 1508 this.leftMajorLabel.className = "graph-axis-text graph-axis-text-major"; 1509 this.leftMajorLabel.appendChild(content); 1510 this.leftMajorLabel.style.position = "absolute"; 1511 this.leftMajorLabel.style.left = links.Graph.px(0); 1512 this.leftMajorLabel.style.top = links.Graph.px(yvalueMajor); 1513 this.leftMajorLabel.title = leftDate; 1514 this.frame.canvas.appendChild(this.leftMajorLabel); 1515 1516 this.hStep.start(); 1517 var count = 0; 1518 while (!this.hStep.end() && count < 200) { 1519 count++; 1520 var x = this.timeToScreen(this.hStep.getCurrent()); 1521 var hvline = this.hStep.isMajor() ? this.frame.clientHeight : 1522 (this.axisOffset + this.axisTextMinorHeight); 1523 1524 //create vertical line 1525 var vline = document.createElement("DIV"); 1526 vline.className = this.hStep.isMajor() ? "graph-axis-grid graph-axis-grid-major" : 1527 "graph-axis-grid graph-axis-grid-minor"; 1528 vline.style.position = "absolute"; 1529 vline.style.borderLeftStyle = "solid"; 1530 vline.style.top = links.Graph.px(0); 1531 vline.style.width = links.Graph.px(0); 1532 vline.style.height = links.Graph.px(hvline); 1533 vline.style.left = links.Graph.px(x - vline.offsetWidth/2); 1534 this.frame.canvas.axis.appendChild(vline); 1535 1536 if (this.hStep.isMajor()) 1537 { 1538 var content = document.createTextNode(this.hStep.getLabelMajor()); 1539 var majorValue = document.createElement("DIV"); 1540 this.frame.canvas.axis.appendChild(majorValue); 1541 majorValue.className = "graph-axis-text graph-axis-text-major"; 1542 majorValue.appendChild(content); 1543 majorValue.style.position = "absolute"; 1544 majorValue.style.width = links.Graph.px(majorValue.clientWidth); 1545 majorValue.style.left = links.Graph.px(x); 1546 majorValue.style.top = links.Graph.px(yvalueMajor); 1547 majorValue.title = this.hStep.getCurrent(); 1548 majorValue.x = x; 1549 this.majorLabels.push(majorValue); 1550 } 1551 1552 // minor label 1553 var content = document.createTextNode(this.hStep.getLabelMinor()); 1554 var minorValue = document.createElement("DIV"); 1555 minorValue.appendChild(content); 1556 minorValue.className = "graph-axis-text graph-axis-text-minor"; 1557 minorValue.style.position = "absolute"; 1558 minorValue.style.left = links.Graph.px(x); 1559 minorValue.style.top = links.Graph.px(yvalueMinor); 1560 minorValue.title = this.hStep.getCurrent(); 1561 this.frame.canvas.axis.appendChild(minorValue); 1562 1563 this.hStep.next(); 1564 } 1565 1566 // make horizontal axis line on top 1567 var line = document.createElement("DIV"); 1568 line.className = "graph-axis"; 1569 line.style.position = "absolute"; 1570 line.style.borderTopStyle = "solid"; 1571 line.style.top = links.Graph.px(0); 1572 line.style.left = links.Graph.px(this.timeToScreen(start)); 1573 line.style.width = links.Graph.px(this.timeToScreen(end) - this.timeToScreen(start)); 1574 line.style.height = links.Graph.px(0); 1575 this.frame.canvas.axis.appendChild(line); 1576 1577 // make horizontal axis line on bottom side 1578 var line = document.createElement("DIV"); 1579 line.className = "graph-axis"; 1580 line.style.position = "absolute"; 1581 line.style.borderTopStyle = "solid"; 1582 line.style.top = links.Graph.px(this.axisOffset); 1583 line.style.left = links.Graph.px(this.timeToScreen(start)); 1584 line.style.width = links.Graph.px(this.timeToScreen(end) - this.timeToScreen(start)); 1585 line.style.height = links.Graph.px(0); 1586 this.frame.canvas.axis.appendChild(line); 1587 1588 // reposition the left major label 1589 this._redrawAxisLeftMajorLabel(); 1590 1591 var endTime = new Date(); // TODO: cleanup 1592 //document.title = (endTime - startTime) + " ms"; // TODO: cleanup 1593 }; 1594 1595 1596 /** 1597 * Reposition the major labels of the horizontal axis 1598 */ 1599 links.Graph.prototype._redrawAxisLeftMajorLabel = function() { 1600 var offset = parseFloat(this.frame.canvas.axis.style.left); 1601 1602 var lastBelowZero = null; 1603 var firstAboveZero = null; 1604 var xPrev = null; 1605 for (var i in this.majorLabels) { 1606 if (this.majorLabels.hasOwnProperty(i)) { 1607 var label = this.majorLabels[i]; 1608 1609 if (label.x + offset < 0) 1610 lastBelowZero = label; 1611 1612 if (label.x + offset > 0 && (xPrev == null || xPrev + offset < 0)) { 1613 firstAboveZero = label; 1614 } 1615 1616 xPrev = label.x; 1617 } 1618 } 1619 1620 if (lastBelowZero) 1621 lastBelowZero.style.visibility = "hidden"; 1622 1623 if (firstAboveZero) 1624 firstAboveZero.style.visibility = "visible"; 1625 1626 if (firstAboveZero && this.leftMajorLabel.clientWidth > firstAboveZero.x + offset ) { 1627 this.leftMajorLabel.style.visibility = "hidden"; 1628 } 1629 else { 1630 var leftTime = this.hStep.getLabelMajor(this._screenToTime(-offset)); 1631 this.leftMajorLabel.title = leftTime; 1632 this.leftMajorLabel.innerHTML = leftTime; 1633 if (this.leftMajorLabel.style.visibility != "visible") { 1634 this.leftMajorLabel.style.visibility = "visible"; 1635 } 1636 } 1637 }; 1638 1639 /** 1640 * Draw the vertical axis in the graph 1641 */ 1642 links.Graph.prototype._redrawVerticalAxis = function () { 1643 //var testStart = new Date(); // TODO: cleanup 1644 var i; 1645 1646 if (!this.main.axisLeft) { 1647 // create the left vertical axis 1648 this.main.axisLeft = document.createElement("DIV"); 1649 this.main.axisLeft.style.position = "absolute"; 1650 this.main.axisLeft.className = "graph-axis graph-axis-vertical"; 1651 this.main.axisLeft.style.borderRightStyle = "solid"; 1652 1653 this.main.appendChild(this.main.axisLeft); 1654 } else { 1655 // clear any existing data 1656 while (this.main.axisLeft.hasChildNodes()) { 1657 this.main.axisLeft.removeChild(this.main.axisLeft.lastChild); 1658 } 1659 } 1660 1661 if (!this.main.axisRight) { 1662 // create the left vertical axis 1663 this.main.axisRight = document.createElement("DIV"); 1664 this.main.axisRight.style.position = "absolute"; 1665 this.main.axisRight.className = "graph-axis graph-axis-vertical"; 1666 this.main.axisRight.style.borderRightStyle = "solid"; 1667 this.main.appendChild(this.main.axisRight); 1668 } else { 1669 // do nothing 1670 } 1671 1672 if (!this.main.zoomButtons) { 1673 // create zoom buttons for the vertical axis 1674 this.main.zoomButtons = document.createElement("DIV"); 1675 this.main.zoomButtons.className = "graph-axis-button-menu"; 1676 this.main.zoomButtons.style.position = "absolute"; 1677 1678 var graph = this; 1679 var zoomIn = document.createElement("DIV"); 1680 zoomIn.innerHTML = "+"; 1681 zoomIn.title = "Zoom in vertically (shift + scroll wheel)"; 1682 zoomIn.className = "graph-axis-button"; 1683 this.main.zoomButtons.appendChild(zoomIn); 1684 links.Graph.addEventListener(zoomIn, "mousedown", function (event) { 1685 graph._zoomVertical(0.2); 1686 links.Graph.preventDefault(event); 1687 }); 1688 1689 var zoomOut = document.createElement("DIV"); 1690 zoomOut.innerHTML = "−"; 1691 zoomOut.className = "graph-axis-button"; 1692 zoomOut.title = "Zoom out vertically (shift + scroll wheel)"; 1693 this.main.zoomButtons.appendChild(zoomOut); 1694 links.Graph.addEventListener(zoomOut, "mousedown", function (event) { 1695 graph._zoomVertical(-0.2); 1696 links.Graph.preventDefault(event); 1697 }); 1698 1699 this.main.appendChild(this.main.zoomButtons); 1700 } 1701 1702 // clear any existing data from the grid 1703 while (this.frame.vgrid.hasChildNodes()) { 1704 this.frame.vgrid.removeChild(this.frame.vgrid.lastChild); 1705 } 1706 1707 // determine the range start, end, and step 1708 this.vStart = (this.vStart != undefined && this.vStart < this.vMax) ? this.vStart : this.vMin; 1709 this.vEnd = (this.vEnd != undefined && this.vEnd > this.vMin) ? this.vEnd : this.vMax; 1710 // TODO: allow start and end larger than visible area? 1711 this.vStart = Math.max(this.vStart, this.vMin); 1712 this.vEnd = Math.min(this.vEnd, this.vMax); 1713 1714 var start = this.vStart; 1715 var end = this.vEnd; 1716 var stepnum = parseInt(this.axisOffset / 40); 1717 var step = this.vStepSize || ((this.vEnd - this.vStart) / stepnum); 1718 var prettyStep = true; 1719 this.vStep._setRange(start, end, step, prettyStep); 1720 1721 if (this.vEnd > this.vStart) { 1722 // calculate the conversion from y value to position on screen 1723 var graphBottom = this.axisOffset; 1724 var graphTop = 0; 1725 var yScale = (graphTop - graphBottom) / (this.vEnd - this.vStart); 1726 var yShift = graphBottom - this.vStart * yScale; 1727 this.yToScreen = function (y) { 1728 return y * yScale + yShift; 1729 }; 1730 this.screenToY = function (ys) { 1731 return (ys - yShift) / yScale; 1732 }; 1733 // TODO: make a more neat solution for this.yToScreen() 1734 } 1735 else { 1736 this.yToScreen = function () { 1737 return 0; 1738 }; 1739 this.screenToY = function () { 1740 return 0; 1741 }; 1742 } 1743 1744 if (this.vAreas && !this.frame.background.childNodes.length) { 1745 // create vertical background areas 1746 for (i = 0; i < this.vAreas.length; i++) { 1747 var area = this.vAreas[i]; 1748 var divArea = document.createElement('DIV'); 1749 divArea.className = 'graph-background-area'; 1750 divArea.start = (area.start != null) ? Number(area.start) : null; 1751 divArea.end = (area.end != null) ? Number(area.end) : null; 1752 if (area.className) { 1753 divArea.className += ' ' + area.className; 1754 } 1755 if (area.color) { 1756 divArea.style.backgroundColor = area.color; 1757 } 1758 this.frame.background.appendChild(divArea); 1759 } 1760 } 1761 if (this.frame.background.childNodes.length) { 1762 // reposition vertical background areas 1763 var childs = this.frame.background.childNodes; 1764 for (i = 0; i < childs.length; i++) { 1765 var child = childs[i]; 1766 var areaStart = this.yToScreen(child.start != null ? Math.max(child.start, this.vStart) : this.vStart); 1767 var areaEnd = this.yToScreen(child.end != null ? Math.min(child.end, this.vEnd) : this.vEnd); 1768 child.style.top = areaEnd + 'px'; 1769 child.style.height = Math.max(areaStart - areaEnd, 0) + 'px'; 1770 } 1771 } 1772 1773 var maxWidth = 0; 1774 var count = 0; 1775 this.vStep.start(); 1776 if ( this.yToScreen(this.vStep.getCurrent()) > this.axisOffset) { 1777 this.vStep.next(); 1778 } 1779 while(!this.vStep.end() && count < 100) { 1780 count++; 1781 var y = this.vStep.getCurrent(); 1782 var yScreen = this.yToScreen(y); 1783 1784 // use scientific notation when necessary 1785 if (Math.abs(y) > 1e6) { 1786 y = y.toExponential(); 1787 } 1788 else if (Math.abs(y) < 1e-4) { 1789 if (Math.abs(y) > this.vStep.getStep()/2) 1790 y = y.toExponential(); 1791 else 1792 y = 0; 1793 } 1794 1795 // create the text of the label 1796 var content = document.createTextNode(y); 1797 var labelText = document.createElement("DIV"); 1798 labelText.appendChild(content); 1799 labelText.className = "graph-axis-text graph-axis-text-vertical"; 1800 labelText.style.position = "absolute"; 1801 labelText.style.whiteSpace = "nowrap"; 1802 labelText.style.textAlign = "right"; 1803 this.main.axisLeft.appendChild(labelText); 1804 1805 // create the label line 1806 var labelLine = document.createElement("DIV"); 1807 labelLine.className = "graph-axis-grid graph-axis-grid-vertical"; 1808 labelLine.style.position = "absolute"; 1809 labelLine.style.borderTopStyle = "solid"; 1810 labelLine.style.width = "5px"; 1811 this.main.axisLeft.appendChild(labelLine); 1812 1813 // create the grid line 1814 var labelGridLine = document.createElement("DIV"); 1815 labelGridLine.className = (y != 0) ? "graph-axis-grid graph-axis-grid-minor" : 1816 "graph-axis-grid graph-axis-grid-major"; 1817 labelGridLine.style.position = "absolute"; 1818 labelGridLine.style.left = "0px"; 1819 labelGridLine.style.width = "100%"; 1820 labelGridLine.style.borderTopStyle = "solid"; 1821 this.frame.vgrid.appendChild(labelGridLine); 1822 1823 // position the label text and line vertically 1824 var h = labelText.offsetHeight; 1825 labelText.style.top = links.Graph.px(yScreen - h/2); 1826 labelLine.style.top = links.Graph.px(yScreen); 1827 labelGridLine.style.top = links.Graph.px(yScreen); 1828 1829 // calculate the widest label so far. 1830 maxWidth = Math.max(maxWidth, labelText.offsetWidth); 1831 1832 this.vStep.next(); 1833 } 1834 1835 // right align all elements 1836 maxWidth += this.main.zoomButtons.clientWidth; // append width of the zoom buttons 1837 for (i = 0; i < this.main.axisLeft.childNodes.length; i++) { 1838 this.main.axisLeft.childNodes[i].style.left = 1839 links.Graph.px(maxWidth - this.main.axisLeft.childNodes[i].offsetWidth); 1840 } 1841 1842 // resize the axis 1843 this.main.axisLeft.style.left = links.Graph.px(this.mainPadding); 1844 this.main.axisLeft.style.top = links.Graph.px(this.mainPadding); 1845 this.main.axisLeft.style.height = links.Graph.px(this.axisOffset + 1); 1846 this.main.axisLeft.style.width = links.Graph.px(maxWidth); 1847 1848 this.main.axisRight.style.left = 1849 links.Graph.px(this.main.clientWidth - this.legendWidth - this.mainPadding - 2); 1850 this.main.axisRight.style.top = links.Graph.px(this.mainPadding); 1851 this.main.axisRight.style.height = links.Graph.px(this.axisOffset + 1); 1852 1853 //var testEnd = new Date(); // TODO: cleanup 1854 //document.title += " v:" +(testEnd - testStart) + "ms"; // TODO: cleanup 1855 }; 1856 1857 1858 /** 1859 * Draw all events that are provided in the data on the graph 1860 */ 1861 links.Graph.prototype._redrawData = function() { 1862 this._calcConversionFactor(); 1863 1864 // determine the size of the graph 1865 var start = this._screenToTime(-this.axisMargin); 1866 var end = this._screenToTime(this.frame.clientWidth + this.axisMargin); 1867 //var width = this.frame.clientWidth + 2*this.axisMargin; 1868 /* 1869 // TODO: use axisMargin? 1870 var start = this._screenToTime(0); 1871 var end = this._screenToTime(this.frame.clientWidth); 1872 var width = this.frame.clientWidth; 1873 */ 1874 1875 var graph = this.frame.canvas.graph; 1876 var ctx = graph.getContext("2d"); 1877 1878 // clear the graph. 1879 // It is important to clear the old size of the graph (before resizing), else 1880 // Safari does not clear the whole graph. 1881 ctx.clearRect(0, 0, graph.height, graph.width); 1882 1883 // resize the graph element 1884 var left = this.timeToScreen(start); 1885 var right = this.timeToScreen(end); 1886 var graphWidth = right - left; 1887 var height = this.axisOffset; 1888 1889 graph.style.left = links.Graph.px(left); 1890 graph.width = graphWidth; 1891 graph.height = height; 1892 1893 var offset = parseFloat(graph.style.left); 1894 1895 // draw the graph(s) 1896 for (var col = 0, colCount = this.data.length; col < colCount; col++) { 1897 var style = this._getLineStyle(col); 1898 var color = this._getLineColor(col); 1899 var textColor = this._getTextColor(col); 1900 var font = this._getFont(col); 1901 var width = this._getLineWidth(col); 1902 var radius = this._getLineRadius(col); 1903 var visible = this._getLineVisible(col); 1904 var type = this.data[col].type || 'line'; 1905 var data = this.data[col].data; 1906 var d; 1907 1908 // determine the first and last row inside the visible area 1909 var rowRange = this._getVisbleRowRange(data, start, end, type, 1910 this.data[col].visibleRowRange); 1911 this.data[col].visibleRowRange = rowRange; 1912 var rowStep = this._calculateRowStep(rowRange); 1913 1914 if (visible && rowRange) { 1915 switch (type) { 1916 case 'line': 1917 if (style == "line" || style == "dot-line") { 1918 // draw line 1919 ctx.strokeStyle = color; 1920 ctx.lineWidth = width; 1921 1922 ctx.beginPath(); 1923 var row = rowRange.start; 1924 while (row <= rowRange.end) { 1925 // find the first data row with a non-null value 1926 while (row <= rowRange.end && data[row].value == null) { 1927 row += rowStep; 1928 } 1929 if (row <= rowRange.end) { 1930 // move to the first non-null data point 1931 value = data[row].value; 1932 var x = this.timeToScreen(data[row].date) - offset; 1933 var y = this.yToScreen(value); 1934 ctx.moveTo(x, y); 1935 1936 /* TODO: implement fill style 1937 ctx.moveTo(x, this.yToScreen(0)); 1938 ctx.lineTo(x, y); 1939 */ 1940 row += rowStep; 1941 } 1942 1943 // draw lines as long as data values are not null 1944 while (row <= rowRange.end && (value = data[row].value) != null) { 1945 x = this.timeToScreen(data[row].date) - offset; 1946 y = this.yToScreen(value); 1947 ctx.lineTo(x, y); 1948 row += rowStep; 1949 } 1950 1951 /* TODO: implement fill style 1952 ctx.lineTo(x, this.yToScreen(0)); 1953 */ 1954 } 1955 1956 /* TODO: implement fill style 1957 ctx.fillStyle = "rgba(255,255,0, 0.5)"; 1958 ctx.fill(); 1959 */ 1960 1961 ctx.stroke(); 1962 } 1963 1964 if (type == 'line' && (style == "dot" || style == "dot-line")) { 1965 // draw dots 1966 var diameter = 2 * radius; 1967 ctx.fillStyle = color; 1968 1969 for (row = rowRange.start; row <= rowRange.end; row += rowStep) { 1970 var value = data[row].value; 1971 if (value != null) { 1972 x = this.timeToScreen(data[row].date) - offset; 1973 y = this.yToScreen(value); 1974 ctx.fillRect(x - radius, y - radius, diameter, diameter); 1975 } 1976 } 1977 } 1978 break; 1979 1980 case 'area': 1981 // draw background area 1982 for (row = rowRange.start; row <= rowRange.end; row += rowStep) { 1983 d = data[row]; 1984 ctx.fillStyle = d.color || color; 1985 1986 var xStart = this.timeToScreen(d.start) - offset; 1987 var yStart = this.timeToScreen(d.end) - offset; 1988 ctx.fillRect(xStart, 0, yStart - xStart, height); 1989 1990 if (d.text) { 1991 // draw text 1992 ctx.font = d.font || font; 1993 ctx.textAlign = 'left'; 1994 ctx.textBaseline = 'top'; 1995 ctx.fillStyle = d.textColor || textColor; 1996 ctx.fillText(d.text, xStart + 2, 0); 1997 } 1998 } 1999 break; 2000 2001 case 'event': 2002 // draw event background area 2003 for (row = rowRange.start; row <= rowRange.end; row += rowStep) { 2004 d = data[row]; 2005 ctx.fillStyle = d.color || color; 2006 2007 // area with a start only 2008 var dWidth = d.width || width; 2009 xStart = this.timeToScreen(d.date) - offset; 2010 ctx.fillRect(xStart - dWidth / 2, 0, dWidth, height); 2011 2012 if (d.text) { 2013 // draw text 2014 ctx.font = d.font || font; 2015 ctx.textAlign = 'left'; 2016 ctx.textBaseline = 'top'; 2017 ctx.fillStyle = d.textColor || textColor; 2018 ctx.fillText(d.text, xStart + dWidth / 2 + 2, 0); 2019 } 2020 } 2021 break; 2022 2023 default: 2024 throw new Error('Unknown type of dataset "' + type + '". ' + 2025 'Choose "line" or "area"'); 2026 } 2027 } 2028 } 2029 }; 2030 2031 /** 2032 * Calculate the row step (skipping datapoints in case of much data) 2033 * @param {Object} rowRange Object containing parameters 2034 * {Date} start 2035 * {Date} end 2036 * @return {Number} rowStep an integer number 2037 * @private 2038 */ 2039 links.Graph.prototype._calculateRowStep = function(rowRange) { 2040 var rowStep; 2041 2042 // choose a step size, depending on the width of the screen in pixels 2043 // and the number of data points. 2044 if ( this.autoDataStep && rowRange ) { 2045 // skip data points in case of much data 2046 var rowCount = (rowRange.end - rowRange.start); 2047 var canvasWidth = (this.frame.clientWidth + 2 * this.axisMargin); 2048 rowStep = Math.max(Math.floor(rowCount / canvasWidth), 1); 2049 } 2050 else { 2051 // draw all data points 2052 rowStep = 1; 2053 } 2054 2055 return rowStep; 2056 }; 2057 2058 /** 2059 * Redraw the tooltip showing the currently hovered value 2060 */ 2061 links.Graph.prototype._redrawDataTooltip = function () { 2062 var tooltip = this.tooltip; 2063 if (this.showTooltip && tooltip) { 2064 var dataPoint = tooltip.dataPoint; 2065 if (dataPoint) { 2066 var dot = tooltip.dot; 2067 var label = tooltip.label; 2068 2069 var graph = this.frame.canvas.graph; 2070 var offset = parseFloat(graph.style.left) + this.axisMargin; 2071 var radius = dataPoint.radius || 4; 2072 var color = dataPoint.color || '#4d4d4d'; 2073 var left = this.timeToScreen(dataPoint.date) + offset; 2074 var top = (dataPoint.value != undefined) ? this.yToScreen(dataPoint.value) : 16; 2075 2076 if (!dot) { 2077 dot = document.createElement('div'); 2078 dot.className = 'graph-tooltip-dot'; 2079 tooltip.dot = dot; 2080 } 2081 if (dot.style.borderColor != color && dot.parentNode) { 2082 // note: this is a workaround for a bug in Chrome on Windows, 2083 // which does not apply changed border color correctly 2084 dot.parentNode.removeChild(dot); 2085 } 2086 if (!dot.parentNode) { 2087 this.frame.canvas.appendChild(dot); 2088 } 2089 2090 if (!label) { 2091 // note: we could create label as a child of dot, but there 2092 // appears to be a bug in Chrome on Windows giving issues. 2093 label = document.createElement('div'); 2094 label.className = 'graph-tooltip-label'; 2095 tooltip.label = label; 2096 } 2097 if (!label.parentNode) { 2098 // note: the label must be added to the DOM before changing 2099 // its innerHTML, else you encounter a bug on IE 6-8. 2100 this.frame.canvas.appendChild(label); 2101 } 2102 2103 dot.style.left = left + 'px'; 2104 dot.style.top = top + 'px'; 2105 dot.style.borderColor = color; 2106 dot.style.borderRadius = radius + 'px'; 2107 dot.style.borderWidth = radius + 'px'; 2108 dot.style.marginLeft = -radius + 'px'; 2109 dot.style.marginTop = -radius + 'px'; 2110 dot.style.display = dataPoint.title ? 'none': ''; 2111 2112 var html; 2113 if (this.tooltipFormatter) { 2114 // custom format function 2115 html = this.tooltipFormatter(dataPoint); 2116 } 2117 else { 2118 html = '<table style="color: ' + color + '">'; 2119 if (dataPoint.title) { 2120 html += '<tr><td>' + dataPoint.title + '</td></tr>'; 2121 } 2122 else { 2123 html += '<tr><td>Date:</td><td>' + dataPoint.date + '</td></tr>'; 2124 if (dataPoint.value != undefined) { 2125 html += '<tr><td>Value:</td><td>' + dataPoint.value.toPrecision(4) + '</td></tr>'; 2126 } 2127 } 2128 html += '</table>'; 2129 } 2130 label.innerHTML = html; 2131 2132 var width = label.clientWidth; 2133 var graphWidth = this.timeToScreen(this.end) - this.timeToScreen(this.start); 2134 var height = label.clientHeight; 2135 var margin = 10; 2136 var showAbove = (top - height - margin > 0); 2137 var showRight = (left + width + margin < graphWidth); 2138 label.style.top = (showAbove ? (top - height - radius) : (top + radius)) + 'px'; 2139 label.style.left = (showRight ? (left + radius) : (left - width - radius)) + 'px'; 2140 } 2141 else { 2142 // remove the dot when visible 2143 if (tooltip.dot && tooltip.dot.parentNode) { 2144 tooltip.dot.parentNode.removeChild(tooltip.dot); 2145 tooltip.dot = undefined; // remove the dot, else we get issues on IE8- 2146 } 2147 if (tooltip.label && tooltip.label.parentNode) { 2148 tooltip.label.parentNode.removeChild(tooltip.label); 2149 tooltip.label = undefined; // remove the label, else we get issues on IE8- 2150 } 2151 } 2152 } 2153 }; 2154 2155 /** 2156 * Set a tooltip for the currently hovered data 2157 * @param {Object} dataPoint object containing parameters: 2158 * {String} date 2159 * {String} value 2160 * {String} color 2161 * {String} radius 2162 * @private 2163 */ 2164 links.Graph.prototype._setTooltip = function (dataPoint) { 2165 if (!this.tooltip) { 2166 this.tooltip = {}; 2167 } 2168 this.tooltip.dataPoint = dataPoint; 2169 2170 this._redrawDataTooltip(); 2171 }; 2172 2173 2174 /** 2175 * Find the data point closest to given date and value (euclidean distance). 2176 * If no data point is found near given position, undefined is returned. 2177 * @param {Date} date 2178 * @param {Number} value 2179 * @return {Object | undefined} dataPoint An object containing parameters 2180 * {Date} date 2181 * {Number} value 2182 * {String} color 2183 * {Number} radius 2184 * @private 2185 */ 2186 links.Graph.prototype._findClosestDataPoint = function (date, value) { 2187 var maxDistance = 30; // px 2188 var winner = undefined; 2189 var graph = this; 2190 function isVisible (dataPoint) { 2191 return dataPoint.date >= graph.start && 2192 dataPoint.date <= graph.end && 2193 dataPoint.value >= graph.vStart && 2194 dataPoint.value <= graph.vEnd 2195 } 2196 2197 for (var col = 0, colCount = this.data.length; col < colCount; col++) { 2198 var visible = this._getLineVisible(col); 2199 var rowRange = this.data[col].visibleRowRange; 2200 var data = this.data[col].data; 2201 var type = this.data[col].type; 2202 2203 if (visible && rowRange) { 2204 var rowStep = this._calculateRowStep(rowRange); 2205 var row = rowRange.start; 2206 while (row <= rowRange.end) { 2207 var dataPoint = data[row]; 2208 if (type == 'event') { 2209 dataPoint = { 2210 date: dataPoint.date, 2211 value: this.screenToY(16), // TODO: use the real font height 2212 text: dataPoint.text, 2213 title: dataPoint.title 2214 }; 2215 } 2216 else if (type == 'area') { 2217 dataPoint = { 2218 date: dataPoint.start, 2219 value: this.screenToY(16), // TODO: use the real font height 2220 text: dataPoint.text, 2221 title: dataPoint.title 2222 }; 2223 } 2224 2225 if (dataPoint.value != null) { 2226 // first data point found right from x. 2227 var dateDistance = Math.abs(dataPoint.date - date) * this.ttsFactor; 2228 if (dateDistance < maxDistance) { 2229 var valueDistance = Math.abs(this.yToScreen(dataPoint.value) - this.yToScreen(value)); 2230 if ((valueDistance < maxDistance) && isVisible(dataPoint)) { 2231 var distance = Math.sqrt( 2232 dateDistance * dateDistance + 2233 valueDistance * valueDistance); 2234 if (!winner || distance < winner.distance) { 2235 // we have a new winner 2236 var color = this._getLineColor(col); 2237 var radius; 2238 if (type == 'event' || type == 'area') { 2239 radius = this._getLineWidth(col); 2240 color = this._getTextColor(col); 2241 } 2242 else if (this._getLineStyle(col) == 'line') { 2243 radius = this._getLineWidth(col) * 2; 2244 } 2245 else { 2246 radius = this._getLineRadius(col) * 2; 2247 } 2248 radius = Math.max(radius, 4); 2249 2250 winner = { 2251 distance: distance, 2252 dataPoint: { 2253 date: dataPoint.date, 2254 //value: (dataPoint.value != undefined) ? dataPoint.value : this.screenToY(10), 2255 value: dataPoint.value, 2256 title: dataPoint.title, 2257 text: dataPoint.text, 2258 color: color, 2259 radius: radius, 2260 line: col 2261 } 2262 }; 2263 } 2264 } 2265 } 2266 else if (dataPoint.date > date) { 2267 // skip the rest of the data 2268 row = rowRange.end; 2269 } 2270 } 2271 row += rowStep; 2272 } 2273 } 2274 } 2275 2276 return winner ? winner.dataPoint : undefined; 2277 }; 2278 2279 /** 2280 * Average a range of values in the given data table 2281 * @param {Array} data table containing objects with parameters date and value 2282 * @param {Number} start index to start averaging 2283 * @param {Number} length the number of values to average 2284 * @return {Object} An object with average values for the date and value 2285 * 2286 */ 2287 // TODO: this method is not used. Delete it? 2288 links.Graph.prototype._average = function(data, start, length) { 2289 var sumDate = 0; 2290 var countDate = 0; 2291 var sumValue = 0; 2292 var countValue = 0; 2293 2294 for (var row = start, end = Math.min(start+length, data.length); row < end; row++) { 2295 var d = data[row]; 2296 if (d.date != undefined) { 2297 sumDate += d.date.valueOf(); 2298 countDate += 1; 2299 } 2300 if (d.value != undefined) { 2301 sumValue += d.value; 2302 countValue += 1; 2303 } 2304 } 2305 2306 var avgDate = new Date(Math.round(sumDate / countDate)); 2307 var avgValue = sumValue / countValue; 2308 2309 return {"date": avgDate, "value": avgValue}; 2310 }; 2311 2312 2313 /** 2314 * Draw all events that are provided in the data on the graph 2315 */ 2316 links.Graph.prototype._redrawLegend = function() { 2317 // Calculate the number of functions that need a legend entry 2318 var legendCount = 0; 2319 for (var col = 0, len = this.data.length; col < len; col++) { 2320 if (this._getLineLegend(col) == true) 2321 legendCount ++; 2322 } 2323 2324 if (legendCount == 0 || (this.legend && this.legend.visible === false) ) { 2325 // no legend entries 2326 if (this.main.legend) { 2327 // remove if existing 2328 this.main.removeChild(this.main.legend); 2329 this.main.legend = undefined; 2330 } 2331 2332 this.legendWidth = 0; 2333 return; 2334 } 2335 2336 var scrollTop = 0; 2337 if (!this.main.legend) { 2338 // create the legend 2339 this.main.legend = document.createElement("DIV"); 2340 this.main.legend.className = "graph-legend"; 2341 this.main.legend.style.position = "absolute"; 2342 this.main.legend.style.overflowY = "auto"; 2343 2344 this.main.appendChild(this.main.legend); 2345 } else { 2346 // clear any existing contents of the legend 2347 scrollTop = this.main.legend.scrollTop; 2348 while (this.main.legend.hasChildNodes()) { 2349 this.main.legend.removeChild(this.main.legend.lastChild); 2350 } 2351 } 2352 2353 var maxWidth = 0; 2354 for (var col = 0, len = this.data.length; col < len; col++) { 2355 var showLegend = this._getLineLegend(col); 2356 2357 if (showLegend) { 2358 var color = this._getLineColor(col); 2359 var label = this.data[col].label; 2360 2361 var divLegendItem = document.createElement("DIV"); 2362 divLegendItem.className = "graph-legend-item"; 2363 this.main.legend.appendChild(divLegendItem); 2364 2365 if (this.legend && this.legend.toggleVisibility) { 2366 // show a checkbox to show/hide graph 2367 var chkShow = document.createElement("INPUT"); 2368 chkShow.type = "checkbox"; 2369 chkShow.checked = this._getLineVisible(col); 2370 chkShow.defaultChecked = this._getLineVisible(col); // for IE 2371 chkShow.style.marginRight = links.Graph.px(this.mainPadding); 2372 chkShow.col = col; // store its column number 2373 2374 var me = this; 2375 chkShow.onmousedown = function (event) { 2376 me._setLineVisible(this.col, !this.checked ); 2377 me._checkSize(); 2378 me.redraw(); 2379 }; 2380 2381 divLegendItem.appendChild(chkShow); 2382 } 2383 2384 var spanColor = document.createElement("SPAN"); 2385 spanColor.style.backgroundColor = color; 2386 spanColor.innerHTML = " "; 2387 divLegendItem.appendChild(spanColor); 2388 2389 var text = document.createTextNode(" " + label); 2390 divLegendItem.appendChild(text); 2391 // TODO: test on IE 2392 2393 maxWidth = Math.max(maxWidth, divLegendItem.clientWidth); 2394 } 2395 } 2396 2397 // position the legend in the upper right corner 2398 // TODO: make location customizable 2399 this.main.legend.style.top = links.Graph.px(this.mainPadding); 2400 this.main.legend.style.height = "auto"; 2401 var scroll = false; 2402 if (this.main.legend.clientHeight > (this.axisOffset - 1)) { 2403 this.main.legend.style.height = links.Graph.px(this.axisOffset - 1) ; 2404 scroll = true; 2405 } 2406 2407 if (this.legend && this.legend.width) { 2408 this.main.legend.style.width = this.legend.width; 2409 } 2410 else if (scroll) { 2411 this.main.legend.style.width = "auto"; 2412 this.main.legend.style.width = links.Graph.px(this.main.legend.clientWidth + 40); // adjust for scroll bar width 2413 } 2414 else { 2415 this.main.legend.style.width = "auto"; 2416 this.main.legend.style.width = links.Graph.px(this.main.legend.clientWidth + 5); // +5 to prevent wrapping text 2417 } 2418 2419 this.legendWidth = 2420 (this.main.legend.offsetWidth ? this.main.legend.offsetWidth : this.main.legend.clientWidth) + 2421 this.mainPadding; // TODO: test on IE6 2422 2423 this.main.legend.style.left = links.Graph.px(this.main.clientWidth - this.legendWidth); 2424 2425 // restore the previous scroll position 2426 if (scrollTop) { 2427 this.main.legend.scrollTop = scrollTop; 2428 } 2429 }; 2430 2431 /** 2432 * Determines 2433 * @param {Array} data An array containing objects with parameters 2434 * d (datetime) and v (value) 2435 * @param {Date} start The start date of the visible range 2436 * @param {Date} end The end date of the visible range 2437 * @param {String} type Type of data. 'line' (default), 'area', or 'event' 2438 * @param {Object} oldRowRange previous row range, can serve as start 2439 * to find the current visible range faster. 2440 * @return {object} Range object containing start row and end row 2441 * range.start {int} row number of first visible row 2442 * range.end {int} row number of last visible row +1 2443 * (this can be the rowcount +1) 2444 */ 2445 links.Graph.prototype._getVisbleRowRange = function(data, start, end, type, oldRowRange) { 2446 if (!data) { 2447 data = []; 2448 } 2449 var fieldStart = 'date'; 2450 var fieldEnd = 'date'; 2451 if (type == 'area') { 2452 fieldStart = 'start'; 2453 fieldEnd = 'end'; 2454 } 2455 var rowCount = data.length; 2456 2457 // initialize 2458 var rowRange = { 2459 start: 0, 2460 end: (rowCount-1) 2461 }; 2462 if (oldRowRange != null) { 2463 rowRange.start = oldRowRange.start; 2464 rowRange.end = oldRowRange.end; 2465 } 2466 2467 // check if the current range does not exceed the actual number of rows 2468 if (rowRange.start > rowCount - 1 && rowCount > 0) { 2469 rowRange.start = rowCount - 1; 2470 } 2471 2472 if (rowRange.end > rowCount - 1) { 2473 rowRange.end = rowCount - 1; 2474 } 2475 2476 // find the first visible row. Start searching at the previous first visible row 2477 while (rowRange.start > 0 && 2478 data[rowRange.start][fieldStart].valueOf() > start.valueOf()) { 2479 rowRange.start--; 2480 } 2481 while (rowRange.start < rowCount-1 && 2482 data[rowRange.start][fieldStart].valueOf() < start.valueOf()) { 2483 rowRange.start++; 2484 } 2485 2486 // find the last visible row. Start searching at the previous last visible row 2487 while (rowRange.end > rowRange.start && 2488 data[rowRange.end][fieldEnd].valueOf() > end.valueOf()) { 2489 rowRange.end--; 2490 } 2491 while (rowRange.end < rowCount-1 && 2492 data[rowRange.end][fieldEnd].valueOf() < end.valueOf()) { 2493 rowRange.end++; 2494 } 2495 2496 return rowRange; 2497 }; 2498 2499 2500 /** 2501 * Determines the row range of a datatable 2502 * @param data {Array} An array containing objects with parameters 2503 * d (datetime) and v (value) 2504 * @param {String[]} [fields] Optional array with field names to be read 2505 * for min/max. These fields must contain Date 2506 * objects. If fields is undefined, the data will 2507 * be searched for ['date']. 2508 * @return {object} Range object containing start row and end row 2509 * range.start {Date} first date in the data 2510 * range.end {Date} last date in the data 2511 */ 2512 links.Graph.prototype._getRowRange = function(data, fields) { 2513 if (!data) { 2514 data = []; 2515 } 2516 if (!fields) { 2517 fields = ['date']; 2518 } 2519 2520 var rowRange = { 2521 min: undefined, // number 2522 max: undefined // number 2523 }; 2524 2525 if (data.length > 0) { 2526 for (var f = 0; f < fields.length; f++) { 2527 var field = fields[f]; 2528 2529 rowRange.min = data[0][field].valueOf(); 2530 rowRange.max = data[0][field].valueOf(); 2531 2532 for (var row = 1, rows = data.length; row < rows; row++) { 2533 var d = data[row][field]; 2534 if (d != undefined) { 2535 rowRange.min = Math.min(d.valueOf(), rowRange.min); 2536 rowRange.max = Math.max(d.valueOf(), rowRange.max); 2537 } 2538 } 2539 } 2540 } 2541 2542 if (rowRange.min != null && !isNaN(rowRange.min) && 2543 rowRange.max != null && !isNaN(rowRange.max)) { 2544 return { 2545 min: new Date(rowRange.min), 2546 max: new Date(rowRange.max) 2547 }; 2548 } 2549 return null; 2550 }; 2551 2552 /** 2553 * Calculate the maximum and minimum value of all graphs in the provided data 2554 * table. 2555 * @param data {Array} An array containing objects with parameters 2556 * d (datetime) and v (value) 2557 * @return {Object} An object with parameters min and max (both numbers) 2558 */ 2559 links.Graph.prototype._getDataRange = function(data) { 2560 if (!data) { 2561 data = []; 2562 } 2563 2564 var dataRange = null; 2565 for (var row = 0, rows = data.length; row < rows; row++) { 2566 var value = data[row].value; 2567 if (value != undefined) { 2568 if (dataRange) { 2569 // find max/min 2570 dataRange.min = Math.min(value, dataRange.min); 2571 dataRange.max = Math.max(value, dataRange.max); 2572 } 2573 else { 2574 // first defined value 2575 dataRange = { 2576 min: value, 2577 max: value 2578 } 2579 } 2580 } 2581 } 2582 2583 if (dataRange && 2584 dataRange.min != null && !isNaN(dataRange.min) && 2585 dataRange.max != null && !isNaN(dataRange.max)) { 2586 return dataRange; 2587 } 2588 return null; 2589 }; 2590 2591 2592 /** 2593 * Returns a string with the style for the given column in data. 2594 * Available styles are "dot", "line", or "dot-line" 2595 * @param {int} column The column number 2596 * @return {string} style The style for this line 2597 */ 2598 links.Graph.prototype._getLineStyle = function(column) { 2599 if (this.lines && column < this.lines.length) { 2600 var line = this.lines[column]; 2601 if (line && line.style != undefined) 2602 return line.style.toLowerCase(); 2603 } 2604 2605 if (this.line && this.line.style != undefined) 2606 return this.line.style.toLowerCase(); 2607 2608 return "line"; 2609 }; 2610 2611 /** 2612 * Returns a string with the color for the given column in data. 2613 * @param {int} column The column number 2614 * @return {string} color The color for this line 2615 */ 2616 links.Graph.prototype._getLineColor = function(column) { 2617 if (this.lines && column < this.lines.length) { 2618 var line = this.lines[column]; 2619 if (line && line.color != undefined) 2620 return line.color; 2621 } 2622 2623 if (this.line && this.line.color != undefined) 2624 return this.line.color; 2625 2626 if (column < this.defaultColors.length) { 2627 return this.defaultColors[column]; 2628 } 2629 2630 return "black"; 2631 }; 2632 2633 /** 2634 * Returns a string with the text color for the given column in data. 2635 * @param {int} column The column number 2636 * @return {string} color The text color for this line 2637 */ 2638 links.Graph.prototype._getTextColor = function(column) { 2639 if (this.lines && column < this.lines.length) { 2640 var line = this.lines[column]; 2641 if (line && line.textColor != undefined) 2642 return line.textColor; 2643 } 2644 2645 if (this.line && this.line.textColor != undefined) 2646 return this.line.textColor; 2647 2648 return "#4D4D4D"; 2649 }; 2650 2651 /** 2652 * Returns a string with the font the given column in data. 2653 * @param {int} column The column number 2654 * @return {string} font The font for this line, for example '13px arial' 2655 */ 2656 links.Graph.prototype._getFont = function(column) { 2657 if (this.lines && column < this.lines.length) { 2658 var line = this.lines[column]; 2659 if (line && line.font != undefined) 2660 return line.font; 2661 } 2662 2663 if (this.line && this.line.font != undefined) 2664 return this.line.font; 2665 2666 return "13px arial"; 2667 }; 2668 2669 /** 2670 * Returns a float with the line width for the given column in data. 2671 * @param {Number} column The column number 2672 * @return {Number} linewidthh The width for this line 2673 */ 2674 links.Graph.prototype._getLineWidth = function(column) { 2675 if (this.lines && column < this.lines.length) { 2676 var line = this.lines[column]; 2677 if (line && line.width != undefined) 2678 return parseFloat(line.width); 2679 } 2680 2681 if (this.line && this.line.width != undefined) 2682 return parseFloat(this.line.width); 2683 2684 return 2.0; 2685 }; 2686 2687 /** 2688 * Returns a float with the line radius (radius for the dots) for the given 2689 * column in data. 2690 * @param {int} column The column number 2691 * @return {Number} lineRadius The radius for the dots on this line 2692 */ 2693 links.Graph.prototype._getLineRadius = function(column) { 2694 if (this.lines && column < this.lines.length) { 2695 var line = this.lines[column]; 2696 if (line && line.radius != undefined) 2697 return parseFloat(line.radius); 2698 } 2699 2700 if (this.line && this.line.radius != undefined) 2701 return parseFloat(this.line.radius); 2702 2703 return 3.0; 2704 }; 2705 2706 /** 2707 * Returns whether a certain line must be displayed in the legend 2708 * @param {int} column The column number 2709 * @return {boolean} showLegend Whether this line must be displayed in the legend 2710 */ 2711 links.Graph.prototype._getLineLegend = function(column) { 2712 if (this.lines && column < this.lines.length) { 2713 var line = this.lines[column]; 2714 if (line && line.legend != undefined) 2715 return line.legend; 2716 } 2717 2718 if (this.line && this.line.legend != undefined) 2719 return this.line.legend; 2720 2721 return true; 2722 }; 2723 2724 /** 2725 * Returns whether a certain line is visible (and must be drawn) 2726 * @param {int} column The column number 2727 * @return {boolean} visible True if this line is visible 2728 */ 2729 links.Graph.prototype._getLineVisible = function(column) { 2730 if (this.lines && column < this.lines.length) { 2731 var line = this.lines[column]; 2732 if (line && line.visible != undefined) 2733 return line.visible; 2734 } 2735 2736 if (this.line && this.line.visible != undefined) 2737 return this.line.visible; 2738 2739 return true; 2740 }; 2741 2742 2743 /** 2744 * Change the visibility of a line 2745 * @param {int} column The column number (one based) 2746 * @param {boolean} visible True if this line must be visible 2747 */ 2748 links.Graph.prototype._setLineVisible = function(column, visible) { 2749 column = parseInt(column); 2750 if (column < 0) 2751 return; 2752 2753 if (!this.lines) 2754 this.lines = []; 2755 2756 if (!this.lines[column]) 2757 this.lines[column] = {}; 2758 2759 this.lines[column].visible = visible; 2760 }; 2761 2762 /** 2763 * Check if the current frame size corresponds with the end Date. If the size 2764 * does not correspond, the end Date is changed to match the frame size. 2765 * 2766 * This function is used before a mousedown and scroll event, to check if 2767 * the frame size is not changed (caused by resizing events on the page). 2768 */ 2769 links.Graph.prototype._checkSize = function() { 2770 if (this.lastMainWidth != this.main.clientWidth || 2771 this.lastMainHeight != this.main.clientHeight) { 2772 2773 var diff = this.main.clientWidth - this.lastMainWidth; 2774 2775 // recalculate the current end Date based on the real size of the frame 2776 this.end = new Date((this.frame.clientWidth + diff) / (this.frame.clientWidth) * 2777 (this.end.valueOf() - this.start.valueOf()) + 2778 this.start.valueOf() ); 2779 // startEnd is the stored end position on start of a mouse movement 2780 if (this.startEnd) { 2781 this.startEnd = new Date((this.frame.clientWidth + diff) / (this.frame.clientWidth) * 2782 (this.startEnd.valueOf() - this.start.valueOf()) + 2783 this.start.valueOf() ); 2784 } 2785 2786 // redraw the graph 2787 this.redraw(); 2788 } 2789 }; 2790 2791 /** 2792 * Start a moving operation inside the provided parent element 2793 * @param {Event} event The event that occurred (required for 2794 * retrieving the mouse position) 2795 */ 2796 links.Graph.prototype._onMouseDown = function(event) { 2797 event = event || window.event; 2798 2799 if (!this.moveable) 2800 return; 2801 2802 // check if mouse is still down (may be up when focus is lost for example 2803 // in an iframe) 2804 if (this.leftButtonDown) { 2805 this.onMouseUp(event); 2806 } 2807 2808 // only react on left mouse button down 2809 this.leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); 2810 if (!this.leftButtonDown && !this.touchDown) { 2811 return; 2812 } 2813 2814 // check if frame is not resized (causing a mismatch with the end Date) 2815 this._checkSize(); 2816 2817 // get mouse position 2818 this.startMouseX = links.Graph._getPageX(event); 2819 this.startMouseY = links.Graph._getPageY(event); 2820 2821 this.startStart = new Date(this.start.valueOf()); 2822 this.startEnd = new Date(this.end.valueOf()); 2823 this.startVStart = this.vStart; 2824 this.startVEnd = this.vEnd; 2825 this.startGraphLeft = parseFloat(this.frame.canvas.graph.style.left); 2826 this.startAxisLeft = parseFloat(this.frame.canvas.axis.style.left); 2827 2828 this.frame.style.cursor = 'move'; 2829 2830 // add event listeners to handle moving the contents 2831 // we store the function onmousemove and onmouseup in the graph, so we can 2832 // remove the eventlisteners lateron in the function mouseUp() 2833 var me = this; 2834 if (!this.onmousemove) { 2835 this.onmousemove = function (event) {me._onMouseMove(event);}; 2836 links.Graph.addEventListener(document, "mousemove", this.onmousemove); 2837 } 2838 if (!this.onmouseup) { 2839 this.onmouseup = function (event) {me._onMouseUp(event);}; 2840 links.Graph.addEventListener(document, "mouseup", this.onmouseup); 2841 } 2842 links.Graph.preventDefault(event); 2843 }; 2844 2845 2846 /** 2847 * Perform moving operating. 2848 * This function activated from within the funcion links.Graph._onMouseDown(). 2849 * @param {Event} event Well, eehh, the event 2850 */ 2851 links.Graph.prototype._onMouseMove = function (event) { 2852 event = event || window.event; 2853 2854 var mouseX = links.Graph._getPageX(event); 2855 var mouseY = links.Graph._getPageY(event); 2856 2857 // calculate change in mouse position 2858 var diffX = mouseX - this.startMouseX; 2859 //var diffY = mouseY - this.startMouseY; 2860 var diffY = this.screenToY(this.startMouseY) - this.screenToY(mouseY); 2861 var diffYs = mouseY - this.startMouseY; 2862 2863 // FIXME: on millisecond scale this.start needs to be rounded to integer milliseconds. 2864 var diffMillisecs = (-diffX) / this.frame.clientWidth * 2865 (this.startEnd.valueOf() - this.startStart.valueOf()); 2866 2867 var newStart = new Date(this.startStart.valueOf() + Math.round(diffMillisecs)); 2868 var newEnd = new Date(this.startEnd.valueOf() + Math.round(diffMillisecs)); 2869 this._applyRange(newStart, newEnd); 2870 2871 // if the applied range is moved due to a fixed min or max, 2872 // change the diffMillisecs and diffX accordingly 2873 var appliedDiff = (this.start.valueOf() - newStart.valueOf()); 2874 if (appliedDiff) { 2875 diffMillisecs += appliedDiff; 2876 diffX = -diffMillisecs * this.frame.clientWidth / 2877 (this.startEnd.valueOf() - this.startStart.valueOf()); 2878 } 2879 2880 // adjust vertical axis setting when needed 2881 // TODO: put that in a separate method _applyVerticalRange() 2882 var vStartNew = this.startVStart + diffY; 2883 var vEndNew = this.startVEnd + diffY; 2884 var d; 2885 if (vStartNew < this.vMin) { 2886 d = (this.vMin - vStartNew); 2887 vStartNew += d; 2888 vEndNew += d; 2889 } 2890 if (vEndNew > this.vMax) { 2891 d = (vEndNew - this.vMax); 2892 vStartNew -= d; 2893 vEndNew -= d; 2894 } 2895 var epsilon = (this.vEnd - this.vStart) / 1000000; 2896 var movedVertically = (Math.abs(vStartNew - this.vStart) > epsilon || 2897 Math.abs(vEndNew - this.vEnd) > epsilon); 2898 if (movedVertically) { 2899 this.vStart = vStartNew; 2900 this.vEnd = vEndNew; 2901 } 2902 2903 if ((!this.redrawWhileMoving || 2904 Math.abs(this.startAxisLeft + diffX) < this.axisMargin) && 2905 !movedVertically) { 2906 // move the horizontal axis and data(this is fast) 2907 this.frame.canvas.axis.style.left = links.Graph.px(this.startAxisLeft + diffX); 2908 this.frame.canvas.graph.style.left = links.Graph.px(this.startGraphLeft + diffX); 2909 } 2910 else { 2911 // redraw the horizontal and vertical axis and the data (this is slow) 2912 this._redrawVerticalAxis(); 2913 2914 this.frame.canvas.axis.style.left = links.Graph.px(0); 2915 this.startAxisLeft = -diffX; 2916 this._redrawHorizontalAxis(); 2917 2918 this.frame.canvas.graph.style.left = links.Graph.px(0); 2919 this.startGraphLeft = -diffX - this.axisMargin; 2920 this._redrawData(); 2921 } 2922 this._redrawAxisLeftMajorLabel(); // reposition the left major label 2923 this._redrawDataTooltip(); 2924 2925 // fire a rangechange event 2926 var properties = { 2927 'start': new Date(this.start.valueOf()), 2928 'end': new Date(this.end.valueOf()) 2929 }; 2930 this.trigger('rangechange', properties); 2931 2932 links.Graph.preventDefault(event); 2933 }; 2934 2935 2936 /** 2937 * Perform mouse out, but simulate mouse leave. 2938 * This function force the tooltip to hide when the mouse leaves the frame. 2939 * It is also called (as an event listener) when the graph is dragged and the 2940 * mouse button is released. This way the tooltip can be hidden after a drag. 2941 * @param {Event} event 2942 */ 2943 links.Graph.prototype._onMouseOut = function (event) { 2944 event = event || window.event; 2945 var me = this; 2946 2947 // Do not hide when dragging the graph 2948 if (event.which > 0 && event.type == 'mouseout' ) { 2949 if (!this.onmouseupoutside) { 2950 this.onmouseupoutside = function (event) {me._onMouseOut(event);}; 2951 links.Graph.addEventListener(document, "mouseup", this.onmouseupoutside); 2952 } 2953 return; 2954 } 2955 2956 // Remove event listener when mouse is released outside of graph 2957 if (event.type == 'mouseup') { 2958 if (this.onmouseupoutside) { 2959 links.Graph.removeEventListener(document, "mouseup", this.onmouseupoutside); 2960 this.onmouseupoutside = undefined; 2961 } 2962 } 2963 2964 if (links.Graph.isOutside(event, this.frame)) 2965 this._setTooltip(undefined); 2966 } 2967 2968 /** 2969 * Perform mouse hover 2970 * @param {Event} event 2971 */ 2972 links.Graph.prototype._onMouseHover = function (event) { 2973 event = event || window.event; 2974 2975 /* TODO: check target 2976 var target = event.target || event.srcElement; 2977 console.log(target == this.frame.canvas) 2978 if (target != this.frame.canvas) { 2979 return; 2980 }*/ 2981 2982 // TODO: handle touch 2983 if (this.leftButtonDown) { 2984 return; 2985 } 2986 2987 var mouseX = links.Graph._getPageX(event); 2988 var mouseY = links.Graph._getPageY(event); 2989 var offsetX = links.Graph._getAbsoluteLeft(this.frame.canvas); 2990 var offsetY = links.Graph._getAbsoluteTop(this.frame.canvas); 2991 2992 // calculate the timestamp from the mouse position 2993 var date = this._screenToTime(mouseX - offsetX); 2994 var value = this.screenToY(mouseY - offsetY); 2995 2996 // find the value closest to the current date 2997 var dataPoint = this._findClosestDataPoint(date, value); 2998 this._setTooltip(dataPoint); 2999 }; 3000 3001 /** 3002 * Stop moving operating. 3003 * This function activated from within the function links.Graph._onMouseDown(). 3004 * @param {event} event The event 3005 */ 3006 links.Graph.prototype._onMouseUp = function (event) { 3007 this.frame.style.cursor = 'auto'; 3008 this.leftButtonDown = false; 3009 3010 this.frame.canvas.axis.style.left = links.Graph.px(0); 3011 this._redrawHorizontalAxis(); 3012 3013 this.frame.canvas.graph.style.left = links.Graph.px(0); 3014 this._redrawData(); 3015 3016 // fire a rangechanged event 3017 var properties = { 3018 'start': new Date(this.start.valueOf()), 3019 'end': new Date(this.end.valueOf()) 3020 }; 3021 this.trigger('rangechanged', properties); 3022 3023 // remove event listeners 3024 if (this.onmousemove) { 3025 links.Graph.removeEventListener(document, "mousemove", this.onmousemove); 3026 this.onmousemove = undefined; 3027 } 3028 if (this.onmouseup) { 3029 links.Graph.removeEventListener(document, "mouseup", this.onmouseup); 3030 this.onmouseup = undefined; 3031 } 3032 links.Graph.preventDefault(event); 3033 }; 3034 3035 3036 3037 /** 3038 * Event handler for touchstart event on mobile devices 3039 */ 3040 links.Graph.prototype._onTouchStart = function(event) { 3041 links.Graph.preventDefault(event); 3042 3043 if (this.touchDown) { 3044 // if already moving, return 3045 return; 3046 } 3047 this.touchDown = true; 3048 3049 var me = this; 3050 if (!this.ontouchmove) { 3051 this.ontouchmove = function (event) {me._onTouchMove(event);}; 3052 links.Graph.addEventListener(document, "touchmove", this.ontouchmove); 3053 } 3054 if (!this.ontouchend) { 3055 this.ontouchend = function (event) {me._onTouchEnd(event);}; 3056 links.Graph.addEventListener(document, "touchend", this.ontouchend); 3057 } 3058 3059 this._onMouseDown(event); 3060 }; 3061 3062 /** 3063 * Event handler for touchmove event on mobile devices 3064 */ 3065 links.Graph.prototype._onTouchMove = function(event) { 3066 links.Graph.preventDefault(event); 3067 this._onMouseMove(event); 3068 }; 3069 3070 /** 3071 * Event handler for touchend event on mobile devices 3072 */ 3073 links.Graph.prototype._onTouchEnd = function(event) { 3074 links.Graph.preventDefault(event); 3075 this.touchDown = false; 3076 3077 if (this.ontouchmove) { 3078 links.Graph.removeEventListener(document, "touchmove", this.ontouchmove); 3079 this.ontouchmove = undefined; 3080 } 3081 if (this.ontouchend) { 3082 links.Graph.removeEventListener(document, "touchend", this.ontouchend); 3083 this.ontouchend = undefined; 3084 } 3085 3086 this._onMouseUp(event); 3087 }; 3088 3089 3090 /** 3091 * Event handler for mouse wheel event, used to zoom the graph 3092 * Code from http://adomas.org/javascript-mouse-wheel/ 3093 * @param {event} event The event 3094 */ 3095 links.Graph.prototype._onWheel = function(event) { 3096 event = event || window.event; 3097 3098 if (!this.zoomable) 3099 return; 3100 3101 // retrieve delta 3102 var delta = 0; 3103 if (event.wheelDelta) { /* IE/Opera. */ 3104 delta = event.wheelDelta/120; 3105 } else if (event.detail) { /* Mozilla case. */ 3106 // In Mozilla, sign of delta is different than in IE. 3107 // Also, delta is multiple of 3. 3108 delta = -event.detail/3; 3109 } 3110 3111 // If delta is nonzero, handle it. 3112 // Basically, delta is now positive if wheel was scrolled up, 3113 // and negative, if wheel was scrolled down. 3114 if (delta) { 3115 // check if frame is not resized (causing a mismatch with the end date) 3116 this._checkSize(); 3117 3118 // perform the zoom action. Delta is normally 1 or -1 3119 var zoomFactor = delta / 5.0; 3120 3121 if (!event.shiftKey) { 3122 // zoom horizontally 3123 var zoomAroundDate; 3124 var frameLeft = links.Graph._getAbsoluteLeft(this.frame); 3125 if (event.clientX != undefined && frameLeft != undefined ) { 3126 var x = event.clientX - frameLeft; 3127 zoomAroundDate = this._screenToTime(x); 3128 } 3129 else { 3130 zoomAroundDate = undefined; 3131 } 3132 this._zoom(zoomFactor, zoomAroundDate); 3133 3134 // fire a rangechange event 3135 var properties = { 3136 'start': new Date(this.start.valueOf()), 3137 'end': new Date(this.end.valueOf()) 3138 }; 3139 this.trigger('rangechange', properties); 3140 this.trigger('rangechanged', properties); 3141 } 3142 else { 3143 // zoom vertically 3144 var zoomAroundValue; 3145 var frameTop = links.Graph._getAbsoluteTop(this.frame); 3146 if (event.clientY != undefined && frameTop != undefined ) { 3147 var y = event.clientY - frameTop; 3148 zoomAroundValue = this.screenToY(y); 3149 } 3150 else { 3151 zoomAroundValue = undefined; 3152 } 3153 this._zoomVertical (zoomFactor, zoomAroundValue); 3154 3155 } 3156 } 3157 3158 // Prevent default actions caused by mouse wheel. 3159 // That might be ugly, but we handle scrolls somehow 3160 // anyway, so don't bother here.. 3161 if (event.preventDefault) 3162 event.preventDefault(); 3163 event.returnValue = false; 3164 }; 3165 3166 /** 3167 * Retrieve the absolute left value of a DOM element 3168 * @param {Element} elem A dom element, for example a div 3169 * @return {number} left The absolute left position of this element 3170 * in the browser page. 3171 */ 3172 links.Graph._getAbsoluteLeft = function(elem) { 3173 var doc = document.documentElement; 3174 var body = document.body; 3175 3176 var left = elem.offsetLeft; 3177 var e = elem.offsetParent; 3178 while (e != null && e != body && e != doc) { 3179 left += e.offsetLeft; 3180 left -= e.scrollLeft; 3181 e = e.offsetParent; 3182 } 3183 return left; 3184 }; 3185 3186 /** 3187 * Retrieve the absolute top value of a DOM element 3188 * @param {Element} elem A dom element, for example a div 3189 * @return {number} top The absolute top position of this element 3190 * in the browser page. 3191 */ 3192 links.Graph._getAbsoluteTop = function(elem) { 3193 var doc = document.documentElement; 3194 var body = document.body; 3195 3196 var top = elem.offsetTop; 3197 var e = elem.offsetParent; 3198 while (e != null && e != body && e != doc) { 3199 top += e.offsetTop; 3200 top -= e.scrollTop; 3201 e = e.offsetParent; 3202 } 3203 return top; 3204 }; 3205 3206 /** 3207 * Get the absolute, vertical mouse position from an event. 3208 * @param {Event} event 3209 * @return {Number} pageY 3210 */ 3211 links.Graph._getPageY = function (event) { 3212 if (('targetTouches' in event) && event.targetTouches.length) { 3213 event = event.targetTouches[0]; 3214 } 3215 3216 if ('pageY' in event) { 3217 return event.pageY; 3218 } 3219 3220 // calculate pageY from clientY 3221 var clientY = event.clientY; 3222 var doc = document.documentElement; 3223 var body = document.body; 3224 return clientY + 3225 ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - 3226 ( doc && doc.clientTop || body && body.clientTop || 0 ); 3227 }; 3228 3229 /** 3230 * Get the absolute, horizontal mouse position from an event. 3231 * @param {Event} event 3232 * @return {Number} pageX 3233 */ 3234 links.Graph._getPageX = function (event) { 3235 if (('targetTouches' in event) && event.targetTouches.length) { 3236 event = event.targetTouches[0]; 3237 } 3238 3239 if ('pageX' in event) { 3240 return event.pageX; 3241 } 3242 3243 // calculate pageX from clientX 3244 var clientX = event.clientX; 3245 var doc = document.documentElement; 3246 var body = document.body; 3247 return clientX + 3248 ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - 3249 ( doc && doc.clientLeft || body && body.clientLeft || 0 ); 3250 }; 3251 3252 /** 3253 * Set a new value for the visible range int the Graph. 3254 * Set start to null to include everything from the earliest date to end. 3255 * Set end to null to include everything from start to the last date. 3256 * Example usage: 3257 * myGraph.setVisibleChartRange(new Date("2010-08-22"), 3258 * new Date("2010-09-13")); 3259 * @param {Date} start The start date for the graph 3260 * @param {Date} end The end date for the graph 3261 * @param {Boolean} redrawNow Optional. If true (default), the graph is 3262 * automatically redrawn after the range is changed 3263 */ 3264 links.Graph.prototype.setVisibleChartRange = function(start, end, redrawNow) { 3265 var col, cols, rowRange, d; 3266 3267 // TODO: rewrite this method for the new data format 3268 if (start != null) { 3269 // clone the value 3270 start = new Date(start.valueOf()); 3271 } else { 3272 // use earliest date from the data 3273 var startValue = null; // number 3274 for (col = 0, cols = this.data.length; col < cols; col++) { 3275 rowRange = this.data[col].rowRange; 3276 if (rowRange) { 3277 d = rowRange.min; 3278 3279 if (d != undefined) { 3280 if (startValue != undefined) { 3281 startValue = Math.min(startValue, d.valueOf()); 3282 } 3283 else { 3284 startValue = d.valueOf(); 3285 } 3286 } 3287 } 3288 } 3289 3290 if (startValue != undefined) { 3291 start = new Date(startValue); 3292 } 3293 else { 3294 start = new Date(); 3295 } 3296 } 3297 3298 if (end != null) { 3299 // clone the value 3300 end = new Date(end.valueOf()); 3301 } else { 3302 // use lastest date from the data 3303 var endValue = null; 3304 for (col = 0, cols = this.data.length; col < cols; col++) { 3305 rowRange = this.data[col].rowRange; 3306 if (rowRange) { 3307 d = rowRange.max; 3308 3309 if (d != undefined) { 3310 if (endValue != undefined) { 3311 endValue = Math.max(endValue, d.valueOf()); 3312 } 3313 else { 3314 endValue = d; 3315 } 3316 } 3317 } 3318 } 3319 3320 if (endValue != undefined) { 3321 end = new Date(endValue); 3322 } else { 3323 end = new Date(); 3324 end.setDate(this.end.getDate() + 20); 3325 } 3326 } 3327 3328 // prevent start Date <= end Date 3329 if (end.valueOf() <= start.valueOf()) { 3330 end = new Date(start.valueOf()); 3331 end.setDate(end.getDate() + 20); 3332 } 3333 3334 // apply new start and end 3335 this._applyRange(start, end); 3336 3337 this._calcConversionFactor(); 3338 3339 if (redrawNow == undefined) { 3340 redrawNow = true; 3341 } 3342 if (redrawNow) { 3343 this.redraw(); 3344 } 3345 }; 3346 3347 /** 3348 * Adjust the visible chart range to fit the contents. 3349 */ 3350 links.Graph.prototype.setVisibleChartRangeAuto = function() { 3351 this.setVisibleChartRange(undefined, undefined); 3352 }; 3353 3354 /** 3355 * Adjust the visible range such that the current time is located in the center 3356 * of the graph 3357 */ 3358 links.Graph.prototype.setVisibleChartRangeNow = function() { 3359 var now = new Date(); 3360 3361 var diff = (this.end.valueOf() - this.start.valueOf()); 3362 3363 var startNew = new Date(now.valueOf() - diff/2); 3364 var endNew = new Date(startNew.valueOf() + diff); 3365 this.setVisibleChartRange(startNew, endNew); 3366 }; 3367 3368 /** 3369 * Retrieve the current visible range in the Graph. 3370 * @return {Object} An object with start and end properties 3371 */ 3372 links.Graph.prototype.getVisibleChartRange = function() { 3373 return { 3374 'start': new Date(this.start.valueOf()), 3375 'end': new Date(this.end.valueOf()) 3376 }; 3377 }; 3378 3379 /** 3380 * Retrieve the current value range (range on the vertical axis) 3381 */ 3382 links.Graph.prototype.getValueRange = function() { 3383 return { 3384 'start': this.vStart, 3385 'end': this.vEnd 3386 }; 3387 }; 3388 3389 /** 3390 * Set vertical value range 3391 * @param {Number} start Start of the range. If undefined, start will 3392 * be set to match the minimum data value 3393 * @param {Number} end End of the range. If undefined, end will 3394 * be set to match the maximum data value 3395 * @param {Boolean} redrawNow Optional. If true (default) the graph is 3396 * redrawn after the range has been changed 3397 */ 3398 links.Graph.prototype.setValueRange = function(start, end, redrawNow) { 3399 this.vStart = start ? Number(start) : undefined; 3400 this.vEnd = end ? Number(end) : undefined; 3401 3402 if (this.vEnd <= this.vStart) { 3403 this.vEnd = undefined; 3404 } 3405 3406 if (redrawNow == undefined) { 3407 redrawNow = true; 3408 } 3409 if (redrawNow) { 3410 this.redraw(); 3411 } 3412 }; 3413 3414 /** 3415 * Adjust the vertical range to auto fit the contents 3416 */ 3417 links.Graph.prototype.setValueRangeAuto = function() { 3418 this.setValueRange(undefined, undefined); 3419 }; 3420 3421 3422 /** ------------------------------------------------------------------------ **/ 3423 3424 3425 /** 3426 * Event listener (singleton) 3427 */ 3428 links.events = links.events || { 3429 'listeners': [], 3430 3431 /** 3432 * Find a single listener by its object 3433 * @param {Object} object 3434 * @return {Number} index -1 when not found 3435 */ 3436 'indexOf': function (object) { 3437 var listeners = this.listeners; 3438 for (var i = 0, iMax = this.listeners.length; i < iMax; i++) { 3439 var listener = listeners[i]; 3440 if (listener && listener.object == object) { 3441 return i; 3442 } 3443 } 3444 return -1; 3445 }, 3446 3447 /** 3448 * Add an event listener 3449 * @param {Object} object 3450 * @param {String} event The name of an event, for example 'select' 3451 * @param {function} callback The callback method, called when the 3452 * event takes place 3453 */ 3454 'addListener': function (object, event, callback) { 3455 var index = this.indexOf(object); 3456 var listener = this.listeners[index]; 3457 if (!listener) { 3458 listener = { 3459 'object': object, 3460 'events': {} 3461 }; 3462 this.listeners.push(listener); 3463 } 3464 3465 var callbacks = listener.events[event]; 3466 if (!callbacks) { 3467 callbacks = []; 3468 listener.events[event] = callbacks; 3469 } 3470 3471 // add the callback if it does not yet exist 3472 if (callbacks.indexOf(callback) == -1) { 3473 callbacks.push(callback); 3474 } 3475 }, 3476 3477 /** 3478 * Remove an event listener 3479 * @param {Object} object 3480 * @param {String} event The name of an event, for example 'select' 3481 * @param {function} callback The registered callback method 3482 */ 3483 'removeListener': function (object, event, callback) { 3484 var index = this.indexOf(object); 3485 var listener = this.listeners[index]; 3486 if (listener) { 3487 var callbacks = listener.events[event]; 3488 if (callbacks) { 3489 var index = callbacks.indexOf(callback); 3490 if (index != -1) { 3491 callbacks.splice(index, 1); 3492 } 3493 3494 // remove the array when empty 3495 if (callbacks.length == 0) { 3496 delete listener.events[event]; 3497 } 3498 } 3499 3500 // count the number of registered events. remove listener when empty 3501 var count = 0; 3502 var events = listener.events; 3503 for (var event in events) { 3504 if (events.hasOwnProperty(event)) { 3505 count++; 3506 } 3507 } 3508 if (count == 0) { 3509 delete this.listeners[index]; 3510 } 3511 } 3512 }, 3513 3514 /** 3515 * Remove all registered event listeners 3516 */ 3517 'removeAllListeners': function () { 3518 this.listeners = []; 3519 }, 3520 3521 /** 3522 * Trigger an event. All registered event handlers will be called 3523 * @param {Object} object 3524 * @param {String} event 3525 * @param {Object} properties (optional) 3526 */ 3527 'trigger': function (object, event, properties) { 3528 var index = this.indexOf(object); 3529 var listener = this.listeners[index]; 3530 if (listener) { 3531 var callbacks = listener.events[event]; 3532 if (callbacks) { 3533 for (var i = 0, iMax = callbacks.length; i < iMax; i++) { 3534 callbacks[i](properties); 3535 } 3536 } 3537 } 3538 } 3539 }; 3540 3541 3542 /** ------------------------------------------------------------------------ **/ 3543 3544 3545 /** 3546 * Add and event listener. Works for all browsers 3547 * @param {Element} element An html element 3548 * @param {string} action The action, for example "click", 3549 * without the prefix "on" 3550 * @param {function} listener The callback function to be executed 3551 * @param {boolean} useCapture 3552 */ 3553 links.Graph.addEventListener = function (element, action, listener, useCapture) { 3554 if (element.addEventListener) { 3555 if (useCapture == undefined) 3556 useCapture = false; 3557 3558 if (action == "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { 3559 action = "DOMMouseScroll"; // For Firefox 3560 } 3561 3562 element.addEventListener(action, listener, useCapture); 3563 } else { 3564 element.attachEvent("on" + action, listener); // IE browsers 3565 } 3566 }; 3567 3568 /** 3569 * Remove an event listener from an element 3570 * @param {Element} element An html dom element 3571 * @param {string} action The name of the event, for example "mousedown" 3572 * @param {function} listener The listener function 3573 * @param {boolean} useCapture 3574 */ 3575 links.Graph.removeEventListener = function(element, action, listener, useCapture) { 3576 if (element.removeEventListener) { 3577 // non-IE browsers 3578 if (useCapture == undefined) 3579 useCapture = false; 3580 3581 if (action == "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { 3582 action = "DOMMouseScroll"; // For Firefox 3583 } 3584 3585 element.removeEventListener(action, listener, useCapture); 3586 } else { 3587 // IE browsers 3588 element.detachEvent("on" + action, listener); 3589 } 3590 }; 3591 3592 3593 /** 3594 * Stop event propagation 3595 */ 3596 links.Graph.stopPropagation = function (event) { 3597 if (!event) 3598 event = window.event; 3599 3600 if (event.stopPropagation) { 3601 event.stopPropagation(); // non-IE browsers 3602 } 3603 else { 3604 event.cancelBubble = true; // IE browsers 3605 } 3606 }; 3607 3608 3609 /** 3610 * Cancels the event if it is cancelable, without stopping further propagation of the event. 3611 */ 3612 links.Graph.preventDefault = function (event) { 3613 if (!event) 3614 event = window.event; 3615 3616 if (event.preventDefault) { 3617 event.preventDefault(); // non-IE browsers 3618 } 3619 else { 3620 event.returnValue = false; // IE browsers 3621 } 3622 }; 3623 3624 /** 3625 * Check if an event took place outside a specified parent element. 3626 * @param {Event} event A javascript (mouse) event object 3627 * @param {Element} parent The DOM element to check if event was inside it 3628 * @return {boolean} 3629 */ 3630 links.Graph.isOutside = function (event, parent) { 3631 var elem = event.relatedTarget || event.toElement || event.fromElement 3632 3633 while ( elem && elem !== parent) { 3634 elem = elem.parentNode; 3635 } 3636 3637 return elem !== parent; 3638 } 3639