1 /** 2 * @license 3 * Copyright 2006 Dan Vanderkam (danvdk@gmail.com) 4 * MIT-licensed (http://opensource.org/licenses/MIT) 5 */ 6 7 /** 8 * @fileoverview Creates an interactive, zoomable graph based on a CSV file or 9 * string. Dygraph can handle multiple series with or without error bars. The 10 * date/value ranges will be automatically set. Dygraph uses the 11 * <canvas> tag, so it only works in FF1.5+. 12 * @author danvdk@gmail.com (Dan Vanderkam) 13 14 Usage: 15 <div id="graphdiv" style="width:800px; height:500px;"></div> 16 <script type="text/javascript"> 17 new Dygraph(document.getElementById("graphdiv"), 18 "datafile.csv", // CSV file with headers 19 { }); // options 20 </script> 21 22 The CSV file is of the form 23 24 Date,SeriesA,SeriesB,SeriesC 25 YYYYMMDD,A1,B1,C1 26 YYYYMMDD,A2,B2,C2 27 28 If the 'errorBars' option is set in the constructor, the input should be of 29 the form 30 Date,SeriesA,SeriesB,... 31 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,... 32 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,... 33 34 If the 'fractions' option is set, the input should be of the form: 35 36 Date,SeriesA,SeriesB,... 37 YYYYMMDD,A1/B1,A2/B2,... 38 YYYYMMDD,A1/B1,A2/B2,... 39 40 And error bars will be calculated automatically using a binomial distribution. 41 42 For further documentation and examples, see http://dygraphs.com/ 43 */ 44 45 import DygraphLayout from './dygraph-layout'; 46 import DygraphCanvasRenderer from './dygraph-canvas'; 47 import DygraphOptions from './dygraph-options'; 48 import DygraphInteraction from './dygraph-interaction-model'; 49 import * as DygraphTickers from './dygraph-tickers'; 50 import * as utils from './dygraph-utils'; 51 import DEFAULT_ATTRS from './dygraph-default-attrs'; 52 import OPTIONS_REFERENCE from './dygraph-options-reference'; 53 import IFrameTarp from './iframe-tarp'; 54 55 import DefaultHandler from './datahandler/default'; 56 import ErrorBarsHandler from './datahandler/bars-error'; 57 import CustomBarsHandler from './datahandler/bars-custom'; 58 import DefaultFractionHandler from './datahandler/default-fractions'; 59 import FractionsBarsHandler from './datahandler/bars-fractions'; 60 import BarsHandler from './datahandler/bars'; 61 62 import AnnotationsPlugin from './plugins/annotations'; 63 import AxesPlugin from './plugins/axes'; 64 import ChartLabelsPlugin from './plugins/chart-labels'; 65 import GridPlugin from './plugins/grid'; 66 import LegendPlugin from './plugins/legend'; 67 import RangeSelectorPlugin from './plugins/range-selector'; 68 69 import GVizChart from './dygraph-gviz'; 70 71 "use strict"; 72 73 /** 74 * Creates an interactive, zoomable chart. 75 * 76 * @constructor 77 * @param {div | String} div A div or the id of a div into which to construct 78 * the chart. 79 * @param {String | Function} file A file containing CSV data or a function 80 * that returns this data. The most basic expected format for each line is 81 * "YYYY/MM/DD,val1,val2,...". For more information, see 82 * http://dygraphs.com/data.html. 83 * @param {Object} attrs Various other attributes, e.g. errorBars determines 84 * whether the input data contains error ranges. For a complete list of 85 * options, see http://dygraphs.com/options.html. 86 */ 87 var Dygraph = function(div, data, opts) { 88 this.__init__(div, data, opts); 89 }; 90 91 Dygraph.NAME = "Dygraph"; 92 Dygraph.VERSION = "2.0.0"; 93 94 // Various default values 95 Dygraph.DEFAULT_ROLL_PERIOD = 1; 96 Dygraph.DEFAULT_WIDTH = 480; 97 Dygraph.DEFAULT_HEIGHT = 320; 98 99 // For max 60 Hz. animation: 100 Dygraph.ANIMATION_STEPS = 12; 101 Dygraph.ANIMATION_DURATION = 200; 102 103 /** 104 * Standard plotters. These may be used by clients. 105 * Available plotters are: 106 * - Dygraph.Plotters.linePlotter: draws central lines (most common) 107 * - Dygraph.Plotters.errorPlotter: draws error bars 108 * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph) 109 * 110 * By default, the plotter is [fillPlotter, errorPlotter, linePlotter]. 111 * This causes all the lines to be drawn over all the fills/error bars. 112 */ 113 Dygraph.Plotters = DygraphCanvasRenderer._Plotters; 114 115 116 // Used for initializing annotation CSS rules only once. 117 Dygraph.addedAnnotationCSS = false; 118 119 /** 120 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit 121 * and context <canvas> inside of it. See the constructor for details. 122 * on the parameters. 123 * @param {Element} div the Element to render the graph into. 124 * @param {string | Function} file Source data 125 * @param {Object} attrs Miscellaneous other options 126 * @private 127 */ 128 Dygraph.prototype.__init__ = function(div, file, attrs) { 129 this.is_initial_draw_ = true; 130 this.readyFns_ = []; 131 132 // Support two-argument constructor 133 if (attrs === null || attrs === undefined) { attrs = {}; } 134 135 attrs = Dygraph.copyUserAttrs_(attrs); 136 137 if (typeof(div) == 'string') { 138 div = document.getElementById(div); 139 } 140 141 if (!div) { 142 throw new Error('Constructing dygraph with a non-existent div!'); 143 } 144 145 // Copy the important bits into the object 146 // TODO(danvk): most of these should just stay in the attrs_ dictionary. 147 this.maindiv_ = div; 148 this.file_ = file; 149 this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD; 150 this.previousVerticalX_ = -1; 151 this.fractions_ = attrs.fractions || false; 152 this.dateWindow_ = attrs.dateWindow || null; 153 154 this.annotations_ = []; 155 156 // Clear the div. This ensure that, if multiple dygraphs are passed the same 157 // div, then only one will be drawn. 158 div.innerHTML = ""; 159 160 // For historical reasons, the 'width' and 'height' options trump all CSS 161 // rules _except_ for an explicit 'width' or 'height' on the div. 162 // As an added convenience, if the div has zero height (like <div></div> does 163 // without any styles), then we use a default height/width. 164 if (div.style.width === '' && attrs.width) { 165 div.style.width = attrs.width + "px"; 166 } 167 if (div.style.height === '' && attrs.height) { 168 div.style.height = attrs.height + "px"; 169 } 170 if (div.style.height === '' && div.clientHeight === 0) { 171 div.style.height = Dygraph.DEFAULT_HEIGHT + "px"; 172 if (div.style.width === '') { 173 div.style.width = Dygraph.DEFAULT_WIDTH + "px"; 174 } 175 } 176 // These will be zero if the dygraph's div is hidden. In that case, 177 // use the user-specified attributes if present. If not, use zero 178 // and assume the user will call resize to fix things later. 179 this.width_ = div.clientWidth || attrs.width || 0; 180 this.height_ = div.clientHeight || attrs.height || 0; 181 182 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_. 183 if (attrs.stackedGraph) { 184 attrs.fillGraph = true; 185 // TODO(nikhilk): Add any other stackedGraph checks here. 186 } 187 188 // DEPRECATION WARNING: All option processing should be moved from 189 // attrs_ and user_attrs_ to options_, which holds all this information. 190 // 191 // Dygraphs has many options, some of which interact with one another. 192 // To keep track of everything, we maintain two sets of options: 193 // 194 // this.user_attrs_ only options explicitly set by the user. 195 // this.attrs_ defaults, options derived from user_attrs_, data. 196 // 197 // Options are then accessed this.attr_('attr'), which first looks at 198 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent 199 // defaults without overriding behavior that the user specifically asks for. 200 this.user_attrs_ = {}; 201 utils.update(this.user_attrs_, attrs); 202 203 // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified. 204 this.attrs_ = {}; 205 utils.updateDeep(this.attrs_, DEFAULT_ATTRS); 206 207 this.boundaryIds_ = []; 208 this.setIndexByName_ = {}; 209 this.datasetIndex_ = []; 210 211 this.registeredEvents_ = []; 212 this.eventListeners_ = {}; 213 214 this.attributes_ = new DygraphOptions(this); 215 216 // Create the containing DIV and other interactive elements 217 this.createInterface_(); 218 219 // Activate plugins. 220 this.plugins_ = []; 221 var plugins = Dygraph.PLUGINS.concat(this.getOption('plugins')); 222 for (var i = 0; i < plugins.length; i++) { 223 // the plugins option may contain either plugin classes or instances. 224 // Plugin instances contain an activate method. 225 var Plugin = plugins[i]; // either a constructor or an instance. 226 var pluginInstance; 227 if (typeof(Plugin.activate) !== 'undefined') { 228 pluginInstance = Plugin; 229 } else { 230 pluginInstance = new Plugin(); 231 } 232 233 var pluginDict = { 234 plugin: pluginInstance, 235 events: {}, 236 options: {}, 237 pluginOptions: {} 238 }; 239 240 var handlers = pluginInstance.activate(this); 241 for (var eventName in handlers) { 242 if (!handlers.hasOwnProperty(eventName)) continue; 243 // TODO(danvk): validate eventName. 244 pluginDict.events[eventName] = handlers[eventName]; 245 } 246 247 this.plugins_.push(pluginDict); 248 } 249 250 // At this point, plugins can no longer register event handlers. 251 // Construct a map from event -> ordered list of [callback, plugin]. 252 for (var i = 0; i < this.plugins_.length; i++) { 253 var plugin_dict = this.plugins_[i]; 254 for (var eventName in plugin_dict.events) { 255 if (!plugin_dict.events.hasOwnProperty(eventName)) continue; 256 var callback = plugin_dict.events[eventName]; 257 258 var pair = [plugin_dict.plugin, callback]; 259 if (!(eventName in this.eventListeners_)) { 260 this.eventListeners_[eventName] = [pair]; 261 } else { 262 this.eventListeners_[eventName].push(pair); 263 } 264 } 265 } 266 267 this.createDragInterface_(); 268 269 this.start_(); 270 }; 271 272 /** 273 * Triggers a cascade of events to the various plugins which are interested in them. 274 * Returns true if the "default behavior" should be prevented, i.e. if one 275 * of the event listeners called event.preventDefault(). 276 * @private 277 */ 278 Dygraph.prototype.cascadeEvents_ = function(name, extra_props) { 279 if (!(name in this.eventListeners_)) return false; 280 281 // QUESTION: can we use objects & prototypes to speed this up? 282 var e = { 283 dygraph: this, 284 cancelable: false, 285 defaultPrevented: false, 286 preventDefault: function() { 287 if (!e.cancelable) throw "Cannot call preventDefault on non-cancelable event."; 288 e.defaultPrevented = true; 289 }, 290 propagationStopped: false, 291 stopPropagation: function() { 292 e.propagationStopped = true; 293 } 294 }; 295 utils.update(e, extra_props); 296 297 var callback_plugin_pairs = this.eventListeners_[name]; 298 if (callback_plugin_pairs) { 299 for (var i = callback_plugin_pairs.length - 1; i >= 0; i--) { 300 var plugin = callback_plugin_pairs[i][0]; 301 var callback = callback_plugin_pairs[i][1]; 302 callback.call(plugin, e); 303 if (e.propagationStopped) break; 304 } 305 } 306 return e.defaultPrevented; 307 }; 308 309 /** 310 * Fetch a plugin instance of a particular class. Only for testing. 311 * @private 312 * @param {!Class} type The type of the plugin. 313 * @return {Object} Instance of the plugin, or null if there is none. 314 */ 315 Dygraph.prototype.getPluginInstance_ = function(type) { 316 for (var i = 0; i < this.plugins_.length; i++) { 317 var p = this.plugins_[i]; 318 if (p.plugin instanceof type) { 319 return p.plugin; 320 } 321 } 322 return null; 323 }; 324 325 /** 326 * Returns the zoomed status of the chart for one or both axes. 327 * 328 * Axis is an optional parameter. Can be set to 'x' or 'y'. 329 * 330 * The zoomed status for an axis is set whenever a user zooms using the mouse 331 * or when the dateWindow or valueRange are updated. Double-clicking or calling 332 * resetZoom() resets the zoom status for the chart. 333 */ 334 Dygraph.prototype.isZoomed = function(axis) { 335 const isZoomedX = !!this.dateWindow_; 336 if (axis === 'x') return isZoomedX; 337 338 const isZoomedY = this.axes_.map(axis => !!axis.valueRange).indexOf(true) >= 0; 339 if (axis === null || axis === undefined) { 340 return isZoomedX || isZoomedY; 341 } 342 if (axis === 'y') return isZoomedY; 343 344 throw new Error(`axis parameter is [${axis}] must be null, 'x' or 'y'.`); 345 }; 346 347 /** 348 * Returns information about the Dygraph object, including its containing ID. 349 */ 350 Dygraph.prototype.toString = function() { 351 var maindiv = this.maindiv_; 352 var id = (maindiv && maindiv.id) ? maindiv.id : maindiv; 353 return "[Dygraph " + id + "]"; 354 }; 355 356 /** 357 * @private 358 * Returns the value of an option. This may be set by the user (either in the 359 * constructor or by calling updateOptions) or by dygraphs, and may be set to a 360 * per-series value. 361 * @param {string} name The name of the option, e.g. 'rollPeriod'. 362 * @param {string} [seriesName] The name of the series to which the option 363 * will be applied. If no per-series value of this option is available, then 364 * the global value is returned. This is optional. 365 * @return { ... } The value of the option. 366 */ 367 Dygraph.prototype.attr_ = function(name, seriesName) { 368 // For "production" code, this gets removed by uglifyjs. 369 if (typeof(process) !== 'undefined') { 370 if (process.env.NODE_ENV != 'production') { 371 if (typeof(OPTIONS_REFERENCE) === 'undefined') { 372 console.error('Must include options reference JS for testing'); 373 } else if (!OPTIONS_REFERENCE.hasOwnProperty(name)) { 374 console.error('Dygraphs is using property ' + name + ', which has no ' + 375 'entry in the Dygraphs.OPTIONS_REFERENCE listing.'); 376 // Only log this error once. 377 OPTIONS_REFERENCE[name] = true; 378 } 379 } 380 } 381 return seriesName ? this.attributes_.getForSeries(name, seriesName) : this.attributes_.get(name); 382 }; 383 384 /** 385 * Returns the current value for an option, as set in the constructor or via 386 * updateOptions. You may pass in an (optional) series name to get per-series 387 * values for the option. 388 * 389 * All values returned by this method should be considered immutable. If you 390 * modify them, there is no guarantee that the changes will be honored or that 391 * dygraphs will remain in a consistent state. If you want to modify an option, 392 * use updateOptions() instead. 393 * 394 * @param {string} name The name of the option (e.g. 'strokeWidth') 395 * @param {string=} opt_seriesName Series name to get per-series values. 396 * @return {*} The value of the option. 397 */ 398 Dygraph.prototype.getOption = function(name, opt_seriesName) { 399 return this.attr_(name, opt_seriesName); 400 }; 401 402 /** 403 * Like getOption(), but specifically returns a number. 404 * This is a convenience function for working with the Closure Compiler. 405 * @param {string} name The name of the option (e.g. 'strokeWidth') 406 * @param {string=} opt_seriesName Series name to get per-series values. 407 * @return {number} The value of the option. 408 * @private 409 */ 410 Dygraph.prototype.getNumericOption = function(name, opt_seriesName) { 411 return /** @type{number} */(this.getOption(name, opt_seriesName)); 412 }; 413 414 /** 415 * Like getOption(), but specifically returns a string. 416 * This is a convenience function for working with the Closure Compiler. 417 * @param {string} name The name of the option (e.g. 'strokeWidth') 418 * @param {string=} opt_seriesName Series name to get per-series values. 419 * @return {string} The value of the option. 420 * @private 421 */ 422 Dygraph.prototype.getStringOption = function(name, opt_seriesName) { 423 return /** @type{string} */(this.getOption(name, opt_seriesName)); 424 }; 425 426 /** 427 * Like getOption(), but specifically returns a boolean. 428 * This is a convenience function for working with the Closure Compiler. 429 * @param {string} name The name of the option (e.g. 'strokeWidth') 430 * @param {string=} opt_seriesName Series name to get per-series values. 431 * @return {boolean} The value of the option. 432 * @private 433 */ 434 Dygraph.prototype.getBooleanOption = function(name, opt_seriesName) { 435 return /** @type{boolean} */(this.getOption(name, opt_seriesName)); 436 }; 437 438 /** 439 * Like getOption(), but specifically returns a function. 440 * This is a convenience function for working with the Closure Compiler. 441 * @param {string} name The name of the option (e.g. 'strokeWidth') 442 * @param {string=} opt_seriesName Series name to get per-series values. 443 * @return {function(...)} The value of the option. 444 * @private 445 */ 446 Dygraph.prototype.getFunctionOption = function(name, opt_seriesName) { 447 return /** @type{function(...)} */(this.getOption(name, opt_seriesName)); 448 }; 449 450 Dygraph.prototype.getOptionForAxis = function(name, axis) { 451 return this.attributes_.getForAxis(name, axis); 452 }; 453 454 /** 455 * @private 456 * @param {string} axis The name of the axis (i.e. 'x', 'y' or 'y2') 457 * @return { ... } A function mapping string -> option value 458 */ 459 Dygraph.prototype.optionsViewForAxis_ = function(axis) { 460 var self = this; 461 return function(opt) { 462 var axis_opts = self.user_attrs_.axes; 463 if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) { 464 return axis_opts[axis][opt]; 465 } 466 467 // I don't like that this is in a second spot. 468 if (axis === 'x' && opt === 'logscale') { 469 // return the default value. 470 // TODO(konigsberg): pull the default from a global default. 471 return false; 472 } 473 474 // user-specified attributes always trump defaults, even if they're less 475 // specific. 476 if (typeof(self.user_attrs_[opt]) != 'undefined') { 477 return self.user_attrs_[opt]; 478 } 479 480 axis_opts = self.attrs_.axes; 481 if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) { 482 return axis_opts[axis][opt]; 483 } 484 // check old-style axis options 485 // TODO(danvk): add a deprecation warning if either of these match. 486 if (axis == 'y' && self.axes_[0].hasOwnProperty(opt)) { 487 return self.axes_[0][opt]; 488 } else if (axis == 'y2' && self.axes_[1].hasOwnProperty(opt)) { 489 return self.axes_[1][opt]; 490 } 491 return self.attr_(opt); 492 }; 493 }; 494 495 /** 496 * Returns the current rolling period, as set by the user or an option. 497 * @return {number} The number of points in the rolling window 498 */ 499 Dygraph.prototype.rollPeriod = function() { 500 return this.rollPeriod_; 501 }; 502 503 /** 504 * Returns the currently-visible x-range. This can be affected by zooming, 505 * panning or a call to updateOptions. 506 * Returns a two-element array: [left, right]. 507 * If the Dygraph has dates on the x-axis, these will be millis since epoch. 508 */ 509 Dygraph.prototype.xAxisRange = function() { 510 return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes(); 511 }; 512 513 /** 514 * Returns the lower- and upper-bound x-axis values of the data set. 515 */ 516 Dygraph.prototype.xAxisExtremes = function() { 517 var pad = this.getNumericOption('xRangePad') / this.plotter_.area.w; 518 if (this.numRows() === 0) { 519 return [0 - pad, 1 + pad]; 520 } 521 var left = this.rawData_[0][0]; 522 var right = this.rawData_[this.rawData_.length - 1][0]; 523 if (pad) { 524 // Must keep this in sync with dygraph-layout _evaluateLimits() 525 var range = right - left; 526 left -= range * pad; 527 right += range * pad; 528 } 529 return [left, right]; 530 }; 531 532 /** 533 * Returns the lower- and upper-bound y-axis values for each axis. These are 534 * the ranges you'll get if you double-click to zoom out or call resetZoom(). 535 * The return value is an array of [low, high] tuples, one for each y-axis. 536 */ 537 Dygraph.prototype.yAxisExtremes = function() { 538 // TODO(danvk): this is pretty inefficient 539 const packed = this.gatherDatasets_(this.rolledSeries_, null); 540 const { extremes } = packed; 541 const saveAxes = this.axes_; 542 this.computeYAxisRanges_(extremes); 543 const newAxes = this.axes_; 544 this.axes_ = saveAxes; 545 return newAxes.map(axis => axis.extremeRange); 546 } 547 548 /** 549 * Returns the currently-visible y-range for an axis. This can be affected by 550 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If 551 * called with no arguments, returns the range of the first axis. 552 * Returns a two-element array: [bottom, top]. 553 */ 554 Dygraph.prototype.yAxisRange = function(idx) { 555 if (typeof(idx) == "undefined") idx = 0; 556 if (idx < 0 || idx >= this.axes_.length) { 557 return null; 558 } 559 var axis = this.axes_[idx]; 560 return [ axis.computedValueRange[0], axis.computedValueRange[1] ]; 561 }; 562 563 /** 564 * Returns the currently-visible y-ranges for each axis. This can be affected by 565 * zooming, panning, calls to updateOptions, etc. 566 * Returns an array of [bottom, top] pairs, one for each y-axis. 567 */ 568 Dygraph.prototype.yAxisRanges = function() { 569 var ret = []; 570 for (var i = 0; i < this.axes_.length; i++) { 571 ret.push(this.yAxisRange(i)); 572 } 573 return ret; 574 }; 575 576 // TODO(danvk): use these functions throughout dygraphs. 577 /** 578 * Convert from data coordinates to canvas/div X/Y coordinates. 579 * If specified, do this conversion for the coordinate system of a particular 580 * axis. Uses the first axis by default. 581 * Returns a two-element array: [X, Y] 582 * 583 * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord 584 * instead of toDomCoords(null, y, axis). 585 */ 586 Dygraph.prototype.toDomCoords = function(x, y, axis) { 587 return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ]; 588 }; 589 590 /** 591 * Convert from data x coordinates to canvas/div X coordinate. 592 * If specified, do this conversion for the coordinate system of a particular 593 * axis. 594 * Returns a single value or null if x is null. 595 */ 596 Dygraph.prototype.toDomXCoord = function(x) { 597 if (x === null) { 598 return null; 599 } 600 601 var area = this.plotter_.area; 602 var xRange = this.xAxisRange(); 603 return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w; 604 }; 605 606 /** 607 * Convert from data x coordinates to canvas/div Y coordinate and optional 608 * axis. Uses the first axis by default. 609 * 610 * returns a single value or null if y is null. 611 */ 612 Dygraph.prototype.toDomYCoord = function(y, axis) { 613 var pct = this.toPercentYCoord(y, axis); 614 615 if (pct === null) { 616 return null; 617 } 618 var area = this.plotter_.area; 619 return area.y + pct * area.h; 620 }; 621 622 /** 623 * Convert from canvas/div coords to data coordinates. 624 * If specified, do this conversion for the coordinate system of a particular 625 * axis. Uses the first axis by default. 626 * Returns a two-element array: [X, Y]. 627 * 628 * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord 629 * instead of toDataCoords(null, y, axis). 630 */ 631 Dygraph.prototype.toDataCoords = function(x, y, axis) { 632 return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ]; 633 }; 634 635 /** 636 * Convert from canvas/div x coordinate to data coordinate. 637 * 638 * If x is null, this returns null. 639 */ 640 Dygraph.prototype.toDataXCoord = function(x) { 641 if (x === null) { 642 return null; 643 } 644 645 var area = this.plotter_.area; 646 var xRange = this.xAxisRange(); 647 648 if (!this.attributes_.getForAxis("logscale", 'x')) { 649 return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]); 650 } else { 651 var pct = (x - area.x) / area.w; 652 return utils.logRangeFraction(xRange[0], xRange[1], pct); 653 } 654 }; 655 656 /** 657 * Convert from canvas/div y coord to value. 658 * 659 * If y is null, this returns null. 660 * if axis is null, this uses the first axis. 661 */ 662 Dygraph.prototype.toDataYCoord = function(y, axis) { 663 if (y === null) { 664 return null; 665 } 666 667 var area = this.plotter_.area; 668 var yRange = this.yAxisRange(axis); 669 670 if (typeof(axis) == "undefined") axis = 0; 671 if (!this.attributes_.getForAxis("logscale", axis)) { 672 return yRange[0] + (area.y + area.h - y) / area.h * (yRange[1] - yRange[0]); 673 } else { 674 // Computing the inverse of toDomCoord. 675 var pct = (y - area.y) / area.h; 676 // Note reversed yRange, y1 is on top with pct==0. 677 return utils.logRangeFraction(yRange[1], yRange[0], pct); 678 } 679 }; 680 681 /** 682 * Converts a y for an axis to a percentage from the top to the 683 * bottom of the drawing area. 684 * 685 * If the coordinate represents a value visible on the canvas, then 686 * the value will be between 0 and 1, where 0 is the top of the canvas. 687 * However, this method will return values outside the range, as 688 * values can fall outside the canvas. 689 * 690 * If y is null, this returns null. 691 * if axis is null, this uses the first axis. 692 * 693 * @param {number} y The data y-coordinate. 694 * @param {number} [axis] The axis number on which the data coordinate lives. 695 * @return {number} A fraction in [0, 1] where 0 = the top edge. 696 */ 697 Dygraph.prototype.toPercentYCoord = function(y, axis) { 698 if (y === null) { 699 return null; 700 } 701 if (typeof(axis) == "undefined") axis = 0; 702 703 var yRange = this.yAxisRange(axis); 704 705 var pct; 706 var logscale = this.attributes_.getForAxis("logscale", axis); 707 if (logscale) { 708 var logr0 = utils.log10(yRange[0]); 709 var logr1 = utils.log10(yRange[1]); 710 pct = (logr1 - utils.log10(y)) / (logr1 - logr0); 711 } else { 712 // yRange[1] - y is unit distance from the bottom. 713 // yRange[1] - yRange[0] is the scale of the range. 714 // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom. 715 pct = (yRange[1] - y) / (yRange[1] - yRange[0]); 716 } 717 return pct; 718 }; 719 720 /** 721 * Converts an x value to a percentage from the left to the right of 722 * the drawing area. 723 * 724 * If the coordinate represents a value visible on the canvas, then 725 * the value will be between 0 and 1, where 0 is the left of the canvas. 726 * However, this method will return values outside the range, as 727 * values can fall outside the canvas. 728 * 729 * If x is null, this returns null. 730 * @param {number} x The data x-coordinate. 731 * @return {number} A fraction in [0, 1] where 0 = the left edge. 732 */ 733 Dygraph.prototype.toPercentXCoord = function(x) { 734 if (x === null) { 735 return null; 736 } 737 738 var xRange = this.xAxisRange(); 739 var pct; 740 var logscale = this.attributes_.getForAxis("logscale", 'x') ; 741 if (logscale === true) { // logscale can be null so we test for true explicitly. 742 var logr0 = utils.log10(xRange[0]); 743 var logr1 = utils.log10(xRange[1]); 744 pct = (utils.log10(x) - logr0) / (logr1 - logr0); 745 } else { 746 // x - xRange[0] is unit distance from the left. 747 // xRange[1] - xRange[0] is the scale of the range. 748 // The full expression below is the % from the left. 749 pct = (x - xRange[0]) / (xRange[1] - xRange[0]); 750 } 751 return pct; 752 }; 753 754 /** 755 * Returns the number of columns (including the independent variable). 756 * @return {number} The number of columns. 757 */ 758 Dygraph.prototype.numColumns = function() { 759 if (!this.rawData_) return 0; 760 return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length; 761 }; 762 763 /** 764 * Returns the number of rows (excluding any header/label row). 765 * @return {number} The number of rows, less any header. 766 */ 767 Dygraph.prototype.numRows = function() { 768 if (!this.rawData_) return 0; 769 return this.rawData_.length; 770 }; 771 772 /** 773 * Returns the value in the given row and column. If the row and column exceed 774 * the bounds on the data, returns null. Also returns null if the value is 775 * missing. 776 * @param {number} row The row number of the data (0-based). Row 0 is the 777 * first row of data, not a header row. 778 * @param {number} col The column number of the data (0-based) 779 * @return {number} The value in the specified cell or null if the row/col 780 * were out of range. 781 */ 782 Dygraph.prototype.getValue = function(row, col) { 783 if (row < 0 || row > this.rawData_.length) return null; 784 if (col < 0 || col > this.rawData_[row].length) return null; 785 786 return this.rawData_[row][col]; 787 }; 788 789 /** 790 * Generates interface elements for the Dygraph: a containing div, a div to 791 * display the current point, and a textbox to adjust the rolling average 792 * period. Also creates the Renderer/Layout elements. 793 * @private 794 */ 795 Dygraph.prototype.createInterface_ = function() { 796 // Create the all-enclosing graph div 797 var enclosing = this.maindiv_; 798 799 this.graphDiv = document.createElement("div"); 800 801 // TODO(danvk): any other styles that are useful to set here? 802 this.graphDiv.style.textAlign = 'left'; // This is a CSS "reset" 803 this.graphDiv.style.position = 'relative'; 804 enclosing.appendChild(this.graphDiv); 805 806 // Create the canvas for interactive parts of the chart. 807 this.canvas_ = utils.createCanvas(); 808 this.canvas_.style.position = "absolute"; 809 810 // ... and for static parts of the chart. 811 this.hidden_ = this.createPlotKitCanvas_(this.canvas_); 812 813 this.canvas_ctx_ = utils.getContext(this.canvas_); 814 this.hidden_ctx_ = utils.getContext(this.hidden_); 815 816 this.resizeElements_(); 817 818 // The interactive parts of the graph are drawn on top of the chart. 819 this.graphDiv.appendChild(this.hidden_); 820 this.graphDiv.appendChild(this.canvas_); 821 this.mouseEventElement_ = this.createMouseEventElement_(); 822 823 // Create the grapher 824 this.layout_ = new DygraphLayout(this); 825 826 var dygraph = this; 827 828 this.mouseMoveHandler_ = function(e) { 829 dygraph.mouseMove_(e); 830 }; 831 832 this.mouseOutHandler_ = function(e) { 833 // The mouse has left the chart if: 834 // 1. e.target is inside the chart 835 // 2. e.relatedTarget is outside the chart 836 var target = e.target || e.fromElement; 837 var relatedTarget = e.relatedTarget || e.toElement; 838 if (utils.isNodeContainedBy(target, dygraph.graphDiv) && 839 !utils.isNodeContainedBy(relatedTarget, dygraph.graphDiv)) { 840 dygraph.mouseOut_(e); 841 } 842 }; 843 844 this.addAndTrackEvent(window, 'mouseout', this.mouseOutHandler_); 845 this.addAndTrackEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); 846 847 // Don't recreate and register the resize handler on subsequent calls. 848 // This happens when the graph is resized. 849 if (!this.resizeHandler_) { 850 this.resizeHandler_ = function(e) { 851 dygraph.resize(); 852 }; 853 854 // Update when the window is resized. 855 // TODO(danvk): drop frames depending on complexity of the chart. 856 this.addAndTrackEvent(window, 'resize', this.resizeHandler_); 857 } 858 }; 859 860 Dygraph.prototype.resizeElements_ = function() { 861 this.graphDiv.style.width = this.width_ + "px"; 862 this.graphDiv.style.height = this.height_ + "px"; 863 864 var canvasScale = utils.getContextPixelRatio(this.canvas_ctx_); 865 this.canvas_.width = this.width_ * canvasScale; 866 this.canvas_.height = this.height_ * canvasScale; 867 this.canvas_.style.width = this.width_ + "px"; // for IE 868 this.canvas_.style.height = this.height_ + "px"; // for IE 869 if (canvasScale !== 1) { 870 this.canvas_ctx_.scale(canvasScale, canvasScale); 871 } 872 873 var hiddenScale = utils.getContextPixelRatio(this.hidden_ctx_); 874 this.hidden_.width = this.width_ * hiddenScale; 875 this.hidden_.height = this.height_ * hiddenScale; 876 this.hidden_.style.width = this.width_ + "px"; // for IE 877 this.hidden_.style.height = this.height_ + "px"; // for IE 878 if (hiddenScale !== 1) { 879 this.hidden_ctx_.scale(hiddenScale, hiddenScale); 880 } 881 }; 882 883 /** 884 * Detach DOM elements in the dygraph and null out all data references. 885 * Calling this when you're done with a dygraph can dramatically reduce memory 886 * usage. See, e.g., the tests/perf.html example. 887 */ 888 Dygraph.prototype.destroy = function() { 889 this.canvas_ctx_.restore(); 890 this.hidden_ctx_.restore(); 891 892 // Destroy any plugins, in the reverse order that they were registered. 893 for (var i = this.plugins_.length - 1; i >= 0; i--) { 894 var p = this.plugins_.pop(); 895 if (p.plugin.destroy) p.plugin.destroy(); 896 } 897 898 var removeRecursive = function(node) { 899 while (node.hasChildNodes()) { 900 removeRecursive(node.firstChild); 901 node.removeChild(node.firstChild); 902 } 903 }; 904 905 this.removeTrackedEvents_(); 906 907 // remove mouse event handlers (This may not be necessary anymore) 908 utils.removeEvent(window, 'mouseout', this.mouseOutHandler_); 909 utils.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); 910 911 // remove window handlers 912 utils.removeEvent(window,'resize', this.resizeHandler_); 913 this.resizeHandler_ = null; 914 915 removeRecursive(this.maindiv_); 916 917 var nullOut = function(obj) { 918 for (var n in obj) { 919 if (typeof(obj[n]) === 'object') { 920 obj[n] = null; 921 } 922 } 923 }; 924 // These may not all be necessary, but it can't hurt... 925 nullOut(this.layout_); 926 nullOut(this.plotter_); 927 nullOut(this); 928 }; 929 930 /** 931 * Creates the canvas on which the chart will be drawn. Only the Renderer ever 932 * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots 933 * or the zoom rectangles) is done on this.canvas_. 934 * @param {Object} canvas The Dygraph canvas over which to overlay the plot 935 * @return {Object} The newly-created canvas 936 * @private 937 */ 938 Dygraph.prototype.createPlotKitCanvas_ = function(canvas) { 939 var h = utils.createCanvas(); 940 h.style.position = "absolute"; 941 // TODO(danvk): h should be offset from canvas. canvas needs to include 942 // some extra area to make it easier to zoom in on the far left and far 943 // right. h needs to be precisely the plot area, so that clipping occurs. 944 h.style.top = canvas.style.top; 945 h.style.left = canvas.style.left; 946 h.width = this.width_; 947 h.height = this.height_; 948 h.style.width = this.width_ + "px"; // for IE 949 h.style.height = this.height_ + "px"; // for IE 950 return h; 951 }; 952 953 /** 954 * Creates an overlay element used to handle mouse events. 955 * @return {Object} The mouse event element. 956 * @private 957 */ 958 Dygraph.prototype.createMouseEventElement_ = function() { 959 return this.canvas_; 960 }; 961 962 /** 963 * Generate a set of distinct colors for the data series. This is done with a 964 * color wheel. Saturation/Value are customizable, and the hue is 965 * equally-spaced around the color wheel. If a custom set of colors is 966 * specified, that is used instead. 967 * @private 968 */ 969 Dygraph.prototype.setColors_ = function() { 970 var labels = this.getLabels(); 971 var num = labels.length - 1; 972 this.colors_ = []; 973 this.colorsMap_ = {}; 974 975 // These are used for when no custom colors are specified. 976 var sat = this.getNumericOption('colorSaturation') || 1.0; 977 var val = this.getNumericOption('colorValue') || 0.5; 978 var half = Math.ceil(num / 2); 979 980 var colors = this.getOption('colors'); 981 var visibility = this.visibility(); 982 for (var i = 0; i < num; i++) { 983 if (!visibility[i]) { 984 continue; 985 } 986 var label = labels[i + 1]; 987 var colorStr = this.attributes_.getForSeries('color', label); 988 if (!colorStr) { 989 if (colors) { 990 colorStr = colors[i % colors.length]; 991 } else { 992 // alternate colors for high contrast. 993 var idx = i % 2 ? (half + (i + 1)/ 2) : Math.ceil((i + 1) / 2); 994 var hue = (1.0 * idx / (1 + num)); 995 colorStr = utils.hsvToRGB(hue, sat, val); 996 } 997 } 998 this.colors_.push(colorStr); 999 this.colorsMap_[label] = colorStr; 1000 } 1001 }; 1002 1003 /** 1004 * Return the list of colors. This is either the list of colors passed in the 1005 * attributes or the autogenerated list of rgb(r,g,b) strings. 1006 * This does not return colors for invisible series. 1007 * @return {Array.<string>} The list of colors. 1008 */ 1009 Dygraph.prototype.getColors = function() { 1010 return this.colors_; 1011 }; 1012 1013 /** 1014 * Returns a few attributes of a series, i.e. its color, its visibility, which 1015 * axis it's assigned to, and its column in the original data. 1016 * Returns null if the series does not exist. 1017 * Otherwise, returns an object with column, visibility, color and axis properties. 1018 * The "axis" property will be set to 1 for y1 and 2 for y2. 1019 * The "column" property can be fed back into getValue(row, column) to get 1020 * values for this series. 1021 */ 1022 Dygraph.prototype.getPropertiesForSeries = function(series_name) { 1023 var idx = -1; 1024 var labels = this.getLabels(); 1025 for (var i = 1; i < labels.length; i++) { 1026 if (labels[i] == series_name) { 1027 idx = i; 1028 break; 1029 } 1030 } 1031 if (idx == -1) return null; 1032 1033 return { 1034 name: series_name, 1035 column: idx, 1036 visible: this.visibility()[idx - 1], 1037 color: this.colorsMap_[series_name], 1038 axis: 1 + this.attributes_.axisForSeries(series_name) 1039 }; 1040 }; 1041 1042 /** 1043 * Create the text box to adjust the averaging period 1044 * @private 1045 */ 1046 Dygraph.prototype.createRollInterface_ = function() { 1047 // Create a roller if one doesn't exist already. 1048 var roller = this.roller_; 1049 if (!roller) { 1050 this.roller_ = roller = document.createElement("input"); 1051 roller.type = "text"; 1052 roller.style.display = "none"; 1053 roller.className = 'dygraph-roller'; 1054 this.graphDiv.appendChild(roller); 1055 } 1056 1057 var display = this.getBooleanOption('showRoller') ? 'block' : 'none'; 1058 1059 var area = this.getArea(); 1060 var textAttr = { 1061 "top": (area.y + area.h - 25) + "px", 1062 "left": (area.x + 1) + "px", 1063 "display": display 1064 }; 1065 roller.size = "2"; 1066 roller.value = this.rollPeriod_; 1067 utils.update(roller.style, textAttr); 1068 1069 roller.onchange = () => this.adjustRoll(roller.value); 1070 }; 1071 1072 /** 1073 * Set up all the mouse handlers needed to capture dragging behavior for zoom 1074 * events. 1075 * @private 1076 */ 1077 Dygraph.prototype.createDragInterface_ = function() { 1078 var context = { 1079 // Tracks whether the mouse is down right now 1080 isZooming: false, 1081 isPanning: false, // is this drag part of a pan? 1082 is2DPan: false, // if so, is that pan 1- or 2-dimensional? 1083 dragStartX: null, // pixel coordinates 1084 dragStartY: null, // pixel coordinates 1085 dragEndX: null, // pixel coordinates 1086 dragEndY: null, // pixel coordinates 1087 dragDirection: null, 1088 prevEndX: null, // pixel coordinates 1089 prevEndY: null, // pixel coordinates 1090 prevDragDirection: null, 1091 cancelNextDblclick: false, // see comment in dygraph-interaction-model.js 1092 1093 // The value on the left side of the graph when a pan operation starts. 1094 initialLeftmostDate: null, 1095 1096 // The number of units each pixel spans. (This won't be valid for log 1097 // scales) 1098 xUnitsPerPixel: null, 1099 1100 // TODO(danvk): update this comment 1101 // The range in second/value units that the viewport encompasses during a 1102 // panning operation. 1103 dateRange: null, 1104 1105 // Top-left corner of the canvas, in DOM coords 1106 // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY. 1107 px: 0, 1108 py: 0, 1109 1110 // Values for use with panEdgeFraction, which limit how far outside the 1111 // graph's data boundaries it can be panned. 1112 boundedDates: null, // [minDate, maxDate] 1113 boundedValues: null, // [[minValue, maxValue] ...] 1114 1115 // We cover iframes during mouse interactions. See comments in 1116 // dygraph-utils.js for more info on why this is a good idea. 1117 tarp: new IFrameTarp(), 1118 1119 // contextB is the same thing as this context object but renamed. 1120 initializeMouseDown: function(event, g, contextB) { 1121 // prevents mouse drags from selecting page text. 1122 if (event.preventDefault) { 1123 event.preventDefault(); // Firefox, Chrome, etc. 1124 } else { 1125 event.returnValue = false; // IE 1126 event.cancelBubble = true; 1127 } 1128 1129 var canvasPos = utils.findPos(g.canvas_); 1130 contextB.px = canvasPos.x; 1131 contextB.py = canvasPos.y; 1132 contextB.dragStartX = utils.dragGetX_(event, contextB); 1133 contextB.dragStartY = utils.dragGetY_(event, contextB); 1134 contextB.cancelNextDblclick = false; 1135 contextB.tarp.cover(); 1136 }, 1137 destroy: function() { 1138 var context = this; 1139 if (context.isZooming || context.isPanning) { 1140 context.isZooming = false; 1141 context.dragStartX = null; 1142 context.dragStartY = null; 1143 } 1144 1145 if (context.isPanning) { 1146 context.isPanning = false; 1147 context.draggingDate = null; 1148 context.dateRange = null; 1149 for (var i = 0; i < self.axes_.length; i++) { 1150 delete self.axes_[i].draggingValue; 1151 delete self.axes_[i].dragValueRange; 1152 } 1153 } 1154 1155 context.tarp.uncover(); 1156 } 1157 }; 1158 1159 var interactionModel = this.getOption("interactionModel"); 1160 1161 // Self is the graph. 1162 var self = this; 1163 1164 // Function that binds the graph and context to the handler. 1165 var bindHandler = function(handler) { 1166 return function(event) { 1167 handler(event, self, context); 1168 }; 1169 }; 1170 1171 for (var eventName in interactionModel) { 1172 if (!interactionModel.hasOwnProperty(eventName)) continue; 1173 this.addAndTrackEvent(this.mouseEventElement_, eventName, 1174 bindHandler(interactionModel[eventName])); 1175 } 1176 1177 // If the user releases the mouse button during a drag, but not over the 1178 // canvas, then it doesn't count as a zooming action. 1179 if (!interactionModel.willDestroyContextMyself) { 1180 var mouseUpHandler = function(event) { 1181 context.destroy(); 1182 }; 1183 1184 this.addAndTrackEvent(document, 'mouseup', mouseUpHandler); 1185 } 1186 }; 1187 1188 /** 1189 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears 1190 * up any previous zoom rectangles that were drawn. This could be optimized to 1191 * avoid extra redrawing, but it's tricky to avoid interactions with the status 1192 * dots. 1193 * 1194 * @param {number} direction the direction of the zoom rectangle. Acceptable 1195 * values are utils.HORIZONTAL and utils.VERTICAL. 1196 * @param {number} startX The X position where the drag started, in canvas 1197 * coordinates. 1198 * @param {number} endX The current X position of the drag, in canvas coords. 1199 * @param {number} startY The Y position where the drag started, in canvas 1200 * coordinates. 1201 * @param {number} endY The current Y position of the drag, in canvas coords. 1202 * @param {number} prevDirection the value of direction on the previous call to 1203 * this function. Used to avoid excess redrawing 1204 * @param {number} prevEndX The value of endX on the previous call to this 1205 * function. Used to avoid excess redrawing 1206 * @param {number} prevEndY The value of endY on the previous call to this 1207 * function. Used to avoid excess redrawing 1208 * @private 1209 */ 1210 Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, 1211 endY, prevDirection, prevEndX, 1212 prevEndY) { 1213 var ctx = this.canvas_ctx_; 1214 1215 // Clean up from the previous rect if necessary 1216 if (prevDirection == utils.HORIZONTAL) { 1217 ctx.clearRect(Math.min(startX, prevEndX), this.layout_.getPlotArea().y, 1218 Math.abs(startX - prevEndX), this.layout_.getPlotArea().h); 1219 } else if (prevDirection == utils.VERTICAL) { 1220 ctx.clearRect(this.layout_.getPlotArea().x, Math.min(startY, prevEndY), 1221 this.layout_.getPlotArea().w, Math.abs(startY - prevEndY)); 1222 } 1223 1224 // Draw a light-grey rectangle to show the new viewing area 1225 if (direction == utils.HORIZONTAL) { 1226 if (endX && startX) { 1227 ctx.fillStyle = "rgba(128,128,128,0.33)"; 1228 ctx.fillRect(Math.min(startX, endX), this.layout_.getPlotArea().y, 1229 Math.abs(endX - startX), this.layout_.getPlotArea().h); 1230 } 1231 } else if (direction == utils.VERTICAL) { 1232 if (endY && startY) { 1233 ctx.fillStyle = "rgba(128,128,128,0.33)"; 1234 ctx.fillRect(this.layout_.getPlotArea().x, Math.min(startY, endY), 1235 this.layout_.getPlotArea().w, Math.abs(endY - startY)); 1236 } 1237 } 1238 }; 1239 1240 /** 1241 * Clear the zoom rectangle (and perform no zoom). 1242 * @private 1243 */ 1244 Dygraph.prototype.clearZoomRect_ = function() { 1245 this.currentZoomRectArgs_ = null; 1246 this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_); 1247 }; 1248 1249 /** 1250 * Zoom to something containing [lowX, highX]. These are pixel coordinates in 1251 * the canvas. The exact zoom window may be slightly larger if there are no data 1252 * points near lowX or highX. Don't confuse this function with doZoomXDates, 1253 * which accepts dates that match the raw data. This function redraws the graph. 1254 * 1255 * @param {number} lowX The leftmost pixel value that should be visible. 1256 * @param {number} highX The rightmost pixel value that should be visible. 1257 * @private 1258 */ 1259 Dygraph.prototype.doZoomX_ = function(lowX, highX) { 1260 this.currentZoomRectArgs_ = null; 1261 // Find the earliest and latest dates contained in this canvasx range. 1262 // Convert the call to date ranges of the raw data. 1263 var minDate = this.toDataXCoord(lowX); 1264 var maxDate = this.toDataXCoord(highX); 1265 this.doZoomXDates_(minDate, maxDate); 1266 }; 1267 1268 /** 1269 * Zoom to something containing [minDate, maxDate] values. Don't confuse this 1270 * method with doZoomX which accepts pixel coordinates. This function redraws 1271 * the graph. 1272 * 1273 * @param {number} minDate The minimum date that should be visible. 1274 * @param {number} maxDate The maximum date that should be visible. 1275 * @private 1276 */ 1277 Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { 1278 // TODO(danvk): when xAxisRange is null (i.e. "fit to data", the animation 1279 // can produce strange effects. Rather than the x-axis transitioning slowly 1280 // between values, it can jerk around.) 1281 var old_window = this.xAxisRange(); 1282 var new_window = [minDate, maxDate]; 1283 const zoomCallback = this.getFunctionOption('zoomCallback'); 1284 this.doAnimatedZoom(old_window, new_window, null, null, () => { 1285 if (zoomCallback) { 1286 zoomCallback.call(this, minDate, maxDate, this.yAxisRanges()); 1287 } 1288 }); 1289 }; 1290 1291 /** 1292 * Zoom to something containing [lowY, highY]. These are pixel coordinates in 1293 * the canvas. This function redraws the graph. 1294 * 1295 * @param {number} lowY The topmost pixel value that should be visible. 1296 * @param {number} highY The lowest pixel value that should be visible. 1297 * @private 1298 */ 1299 Dygraph.prototype.doZoomY_ = function(lowY, highY) { 1300 this.currentZoomRectArgs_ = null; 1301 // Find the highest and lowest values in pixel range for each axis. 1302 // Note that lowY (in pixels) corresponds to the max Value (in data coords). 1303 // This is because pixels increase as you go down on the screen, whereas data 1304 // coordinates increase as you go up the screen. 1305 var oldValueRanges = this.yAxisRanges(); 1306 var newValueRanges = []; 1307 for (var i = 0; i < this.axes_.length; i++) { 1308 var hi = this.toDataYCoord(lowY, i); 1309 var low = this.toDataYCoord(highY, i); 1310 newValueRanges.push([low, hi]); 1311 } 1312 1313 const zoomCallback = this.getFunctionOption('zoomCallback'); 1314 this.doAnimatedZoom(null, null, oldValueRanges, newValueRanges, () => { 1315 if (zoomCallback) { 1316 const [minX, maxX] = this.xAxisRange(); 1317 zoomCallback.call(this, minX, maxX, this.yAxisRanges()); 1318 } 1319 }); 1320 }; 1321 1322 /** 1323 * Transition function to use in animations. Returns values between 0.0 1324 * (totally old values) and 1.0 (totally new values) for each frame. 1325 * @private 1326 */ 1327 Dygraph.zoomAnimationFunction = function(frame, numFrames) { 1328 var k = 1.5; 1329 return (1.0 - Math.pow(k, -frame)) / (1.0 - Math.pow(k, -numFrames)); 1330 }; 1331 1332 /** 1333 * Reset the zoom to the original view coordinates. This is the same as 1334 * double-clicking on the graph. 1335 */ 1336 Dygraph.prototype.resetZoom = function() { 1337 const dirtyX = this.isZoomed('x'); 1338 const dirtyY = this.isZoomed('y'); 1339 const dirty = dirtyX || dirtyY; 1340 1341 // Clear any selection, since it's likely to be drawn in the wrong place. 1342 this.clearSelection(); 1343 1344 if (!dirty) return; 1345 1346 // Calculate extremes to avoid lack of padding on reset. 1347 const [minDate, maxDate] = this.xAxisExtremes(); 1348 1349 const animatedZooms = this.getBooleanOption('animatedZooms'); 1350 const zoomCallback = this.getFunctionOption('zoomCallback'); 1351 1352 // TODO(danvk): merge this block w/ the code below. 1353 if (!animatedZooms) { 1354 this.dateWindow_ = null; 1355 this.axes_.forEach(axis => { 1356 if (axis.valueRange) delete axis.valueRange; 1357 }); 1358 1359 this.drawGraph_(); 1360 if (zoomCallback) { 1361 zoomCallback.call(this, minDate, maxDate, this.yAxisRanges()); 1362 } 1363 return; 1364 } 1365 1366 var oldWindow=null, newWindow=null, oldValueRanges=null, newValueRanges=null; 1367 if (dirtyX) { 1368 oldWindow = this.xAxisRange(); 1369 newWindow = [minDate, maxDate]; 1370 } 1371 1372 if (dirtyY) { 1373 oldValueRanges = this.yAxisRanges(); 1374 newValueRanges = this.yAxisExtremes(); 1375 } 1376 1377 this.doAnimatedZoom(oldWindow, newWindow, oldValueRanges, newValueRanges, 1378 () => { 1379 this.dateWindow_ = null; 1380 this.axes_.forEach(axis => { 1381 if (axis.valueRange) delete axis.valueRange; 1382 }); 1383 if (zoomCallback) { 1384 zoomCallback.call(this, minDate, maxDate, this.yAxisRanges()); 1385 } 1386 }); 1387 }; 1388 1389 /** 1390 * Combined animation logic for all zoom functions. 1391 * either the x parameters or y parameters may be null. 1392 * @private 1393 */ 1394 Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, newYRanges, callback) { 1395 var steps = this.getBooleanOption("animatedZooms") ? 1396 Dygraph.ANIMATION_STEPS : 1; 1397 1398 var windows = []; 1399 var valueRanges = []; 1400 var step, frac; 1401 1402 if (oldXRange !== null && newXRange !== null) { 1403 for (step = 1; step <= steps; step++) { 1404 frac = Dygraph.zoomAnimationFunction(step, steps); 1405 windows[step-1] = [oldXRange[0]*(1-frac) + frac*newXRange[0], 1406 oldXRange[1]*(1-frac) + frac*newXRange[1]]; 1407 } 1408 } 1409 1410 if (oldYRanges !== null && newYRanges !== null) { 1411 for (step = 1; step <= steps; step++) { 1412 frac = Dygraph.zoomAnimationFunction(step, steps); 1413 var thisRange = []; 1414 for (var j = 0; j < this.axes_.length; j++) { 1415 thisRange.push([oldYRanges[j][0]*(1-frac) + frac*newYRanges[j][0], 1416 oldYRanges[j][1]*(1-frac) + frac*newYRanges[j][1]]); 1417 } 1418 valueRanges[step-1] = thisRange; 1419 } 1420 } 1421 1422 utils.repeatAndCleanup(step => { 1423 if (valueRanges.length) { 1424 for (var i = 0; i < this.axes_.length; i++) { 1425 var w = valueRanges[step][i]; 1426 this.axes_[i].valueRange = [w[0], w[1]]; 1427 } 1428 } 1429 if (windows.length) { 1430 this.dateWindow_ = windows[step]; 1431 } 1432 this.drawGraph_(); 1433 }, steps, Dygraph.ANIMATION_DURATION / steps, callback); 1434 }; 1435 1436 /** 1437 * Get the current graph's area object. 1438 * 1439 * Returns: {x, y, w, h} 1440 */ 1441 Dygraph.prototype.getArea = function() { 1442 return this.plotter_.area; 1443 }; 1444 1445 /** 1446 * Convert a mouse event to DOM coordinates relative to the graph origin. 1447 * 1448 * Returns a two-element array: [X, Y]. 1449 */ 1450 Dygraph.prototype.eventToDomCoords = function(event) { 1451 if (event.offsetX && event.offsetY) { 1452 return [ event.offsetX, event.offsetY ]; 1453 } else { 1454 var eventElementPos = utils.findPos(this.mouseEventElement_); 1455 var canvasx = utils.pageX(event) - eventElementPos.x; 1456 var canvasy = utils.pageY(event) - eventElementPos.y; 1457 return [canvasx, canvasy]; 1458 } 1459 }; 1460 1461 /** 1462 * Given a canvas X coordinate, find the closest row. 1463 * @param {number} domX graph-relative DOM X coordinate 1464 * Returns {number} row number. 1465 * @private 1466 */ 1467 Dygraph.prototype.findClosestRow = function(domX) { 1468 var minDistX = Infinity; 1469 var closestRow = -1; 1470 var sets = this.layout_.points; 1471 for (var i = 0; i < sets.length; i++) { 1472 var points = sets[i]; 1473 var len = points.length; 1474 for (var j = 0; j < len; j++) { 1475 var point = points[j]; 1476 if (!utils.isValidPoint(point, true)) continue; 1477 var dist = Math.abs(point.canvasx - domX); 1478 if (dist < minDistX) { 1479 minDistX = dist; 1480 closestRow = point.idx; 1481 } 1482 } 1483 } 1484 1485 return closestRow; 1486 }; 1487 1488 /** 1489 * Given canvas X,Y coordinates, find the closest point. 1490 * 1491 * This finds the individual data point across all visible series 1492 * that's closest to the supplied DOM coordinates using the standard 1493 * Euclidean X,Y distance. 1494 * 1495 * @param {number} domX graph-relative DOM X coordinate 1496 * @param {number} domY graph-relative DOM Y coordinate 1497 * Returns: {row, seriesName, point} 1498 * @private 1499 */ 1500 Dygraph.prototype.findClosestPoint = function(domX, domY) { 1501 var minDist = Infinity; 1502 var dist, dx, dy, point, closestPoint, closestSeries, closestRow; 1503 for ( var setIdx = this.layout_.points.length - 1 ; setIdx >= 0 ; --setIdx ) { 1504 var points = this.layout_.points[setIdx]; 1505 for (var i = 0; i < points.length; ++i) { 1506 point = points[i]; 1507 if (!utils.isValidPoint(point)) continue; 1508 dx = point.canvasx - domX; 1509 dy = point.canvasy - domY; 1510 dist = dx * dx + dy * dy; 1511 if (dist < minDist) { 1512 minDist = dist; 1513 closestPoint = point; 1514 closestSeries = setIdx; 1515 closestRow = point.idx; 1516 } 1517 } 1518 } 1519 var name = this.layout_.setNames[closestSeries]; 1520 return { 1521 row: closestRow, 1522 seriesName: name, 1523 point: closestPoint 1524 }; 1525 }; 1526 1527 /** 1528 * Given canvas X,Y coordinates, find the touched area in a stacked graph. 1529 * 1530 * This first finds the X data point closest to the supplied DOM X coordinate, 1531 * then finds the series which puts the Y coordinate on top of its filled area, 1532 * using linear interpolation between adjacent point pairs. 1533 * 1534 * @param {number} domX graph-relative DOM X coordinate 1535 * @param {number} domY graph-relative DOM Y coordinate 1536 * Returns: {row, seriesName, point} 1537 * @private 1538 */ 1539 Dygraph.prototype.findStackedPoint = function(domX, domY) { 1540 var row = this.findClosestRow(domX); 1541 var closestPoint, closestSeries; 1542 for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) { 1543 var boundary = this.getLeftBoundary_(setIdx); 1544 var rowIdx = row - boundary; 1545 var points = this.layout_.points[setIdx]; 1546 if (rowIdx >= points.length) continue; 1547 var p1 = points[rowIdx]; 1548 if (!utils.isValidPoint(p1)) continue; 1549 var py = p1.canvasy; 1550 if (domX > p1.canvasx && rowIdx + 1 < points.length) { 1551 // interpolate series Y value using next point 1552 var p2 = points[rowIdx + 1]; 1553 if (utils.isValidPoint(p2)) { 1554 var dx = p2.canvasx - p1.canvasx; 1555 if (dx > 0) { 1556 var r = (domX - p1.canvasx) / dx; 1557 py += r * (p2.canvasy - p1.canvasy); 1558 } 1559 } 1560 } else if (domX < p1.canvasx && rowIdx > 0) { 1561 // interpolate series Y value using previous point 1562 var p0 = points[rowIdx - 1]; 1563 if (utils.isValidPoint(p0)) { 1564 var dx = p1.canvasx - p0.canvasx; 1565 if (dx > 0) { 1566 var r = (p1.canvasx - domX) / dx; 1567 py += r * (p0.canvasy - p1.canvasy); 1568 } 1569 } 1570 } 1571 // Stop if the point (domX, py) is above this series' upper edge 1572 if (setIdx === 0 || py < domY) { 1573 closestPoint = p1; 1574 closestSeries = setIdx; 1575 } 1576 } 1577 var name = this.layout_.setNames[closestSeries]; 1578 return { 1579 row: row, 1580 seriesName: name, 1581 point: closestPoint 1582 }; 1583 }; 1584 1585 /** 1586 * When the mouse moves in the canvas, display information about a nearby data 1587 * point and draw dots over those points in the data series. This function 1588 * takes care of cleanup of previously-drawn dots. 1589 * @param {Object} event The mousemove event from the browser. 1590 * @private 1591 */ 1592 Dygraph.prototype.mouseMove_ = function(event) { 1593 // This prevents JS errors when mousing over the canvas before data loads. 1594 var points = this.layout_.points; 1595 if (points === undefined || points === null) return; 1596 1597 var canvasCoords = this.eventToDomCoords(event); 1598 var canvasx = canvasCoords[0]; 1599 var canvasy = canvasCoords[1]; 1600 1601 var highlightSeriesOpts = this.getOption("highlightSeriesOpts"); 1602 var selectionChanged = false; 1603 if (highlightSeriesOpts && !this.isSeriesLocked()) { 1604 var closest; 1605 if (this.getBooleanOption("stackedGraph")) { 1606 closest = this.findStackedPoint(canvasx, canvasy); 1607 } else { 1608 closest = this.findClosestPoint(canvasx, canvasy); 1609 } 1610 selectionChanged = this.setSelection(closest.row, closest.seriesName); 1611 } else { 1612 var idx = this.findClosestRow(canvasx); 1613 selectionChanged = this.setSelection(idx); 1614 } 1615 1616 var callback = this.getFunctionOption("highlightCallback"); 1617 if (callback && selectionChanged) { 1618 callback.call(this, event, 1619 this.lastx_, 1620 this.selPoints_, 1621 this.lastRow_, 1622 this.highlightSet_); 1623 } 1624 }; 1625 1626 /** 1627 * Fetch left offset from the specified set index or if not passed, the 1628 * first defined boundaryIds record (see bug #236). 1629 * @private 1630 */ 1631 Dygraph.prototype.getLeftBoundary_ = function(setIdx) { 1632 if (this.boundaryIds_[setIdx]) { 1633 return this.boundaryIds_[setIdx][0]; 1634 } else { 1635 for (var i = 0; i < this.boundaryIds_.length; i++) { 1636 if (this.boundaryIds_[i] !== undefined) { 1637 return this.boundaryIds_[i][0]; 1638 } 1639 } 1640 return 0; 1641 } 1642 }; 1643 1644 Dygraph.prototype.animateSelection_ = function(direction) { 1645 var totalSteps = 10; 1646 var millis = 30; 1647 if (this.fadeLevel === undefined) this.fadeLevel = 0; 1648 if (this.animateId === undefined) this.animateId = 0; 1649 var start = this.fadeLevel; 1650 var steps = direction < 0 ? start : totalSteps - start; 1651 if (steps <= 0) { 1652 if (this.fadeLevel) { 1653 this.updateSelection_(1.0); 1654 } 1655 return; 1656 } 1657 1658 var thisId = ++this.animateId; 1659 var that = this; 1660 var cleanupIfClearing = function() { 1661 // if we haven't reached fadeLevel 0 in the max frame time, 1662 // ensure that the clear happens and just go to 0 1663 if (that.fadeLevel !== 0 && direction < 0) { 1664 that.fadeLevel = 0; 1665 that.clearSelection(); 1666 } 1667 }; 1668 utils.repeatAndCleanup( 1669 function(n) { 1670 // ignore simultaneous animations 1671 if (that.animateId != thisId) return; 1672 1673 that.fadeLevel += direction; 1674 if (that.fadeLevel === 0) { 1675 that.clearSelection(); 1676 } else { 1677 that.updateSelection_(that.fadeLevel / totalSteps); 1678 } 1679 }, 1680 steps, millis, cleanupIfClearing); 1681 }; 1682 1683 /** 1684 * Draw dots over the selectied points in the data series. This function 1685 * takes care of cleanup of previously-drawn dots. 1686 * @private 1687 */ 1688 Dygraph.prototype.updateSelection_ = function(opt_animFraction) { 1689 /*var defaultPrevented = */ 1690 this.cascadeEvents_('select', { 1691 selectedRow: this.lastRow_ === -1 ? undefined : this.lastRow_, 1692 selectedX: this.lastx_ === -1 ? undefined : this.lastx_, 1693 selectedPoints: this.selPoints_ 1694 }); 1695 // TODO(danvk): use defaultPrevented here? 1696 1697 // Clear the previously drawn vertical, if there is one 1698 var i; 1699 var ctx = this.canvas_ctx_; 1700 if (this.getOption('highlightSeriesOpts')) { 1701 ctx.clearRect(0, 0, this.width_, this.height_); 1702 var alpha = 1.0 - this.getNumericOption('highlightSeriesBackgroundAlpha'); 1703 var backgroundColor = utils.toRGB_(this.getOption('highlightSeriesBackgroundColor')); 1704 1705 if (alpha) { 1706 // Activating background fade includes an animation effect for a gradual 1707 // fade. TODO(klausw): make this independently configurable if it causes 1708 // issues? Use a shared preference to control animations? 1709 var animateBackgroundFade = true; 1710 if (animateBackgroundFade) { 1711 if (opt_animFraction === undefined) { 1712 // start a new animation 1713 this.animateSelection_(1); 1714 return; 1715 } 1716 alpha *= opt_animFraction; 1717 } 1718 ctx.fillStyle = 'rgba(' + backgroundColor.r + ',' + backgroundColor.g + ',' + backgroundColor.b + ',' + alpha + ')'; 1719 ctx.fillRect(0, 0, this.width_, this.height_); 1720 } 1721 1722 // Redraw only the highlighted series in the interactive canvas (not the 1723 // static plot canvas, which is where series are usually drawn). 1724 this.plotter_._renderLineChart(this.highlightSet_, ctx); 1725 } else if (this.previousVerticalX_ >= 0) { 1726 // Determine the maximum highlight circle size. 1727 var maxCircleSize = 0; 1728 var labels = this.attr_('labels'); 1729 for (i = 1; i < labels.length; i++) { 1730 var r = this.getNumericOption('highlightCircleSize', labels[i]); 1731 if (r > maxCircleSize) maxCircleSize = r; 1732 } 1733 var px = this.previousVerticalX_; 1734 ctx.clearRect(px - maxCircleSize - 1, 0, 1735 2 * maxCircleSize + 2, this.height_); 1736 } 1737 1738 if (this.selPoints_.length > 0) { 1739 // Draw colored circles over the center of each selected point 1740 var canvasx = this.selPoints_[0].canvasx; 1741 ctx.save(); 1742 for (i = 0; i < this.selPoints_.length; i++) { 1743 var pt = this.selPoints_[i]; 1744 if (isNaN(pt.canvasy)) continue; 1745 1746 var circleSize = this.getNumericOption('highlightCircleSize', pt.name); 1747 var callback = this.getFunctionOption("drawHighlightPointCallback", pt.name); 1748 var color = this.plotter_.colors[pt.name]; 1749 if (!callback) { 1750 callback = utils.Circles.DEFAULT; 1751 } 1752 ctx.lineWidth = this.getNumericOption('strokeWidth', pt.name); 1753 ctx.strokeStyle = color; 1754 ctx.fillStyle = color; 1755 callback.call(this, this, pt.name, ctx, canvasx, pt.canvasy, 1756 color, circleSize, pt.idx); 1757 } 1758 ctx.restore(); 1759 1760 this.previousVerticalX_ = canvasx; 1761 } 1762 }; 1763 1764 /** 1765 * Manually set the selected points and display information about them in the 1766 * legend. The selection can be cleared using clearSelection() and queried 1767 * using getSelection(). 1768 * 1769 * To set a selected series but not a selected point, call setSelection with 1770 * row=false and the selected series name. 1771 * 1772 * @param {number} row Row number that should be highlighted (i.e. appear with 1773 * hover dots on the chart). 1774 * @param {seriesName} optional series name to highlight that series with the 1775 * the highlightSeriesOpts setting. 1776 * @param { locked } optional If true, keep seriesName selected when mousing 1777 * over the graph, disabling closest-series highlighting. Call clearSelection() 1778 * to unlock it. 1779 */ 1780 Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) { 1781 // Extract the points we've selected 1782 this.selPoints_ = []; 1783 1784 var changed = false; 1785 if (row !== false && row >= 0) { 1786 if (row != this.lastRow_) changed = true; 1787 this.lastRow_ = row; 1788 for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) { 1789 var points = this.layout_.points[setIdx]; 1790 // Check if the point at the appropriate index is the point we're looking 1791 // for. If it is, just use it, otherwise search the array for a point 1792 // in the proper place. 1793 var setRow = row - this.getLeftBoundary_(setIdx); 1794 if (setRow >= 0 && setRow < points.length && points[setRow].idx == row) { 1795 var point = points[setRow]; 1796 if (point.yval !== null) this.selPoints_.push(point); 1797 } else { 1798 for (var pointIdx = 0; pointIdx < points.length; ++pointIdx) { 1799 var point = points[pointIdx]; 1800 if (point.idx == row) { 1801 if (point.yval !== null) { 1802 this.selPoints_.push(point); 1803 } 1804 break; 1805 } 1806 } 1807 } 1808 } 1809 } else { 1810 if (this.lastRow_ >= 0) changed = true; 1811 this.lastRow_ = -1; 1812 } 1813 1814 if (this.selPoints_.length) { 1815 this.lastx_ = this.selPoints_[0].xval; 1816 } else { 1817 this.lastx_ = -1; 1818 } 1819 1820 if (opt_seriesName !== undefined) { 1821 if (this.highlightSet_ !== opt_seriesName) changed = true; 1822 this.highlightSet_ = opt_seriesName; 1823 } 1824 1825 if (opt_locked !== undefined) { 1826 this.lockedSet_ = opt_locked; 1827 } 1828 1829 if (changed) { 1830 this.updateSelection_(undefined); 1831 } 1832 return changed; 1833 }; 1834 1835 /** 1836 * The mouse has left the canvas. Clear out whatever artifacts remain 1837 * @param {Object} event the mouseout event from the browser. 1838 * @private 1839 */ 1840 Dygraph.prototype.mouseOut_ = function(event) { 1841 if (this.getFunctionOption("unhighlightCallback")) { 1842 this.getFunctionOption("unhighlightCallback").call(this, event); 1843 } 1844 1845 if (this.getBooleanOption("hideOverlayOnMouseOut") && !this.lockedSet_) { 1846 this.clearSelection(); 1847 } 1848 }; 1849 1850 /** 1851 * Clears the current selection (i.e. points that were highlighted by moving 1852 * the mouse over the chart). 1853 */ 1854 Dygraph.prototype.clearSelection = function() { 1855 this.cascadeEvents_('deselect', {}); 1856 1857 this.lockedSet_ = false; 1858 // Get rid of the overlay data 1859 if (this.fadeLevel) { 1860 this.animateSelection_(-1); 1861 return; 1862 } 1863 this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_); 1864 this.fadeLevel = 0; 1865 this.selPoints_ = []; 1866 this.lastx_ = -1; 1867 this.lastRow_ = -1; 1868 this.highlightSet_ = null; 1869 }; 1870 1871 /** 1872 * Returns the number of the currently selected row. To get data for this row, 1873 * you can use the getValue method. 1874 * @return {number} row number, or -1 if nothing is selected 1875 */ 1876 Dygraph.prototype.getSelection = function() { 1877 if (!this.selPoints_ || this.selPoints_.length < 1) { 1878 return -1; 1879 } 1880 1881 for (var setIdx = 0; setIdx < this.layout_.points.length; setIdx++) { 1882 var points = this.layout_.points[setIdx]; 1883 for (var row = 0; row < points.length; row++) { 1884 if (points[row].x == this.selPoints_[0].x) { 1885 return points[row].idx; 1886 } 1887 } 1888 } 1889 return -1; 1890 }; 1891 1892 /** 1893 * Returns the name of the currently-highlighted series. 1894 * Only available when the highlightSeriesOpts option is in use. 1895 */ 1896 Dygraph.prototype.getHighlightSeries = function() { 1897 return this.highlightSet_; 1898 }; 1899 1900 /** 1901 * Returns true if the currently-highlighted series was locked 1902 * via setSelection(..., seriesName, true). 1903 */ 1904 Dygraph.prototype.isSeriesLocked = function() { 1905 return this.lockedSet_; 1906 }; 1907 1908 /** 1909 * Fires when there's data available to be graphed. 1910 * @param {string} data Raw CSV data to be plotted 1911 * @private 1912 */ 1913 Dygraph.prototype.loadedEvent_ = function(data) { 1914 this.rawData_ = this.parseCSV_(data); 1915 this.cascadeDataDidUpdateEvent_(); 1916 this.predraw_(); 1917 }; 1918 1919 /** 1920 * Add ticks on the x-axis representing years, months, quarters, weeks, or days 1921 * @private 1922 */ 1923 Dygraph.prototype.addXTicks_ = function() { 1924 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ... 1925 var range; 1926 if (this.dateWindow_) { 1927 range = [this.dateWindow_[0], this.dateWindow_[1]]; 1928 } else { 1929 range = this.xAxisExtremes(); 1930 } 1931 1932 var xAxisOptionsView = this.optionsViewForAxis_('x'); 1933 var xTicks = xAxisOptionsView('ticker')( 1934 range[0], 1935 range[1], 1936 this.plotter_.area.w, // TODO(danvk): should be area.width 1937 xAxisOptionsView, 1938 this); 1939 // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks); 1940 // console.log(msg); 1941 this.layout_.setXTicks(xTicks); 1942 }; 1943 1944 /** 1945 * Returns the correct handler class for the currently set options. 1946 * @private 1947 */ 1948 Dygraph.prototype.getHandlerClass_ = function() { 1949 var handlerClass; 1950 if (this.attr_('dataHandler')) { 1951 handlerClass = this.attr_('dataHandler'); 1952 } else if (this.fractions_) { 1953 if (this.getBooleanOption('errorBars')) { 1954 handlerClass = FractionsBarsHandler; 1955 } else { 1956 handlerClass = DefaultFractionHandler; 1957 } 1958 } else if (this.getBooleanOption('customBars')) { 1959 handlerClass = CustomBarsHandler; 1960 } else if (this.getBooleanOption('errorBars')) { 1961 handlerClass = ErrorBarsHandler; 1962 } else { 1963 handlerClass = DefaultHandler; 1964 } 1965 return handlerClass; 1966 }; 1967 1968 /** 1969 * @private 1970 * This function is called once when the chart's data is changed or the options 1971 * dictionary is updated. It is _not_ called when the user pans or zooms. The 1972 * idea is that values derived from the chart's data can be computed here, 1973 * rather than every time the chart is drawn. This includes things like the 1974 * number of axes, rolling averages, etc. 1975 */ 1976 Dygraph.prototype.predraw_ = function() { 1977 var start = new Date(); 1978 1979 // Create the correct dataHandler 1980 this.dataHandler_ = new (this.getHandlerClass_())(); 1981 1982 this.layout_.computePlotArea(); 1983 1984 // TODO(danvk): move more computations out of drawGraph_ and into here. 1985 this.computeYAxes_(); 1986 1987 if (!this.is_initial_draw_) { 1988 this.canvas_ctx_.restore(); 1989 this.hidden_ctx_.restore(); 1990 } 1991 1992 this.canvas_ctx_.save(); 1993 this.hidden_ctx_.save(); 1994 1995 // Create a new plotter. 1996 this.plotter_ = new DygraphCanvasRenderer(this, 1997 this.hidden_, 1998 this.hidden_ctx_, 1999 this.layout_); 2000 2001 // The roller sits in the bottom left corner of the chart. We don't know where 2002 // this will be until the options are available, so it's positioned here. 2003 this.createRollInterface_(); 2004 2005 this.cascadeEvents_('predraw'); 2006 2007 // Convert the raw data (a 2D array) into the internal format and compute 2008 // rolling averages. 2009 this.rolledSeries_ = [null]; // x-axis is the first series and it's special 2010 for (var i = 1; i < this.numColumns(); i++) { 2011 // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too. 2012 var series = this.dataHandler_.extractSeries(this.rawData_, i, this.attributes_); 2013 if (this.rollPeriod_ > 1) { 2014 series = this.dataHandler_.rollingAverage(series, this.rollPeriod_, this.attributes_); 2015 } 2016 2017 this.rolledSeries_.push(series); 2018 } 2019 2020 // If the data or options have changed, then we'd better redraw. 2021 this.drawGraph_(); 2022 2023 // This is used to determine whether to do various animations. 2024 var end = new Date(); 2025 this.drawingTimeMs_ = (end - start); 2026 }; 2027 2028 /** 2029 * Point structure. 2030 * 2031 * xval_* and yval_* are the original unscaled data values, 2032 * while x_* and y_* are scaled to the range (0.0-1.0) for plotting. 2033 * yval_stacked is the cumulative Y value used for stacking graphs, 2034 * and bottom/top/minus/plus are used for error bar graphs. 2035 * 2036 * @typedef {{ 2037 * idx: number, 2038 * name: string, 2039 * x: ?number, 2040 * xval: ?number, 2041 * y_bottom: ?number, 2042 * y: ?number, 2043 * y_stacked: ?number, 2044 * y_top: ?number, 2045 * yval_minus: ?number, 2046 * yval: ?number, 2047 * yval_plus: ?number, 2048 * yval_stacked 2049 * }} 2050 */ 2051 Dygraph.PointType = undefined; 2052 2053 /** 2054 * Calculates point stacking for stackedGraph=true. 2055 * 2056 * For stacking purposes, interpolate or extend neighboring data across 2057 * NaN values based on stackedGraphNaNFill settings. This is for display 2058 * only, the underlying data value as shown in the legend remains NaN. 2059 * 2060 * @param {Array.<Dygraph.PointType>} points Point array for a single series. 2061 * Updates each Point's yval_stacked property. 2062 * @param {Array.<number>} cumulativeYval Accumulated top-of-graph stacked Y 2063 * values for the series seen so far. Index is the row number. Updated 2064 * based on the current series's values. 2065 * @param {Array.<number>} seriesExtremes Min and max values, updated 2066 * to reflect the stacked values. 2067 * @param {string} fillMethod Interpolation method, one of 'all', 'inside', or 2068 * 'none'. 2069 * @private 2070 */ 2071 Dygraph.stackPoints_ = function( 2072 points, cumulativeYval, seriesExtremes, fillMethod) { 2073 var lastXval = null; 2074 var prevPoint = null; 2075 var nextPoint = null; 2076 var nextPointIdx = -1; 2077 2078 // Find the next stackable point starting from the given index. 2079 var updateNextPoint = function(idx) { 2080 // If we've previously found a non-NaN point and haven't gone past it yet, 2081 // just use that. 2082 if (nextPointIdx >= idx) return; 2083 2084 // We haven't found a non-NaN point yet or have moved past it, 2085 // look towards the right to find a non-NaN point. 2086 for (var j = idx; j < points.length; ++j) { 2087 // Clear out a previously-found point (if any) since it's no longer 2088 // valid, we shouldn't use it for interpolation anymore. 2089 nextPoint = null; 2090 if (!isNaN(points[j].yval) && points[j].yval !== null) { 2091 nextPointIdx = j; 2092 nextPoint = points[j]; 2093 break; 2094 } 2095 } 2096 }; 2097 2098 for (var i = 0; i < points.length; ++i) { 2099 var point = points[i]; 2100 var xval = point.xval; 2101 if (cumulativeYval[xval] === undefined) { 2102 cumulativeYval[xval] = 0; 2103 } 2104 2105 var actualYval = point.yval; 2106 if (isNaN(actualYval) || actualYval === null) { 2107 if(fillMethod == 'none') { 2108 actualYval = 0; 2109 } else { 2110 // Interpolate/extend for stacking purposes if possible. 2111 updateNextPoint(i); 2112 if (prevPoint && nextPoint && fillMethod != 'none') { 2113 // Use linear interpolation between prevPoint and nextPoint. 2114 actualYval = prevPoint.yval + (nextPoint.yval - prevPoint.yval) * 2115 ((xval - prevPoint.xval) / (nextPoint.xval - prevPoint.xval)); 2116 } else if (prevPoint && fillMethod == 'all') { 2117 actualYval = prevPoint.yval; 2118 } else if (nextPoint && fillMethod == 'all') { 2119 actualYval = nextPoint.yval; 2120 } else { 2121 actualYval = 0; 2122 } 2123 } 2124 } else { 2125 prevPoint = point; 2126 } 2127 2128 var stackedYval = cumulativeYval[xval]; 2129 if (lastXval != xval) { 2130 // If an x-value is repeated, we ignore the duplicates. 2131 stackedYval += actualYval; 2132 cumulativeYval[xval] = stackedYval; 2133 } 2134 lastXval = xval; 2135 2136 point.yval_stacked = stackedYval; 2137 2138 if (stackedYval > seriesExtremes[1]) { 2139 seriesExtremes[1] = stackedYval; 2140 } 2141 if (stackedYval < seriesExtremes[0]) { 2142 seriesExtremes[0] = stackedYval; 2143 } 2144 } 2145 }; 2146 2147 2148 /** 2149 * Loop over all fields and create datasets, calculating extreme y-values for 2150 * each series and extreme x-indices as we go. 2151 * 2152 * dateWindow is passed in as an explicit parameter so that we can compute 2153 * extreme values "speculatively", i.e. without actually setting state on the 2154 * dygraph. 2155 * 2156 * @param {Array.<Array.<Array.<(number|Array<number>)>>} rolledSeries, where 2157 * rolledSeries[seriesIndex][row] = raw point, where 2158 * seriesIndex is the column number starting with 1, and 2159 * rawPoint is [x,y] or [x, [y, err]] or [x, [y, yminus, yplus]]. 2160 * @param {?Array.<number>} dateWindow [xmin, xmax] pair, or null. 2161 * @return {{ 2162 * points: Array.<Array.<Dygraph.PointType>>, 2163 * seriesExtremes: Array.<Array.<number>>, 2164 * boundaryIds: Array.<number>}} 2165 * @private 2166 */ 2167 Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { 2168 var boundaryIds = []; 2169 var points = []; 2170 var cumulativeYval = []; // For stacked series. 2171 var extremes = {}; // series name -> [low, high] 2172 var seriesIdx, sampleIdx; 2173 var firstIdx, lastIdx; 2174 var axisIdx; 2175 2176 // Loop over the fields (series). Go from the last to the first, 2177 // because if they're stacked that's how we accumulate the values. 2178 var num_series = rolledSeries.length - 1; 2179 var series; 2180 for (seriesIdx = num_series; seriesIdx >= 1; seriesIdx--) { 2181 if (!this.visibility()[seriesIdx - 1]) continue; 2182 2183 // Prune down to the desired range, if necessary (for zooming) 2184 // Because there can be lines going to points outside of the visible area, 2185 // we actually prune to visible points, plus one on either side. 2186 if (dateWindow) { 2187 series = rolledSeries[seriesIdx]; 2188 var low = dateWindow[0]; 2189 var high = dateWindow[1]; 2190 2191 // TODO(danvk): do binary search instead of linear search. 2192 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer. 2193 firstIdx = null; 2194 lastIdx = null; 2195 for (sampleIdx = 0; sampleIdx < series.length; sampleIdx++) { 2196 if (series[sampleIdx][0] >= low && firstIdx === null) { 2197 firstIdx = sampleIdx; 2198 } 2199 if (series[sampleIdx][0] <= high) { 2200 lastIdx = sampleIdx; 2201 } 2202 } 2203 2204 if (firstIdx === null) firstIdx = 0; 2205 var correctedFirstIdx = firstIdx; 2206 var isInvalidValue = true; 2207 while (isInvalidValue && correctedFirstIdx > 0) { 2208 correctedFirstIdx--; 2209 // check if the y value is null. 2210 isInvalidValue = series[correctedFirstIdx][1] === null; 2211 } 2212 2213 if (lastIdx === null) lastIdx = series.length - 1; 2214 var correctedLastIdx = lastIdx; 2215 isInvalidValue = true; 2216 while (isInvalidValue && correctedLastIdx < series.length - 1) { 2217 correctedLastIdx++; 2218 isInvalidValue = series[correctedLastIdx][1] === null; 2219 } 2220 2221 if (correctedFirstIdx!==firstIdx) { 2222 firstIdx = correctedFirstIdx; 2223 } 2224 if (correctedLastIdx !== lastIdx) { 2225 lastIdx = correctedLastIdx; 2226 } 2227 2228 boundaryIds[seriesIdx-1] = [firstIdx, lastIdx]; 2229 2230 // .slice's end is exclusive, we want to include lastIdx. 2231 series = series.slice(firstIdx, lastIdx + 1); 2232 } else { 2233 series = rolledSeries[seriesIdx]; 2234 boundaryIds[seriesIdx-1] = [0, series.length-1]; 2235 } 2236 2237 var seriesName = this.attr_("labels")[seriesIdx]; 2238 var seriesExtremes = this.dataHandler_.getExtremeYValues(series, 2239 dateWindow, this.getBooleanOption("stepPlot",seriesName)); 2240 2241 var seriesPoints = this.dataHandler_.seriesToPoints(series, 2242 seriesName, boundaryIds[seriesIdx-1][0]); 2243 2244 if (this.getBooleanOption("stackedGraph")) { 2245 axisIdx = this.attributes_.axisForSeries(seriesName); 2246 if (cumulativeYval[axisIdx] === undefined) { 2247 cumulativeYval[axisIdx] = []; 2248 } 2249 Dygraph.stackPoints_(seriesPoints, cumulativeYval[axisIdx], seriesExtremes, 2250 this.getBooleanOption("stackedGraphNaNFill")); 2251 } 2252 2253 extremes[seriesName] = seriesExtremes; 2254 points[seriesIdx] = seriesPoints; 2255 } 2256 2257 return { points: points, extremes: extremes, boundaryIds: boundaryIds }; 2258 }; 2259 2260 /** 2261 * Update the graph with new data. This method is called when the viewing area 2262 * has changed. If the underlying data or options have changed, predraw_ will 2263 * be called before drawGraph_ is called. 2264 * 2265 * @private 2266 */ 2267 Dygraph.prototype.drawGraph_ = function() { 2268 var start = new Date(); 2269 2270 // This is used to set the second parameter to drawCallback, below. 2271 var is_initial_draw = this.is_initial_draw_; 2272 this.is_initial_draw_ = false; 2273 2274 this.layout_.removeAllDatasets(); 2275 this.setColors_(); 2276 this.attrs_.pointSize = 0.5 * this.getNumericOption('highlightCircleSize'); 2277 2278 var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_); 2279 var points = packed.points; 2280 var extremes = packed.extremes; 2281 this.boundaryIds_ = packed.boundaryIds; 2282 2283 this.setIndexByName_ = {}; 2284 var labels = this.attr_("labels"); 2285 var dataIdx = 0; 2286 for (var i = 1; i < points.length; i++) { 2287 if (!this.visibility()[i - 1]) continue; 2288 this.layout_.addDataset(labels[i], points[i]); 2289 this.datasetIndex_[i] = dataIdx++; 2290 } 2291 for (var i = 0; i < labels.length; i++) { 2292 this.setIndexByName_[labels[i]] = i; 2293 } 2294 2295 this.computeYAxisRanges_(extremes); 2296 this.layout_.setYAxes(this.axes_); 2297 2298 this.addXTicks_(); 2299 2300 // Tell PlotKit to use this new data and render itself 2301 this.layout_.evaluate(); 2302 this.renderGraph_(is_initial_draw); 2303 2304 if (this.getStringOption("timingName")) { 2305 var end = new Date(); 2306 console.log(this.getStringOption("timingName") + " - drawGraph: " + (end - start) + "ms"); 2307 } 2308 }; 2309 2310 /** 2311 * This does the work of drawing the chart. It assumes that the layout and axis 2312 * scales have already been set (e.g. by predraw_). 2313 * 2314 * @private 2315 */ 2316 Dygraph.prototype.renderGraph_ = function(is_initial_draw) { 2317 this.cascadeEvents_('clearChart'); 2318 this.plotter_.clear(); 2319 2320 const underlayCallback = this.getFunctionOption('underlayCallback'); 2321 if (underlayCallback) { 2322 // NOTE: we pass the dygraph object to this callback twice to avoid breaking 2323 // users who expect a deprecated form of this callback. 2324 underlayCallback.call(this, 2325 this.hidden_ctx_, this.layout_.getPlotArea(), this, this); 2326 } 2327 2328 var e = { 2329 canvas: this.hidden_, 2330 drawingContext: this.hidden_ctx_ 2331 }; 2332 this.cascadeEvents_('willDrawChart', e); 2333 this.plotter_.render(); 2334 this.cascadeEvents_('didDrawChart', e); 2335 this.lastRow_ = -1; // because plugins/legend.js clears the legend 2336 2337 // TODO(danvk): is this a performance bottleneck when panning? 2338 // The interaction canvas should already be empty in that situation. 2339 this.canvas_.getContext('2d').clearRect(0, 0, this.width_, this.height_); 2340 2341 const drawCallback = this.getFunctionOption("drawCallback"); 2342 if (drawCallback !== null) { 2343 drawCallback.call(this, this, is_initial_draw); 2344 } 2345 if (is_initial_draw) { 2346 this.readyFired_ = true; 2347 while (this.readyFns_.length > 0) { 2348 var fn = this.readyFns_.pop(); 2349 fn(this); 2350 } 2351 } 2352 }; 2353 2354 /** 2355 * @private 2356 * Determine properties of the y-axes which are independent of the data 2357 * currently being displayed. This includes things like the number of axes and 2358 * the style of the axes. It does not include the range of each axis and its 2359 * tick marks. 2360 * This fills in this.axes_. 2361 * axes_ = [ { options } ] 2362 * indices are into the axes_ array. 2363 */ 2364 Dygraph.prototype.computeYAxes_ = function() { 2365 var axis, index, opts, v; 2366 2367 // this.axes_ doesn't match this.attributes_.axes_.options. It's used for 2368 // data computation as well as options storage. 2369 // Go through once and add all the axes. 2370 this.axes_ = []; 2371 2372 for (axis = 0; axis < this.attributes_.numAxes(); axis++) { 2373 // Add a new axis, making a copy of its per-axis options. 2374 opts = { g : this }; 2375 utils.update(opts, this.attributes_.axisOptions(axis)); 2376 this.axes_[axis] = opts; 2377 } 2378 2379 for (axis = 0; axis < this.axes_.length; axis++) { 2380 if (axis === 0) { 2381 opts = this.optionsViewForAxis_('y' + (axis ? '2' : '')); 2382 v = opts("valueRange"); 2383 if (v) this.axes_[axis].valueRange = v; 2384 } else { // To keep old behavior 2385 var axes = this.user_attrs_.axes; 2386 if (axes && axes.y2) { 2387 v = axes.y2.valueRange; 2388 if (v) this.axes_[axis].valueRange = v; 2389 } 2390 } 2391 } 2392 }; 2393 2394 /** 2395 * Returns the number of y-axes on the chart. 2396 * @return {number} the number of axes. 2397 */ 2398 Dygraph.prototype.numAxes = function() { 2399 return this.attributes_.numAxes(); 2400 }; 2401 2402 /** 2403 * @private 2404 * Returns axis properties for the given series. 2405 * @param {string} setName The name of the series for which to get axis 2406 * properties, e.g. 'Y1'. 2407 * @return {Object} The axis properties. 2408 */ 2409 Dygraph.prototype.axisPropertiesForSeries = function(series) { 2410 // TODO(danvk): handle errors. 2411 return this.axes_[this.attributes_.axisForSeries(series)]; 2412 }; 2413 2414 /** 2415 * @private 2416 * Determine the value range and tick marks for each axis. 2417 * @param {Object} extremes A mapping from seriesName -> [low, high] 2418 * This fills in the valueRange and ticks fields in each entry of this.axes_. 2419 */ 2420 Dygraph.prototype.computeYAxisRanges_ = function(extremes) { 2421 var isNullUndefinedOrNaN = function(num) { 2422 return isNaN(parseFloat(num)); 2423 }; 2424 var numAxes = this.attributes_.numAxes(); 2425 var ypadCompat, span, series, ypad; 2426 2427 var p_axis; 2428 2429 // Compute extreme values, a span and tick marks for each axis. 2430 for (var i = 0; i < numAxes; i++) { 2431 var axis = this.axes_[i]; 2432 var logscale = this.attributes_.getForAxis("logscale", i); 2433 var includeZero = this.attributes_.getForAxis("includeZero", i); 2434 var independentTicks = this.attributes_.getForAxis("independentTicks", i); 2435 series = this.attributes_.seriesForAxis(i); 2436 2437 // Add some padding. This supports two Y padding operation modes: 2438 // 2439 // - backwards compatible (yRangePad not set): 2440 // 10% padding for automatic Y ranges, but not for user-supplied 2441 // ranges, and move a close-to-zero edge to zero, since drawing at the edge 2442 // results in invisible lines. Unfortunately lines drawn at the edge of a 2443 // user-supplied range will still be invisible. If logscale is 2444 // set, add a variable amount of padding at the top but 2445 // none at the bottom. 2446 // 2447 // - new-style (yRangePad set by the user): 2448 // always add the specified Y padding. 2449 // 2450 ypadCompat = true; 2451 ypad = 0.1; // add 10% 2452 const yRangePad = this.getNumericOption('yRangePad'); 2453 if (yRangePad !== null) { 2454 ypadCompat = false; 2455 // Convert pixel padding to ratio 2456 ypad = yRangePad / this.plotter_.area.h; 2457 } 2458 2459 if (series.length === 0) { 2460 // If no series are defined or visible then use a reasonable default 2461 axis.extremeRange = [0, 1]; 2462 } else { 2463 // Calculate the extremes of extremes. 2464 var minY = Infinity; // extremes[series[0]][0]; 2465 var maxY = -Infinity; // extremes[series[0]][1]; 2466 var extremeMinY, extremeMaxY; 2467 2468 for (var j = 0; j < series.length; j++) { 2469 // this skips invisible series 2470 if (!extremes.hasOwnProperty(series[j])) continue; 2471 2472 // Only use valid extremes to stop null data series' from corrupting the scale. 2473 extremeMinY = extremes[series[j]][0]; 2474 if (extremeMinY !== null) { 2475 minY = Math.min(extremeMinY, minY); 2476 } 2477 extremeMaxY = extremes[series[j]][1]; 2478 if (extremeMaxY !== null) { 2479 maxY = Math.max(extremeMaxY, maxY); 2480 } 2481 } 2482 2483 // Include zero if requested by the user. 2484 if (includeZero && !logscale) { 2485 if (minY > 0) minY = 0; 2486 if (maxY < 0) maxY = 0; 2487 } 2488 2489 // Ensure we have a valid scale, otherwise default to [0, 1] for safety. 2490 if (minY == Infinity) minY = 0; 2491 if (maxY == -Infinity) maxY = 1; 2492 2493 span = maxY - minY; 2494 // special case: if we have no sense of scale, center on the sole value. 2495 if (span === 0) { 2496 if (maxY !== 0) { 2497 span = Math.abs(maxY); 2498 } else { 2499 // ... and if the sole value is zero, use range 0-1. 2500 maxY = 1; 2501 span = 1; 2502 } 2503 } 2504 2505 var maxAxisY = maxY, minAxisY = minY; 2506 if (ypadCompat) { 2507 if (logscale) { 2508 maxAxisY = maxY + ypad * span; 2509 minAxisY = minY; 2510 } else { 2511 maxAxisY = maxY + ypad * span; 2512 minAxisY = minY - ypad * span; 2513 2514 // Backwards-compatible behavior: Move the span to start or end at zero if it's 2515 // close to zero. 2516 if (minAxisY < 0 && minY >= 0) minAxisY = 0; 2517 if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0; 2518 } 2519 } 2520 axis.extremeRange = [minAxisY, maxAxisY]; 2521 } 2522 if (axis.valueRange) { 2523 // This is a user-set value range for this axis. 2524 var y0 = isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0]; 2525 var y1 = isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1]; 2526 axis.computedValueRange = [y0, y1]; 2527 } else { 2528 axis.computedValueRange = axis.extremeRange; 2529 } 2530 if (!ypadCompat) { 2531 // When using yRangePad, adjust the upper/lower bounds to add 2532 // padding unless the user has zoomed/panned the Y axis range. 2533 if (logscale) { 2534 y0 = axis.computedValueRange[0]; 2535 y1 = axis.computedValueRange[1]; 2536 var y0pct = ypad / (2 * ypad - 1); 2537 var y1pct = (ypad - 1) / (2 * ypad - 1); 2538 axis.computedValueRange[0] = utils.logRangeFraction(y0, y1, y0pct); 2539 axis.computedValueRange[1] = utils.logRangeFraction(y0, y1, y1pct); 2540 } else { 2541 y0 = axis.computedValueRange[0]; 2542 y1 = axis.computedValueRange[1]; 2543 span = y1 - y0; 2544 axis.computedValueRange[0] = y0 - span * ypad; 2545 axis.computedValueRange[1] = y1 + span * ypad; 2546 } 2547 } 2548 2549 2550 if (independentTicks) { 2551 axis.independentTicks = independentTicks; 2552 var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); 2553 var ticker = opts('ticker'); 2554 axis.ticks = ticker(axis.computedValueRange[0], 2555 axis.computedValueRange[1], 2556 this.plotter_.area.h, 2557 opts, 2558 this); 2559 // Define the first independent axis as primary axis. 2560 if (!p_axis) p_axis = axis; 2561 } 2562 } 2563 if (p_axis === undefined) { 2564 throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated."); 2565 } 2566 // Add ticks. By default, all axes inherit the tick positions of the 2567 // primary axis. However, if an axis is specifically marked as having 2568 // independent ticks, then that is permissible as well. 2569 for (var i = 0; i < numAxes; i++) { 2570 var axis = this.axes_[i]; 2571 2572 if (!axis.independentTicks) { 2573 var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); 2574 var ticker = opts('ticker'); 2575 var p_ticks = p_axis.ticks; 2576 var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0]; 2577 var scale = axis.computedValueRange[1] - axis.computedValueRange[0]; 2578 var tick_values = []; 2579 for (var k = 0; k < p_ticks.length; k++) { 2580 var y_frac = (p_ticks[k].v - p_axis.computedValueRange[0]) / p_scale; 2581 var y_val = axis.computedValueRange[0] + y_frac * scale; 2582 tick_values.push(y_val); 2583 } 2584 2585 axis.ticks = ticker(axis.computedValueRange[0], 2586 axis.computedValueRange[1], 2587 this.plotter_.area.h, 2588 opts, 2589 this, 2590 tick_values); 2591 } 2592 } 2593 }; 2594 2595 /** 2596 * Detects the type of the str (date or numeric) and sets the various 2597 * formatting attributes in this.attrs_ based on this type. 2598 * @param {string} str An x value. 2599 * @private 2600 */ 2601 Dygraph.prototype.detectTypeFromString_ = function(str) { 2602 var isDate = false; 2603 var dashPos = str.indexOf('-'); // could be 2006-01-01 _or_ 1.0e-2 2604 if ((dashPos > 0 && (str[dashPos-1] != 'e' && str[dashPos-1] != 'E')) || 2605 str.indexOf('/') >= 0 || 2606 isNaN(parseFloat(str))) { 2607 isDate = true; 2608 } else if (str.length == 8 && str > '19700101' && str < '20371231') { 2609 // TODO(danvk): remove support for this format. 2610 isDate = true; 2611 } 2612 2613 this.setXAxisOptions_(isDate); 2614 }; 2615 2616 Dygraph.prototype.setXAxisOptions_ = function(isDate) { 2617 if (isDate) { 2618 this.attrs_.xValueParser = utils.dateParser; 2619 this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter; 2620 this.attrs_.axes.x.ticker = DygraphTickers.dateTicker; 2621 this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter; 2622 } else { 2623 /** @private (shut up, jsdoc!) */ 2624 this.attrs_.xValueParser = function(x) { return parseFloat(x); }; 2625 // TODO(danvk): use Dygraph.numberValueFormatter here? 2626 /** @private (shut up, jsdoc!) */ 2627 this.attrs_.axes.x.valueFormatter = function(x) { return x; }; 2628 this.attrs_.axes.x.ticker = DygraphTickers.numericTicks; 2629 this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; 2630 } 2631 }; 2632 2633 /** 2634 * @private 2635 * Parses a string in a special csv format. We expect a csv file where each 2636 * line is a date point, and the first field in each line is the date string. 2637 * We also expect that all remaining fields represent series. 2638 * if the errorBars attribute is set, then interpret the fields as: 2639 * date, series1, stddev1, series2, stddev2, ... 2640 * @param {[Object]} data See above. 2641 * 2642 * @return [Object] An array with one entry for each row. These entries 2643 * are an array of cells in that row. The first entry is the parsed x-value for 2644 * the row. The second, third, etc. are the y-values. These can take on one of 2645 * three forms, depending on the CSV and constructor parameters: 2646 * 1. numeric value 2647 * 2. [ value, stddev ] 2648 * 3. [ low value, center value, high value ] 2649 */ 2650 Dygraph.prototype.parseCSV_ = function(data) { 2651 var ret = []; 2652 var line_delimiter = utils.detectLineDelimiter(data); 2653 var lines = data.split(line_delimiter || "\n"); 2654 var vals, j; 2655 2656 // Use the default delimiter or fall back to a tab if that makes sense. 2657 var delim = this.getStringOption('delimiter'); 2658 if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) { 2659 delim = '\t'; 2660 } 2661 2662 var start = 0; 2663 if (!('labels' in this.user_attrs_)) { 2664 // User hasn't explicitly set labels, so they're (presumably) in the CSV. 2665 start = 1; 2666 this.attrs_.labels = lines[0].split(delim); // NOTE: _not_ user_attrs_. 2667 this.attributes_.reparseSeries(); 2668 } 2669 var line_no = 0; 2670 2671 var xParser; 2672 var defaultParserSet = false; // attempt to auto-detect x value type 2673 var expectedCols = this.attr_("labels").length; 2674 var outOfOrder = false; 2675 for (var i = start; i < lines.length; i++) { 2676 var line = lines[i]; 2677 line_no = i; 2678 if (line.length === 0) continue; // skip blank lines 2679 if (line[0] == '#') continue; // skip comment lines 2680 var inFields = line.split(delim); 2681 if (inFields.length < 2) continue; 2682 2683 var fields = []; 2684 if (!defaultParserSet) { 2685 this.detectTypeFromString_(inFields[0]); 2686 xParser = this.getFunctionOption("xValueParser"); 2687 defaultParserSet = true; 2688 } 2689 fields[0] = xParser(inFields[0], this); 2690 2691 // If fractions are expected, parse the numbers as "A/B" 2692 if (this.fractions_) { 2693 for (j = 1; j < inFields.length; j++) { 2694 // TODO(danvk): figure out an appropriate way to flag parse errors. 2695 vals = inFields[j].split("/"); 2696 if (vals.length != 2) { 2697 console.error('Expected fractional "num/den" values in CSV data ' + 2698 "but found a value '" + inFields[j] + "' on line " + 2699 (1 + i) + " ('" + line + "') which is not of this form."); 2700 fields[j] = [0, 0]; 2701 } else { 2702 fields[j] = [utils.parseFloat_(vals[0], i, line), 2703 utils.parseFloat_(vals[1], i, line)]; 2704 } 2705 } 2706 } else if (this.getBooleanOption("errorBars")) { 2707 // If there are error bars, values are (value, stddev) pairs 2708 if (inFields.length % 2 != 1) { 2709 console.error('Expected alternating (value, stdev.) pairs in CSV data ' + 2710 'but line ' + (1 + i) + ' has an odd number of values (' + 2711 (inFields.length - 1) + "): '" + line + "'"); 2712 } 2713 for (j = 1; j < inFields.length; j += 2) { 2714 fields[(j + 1) / 2] = [utils.parseFloat_(inFields[j], i, line), 2715 utils.parseFloat_(inFields[j + 1], i, line)]; 2716 } 2717 } else if (this.getBooleanOption("customBars")) { 2718 // Bars are a low;center;high tuple 2719 for (j = 1; j < inFields.length; j++) { 2720 var val = inFields[j]; 2721 if (/^ *$/.test(val)) { 2722 fields[j] = [null, null, null]; 2723 } else { 2724 vals = val.split(";"); 2725 if (vals.length == 3) { 2726 fields[j] = [ utils.parseFloat_(vals[0], i, line), 2727 utils.parseFloat_(vals[1], i, line), 2728 utils.parseFloat_(vals[2], i, line) ]; 2729 } else { 2730 console.warn('When using customBars, values must be either blank ' + 2731 'or "low;center;high" tuples (got "' + val + 2732 '" on line ' + (1+i)); 2733 } 2734 } 2735 } 2736 } else { 2737 // Values are just numbers 2738 for (j = 1; j < inFields.length; j++) { 2739 fields[j] = utils.parseFloat_(inFields[j], i, line); 2740 } 2741 } 2742 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) { 2743 outOfOrder = true; 2744 } 2745 2746 if (fields.length != expectedCols) { 2747 console.error("Number of columns in line " + i + " (" + fields.length + 2748 ") does not agree with number of labels (" + expectedCols + 2749 ") " + line); 2750 } 2751 2752 // If the user specified the 'labels' option and none of the cells of the 2753 // first row parsed correctly, then they probably double-specified the 2754 // labels. We go with the values set in the option, discard this row and 2755 // log a warning to the JS console. 2756 if (i === 0 && this.attr_('labels')) { 2757 var all_null = true; 2758 for (j = 0; all_null && j < fields.length; j++) { 2759 if (fields[j]) all_null = false; 2760 } 2761 if (all_null) { 2762 console.warn("The dygraphs 'labels' option is set, but the first row " + 2763 "of CSV data ('" + line + "') appears to also contain " + 2764 "labels. Will drop the CSV labels and use the option " + 2765 "labels."); 2766 continue; 2767 } 2768 } 2769 ret.push(fields); 2770 } 2771 2772 if (outOfOrder) { 2773 console.warn("CSV is out of order; order it correctly to speed loading."); 2774 ret.sort(function(a,b) { return a[0] - b[0]; }); 2775 } 2776 2777 return ret; 2778 }; 2779 2780 // In native format, all values must be dates or numbers. 2781 // This check isn't perfect but will catch most mistaken uses of strings. 2782 function validateNativeFormat(data) { 2783 const firstRow = data[0]; 2784 const firstX = firstRow[0]; 2785 if (typeof firstX !== 'number' && !utils.isDateLike(firstX)) { 2786 throw new Error(`Expected number or date but got ${typeof firstX}: ${firstX}.`); 2787 } 2788 for (let i = 1; i < firstRow.length; i++) { 2789 const val = firstRow[i]; 2790 if (val === null || val === undefined) continue; 2791 if (typeof val === 'number') continue; 2792 if (utils.isArrayLike(val)) continue; // e.g. error bars or custom bars. 2793 throw new Error(`Expected number or array but got ${typeof val}: ${val}.`); 2794 } 2795 } 2796 2797 /** 2798 * The user has provided their data as a pre-packaged JS array. If the x values 2799 * are numeric, this is the same as dygraphs' internal format. If the x values 2800 * are dates, we need to convert them from Date objects to ms since epoch. 2801 * @param {!Array} data 2802 * @return {Object} data with numeric x values. 2803 * @private 2804 */ 2805 Dygraph.prototype.parseArray_ = function(data) { 2806 // Peek at the first x value to see if it's numeric. 2807 if (data.length === 0) { 2808 console.error("Can't plot empty data set"); 2809 return null; 2810 } 2811 if (data[0].length === 0) { 2812 console.error("Data set cannot contain an empty row"); 2813 return null; 2814 } 2815 2816 validateNativeFormat(data); 2817 2818 var i; 2819 if (this.attr_("labels") === null) { 2820 console.warn("Using default labels. Set labels explicitly via 'labels' " + 2821 "in the options parameter"); 2822 this.attrs_.labels = [ "X" ]; 2823 for (i = 1; i < data[0].length; i++) { 2824 this.attrs_.labels.push("Y" + i); // Not user_attrs_. 2825 } 2826 this.attributes_.reparseSeries(); 2827 } else { 2828 var num_labels = this.attr_("labels"); 2829 if (num_labels.length != data[0].length) { 2830 console.error("Mismatch between number of labels (" + num_labels + ")" + 2831 " and number of columns in array (" + data[0].length + ")"); 2832 return null; 2833 } 2834 } 2835 2836 if (utils.isDateLike(data[0][0])) { 2837 // Some intelligent defaults for a date x-axis. 2838 this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter; 2839 this.attrs_.axes.x.ticker = DygraphTickers.dateTicker; 2840 this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter; 2841 2842 // Assume they're all dates. 2843 var parsedData = utils.clone(data); 2844 for (i = 0; i < data.length; i++) { 2845 if (parsedData[i].length === 0) { 2846 console.error("Row " + (1 + i) + " of data is empty"); 2847 return null; 2848 } 2849 if (parsedData[i][0] === null || 2850 typeof(parsedData[i][0].getTime) != 'function' || 2851 isNaN(parsedData[i][0].getTime())) { 2852 console.error("x value in row " + (1 + i) + " is not a Date"); 2853 return null; 2854 } 2855 parsedData[i][0] = parsedData[i][0].getTime(); 2856 } 2857 return parsedData; 2858 } else { 2859 // Some intelligent defaults for a numeric x-axis. 2860 /** @private (shut up, jsdoc!) */ 2861 this.attrs_.axes.x.valueFormatter = function(x) { return x; }; 2862 this.attrs_.axes.x.ticker = DygraphTickers.numericTicks; 2863 this.attrs_.axes.x.axisLabelFormatter = utils.numberAxisLabelFormatter; 2864 return data; 2865 } 2866 }; 2867 2868 /** 2869 * Parses a DataTable object from gviz. 2870 * The data is expected to have a first column that is either a date or a 2871 * number. All subsequent columns must be numbers. If there is a clear mismatch 2872 * between this.xValueParser_ and the type of the first column, it will be 2873 * fixed. Fills out rawData_. 2874 * @param {!google.visualization.DataTable} data See above. 2875 * @private 2876 */ 2877 Dygraph.prototype.parseDataTable_ = function(data) { 2878 var shortTextForAnnotationNum = function(num) { 2879 // converts [0-9]+ [A-Z][a-z]* 2880 // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab 2881 // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz 2882 var shortText = String.fromCharCode(65 /* A */ + num % 26); 2883 num = Math.floor(num / 26); 2884 while ( num > 0 ) { 2885 shortText = String.fromCharCode(65 /* A */ + (num - 1) % 26 ) + shortText.toLowerCase(); 2886 num = Math.floor((num - 1) / 26); 2887 } 2888 return shortText; 2889 }; 2890 2891 var cols = data.getNumberOfColumns(); 2892 var rows = data.getNumberOfRows(); 2893 2894 var indepType = data.getColumnType(0); 2895 if (indepType == 'date' || indepType == 'datetime') { 2896 this.attrs_.xValueParser = utils.dateParser; 2897 this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter; 2898 this.attrs_.axes.x.ticker = DygraphTickers.dateTicker; 2899 this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter; 2900 } else if (indepType == 'number') { 2901 this.attrs_.xValueParser = function(x) { return parseFloat(x); }; 2902 this.attrs_.axes.x.valueFormatter = function(x) { return x; }; 2903 this.attrs_.axes.x.ticker = DygraphTickers.numericTicks; 2904 this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; 2905 } else { 2906 throw new Error( 2907 "only 'date', 'datetime' and 'number' types are supported " + 2908 "for column 1 of DataTable input (Got '" + indepType + "')"); 2909 } 2910 2911 // Array of the column indices which contain data (and not annotations). 2912 var colIdx = []; 2913 var annotationCols = {}; // data index -> [annotation cols] 2914 var hasAnnotations = false; 2915 var i, j; 2916 for (i = 1; i < cols; i++) { 2917 var type = data.getColumnType(i); 2918 if (type == 'number') { 2919 colIdx.push(i); 2920 } else if (type == 'string' && this.getBooleanOption('displayAnnotations')) { 2921 // This is OK -- it's an annotation column. 2922 var dataIdx = colIdx[colIdx.length - 1]; 2923 if (!annotationCols.hasOwnProperty(dataIdx)) { 2924 annotationCols[dataIdx] = [i]; 2925 } else { 2926 annotationCols[dataIdx].push(i); 2927 } 2928 hasAnnotations = true; 2929 } else { 2930 throw new Error( 2931 "Only 'number' is supported as a dependent type with Gviz." + 2932 " 'string' is only supported if displayAnnotations is true"); 2933 } 2934 } 2935 2936 // Read column labels 2937 // TODO(danvk): add support back for errorBars 2938 var labels = [data.getColumnLabel(0)]; 2939 for (i = 0; i < colIdx.length; i++) { 2940 labels.push(data.getColumnLabel(colIdx[i])); 2941 if (this.getBooleanOption("errorBars")) i += 1; 2942 } 2943 this.attrs_.labels = labels; 2944 cols = labels.length; 2945 2946 var ret = []; 2947 var outOfOrder = false; 2948 var annotations = []; 2949 for (i = 0; i < rows; i++) { 2950 var row = []; 2951 if (typeof(data.getValue(i, 0)) === 'undefined' || 2952 data.getValue(i, 0) === null) { 2953 console.warn("Ignoring row " + i + 2954 " of DataTable because of undefined or null first column."); 2955 continue; 2956 } 2957 2958 if (indepType == 'date' || indepType == 'datetime') { 2959 row.push(data.getValue(i, 0).getTime()); 2960 } else { 2961 row.push(data.getValue(i, 0)); 2962 } 2963 if (!this.getBooleanOption("errorBars")) { 2964 for (j = 0; j < colIdx.length; j++) { 2965 var col = colIdx[j]; 2966 row.push(data.getValue(i, col)); 2967 if (hasAnnotations && 2968 annotationCols.hasOwnProperty(col) && 2969 data.getValue(i, annotationCols[col][0]) !== null) { 2970 var ann = {}; 2971 ann.series = data.getColumnLabel(col); 2972 ann.xval = row[0]; 2973 ann.shortText = shortTextForAnnotationNum(annotations.length); 2974 ann.text = ''; 2975 for (var k = 0; k < annotationCols[col].length; k++) { 2976 if (k) ann.text += "\n"; 2977 ann.text += data.getValue(i, annotationCols[col][k]); 2978 } 2979 annotations.push(ann); 2980 } 2981 } 2982 2983 // Strip out infinities, which give dygraphs problems later on. 2984 for (j = 0; j < row.length; j++) { 2985 if (!isFinite(row[j])) row[j] = null; 2986 } 2987 } else { 2988 for (j = 0; j < cols - 1; j++) { 2989 row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]); 2990 } 2991 } 2992 if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) { 2993 outOfOrder = true; 2994 } 2995 ret.push(row); 2996 } 2997 2998 if (outOfOrder) { 2999 console.warn("DataTable is out of order; order it correctly to speed loading."); 3000 ret.sort(function(a,b) { return a[0] - b[0]; }); 3001 } 3002 this.rawData_ = ret; 3003 3004 if (annotations.length > 0) { 3005 this.setAnnotations(annotations, true); 3006 } 3007 this.attributes_.reparseSeries(); 3008 }; 3009 3010 /** 3011 * Signals to plugins that the chart data has updated. 3012 * This happens after the data has updated but before the chart has redrawn. 3013 * @private 3014 */ 3015 Dygraph.prototype.cascadeDataDidUpdateEvent_ = function() { 3016 // TODO(danvk): there are some issues checking xAxisRange() and using 3017 // toDomCoords from handlers of this event. The visible range should be set 3018 // when the chart is drawn, not derived from the data. 3019 this.cascadeEvents_('dataDidUpdate', {}); 3020 }; 3021 3022 /** 3023 * Get the CSV data. If it's in a function, call that function. If it's in a 3024 * file, do an XMLHttpRequest to get it. 3025 * @private 3026 */ 3027 Dygraph.prototype.start_ = function() { 3028 var data = this.file_; 3029 3030 // Functions can return references of all other types. 3031 if (typeof data == 'function') { 3032 data = data(); 3033 } 3034 3035 if (utils.isArrayLike(data)) { 3036 this.rawData_ = this.parseArray_(data); 3037 this.cascadeDataDidUpdateEvent_(); 3038 this.predraw_(); 3039 } else if (typeof data == 'object' && 3040 typeof data.getColumnRange == 'function') { 3041 // must be a DataTable from gviz. 3042 this.parseDataTable_(data); 3043 this.cascadeDataDidUpdateEvent_(); 3044 this.predraw_(); 3045 } else if (typeof data == 'string') { 3046 // Heuristic: a newline means it's CSV data. Otherwise it's an URL. 3047 var line_delimiter = utils.detectLineDelimiter(data); 3048 if (line_delimiter) { 3049 this.loadedEvent_(data); 3050 } else { 3051 // REMOVE_FOR_IE 3052 var req; 3053 if (window.XMLHttpRequest) { 3054 // Firefox, Opera, IE7, and other browsers will use the native object 3055 req = new XMLHttpRequest(); 3056 } else { 3057 // IE 5 and 6 will use the ActiveX control 3058 req = new ActiveXObject("Microsoft.XMLHTTP"); 3059 } 3060 3061 var caller = this; 3062 req.onreadystatechange = function () { 3063 if (req.readyState == 4) { 3064 if (req.status === 200 || // Normal http 3065 req.status === 0) { // Chrome w/ --allow-file-access-from-files 3066 caller.loadedEvent_(req.responseText); 3067 } 3068 } 3069 }; 3070 3071 req.open("GET", data, true); 3072 req.send(null); 3073 } 3074 } else { 3075 console.error("Unknown data format: " + (typeof data)); 3076 } 3077 }; 3078 3079 /** 3080 * Changes various properties of the graph. These can include: 3081 * <ul> 3082 * <li>file: changes the source data for the graph</li> 3083 * <li>errorBars: changes whether the data contains stddev</li> 3084 * </ul> 3085 * 3086 * There's a huge variety of options that can be passed to this method. For a 3087 * full list, see http://dygraphs.com/options.html. 3088 * 3089 * @param {Object} input_attrs The new properties and values 3090 * @param {boolean} block_redraw Usually the chart is redrawn after every 3091 * call to updateOptions(). If you know better, you can pass true to 3092 * explicitly block the redraw. This can be useful for chaining 3093 * updateOptions() calls, avoiding the occasional infinite loop and 3094 * preventing redraws when it's not necessary (e.g. when updating a 3095 * callback). 3096 */ 3097 Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) { 3098 if (typeof(block_redraw) == 'undefined') block_redraw = false; 3099 3100 // copyUserAttrs_ drops the "file" parameter as a convenience to us. 3101 var file = input_attrs.file; 3102 var attrs = Dygraph.copyUserAttrs_(input_attrs); 3103 3104 // TODO(danvk): this is a mess. Move these options into attr_. 3105 if ('rollPeriod' in attrs) { 3106 this.rollPeriod_ = attrs.rollPeriod; 3107 } 3108 if ('dateWindow' in attrs) { 3109 this.dateWindow_ = attrs.dateWindow; 3110 } 3111 3112 // TODO(danvk): validate per-series options. 3113 // Supported: 3114 // strokeWidth 3115 // pointSize 3116 // drawPoints 3117 // highlightCircleSize 3118 3119 // Check if this set options will require new points. 3120 var requiresNewPoints = utils.isPixelChangingOptionList(this.attr_("labels"), attrs); 3121 3122 utils.updateDeep(this.user_attrs_, attrs); 3123 3124 this.attributes_.reparseSeries(); 3125 3126 if (file) { 3127 // This event indicates that the data is about to change, but hasn't yet. 3128 // TODO(danvk): support cancelation of the update via this event. 3129 this.cascadeEvents_('dataWillUpdate', {}); 3130 3131 this.file_ = file; 3132 if (!block_redraw) this.start_(); 3133 } else { 3134 if (!block_redraw) { 3135 if (requiresNewPoints) { 3136 this.predraw_(); 3137 } else { 3138 this.renderGraph_(false); 3139 } 3140 } 3141 } 3142 }; 3143 3144 /** 3145 * Make a copy of input attributes, removing file as a convenience. 3146 * @private 3147 */ 3148 Dygraph.copyUserAttrs_ = function(attrs) { 3149 var my_attrs = {}; 3150 for (var k in attrs) { 3151 if (!attrs.hasOwnProperty(k)) continue; 3152 if (k == 'file') continue; 3153 if (attrs.hasOwnProperty(k)) my_attrs[k] = attrs[k]; 3154 } 3155 return my_attrs; 3156 }; 3157 3158 /** 3159 * Resizes the dygraph. If no parameters are specified, resizes to fill the 3160 * containing div (which has presumably changed size since the dygraph was 3161 * instantiated. If the width/height are specified, the div will be resized. 3162 * 3163 * This is far more efficient than destroying and re-instantiating a 3164 * Dygraph, since it doesn't have to reparse the underlying data. 3165 * 3166 * @param {number} width Width (in pixels) 3167 * @param {number} height Height (in pixels) 3168 */ 3169 Dygraph.prototype.resize = function(width, height) { 3170 if (this.resize_lock) { 3171 return; 3172 } 3173 this.resize_lock = true; 3174 3175 if ((width === null) != (height === null)) { 3176 console.warn("Dygraph.resize() should be called with zero parameters or " + 3177 "two non-NULL parameters. Pretending it was zero."); 3178 width = height = null; 3179 } 3180 3181 var old_width = this.width_; 3182 var old_height = this.height_; 3183 3184 if (width) { 3185 this.maindiv_.style.width = width + "px"; 3186 this.maindiv_.style.height = height + "px"; 3187 this.width_ = width; 3188 this.height_ = height; 3189 } else { 3190 this.width_ = this.maindiv_.clientWidth; 3191 this.height_ = this.maindiv_.clientHeight; 3192 } 3193 3194 if (old_width != this.width_ || old_height != this.height_) { 3195 // Resizing a canvas erases it, even when the size doesn't change, so 3196 // any resize needs to be followed by a redraw. 3197 this.resizeElements_(); 3198 this.predraw_(); 3199 } 3200 3201 this.resize_lock = false; 3202 }; 3203 3204 /** 3205 * Adjusts the number of points in the rolling average. Updates the graph to 3206 * reflect the new averaging period. 3207 * @param {number} length Number of points over which to average the data. 3208 */ 3209 Dygraph.prototype.adjustRoll = function(length) { 3210 this.rollPeriod_ = length; 3211 this.predraw_(); 3212 }; 3213 3214 /** 3215 * Returns a boolean array of visibility statuses. 3216 */ 3217 Dygraph.prototype.visibility = function() { 3218 // Do lazy-initialization, so that this happens after we know the number of 3219 // data series. 3220 if (!this.getOption("visibility")) { 3221 this.attrs_.visibility = []; 3222 } 3223 // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs. 3224 while (this.getOption("visibility").length < this.numColumns() - 1) { 3225 this.attrs_.visibility.push(true); 3226 } 3227 return this.getOption("visibility"); 3228 }; 3229 3230 /** 3231 * Changes the visibility of one or more series. 3232 * 3233 * @param {number|number[]|object} num the series index or an array of series indices 3234 * or a boolean array of visibility states by index 3235 * or an object mapping series numbers, as keys, to 3236 * visibility state (boolean values) 3237 * @param {boolean} value the visibility state expressed as a boolean 3238 */ 3239 Dygraph.prototype.setVisibility = function(num, value) { 3240 var x = this.visibility(); 3241 var numIsObject = false; 3242 3243 if (!Array.isArray(num)) { 3244 if (num !== null && typeof num === 'object') { 3245 numIsObject = true; 3246 } else { 3247 num = [num]; 3248 } 3249 } 3250 3251 if (numIsObject) { 3252 for (var i in num) { 3253 if (num.hasOwnProperty(i)) { 3254 if (i < 0 || i >= x.length) { 3255 console.warn("Invalid series number in setVisibility: " + i); 3256 } else { 3257 x[i] = num[i]; 3258 } 3259 } 3260 } 3261 } else { 3262 for (var i = 0; i < num.length; i++) { 3263 if (typeof num[i] === 'boolean') { 3264 if (i >= x.length) { 3265 console.warn("Invalid series number in setVisibility: " + i); 3266 } else { 3267 x[i] = num[i]; 3268 } 3269 } else { 3270 if (num[i] < 0 || num[i] >= x.length) { 3271 console.warn("Invalid series number in setVisibility: " + num[i]); 3272 } else { 3273 x[num[i]] = value; 3274 } 3275 } 3276 } 3277 } 3278 3279 this.predraw_(); 3280 }; 3281 3282 /** 3283 * How large of an area will the dygraph render itself in? 3284 * This is used for testing. 3285 * @return A {width: w, height: h} object. 3286 * @private 3287 */ 3288 Dygraph.prototype.size = function() { 3289 return { width: this.width_, height: this.height_ }; 3290 }; 3291 3292 /** 3293 * Update the list of annotations and redraw the chart. 3294 * See dygraphs.com/annotations.html for more info on how to use annotations. 3295 * @param ann {Array} An array of annotation objects. 3296 * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional). 3297 */ 3298 Dygraph.prototype.setAnnotations = function(ann, suppressDraw) { 3299 // Only add the annotation CSS rule once we know it will be used. 3300 this.annotations_ = ann; 3301 if (!this.layout_) { 3302 console.warn("Tried to setAnnotations before dygraph was ready. " + 3303 "Try setting them in a ready() block. See " + 3304 "dygraphs.com/tests/annotation.html"); 3305 return; 3306 } 3307 3308 this.layout_.setAnnotations(this.annotations_); 3309 if (!suppressDraw) { 3310 this.predraw_(); 3311 } 3312 }; 3313 3314 /** 3315 * Return the list of annotations. 3316 */ 3317 Dygraph.prototype.annotations = function() { 3318 return this.annotations_; 3319 }; 3320 3321 /** 3322 * Get the list of label names for this graph. The first column is the 3323 * x-axis, so the data series names start at index 1. 3324 * 3325 * Returns null when labels have not yet been defined. 3326 */ 3327 Dygraph.prototype.getLabels = function() { 3328 var labels = this.attr_("labels"); 3329 return labels ? labels.slice() : null; 3330 }; 3331 3332 /** 3333 * Get the index of a series (column) given its name. The first column is the 3334 * x-axis, so the data series start with index 1. 3335 */ 3336 Dygraph.prototype.indexFromSetName = function(name) { 3337 return this.setIndexByName_[name]; 3338 }; 3339 3340 /** 3341 * Find the row number corresponding to the given x-value. 3342 * Returns null if there is no such x-value in the data. 3343 * If there are multiple rows with the same x-value, this will return the 3344 * first one. 3345 * @param {number} xVal The x-value to look for (e.g. millis since epoch). 3346 * @return {?number} The row number, which you can pass to getValue(), or null. 3347 */ 3348 Dygraph.prototype.getRowForX = function(xVal) { 3349 var low = 0, 3350 high = this.numRows() - 1; 3351 3352 while (low <= high) { 3353 var idx = (high + low) >> 1; 3354 var x = this.getValue(idx, 0); 3355 if (x < xVal) { 3356 low = idx + 1; 3357 } else if (x > xVal) { 3358 high = idx - 1; 3359 } else if (low != idx) { // equal, but there may be an earlier match. 3360 high = idx; 3361 } else { 3362 return idx; 3363 } 3364 } 3365 3366 return null; 3367 }; 3368 3369 /** 3370 * Trigger a callback when the dygraph has drawn itself and is ready to be 3371 * manipulated. This is primarily useful when dygraphs has to do an XHR for the 3372 * data (i.e. a URL is passed as the data source) and the chart is drawn 3373 * asynchronously. If the chart has already drawn, the callback will fire 3374 * immediately. 3375 * 3376 * This is a good place to call setAnnotation(). 3377 * 3378 * @param {function(!Dygraph)} callback The callback to trigger when the chart 3379 * is ready. 3380 */ 3381 Dygraph.prototype.ready = function(callback) { 3382 if (this.is_initial_draw_) { 3383 this.readyFns_.push(callback); 3384 } else { 3385 callback.call(this, this); 3386 } 3387 }; 3388 3389 /** 3390 * Add an event handler. This event handler is kept until the graph is 3391 * destroyed with a call to graph.destroy(). 3392 * 3393 * @param {!Node} elem The element to add the event to. 3394 * @param {string} type The type of the event, e.g. 'click' or 'mousemove'. 3395 * @param {function(Event):(boolean|undefined)} fn The function to call 3396 * on the event. The function takes one parameter: the event object. 3397 * @private 3398 */ 3399 Dygraph.prototype.addAndTrackEvent = function(elem, type, fn) { 3400 utils.addEvent(elem, type, fn); 3401 this.registeredEvents_.push({elem, type, fn}); 3402 }; 3403 3404 Dygraph.prototype.removeTrackedEvents_ = function() { 3405 if (this.registeredEvents_) { 3406 for (var idx = 0; idx < this.registeredEvents_.length; idx++) { 3407 var reg = this.registeredEvents_[idx]; 3408 utils.removeEvent(reg.elem, reg.type, reg.fn); 3409 } 3410 } 3411 3412 this.registeredEvents_ = []; 3413 }; 3414 3415 3416 // Installed plugins, in order of precedence (most-general to most-specific). 3417 Dygraph.PLUGINS = [ 3418 LegendPlugin, 3419 AxesPlugin, 3420 RangeSelectorPlugin, // Has to be before ChartLabels so that its callbacks are called after ChartLabels' callbacks. 3421 ChartLabelsPlugin, 3422 AnnotationsPlugin, 3423 GridPlugin 3424 ]; 3425 3426 // There are many symbols which have historically been available through the 3427 // Dygraph class. These are exported here for backwards compatibility. 3428 Dygraph.GVizChart = GVizChart; 3429 Dygraph.DASHED_LINE = utils.DASHED_LINE; 3430 Dygraph.DOT_DASH_LINE = utils.DOT_DASH_LINE; 3431 Dygraph.dateAxisLabelFormatter = utils.dateAxisLabelFormatter; 3432 Dygraph.toRGB_ = utils.toRGB_; 3433 Dygraph.findPos = utils.findPos; 3434 Dygraph.pageX = utils.pageX; 3435 Dygraph.pageY = utils.pageY; 3436 Dygraph.dateString_ = utils.dateString_; 3437 Dygraph.defaultInteractionModel = DygraphInteraction.defaultModel; 3438 Dygraph.nonInteractiveModel = Dygraph.nonInteractiveModel_ = DygraphInteraction.nonInteractiveModel_; 3439 Dygraph.Circles = utils.Circles; 3440 3441 Dygraph.Plugins = { 3442 Legend: LegendPlugin, 3443 Axes: AxesPlugin, 3444 Annotations: AnnotationsPlugin, 3445 ChartLabels: ChartLabelsPlugin, 3446 Grid: GridPlugin, 3447 RangeSelector: RangeSelectorPlugin 3448 }; 3449 3450 Dygraph.DataHandlers = { 3451 DefaultHandler, 3452 BarsHandler, 3453 CustomBarsHandler, 3454 DefaultFractionHandler, 3455 ErrorBarsHandler, 3456 FractionsBarsHandler 3457 }; 3458 3459 Dygraph.startPan = DygraphInteraction.startPan; 3460 Dygraph.startZoom = DygraphInteraction.startZoom; 3461 Dygraph.movePan = DygraphInteraction.movePan; 3462 Dygraph.moveZoom = DygraphInteraction.moveZoom; 3463 Dygraph.endPan = DygraphInteraction.endPan; 3464 Dygraph.endZoom = DygraphInteraction.endZoom; 3465 3466 Dygraph.numericLinearTicks = DygraphTickers.numericLinearTicks; 3467 Dygraph.numericTicks = DygraphTickers.numericTicks; 3468 Dygraph.dateTicker = DygraphTickers.dateTicker; 3469 Dygraph.Granularity = DygraphTickers.Granularity; 3470 Dygraph.getDateAxis = DygraphTickers.getDateAxis; 3471 Dygraph.floatFormat = utils.floatFormat; 3472 3473 export default Dygraph; 3474