1 /** 2 * Returns a default quantitative, linear, scale for the specified domain. The 3 * arguments to this constructor are optional, and equivalent to calling 4 * {@link #domain}. The default domain and range are [0,1]. 5 * 6 * <p>This constructor is typically not used directly; see one of the 7 * quantitative scale implementations instead. 8 * 9 * @class Represents an abstract quantitative scale; a function that performs a 10 * numeric transformation. This class is typically not used directly; see one of 11 * the quantitative scale implementations (linear, log, root, etc.) 12 * instead. <style type="text/css">sub{line-height:0}</style> A quantitative 13 * scale represents a 1-dimensional transformation from a numeric domain of 14 * input data [<i>d<sub>0</sub></i>, <i>d<sub>1</sub></i>] to a numeric range of 15 * pixels [<i>r<sub>0</sub></i>, <i>r<sub>1</sub></i>]. In addition to 16 * readability, scales offer several useful features: 17 * 18 * <p>1. The range can be expressed in colors, rather than pixels. For example: 19 * 20 * <pre> .fillStyle(pv.Scale.linear(0, 100).range("red", "green"))</pre> 21 * 22 * will fill the marks "red" on an input value of 0, "green" on an input value 23 * of 100, and some color in-between for intermediate values. 24 * 25 * <p>2. The domain and range can be subdivided for a non-uniform 26 * transformation. For example, you may want a diverging color scale that is 27 * increasingly red for negative values, and increasingly green for positive 28 * values: 29 * 30 * <pre> .fillStyle(pv.Scale.linear(-1, 0, 1).range("red", "white", "green"))</pre> 31 * 32 * The domain can be specified as a series of <i>n</i> monotonically-increasing 33 * values; the range must also be specified as <i>n</i> values, resulting in 34 * <i>n - 1</i> contiguous linear scales. 35 * 36 * <p>3. Quantitative scales can be inverted for interaction. The 37 * {@link #invert} method takes a value in the output range, and returns the 38 * corresponding value in the input domain. This is frequently used to convert 39 * the mouse location (see {@link pv.Mark#mouse}) to a value in the input 40 * domain. Note that inversion is only supported for numeric ranges, and not 41 * colors. 42 * 43 * <p>4. A scale can be queried for reasonable "tick" values. The {@link #ticks} 44 * method provides a convenient way to get a series of evenly-spaced rounded 45 * values in the input domain. Frequently these are used in conjunction with 46 * {@link pv.Rule} to display tick marks or grid lines. 47 * 48 * <p>5. A scale can be "niced" to extend the domain to suitable rounded 49 * numbers. If the minimum and maximum of the domain are messy because they are 50 * derived from data, you can use {@link #nice} to round these values down and 51 * up to even numbers. 52 * 53 * @param {number...} domain... optional domain values. 54 * @see pv.Scale.linear 55 * @see pv.Scale.log 56 * @see pv.Scale.root 57 * @extends pv.Scale 58 */ 59 pv.Scale.quantitative = function() { 60 var d = [0, 1], // default domain 61 l = [0, 1], // default transformed domain 62 r = [0, 1], // default range 63 i = [pv.identity], // default interpolators 64 type = Number, // default type 65 n = false, // whether the domain is negative 66 f = pv.identity, // default forward transform 67 g = pv.identity, // default inverse transform 68 tickFormat = String; // default tick formatting function 69 70 /** @private */ 71 function newDate(x) { 72 return new Date(x); 73 } 74 75 /** @private */ 76 function scale(x) { 77 var j = pv.search(d, x); 78 if (j < 0) j = -j - 2; 79 j = Math.max(0, Math.min(i.length - 1, j)); 80 return i[j]((f(x) - l[j]) / (l[j + 1] - l[j])); 81 } 82 83 /** @private */ 84 scale.transform = function(forward, inverse) { 85 /** @ignore */ f = function(x) { return n ? -forward(-x) : forward(x); }; 86 /** @ignore */ g = function(y) { return n ? -inverse(-y) : inverse(y); }; 87 l = d.map(f); 88 return this; 89 }; 90 91 /** 92 * Sets or gets the input domain. This method can be invoked several ways: 93 * 94 * <p>1. <tt>domain(min, ..., max)</tt> 95 * 96 * <p>Specifying the domain as a series of numbers is the most explicit and 97 * recommended approach. Most commonly, two numbers are specified: the minimum 98 * and maximum value. However, for a diverging scale, or other subdivided 99 * non-uniform scales, multiple values can be specified. Values can be derived 100 * from data using {@link pv.min} and {@link pv.max}. For example: 101 * 102 * <pre> .domain(0, pv.max(array))</pre> 103 * 104 * An alternative method for deriving minimum and maximum values from data 105 * follows. 106 * 107 * <p>2. <tt>domain(array, minf, maxf)</tt> 108 * 109 * <p>When both the minimum and maximum value are derived from data, the 110 * arguments to the <tt>domain</tt> method can be specified as the array of 111 * data, followed by zero, one or two accessor functions. For example, if the 112 * array of data is just an array of numbers: 113 * 114 * <pre> .domain(array)</pre> 115 * 116 * On the other hand, if the array elements are objects representing stock 117 * values per day, and the domain should consider the stock's daily low and 118 * daily high: 119 * 120 * <pre> .domain(array, function(d) d.low, function(d) d.high)</pre> 121 * 122 * The first method of setting the domain is preferred because it is more 123 * explicit; setting the domain using this second method should be used only 124 * if brevity is required. 125 * 126 * <p>3. <tt>domain()</tt> 127 * 128 * <p>Invoking the <tt>domain</tt> method with no arguments returns the 129 * current domain as an array of numbers. 130 * 131 * @function 132 * @name pv.Scale.quantitative.prototype.domain 133 * @param {number...} domain... domain values. 134 * @returns {pv.Scale.quantitative} <tt>this</tt>, or the current domain. 135 */ 136 scale.domain = function(array, min, max) { 137 if (arguments.length) { 138 var o; // the object we use to infer the domain type 139 if (array instanceof Array) { 140 if (arguments.length < 2) min = pv.identity; 141 if (arguments.length < 3) max = min; 142 o = array.length && min(array[0]); 143 d = array.length ? [pv.min(array, min), pv.max(array, max)] : []; 144 } else { 145 o = array; 146 d = Array.prototype.slice.call(arguments).map(Number); 147 } 148 if (!d.length) d = [-Infinity, Infinity]; 149 else if (d.length == 1) d = [d[0], d[0]]; 150 n = (d[0] || d[d.length - 1]) < 0; 151 l = d.map(f); 152 type = (o instanceof Date) ? newDate : Number; 153 return this; 154 } 155 return d.map(type); 156 }; 157 158 /** 159 * Sets or gets the output range. This method can be invoked several ways: 160 * 161 * <p>1. <tt>range(min, ..., max)</tt> 162 * 163 * <p>The range may be specified as a series of numbers or colors. Most 164 * commonly, two numbers are specified: the minimum and maximum pixel values. 165 * For a color scale, values may be specified as {@link pv.Color}s or 166 * equivalent strings. For a diverging scale, or other subdivided non-uniform 167 * scales, multiple values can be specified. For example: 168 * 169 * <pre> .range("red", "white", "green")</pre> 170 * 171 * <p>Currently, only numbers and colors are supported as range values. The 172 * number of range values must exactly match the number of domain values, or 173 * the behavior of the scale is undefined. 174 * 175 * <p>2. <tt>range()</tt> 176 * 177 * <p>Invoking the <tt>range</tt> method with no arguments returns the current 178 * range as an array of numbers or colors. 179 * 180 * @function 181 * @name pv.Scale.quantitative.prototype.range 182 * @param {...} range... range values. 183 * @returns {pv.Scale.quantitative} <tt>this</tt>, or the current range. 184 */ 185 scale.range = function() { 186 if (arguments.length) { 187 r = Array.prototype.slice.call(arguments); 188 if (!r.length) r = [-Infinity, Infinity]; 189 else if (r.length == 1) r = [r[0], r[0]]; 190 i = []; 191 for (var j = 0; j < r.length - 1; j++) { 192 i.push(pv.Scale.interpolator(r[j], r[j + 1])); 193 } 194 return this; 195 } 196 return r; 197 }; 198 199 /** 200 * Inverts the specified value in the output range, returning the 201 * corresponding value in the input domain. This is frequently used to convert 202 * the mouse location (see {@link pv.Mark#mouse}) to a value in the input 203 * domain. Inversion is only supported for numeric ranges, and not colors. 204 * 205 * <p>Note that this method does not do any rounding or bounds checking. If 206 * the input domain is discrete (e.g., an array index), the returned value 207 * should be rounded. If the specified <tt>y</tt> value is outside the range, 208 * the returned value may be equivalently outside the input domain. 209 * 210 * @function 211 * @name pv.Scale.quantitative.prototype.invert 212 * @param {number} y a value in the output range (a pixel location). 213 * @returns {number} a value in the input domain. 214 */ 215 scale.invert = function(y) { 216 var j = pv.search(r, y); 217 if (j < 0) j = -j - 2; 218 j = Math.max(0, Math.min(i.length - 1, j)); 219 return type(g(l[j] + (y - r[j]) / (r[j + 1] - r[j]) * (l[j + 1] - l[j]))); 220 }; 221 222 /** 223 * Returns an array of evenly-spaced, suitably-rounded values in the input 224 * domain. This method attempts to return between 5 and 10 tick values. These 225 * values are frequently used in conjunction with {@link pv.Rule} to display 226 * tick marks or grid lines. 227 * 228 * @function 229 * @name pv.Scale.quantitative.prototype.ticks 230 * @param {number} [m] optional number of desired ticks. 231 * @returns {number[]} an array input domain values to use as ticks. 232 */ 233 scale.ticks = function(m) { 234 var start = d[0], 235 end = d[d.length - 1], 236 reverse = end < start, 237 min = reverse ? end : start, 238 max = reverse ? start : end, 239 span = max - min; 240 241 /* Special case: empty, invalid or infinite span. */ 242 if (!span || !isFinite(span)) { 243 if (type == newDate) tickFormat = pv.Format.date("%x"); 244 return [type(min)]; 245 } 246 247 /* Special case: dates. */ 248 if (type == newDate) { 249 /* Floor the date d given the precision p. */ 250 function floor(d, p) { 251 switch (p) { 252 case 31536e6: d.setMonth(0); 253 case 2592e6: d.setDate(1); 254 case 6048e5: if (p == 6048e5) d.setDate(d.getDate() - d.getDay()); 255 case 864e5: d.setHours(0); 256 case 36e5: d.setMinutes(0); 257 case 6e4: d.setSeconds(0); 258 case 1e3: d.setMilliseconds(0); 259 } 260 } 261 262 var precision, format, increment, step = 1; 263 if (span >= 3 * 31536e6) { 264 precision = 31536e6; 265 format = "%Y"; 266 /** @ignore */ increment = function(d) { d.setFullYear(d.getFullYear() + step); }; 267 } else if (span >= 3 * 2592e6) { 268 precision = 2592e6; 269 format = "%m/%Y"; 270 /** @ignore */ increment = function(d) { d.setMonth(d.getMonth() + step); }; 271 } else if (span >= 3 * 6048e5) { 272 precision = 6048e5; 273 format = "%m/%d"; 274 /** @ignore */ increment = function(d) { d.setDate(d.getDate() + 7 * step); }; 275 } else if (span >= 3 * 864e5) { 276 precision = 864e5; 277 format = "%m/%d"; 278 /** @ignore */ increment = function(d) { d.setDate(d.getDate() + step); }; 279 } else if (span >= 3 * 36e5) { 280 precision = 36e5; 281 format = "%I:%M %p"; 282 /** @ignore */ increment = function(d) { d.setHours(d.getHours() + step); }; 283 } else if (span >= 3 * 6e4) { 284 precision = 6e4; 285 format = "%I:%M %p"; 286 /** @ignore */ increment = function(d) { d.setMinutes(d.getMinutes() + step); }; 287 } else if (span >= 3 * 1e3) { 288 precision = 1e3; 289 format = "%I:%M:%S"; 290 /** @ignore */ increment = function(d) { d.setSeconds(d.getSeconds() + step); }; 291 } else { 292 precision = 1; 293 format = "%S.%Qs"; 294 /** @ignore */ increment = function(d) { d.setTime(d.getTime() + step); }; 295 } 296 tickFormat = pv.Format.date(format); 297 298 var date = new Date(min), dates = []; 299 floor(date, precision); 300 301 /* If we'd generate too many ticks, skip some!. */ 302 var n = span / precision; 303 if (n > 10) { 304 switch (precision) { 305 case 36e5: { 306 step = (n > 20) ? 6 : 3; 307 date.setHours(Math.floor(date.getHours() / step) * step); 308 break; 309 } 310 case 2592e6: { 311 step = 3; // seasons 312 date.setMonth(Math.floor(date.getMonth() / step) * step); 313 break; 314 } 315 case 6e4: { 316 step = (n > 30) ? 15 : ((n > 15) ? 10 : 5); 317 date.setMinutes(Math.floor(date.getMinutes() / step) * step); 318 break; 319 } 320 case 1e3: { 321 step = (n > 90) ? 15 : ((n > 60) ? 10 : 5); 322 date.setSeconds(Math.floor(date.getSeconds() / step) * step); 323 break; 324 } 325 case 1: { 326 step = (n > 1000) ? 250 : ((n > 200) ? 100 : ((n > 100) ? 50 : ((n > 50) ? 25 : 5))); 327 date.setMilliseconds(Math.floor(date.getMilliseconds() / step) * step); 328 break; 329 } 330 default: { 331 step = pv.logCeil(n / 15, 10); 332 if (n / step < 2) step /= 5; 333 else if (n / step < 5) step /= 2; 334 date.setFullYear(Math.floor(date.getFullYear() / step) * step); 335 break; 336 } 337 } 338 } 339 340 while (true) { 341 increment(date); 342 if (date > max) break; 343 dates.push(new Date(date)); 344 } 345 return reverse ? dates.reverse() : dates; 346 } 347 348 /* Normal case: numbers. */ 349 if (!arguments.length) m = 10; 350 var step = pv.logFloor(span / m, 10), 351 err = m / (span / step); 352 if (err <= .15) step *= 10; 353 else if (err <= .35) step *= 5; 354 else if (err <= .75) step *= 2; 355 var start = Math.ceil(min / step) * step, 356 end = Math.floor(max / step) * step; 357 tickFormat = pv.Format.number() 358 .fractionDigits(Math.max(0, -Math.floor(pv.log(step, 10) + .01))); 359 var ticks = pv.range(start, end + step, step); 360 return reverse ? ticks.reverse() : ticks; 361 }; 362 363 /** 364 * Formats the specified tick value using the appropriate precision, based on 365 * the step interval between tick marks. If {@link #ticks} has not been called, 366 * the argument is converted to a string, but no formatting is applied. 367 * 368 * @function 369 * @name pv.Scale.quantitative.prototype.tickFormat 370 * @param {number} t a tick value. 371 * @returns {string} a formatted tick value. 372 */ 373 scale.tickFormat = function (t) { return tickFormat(t); }; 374 375 /** 376 * "Nices" this scale, extending the bounds of the input domain to 377 * evenly-rounded values. Nicing is useful if the domain is computed 378 * dynamically from data, and may be irregular. For example, given a domain of 379 * [0.20147987687960267, 0.996679553296417], a call to <tt>nice()</tt> might 380 * extend the domain to [0.2, 1]. 381 * 382 * <p>This method must be invoked each time after setting the domain. 383 * 384 * @function 385 * @name pv.Scale.quantitative.prototype.nice 386 * @returns {pv.Scale.quantitative} <tt>this</tt>. 387 */ 388 scale.nice = function() { 389 if (d.length != 2) return this; // TODO support non-uniform domains 390 var start = d[0], 391 end = d[d.length - 1], 392 reverse = end < start, 393 min = reverse ? end : start, 394 max = reverse ? start : end, 395 span = max - min; 396 397 /* Special case: empty, invalid or infinite span. */ 398 if (!span || !isFinite(span)) return this; 399 400 var step = Math.pow(10, Math.round(Math.log(span) / Math.log(10)) - 1); 401 d = [Math.floor(min / step) * step, Math.ceil(max / step) * step]; 402 if (reverse) d.reverse(); 403 l = d.map(f); 404 return this; 405 }; 406 407 /** 408 * Returns a view of this scale by the specified accessor function <tt>f</tt>. 409 * Given a scale <tt>y</tt>, <tt>y.by(function(d) d.foo)</tt> is equivalent to 410 * <tt>function(d) y(d.foo)</tt>. 411 * 412 * <p>This method is provided for convenience, such that scales can be 413 * succinctly defined inline. For example, given an array of data elements 414 * that have a <tt>score</tt> attribute with the domain [0, 1], the height 415 * property could be specified as: 416 * 417 * <pre> .height(pv.Scale.linear().range(0, 480).by(function(d) d.score))</pre> 418 * 419 * This is equivalent to: 420 * 421 * <pre> .height(function(d) d.score * 480)</pre> 422 * 423 * This method should be used judiciously; it is typically more clear to 424 * invoke the scale directly, passing in the value to be scaled. 425 * 426 * @function 427 * @name pv.Scale.quantitative.prototype.by 428 * @param {function} f an accessor function. 429 * @returns {pv.Scale.quantitative} a view of this scale by the specified 430 * accessor function. 431 */ 432 scale.by = function(f) { 433 function by() { return scale(f.apply(this, arguments)); } 434 for (var method in scale) by[method] = scale[method]; 435 return by; 436 }; 437 438 scale.domain.apply(scale, arguments); 439 return scale; 440 }; 441