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, 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 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