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