Make old-style per-series options throw (#783)
[dygraphs.git] / src / dygraph-options.js
1 /**
2 * @license
3 * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://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, otherwise set a
143 // 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 // For "production" code, this gets removed by uglifyjs.
170 if (process.env.NODE_ENV != 'production') {
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 // For "production" code, this gets removed by uglifyjs.
329 if (process.env.NODE_ENV != 'production') {
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://bit.ly/1tceaJs');
385 } else {
386 console.warn('Unknown option ' + optionName + ' (full list of options at dygraphs.com/options.html');
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;