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