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