1 /**
  2  * Source: Range.js
  3  * Copyright (c) 2013-2014 Oculus Info Inc.
  4  * @fileOverview The Range implementation
  5  */
  6 
  7 /**
  8  * @namespace
  9  * @ignore
 10  * Ensure namespace exists
 11  */
 12 aperture = (
 13 /** @private */
 14 function(namespace) {
 15 
 16 	// util is always defined by this point
 17 	var util = aperture.util;
 18 
 19 	// we use this to check for VALID numbers (NaN not allowed)
 20 	function isValidNumber( value ) {
 21 		return !isNaN( value );
 22 	}
 23 
 24 	namespace.Range = aperture.Class.extend( 'aperture.Range',
 25 
 26 		/** @lends aperture.Range.prototype */
 27 		{
 28 			/**
 29 			 * @class Represents an abstract model property range. Range is
 30 			 * implemented for both scalar and ordinal properties by
 31 			 * {@link aperture.Scalar Scalar} and
 32 			 * {@link aperture.Ordinal Ordinal}.
 33 			 * <p>
 34 			 *
 35 			 * @constructs
 36 			 * @description
 37 			 * This constructor is abstract and may not be called.
 38 			 * @extends aperture.Class
 39 			 *
 40 			 * @param {String} name
 41 			 *      the label of the property described.
 42 			 *
 43 			 * @returns {this}
 44 			 *      a new Range
 45 			 */
 46 			init : function( name ) {
 47 
 48 				// views may allow the user to override label.
 49 				this.label = name;
 50 
 51 				// default formatting.
 52 				this.formatter_ = new namespace.Format();
 53 
 54 			},
 55 
 56 			/**
 57 			 * Gets or sets the value of the name for this property.
 58 			 * If this is a view, the base range will be left untouched.
 59 			 * To delete a view's name and once again fallback to the base
 60 			 * label set the view's name value to null.
 61 			 *
 62 			 * @param {String} [text]
 63 			 *      the value, if setting it rather than getting it
 64 			 *
 65 			 * @returns {String|this}
 66 			 *      the label of this property if a get, or this if a set.
 67 			 */
 68 			name : function( text ) {
 69 
 70 				// get
 71 				if ( text === undefined ) {
 72 					return this.label;
 73 				}
 74 				// set
 75 				if ( text === null ) {
 76 					delete this.label;
 77 				} else {
 78 					this.label = text;
 79 				}
 80 
 81 				return this;
 82 			},
 83 
 84 			/**
 85 			 * Expands the property range to encompass the value, if necessary.
 86 			 * This method is abstract and implemented by specific types of ranges.
 87 			 *
 88 			 * @param {Array|Number|String} value
 89 			 *      a case or set of cases to include in the property range.
 90 			 *
 91 			 * @returns {this}
 92 			 *      a reference to this property.
 93 			 *
 94 			 * @name aperture.Range.prototype.expand
 95 			 * @function
 96 			 */
 97 
 98 			/**
 99 			 * Clears the property range, then optionally expands
100 			 * it with new values.
101 			 * This method is abstract and implemented by specific types of ranges.
102 			 *
103 			 * @param {Array|Number} [values]
104 			 *
105 			 * @returns {this}
106 			 *      a reference to this property.
107 			 *
108 			 * @name aperture.Range.prototype.reset
109 			 * @function
110 			 */
111 
112 			/**
113 			 * Returns a new banded scalar view of this range. Banded views are used for
114 			 * axis articulation, and for scalars can also be used for quantizing values
115 			 * to labeled ordinal bands of values.
116 			 * Banded views are live, meaning subsequent range changes are allowed through either
117 			 * the view or its source.
118 			 * This method is abstract and implemented by specific types of ranges.
119 			 *
120 			 * @returns {aperture.Range}
121 			 *      a new scalar view of this Range.
122 			 *
123 			 * @name aperture.Range.prototype.banded
124 			 * @function
125 			 */
126 
127 			/**
128 			 * See the mappedTo function.
129 			 *
130 			 * @deprecated
131 			 */
132 			mapKey : function ( to ) {
133 				return this.mappedTo( to );
134 			},
135 
136 			/**
137 			 * Creates a key for mapping from this model range to a visual property
138 			 * range. This method is abstract and implemented by specific types
139 			 * of ranges.
140 			 *
141 			 * @param {Array} to
142 			 *      the ordered set of colors or numbers to map to, from this property's
143 			 *      range.
144 			 *
145 			 * @returns {aperture.MapKey}
146 			 *      a new map key.
147 			 *
148 			 * @name aperture.Range.prototype.mappedTo
149 			 * @function
150 			 */
151 
152 			/**
153 			 * Returns the value's position within the Range object.
154 			 * Ranges implement this function to map data values into the range.
155 			 * This method is abstract and implemented by specific types of ranges.
156 			 * @param value
157 			 *      the value to map within the Range
158 			 *
159 			 * @returns the mapped value.
160 			 *
161 			 * @name aperture.Range.prototype.map
162 			 * @function
163 			 */
164 
165 			/**
166 			 * Retrieves the contents of this range as an array.  The content of the array
167 			 * depends on the type of range (e.g. Scalar, Ordinal, etc). Ordinals return
168 			 * the sum set of cases in the order added, whereas Scalars return a two element
169 			 * array of min, max or undefined if the range is yet unset.
170 			 * This method is abstract and implemented by specific types of ranges.
171 			 *
172 			 * @returns {Array} array with the contents of the range
173 			 */
174 			get : function() {
175 
176 				if (!this.range.values || !this.range.values.length) {
177 					return this.range.values;
178 				}
179 				// if range has been revised but not view, refresh view now.
180 				if (this.revision !== this.range.revision) {
181 					this.revision = this.range.revision;
182 					this.view = this.doView();
183 				}
184 
185 				return this.view;
186 			},
187 
188 			/**
189 			 * Returns the start of the range. For scalars this will be the minimum of the extents, and
190 			 * for ordinals it will be the first case. To reset the start and end extents use the reset function.
191 			 */
192 			start : function() {
193 				return this.get()[0];
194 			},
195 
196 			/**
197 			 * Returns the end of the range. For scalars this will be the maximum of the extents, and
198 			 * for ordinals it will be the last case. To reset the start and end extents use the reset function.
199 			 */
200 			end : function() {
201 				var e = this.get();
202 
203 				return e && e[e.length-1];
204 			},
205 
206 			/**
207 			 * Formats a value as a String using the current formatter.
208 			 *
209 			 * @param value
210 			 *      the value to format into a string
211 			 *
212 			 * @returns {String}
213 			 *      the formatted value.
214 			 */
215 			format : function ( value ) {
216 				return this.formatter_.format( value );
217 			},
218 
219 			/**
220 			 * Gets or sets the current formatter as a function.
221 			 * The default formatter simply uses the JavaScript String function.
222 			 *
223 			 * @param {aperture.Format} [formatter]
224 			 *      if setting the formatter, a Format object which
225 			 *      will format values.
226 			 *
227 			 * @returns {aperture.Format|this}
228 			 *      if getting the formatter, it will be returned,
229 			 *      otherwise a reference to this,
230 			 *      convenient for chained method calls.
231 			 */
232 			formatter : function ( f ) {
233 				// get
234 				if ( f == null ) {
235 					return this.formatter_;
236 				}
237 				if ( !f.typeOf || !f.typeOf(namespace.Format) ) {
238 					throw new Error('Range formatter must be a Format object');
239 				}
240 
241 				this.formatter_ = f;
242 
243 				return this;
244 			},
245 
246 			/**
247 			 * Returns a displayable string which
248 			 * includes the property label and extent of the property.
249 			 *
250 			 * @returns {String}
251 			 *      a string.
252 			 */
253 			toString : function ( ) {
254 				var range = this.get();
255 
256 				return this.label + range? (' [' + range.toString() + ']') : '';
257 			}
258 		}
259 	);
260 
261 	/**
262 	 * @private
263 	 * Increment revision so views have a quick dirty check option.
264 	 * Used by both scalars and ordinals, on themselves.
265 	 */
266 	var revise = function () {
267 		this.revision++;
268 
269 		if (this.revision === Number.MAX_VALUE) {
270 			this.revision = 0;
271 		}
272 	},
273 
274 	/**
275 	 * @private
276 	 * Throw an error for this case.
277 	 */
278 	noBandedViews = function () {
279 		throw new Error('Cannot create a scalar view of a banded scalar!');
280 	},
281 
282 	/**
283 	 * @private
284 	 * The range factory function for scalars.
285 	 */
286 	range = (
287 
288 		/**
289 		 * @private
290 		 */
291 		function() {
292 
293 			/**
294 			 * @private
295 			 * Modify range
296 			 */
297 			var set = function ( min, max ) {
298 
299 				var rv = this.values;
300 
301 				if( rv ) {
302 					// Have an existing range, expand
303 					if( min < rv[0] ) {
304 						rv[0] = min;
305 						this.revise();
306 					}
307 					if( max > rv[1] ) {
308 						rv[1] = max;
309 						this.revise();
310 					}
311 				} else {
312 					// No range set yet, set with min/max
313 					this.values = [min, max];
314 					this.revise();
315 				}
316 			},
317 
318 			/**
319 			 * @private
320 			 * Clear any existing values.
321 			 */
322 			reset = function() {
323 				this.values = null;
324 				this.revise();
325 			};
326 
327 			/**
328 			 * @private
329 			 * Factory method.
330 			 */
331 			return function () {
332 				return {
333 					values : null,
334 					revision : 0,
335 					revise : revise,
336 					set : set,
337 					reset : reset
338 				};
339 			};
340 	}());
341 
342 
343 	namespace.Scalar = namespace.Range.extend( 'aperture.Scalar',
344 
345 		/** @lends aperture.Scalar.prototype */
346 		{
347 			/**
348 			 * @class Represents a scalar model property range. Unlike
349 			 * in the case of Ordinals, Scalar property map keys
350 			 * use interpolation when mapping values to visual
351 			 * properties. If the desired visual mapping of a raw scalar value
352 			 * is ordinal rather than scalar (for instance a change value
353 			 * where positive is an 'up' color and negative is a 'down' color), call
354 			 * <span class="fixedFont">quantized</span> to derive an ordinal view of
355 			 * the Scalar.
356 			 * <p>
357 			 *
358 			 * @augments aperture.Range
359 			 * @constructs
360 			 * @description
361 			 * Constructs a new scalar range.
362 			 *
363 			 * @param {String} name
364 			 *      the name of the property described.
365 			 * @param {Array|Number|String|Date} [values]
366 			 *      an optional array of values (or a single value) with which to
367 			 *      populate the range. Equivalent to calling {@link #expand} after construction.
368 			 *
369 			 * @returns {this}
370 			 *      a new Scalar
371 			 */
372 			init : function( name, values ) {
373 				namespace.Range.prototype.init.call(this, name);
374 
375 				// create a range object,
376 				// shareable and settable by both base and view
377 				this.range = range();
378 
379 				// starting view revision is 0
380 				// this gets checked later against range revision
381 				this.revision = 0;
382 
383 				// handle initial value expansion
384 				if( values != null ) {
385 					this.expand(values);
386 				}
387 			},
388 
389 			/**
390 			 * Expands the property range to encompass the value, if necessary.
391 			 *
392 			 * @param {Array|Number|String|Date} value
393 			 *      a case or set of cases to include in the property range. Each must
394 			 *      be a Number, a String representation of a number, or a
395 			 *      Date.
396 			 *
397 			 * @returns {this}
398 			 *      a reference to this property.
399 			 */
400 			expand : function ( value ) {
401 				var min, max, rv = this.range.values;
402 
403 				if( util.isArray(value) ) {
404 					// Ensure they're all valid numbers
405 					var numbers = util.filter(util.map(value,Number), isValidNumber);
406 					if (!numbers.length) {
407 						return this;
408 					}
409 					// Find the min/max
410 					min = Math.min.apply(Math,numbers);
411 					max = Math.max.apply(Math,numbers);
412 				} else {
413 					// A single value
414 					min = max = Number(value);
415 					if (isNaN(min)) {
416 						return this;
417 					}
418 				}
419 
420 				this.range.set( min, max );
421 
422 				return this;
423 			},
424 
425 			/**
426 			 * Clears the property range, then optionally expands
427 			 * it with new values.
428 			 *
429 			 * @param {Array|Number|String|Date} [values]
430 			 *
431 			 * @returns {this}
432 			 *      a reference to this property.
433 			 */
434 			reset : function ( values ) {
435 				this.range.reset();
436 
437 				if ( values != null ) {
438 					this.expand ( values );
439 				}
440 
441 				return this;
442 			},
443 
444 			/**
445 			 * Returns the value's normalized position within the Range
446 			 * object.  The return value will be in the range of [0,1].
447 			 *
448 			 * @param {Number} value
449 			 *      the value to normalize by the Range
450 			 *
451 			 * @return {Number} the normalized value of the input in the range [0,1]
452 			 */
453 			map : function( value ) {
454 				// call function in case extended.
455 				var d = this.get();
456 
457 				// if anything is invalid (null or NaN(!==NaN)), return 0 to keep our clamped contract.
458 				if( !d || value == null || (value = Number(value)) !== value) {
459 					return 0;
460 				}
461 
462 				// return limit or interpolate
463 				return value <= d[0]? 0
464 						: value >= d[1]? 1
465 								: (value-d[0]) / (d[1]-d[0]);
466 			},
467 
468 			/**
469 			 * Creates and returns a key for mapping from this model range to a visual property
470 			 * range. Mappings are evaluated dynamically, meaning subsequent
471 			 * range changes are allowed. Multiple map keys may be generated from the
472 			 * same range object.
473 			 *
474 			 * @param {Array} to
475 			 *      the ordered set of colors or numbers to map to, from this property's
476 			 *      range.
477 			 *
478 			 * @returns {aperture.MapKey}
479 			 *      a new map key.
480 			 */
481 			mappedTo : function ( to ) {
482 
483 				// allow for array wrapping or not.
484 				if (arguments.length > 1) {
485 					to = Array.prototype.slice.call(arguments);
486 				}
487 				// diagnose problems early so they don't cascade later
488 				if ( to.length === 0 || (util.isNumber(to[0]) && isNaN(to[0]))) {
489 					aperture.log.error('Cannot map a scalar range to array length zero or NaN values.');
490 					return;
491 				}
492 
493 				if ( !util.isNumber(to[0]) && !to[0].blend ) {
494 					// assume colors are strings
495 					if (util.isString(to[0])) {
496 						to = util.map(to, function(s) {
497 							return new aperture.Color(s);
498 						});
499 					} else {
500 						aperture.log.error('Mappings of Scalar ranges must map to numbers or objects with a blend function.');
501 						return;
502 					}
503 				}
504 
505 				return new namespace.ScalarMapKey( this, to );
506 			},
507 
508 			/**
509 			 * @private
510 			 *
511 			 * Views override this to chain updates together. This will never
512 			 * be called if the range is empty / null. The default implementation
513 			 * returns a single copy of the source range which is subsequently transformed
514 			 * by downstream views, in place.
515 			 */
516 			doView : function() {
517 				return this.range.values.slice();
518 			}
519 		}
520 	);
521 
522 	/**
523 	 * Returns a new scalar view of this range which is symmetric about zero.
524 	 * Views are dynamic, adapting to any subsequent changes
525 	 * in the base range.
526 	 *
527 	 * @returns {aperture.Scalar}
528 	 *      a new view of this Range.
529 	 *
530 	 * @name aperture.Scalar.prototype.symmetric
531 	 * @function
532 	 */
533 	namespace.Scalar.addView( 'symmetric',
534 		{
535 			init : function ( ) {
536 				this.revision = 0;
537 			},
538 
539 			doView : function ( ) {
540 
541 				// start by copying our upstream view.
542 				var v = this._base.doView();
543 
544 				// then balance around zero
545 				if( Math.abs(v[0]) > Math.abs(v[1]) ) {
546 					v[1] = -v[0];
547 				} else {
548 					v[0] = -v[1];
549 				}
550 
551 				// return value for downstream views.
552 				return v;
553 			}
554 		}
555 	);
556 
557 	/**
558 	 * Returns a new scalar view which ranges from zero to the greatest absolute
559 	 * distance from zero and which maps the absolute magnitude of values.
560 	 * Views are dynamic, adapting to any subsequent changes
561 	 * in the base range.
562 	 *
563 	 * @returns {aperture.Scalar}
564 	 *      a new view of this Range.
565 	 *
566 	 * @name aperture.Scalar.prototype.absolute
567 	 * @function
568 	 */
569 	namespace.Scalar.addView( 'absolute',
570 		{
571 			init : function ( ) {
572 				this.revision = 0;
573 			},
574 
575 			doView : function ( ) {
576 
577 				// start by copying our upstream view.
578 				var v = this._base.doView();
579 
580 				v[1] = Math.max ( Math.abs(v[0]), Math.abs(v[1]) );
581 				v[0] = 0;
582 
583 				// return value for downstream views.
584 				return v;
585 			},
586 
587 			// Override of map function for absolute cases.
588 			map : function ( value ) {
589 
590 				// error check (note that math.abs below will take care of other invalid cases)
591 				if( value == null ) {
592 					return 0;
593 				}
594 				return this._base.map.call( this, Math.abs(value) );
595 			}
596 		}
597 	);
598 
599 	// used in banding
600 	function roundStep( step ) {
601 		var round = Math.pow( 10, Math.floor( Math.log( step ) * Math.LOG10E ) );
602 
603 		// round steps are considered 1, 2, or 5.
604 		step /= round;
605 
606 		if (step <= 2) {
607 			step = 2;
608 		} else if (step <= 5) {
609 			step = 5;
610 		} else {
611 			step = 10;
612 		}
613 
614 		return step * round;
615 	}
616 
617 	/**
618 	 * Returns a new banded scalar view of this range based on the specification
619 	 * supplied. Bands are used for axis articulation, or
620 	 * for subsequently quantizing scalars into labeled ordinals
621 	 * (e.g. up / down, or good / bad) for visual mapping (e.g. up color, down color).
622 	 * A banded view returns multiple band object values for
623 	 * <span class="fixedFont">get()</span>, where each object has a
624 	 * <span class="fixedFont">min</span>,
625 	 * <span class="fixedFont">label</span>, and
626 	 * <span class="fixedFont">limit</span> property.
627 	 * <br><br>
628 	 * Banded views are live, meaning subsequent range changes are allowed. Multiple
629 	 * bands may be generated from the same range object for different visual
630 	 * applications. Scalar bands may be specified simply by supplying a desired
631 	 * approximate count, appropriate to the visual range available, or by specifying
632 	 * predefined labeled value bands based on the domain of the values, such
633 	 * as 'Very Good' or 'Very Poor'. Bounds are always evaluated by a
634 	 * minimum threshold condition and must be contiguous.
635 	 * <br><br>
636 	 * Banded or quantized views must be the last in the chain of views -
637 	 * other optional views such as logarithmic, absolute, or symmetric can be
638 	 * the source of a banded view but cannot be derived from one.For example:
639 	 *
640 	 * @example
641 	 *
642 	 * // default banded view
643 	 * myTimeRange.banded();
644 	 *
645 	 * // view with around five bands, or a little less
646 	 * myTimeRange.banded(5);
647 	 *
648 	 * // view with around five bands, and don't round the edges
649 	 * myTimeRange.banded(5, false);
650 	 *
651 	 * // or, view banded every thousand
652 	 * myTimeRange.banded({ span: 1000 });
653 	 *
654 	 * // or, view with these exact bands
655 	 * myTimeRange.banded([{min: 0}, {min: 500}]);
656 	 *
657 	 * // or, using shortcut for above.
658 	 * myTimeRange.banded([0, 500]);
659 	 *
660 	 * @param {Number|Object|Array} [bands=1(minimum)]
661 	 *      the approximate count of bands to create, OR a band specification object
662 	 *      containing a span field indicating the regular interval for bands, OR an
663 	 *      array of predefined bands supplied as objects with min and label properties,
664 	 *      in ascending order. If this value is not supplied one band will be created,
665 	 *      or two if the range extents span zero.
666 	 *
667 	 * @param {boolean} [roundTo=true]
668 	 *      whether or not to round the range extents to band edges
669 	 *
670 	 * @returns {aperture.Scalar}
671 	 *      a new view of this Range, with limitations on further view creation.
672 	 *
673 	 * @name aperture.Scalar.prototype.banded
674 	 * @function
675 	 */
676 	namespace.Scalar.addView( 'banded',
677 		{
678 			init : function ( bands, roundTo ) {
679 				this.revision = 0;
680 
681 				// prevent derivation of more views. would be nice to support secondary
682 				// banding, but not supported yet.
683 				this.abs = this.log = this.symmetric = noBandedViews;
684 				this.bandSpec = {roundTo : this.bandSpec? false : roundTo === undefined? true : roundTo};
685 
686 				// predefined bands? validate labels.
687 				if ( util.isNumber(bands) ) {
688 					if ( isNaN(bands) || bands < 1 ) {
689 						bands = 1;
690 					}
691 				} else if ( bands && bands.span && !isNaN(bands.span)) {
692 					this.bandSpec.autoBands = bands;
693 				} else if ( util.isArray(bands) ) {
694 					// don't continue as array if not valid...
695 					if ( bands.length < 1 ) {
696 						bands = 1;
697 					} else {
698 						var band, limit, i;
699 
700 						// copy (hmm, but note this does not deep copy)
701 						this.bandSpec.bands = bands = bands.slice();
702 
703 						// process in descending order.
704 						for ( i = bands.length; i-->0; ) {
705 							band = bands[i];
706 
707 							// replace numbers with objects.
708 							if ( util.isNumber(band) ) {
709 								band = { min : band };
710 								bands.splice( i, 1, band );
711 							}
712 
713 							// set limit for convenience if not set.
714 							if (band.limit === undefined && limit !== undefined) {
715 								band.limit = limit;
716 							}
717 
718 							limit = band.min;
719 						}
720 					}
721 
722 				} else {
723 					bands = 1;
724 				}
725 
726 				// a number. generate them from the source data.
727 				if (!this.bandSpec.bands) {
728 					this.bandSpec.autoBands = bands;
729 				}
730 			},
731 
732 			doView : function ( ) {
733 
734 				var v = this._base.doView(),
735 					bands = this.bandSpec.bands;
736 
737 				// second order bands
738 				if (!util.isNumber(v[0])) {
739 					v = this._base.extents;
740 				}
741 				if (this.bandSpec.autoBands) {
742 
743 					// TODO: cover log case.
744 
745 					// first get extents, forcing an update.
746 					// note end and start here may vary from the actual v[0] and v[1] values
747 					var spec = this.bandSpec.autoBands,
748 						start = v[0],
749 						end = v[1];
750 
751 					// if zero range, handle problem case by bumping up the end of range by a tenth (or 1 if zero).
752 					if (end === start) {
753 						end = (end? end + 0.1* Math.abs(end) : 1);
754 					}
755 
756 					// delegate to any specialized class/view, or handle standard cases here.
757 					if (this.doBands) {
758 						bands = this.doBands(start, end, spec);
759 					} else {
760 						bands = [];
761 
762 						var step = spec.span;
763 
764 						if (!step || step < 0) {
765 							// if range spans zero, want an increment to fall on zero,
766 							// so use the larger half to calculate the round step.
767 							if (end * start < 0) {
768 								// cannot properly create only one band if it spans zero.
769 								if (spec === 1) {
770 									spec = 2;
771 								}
772 								// use the greater absolute.
773 								if (end > -start) {
774 									spec *= end / (end-start);
775 									start = 0;
776 
777 								} else {
778 									spec *= -start / (end-start);
779 									end = 0;
780 								}
781 							}
782 
783 							step = roundStep((end - start) / spec);
784 						}
785 
786 						var next = Math.floor( v[0] / step ) * step,
787 							min;
788 
789 						// build the range.
790 						do {
791 							min = next;
792 							next += step;
793 							bands.push({
794 								min : min,
795 								limit : next
796 							});
797 
798 						} while (next < v[1]);
799 					}
800 				} else {
801 					var first = 0, last = bands.length;
802 
803 					while ( --last > 0 ) {
804 						if ( v[1] > bands[last].min ) {
805 							first = last+ 1;
806 							while ( --first > 0 ) {
807 								if ( v[0] >= bands[first].min ) {
808 									break;
809 								}
810 							}
811 							break;
812 						}
813 					}
814 
815 					// take a copy of the active subset
816 					bands = bands.slice(first, last+1);
817 				}
818 
819 				// if not rounded, replace any partial bands with unbounded bands,
820 				// signaling that the bottom should not be ticked.
821 				if ( !this.bandSpec.roundTo ) {
822 					// Only do this if there is more than 1 band, otherwise
823 					// both the band min and limit values will be unbounded
824 					// and there will not be a top or bottom tick.
825 					if ( v[0] !== bands[0].min && bands.length > 1) {
826 						bands[0] = {
827 							min : -Number.MAX_VALUE,
828 							limit : bands[0].limit,
829 							label : bands[0].label
830 						};
831 					}
832 
833 					var e = bands.length - 1;
834 
835 					if ( v[1] !== bands[e].limit ) {
836 						bands[e] = {
837 							min : bands[e].min,
838 							limit : Number.MAX_VALUE,
839 							label : bands[e].label
840 						};
841 					}
842 
843 				} else {
844 					// else revise the extents for update below.
845 					if (bands[0].min != null) {
846 						v[0] = bands[0].min;
847 					}
848 					if (bands[bands.length-1].limit != null) {
849 						v[1] = bands[bands.length-1].limit;
850 					}
851 				}
852 
853 				// store extents for mapping
854 				this.extents = v;
855 
856 				return bands;
857 			},
858 
859 			// override to use extents instead of the result of get.
860 			start : function() {
861 				this.get();
862 				return this.extents && this.extents[0];
863 			},
864 			end : function() {
865 				this.get();
866 				return this.extents && this.extents[1];
867 			},
868 			map : function( value ) {
869 
870 				// call function to update if necessary.
871 				this.get();
872 
873 				var d = this.extents;
874 
875 				// if anything is invalid, return 0 to keep our clamped contract.
876 				if( !d || value == null || isNaN(value = Number(value)) ) {
877 					return 0;
878 				}
879 				// return limit or interpolate
880 				return value <= d[0]? 0
881 						: value >= d[1]? 1
882 								: (value-d[0]) / (d[1]-d[0]);
883 			}
884 		}
885 	);
886 
887 	/**
888 	 * Returns a quantized ordinal view of a banded scalar view range.
889 	 * Quantized views map ordinally (and produce ordinal mappings)
890 	 * and format scalar values by returning the ordinal band they fall into.
891 	 *
892 	 * @returns {aperture.Scalar}
893 	 *      a new view of this Range, with ordinal mapping.
894 	 *
895 	 * @name aperture.Scalar.prototype.quantized
896 	 * @function
897 	 */
898 	namespace.Scalar.addView( 'quantized',
899 		{
900 			init : function ( ) {
901 				if ( !this.typeOf( namespace.Scalar.prototype.banded ) ) {
902 					throw new Error('Only banded scalars can be quantized.');
903 				}
904 				this.banded = noBandedViews;
905 				this.revision = 0;
906 			},
907 
908 			// In our view implementation we add labels to a copy of the
909 			// banded set.
910 			doView : function ( ) {
911 				var src = this._base.doView();
912 
913 				if (this.bandSpec.autoBands) {
914 					var label,
915 						band,
916 						bands = [],
917 						i = src.length;
918 
919 					// process in descending order. make sure we have labels etc.
920 					while ( i-- > 0 ) {
921 						band = src[i];
922 
923 						if (band.min !== -Math.MAX_VALUE) {
924 							label = view.format( band.min );
925 
926 							if (band.limit !== Math.MAX_VALUE) {
927 								label += ' - ' + view.format( band.limit );
928 							} else {
929 								label += ' +';
930 							}
931 
932 						} else {
933 							if (band.limit !== Math.MAX_VALUE) {
934 								label = '< ' + view.format( band.limit );
935 							} else {
936 								label = 'all';
937 							}
938 						}
939 
940 						// push new def
941 						bands.push({
942 							min : band.min,
943 							limit : band.limit,
944 							label : label
945 						});
946 					}
947 
948 					return bands;
949 				}
950 
951 				return src;
952 			},
953 
954 			// Implemented to create an ordinal mapping.
955 			mappedTo : function ( to ) {
956 
957 				// co-opt this method from ordinal
958 				return namespace.Ordinal.prototype.mappedTo.call( this, to );
959 			},
960 
961 			// Implemented to map a scalar value to an ordinal value by finding its band.
962 			map : function ( value ) {
963 				var v = this.get();
964 
965 				// if anything is invalid, return 0 to keep our clamped contract. otherwise...
966 				if( v && value != null && !isNaN(value = Number(value)) ) {
967 					var i = v.length;
968 
969 					while (i-- > 0) {
970 						if ( value >= v[i].min ) {
971 							return i;
972 						}
973 					}
974 				}
975 
976 				return 0;
977 			},
978 
979 			/**
980 			 * Implemented to return the band label for a value
981 			 */
982 			format : function ( value ) {
983 				return this.get()[this.map( value )].label;
984 			}
985 		}
986 	);
987 
988 	/**
989 	 * Returns a new scalar view which maps the order of magnitude of source values.
990 	 * Log views are constructed with a <span class="fixedFont">zero</span>
991 	 * threshold specifying the absolute value under which values should be no longer
992 	 * be mapped logarithmically, even if in range. Specifying this value enables
993 	 * a range to safely approach or span zero and still map effectively.
994 	 * Log views can map negative or positive values and are
995 	 * dynamic, adapting to any subsequent changes in the base range.
996 	 *
997 	 * @param zero
998 	 *      the minimum absolute value above which to map logarithmically.
999 	 *      if not supplied this value will default to 0.1.
1000 	 *
1001 	 * @returns {aperture.Scalar}
1002 	 *      a new view of this Range.
1003 	 *
1004 	 * @name aperture.Scalar.prototype.logarithmic
1005 	 * @function
1006 	 */
1007 	namespace.Scalar.addView( 'logarithmic',
1008 		{
1009 			init : function ( zero ) {
1010 				this.revision = 0;
1011 				this.absMin = zero || 0.1;
1012 			},
1013 
1014 			doView : function ( ) {
1015 
1016 				// start by copying our upstream view.
1017 				var v = this._base.doView();
1018 
1019 				// constraint the range boundaries based on the
1020 				// log minimum configured.
1021 				if ( v[0] < 0 ) {
1022 					v[0] = Math.min( v[0], -this.absMin );
1023 					v[1] = ( v[1] < 0 )?
1024 						Math.min( v[1], -this.absMin ): // both neg
1025 						Math.max( v[1],  this.absMin ); // spans zero
1026 				} else {
1027 					// both positive
1028 					v[0] = Math.max( v[0], this.absMin );
1029 					v[1] = Math.max( v[1], this.absMin );
1030 				}
1031 
1032 				// cache derived constants for fast map calculations.
1033 				var log0 = Math.log(Math.abs( v[0] ))* Math.LOG10E;
1034 				var log1 = Math.log(Math.abs( v[1] ))* Math.LOG10E;
1035 
1036 				// find our abs log min and max - if spans, the zeroish value, else the smaller
1037 				this.logMin = v[0]*v[1] < 0? Math.log(this.absMin)* Math.LOG10E : Math.min( log0, log1 );
1038 				this.mappedLogMin = 0;
1039 				this.oneOverLogRange = 0;
1040 
1041 				// establish the range
1042 				var logRange = log0 - this.logMin + log1 - this.logMin;
1043 
1044 				if (logRange) {
1045 					this.oneOverLogRange = 1 / logRange;
1046 
1047 					// now find mapped closest-to-zero value (between 0 and 1)
1048 					this.mappedLogMin = v[0] >= 0? 0: v[1] <= 0? 1:
1049 						(log0 - this.logMin) * this.oneOverLogRange;
1050 				}
1051 
1052 				// return value for downstream views.
1053 				return v;
1054 			},
1055 
1056 			// Override of map function for logarithmic cases.
1057 			map : function ( value ) {
1058 
1059 				// call base map impl, which also updates view if necessary.
1060 				// handles simple edge cases, out of bounds, bad value check, etc.
1061 				switch (this._base.map.call( this, value )) {
1062 				case 0:
1063 					return 0;
1064 				case 1:
1065 					return 1;
1066 				}
1067 
1068 				var absValue = Math.abs( value = Number(value) );
1069 
1070 				// otherwise do a log mapping
1071 				return this.mappedLogMin +
1072 
1073 					// zero(ish)?
1074 					( absValue <= this.absMin? 0 :
1075 						// or - direction * mapped log value
1076 						( value > 0? 1 : -1 ) *
1077 							( Math.log( absValue )* Math.LOG10E - this.logMin ) * this.oneOverLogRange );
1078 			}
1079 		}
1080 	);
1081 
1082 	// time banding has specialized rules for rounding.
1083 	// band options here are broken into hierarchical orders.
1084 	var timeOrders = (function () {
1085 
1086 		function roundY( date, base ) {
1087 			date.setFullYear(Math.floor(date.getFullYear() / base) * base, 0,1);
1088 			date.setHours(0,0,0,0);
1089 		}
1090 		function roundM( date, base ) {
1091 			date.setMonth(Math.floor(date.getMonth() / base) * base, 1);
1092 			date.setHours(0,0,0,0);
1093 		}
1094 		function roundW( date, base ) {
1095 			date.setDate(date.getDate() - date.getDay());
1096 			date.setHours(0,0,0,0);
1097 		}
1098 		function roundD( date, base ) {
1099 			date.setDate(1 + Math.floor((date.getDate() - 1) / base) * base);
1100 			date.setHours(0,0,0,0);
1101 		}
1102 		function roundF( date, base ) {
1103 			this.setter.call(date, Math.floor(this.getter.call(date) / base) * base, 0,0,0);
1104 		}
1105 		function add( date, value ){
1106 			this.setter.call(date, this.getter.call(date) + value);
1107 		}
1108 
1109 		// define using logical schema...
1110 		var orders = [
1111 				// above one year, normal scalar band rules apply
1112 				{ field: 'FullYear', span: /*366 days*/316224e5, round: roundY, steps: [ 1 ] },
1113 				{ field: 'Month', span: /*31 days*/26784e5, round: roundM, steps: [ 3, 1 ] },
1114 				{ field: 'Date', span: 864e5, round: roundW, steps: [ 7 ] },
1115 				{ field: 'Date', span: 864e5, round: roundD, steps: [ 1 ] },
1116 				{ field: 'Hours', span:36e5, round: roundF, steps: [ 12, 6, 3, 1 ] },
1117 				{ field: 'Minutes', span: 6e4, round: roundF, steps: [ 30, 15, 5, 1 ] },
1118 				{ field: 'Seconds', span: 1e3, round: roundF, steps: [ 30, 15, 5, 1 ] },
1119 				{ field: 'Milliseconds', span: 1, round: roundF, steps: [ 500, 250, 100, 50, 25, 10, 5, 1 ] }
1120 				// below seconds, normal scalar band rules apply
1121 		], timeOrders = [], last, dateProto = Date.prototype;
1122 
1123 		// ...then flatten for convenience.
1124 		util.forEach( orders, function( order ) {
1125 			util.forEach( order.steps, function( step ) {
1126 				timeOrders.push(last = {
1127 					name   : order.field,
1128 					span   : order.span * step,
1129 					next   : last,
1130 					base   : step,
1131 					field  : {
1132 						round  : order.round,
1133 						add    : add,
1134 						getter : dateProto['get' + order.field],
1135 						setter : dateProto['set' + order.field]
1136 					},
1137 					format : new namespace.TimeFormat( order.field )
1138 				});
1139 			});
1140 		});
1141 
1142 		return timeOrders;
1143 	}());
1144 
1145 	// log warning if using unsupported view functions
1146 	function noTimeView() {
1147 		aperture.log.warn('Absolute, logarithmic or symmetric views are inappropriate for time scalars and are intentionally excluded.');
1148 	}
1149 
1150 	var fieldAliases = { 'Year' : 'FullYear', 'Day' : 'Date' };
1151 
1152 	namespace.TimeScalar = namespace.Scalar.extend( 'aperture.TimeScalar',
1153 
1154 		/** @lends aperture.TimeScalar.prototype */
1155 		{
1156 			/**
1157 			 * @class Extends a scalar model property range with
1158 			 * modest specialization of formatting and banding for
1159 			 * JavaScript Dates. Dates are mappable by time by simple
1160 			 * scalars as well, however this class is more appropriate
1161 			 * for determining and labeling bands within a scalar range.
1162 			 * When banded, default date formatting is used
1163 			 * (for the purposes of axis labeling) unless explicitly
1164 			 * overridden in the banded view.
1165 			 * <p>
1166 			 *
1167 			 * @augments aperture.Scalar
1168 			 * @constructs
1169 			 * @description
1170 			 * Constructs a new scalar time range.
1171 			 *
1172 			 * @param {String} name
1173 			 *      the name of the property described.
1174 			 * @param {Array|Number|String|Date} [values]
1175 			 *      an optional array of values (or a single value) with which to
1176 			 *      populate the range. Equivalent to calling {@link #expand} after construction.
1177 			 *
1178 			 * @returns {this}
1179 			 *      a new Scalar
1180 			 */
1181 			init : function( name, values ) {
1182 				namespace.Scalar.prototype.init.call(this, name, values);
1183 				this.formatter_ = new namespace.TimeFormat();
1184 			},
1185 
1186 			/**
1187 			 * Overrides the implementation in {@link aperture.Scalar Scalar}
1188 			 * to expect a units field if the band specification object option
1189 			 * is exercised. The units field in that case will be a
1190 			 * string for the span, corresponding to the exact name of a
1191 			 * common field in the Date class. For example:
1192 			 *
1193 			 * @example
1194 			 * // band every three years
1195 			 * myTimeRange.banded( {
1196 			 *     span: 3,
1197 			 *     units: 'FullYear',
1198 			 * };
1199 			 *
1200 			 * @param {Number|Object|Array} [bands=1(minimum)]
1201 			 *      the approximate count of bands to create, OR a band specification object
1202 			 *      containing a span field indicating the regular interval for bands, OR an
1203 			 *      array of predefined bands supplied as objects with min and label properties,
1204 			 *      in ascending order. If this value is not supplied one band will be created,
1205 			 *      or two if the range extents span zero.
1206 			 *
1207 			 * @param {boolean} [roundTo=true]
1208 			 *      whether or not to round the range extents to band edges
1209 			 *
1210 			 * @returns {aperture.TimeScalar}
1211 			 *      a new view of this Range, with limitations on further view creation.
1212 			 *
1213 			 */
1214 			banded : function( bands, roundTo ) {
1215 				var view = namespace.Scalar.prototype.banded.call(this, bands, roundTo);
1216 
1217 				// update unless overridden.
1218 				view.autoFormat = true;
1219 				view.get(); // Force the view to populate all its properties.
1220 				return view;
1221 			},
1222 
1223 			// unregister these view factories
1224 			absolute : noTimeView,
1225 			logarithmic : noTimeView,
1226 			symmetric : noTimeView,
1227 
1228 			// band specialization - only called by banded views.
1229 			doBands : function(start, end, spec) {
1230 				var order, base, i = 0;
1231 
1232 				// is span predetermined?
1233 				if (spec.span) {
1234 					base = !isNaN(spec.span) && spec.span > 1? spec.span : 1;
1235 
1236 					if (spec.units) {
1237 						var units = fieldAliases[spec.units] || spec.units;
1238 
1239 						// find appropriate order (excluding week, unless matched exactly)
1240 						for (len = timeOrders.length; i < len; i++) {
1241 							if (timeOrders[i].name === units) {
1242 								if ((order = timeOrders[i]).base <= base
1243 										&& (order.base !== 7 || base === 7) ) {
1244 									break;
1245 								}
1246 							}
1247 						}
1248 					}
1249 					if (!order) {
1250 						aperture.log.error('Invalid units in band specification: ' + units);
1251 						spec = 1;
1252 						i = 0;
1253 					}
1254 				}
1255 				if (!order) {
1256 					var interval = Math.max(1, (end - start) / spec), len;
1257 
1258 					// find first under interval.
1259 					for (len = timeOrders.length; i < len; i++) {
1260 						if ((order = timeOrders[i]).span < interval) {
1261 							order = order.next || order; // then pick the next higher
1262 							break;
1263 						}
1264 					}
1265 
1266 					// step in base units. in years? use multiple of base then.
1267 					base = order.next? order.base : Math.max(1, roundStep( interval / 31536e6 )); // in years (/365 day yr)
1268 				}
1269 
1270 				// only auto update format if we haven't had it overridden.
1271 				if (this.autoFormat) {
1272 					this.formatter_ = order.format;
1273 				}
1274 
1275 				// round the start date
1276 				var date = new Date(start), field = order.field, band, bands = [];
1277 				field.round(date, base);
1278 
1279 				// stepping function for bands, in milliseconds
1280 				// (this arbitrary threshold limit kills any chance of an infinite loop, jic.)
1281 				while (i++ < 1000) {
1282 					var next = date.getTime();
1283 
1284 					// last limit is this
1285 					if (band) {
1286 						band.limit = next;
1287 					}
1288 
1289 					// break once we're at or past the end
1290 					if (next >= end) {
1291 						break;
1292 					}
1293 
1294 					// create band (set limit next round)
1295 					bands.push(band = {min: next});
1296 
1297 					field.add(date, base);
1298 				}
1299 
1300 				return bands;
1301 			}
1302 		}
1303 	);
1304 
1305 	namespace.Ordinal = namespace.Range.extend( 'aperture.Ordinal',
1306 
1307 		/** @lends aperture.Ordinal.prototype */
1308 		{
1309 			/**
1310 			 * @class Represents an ordinal model property range. Unlike Scalar
1311 			 * property mappings, which interpolate, Ordinals map ordered
1312 			 * model cases to order visual property options: for instance
1313 			 * series colors, or up / down indicators.
1314 			 * <p>
1315 			 *
1316 			 * @augments aperture.Range
1317 			 * @constructs
1318 			 * @description
1319 			 * Constructs a new ordinal range.
1320 			 *
1321 			 * @param {String} name
1322 			 *      the name of the property described.
1323 			 * @param {Array|Object} [values]
1324 			 *      an optional array of ordinal values/cases with witch to populate
1325 			 *      the object, or a single such value.
1326 			 *
1327 			 * @returns {this}
1328 			 *      a new Ordinal
1329 			 */
1330 			init : function( name, values ) {
1331 				namespace.Range.prototype.init.call(this, name);
1332 
1333 				/**
1334 				 * @private
1335 				 * a record of the values (and their order) that we've seen so far
1336 				 */
1337 				this.range = {values : [], revision : 0, revise : revise};
1338 
1339 				// starting view revision is 0
1340 				// this gets checked later against range revision
1341 				this.revision = 0;
1342 
1343 				if( values != null ) {
1344 					this.expand(values);
1345 				}
1346 			},
1347 
1348 			/**
1349 			 * Removes a property value from the set of ordinal cases.
1350 			 *
1351 			 * @param value
1352 			 *      a case to remove from the property.
1353 			 *
1354 			 * @returns
1355 			 *      the value removed, or null if not found.
1356 			 */
1357 			revoke : function ( value ) {
1358 				if( util.isArray(value) ) {
1359 					// Revoking an array of things
1360 					var args = [this.range.values];
1361 					this.range.values = util.without.apply(util, args.concat(value));
1362 				} else {
1363 					// Revoking a single thing
1364 					this.range.values = util.without(this.range.values, value);
1365 				}
1366 
1367 				this.range.revise();
1368 
1369 				return this;
1370 			},
1371 
1372 			/**
1373 			 * Clears the property range, then optionally expands
1374 			 * it with new values.
1375 			 *
1376 			 * @param {Array|Object} [values]
1377 			 *      an optional array of ordinal values/cases with witch to repopulate
1378 			 *      the object, or a single such value.
1379 			 *
1380 			 * @returns {this}
1381 			 *      a reference to this property.
1382 			 */
1383 			reset : function ( values ) {
1384 				this.range.values = [];
1385 				this.range.revise();
1386 
1387 				if ( values != null ) {
1388 					this.expand ( values );
1389 				}
1390 
1391 				return this;
1392 			},
1393 
1394 
1395 			/**
1396 			 * Expands the property range to encompass the value, if necessary.
1397 			 *
1398 			 * @param {Array|Object} value
1399 			 *      a case or set of cases to include in the property range.
1400 			 *
1401 			 * @returns {this}
1402 			 *      a reference to this property.
1403 			 */
1404 			expand : function ( value ) {
1405 				var values = this.range.values,
1406 					size = values.length,
1407 					changed, i= 0, n;
1408 
1409 				if ( util.isArray(value) ) {
1410 					for (n= value.length; i< n; i++) {
1411 						changed = ( util.indexOf(values, value[i]) === -1 && values.push(value[i])) || changed;
1412 					}
1413 				} else {
1414 					changed = ( util.indexOf(values, value) === -1 && values.push(value));
1415 				}
1416 
1417 				if (changed) {
1418 					this.range.revise();
1419 				}
1420 				return this;
1421 			},
1422 
1423 			/**
1424 			 * Creates a key for mapping from this model range to a visual property
1425 			 * range.
1426 			 *
1427 			 * @param {Array} to
1428 			 *      the ordered set of colors or numbers to map to, from this property's
1429 			 *      range.
1430 			 *
1431 			 * @returns {aperture.MapKey}
1432 			 *      a new map key.
1433 			 */
1434 			mappedTo : function ( to ) {
1435 
1436 				// allow for array wrapping or not.
1437 				if (arguments.length > 1) {
1438 					to = Array.prototype.slice.call(arguments);
1439 				}
1440 				// diagnose problems early so they don't cascade later
1441 				if ( to.length === 0 ) {
1442 					return;
1443 				}
1444 
1445 				return new namespace.OrdinalMapKey( this, to );
1446 			},
1447 
1448 			/**
1449 			 * Returns the mapped index of the specified value, adding
1450 			 * it if it has not already been seen.
1451 			 */
1452 			map : function ( value ) {
1453 				var values = this.range.values,
1454 					i = util.indexOf( values, value );
1455 
1456 				// add if have not yet seen.
1457 				if (i < 0) {
1458 					i = values.length;
1459 
1460 					values.push( value );
1461 				}
1462 
1463 				return i;
1464 			},
1465 
1466 			/**
1467 			 * Returns the index of the specified value, or -1 if not found.
1468 			 */
1469 			indexOf : function ( value ) {
1470 				return util.indexOf( this.range.values, value );
1471 			},
1472 
1473 			/**
1474 			 * @private
1475 			 *
1476 			 * Views override this to chain updates together. This will never
1477 			 * be called if the range is empty / null. The default implementation
1478 			 * returns the source range (not a copy) which is subsequently transformed
1479 			 * by downstream views, in place.
1480 			 */
1481 			doView : function() {
1482 				return this.range.values;
1483 			}
1484 		}
1485 	);
1486 
1487 	/**
1488 	 * Returns a new banded scalar view of this range which maps to the normalized
1489 	 * center of band. Banded views are used for axis articulation.
1490 	 * Banded views are live, meaning subsequent range changes are allowed.
1491 	 *
1492 	 * @returns {aperture.Ordinal}
1493 	 *      a new scalar view of this Range, with limitations on further view creation.
1494 	 *
1495 	 * @name aperture.Ordinal.prototype.banded
1496 	 * @function
1497 	 */
1498 	namespace.Ordinal.addView( 'banded',
1499 		{
1500 			init : function ( view ) {
1501 				this.revision = 0;
1502 			},
1503 
1504 			// implemented to return a banded version.
1505 			doView : function ( ) {
1506 
1507 				// start by copying our upstream view.
1508 				var v = this._base.doView(),
1509 					bands = [],
1510 					i= v.length,
1511 					limit = '';
1512 
1513 				bands.length = i;
1514 
1515 				while (i-- > 0) {
1516 					bands[i]= {
1517 						min: v[i],
1518 						label: v[i].toString(),
1519 						limit: limit
1520 					};
1521 					limit = v[i];
1522 				}
1523 
1524 				return bands;
1525 			},
1526 
1527 			// Implemented to create an ordinal mapping.
1528 			mappedTo : function ( to ) {
1529 
1530 				// co-opt this method from scalar
1531 				return namespace.Scalar.prototype.mappedTo.call( this, to );
1532 			},
1533 
1534 			// Implemented to map an ordinal value to a scalar value.
1535 			map : function ( value ) {
1536 
1537 				var n = this.get().length;
1538 
1539 				// normalize.
1540 				return n === 0? 0: this._base.map( value ) / n;
1541 			},
1542 
1543 			// would be nice to support this for aggregated bins, but not right now.
1544 			banded : function () {
1545 				throw new Error('Cannot create a view of a banded ordinal!');
1546 			}
1547 		}
1548 	);
1549 
1550 	return namespace;
1551 
1552 }(aperture || {}));
1553