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