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