      1 'use strict';
      2
      3 /**
      4  * @license
      5  * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
      6  * MIT-licenced: https://opensource.org/license/MIT
      7  */
      8
      9 /**
     10  * @fileoverview DygraphOptions is responsible for parsing and returning
     11  * information about options.
     12  */
     13
     14 // TODO: remove this jshint directive & fix the warnings.
     15 /*jshint sub:true */
     16
     17 import * as utils from './dygraph-utils';
     18 import DEFAULT_ATTRS from './dygraph-default-attrs';
     19 import OPTIONS_REFERENCE from './dygraph-options-reference';
     20
     21 /*
     22  * Interesting member variables: (REMOVING THIS LIST AS I CLOSURIZE)
     23  * global_ - global attributes (common among all graphs, AIUI)
     24  * user - attributes set by the user
     25  * series_ - { seriesName -> { idx, yAxis, options }}
     26  */
     27
     28 /**
     29  * This parses attributes into an object that can be easily queried.
     30  *
     31  * It doesn't necessarily mean that all options are available, specifically
     32  * if labels are not yet available, since those drive details of the per-series
     33  * and per-axis options.
     34  *
     35  * @param {Dygraph} dygraph The chart to which these options belong.
     36  * @constructor
     37  */
     38 var DygraphOptions = function(dygraph) {
     39   /**
     40    * The dygraph.
     41    * @type {!Dygraph}
     42    */
     43   this.dygraph_ = dygraph;
     44
     45   /**
     46    * Array of axis index to { series : [ series names ] , options : { axis-specific options. } }
     47    * @type {Array.<{series : Array.<string>, options : Object}>} @private
     48    */
     49   this.yAxes_ = [];
     50
     51   /**
     52    * Contains x-axis specific options, which are stored in the options key.
     53    * This matches the yAxes_ object structure (by being a dictionary with an
     54    * options element) allowing for shared code.
     55    * @type {options: Object} @private
     56    */
     57   this.xAxis_ = {};
     58   this.series_ = {};
     59
     60   // Once these two objects are initialized, you can call get();
     61   this.global_ = this.dygraph_.attrs_;
     62   this.user_ = this.dygraph_.user_attrs_ || {};
     63
     64   /**
     65    * A list of series in columnar order.
     66    * @type {Array.<string>}
     67    */
     68   this.labels_ = [];
     69
     70   this.highlightSeries_ = this.get("highlightSeriesOpts") || {};
     71   this.reparseSeries();
     72 };
     73
     74 /**
     75  * Not optimal, but does the trick when you're only using two axes.
     76  * If we move to more axes, this can just become a function.
     77  *
     78  * @type {Object.<number>}
     79  * @private
     80  */
     81 DygraphOptions.AXIS_STRING_MAPPINGS_ = {
     82   'y' : 0,
     83   'Y' : 0,
     84   'y1' : 0,
     85   'Y1' : 0,
     86   'y2' : 1,
     87   'Y2' : 1
     88 };
     89
     90 /**
     91  * @param {string|number} axis
     92  * @private
     93  */
     94 DygraphOptions.axisToIndex_ = function(axis) {
     95   if (typeof(axis) == "string") {
     96     if (DygraphOptions.AXIS_STRING_MAPPINGS_.hasOwnProperty(axis)) {
     97       return DygraphOptions.AXIS_STRING_MAPPINGS_[axis];
     98     }
     99     throw "Unknown axis : " + axis;
    100   }
    101   if (typeof(axis) == "number") {
    102     if (axis === 0 || axis === 1) {
    103       return axis;
    104     }
    105     throw "Dygraphs only supports two y-axes, indexed from 0-1.";
    106   }
    107   if (axis) {
    108     throw "Unknown axis : " + axis;
    109   }
    110   // No axis specification means axis 0.
    111   return 0;
    112 };
    113
    114 /**
    115  * Reparses options that are all related to series. This typically occurs when
    116  * options are either updated, or source data has been made available.
    117  *
    118  * TODO(konigsberg): The method name is kind of weak; fix.
    119  */
    120 DygraphOptions.prototype.reparseSeries = function() {
    121   var labels = this.get("labels");
    122   if (!labels) {
    123     return; // -- can't do more for now, will parse after getting the labels.
    124   }
    125
    126   this.labels_ = labels.slice(1);
    127
    128   this.yAxes_ = [ { series : [], options : {}} ]; // Always one axis at least.
    129   this.xAxis_ = { options : {} };
    130   this.series_ = {};
    131
    132   // Series are specified in the series element:
    133   //
    134   // {
    135   //   labels: [ "X", "foo", "bar" ],
    136   //   pointSize: 3,
    137   //   series : {
    138   //     foo : {}, // options for foo
    139   //     bar : {} // options for bar
    140   //   }
    141   // }
    142   //
    143   // So, if series is found, it's expected to contain per-series data,
    144   // otherwise set a default.
    145   var seriesDict = this.user_.series || {};
    146   for (var idx = 0; idx < this.labels_.length; idx++) {
    147     var seriesName = this.labels_[idx];
    148     var optionsForSeries = seriesDict[seriesName] || {};
    149     var yAxis = DygraphOptions.axisToIndex_(optionsForSeries["axis"]);
    150
    151     this.series_[seriesName] = {
    152       idx: idx,
    153       yAxis: yAxis,
    154       options : optionsForSeries };
    155
    156     if (!this.yAxes_[yAxis]) {
    157       this.yAxes_[yAxis] =  { series : [ seriesName ], options : {} };
    158     } else {
    159       this.yAxes_[yAxis].series.push(seriesName);
    160     }
    161   }
    162
    163   var axis_opts = this.user_["axes"] || {};
    164   utils.update(this.yAxes_[0].options, axis_opts["y"] || {});
    165   if (this.yAxes_.length > 1) {
    166     utils.update(this.yAxes_[1].options, axis_opts["y2"] || {});
    167   }
    168   utils.update(this.xAxis_.options, axis_opts["x"] || {});
    169
    170   if (typeof process !== 'undefined' && process.env.NODE_ENV != 'production') {
    171   // For "production" code, this gets removed by uglifyjs.
    172     this.validateOptions_();
    173   }
    174 };
    175
    176 /**
    177  * Get a global value.
    178  *
    179  * @param {string} name the name of the option.
    180  */
    181 DygraphOptions.prototype.get = function(name) {
    182   var result = this.getGlobalUser_(name);
    183   if (result !== null) {
    184     return result;
    185   }
    186   return this.getGlobalDefault_(name);
    187 };
    188
    189 DygraphOptions.prototype.getGlobalUser_ = function(name) {
    190   if (this.user_.hasOwnProperty(name)) {
    191     return this.user_[name];
    192   }
    193   return null;
    194 };
    195
    196 DygraphOptions.prototype.getGlobalDefault_ = function(name) {
    197   if (this.global_.hasOwnProperty(name)) {
    198     return this.global_[name];
    199   }
    200   if (DEFAULT_ATTRS.hasOwnProperty(name)) {
    201     return DEFAULT_ATTRS[name];
    202   }
    203   return null;
    204 };
    205
    206 /**
    207  * Get a value for a specific axis. If there is no specific value for the axis,
    208  * the global value is returned.
    209  *
    210  * @param {string} name the name of the option.
    211  * @param {string|number} axis the axis to search. Can be the string representation
    212  * ("y", "y2") or the axis number (0, 1).
    213  */
    214 DygraphOptions.prototype.getForAxis = function(name, axis) {
    215   var axisIdx;
    216   var axisString;
    217
    218   // Since axis can be a number or a string, straighten everything out here.
    219   if (typeof(axis) == 'number') {
    220     axisIdx = axis;
    221     axisString = axisIdx === 0 ? "y" : "y2";
    222   } else {
    223     if (axis == "y1") { axis = "y"; } // Standardize on 'y'. Is this bad? I think so.
    224     if (axis == "y") {
    225       axisIdx = 0;
    226     } else if (axis == "y2") {
    227       axisIdx = 1;
    228     } else if (axis == "x") {
    229       axisIdx = -1; // simply a placeholder for below.
    230     } else {
    231       throw "Unknown axis " + axis;
    232     }
    233     axisString = axis;
    234   }
    235
    236   var userAxis = (axisIdx == -1) ? this.xAxis_ : this.yAxes_[axisIdx];
    237
    238   // Search the user-specified axis option first.
    239   if (userAxis) { // This condition could be removed if we always set up this.yAxes_ for y2.
    240     var axisOptions = userAxis.options;
    241     if (axisOptions.hasOwnProperty(name)) {
    242       return axisOptions[name];
    243     }
    244   }
    245
    246   // User-specified global options second.
    247   // But, hack, ignore globally-specified 'logscale' for 'x' axis declaration.
    248   if (!(axis === 'x' && name === 'logscale')) {
    249     var result = this.getGlobalUser_(name);
    250     if (result !== null) {
    251       return result;
    252     }
    253   }
    254   // Default axis options third.
    255   var defaultAxisOptions = DEFAULT_ATTRS.axes[axisString];
    256   if (defaultAxisOptions.hasOwnProperty(name)) {
    257     return defaultAxisOptions[name];
    258   }
    259
    260   // Default global options last.
    261   return this.getGlobalDefault_(name);
    262 };
    263
    264 /**
    265  * Get a value for a specific series. If there is no specific value for the series,
    266  * the value for the axis is returned (and afterwards, the global value.)
    267  *
    268  * @param {string} name the name of the option.
    269  * @param {string} series the series to search.
    270  */
    271 DygraphOptions.prototype.getForSeries = function(name, series) {
    272   // Honors indexes as series.
    273   if (series === this.dygraph_.getHighlightSeries()) {
    274     if (this.highlightSeries_.hasOwnProperty(name)) {
    275       return this.highlightSeries_[name];
    276     }
    277   }
    278
    279   if (!this.series_.hasOwnProperty(series)) {
    280     throw "Unknown series: " + series;
    281   }
    282
    283   var seriesObj = this.series_[series];
    284   var seriesOptions = seriesObj["options"];
    285   if (seriesOptions.hasOwnProperty(name)) {
    286     return seriesOptions[name];
    287   }
    288
    289   return this.getForAxis(name, seriesObj["yAxis"]);
    290 };
    291
    292 /**
    293  * Returns the number of y-axes on the chart.
    294  * @return {number} the number of axes.
    295  */
    296 DygraphOptions.prototype.numAxes = function() {
    297   return this.yAxes_.length;
    298 };
    299
    300 /**
    301  * Return the y-axis for a given series, specified by name.
    302  */
    303 DygraphOptions.prototype.axisForSeries = function(series) {
    304   return this.series_[series].yAxis;
    305 };
    306
    307 /**
    308  * Returns the options for the specified axis.
    309  */
    310 // TODO(konigsberg): this is y-axis specific. Support the x axis.
    311 DygraphOptions.prototype.axisOptions = function(yAxis) {
    312   return this.yAxes_[yAxis].options;
    313 };
    314
    315 /**
    316  * Return the series associated with an axis.
    317  */
    318 DygraphOptions.prototype.seriesForAxis = function(yAxis) {
    319   return this.yAxes_[yAxis].series;
    320 };
    321
    322 /**
    323  * Return the list of all series, in their columnar order.
    324  */
    325 DygraphOptions.prototype.seriesNames = function() {
    326   return this.labels_;
    327 };
    328
    329 if (typeof process !== 'undefined' && process.env.NODE_ENV != 'production') {
    330 // For "production" code, this gets removed by uglifyjs.
    331
    332 /**
    333  * Validate all options.
    334  * This requires OPTIONS_REFERENCE, which is only available in debug builds.
    335  * @private
    336  */
    337 DygraphOptions.prototype.validateOptions_ = function() {
    338   if (typeof OPTIONS_REFERENCE === 'undefined') {
    339     throw 'Called validateOptions_ in prod build.';
    340   }
    341
    342   var that = this;
    343   var validateOption = function(optionName) {
    344     if (!OPTIONS_REFERENCE[optionName]) {
    345       that.warnInvalidOption_(optionName);
    346     }
    347   };
    348
    349   var optionsDicts = [this.xAxis_.options,
    350                       this.yAxes_[0].options,
    351                       this.yAxes_[1] && this.yAxes_[1].options,
    352                       this.global_,
    353                       this.user_,
    354                       this.highlightSeries_];
    355   var names = this.seriesNames();
    356   for (var i = 0; i < names.length; i++) {
    357     var name = names[i];
    358     if (this.series_.hasOwnProperty(name)) {
    359       optionsDicts.push(this.series_[name].options);
    360     }
    361   }
    362   for (var i = 0; i < optionsDicts.length; i++) {
    363     var dict = optionsDicts[i];
    364     if (!dict) continue;
    365     for (var optionName in dict) {
    366       if (dict.hasOwnProperty(optionName)) {
    367         validateOption(optionName);
    368       }
    369     }
    370   }
    371 };
    372
    373 var WARNINGS = {};  // Only show any particular warning once.
    374
    375 /**
    376  * Logs a warning about invalid options.
    377  * TODO: make this throw for testing
    378  * @private
    379  */
    380 DygraphOptions.prototype.warnInvalidOption_ = function(optionName) {
    381   if (!WARNINGS[optionName]) {
    382     WARNINGS[optionName] = true;
    383     var isSeries = (this.labels_.indexOf(optionName) >= 0);
    384     if (isSeries) {
    385       console.warn('Use new-style per-series options (saw ' + optionName + ' as top-level options key). See http://blog.dygraphs.com/2012/12/the-new-and-better-way-to-specify.html (The New and Better Way to Specify Series and Axis Options).');
    386     } else {
    387       console.warn('Unknown option ' + optionName + ' (see https://dygraphs.com/options.html for the full list of options)');
    388     }
    389     throw "invalid option " + optionName;
    390   }
    391 };
    392
    393 // Reset list of previously-shown warnings. Used for testing.
    394 DygraphOptions.resetWarnings_ = function() {
    395   WARNINGS = {};
    396 };
    397
    398 }
    399
    400 export default DygraphOptions;
    401