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