1 'use strict';
  2 
  3 /**
  4  * @license
  5  * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
  6  * MIT-licenced: https://opensource.org/licenses/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