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