/** * @class The built-in Array class. * @name Array */ if (!Array.prototype.map) { /** * Creates a new array with the results of calling a provided function on * every element in this array. Implemented in Javascript 1.6. * * @see map * documentation. * @param {function} f function that produces an element of the new Array from * an element of the current one. * @param [o] object to use as this when executing f. */ Array.prototype.map = function(f, o) { var n = this.length; var result = new Array(n); for (var i = 0; i < n; i++) { if (i in this) { result[i] = f.call(o, this[i], i, this); } } return result; }; } if (!Array.prototype.filter) { /** * Creates a new array with all elements that pass the test implemented by the * provided function. Implemented in Javascript 1.6. * * @see filter * documentation. * @param {function} f function to test each element of the array. * @param [o] object to use as this when executing f. */ Array.prototype.filter = function(f, o) { var n = this.length; var result = new Array(); for (var i = 0; i < n; i++) { if (i in this) { var v = this[i]; if (f.call(o, v, i, this)) result.push(v); } } return result; }; } if (!Array.prototype.forEach) { /** * Executes a provided function once per array element. Implemented in * Javascript 1.6. * * @see forEach * documentation. * @param {function} f function to execute for each element. * @param [o] object to use as this when executing f. */ Array.prototype.forEach = function(f, o) { var n = this.length >>> 0; for (var i = 0; i < n; i++) { if (i in this) f.call(o, this[i], i, this); } }; } if (!Array.prototype.reduce) { /** * Apply a function against an accumulator and each value of the array (from * left-to-right) as to reduce it to a single value. Implemented in Javascript * 1.8. * * @see reduce * documentation. * @param {function} f function to execute on each value in the array. * @param [v] object to use as the first argument to the first call of * t. */ Array.prototype.reduce = function(f, v) { var len = this.length; if (!len && (arguments.length == 1)) { throw new Error("reduce: empty array, no initial value"); } var i = 0; if (arguments.length < 2) { while (true) { if (i in this) { v = this[i++]; break; } if (++i >= len) { throw new Error("reduce: no values, no initial value"); } } } for (; i < len; i++) { if (i in this) { v = f(v, this[i], i, this); } } return v; }; } /** * @class The built-in Date class. * @name Date */ Date.__parse__ = Date.parse; /** * Parses a date from a string, optionally using the specified formatting. If * only a single argument is specified (i.e., format is not specified), * this method invokes the native implementation to guarantee * backwards-compatibility. * *
The format string is in the same format expected by the strptime * function in C. The following conversion specifications are supported:
The format string is in the same format expected by the strftime * function in C. The following conversion specifications are supported:
function g() {} * g.prototype = f.prototype; * return new g();* * For more details, see Douglas Crockford's essay on prototypal inheritance. * * @param {function} f a constructor. * @returns a suitable prototype object. * @see Douglas Crockford's essay on prototypal * inheritance. */ pv.extend = function(f) { function g() {} g.prototype = f.prototype || f; return new g(); }; try { eval("pv.parse = function(x) x;"); // native support } catch (e) { /** * @private Parses a Protovis specification, which may use JavaScript 1.8 * function expresses, replacing those function expressions with proper * functions such that the code can be run by a JavaScript 1.6 interpreter. This * hack only supports function expressions (using clumsy regular expressions, no * less), and not other JavaScript 1.8 features such as let expressions. * * @param {string} s a Protovis specification (i.e., a string of JavaScript 1.8 * source code). * @returns {string} a conformant JavaScript 1.6 source code. */ pv.parse = function(js) { // hacky regex support var re = new RegExp("function(\\s+\\w+)?\\([^)]*\\)\\s*", "mg"), m, d, i = 0, s = ""; while (m = re.exec(js)) { var j = m.index + m[0].length; if (js.charAt(j--) != '{') { s += js.substring(i, j) + "{return "; i = j; for (var p = 0; p >= 0 && j < js.length; j++) { var c = js.charAt(j); switch (c) { case '"': case '\'': { while (++j < js.length && (d = js.charAt(j)) != c) { if (d == '\\') j++; } break; } case '[': case '(': p++; break; case ']': case ')': p--; break; case ';': case ',': if (p == 0) p--; break; } } s += pv.parse(js.substring(i, --j)) + ";}"; i = j; } re.lastIndex = j; } s += js.substring(i); return s; }; } /** * Returns the passed-in argument, x; the identity function. This method * is provided for convenience since it is used as the default behavior for a * number of property functions. * * @param x a value. * @returns the value x. */ pv.identity = function(x) { return x; }; /** * Returns this.index. This method is provided for convenience for use * with scales. For example, to color bars by their index, say: * *
.fillStyle(pv.Colors.category10().by(pv.index))* * This method is equivalent to function() this.index, but more * succinct. Note that the index property is also supported for * accessor functions with {@link pv.max}, {@link pv.min} and other array * utility methods. * * @see pv.Scale * @see pv.Mark#index */ pv.index = function() { return this.index; }; /** * Returns this.childIndex. This method is provided for convenience for * use with scales. For example, to color bars by their child index, say: * *
.fillStyle(pv.Colors.category10().by(pv.child))* * This method is equivalent to function() this.childIndex, but more * succinct. * * @see pv.Scale * @see pv.Mark#childIndex */ pv.child = function() { return this.childIndex; }; /** * Returns this.parent.index. This method is provided for convenience * for use with scales. This method is provided for convenience for use with * scales. For example, to color bars by their parent index, say: * *
.fillStyle(pv.Colors.category10().by(pv.parent))* * Tthis method is equivalent to function() this.parent.index, but more * succinct. * * @see pv.Scale * @see pv.Mark#index */ pv.parent = function() { return this.parent.index; }; /** * Returns an array of numbers, starting at start, incrementing by * step, until stop is reached. The stop value is exclusive. If * only a single argument is specified, this value is interpeted as the * stop value, with the start value as zero. If only two arguments * are specified, the step value is implied to be one. * *
The method is modeled after the built-in range method from * Python. See the Python documentation for more details. * * @see Python range * @param {number} [start] the start value. * @param {number} stop the stop value. * @param {number} [step] the step value. * @returns {number[]} an array of numbers. */ pv.range = function(start, stop, step) { if (arguments.length == 1) { stop = start; start = 0; } if (step == undefined) step = 1; else if (!step) throw new Error("step must be non-zero"); var array = [], i = 0, j; if (step < 0) { while ((j = start + step * i++) > stop) { array.push(j); } } else { while ((j = start + step * i++) < stop) { array.push(j); } } return array; }; /** * Returns a random number in the range [min, max) that is a * multiple of step. More specifically, the returned number is of the * form min + n * step, where n is a nonnegative * integer. If step is not specified, it defaults to 1, returning a * random integer if min is also an integer. * * @param min {number} minimum value. * @param [max] {number} maximum value. * @param [step] {numbeR} step value. */ pv.random = function(min, max, step) { if (arguments.length == 1) { max = min; min = 0; } if (step == undefined) { step = 1; } return step ? (Math.floor(Math.random() * (max - min) / step) * step + min) : (Math.random() * (max - min) + min); }; /** * Concatenates the specified array with itself n times. For example, * pv.repeat([1, 2]) returns [1, 2, 1, 2]. * * @param {array} a an array. * @param {number} [n] the number of times to repeat; defaults to two. * @returns {array} an array that repeats the specified array. */ pv.repeat = function(array, n) { if (arguments.length == 1) n = 2; return pv.blend(pv.range(n).map(function() { return array; })); }; /** * Given two arrays a and b, returns an array of all possible * pairs of elements [ai, bj]. The outer loop is on array * a, while the inner loop is on b, such that the order of * returned elements is [a0, b0], [a0, * b1], ... [a0, bm], [a1, * b0], [a1, b1], ... [a1, * bm], ... [an, bm]. If either array is empty, * an empty array is returned. * * @param {array} a an array. * @param {array} b an array. * @returns {array} an array of pairs of elements in a and b. */ pv.cross = function(a, b) { var array = []; for (var i = 0, n = a.length, m = b.length; i < n; i++) { for (var j = 0, x = a[i]; j < m; j++) { array.push([x, b[j]]); } } return array; }; /** * Given the specified array of arrays, concatenates the arrays into a single * array. If the individual arrays are explicitly known, an alternative to blend * is to use JavaScript's concat method directly. These two equivalent * expressions:
pv.normalize(array, function(d) d.foo)* * returns a normalized array on the "foo" property. If an accessor function is * not specified, the identity function is used. Accessor functions can refer to * this.index. * * @param {array} array an array of objects, or numbers. * @param {function} [f] an optional accessor function. * @returns {number[]} an array of numbers that sums to one. */ pv.normalize = function(array, f) { var norm = map(array, f), sum = pv.sum(norm); for (var i = 0; i < norm.length; i++) norm[i] /= sum; return norm; }; /** * Returns the sum of the specified array. If the specified array is not an * array of numbers, an optional accessor function f can be specified * to map the elements to numbers. See {@link #normalize} for an example. * Accessor functions can refer to this.index. * * @param {array} array an array of objects, or numbers. * @param {function} [f] an optional accessor function. * @returns {number} the sum of the specified array. */ pv.sum = function(array, f) { var o = {}; return array.reduce(f ? function(p, d, i) { o.index = i; return p + f.call(o, d); } : function(p, d) { return p + d; }, 0); }; /** * Returns the maximum value of the specified array. If the specified array is * not an array of numbers, an optional accessor function f can be * specified to map the elements to numbers. See {@link #normalize} for an * example. Accessor functions can refer to this.index. * * @param {array} array an array of objects, or numbers. * @param {function} [f] an optional accessor function. * @returns {number} the maximum value of the specified array. */ pv.max = function(array, f) { if (f == pv.index) return array.length - 1; return Math.max.apply(null, f ? map(array, f) : array); }; /** * Returns the index of the maximum value of the specified array. If the * specified array is not an array of numbers, an optional accessor function * f can be specified to map the elements to numbers. See * {@link #normalize} for an example. Accessor functions can refer to * this.index. * * @param {array} array an array of objects, or numbers. * @param {function} [f] an optional accessor function. * @returns {number} the index of the maximum value of the specified array. */ pv.max.index = function(array, f) { if (f == pv.index) return array.length - 1; if (!f) f = pv.identity; var maxi = -1, maxx = -Infinity, o = {}; for (var i = 0; i < array.length; i++) { o.index = i; var x = f.call(o, array[i]); if (x > maxx) { maxx = x; maxi = i; } } return maxi; } /** * Returns the minimum value of the specified array of numbers. If the specified * array is not an array of numbers, an optional accessor function f * can be specified to map the elements to numbers. See {@link #normalize} for * an example. Accessor functions can refer to this.index. * * @param {array} array an array of objects, or numbers. * @param {function} [f] an optional accessor function. * @returns {number} the minimum value of the specified array. */ pv.min = function(array, f) { if (f == pv.index) return 0; return Math.min.apply(null, f ? map(array, f) : array); }; /** * Returns the index of the minimum value of the specified array. If the * specified array is not an array of numbers, an optional accessor function * f can be specified to map the elements to numbers. See * {@link #normalize} for an example. Accessor functions can refer to * this.index. * * @param {array} array an array of objects, or numbers. * @param {function} [f] an optional accessor function. * @returns {number} the index of the minimum value of the specified array. */ pv.min.index = function(array, f) { if (f == pv.index) return 0; if (!f) f = pv.identity; var mini = -1, minx = Infinity, o = {}; for (var i = 0; i < array.length; i++) { o.index = i; var x = f.call(o, array[i]); if (x < minx) { minx = x; mini = i; } } return mini; } /** * Returns the arithmetic mean, or average, of the specified array. If the * specified array is not an array of numbers, an optional accessor function * f can be specified to map the elements to numbers. See * {@link #normalize} for an example. Accessor functions can refer to * this.index. * * @param {array} array an array of objects, or numbers. * @param {function} [f] an optional accessor function. * @returns {number} the mean of the specified array. */ pv.mean = function(array, f) { return pv.sum(array, f) / array.length; }; /** * Returns the median of the specified array. If the specified array is not an * array of numbers, an optional accessor function f can be specified * to map the elements to numbers. See {@link #normalize} for an example. * Accessor functions can refer to this.index. * * @param {array} array an array of objects, or numbers. * @param {function} [f] an optional accessor function. * @returns {number} the median of the specified array. */ pv.median = function(array, f) { if (f == pv.index) return (array.length - 1) / 2; array = map(array, f).sort(pv.naturalOrder); if (array.length % 2) return array[Math.floor(array.length / 2)]; var i = array.length / 2; return (array[i - 1] + array[i]) / 2; }; /** * Returns a map constructed from the specified keys, using the * function f to compute the value for each key. The single argument to * the value function is the key. The callback is invoked only for indexes of * the array which have assigned values; it is not invoked for indexes which * have been deleted or which have never been assigned values. * *
For example, this expression creates a map from strings to string length: * *
pv.dict(["one", "three", "seventeen"], function(s) s.length)* * The returned value is {one: 3, three: 5, seventeen: 9}. Accessor * functions can refer to this.index. * * @param {array} keys an array. * @param {function} f a value function. * @returns a map from keys to values. */ pv.dict = function(keys, f) { var m = {}, o = {}; for (var i = 0; i < keys.length; i++) { if (i in keys) { var k = keys[i]; o.index = i; m[k] = f.call(o, k); } } return m; }; /** * Returns a permutation of the specified array, using the specified array of * indexes. The returned array contains the corresponding element in * array for each index in indexes, in order. For example, * *
pv.permute(["a", "b", "c"], [1, 2, 0])* * returns ["b", "c", "a"]. It is acceptable for the array of indexes * to be a different length from the array of elements, and for indexes to be * duplicated or omitted. The optional accessor function f can be used * to perform a simultaneous mapping of the array elements. Accessor functions * can refer to this.index. * * @param {array} array an array. * @param {number[]} indexes an array of indexes into array. * @param {function} [f] an optional accessor function. * @returns {array} an array of elements from array; a permutation. */ pv.permute = function(array, indexes, f) { if (!f) f = pv.identity; var p = new Array(indexes.length), o = {}; indexes.forEach(function(j, i) { o.index = j; p[i] = f.call(o, array[j]); }); return p; }; /** * Returns a map from key to index for the specified keys array. For * example, * *
pv.numerate(["a", "b", "c"])* * returns {a: 0, b: 1, c: 2}. Note that since JavaScript maps only * support string keys, keys must contain strings, or other values that * naturally map to distinct string values. Alternatively, an optional accessor * function f can be specified to compute the string key for the given * element. Accessor functions can refer to this.index. * * @param {array} keys an array, usually of string keys. * @param {function} [f] an optional key function. * @returns a map from key to index. */ pv.numerate = function(keys, f) { if (!f) f = pv.identity; var map = {}, o = {}; keys.forEach(function(x, i) { o.index = i; map[f.call(o, x)] = i; }); return map; }; /** * The comparator function for natural order. This can be used in conjunction with * the built-in array sort method to sort elements by their natural * order, ascending. Note that if no comparator function is specified to the * built-in sort method, the default order is lexicographic, not * natural! * * @see Array.sort. * @param a an element to compare. * @param b an element to compare. * @returns {number} negative if a < b; positive if a > b; otherwise 0. */ pv.naturalOrder = function(a, b) { return (a < b) ? -1 : ((a > b) ? 1 : 0); }; /** * The comparator function for reverse natural order. This can be used in * conjunction with the built-in array sort method to sort elements by * their natural order, descending. Note that if no comparator function is * specified to the built-in sort method, the default order is * lexicographic, not natural! * * @see #naturalOrder * @param a an element to compare. * @param b an element to compare. * @returns {number} negative if a < b; positive if a > b; otherwise 0. */ pv.reverseOrder = function(b, a) { return (a < b) ? -1 : ((a > b) ? 1 : 0); }; /** * @private Computes the value of the specified CSS property p on the * specified element e. * * @param {string} p the name of the CSS property. * @param e the element on which to compute the CSS property. */ pv.css = function(e, p) { return window.getComputedStyle ? window.getComputedStyle(e, null).getPropertyValue(p) : e.currentStyle[p]; }; /** * Namespace constants for SVG, XMLNS, and XLINK. * * @namespace Namespace constants for SVG, XMLNS, and XLINK. */ pv.ns = { /** * The SVG namespace, "http://www.w3.org/2000/svg". * * @type string * @constant */ svg: "http://www.w3.org/2000/svg", /** * The XMLNS namespace, "http://www.w3.org/2000/xmlns". * * @type string * @constant */ xmlns: "http://www.w3.org/2000/xmlns", /** * The XLINK namespace, "http://www.w3.org/1999/xlink". * * @type string * @constant */ xlink: "http://www.w3.org/1999/xlink" }; /** * Protovis major and minor version numbers. * * @namespace Protovis major and minor version numbers. */ pv.version = { /** * The major version number. * * @type number * @constant */ major: 3, /** * The minor version number. * * @type number * @constant */ minor: 1 }; /** * @private Reports the specified error to the JavaScript console. Mozilla only * allows logging to the console for privileged code; if the console is * unavailable, the alert dialog box is used instead. * * @param e the exception that triggered the error. */ pv.error = function(e) { (typeof console == "undefined") ? alert(e) : console.error(e); }; /** * @private Registers the specified listener for events of the specified type on * the specified target. For standards-compliant browsers, this method uses * addEventListener; for Internet Explorer, attachEvent. * * @param target a DOM element. * @param {string} type the type of event, such as "click". * @param {function} the listener callback function. */ pv.listen = function(target, type, listener) { return target.addEventListener ? target.addEventListener(type, listener, false) : target.attachEvent("on" + type, listener); }; /** * Returns the logarithm with a given base value. * * @param {number} x the number for which to compute the logarithm. * @param {number} b the base of the logarithm. * @returns {number} the logarithm value. */ pv.log = function(x, b) { return Math.log(x) / Math.log(b); }; /** * Computes a zero-symmetric logarithm. Computes the logarithm of the absolute * value of the input, and determines the sign of the output according to the * sign of the input value. * * @param {number} x the number for which to compute the logarithm. * @param {number} b the base of the logarithm. * @returns {number} the symmetric log value. */ pv.logSymmetric = function(x, b) { return (x == 0) ? 0 : ((x < 0) ? -pv.log(-x, b) : pv.log(x, b)); }; /** * Computes a zero-symmetric logarithm, with adjustment to values between zero * and the logarithm base. This adjustment introduces distortion for values less * than the base number, but enables simultaneous plotting of log-transformed * data involving both positive and negative numbers. * * @param {number} x the number for which to compute the logarithm. * @param {number} b the base of the logarithm. * @returns {number} the adjusted, symmetric log value. */ pv.logAdjusted = function(x, b) { var negative = x < 0; if (x < b) x += (b - x) / b; return negative ? -pv.log(x, b) : pv.log(x, b); }; /** * Rounds an input value down according to its logarithm. The method takes the * floor of the logarithm of the value and then uses the resulting value as an * exponent for the base value. * * @param {number} x the number for which to compute the logarithm floor. * @param {number} b the base of the logarithm. * @return {number} the rounded-by-logarithm value. */ pv.logFloor = function(x, b) { return (x > 0) ? Math.pow(b, Math.floor(pv.log(x, b))) : -Math.pow(b, -Math.floor(-pv.log(-x, b))); }; /** * Rounds an input value up according to its logarithm. The method takes the * ceiling of the logarithm of the value and then uses the resulting value as an * exponent for the base value. * * @param {number} x the number for which to compute the logarithm ceiling. * @param {number} b the base of the logarithm. * @return {number} the rounded-by-logarithm value. */ pv.logCeil = function(x, b) { return (x > 0) ? Math.pow(b, Math.ceil(pv.log(x, b))) : -Math.pow(b, -Math.ceil(-pv.log(-x, b))); }; /** * Searches the specified array of numbers for the specified value using the * binary search algorithm. The array must be sorted (as by the sort * method) prior to making this call. If it is not sorted, the results are * undefined. If the array contains multiple elements with the specified value, * there is no guarantee which one will be found. * *
The insertion point is defined as the point at which the value * would be inserted into the array: the index of the first element greater than * the value, or array.length, if all elements in the array are less * than the specified value. Note that this guarantees that the return value * will be nonnegative if and only if the value is found. * * @param {number[]} array the array to be searched. * @param {number} value the value to be searched for. * @returns the index of the search value, if it is contained in the array; * otherwise, (-(insertion point) - 1). * @param {function} [f] an optional key function. */ pv.search = function(array, value, f) { if (!f) f = pv.identity; var low = 0, high = array.length - 1; while (low <= high) { var mid = (low + high) >> 1, midValue = f(array[mid]); if (midValue < value) low = mid + 1; else if (midValue > value) high = mid - 1; else return mid; } return -low - 1; }; pv.search.index = function(array, value, f) { var i = pv.search(array, value, f); return (i < 0) ? (-i - 1) : i; }; /** * Returns a {@link pv.Tree} operator for the specified array. This is a * convenience factory method, equivalent to new pv.Tree(array). * * @see pv.Tree * @param {array} array an array from which to construct a tree. * @returns {pv.Tree} a tree operator for the specified array. */ pv.tree = function(array) { return new pv.Tree(array); }; /** * Constructs a tree operator for the specified array. This constructor should * not be invoked directly; use {@link pv.tree} instead. * * @class Represents a tree operator for the specified array. The tree operator * allows a hierarchical map to be constructed from an array; it is similar to * the {@link pv.Nest} operator, except the hierarchy is derived dynamically * from the array elements. * *
For example, given an array of size information for ActionScript classes: * *
{ name: "flare.flex.FlareVis", size: 4116 }, * { name: "flare.physics.DragForce", size: 1082 }, * { name: "flare.physics.GravityForce", size: 1336 }, ...* * To facilitate visualization, it may be useful to nest the elements by their * package hierarchy: * *
var tree = pv.tree(classes) * .keys(function(d) d.name.split(".")) * .map();* * The resulting tree is: * *
{ flare: { * flex: { * FlareVis: { * name: "flare.flex.FlareVis", * size: 4116 } }, * physics: { * DragForce: { * name: "flare.physics.DragForce", * size: 1082 }, * GravityForce: { * name: "flare.physics.GravityForce", * size: 1336 } }, * ... } }* * By specifying a value function, * *
var tree = pv.tree(classes) * .keys(function(d) d.name.split(".")) * .value(function(d) d.size) * .map();* * we can further eliminate redundant data: * *
{ flare: { * flex: { * FlareVis: 4116 }, * physics: { * DragForce: 1082, * GravityForce: 1336 }, * ... } }* * For visualizations with large data sets, performance improvements may be seen * by storing the data in a tree format, and then flattening it into an array at * runtime with {@link pv.Flatten}. * * @param {array} array an array from which to construct a tree. */ pv.Tree = function(array) { this.array = array; }; /** * Assigns a keys function to this operator; required. The keys function * returns an array of strings for each element in the associated * array; these keys determine how the elements are nested in the tree. The * returned keys should be unique for each element in the array; otherwise, the * behavior of this operator is undefined. * * @param {function} k the keys function. * @returns {pv.Tree} this. */ pv.Tree.prototype.keys = function(k) { this.k = k; return this; }; /** * Assigns a value function to this operator; optional. The value * function specifies an optional transformation of the element in the array * before it is inserted into the map. If no value function is specified, it is * equivalent to using the identity function. * * @param {function} k the value function. * @returns {pv.Tree} this. */ pv.Tree.prototype.value = function(v) { this.v = v; return this; }; /** * Returns a hierarchical map of values. The hierarchy is determined by the keys * function; the values in the map are determined by the value function. * * @returns a hierarchical map of values. */ pv.Tree.prototype.map = function() { var map = {}, o = {}; for (var i = 0; i < this.array.length; i++) { o.index = i; var value = this.array[i], keys = this.k.call(o, value), node = map; for (var j = 0; j < keys.length - 1; j++) { node = node[keys[j]] || (node[keys[j]] = {}); } node[keys[j]] = this.v ? this.v.call(o, value) : value; } return map; }; /** * Returns a {@link pv.Nest} operator for the specified array. This is a * convenience factory method, equivalent to new pv.Nest(array). * * @see pv.Nest * @param {array} array an array of elements to nest. * @returns {pv.Nest} a nest operator for the specified array. */ pv.nest = function(array) { return new pv.Nest(array); }; /** * Constructs a nest operator for the specified array. This constructor should * not be invoked directly; use {@link pv.nest} instead. * * @class Represents a {@link Nest} operator for the specified array. Nesting * allows elements in an array to be grouped into a hierarchical tree * structure. The levels in the tree are specified by key functions. The * leaf nodes of the tree can be sorted by value, while the internal nodes can * be sorted by key. Finally, the tree can be returned either has a * multidimensional array via {@link #entries}, or as a hierarchical map via * {@link #map}. The {@link #rollup} routine similarly returns a map, collapsing * the elements in each leaf node using a summary function. * *
For example, consider the following tabular data structure of Barley * yields, from various sites in Minnesota during 1931-2: * *
{ yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm" }, * { yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca" }, * { yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris" }, ...* * To facilitate visualization, it may be useful to nest the elements first by * year, and then by variety, as follows: * *
var nest = pv.nest(yields) * .key(function(d) d.year) * .key(function(d) d.variety) * .entries();* * This returns a nested array. Each element of the outer array is a key-values * pair, listing the values for each distinct key: * *
{ key: 1931, values: [ * { key: "Manchuria", values: [ * { yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm" }, * { yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca" }, * { yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris" }, * ... * ] }, * { key: "Glabron", values: [ * { yield: 43.07, variety: "Glabron", year: 1931, site: "University Farm" }, * { yield: 55.20, variety: "Glabron", year: 1931, site: "Waseca" }, * ... * ] }, * ] }, * { key: 1932, values: ... }* * Further details, including sorting and rollup, is provided below on the * corresponding methods. * * @param {array} array an array of elements to nest. */ pv.Nest = function(array) { this.array = array; this.keys = []; }; /** * Nests using the specified key function. Multiple keys may be added to the * nest; the array elements will be nested in the order keys are specified. * * @param {function} key a key function; must return a string or suitable map * key. * @return {pv.Nest} this. */ pv.Nest.prototype.key = function(key) { this.keys.push(key); return this; }; /** * Sorts the previously-added keys. The natural sort order is used by default * (see {@link pv.naturalOrder}); if an alternative order is desired, * order should be a comparator function. If this method is not called * (i.e., keys are unsorted), keys will appear in the order they appear * in the underlying elements array. For example, * *
pv.nest(yields) * .key(function(d) d.year) * .key(function(d) d.variety) * .sortKeys() * .entries()* * groups yield data by year, then variety, and sorts the variety groups * lexicographically (since the variety attribute is a string). * *
Key sort order is only used in conjunction with {@link #entries}, which * returns an array of key-values pairs. If the nest is used to construct a * {@link #map} instead, keys are unsorted. * * @param {function} [order] an optional comparator function. * @returns {pv.Nest} this. */ pv.Nest.prototype.sortKeys = function(order) { this.keys[this.keys.length - 1].order = order || pv.naturalOrder; return this; }; /** * Sorts the leaf values. The natural sort order is used by default (see * {@link pv.naturalOrder}); if an alternative order is desired, order * should be a comparator function. If this method is not called (i.e., values * are unsorted), values will appear in the order they appear in the * underlying elements array. For example, * *
pv.nest(yields) * .key(function(d) d.year) * .key(function(d) d.variety) * .sortValues(function(a, b) a.yield - b.yield) * .entries()* * groups yield data by year, then variety, and sorts the values for each * variety group by yield. * *
Value sort order, unlike keys, applies to both {@link #entries} and * {@link #map}. It has no effect on {@link #rollup}. * * @param {function} [order] an optional comparator function. * @return {pv.Nest} this. */ pv.Nest.prototype.sortValues = function(order) { this.order = order || pv.naturalOrder; return this; }; /** * Returns a hierarchical map of values. Each key adds one level to the * hierarchy. With only a single key, the returned map will have a key for each * distinct value of the key function; the correspond value with be an array of * elements with that key value. If a second key is added, this will be a nested * map. For example: * *
pv.nest(yields) * .key(function(d) d.variety) * .key(function(d) d.site) * .map()* * returns a map m such that m[variety][site] is an array, a subset of * yields, with each element having the given variety and site. * * @returns a hierarchical map of values. */ pv.Nest.prototype.map = function() { var map = {}, values = []; /* Build the map. */ for (var i, j = 0; j < this.array.length; j++) { var x = this.array[j]; var m = map; for (i = 0; i < this.keys.length - 1; i++) { var k = this.keys[i](x); if (!m[k]) m[k] = {}; m = m[k]; } k = this.keys[i](x); if (!m[k]) { var a = []; values.push(a); m[k] = a; } m[k].push(x); } /* Sort each leaf array. */ if (this.order) { for (var i = 0; i < values.length; i++) { values[i].sort(this.order); } } return map; }; /** * Returns a hierarchical nested array. This method is similar to * {@link pv.entries}, but works recursively on the entire hierarchy. Rather * than returning a map like {@link #map}, this method returns a nested * array. Each element of the array has a key and values * field. For leaf nodes, the values array will be a subset of the * underlying elements array; for non-leaf nodes, the values array will * contain more key-values pairs. * *
For an example usage, see the {@link Nest} constructor. * * @returns a hierarchical nested array. */ pv.Nest.prototype.entries = function() { /** Recursively extracts the entries for the given map. */ function entries(map) { var array = []; for (var k in map) { var v = map[k]; array.push({ key: k, values: (v instanceof Array) ? v : entries(v) }); }; return array; } /** Recursively sorts the values for the given key-values array. */ function sort(array, i) { var o = this.keys[i].order; if (o) array.sort(function(a, b) { return o(a.key, b.key); }); if (++i < this.keys.length) { for (var j = 0; j < array.length; j++) { sort.call(this, array[j].values, i); } } return array; } return sort.call(this, entries(this.map()), 0); }; /** * Returns a rollup map. The behavior of this method is the same as * {@link #map}, except that the leaf values are replaced with the return value * of the specified rollup function f. For example, * *
pv.nest(yields) * .key(function(d) d.site) * .rollup(function(v) pv.median(v, function(d) d.yield))* * first groups yield data by site, and then returns a map from site to median * yield for the given site. * * @see #map * @param {function} f a rollup function. * @returns a hierarchical map, with the leaf values computed by f. */ pv.Nest.prototype.rollup = function(f) { /** Recursively descends to the leaf nodes (arrays) and does rollup. */ function rollup(map) { for (var key in map) { var value = map[key]; if (value instanceof Array) { map[key] = f(value); } else { rollup(value); } } return map; } return rollup(this.map()); }; /** * Returns a {@link pv.Flatten} operator for the specified map. This is a * convenience factory method, equivalent to new pv.Flatten(map). * * @see pv.Flatten * @param map a map to flatten. * @returns {pv.Flatten} a flatten operator for the specified map. */ pv.flatten = function(map) { return new pv.Flatten(map); }; /** * Constructs a flatten operator for the specified map. This constructor should * not be invoked directly; use {@link pv.flatten} instead. * * @class Represents a flatten operator for the specified array. Flattening * allows hierarchical maps to be flattened into an array. The levels in the * input tree are specified by key functions. * *
For example, consider the following hierarchical data structure of Barley * yields, from various sites in Minnesota during 1931-2: * *
{ 1931: { * Manchuria: { * "University Farm": 27.00, * "Waseca": 48.87, * "Morris": 27.43, * ... }, * Glabron: { * "University Farm": 43.07, * "Waseca": 55.20, * ... } }, * 1932: { * ... } }* * To facilitate visualization, it may be useful to flatten the tree into a * tabular array: * *
var array = pv.flatten(yields) * .key("year") * .key("variety") * .key("site") * .key("yield") * .array();* * This returns an array of object elements. Each element in the array has * attributes corresponding to this flatten operator's keys: * *
{ site: "University Farm", variety: "Manchuria", year: 1931, yield: 27 }, * { site: "Waseca", variety: "Manchuria", year: 1931, yield: 48.87 }, * { site: "Morris", variety: "Manchuria", year: 1931, yield: 27.43 }, * { site: "University Farm", variety: "Glabron", year: 1931, yield: 43.07 }, * { site: "Waseca", variety: "Glabron", year: 1931, yield: 55.2 }, ...* *
The flatten operator is roughly the inverse of the {@link pv.Nest} and
* {@link pv.Tree} operators.
*
* @param map a map to flatten.
*/
pv.Flatten = function(map) {
this.map = map;
this.keys = [];
};
/**
* Flattens using the specified key function. Multiple keys may be added to the
* flatten; the tiers of the underlying tree must correspond to the specified
* keys, in order. The order of the returned array is undefined; however, you
* can easily sort it.
*
* @param {string} key the key name.
* @param {function} [f] an optional value map function.
* @return {pv.Nest} this.
*/
pv.Flatten.prototype.key = function(key, f) {
this.keys.push({name: key, value: f});
return this;
};
/**
* Returns the flattened array. Each entry in the array is an object; each
* object has attributes corresponding to this flatten operator's keys.
*
* @returns an array of elements from the flattened map.
*/
pv.Flatten.prototype.array = function() {
var entries = [], stack = [], keys = this.keys;
/* Recursively visits the specified value. */
function visit(value, i) {
if (i < keys.length - 1) {
for (var key in value) {
stack.push(key);
visit(value[key], i + 1);
stack.pop();
}
} else {
entries.push(stack.concat(value));
}
}
visit(this.map, 0);
return entries.map(function(stack) {
var m = {};
for (var i = 0; i < keys.length; i++) {
var k = keys[i], v = stack[i];
m[k.name] = k.value ? k.value.call(null, v) : v;
}
return m;
});
};
/**
* Returns a {@link pv.Vector} for the specified x and y
* coordinate. This is a convenience factory method, equivalent to new
* pv.Vector(x, y).
*
* @see pv.Vector
* @param {number} x the x coordinate.
* @param {number} y the y coordinate.
* @returns {pv.Vector} a vector for the specified coordinates.
*/
pv.vector = function(x, y) {
return new pv.Vector(x, y);
};
/**
* Constructs a {@link pv.Vector} for the specified x and y
* coordinate. This constructor should not be invoked directly; use
* {@link pv.vector} instead.
*
* @class Represents a two-dimensional vector; a 2-tuple ⟨x,
* y⟩.
*
* @param {number} x the x coordinate.
* @param {number} y the y coordinate.
*/
pv.Vector = function(x, y) {
this.x = x;
this.y = y;
};
/**
* Returns a vector perpendicular to this vector: ⟨-y, x⟩.
*
* @returns {pv.Vector} a perpendicular vector.
*/
pv.Vector.prototype.perp = function() {
return new pv.Vector(-this.y, this.x);
};
/**
* Returns a normalized copy of this vector: a vector with the same direction,
* but unit length. If this vector has zero length this method returns a copy of
* this vector.
*
* @returns {pv.Vector} a unit vector.
*/
pv.Vector.prototype.norm = function() {
var l = this.length();
return this.times(l ? (1 / l) : 1);
};
/**
* Returns the magnitude of this vector, defined as sqrt(x * x + y * y).
*
* @returns {number} a length.
*/
pv.Vector.prototype.length = function() {
return Math.sqrt(this.x * this.x + this.y * this.y);
};
/**
* Returns a scaled copy of this vector: ⟨x * k, y * k⟩.
* To perform the equivalent divide operation, use 1 / k.
*
* @param {number} k the scale factor.
* @returns {pv.Vector} a scaled vector.
*/
pv.Vector.prototype.times = function(k) {
return new pv.Vector(this.x * k, this.y * k);
};
/**
* Returns this vector plus the vector v: ⟨x + v.x, y +
* v.y⟩. If only one argument is specified, it is interpreted as the
* vector v.
*
* @param {number} x the x coordinate to add.
* @param {number} y the y coordinate to add.
* @returns {pv.Vector} a new vector.
*/
pv.Vector.prototype.plus = function(x, y) {
return (arguments.length == 1)
? new pv.Vector(this.x + x.x, this.y + x.y)
: new pv.Vector(this.x + x, this.y + y);
};
/**
* Returns this vector minus the vector v: ⟨x - v.x, y -
* v.y⟩. If only one argument is specified, it is interpreted as the
* vector v.
*
* @param {number} x the x coordinate to subtract.
* @param {number} y the y coordinate to subtract.
* @returns {pv.Vector} a new vector.
*/
pv.Vector.prototype.minus = function(x, y) {
return (arguments.length == 1)
? new pv.Vector(this.x - x.x, this.y - x.y)
: new pv.Vector(this.x - x, this.y - y);
};
/**
* Returns the dot product of this vector and the vector v: x * v.x +
* y * v.y. If only one argument is specified, it is interpreted as the
* vector v.
*
* @param {number} x the x coordinate to dot.
* @param {number} y the y coordinate to dot.
* @returns {number} a dot product.
*/
pv.Vector.prototype.dot = function(x, y) {
return (arguments.length == 1)
? this.x * x.x + this.y * x.y
: this.x * x + this.y * y;
};
// TODO code-sharing between scales
/**
* @ignore
* @class
*/
pv.Scale = function() {};
/**
* @private Returns a function that interpolators from the start value to the
* end value, given a parameter t in [0, 1].
*
* @param start the start value.
* @param end the end value.
*/
pv.Scale.interpolator = function(start, end) {
if (typeof start == "number") {
return function(t) {
return t * (end - start) + start;
};
}
/* For now, assume color. */
start = pv.color(start).rgb();
end = pv.color(end).rgb();
return function(t) {
var a = start.a * (1 - t) + end.a * t;
if (a < 1e-5) a = 0; // avoid scientific notation
return (start.a == 0) ? pv.rgb(end.r, end.g, end.b, a)
: ((end.a == 0) ? pv.rgb(start.r, start.g, start.b, a)
: pv.rgb(
Math.round(start.r * (1 - t) + end.r * t),
Math.round(start.g * (1 - t) + end.g * t),
Math.round(start.b * (1 - t) + end.b * t), a));
};
};
/**
* Returns a linear scale for the specified domain. The arguments to this
* constructor are optional, and equivalent to calling {@link #domain}.
*
* @class Represents a linear scale. Most commonly, a linear scale
* represents a 1-dimensional linear transformation from a numeric domain of
* input data [d0, d1] to a numeric range of
* pixels [r0, r1]. The equation for such a
* scale is:
*
*
f(x) = (x - d0) / (d1 - d0) * * (r1 - r0) + r0* * For example, a linear scale from the domain [0, 100] to range [0, 640]: * *
f(x) = (x - 0) / (100 - 0) * (640 - 0) + 0* * Thus, saying * *
* f(x) = x / 100 * 640
* f(x) = x * 6.4
*
.height(function(d) d * 6.4)* * is identical to * *
.height(pv.Scale.linear(0, 100).range(0, 640))* * As you can see, scales do not always make code smaller, but they should make * code more explicit and easier to maintain. In addition to readability, scales * offer several useful features: * *
1. The range can be expressed in colors, rather than pixels. Changing the * example above to * *
.fillStyle(pv.Scale.linear(0, 100).range("red", "green"))* * will cause it to fill the marks "red" on an input value of 0, "green" on an * input value of 100, and some color in-between for intermediate values. * *
2. The domain and range can be subdivided for a "poly-linear" * transformation. For example, you may want a diverging color scale that is * increasingly red for negative values, and increasingly green for positive * values: * *
.fillStyle(pv.Scale.linear(-1, 0, 1).range("red", "white", "green"))* * The domain can be specified as a series of n monotonically-increasing * values; the range must also be specified as n values, resulting in * n - 1 contiguous linear scales. * *
3. Linear scales can be inverted for interaction. The {@link #invert} * method takes a value in the output range, and returns the corresponding value * in the input domain. This is frequently used to convert the mouse location * (see {@link pv.Mark#mouse}) to a value in the input domain. Note that * inversion is only supported for numeric ranges, and not colors. * *
4. A scale can be queried for reasonable "tick" values. The {@link #ticks} * method provides a convenient way to get a series of evenly-spaced rounded * values in the input domain. Frequently these are used in conjunction with * {@link pv.Rule} to display tick marks or grid lines. * *
5. A scale can be "niced" to extend the domain to suitable rounded * numbers. If the minimum and maximum of the domain are messy because they are * derived from data, you can use {@link #nice} to round these values down and * up to even numbers. * * @param {number...} domain... domain values. * @returns {pv.Scale.linear} a linear scale. */ pv.Scale.linear = function() { var d = [0, 1], r = [0, 1], i = [pv.identity], precision = 0; /** @private */ function scale(x) { var j = pv.search(d, x); if (j < 0) j = -j - 2; j = Math.max(0, Math.min(i.length - 1, j)); return i[j]((x - d[j]) / (d[j + 1] - d[j])); } /** * Sets or gets the input domain. This method can be invoked several ways: * *
1. domain(min, ..., max) * *
Specifying the domain as a series of numbers is the most explicit and * recommended approach. Most commonly, two numbers are specified: the minimum * and maximum value. However, for a diverging scale, or other subdivided * poly-linear scales, multiple values can be specified. Values can be derived * from data using {@link pv.min} and {@link pv.max}. For example: * *
.domain(0, pv.max(array))* * An alternative method for deriving minimum and maximum values from data * follows. * *
2. domain(array, minf, maxf) * *
When both the minimum and maximum value are derived from data, the * arguments to the domain method can be specified as the array of * data, followed by zero, one or two accessor functions. For example, if the * array of data is just an array of numbers: * *
.domain(array)* * On the other hand, if the array elements are objects representing stock * values per day, and the domain should consider the stock's daily low and * daily high: * *
.domain(array, function(d) d.low, function(d) d.high)* * The first method of setting the domain is preferred because it is more * explicit; setting the domain using this second method should be used only * if brevity is required. * *
3. domain() * *
Invoking the domain method with no arguments returns the * current domain as an array of numbers. * * @function * @name pv.Scale.linear.prototype.domain * @param {number...} domain... domain values. * @returns {pv.Scale.linear} this, or the current domain. */ scale.domain = function(array, min, max) { if (arguments.length) { if (array instanceof Array) { if (arguments.length < 2) min = pv.identity; if (arguments.length < 3) max = min; d = [pv.min(array, min), pv.max(array, max)]; } else { d = Array.prototype.slice.call(arguments); } return this; } return d; }; /** * Sets or gets the output range. This method can be invoked several ways: * *
1. range(min, ..., max) * *
The range may be specified as a series of numbers or colors. Most * commonly, two numbers are specified: the minimum and maximum pixel values. * For a color scale, values may be specified as {@link pv.Color}s or * equivalent strings. For a diverging scale, or other subdivided poly-linear * scales, multiple values can be specified. For example: * *
.range("red", "white", "green")* *
Currently, only numbers and colors are supported as range values. The * number of range values must exactly match the number of domain values, or * the behavior of the scale is undefined. * *
2. range() * *
Invoking the range method with no arguments returns the current * range as an array of numbers or colors. * * @function * @name pv.Scale.linear.prototype.range * @param {...} range... range values. * @returns {pv.Scale.linear} this, or the current range. */ scale.range = function() { if (arguments.length) { r = Array.prototype.slice.call(arguments); i = []; for (var j = 0; j < r.length - 1; j++) { i.push(pv.Scale.interpolator(r[j], r[j + 1])); } return this; } return r; }; /** * Inverts the specified value in the output range, returning the * corresponding value in the input domain. This is frequently used to convert * the mouse location (see {@link pv.Mark#mouse}) to a value in the input * domain. Inversion is only supported for numeric ranges, and not colors. * *
Note that this method does not do any rounding or bounds checking. If * the input domain is discrete (e.g., an array index), the returned value * should be rounded. If the specified y value is outside the range, * the returned value may be equivalently outside the input domain. * * @function * @name pv.Scale.linear.prototype.invert * @param {number} y a value in the output range (a pixel location). * @returns {number} a value in the input domain. */ scale.invert = function(y) { var j = pv.search(r, y); if (j < 0) j = -j - 2; j = Math.max(0, Math.min(i.length - 1, j)); return (y - r[j]) / (r[j + 1] - r[j]) * (d[j + 1] - d[j]) + d[j]; }; /** * Returns an array of evenly-spaced, suitably-rounded values in the input * domain. This method attempts to return between 5 and 10 tick values. These * values are frequently used in conjunction with {@link pv.Rule} to display * tick marks or grid lines. * * @function * @name pv.Scale.linear.prototype.ticks * @returns {number[]} an array input domain values to use as ticks. */ scale.ticks = function() { var min = d[0], max = d[d.length - 1], span = max - min, step = pv.logCeil(span / 10, 10); if (span / step < 2) step /= 5; else if (span / step < 5) step /= 2; var start = Math.ceil(min / step) * step, end = Math.floor(max / step) * step; precision = Math.max(0, -Math.floor(pv.log(step, 10) + .01)); return pv.range(start, end + step, step); }; /** * Formats the specified tick value using the appropriate precision, based on * the step interval between tick marks. * * @function * @name pv.Scale.linear.prototype.tickFormat * @param {number} t a tick value. * @return {string} a formatted tick value. */ scale.tickFormat = function(t) { return t.toFixed(precision); }; /** * "Nices" this scale, extending the bounds of the input domain to * evenly-rounded values. Nicing is useful if the domain is computed * dynamically from data, and may be irregular. For example, given a domain of * [0.20147987687960267, 0.996679553296417], a call to nice() might * extend the domain to [0.2, 1]. * *
This method must be invoked each time after setting the domain. * * @function * @name pv.Scale.linear.prototype.nice * @returns {pv.Scale.linear} this. */ scale.nice = function() { var min = d[0], max = d[d.length - 1], step = Math.pow(10, Math.round(Math.log(max - min) / Math.log(10)) - 1); d = [Math.floor(min / step) * step, Math.ceil(max / step) * step]; return this; }; /** * Returns a view of this scale by the specified accessor function f. * Given a scale y, y.by(function(d) d.foo) is equivalent to * function(d) y(d.foo). * *
This method is provided for convenience, such that scales can be * succinctly defined inline. For example, given an array of data elements * that have a score attribute with the domain [0, 1], the height * property could be specified as: * *
.height(pv.Scale.linear().range(0, 480).by(function(d) d.score))* * This is equivalent to: * *
.height(function(d) d.score * 480)* * This method should be used judiciously; it is typically more clear to * invoke the scale directly, passing in the value to be scaled. * * @function * @name pv.Scale.linear.prototype.by * @param {function} f an accessor function. * @returns {pv.Scale.linear} a view of this scale by the specified accessor * function. */ scale.by = function(f) { function by() { return scale(f.apply(this, arguments)); } for (var method in scale) by[method] = scale[method]; return by; }; scale.domain.apply(scale, arguments); return scale; }; /** * Returns a log scale for the specified domain. The arguments to this * constructor are optional, and equivalent to calling {@link #domain}. * * @class Represents a log scale.
f(x) = (log(x) - log(d0)) / (log(d1) - * log(d0)) * (r1 - r0) + * r0* * where log(x) represents the zero-symmetric logarthim of x using * the scale's associated base (default: 10, see {@link pv.logSymmetric}). For * example, a log scale from the domain [1, 100] to range [0, 640]: * *
f(x) = (log(x) - log(1)) / (log(100) - log(1)) * (640 - 0) + 0* * Thus, saying * *
* f(x) = log(x) / 2 * 640
* f(x) = log(x) * 320
*
.height(function(d) Math.log(d) * 138.974)* * is equivalent to * *
.height(pv.Scale.log(1, 100).range(0, 640))* * As you can see, scales do not always make code smaller, but they should make * code more explicit and easier to maintain. In addition to readability, scales * offer several useful features: * *
1. The range can be expressed in colors, rather than pixels. Changing the * example above to * *
.fillStyle(pv.Scale.log(1, 100).range("red", "green"))* * will cause it to fill the marks "red" on an input value of 1, "green" on an * input value of 100, and some color in-between for intermediate values. * *
2. The domain and range can be subdivided for a "poly-log" * transformation. For example, you may want a diverging color scale that is * increasingly red for small values, and increasingly green for large values: * *
.fillStyle(pv.Scale.log(1, 10, 100).range("red", "white", "green"))* * The domain can be specified as a series of n monotonically-increasing * values; the range must also be specified as n values, resulting in * n - 1 contiguous log scales. * *
3. Log scales can be inverted for interaction. The {@link #invert} method * takes a value in the output range, and returns the corresponding value in the * input domain. This is frequently used to convert the mouse location (see * {@link pv.Mark#mouse}) to a value in the input domain. Note that inversion is * only supported for numeric ranges, and not colors. * *
4. A scale can be queried for reasonable "tick" values. The {@link #ticks} * method provides a convenient way to get a series of evenly-spaced rounded * values in the input domain. Frequently these are used in conjunction with * {@link pv.Rule} to display tick marks or grid lines. * *
5. A scale can be "niced" to extend the domain to suitable rounded * numbers. If the minimum and maximum of the domain are messy because they are * derived from data, you can use {@link #nice} to round these values down and * up to even numbers. * * @param {number...} domain... domain values. * @returns {pv.Scale.log} a log scale. */ pv.Scale.log = function() { var d = [1, 10], l = [0, 1], b = 10, r = [0, 1], i = [pv.identity]; /** @private */ function scale(x) { var j = pv.search(d, x); if (j < 0) j = -j - 2; j = Math.max(0, Math.min(i.length - 1, j)); return i[j]((log(x) - l[j]) / (l[j + 1] - l[j])); } /** @private */ function log(x) { return pv.logSymmetric(x, b); } /** * Sets or gets the input domain. This method can be invoked several ways: * *
1. domain(min, ..., max) * *
Specifying the domain as a series of numbers is the most explicit and * recommended approach. Most commonly, two numbers are specified: the minimum * and maximum value. However, for a diverging scale, or other subdivided * poly-log scales, multiple values can be specified. Values can be derived * from data using {@link pv.min} and {@link pv.max}. For example: * *
.domain(1, pv.max(array))* * An alternative method for deriving minimum and maximum values from data * follows. * *
2. domain(array, minf, maxf) * *
When both the minimum and maximum value are derived from data, the * arguments to the domain method can be specified as the array of * data, followed by zero, one or two accessor functions. For example, if the * array of data is just an array of numbers: * *
.domain(array)* * On the other hand, if the array elements are objects representing stock * values per day, and the domain should consider the stock's daily low and * daily high: * *
.domain(array, function(d) d.low, function(d) d.high)* * The first method of setting the domain is preferred because it is more * explicit; setting the domain using this second method should be used only * if brevity is required. * *
3. domain() * *
Invoking the domain method with no arguments returns the * current domain as an array of numbers. * * @function * @name pv.Scale.log.prototype.domain * @param {number...} domain... domain values. * @returns {pv.Scale.log} this, or the current domain. */ scale.domain = function(array, min, max) { if (arguments.length) { if (array instanceof Array) { if (arguments.length < 2) min = pv.identity; if (arguments.length < 3) max = min; d = [pv.min(array, min), pv.max(array, max)]; } else { d = Array.prototype.slice.call(arguments); } l = d.map(log); return this; } return d; }; /** * @function * @name pv.Scale.log.prototype.range * @param {...} range... range values. * @returns {pv.Scale.log} this. */ scale.range = function() { if (arguments.length) { r = Array.prototype.slice.call(arguments); i = []; for (var j = 0; j < r.length - 1; j++) { i.push(pv.Scale.interpolator(r[j], r[j + 1])); } return this; } return r; }; /** * Sets or gets the output range. This method can be invoked several ways: * *
1. range(min, ..., max) * *
The range may be specified as a series of numbers or colors. Most * commonly, two numbers are specified: the minimum and maximum pixel values. * For a color scale, values may be specified as {@link pv.Color}s or * equivalent strings. For a diverging scale, or other subdivided poly-log * scales, multiple values can be specified. For example: * *
.range("red", "white", "green")* *
Currently, only numbers and colors are supported as range values. The * number of range values must exactly match the number of domain values, or * the behavior of the scale is undefined. * *
2. range() * *
Invoking the range method with no arguments returns the current * range as an array of numbers or colors. * * @function * @name pv.Scale.log.prototype.invert * @param {...} range... range values. * @returns {pv.Scale.log} this, or the current range. */ scale.invert = function(y) { var j = pv.search(r, y); if (j < 0) j = -j - 2; j = Math.max(0, Math.min(i.length - 1, j)); var t = l[j] + (y - r[j]) / (r[j + 1] - r[j]) * (l[j + 1] - l[j]); return (d[j] < 0) ? -Math.pow(b, -t) : Math.pow(b, t); }; /** * Returns an array of evenly-spaced, suitably-rounded values in the input * domain. These values are frequently used in conjunction with {@link * pv.Rule} to display tick marks or grid lines. * * @function * @name pv.Scale.log.prototype.ticks * @returns {number[]} an array input domain values to use as ticks. */ scale.ticks = function() { // TODO: support multiple domains var start = Math.floor(l[0]), end = Math.ceil(l[1]), ticks = []; for (var i = start; i < end; i++) { var x = Math.pow(b, i); if (d[0] < 0) x = -x; for (var j = 1; j < b; j++) { ticks.push(x * j); } } ticks.push(Math.pow(b, end)); if (ticks[0] < d[0]) ticks.shift(); if (ticks[ticks.length - 1] > d[1]) ticks.pop(); return ticks; }; /** * Formats the specified tick value using the appropriate precision, assuming * base 10. * * @function * @name pv.Scale.log.prototype.tickFormat * @param {number} t a tick value. * @return {string} a formatted tick value. */ scale.tickFormat = function(t) { return t.toPrecision(1); }; /** * "Nices" this scale, extending the bounds of the input domain to * evenly-rounded values. This method uses {@link pv.logFloor} and {@link * pv.logCeil}. Nicing is useful if the domain is computed dynamically from * data, and may be irregular. For example, given a domain of * [0.20147987687960267, 0.996679553296417], a call to nice() might * extend the domain to [0.1, 1]. * *
This method must be invoked each time after setting the domain (and * base). * * @function * @name pv.Scale.log.prototype.nice * @returns {pv.Scale.log} this. */ scale.nice = function() { // TODO: support multiple domains d = [pv.logFloor(d[0], b), pv.logCeil(d[1], b)]; l = d.map(log); return this; }; /** * Sets or gets the logarithm base. Defaults to 10. * * @function * @name pv.Scale.log.prototype.base * @param {number} [v] the new base. * @returns {pv.Scale.log} this, or the current base. */ scale.base = function(v) { if (arguments.length) { b = v; l = d.map(log); return this; } return b; }; /** * Returns a view of this scale by the specified accessor function f. * Given a scale y, y.by(function(d) d.foo) is equivalent to * function(d) y(d.foo). * *
This method is provided for convenience, such that scales can be * succinctly defined inline. For example, given an array of data elements * that have a score attribute with the domain [0, 1], the height * property could be specified as: * *
.height(pv.Scale.log().range(0, 480).by(function(d) d.score))* * This is equivalent to: * *
.height(function(d) d.score * 480)* * This method should be used judiciously; it is typically more clear to * invoke the scale directly, passing in the value to be scaled. * * @function * @name pv.Scale.log.prototype.by * @param {function} f an accessor function. * @returns {pv.Scale.log} a view of this scale by the specified accessor * function. */ scale.by = function(f) { function by() { return scale(f.apply(this, arguments)); } for (var method in scale) by[method] = scale[method]; return by; }; scale.domain.apply(scale, arguments); return scale; }; /** * Returns an ordinal scale for the specified domain. The arguments to this * constructor are optional, and equivalent to calling {@link #domain}. * * @class Represents an ordinal scale. An ordinal scale represents a * pairwise mapping from n discrete values in the input domain to * n discrete values in the output range. For example, an ordinal scale * might map a domain of species ["setosa", "versicolor", "virginica"] to colors * ["red", "green", "blue"]. Thus, saying * *
.fillStyle(function(d) { * switch (d.species) { * case "setosa": return "red"; * case "versicolor": return "green"; * case "virginica": return "blue"; * } * })* * is equivalent to * *
.fillStyle(pv.Scale.ordinal("setosa", "versicolor", "virginica") * .range("red", "green", "blue") * .by(function(d) d.species))* * If the mapping from species to color does not need to be specified * explicitly, the domain can be omitted. In this case it will be inferred * lazily from the data: * *
.fillStyle(pv.colors("red", "green", "blue") * .by(function(d) d.species))* * When the domain is inferred, the first time the scale is invoked, the first * element from the range will be returned. Subsequent calls with unique values * will return subsequent elements from the range. If the inferred domain grows * larger than the range, range values will be reused. However, it is strongly * recommended that the domain and the range contain the same number of * elements. * *
A range can be discretized from a continuous interval (e.g., for pixel * positioning) by using {@link #split}, {@link #splitFlush} or * {@link #splitBanded} after the domain has been set. For example, if * states is an array of the fifty U.S. state names, the state name can * be encoded in the left position: * *
.left(pv.Scale.ordinal(states) * .split(0, 640) * .by(function(d) d.state))* *
N.B.: ordinal scales are not invertible (at least not yet), since the * domain and range and discontinuous. A workaround is to use a linear scale. * * @param {...} domain... domain values. * @returns {pv.Scale.ordinal} an ordinal scale. * @see pv.colors */ pv.Scale.ordinal = function() { var d = [], i = {}, r = [], band = 0; /** @private */ function scale(x) { if (!(x in i)) i[x] = d.push(x) - 1; return r[i[x] % r.length]; } /** * Sets or gets the input domain. This method can be invoked several ways: * *
1. domain(values...) * *
Specifying the domain as a series of values is the most explicit and * recommended approach. However, if the domain values are derived from data, * you may find the second method more appropriate. * *
2. domain(array, f) * *
Rather than enumerating the domain values as explicit arguments to this * method, you can specify a single argument of an array. In addition, you can * specify an optional accessor function to extract the domain values from the * array. * *
3. domain() * *
Invoking the domain method with no arguments returns the * current domain as an array. * * @function * @name pv.Scale.ordinal.prototype.domain * @param {...} domain... domain values. * @returns {pv.Scale.ordinal} this, or the current domain. */ scale.domain = function(array, f) { if (arguments.length) { array = (array instanceof Array) ? ((arguments.length > 1) ? map(array, f) : array) : Array.prototype.slice.call(arguments); /* Filter the specified ordinals to their unique values. */ d = []; var seen = {}; for (var j = 0; j < array.length; j++) { var o = array[j]; if (!(o in seen)) { seen[o] = true; d.push(o); } } i = pv.numerate(d); return this; } return d; }; /** * Sets or gets the output range. This method can be invoked several ways: * *
1. range(values...) * *
Specifying the range as a series of values is the most explicit and * recommended approach. However, if the range values are derived from data, * you may find the second method more appropriate. * *
2. range(array, f) * *
Rather than enumerating the range values as explicit arguments to this * method, you can specify a single argument of an array. In addition, you can * specify an optional accessor function to extract the range values from the * array. * *
3. range() * *
Invoking the range method with no arguments returns the * current range as an array. * * @function * @name pv.Scale.ordinal.prototype.range * @param {...} range... range values. * @returns {pv.Scale.ordinal} this, or the current range. */ scale.range = function(array, f) { if (arguments.length) { r = (array instanceof Array) ? ((arguments.length > 1) ? map(array, f) : array) : Array.prototype.slice.call(arguments); if (typeof r[0] == "string") r = r.map(pv.color); return this; } return r; }; /** * Sets the range from the given continuous interval. The interval * [min, max] is subdivided into n equispaced points, * where n is the number of (unique) values in the domain. The first * and last point are offset from the edge of the range by half the distance * between points. * *
This method must be called after the domain is set. * * @function * @name pv.Scale.ordinal.prototype.split * @param {number} min minimum value of the output range. * @param {number} max maximum value of the output range. * @returns {pv.Scale.ordinal} this. * @see #splitFlush * @see #splitBanded */ scale.split = function(min, max) { var step = (max - min) / this.domain().length; r = pv.range(min + step / 2, max, step); return this; }; /** * Sets the range from the given continuous interval. The interval * [min, max] is subdivided into n equispaced points, * where n is the number of (unique) values in the domain. The first * and last point are exactly on the edge of the range. * *
This method must be called after the domain is set. * * @function * @name pv.Scale.ordinal.prototype.splitFlush * @param {number} min minimum value of the output range. * @param {number} max maximum value of the output range. * @returns {pv.Scale.ordinal} this. * @see #split */ scale.splitFlush = function(min, max) { var n = this.domain().length, step = (max - min) / (n - 1); r = (n == 1) ? [(min + max) / 2] : pv.range(min, max + step / 2, step); return this; }; /** * Sets the range from the given continuous interval. The interval * [min, max] is subdivided into n equispaced bands, * where n is the number of (unique) values in the domain. The first * and last band are offset from the edge of the range by the distance between * bands. * *
The band width argument, band, is typically in the range [0, 1] * and defaults to 1. This fraction corresponds to the amount of space in the * range to allocate to the bands, as opposed to padding. A value of 0.5 means * that the band width will be equal to the padding width. The computed * absolute band width can be retrieved from the range as * scale.range().band. * *
If the band width argument is negative, this method will allocate bands * of a fixed width -band, rather than a relative fraction of * the available space. * *
Tip: to inset the bands by a fixed amount p, specify a minimum * value of min + p (or simply p, if min is * 0). Then set the mark width to scale.range().band - p. * *
This method must be called after the domain is set. * * @function * @name pv.Scale.ordinal.prototype.splitBanded * @param {number} min minimum value of the output range. * @param {number} max maximum value of the output range. * @param {number} [band] the fractional band width in [0, 1]; defaults to 1. * @returns {pv.Scale.ordinal} this. * @see #split */ scale.splitBanded = function(min, max, band) { if (arguments.length < 3) band = 1; if (band < 0) { var n = this.domain().length, total = -band * n, remaining = max - min - total, padding = remaining / (n + 1); r = pv.range(min + padding, max, padding - band); r.band = -band; } else { var step = (max - min) / (this.domain().length + (1 - band)); r = pv.range(min + step * (1 - band), max, step); r.band = step * band; } return this; }; /** * Returns a view of this scale by the specified accessor function f. * Given a scale y, y.by(function(d) d.foo) is equivalent to * function(d) y(d.foo). This method should be used judiciously; it * is typically more clear to invoke the scale directly, passing in the value * to be scaled. * * @function * @name pv.Scale.ordinal.prototype.by * @param {function} f an accessor function. * @returns {pv.Scale.ordinal} a view of this scale by the specified accessor * function. */ scale.by = function(f) { function by() { return scale(f.apply(this, arguments)); } for (var method in scale) by[method] = scale[method]; return by; }; scale.domain.apply(scale, arguments); return scale; }; /** * Returns the {@link pv.Color} for the specified color format string. Colors * may have an associated opacity, or alpha channel. Color formats are specified * by CSS Color Modular Level 3, using either in RGB or HSL color space. For * example:
If the format argument is already an instance of Color, * the argument is returned with no further processing. * * @param {string} format the color specification string, such as "#f00". * @returns {pv.Color} the corresponding Color. * @see SVG color * keywords * @see CSS3 color module */ pv.color = function(format) { if (!format || (format == "transparent")) { return pv.rgb(0, 0, 0, 0); } if (format instanceof pv.Color) { return format; } /* Handle hsl, rgb. */ var m1 = /([a-z]+)\((.*)\)/i.exec(format); if (m1) { var m2 = m1[2].split(","), a = 1; switch (m1[1]) { case "hsla": case "rgba": { a = parseFloat(m2[3]); break; } } switch (m1[1]) { case "hsla": case "hsl": { var h = parseFloat(m2[0]), // degrees s = parseFloat(m2[1]) / 100, // percentage l = parseFloat(m2[2]) / 100; // percentage return (new pv.Color.Hsl(h, s, l, a)).rgb(); } case "rgba": case "rgb": { function parse(c) { // either integer or percentage var f = parseFloat(c); return (c[c.length - 1] == '%') ? Math.round(f * 2.55) : f; } var r = parse(m2[0]), g = parse(m2[1]), b = parse(m2[2]); return pv.rgb(r, g, b, a); } } } /* Named colors. */ format = pv.Color.names[format] || format; /* Hexadecimal colors: #rgb and #rrggbb. */ if (format.charAt(0) == "#") { var r, g, b; if (format.length == 4) { r = format.charAt(1); r += r; g = format.charAt(2); g += g; b = format.charAt(3); b += b; } else if (format.length == 7) { r = format.substring(1, 3); g = format.substring(3, 5); b = format.substring(5, 7); } return pv.rgb(parseInt(r, 16), parseInt(g, 16), parseInt(b, 16), 1); } /* Otherwise, assume named colors. TODO allow lazy conversion to RGB. */ return new pv.Color(format, 1); }; /** * Constructs a color with the specified color format string and opacity. This * constructor should not be invoked directly; use {@link pv.color} instead. * * @class Represents an abstract (possibly translucent) color. The color is * divided into two parts: the color attribute, an opaque color format * string, and the opacity attribute, a float in [0, 1]. The color * space is dependent on the implementing class; all colors support the * {@link #rgb} method to convert to RGB color space for interpolation. * *
See also the Color guide. * * @param {string} color an opaque color format string, such as "#f00". * @param {number} opacity the opacity, in [0,1]. * @see pv.color */ pv.Color = function(color, opacity) { /** * An opaque color format string, such as "#f00". * * @type string * @see SVG color * keywords * @see CSS3 color module */ this.color = color; /** * The opacity, a float in [0, 1]. * * @type number */ this.opacity = opacity; }; /** * Returns a new color that is a brighter version of this color. The behavior of * this method may vary slightly depending on the underlying color space. * Although brighter and darker are inverse operations, the results of a series * of invocations of these two methods might be inconsistent because of rounding * errors. * * @param [k] {number} an optional scale factor; defaults to 1. * @see #darker * @returns {pv.Color} a brighter color. */ pv.Color.prototype.brighter = function(k) { return this.rgb().brighter(k); }; /** * Returns a new color that is a brighter version of this color. The behavior of * this method may vary slightly depending on the underlying color space. * Although brighter and darker are inverse operations, the results of a series * of invocations of these two methods might be inconsistent because of rounding * errors. * * @param [k] {number} an optional scale factor; defaults to 1. * @see #brighter * @returns {pv.Color} a darker color. */ pv.Color.prototype.darker = function(k) { return this.rgb().darker(k); }; /** * Constructs a new RGB color with the specified channel values. * * @param {number} r the red channel, an integer in [0,255]. * @param {number} g the green channel, an integer in [0,255]. * @param {number} b the blue channel, an integer in [0,255]. * @param {number} [a] the alpha channel, a float in [0,1]. * @returns pv.Color.Rgb */ pv.rgb = function(r, g, b, a) { return new pv.Color.Rgb(r, g, b, (arguments.length == 4) ? a : 1); }; /** * Constructs a new RGB color with the specified channel values. * * @class Represents a color in RGB space. * * @param {number} r the red channel, an integer in [0,255]. * @param {number} g the green channel, an integer in [0,255]. * @param {number} b the blue channel, an integer in [0,255]. * @param {number} a the alpha channel, a float in [0,1]. * @extends pv.Color */ pv.Color.Rgb = function(r, g, b, a) { pv.Color.call(this, a ? ("rgb(" + r + "," + g + "," + b + ")") : "none", a); /** * The red channel, an integer in [0, 255]. * * @type number */ this.r = r; /** * The green channel, an integer in [0, 255]. * * @type number */ this.g = g; /** * The blue channel, an integer in [0, 255]. * * @type number */ this.b = b; /** * The alpha channel, a float in [0, 1]. * * @type number */ this.a = a; }; pv.Color.Rgb.prototype = pv.extend(pv.Color); /** * Constructs a new RGB color with the same green, blue and alpha channels as * this color, with the specified red channel. * * @param {number} r the red channel, an integer in [0,255]. */ pv.Color.Rgb.prototype.red = function(r) { return pv.rgb(r, this.g, this.b, this.a); }; /** * Constructs a new RGB color with the same red, blue and alpha channels as this * color, with the specified green channel. * * @param {number} g the green channel, an integer in [0,255]. */ pv.Color.Rgb.prototype.green = function(g) { return pv.rgb(this.r, g, this.b, this.a); }; /** * Constructs a new RGB color with the same red, green and alpha channels as * this color, with the specified blue channel. * * @param {number} b the blue channel, an integer in [0,255]. */ pv.Color.Rgb.prototype.blue = function(b) { return pv.rgb(this.r, this.g, b, this.a); }; /** * Constructs a new RGB color with the same red, green and blue channels as this * color, with the specified alpha channel. * * @param {number} a the alpha channel, a float in [0,1]. */ pv.Color.Rgb.prototype.alpha = function(a) { return pv.rgb(this.r, this.g, this.b, a); }; /** * Returns the RGB color equivalent to this color. This method is abstract and * must be implemented by subclasses. * * @returns {pv.Color.Rgb} an RGB color. * @function * @name pv.Color.prototype.rgb */ /** * Returns this. * * @returns {pv.Color.Rgb} this. */ pv.Color.Rgb.prototype.rgb = function() { return this; }; /** * Returns a new color that is a brighter version of this color. This method * applies an arbitrary scale factor to each of the three RGB components of this * color to create a brighter version of this color. Although brighter and * darker are inverse operations, the results of a series of invocations of * these two methods might be inconsistent because of rounding errors. * * @param [k] {number} an optional scale factor; defaults to 1. * @see #darker * @returns {pv.Color.Rgb} a brighter color. */ pv.Color.Rgb.prototype.brighter = function(k) { k = Math.pow(0.7, arguments.length ? k : 1); var r = this.r, g = this.g, b = this.b, i = 30; if (!r && !g && !b) return pv.rgb(i, i, i, this.a); if (r && (r < i)) r = i; if (g && (g < i)) g = i; if (b && (b < i)) b = i; return pv.rgb( Math.min(255, Math.floor(r / k)), Math.min(255, Math.floor(g / k)), Math.min(255, Math.floor(b / k)), this.a); }; /** * Returns a new color that is a darker version of this color. This method * applies an arbitrary scale factor to each of the three RGB components of this * color to create a darker version of this color. Although brighter and darker * are inverse operations, the results of a series of invocations of these two * methods might be inconsistent because of rounding errors. * * @param [k] {number} an optional scale factor; defaults to 1. * @see #brighter * @returns {pv.Color.Rgb} a darker color. */ pv.Color.Rgb.prototype.darker = function(k) { k = Math.pow(0.7, arguments.length ? k : 1); return pv.rgb( Math.max(0, Math.floor(k * this.r)), Math.max(0, Math.floor(k * this.g)), Math.max(0, Math.floor(k * this.b)), this.a); }; /** * Constructs a new HSL color with the specified values. * * @param {number} h the hue, an integer in [0, 360]. * @param {number} s the saturation, a float in [0, 1]. * @param {number} l the lightness, a float in [0, 1]. * @param {number} [a] the opacity, a float in [0, 1]. * @returns pv.Color.Hsl */ pv.hsl = function(h, s, l, a) { return new pv.Color.Hsl(h, s, l, (arguments.length == 4) ? a : 1); }; /** * Constructs a new HSL color with the specified values. * * @class Represents a color in HSL space. * * @param {number} h the hue, an integer in [0, 360]. * @param {number} s the saturation, a float in [0, 1]. * @param {number} l the lightness, a float in [0, 1]. * @param {number} a the opacity, a float in [0, 1]. * @extends pv.Color */ pv.Color.Hsl = function(h, s, l, a) { pv.Color.call(this, "hsl(" + h + "," + (s * 100) + "%," + (l * 100) + "%)", a); /** * The hue, an integer in [0, 360]. * * @type number */ this.h = h; /** * The saturation, a float in [0, 1]. * * @type number */ this.s = s; /** * The lightness, a float in [0, 1]. * * @type number */ this.l = l; /** * The opacity, a float in [0, 1]. * * @type number */ this.a = a; }; pv.Color.Hsl.prototype = pv.extend(pv.Color); /** * Constructs a new HSL color with the same saturation, lightness and alpha as * this color, and the specified hue. * * @param {number} h the hue, an integer in [0, 360]. */ pv.Color.Hsl.prototype.hue = function(h) { return pv.hsl(h, this.s, this.l, this.a); }; /** * Constructs a new HSL color with the same hue, lightness and alpha as this * color, and the specified saturation. * * @param {number} s the saturation, a float in [0, 1]. */ pv.Color.Hsl.prototype.saturation = function(s) { return pv.hsl(this.h, s, this.l, this.a); }; /** * Constructs a new HSL color with the same hue, saturation and alpha as this * color, and the specified lightness. * * @param {number} l the lightness, a float in [0, 1]. */ pv.Color.Hsl.prototype.lightness = function(l) { return pv.hsl(this.h, this.s, l, this.a); }; /** * Constructs a new HSL color with the same hue, saturation and lightness as * this color, and the specified alpha. * * @param {number} a the opacity, a float in [0, 1]. */ pv.Color.Hsl.prototype.alpha = function(a) { return pv.hsl(this.h, this.s, this.l, a); }; /** * Returns the RGB color equivalent to this HSL color. * * @returns {pv.Color.Rgb} an RGB color. */ pv.Color.Hsl.prototype.rgb = function() { var h = this.h, s = this.s, l = this.l; /* Some simple corrections for h, s and l. */ h = h % 360; if (h < 0) h += 360; s = Math.max(0, Math.min(s, 1)); l = Math.max(0, Math.min(l, 1)); /* From FvD 13.37, CSS Color Module Level 3 */ var m2 = (l <= .5) ? (l * (1 + s)) : (l + s - l * s); var m1 = 2 * l - m2; function v(h) { if (h > 360) h -= 360; else if (h < 0) h += 360; if (h < 60) return m1 + (m2 - m1) * h / 60; if (h < 180) return m2; if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60; return m1; } function vv(h) { return Math.round(v(h) * 255); } return pv.rgb(vv(h + 120), vv(h), vv(h - 120), this.a); }; /** * @private SVG color keywords, per CSS Color Module Level 3. * * @see SVG color * keywords */ pv.Color.names = { aliceblue: "#f0f8ff", antiquewhite: "#faebd7", aqua: "#00ffff", aquamarine: "#7fffd4", azure: "#f0ffff", beige: "#f5f5dc", bisque: "#ffe4c4", black: "#000000", blanchedalmond: "#ffebcd", blue: "#0000ff", blueviolet: "#8a2be2", brown: "#a52a2a", burlywood: "#deb887", cadetblue: "#5f9ea0", chartreuse: "#7fff00", chocolate: "#d2691e", coral: "#ff7f50", cornflowerblue: "#6495ed", cornsilk: "#fff8dc", crimson: "#dc143c", cyan: "#00ffff", darkblue: "#00008b", darkcyan: "#008b8b", darkgoldenrod: "#b8860b", darkgray: "#a9a9a9", darkgreen: "#006400", darkgrey: "#a9a9a9", darkkhaki: "#bdb76b", darkmagenta: "#8b008b", darkolivegreen: "#556b2f", darkorange: "#ff8c00", darkorchid: "#9932cc", darkred: "#8b0000", darksalmon: "#e9967a", darkseagreen: "#8fbc8f", darkslateblue: "#483d8b", darkslategray: "#2f4f4f", darkslategrey: "#2f4f4f", darkturquoise: "#00ced1", darkviolet: "#9400d3", deeppink: "#ff1493", deepskyblue: "#00bfff", dimgray: "#696969", dimgrey: "#696969", dodgerblue: "#1e90ff", firebrick: "#b22222", floralwhite: "#fffaf0", forestgreen: "#228b22", fuchsia: "#ff00ff", gainsboro: "#dcdcdc", ghostwhite: "#f8f8ff", gold: "#ffd700", goldenrod: "#daa520", gray: "#808080", green: "#008000", greenyellow: "#adff2f", grey: "#808080", honeydew: "#f0fff0", hotpink: "#ff69b4", indianred: "#cd5c5c", indigo: "#4b0082", ivory: "#fffff0", khaki: "#f0e68c", lavender: "#e6e6fa", lavenderblush: "#fff0f5", lawngreen: "#7cfc00", lemonchiffon: "#fffacd", lightblue: "#add8e6", lightcoral: "#f08080", lightcyan: "#e0ffff", lightgoldenrodyellow: "#fafad2", lightgray: "#d3d3d3", lightgreen: "#90ee90", lightgrey: "#d3d3d3", lightpink: "#ffb6c1", lightsalmon: "#ffa07a", lightseagreen: "#20b2aa", lightskyblue: "#87cefa", lightslategray: "#778899", lightslategrey: "#778899", lightsteelblue: "#b0c4de", lightyellow: "#ffffe0", lime: "#00ff00", limegreen: "#32cd32", linen: "#faf0e6", magenta: "#ff00ff", maroon: "#800000", mediumaquamarine: "#66cdaa", mediumblue: "#0000cd", mediumorchid: "#ba55d3", mediumpurple: "#9370db", mediumseagreen: "#3cb371", mediumslateblue: "#7b68ee", mediumspringgreen: "#00fa9a", mediumturquoise: "#48d1cc", mediumvioletred: "#c71585", midnightblue: "#191970", mintcream: "#f5fffa", mistyrose: "#ffe4e1", moccasin: "#ffe4b5", navajowhite: "#ffdead", navy: "#000080", oldlace: "#fdf5e6", olive: "#808000", olivedrab: "#6b8e23", orange: "#ffa500", orangered: "#ff4500", orchid: "#da70d6", palegoldenrod: "#eee8aa", palegreen: "#98fb98", paleturquoise: "#afeeee", palevioletred: "#db7093", papayawhip: "#ffefd5", peachpuff: "#ffdab9", peru: "#cd853f", pink: "#ffc0cb", plum: "#dda0dd", powderblue: "#b0e0e6", purple: "#800080", red: "#ff0000", rosybrown: "#bc8f8f", royalblue: "#4169e1", saddlebrown: "#8b4513", salmon: "#fa8072", sandybrown: "#f4a460", seagreen: "#2e8b57", seashell: "#fff5ee", sienna: "#a0522d", silver: "#c0c0c0", skyblue: "#87ceeb", slateblue: "#6a5acd", slategray: "#708090", slategrey: "#708090", snow: "#fffafa", springgreen: "#00ff7f", steelblue: "#4682b4", tan: "#d2b48c", teal: "#008080", thistle: "#d8bfd8", tomato: "#ff6347", turquoise: "#40e0d0", violet: "#ee82ee", wheat: "#f5deb3", white: "#ffffff", whitesmoke: "#f5f5f5", yellow: "#ffff00", yellowgreen: "#9acd32" }; /** * Returns a new categorical color encoding using the specified colors. The * arguments to this method are an array of colors; see {@link pv.color}. For * example, to create a categorical color encoding using the species * attribute: * *
pv.colors("red", "green", "blue").by(function(d) d.species)* * The result of this expression can be used as a fill- or stroke-style * property. This assumes that the data's species attribute is a * string. * * @param {string} colors... categorical colors. * @see pv.Scale.ordinal * @returns {pv.Scale.ordinal} an ordinal color scale. */ pv.colors = function() { var scale = pv.Scale.ordinal(); scale.range.apply(scale, arguments); return scale; }; /** * A collection of standard color palettes for categorical encoding. * * @namespace A collection of standard color palettes for categorical encoding. */ pv.Colors = {}; /** * Returns a new 10-color scheme. The arguments to this constructor are * optional, and equivalent to calling {@link pv.Scale.OrdinalScale#domain}. The * following colors are used: * *
Concrete mark types include familiar visual elements such as bars, lines * and labels. Although a bar mark may be used to construct a bar chart, marks * know nothing about charts; it is only through their specification and * composition that charts are produced. These building blocks permit many * combinatorial possibilities. * *
Marks are associated with data: a mark is generated once per * associated datum, mapping the datum to visual properties such as * position and color. Thus, a single mark specification represents a set of * visual elements that share the same data and visual encoding. The type of * mark defines the names of properties and their meaning. A property may be * static, ignoring the associated datum and returning a constant; or, it may be * dynamic, derived from the associated datum or index. Such dynamic encodings * can be specified succinctly using anonymous functions. Special properties * called event handlers can be registered to add interactivity. * *
Protovis uses inheritance to simplify the specification of related * marks: a new mark can be derived from an existing mark, inheriting its * properties. The new mark can then override properties to specify new * behavior, potentially in terms of the old behavior. In this way, the old mark * serves as the prototype for the new mark. Most mark types share the * same basic properties for consistency and to facilitate inheritance. * *
The prioritization of redundant properties is as follows:
While most properties are variable, some mark types, such as lines * and areas, generate a single visual element rather than a distinct visual * element per datum. With these marks, some properties may be fixed. * Fixed properties can vary per mark, but not per datum! These * properties are evaluated solely for the first (0-index) datum, and typically * are specified as a constant. However, it is valid to use a function if the * property varies between panels or is dynamically generated. * *
See also the Protovis guide. */ pv.Mark = function() { /* * TYPE 0 constant defs * TYPE 1 function defs * TYPE 2 constant properties * TYPE 3 function properties * in order of evaluation! */ this.$properties = []; }; /** @private TOOD */ pv.Mark.prototype.properties = {}; /** * @private Defines and registers a property method for the property with the * given name. This method should be called on a mark class prototype to define * each exposed property. (Note this refers to the JavaScript * prototype, not the Protovis mark prototype, which is the {@link * #proto} field.) * *
The created property method supports several modes of invocation:
m.height(function(d) d * 100);* * The expression d * 100 will be evaluated for the height property of * each mark instance. The return value of the property method (e.g., * m.height) is this mark (m)).
* *
* *
m.top(function() this.proto.top() + 10);* * Note that the index of the mark being evaluated (in the above example, * this.proto) is inherited from the Mark class and set by * this mark. So, if the fifth element's top property is being evaluated, the * fifth instance of this.proto will similarly be queried for the value * of its top property. If the mark being evaluated has a different number of * instances, or its data is unrelated, the behavior of this method is * undefined. In these cases it may be better to index the scene * explicitly to specify the exact instance. * *
Property names should follow standard JavaScript method naming * conventions, using lowerCamel-style capitalization. * *
In addition to creating the property method, every property is registered * in the {@link #properties} map on the prototype. Although this is an * instance field, it is considered immutable and shared by all instances of a * given mark type. The properties map can be queried to see if a mark * type defines a particular property, such as width or height. * * @param {string} name the property name. */ pv.Mark.prototype.property = function(name) { if (!this.hasOwnProperty("properties")) { this.properties = pv.extend(this.properties); } this.properties[name] = true; /* * Define the setter-getter globally, since the default behavior should be the * same for all properties, and since the Protovis inheritance chain is * independent of the JavaScript inheritance chain. For example, anchors * define a "name" property that is evaluated on derived marks, even though * those marks don't normally have a name. */ pv.Mark.prototype[name] = function(v) { if (arguments.length) { this.$properties.push({ name: name, type: (typeof v == "function") ? 3 : 2, value: v }); return this; } return this.scene[this.index][name]; }; return this; }; /* Define all global properties. */ pv.Mark.prototype .property("data") .property("visible") .property("left") .property("right") .property("top") .property("bottom") .property("cursor") .property("title") .property("reverse"); /** * The mark type; a lower camelCase name. The type name controls rendering * behavior, and unless the rendering engine is extended, must be one of the * built-in concrete mark types: area, bar, dot, image, label, line, panel, * rule, or wedge. * * @type string * @name pv.Mark.prototype.type */ /** * The mark prototype, possibly undefined, from which to inherit property * functions. The mark prototype is not necessarily of the same type as this * mark. Any properties defined on this mark will override properties inherited * either from the prototype or from the type-specific defaults. * * @type pv.Mark * @name pv.Mark.prototype.proto */ /** * The enclosing parent panel. The parent panel is generally undefined only for * the root panel; however, it is possible to create "offscreen" marks that are * used only for inheritance purposes. * * @type pv.Panel * @name pv.Mark.prototype.parent */ /** * The child index. -1 if the enclosing parent panel is null; otherwise, the * zero-based index of this mark into the parent panel's children array. * * @type number */ pv.Mark.prototype.childIndex = -1; /** * The mark index. The value of this field depends on which instance (i.e., * which element of the data array) is currently being evaluated. During the * build phase, the index is incremented over each datum; when handling events, * the index is set to the instance that triggered the event. * * @type number */ pv.Mark.prototype.index = -1; /** * The scene graph. The scene graph is an array of objects; each object (or * "node") corresponds to an instance of this mark and an element in the data * array. The scene graph can be traversed to lookup previously-evaluated * properties. * *
For instance, consider a stacked area chart. The bottom property of the * area can be defined using the cousin instance, which is the current * area instance in the previous instantiation of the parent panel. In this * sample code, * *
new pv.Panel() * .width(150).height(150) * .add(pv.Panel) * .data([[1, 1.2, 1.7, 1.5, 1.7], * [.5, 1, .8, 1.1, 1.3], * [.2, .5, .8, .9, 1]]) * .add(pv.Area) * .data(function(d) d) * .bottom(function() { * var c = this.cousin(); * return c ? (c.bottom + c.height) : 0; * }) * .height(function(d) d * 40) * .left(function() this.index * 35) * .root.render();* * the bottom property is computed based on the upper edge of the corresponding * datum in the previous series. The area's parent panel is instantiated once * per series, so the cousin refers to the previous (below) area mark. (Note * that the position of the upper edge is not the same as the top property, * which refers to the top margin: the distance from the top edge of the panel * to the top edge of the mark.) * * @see #first * @see #last * @see #sibling * @see #cousin * @name pv.Mark.prototype.scene */ /** * The root parent panel. This may be undefined for "offscreen" marks that are * created for inheritance purposes only. * * @type pv.Panel * @name pv.Mark.prototype.root */ /** * The data property; an array of objects. The size of the array determines the * number of marks that will be instantiated; each element in the array will be * passed to property functions to compute the property values. Typically, the * data property is specified as a constant array, such as * *
m.data([1, 2, 3, 4, 5]);* * However, it is perfectly acceptable to define the data property as a * function. This function might compute the data dynamically, allowing * different data to be used per enclosing panel. For instance, in the stacked * area graph example (see {@link #scene}), the data function on the area mark * dereferences each series. * * @type array * @name pv.Mark.prototype.data */ /** * The visible property; a boolean determining whether or not the mark instance * is visible. If a mark instance is not visible, its other properties will not * be evaluated. Similarly, for panels no child marks will be rendered. * * @type boolean * @name pv.Mark.prototype.visible */ /** * The left margin; the distance, in pixels, between the left edge of the * enclosing panel and the left edge of this mark. Note that in some cases this * property may be redundant with the right property, or with the conjunction of * right and width. * * @type number * @name pv.Mark.prototype.left */ /** * The right margin; the distance, in pixels, between the right edge of the * enclosing panel and the right edge of this mark. Note that in some cases this * property may be redundant with the left property, or with the conjunction of * left and width. * * @type number * @name pv.Mark.prototype.right */ /** * The top margin; the distance, in pixels, between the top edge of the * enclosing panel and the top edge of this mark. Note that in some cases this * property may be redundant with the bottom property, or with the conjunction * of bottom and height. * * @type number * @name pv.Mark.prototype.top */ /** * The bottom margin; the distance, in pixels, between the bottom edge of the * enclosing panel and the bottom edge of this mark. Note that in some cases * this property may be redundant with the top property, or with the conjunction * of top and height. * * @type number * @name pv.Mark.prototype.bottom */ /** * The cursor property; corresponds to the CSS cursor property. This is * typically used in conjunction with event handlers to indicate interactivity. * * @type string * @name pv.Mark.prototype.cursor * @see CSS2 cursor */ /** * The title property; corresponds to the HTML/SVG title property, allowing the * general of simple plain text tooltips. * * @type string * @name pv.Mark.prototype.title */ /** * The reverse property; a boolean determining whether marks are ordered from * front-to-back or back-to-front. SVG does not support explicit z-ordering; * shapes are rendered in the order they appear. Thus, by default, marks are * rendered in data order. Setting the reverse property to false reverses the * order in which they are rendered; however, the properties are still evaluated * (i.e., built) in forward order. * * @type boolean * @name pv.Mark.prototype.reverse */ /** * Default properties for all mark types. By default, the data array is the * parent data as a single-element array; if the data property is not specified, * this causes each mark to be instantiated as a singleton with the parents * datum. The visible property is true by default, and the reverse property is * false. * * @type pv.Mark */ pv.Mark.prototype.defaults = new pv.Mark() .data(function(d) { return [d]; }) .visible(true) .reverse(false) .cursor("") .title(""); /* Private categorical colors for default fill & stroke styles. */ var defaultFillStyle = pv.Colors.category20().by(pv.parent), defaultStrokeStyle = pv.Colors.category10().by(pv.parent); /** * Sets the prototype of this mark to the specified mark. Any properties not * defined on this mark may be inherited from the specified prototype mark, or * its prototype, and so on. The prototype mark need not be the same type of * mark as this mark. (Note that for inheritance to be useful, properties with * the same name on different mark types should have equivalent meaning.) * * @param {pv.Mark} proto the new prototype. * @return {pv.Mark} this mark. * @see #add */ pv.Mark.prototype.extend = function(proto) { this.proto = proto; return this; }; /** * Adds a new mark of the specified type to the enclosing parent panel, whilst * simultaneously setting the prototype of the new mark to be this mark. * * @param {function} type the type of mark to add; a constructor, such as * pv.Bar. * @return {pv.Mark} the new mark. * @see #extend */ pv.Mark.prototype.add = function(type) { return this.parent.add(type).extend(this); }; /** * Defines a local variable on this mark. Local variables are initialized once * per mark (i.e., per parent panel instance), and can be used to store local * state for the mark. Here are a few reasons you might want to use * def: * *
1. To store local state. For example, say you were visualizing employment * statistics, and your root panel had an array of occupations. In a child * panel, you might want to initialize a local scale, and reference it from a * property function: * *
.def("y", function(d) pv.Scale.linear(0, pv.max(d.values)).range(0, h)) * .height(function(d) this.y()(d))* * In this example, this.y() returns the defined local scale. We then * invoke the scale function, passing in the datum, to compute the height. Note * that defs are similar to fixed properties: they are only evaluated once per * parent panel, and this.y() returns a function, rather than * automatically evaluating this function as a property. * *
2. To store temporary state for interaction. Say you have an array of * bars, and you want to color the bar differently if the mouse is over it. Use * def to define a local variable, and event handlers to override this * variable interactively: * *
.def("i", -1) * .event("mouseover", function() this.i(this.index)) * .event("mouseout", function() this.i(-1)) * .fillStyle(function() this.i() == this.index ? "red" : "blue")* * Notice that this.i() can be used both to set the value of i * (when an argument is specified), and to get the value of i (when no * arguments are specified). In this way, it's like other property methods. * *
3. To specify fixed properties efficiently. Sometimes, the value of a * property may be locally a constant, but dependent on parent panel data which * is variable. In this scenario, you can use def to define a property; * it will only get computed once per mark, rather than once per datum. * * @param {string} name the name of the local variable. * @param {function} [value] an optional initializer; may be a constant or a * function. */ pv.Mark.prototype.def = function(name, value) { this.$properties.push({ name: name, type: (typeof value == "function") ? 1 : 0, value: value }); return this; }; /** * Returns an anchor with the specified name. While anchor names are typically * constants, the anchor name is a true property, which means you can specify a * function to compute the anchor name dynamically. See the * {@link pv.Anchor#name} property for details. * * @param {string} name the anchor name; either a string or a property function. * @returns {pv.Anchor} the new anchor. */ pv.Mark.prototype.anchor = function(name) { var anchor = new pv.Anchor().extend(this).name(name); anchor.parent = this.parent; return anchor; }; /** * Returns the anchor target of this mark, if it is derived from an anchor; * otherwise returns null. For example, if a label is derived from a bar anchor, * *
bar.anchor("top").add(pv.Label);* * then property functions on the label can refer to the bar via the * anchorTarget method. This method is also useful for mark types * defining properties on custom anchors. * * @returns {pv.Mark} the anchor target of this mark; possibly null. */ pv.Mark.prototype.anchorTarget = function() { var target = this; while (!(target instanceof pv.Anchor)) { target = target.proto; if (!target) return null; } return target.proto; }; /** * Returns the first instance of this mark in the scene graph. This method can * only be called when the mark is bound to the scene graph (for example, from * an event handler, or within a property function). * * @returns a node in the scene graph. */ pv.Mark.prototype.first = function() { return this.scene[0]; }; /** * Returns the last instance of this mark in the scene graph. This method can * only be called when the mark is bound to the scene graph (for example, from * an event handler, or within a property function). In addition, note that mark * instances are built sequentially, so the last instance of this mark may not * yet be constructed. * * @returns a node in the scene graph. */ pv.Mark.prototype.last = function() { return this.scene[this.scene.length - 1]; }; /** * Returns the previous instance of this mark in the scene graph, or null if * this is the first instance. * * @returns a node in the scene graph, or null. */ pv.Mark.prototype.sibling = function() { return (this.index == 0) ? null : this.scene[this.index - 1]; }; /** * Returns the current instance in the scene graph of this mark, in the previous * instance of the enclosing parent panel. May return null if this instance * could not be found. See the {@link pv.Layout.stack} function for an example * property function using cousin. * * @see pv.Layout.stack * @returns a node in the scene graph, or null. */ pv.Mark.prototype.cousin = function() { var p = this.parent, s = p && p.sibling(); return (s && s.children) ? s.children[this.childIndex][this.index] : null; }; /** * Renders this mark, including recursively rendering all child marks if this is * a panel. */ pv.Mark.prototype.render = function() { /* * Rendering consists of three phases: bind, build and update. The update * phase is decoupled to allow different rendering engines. * * In the bind phase, inherited property definitions are cached so they do not * need to be queried during build. In the build phase, properties are * evaluated, and the scene graph is generated. In the update phase, the scene * is rendered by creating and updating elements and attributes in the SVG * image. No properties are evaluated during the update phase; instead the * values computed previously in the build phase are simply translated into * SVG. */ this.bind(); this.build(); pv.Scene.updateAll(this.scene); }; /** @private Computes the root data stack for the specified mark. */ function argv(mark) { var stack = []; while (mark) { stack.push(mark.scene[mark.index].data); mark = mark.parent; } return stack; } /** @private TODO */ pv.Mark.prototype.bind = function() { var seen = {}, types = [[], [], [], []], data, visible; /** TODO */ function bind(mark) { do { var properties = mark.$properties; for (var i = properties.length - 1; i >= 0 ; i--) { var p = properties[i]; if (!(p.name in seen)) { seen[p.name] = 1; switch (p.name) { case "data": data = p; break; case "visible": visible = p; break; default: types[p.type].push(p); break; } } } } while (mark = mark.proto); } /** TODO */ function def(name) { return function(v) { var defs = this.scene.defs; if (arguments.length) { if (v == undefined) { delete defs.locked[name]; } else { defs.locked[name] = true; } defs.values[name] = v; return this; } else { return defs.values[name]; } }; } /* Scan the proto chain for all defined properties. */ bind(this); bind(this.defaults); types[1].reverse(); types[3].reverse(); /* Any undefined properties are null. */ var mark = this; do for (var name in mark.properties) { if (!(name in seen)) { seen[name] = 1; types[2].push({name: name, type: 2, value: null}); } } while (mark = mark.proto); /* Define setter-getter for inherited defs. */ var defs = types[0].concat(types[1]); for (var i = 0; i < defs.length; i++) { var d = defs[i]; this[d.name] = def(d.name); } /* Setup binds to evaluate constants before functions. */ this.binds = { data: data, visible: visible, defs: defs, properties: pv.blend(types) }; }; /** * @private Evaluates properties and computes implied properties. Properties are * stored in the {@link #scene} array for each instance of this mark. * *
As marks are built recursively, the {@link #index} property is updated to * match the current index into the data array for each mark. Note that the * index property is only set for the mark currently being built and its * enclosing parent panels. The index property for other marks is unset, but is * inherited from the global Mark class prototype. This allows mark * properties to refer to properties on other marks in the same panel * conveniently; however, in general it is better to reference mark instances * specifically through the scene graph rather than depending on the magical * behavior of {@link #index}. * *
The root scene array has a special property, data, which stores * the current data stack. The first element in this stack is the current datum, * followed by the datum of the enclosing parent panel, and so on. The data * stack should not be accessed directly; instead, property functions are passed * the current data stack as arguments. * *
The evaluation of the data and visible properties is * special. The data property is evaluated first; unlike the other * properties, the data stack is from the parent panel, rather than the current * mark, since the data is not defined until the data property is evaluated. * The visisble property is subsequently evaluated for each instance; * only if true will the {@link #buildInstance} method be called, evaluating * other properties and recursively building the scene graph. * *
If this mark is being re-built, any old instances of this mark that no * longer exist (because the new data array contains fewer elements) will be * cleared using {@link #clearInstance}. * * @param parent the instance of the parent panel from the scene graph. */ pv.Mark.prototype.build = function() { var scene = this.scene; if (!scene) { scene = this.scene = []; scene.mark = this; scene.type = this.type; scene.childIndex = this.childIndex; if (this.parent) { scene.parent = this.parent.scene; scene.parentIndex = this.parent.index; } } /* Set the data stack. */ var stack = this.root.scene.data; if (!stack) this.root.scene.data = stack = argv(this.parent); /* Evaluate defs. */ if (this.binds.defs.length) { var defs = scene.defs; if (!defs) scene.defs = defs = {values: {}, locked: {}}; for (var i = 0; i < this.binds.defs.length; i++) { var d = this.binds.defs[i]; if (!(d.name in defs.locked)) { var v = d.value; if (d.type == 1) { property = d.name; v = v.apply(this, stack); } defs.values[d.name] = v; } } } /* Evaluate special data property. */ var data = this.binds.data; switch (data.type) { case 0: case 1: data = defs.values.data; break; case 2: data = data.value; break; case 3: { property = "data"; data = data.value.apply(this, stack); break; } } /* Create, update and delete scene nodes. */ stack.unshift(null); scene.length = data.length; for (var i = 0; i < data.length; i++) { pv.Mark.prototype.index = this.index = i; var s = scene[i]; if (!s) scene[i] = s = {}; s.data = stack[0] = data[i]; /* Evaluate special visible property. */ var visible = this.binds.visible; switch (visible.type) { case 0: case 1: visible = defs.values.visible; break; case 2: visible = visible.value; break; case 3: { property = "visible"; visible = visible.value.apply(this, stack); break; } } if (s.visible = visible) this.buildInstance(s); } stack.shift(); delete this.index; pv.Mark.prototype.index = -1; if (!this.parent) scene.data = null; return this; }; /** * @private Evaluates the specified array of properties for the specified * instance s in the scene graph. * * @param s a node in the scene graph; the instance of the mark to build. * @param properties an array of properties. */ pv.Mark.prototype.buildProperties = function(s, properties) { for (var i = 0, n = properties.length; i < n; i++) { var p = properties[i], v = p.value; switch (p.type) { case 0: case 1: v = this.scene.defs.values[p.name]; break; case 3: { property = p.name; v = v.apply(this, this.root.scene.data); break; } } s[p.name] = v; } }; /** * @private Evaluates all of the properties for this mark for the specified * instance s in the scene graph. The set of properties to evaluate is * retrieved from the {@link #properties} array for this mark type (see {@link * #type}). After these properties are evaluated, any implied properties * may be computed by the mark and set on the scene graph; see * {@link #buildImplied}. * *
For panels, this method recursively builds the scene graph for all child * marks as well. In general, this method should not need to be overridden by * concrete mark types. * * @param s a node in the scene graph; the instance of the mark to build. */ pv.Mark.prototype.buildInstance = function(s) { this.buildProperties(s, this.binds.properties); this.buildImplied(s); }; /** * @private Computes the implied properties for this mark for the specified * instance s in the scene graph. Implied properties are those with * dependencies on multiple other properties; for example, the width property * may be implied if the left and right properties are set. This method can be * overridden by concrete mark types to define new implied properties, if * necessary. * * @param s a node in the scene graph; the instance of the mark to build. */ pv.Mark.prototype.buildImplied = function(s) { var l = s.left; var r = s.right; var t = s.top; var b = s.bottom; /* Assume width and height are zero if not supported by this mark type. */ var p = this.properties; var w = p.width ? s.width : 0; var h = p.height ? s.height : 0; /* Compute implied width, right and left. */ var width = this.parent ? this.parent.width() : (w + l + r); if (w == null) { w = width - (r = r || 0) - (l = l || 0); } else if (r == null) { r = width - w - (l = l || 0); } else if (l == null) { l = width - w - (r = r || 0); } /* Compute implied height, bottom and top. */ var height = this.parent ? this.parent.height() : (h + t + b); if (h == null) { h = height - (t = t || 0) - (b = b || 0); } else if (b == null) { b = height - h - (t = t || 0); } else if (t == null) { t = height - h - (b = b || 0); } s.left = l; s.right = r; s.top = t; s.bottom = b; /* Only set width and height if they are supported by this mark type. */ if (p.width) s.width = w; if (p.height) s.height = h; }; /** * @private The name of the property being evaluated, for so-called "smart" * functions that change behavior depending on which property is being * evaluated. This functionality is somewhat magical, so for now, this feature * is not exposed outside the library. * * @type string */ var property; /** @private The current mouse location. */ var pageX = 0, pageY = 0; pv.listen(window, "mousemove", function(e) { pageX = e.pageX; pageY = e.pageY; }); /** * Returns the current location of the mouse (cursor) relative to this mark's * parent. The x coordinate corresponds to the left margin, while the * y coordinate corresponds to the top margin. * * @returns {pv.Vector} the mouse location. */ pv.Mark.prototype.mouse = function() { var x = 0, y = 0, mark = (this instanceof pv.Panel) ? this : this.parent; do { x += mark.left(); y += mark.top(); } while (mark = mark.parent); var node = this.root.canvas(); do { x += node.offsetLeft; y += node.offsetTop; } while (node = node.offsetParent); return pv.vector(pageX - x, pageY - y); }; /** * Registers an event handler for the specified event type with this mark. When * an event of the specified type is triggered, the specified handler will be * invoked. The handler is invoked in a similar method to property functions: * the context is this mark instance, and the arguments are the full * data stack. Event handlers can use property methods to manipulate the display * properties of the mark: * *
m.event("click", function() this.fillStyle("red"));* * Alternatively, the external data can be manipulated and the visualization * redrawn: * *
m.event("click", function(d) { * data = all.filter(function(k) k.name == d); * vis.render(); * });* * The return value of the event handler determines which mark gets re-rendered. * Use defs ({@link #def}) to set temporary state from event handlers. * *
The complete set of event types is defined by SVG; see the reference * below. The set of supported event types is:
TODO In the current implementation, event handlers are not inherited from * prototype marks. They must be defined explicitly on each interactive mark. In * addition, only one event handler for a given event type can be defined; when * specifying multiple event handlers for the same type, only the last one will * be used. * * @see SVG events * @param {string} type the event type. * @param {function} handler the event handler. * @returns {pv.Mark} this. */ pv.Mark.prototype.event = function(type, handler) { if (!this.$handlers) this.$handlers = {}; this.$handlers[type] = handler; return this; }; /** @private TODO */ pv.Mark.prototype.dispatch = function(type, scenes, index) { var l = this.$handlers && this.$handlers[type]; if (!l) { if (this.parent) { this.parent.dispatch(type, scenes.parent, scenes.parentIndex); } return; } try { /* Setup the scene stack. */ var mark = this; do { mark.index = index; mark.scene = scenes; index = scenes.parentIndex; scenes = scenes.parent; } while (mark = mark.parent); /* Execute the event listener. */ try { mark = l.apply(this, this.root.scene.data = argv(this)); } finally { this.root.scene.data = null; } /* Update the display. TODO dirtying. */ if (mark instanceof pv.Mark) mark.render(); } finally { /* Restore the scene stack. */ var mark = this; do { if (mark.parent) delete mark.scene; delete mark.index; } while (mark = mark.parent); } }; /** * Constructs a new mark anchor with default properties. * * @class Represents an anchor on a given mark. An anchor is itself a mark, but * without a visual representation. It serves only to provide useful default * properties that can be inherited by other marks. Each type of mark can define * any number of named anchors for convenience. If the concrete mark type does * not define an anchor implementation specifically, one will be inherited from * the mark's parent class. * *
For example, the bar mark provides anchors for its four sides: left, * right, top and bottom. Adding a label to the top anchor of a bar, * *
bar.anchor("top").add(pv.Label);* * will render a text label on the top edge of the bar; the top anchor defines * the appropriate position properties (top and left), as well as text-rendering * properties for convenience (textAlign and textBaseline). * * @extends pv.Mark */ pv.Anchor = function() { pv.Mark.call(this); }; pv.Anchor.prototype = pv.extend(pv.Mark) .property("name"); /** * The anchor name. The set of supported anchor names is dependent on the * concrete mark type; see the mark type for details. For example, bars support * left, right, top and bottom anchors. * *
While anchor names are typically constants, the anchor name is a true * property, which means you can specify a function to compute the anchor name * dynamically. For instance, if you wanted to alternate top and bottom anchors, * saying * *
m.anchor(function() (this.index % 2) ? "top" : "bottom").add(pv.Dot);* * would have the desired effect. * * @type string * @name pv.Anchor.prototype.name */ /** * Constructs a new area mark with default properties. Areas are not typically * constructed directly, but by adding to a panel or an existing mark via * {@link pv.Mark#add}. * * @class Represents an area mark: the solid area between two series of * connected line segments. Unsurprisingly, areas are used most frequently for * area charts. * *
Just as a line represents a polyline, the Area mark type * represents a polygon. However, an area is not an arbitrary polygon; * vertices are paired either horizontally or vertically into parallel * spans, and each span corresponds to an associated datum. Either the * width or the height must be specified, but not both; this determines whether * the area is horizontally-oriented or vertically-oriented. Like lines, areas * can be stroked and filled with arbitrary colors. * *
See also the Area guide. * * @extends pv.Mark */ pv.Area = function() { pv.Mark.call(this); }; pv.Area.prototype = pv.extend(pv.Mark) .property("width") .property("height") .property("lineWidth") .property("strokeStyle") .property("fillStyle") .property("segmented") .property("interpolate"); pv.Area.prototype.type = "area"; /** * The width of a given span, in pixels; used for horizontal spans. If the width * is specified, the height property should be 0 (the default). Either the top * or bottom property should be used to space the spans vertically, typically as * a multiple of the index. * * @type number * @name pv.Area.prototype.width */ /** * The height of a given span, in pixels; used for vertical spans. If the height * is specified, the width property should be 0 (the default). Either the left * or right property should be used to space the spans horizontally, typically * as a multiple of the index. * * @type number * @name pv.Area.prototype.height */ /** * The width of stroked lines, in pixels; used in conjunction with * strokeStyle to stroke the perimeter of the area. Unlike the * {@link Line} mark type, the entire perimeter is stroked, rather than just one * edge. The default value of this property is 1.5, but since the default stroke * style is null, area marks are not stroked by default. * *
This property is fixed for non-segmented areas. See * {@link pv.Mark}. * * @type number * @name pv.Area.prototype.lineWidth */ /** * The style of stroked lines; used in conjunction with lineWidth to * stroke the perimeter of the area. Unlike the {@link Line} mark type, the * entire perimeter is stroked, rather than just one edge. The default value of * this property is null, meaning areas are not stroked by default. * *
This property is fixed for non-segmented areas. See * {@link pv.Mark}. * * @type string * @name pv.Area.prototype.strokeStyle * @see pv.color */ /** * The area fill style; if non-null, the interior of the polygon forming the * area is filled with the specified color. The default value of this property * is a categorical color. * *
This property is fixed for non-segmented areas. See * {@link pv.Mark}. * * @type string * @name pv.Area.prototype.fillStyle * @see pv.color */ /** * Whether the area is segmented; whether variations in fill style, stroke * style, and the other properties are treated as fixed. Rendering segmented * areas is noticeably slower than non-segmented areas. * *
This property is fixed. See {@link pv.Mark}. * * @type boolean * @name pv.Area.prototype.segmented */ /** * How to interpolate between values. Linear interpolation ("linear") is the * default, producing a straight line between points. For piecewise constant * functions (i.e., step functions), either "step-before" or "step-after" can be * specified. * *
Note: this property is currently supported only on non-segmented areas. * *
This property is fixed. See {@link pv.Mark}. * * @type string * @name pv.Area.prototype.interpolate */ /** * Default properties for areas. By default, there is no stroke and the fill * style is a categorical color. * * @type pv.Area */ pv.Area.prototype.defaults = new pv.Area() .extend(pv.Mark.prototype.defaults) .lineWidth(1.5) .fillStyle(defaultFillStyle) .interpolate("linear"); /** * Constructs a new area anchor with default properties. Areas support five * different anchors:
To facilitate stacking of areas, the anchors are defined in terms of their * opposite edge. For example, the top anchor defines the bottom property, such * that the area grows upwards; the bottom anchor instead defines the top * property, such that the area grows downwards. Of course, in general it is * more robust to use panels and the cousin accessor to define stacked area * marks; see {@link pv.Mark#scene} for an example. * * @param {string} name the anchor name; either a string or a property function. * @returns {pv.Anchor} */ pv.Area.prototype.anchor = function(name) { var area = this; return pv.Mark.prototype.anchor.call(this, name) .left(function() { switch (this.name()) { case "bottom": case "top": case "center": return area.left() + area.width() / 2; case "right": return area.left() + area.width(); } return null; }) .right(function() { switch (this.name()) { case "bottom": case "top": case "center": return area.right() + area.width() / 2; case "left": return area.right() + area.width(); } return null; }) .top(function() { switch (this.name()) { case "left": case "right": case "center": return area.top() + area.height() / 2; case "bottom": return area.top() + area.height(); } return null; }) .bottom(function() { switch (this.name()) { case "left": case "right": case "center": return area.bottom() + area.height() / 2; case "top": return area.bottom() + area.height(); } return null; }) .textAlign(function() { switch (this.name()) { case "bottom": case "top": case "center": return "center"; case "right": return "right"; } return "left"; }) .textBaseline(function() { switch (this.name()) { case "right": case "left": case "center": return "middle"; case "top": return "top"; } return "bottom"; }); }; /** * @private Overrides the default behavior of {@link pv.Mark.buildImplied} such * that the width and height are set to zero if null. * * @param s a node in the scene graph; the instance of the mark to build. */ pv.Area.prototype.buildImplied = function(s) { if (s.height == null) s.height = 0; if (s.width == null) s.width = 0; pv.Mark.prototype.buildImplied.call(this, s); }; /** @private */ var pv_Area_specials = {left:1, top:1, right:1, bottom:1, width:1, height:1, name:1}; /** @private */ pv.Area.prototype.bind = function() { pv.Mark.prototype.bind.call(this); var binds = this.binds, properties = binds.properties, specials = binds.specials = []; for (var i = 0, n = properties.length; i < n; i++) { var p = properties[i]; if (p.name in pv_Area_specials) specials.push(p); } }; /** @private */ pv.Area.prototype.buildInstance = function(s) { if (this.index && !this.scene[0].segmented) { this.buildProperties(s, this.binds.specials); this.buildImplied(s); } else { pv.Mark.prototype.buildInstance.call(this, s); } }; /** * Constructs a new bar mark with default properties. Bars are not typically * constructed directly, but by adding to a panel or an existing mark via * {@link pv.Mark#add}. * * @class Represents a bar: an axis-aligned rectangle that can be stroked and * filled. Bars are used for many chart types, including bar charts, histograms * and Gantt charts. Bars can also be used as decorations, for example to draw a * frame border around a panel; in fact, a panel is a special type (a subclass) * of bar. * *
Bars can be positioned in several ways. Most commonly, one of the four * corners is fixed using two margins, and then the width and height properties * determine the extent of the bar relative to this fixed location. For example, * using the bottom and left properties fixes the bottom-left corner; the width * then extends to the right, while the height extends to the top. As an * alternative to the four corners, a bar can be positioned exclusively using * margins; this is convenient as an inset from the containing panel, for * example. See {@link pv.Mark} for details on the prioritization of redundant * positioning properties. * *
See also the Bar guide. * * @extends pv.Mark */ pv.Bar = function() { pv.Mark.call(this); }; pv.Bar.prototype = pv.extend(pv.Mark) .property("width") .property("height") .property("lineWidth") .property("strokeStyle") .property("fillStyle"); pv.Bar.prototype.type = "bar"; /** * The width of the bar, in pixels. If the left position is specified, the bar * extends rightward from the left edge; if the right position is specified, the * bar extends leftward from the right edge. * * @type number * @name pv.Bar.prototype.width */ /** * The height of the bar, in pixels. If the bottom position is specified, the * bar extends upward from the bottom edge; if the top position is specified, * the bar extends downward from the top edge. * * @type number * @name pv.Bar.prototype.height */ /** * The width of stroked lines, in pixels; used in conjunction with * strokeStyle to stroke the bar's border. * * @type number * @name pv.Bar.prototype.lineWidth */ /** * The style of stroked lines; used in conjunction with lineWidth to * stroke the bar's border. The default value of this property is null, meaning * bars are not stroked by default. * * @type string * @name pv.Bar.prototype.strokeStyle * @see pv.color */ /** * The bar fill style; if non-null, the interior of the bar is filled with the * specified color. The default value of this property is a categorical color. * * @type string * @name pv.Bar.prototype.fillStyle * @see pv.color */ /** * Default properties for bars. By default, there is no stroke and the fill * style is a categorical color. * * @type pv.Bar */ pv.Bar.prototype.defaults = new pv.Bar() .extend(pv.Mark.prototype.defaults) .lineWidth(1.5) .fillStyle(defaultFillStyle); /** * Constructs a new bar anchor with default properties. Bars support five * different anchors:
To facilitate stacking of bars, the anchors are defined in terms of their * opposite edge. For example, the top anchor defines the bottom property, such * that the bar grows upwards; the bottom anchor instead defines the top * property, such that the bar grows downwards. Of course, in general it is more * robust to use panels and the cousin accessor to define stacked bars; see * {@link pv.Mark#scene} for an example. * *
Bar anchors also "smartly" specify position properties based on whether * the derived mark type supports the width and height properties. If the * derived mark type does not support these properties (e.g., dots), the * position will be centered on the corresponding edge. Otherwise (e.g., bars), * the position will be in the opposite side. * * @param {string} name the anchor name; either a string or a property function. * @returns {pv.Anchor} */ pv.Bar.prototype.anchor = function(name) { var bar = this; return pv.Mark.prototype.anchor.call(this, name) .left(function() { switch (this.name()) { case "bottom": case "top": case "center": return bar.left() + (this.properties.width ? 0 : (bar.width() / 2)); case "right": return bar.left() + bar.width(); } return null; }) .right(function() { switch (this.name()) { case "bottom": case "top": case "center": return bar.right() + (this.properties.width ? 0 : (bar.width() / 2)); case "left": return bar.right() + bar.width(); } return null; }) .top(function() { switch (this.name()) { case "left": case "right": case "center": return bar.top() + (this.properties.height ? 0 : (bar.height() / 2)); case "bottom": return bar.top() + bar.height(); } return null; }) .bottom(function() { switch (this.name()) { case "left": case "right": case "center": return bar.bottom() + (this.properties.height ? 0 : (bar.height() / 2)); case "top": return bar.bottom() + bar.height(); } return null; }) .textAlign(function() { switch (this.name()) { case "bottom": case "top": case "center": return "center"; case "right": return "right"; } return "left"; }) .textBaseline(function() { switch (this.name()) { case "right": case "left": case "center": return "middle"; case "top": return "top"; } return "bottom"; }); }; /** * Constructs a new dot mark with default properties. Dots are not typically * constructed directly, but by adding to a panel or an existing mark via * {@link pv.Mark#add}. * * @class Represents a dot; a dot is simply a sized glyph centered at a given * point that can also be stroked and filled. The size property is * proportional to the area of the rendered glyph to encourage meaningful visual * encodings. Dots can visually encode up to eight dimensions of data, though * this may be unwise due to integrality. See {@link pv.Mark} for details on the * prioritization of redundant positioning properties. * *
See also the Dot guide. * * @extends pv.Mark */ pv.Dot = function() { pv.Mark.call(this); }; pv.Dot.prototype = pv.extend(pv.Mark) .property("size") .property("shape") .property("angle") .property("lineWidth") .property("strokeStyle") .property("fillStyle"); pv.Dot.prototype.type = "dot"; /** * The size of the dot, in square pixels. Square pixels are used such that the * area of the dot is linearly proportional to the value of the size property, * facilitating representative encodings. * * @see #radius * @type number * @name pv.Dot.prototype.size */ /** * The shape name. Several shapes are supported:
Note: it may be more natural to use the {@link pv.Rule} mark for * horizontal and vertical ticks. The tick shape is only necessary if angled * ticks are needed. * * @type string * @name pv.Dot.prototype.shape */ /** * The rotation angle, in radians. Used to rotate shapes, such as to turn a * cross into a plus. * * @type number * @name pv.Dot.prototype.angle */ /** * The width of stroked lines, in pixels; used in conjunction with * strokeStyle to stroke the dot's shape. * * @type number * @name pv.Dot.prototype.lineWidth */ /** * The style of stroked lines; used in conjunction with lineWidth to * stroke the dot's shape. The default value of this property is a categorical * color. * * @type string * @name pv.Dot.prototype.strokeStyle * @see pv.color */ /** * The fill style; if non-null, the interior of the dot is filled with the * specified color. The default value of this property is null, meaning dots are * not filled by default. * * @type string * @name pv.Dot.prototype.fillStyle * @see pv.color */ /** * Default properties for dots. By default, there is no fill and the stroke * style is a categorical color. The default shape is "circle" with size 20. * * @type pv.Dot */ pv.Dot.prototype.defaults = new pv.Dot() .extend(pv.Mark.prototype.defaults) .size(20) .shape("circle") .lineWidth(1.5) .strokeStyle(defaultStrokeStyle); /** * Constructs a new dot anchor with default properties. Dots support five * different anchors:
For consistency with the other mark types, the anchor positions are * defined in terms of their opposite edge. For example, the top anchor defines * the bottom property, such that a bar added to the top anchor grows upward. * * @param {string} name the anchor name; either a string or a property function. * @returns {pv.Anchor} */ pv.Dot.prototype.anchor = function(name) { var dot = this; return pv.Mark.prototype.anchor.call(this, name) .left(function(d) { switch (this.name()) { case "bottom": case "top": case "center": return dot.left(); case "right": return dot.left() + dot.radius(); } return null; }) .right(function(d) { switch (this.name()) { case "bottom": case "top": case "center": return dot.right(); case "left": return dot.right() + dot.radius(); } return null; }) .top(function(d) { switch (this.name()) { case "left": case "right": case "center": return dot.top(); case "bottom": return dot.top() + dot.radius(); } return null; }) .bottom(function(d) { switch (this.name()) { case "left": case "right": case "center": return dot.bottom(); case "top": return dot.bottom() + dot.radius(); } return null; }) .textAlign(function(d) { switch (this.name()) { case "left": return "right"; case "bottom": case "top": case "center": return "center"; } return "left"; }) .textBaseline(function(d) { switch (this.name()) { case "right": case "left": case "center": return "middle"; case "bottom": return "top"; } return "bottom"; }); }; /** * Returns the radius of the dot, which is defined to be the square root of the * {@link #size} property. * * @returns {number} the radius. */ pv.Dot.prototype.radius = function() { return Math.sqrt(this.size()); }; /** * Constructs a new label mark with default properties. Labels are not typically * constructed directly, but by adding to a panel or an existing mark via * {@link pv.Mark#add}. * * @class Represents a text label, allowing textual annotation of other marks or * arbitrary text within the visualization. The character data must be plain * text (unicode), though the text can be styled using the {@link #font} * property. If rich text is needed, external HTML elements can be overlaid on * the canvas by hand. * *
Labels are positioned using the box model, similarly to {@link Dot}. Thus, * a label has no width or height, but merely a text anchor location. The text * is positioned relative to this anchor location based on the * {@link #textAlign}, {@link #textBaseline} and {@link #textMargin} properties. * Furthermore, the text may be rotated using {@link #textAngle}. * *
Labels ignore events, so as to not interfere with event handlers on * underlying marks, such as bars. In the future, we may support event handlers * on labels. * *
See also the Label guide. * * @extends pv.Mark */ pv.Label = function() { pv.Mark.call(this); }; pv.Label.prototype = pv.extend(pv.Mark) .property("text") .property("font") .property("textAngle") .property("textStyle") .property("textAlign") .property("textBaseline") .property("textMargin") .property("textShadow"); pv.Label.prototype.type = "label"; /** * The character data to render; a string. The default value of the text * property is the identity function, meaning the label's associated datum will * be rendered using its toString. * * @type string * @name pv.Label.prototype.text */ /** * The font format, per the CSS Level 2 specification. The default font is "10px * sans-serif", for consistency with the HTML 5 canvas element specification. * Note that since text is not wrapped, any line-height property will be * ignored. The other font-style, font-variant, font-weight, font-size and * font-family properties are supported. * * @see CSS2 fonts * @type string * @name pv.Label.prototype.font */ /** * The rotation angle, in radians. Text is rotated clockwise relative to the * anchor location. For example, with the default left alignment, an angle of * Math.PI / 2 causes text to proceed downwards. The default angle is zero. * * @type number * @name pv.Label.prototype.textAngle */ /** * The text color. The name "textStyle" is used for consistency with "fillStyle" * and "strokeStyle", although it might be better to rename this property (and * perhaps use the same name as "strokeStyle"). The default color is black. * * @type string * @name pv.Label.prototype.textStyle * @see pv.color */ /** * The horizontal text alignment. One of:
Like areas, lines can be stroked and filled with arbitrary colors. In most * cases, lines are only stroked, but the fill style can be used to construct * arbitrary polygons. * *
See also the Line guide. * * @extends pv.Mark */ pv.Line = function() { pv.Mark.call(this); }; pv.Line.prototype = pv.extend(pv.Mark) .property("lineWidth") .property("strokeStyle") .property("fillStyle") .property("segmented") .property("interpolate"); pv.Line.prototype.type = "line"; /** * The width of stroked lines, in pixels; used in conjunction with * strokeStyle to stroke the line. * * @type number * @name pv.Line.prototype.lineWidth */ /** * The style of stroked lines; used in conjunction with lineWidth to * stroke the line. The default value of this property is a categorical color. * * @type string * @name pv.Line.prototype.strokeStyle * @see pv.color */ /** * The line fill style; if non-null, the interior of the line is closed and * filled with the specified color. The default value of this property is a * null, meaning that lines are not filled by default. * * @type string * @name pv.Line.prototype.fillStyle * @see pv.color */ /** * Whether the line is segmented; whether variations in stroke style, line width * and the other properties are treated as fixed. Rendering segmented lines is * noticeably slower than non-segmented lines. * *
This property is fixed. See {@link pv.Mark}. * * @type boolean * @name pv.Line.prototype.segmented */ /** * How to interpolate between values. Linear interpolation ("linear") is the * default, producing a straight line between points. For piecewise constant * functions (i.e., step functions), either "step-before" or "step-after" can be * specified. * *
Note: this property is currently supported only on non-segmented lines. * *
This property is fixed. See {@link pv.Mark}. * * @type string * @name pv.Line.prototype.interpolate */ /** * Default properties for lines. By default, there is no fill and the stroke * style is a categorical color. The default interpolation is linear. * * @type pv.Line */ pv.Line.prototype.defaults = new pv.Line() .extend(pv.Mark.prototype.defaults) .lineWidth(1.5) .strokeStyle(defaultStrokeStyle) .interpolate("linear"); /** @private */ var pv_Line_specials = {left:1, top:1, right:1, bottom:1, name:1}; /** @private */ pv.Line.prototype.bind = function() { pv.Mark.prototype.bind.call(this); var binds = this.binds, properties = binds.properties, specials = binds.specials = []; for (var i = 0, n = properties.length; i < n; i++) { var p = properties[i]; if (p.name in pv_Line_specials) specials.push(p); } }; /** @private */ pv.Line.prototype.buildInstance = function(s) { if (this.index && !this.scene[0].segmented) { this.buildProperties(s, this.binds.specials); this.buildImplied(s); } else { pv.Mark.prototype.buildInstance.call(this, s); } }; /** * Constructs a new rule with default properties. Rules are not typically * constructed directly, but by adding to a panel or an existing mark via * {@link pv.Mark#add}. * * @class Represents a horizontal or vertical rule. Rules are frequently used * for axes and grid lines. For example, specifying only the bottom property * draws horizontal rules, while specifying only the left draws vertical * rules. Rules can also be used as thin bars. The visual style is controlled in * the same manner as lines. * *
Rules are positioned exclusively the standard box model properties. The * following combinations of properties are supported: * *
Properties | Orientation | * *
---|---|
left | vertical |
right | vertical |
left, bottom, top | vertical |
right, bottom, top | vertical |
top | horizontal |
bottom | horizontal |
top, left, right | horizontal |
bottom, left, right | horizontal |
left, top, height | vertical |
left, bottom, height | vertical |
right, top, height | vertical |
right, bottom, height | vertical |
left, top, width | horizontal |
left, bottom, width | horizontal |
right, top, width | horizontal |
right, bottom, width | horizontal |
Small rules can be used as tick marks; alternatively, a {@link Dot} with * the "tick" shape can be used. * *
See also the Rule guide. * * @see pv.Line * @extends pv.Mark */ pv.Rule = function() { pv.Mark.call(this); }; pv.Rule.prototype = pv.extend(pv.Mark) .property("width") .property("height") .property("lineWidth") .property("strokeStyle"); pv.Rule.prototype.type = "rule"; /** * The width of the rule, in pixels. If the left position is specified, the rule * extends rightward from the left edge; if the right position is specified, the * rule extends leftward from the right edge. * * @type number * @name pv.Rule.prototype.width */ /** * The height of the rule, in pixels. If the bottom position is specified, the * rule extends upward from the bottom edge; if the top position is specified, * the rule extends downward from the top edge. * * @type number * @name pv.Rule.prototype.height */ /** * The width of stroked lines, in pixels; used in conjunction with * strokeStyle to stroke the rule. The default value is 1 pixel. * * @type number * @name pv.Rule.prototype.lineWidth */ /** * The style of stroked lines; used in conjunction with lineWidth to * stroke the rule. The default value of this property is black. * * @type string * @name pv.Rule.prototype.strokeStyle * @see pv.color */ /** * Default properties for rules. By default, a single-pixel black line is * stroked. * * @type pv.Rule */ pv.Rule.prototype.defaults = new pv.Rule() .extend(pv.Mark.prototype.defaults) .lineWidth(1) .strokeStyle("black"); /** * Constructs a new rule anchor with default properties. Rules support five * different anchors:
For consistency with the other mark types, the anchor positions are * defined in terms of their opposite edge. For example, the top anchor defines * the bottom property, such that a bar added to the top anchor grows upward. * * @param {string} name the anchor name; either a string or a property function. * @returns {pv.Anchor} */ pv.Rule.prototype.anchor = function(name) { return pv.Bar.prototype.anchor.call(this, name) .textAlign(function(d) { switch (this.name()) { case "left": return "right"; case "bottom": case "top": case "center": return "center"; case "right": return "left"; } }) .textBaseline(function(d) { switch (this.name()) { case "right": case "left": case "center": return "middle"; case "top": return "bottom"; case "bottom": return "top"; } }); }; /** * @private Overrides the default behavior of {@link pv.Mark.buildImplied} to * determine the orientation (vertical or horizontal) of the rule. * * @param s a node in the scene graph; the instance of the rule to build. */ pv.Rule.prototype.buildImplied = function(s) { var l = s.left, r = s.right, t = s.top, b = s.bottom; /* Determine horizontal or vertical orientation. */ if ((s.width != null) || ((l == null) && (r == null)) || ((r != null) && (l != null))) { s.height = 0; } else { s.width = 0; } pv.Mark.prototype.buildImplied.call(this, s); }; /** * Constructs a new, empty panel with default properties. Panels, with the * exception of the root panel, are not typically constructed directly; instead, * they are added to an existing panel or mark via {@link pv.Mark#add}. * * @class Represents a container mark. Panels allow repeated or nested * structures, commonly used in small multiple displays where a small * visualization is tiled to facilitate comparison across one or more * dimensions. Other types of visualizations may benefit from repeated and * possibly overlapping structure as well, such as stacked area charts. Panels * can also offset the position of marks to provide padding from surrounding * content. * *
All Protovis displays have at least one panel; this is the root panel to * which marks are rendered. The box model properties (four margins, width and * height) are used to offset the positions of contained marks. The data * property determines the panel count: a panel is generated once per associated * datum. When nested panels are used, property functions can declare additional * arguments to access the data associated with enclosing panels. * *
Panels can be rendered inline, facilitating the creation of sparklines. * This allows designers to reuse browser layout features, such as text flow and * tables; designers can also overlay HTML elements such as rich text and * images. * *
All panels have a children array (possibly empty) containing the * child marks in the order they were added. Panels also have a root * field which points to the root (outermost) panel; the root panel's root field * points to itself. * *
See also the Protovis guide. * * @extends pv.Bar */ pv.Panel = function() { pv.Bar.call(this); /** * The child marks; zero or more {@link pv.Mark}s in the order they were * added. * * @see #add * @type pv.Mark[] */ this.children = []; this.root = this; /** * The internal $dom field is set by the Protovis loader; see lang/init.js. It * refers to the script element that contains the Protovis specification, so * that the panel knows where in the DOM to insert the generated SVG element. * * @private */ this.$dom = pv.Panel.$dom; }; pv.Panel.prototype = pv.extend(pv.Bar) .property("canvas") .property("overflow"); pv.Panel.prototype.type = "panel"; /** * The canvas element; either the string ID of the canvas element in the current * document, or a reference to the canvas element itself. If null, a canvas * element will be created and inserted into the document at the location of the * script element containing the current Protovis specification. This property * only applies to root panels and is ignored on nested panels. * *
Note: the "canvas" element here refers to a div (or other suitable * HTML container element), not a canvas element. The name of * this property is a historical anachronism from the first implementation that * used HTML 5 canvas, rather than SVG. * * @type string * @name pv.Panel.prototype.canvas */ /** * Default properties for panels. By default, the margins are zero, the fill * style is transparent. * * @type pv.Panel */ pv.Panel.prototype.defaults = new pv.Panel() .extend(pv.Bar.prototype.defaults) .fillStyle(null) .overflow("visible"); /** * Returns an anchor with the specified name. This method is overridden since * the behavior of Panel anchors is slightly different from normal anchors: * adding to an anchor adds to the anchor target's, rather than the anchor * target's parent. To avoid double margins, we override the anchor's proto so * that the margins are zero. * * @param {string} name the anchor name; either a string or a property function. * @returns {pv.Anchor} the new anchor. */ pv.Panel.prototype.anchor = function(name) { /* A "view" of this panel whose margins appear to be zero. */ function z() { return 0; } z.prototype = this; z.prototype.left = z.prototype.right = z.prototype.top = z.prototype.bottom = z; var anchor = pv.Bar.prototype.anchor.call(new z(), name) .data(function(d) { return [d]; }); anchor.parent = this; return anchor; }; /** * Adds a new mark of the specified type to this panel. Unlike the normal * {@link Mark#add} behavior, adding a mark to a panel does not cause the mark * to inherit from the panel. Since the contained marks are offset by the panel * margins already, inheriting properties is generally undesirable; of course, * it is always possible to change this behavior by calling {@link Mark#extend} * explicitly. * * @param {function} type the type of the new mark to add. * @returns {pv.Mark} the new mark. */ pv.Panel.prototype.add = function(type) { var child = new type(); child.parent = this; child.root = this.root; child.childIndex = this.children.length; this.children.push(child); return child; }; /** @private TODO */ pv.Panel.prototype.bind = function() { pv.Mark.prototype.bind.call(this); for (var i = 0; i < this.children.length; i++) { this.children[i].bind(); } }; /** * @private Evaluates all of the properties for this panel for the specified * instance s in the scene graph, including recursively building the * scene graph for child marks. * * @param s a node in the scene graph; the instance of the panel to build. * @see Mark#scene */ pv.Panel.prototype.buildInstance = function(s) { pv.Bar.prototype.buildInstance.call(this, s); if (!s.children) s.children = []; /* * Build each child, passing in the parent (this panel) scene graph node. The * child mark's scene is initialized from the corresponding entry in the * existing scene graph, such that properties from the previous build can be * reused; this is largely to facilitate the recycling of SVG elements. */ for (var i = 0; i < this.children.length; i++) { this.children[i].scene = s.children[i]; // possibly undefined this.children[i].build(); } /* * Once the child marks have been built, the new scene graph nodes are removed * from the child marks and placed into the scene graph. The nodes cannot * remain on the child nodes because this panel (or a parent panel) may be * instantiated multiple times! */ for (var i = 0; i < this.children.length; i++) { s.children[i] = this.children[i].scene; delete this.children[i].scene; } /* Delete any expired child scenes, should child marks have been removed. */ s.children.length = this.children.length; }; /** * @private Computes the implied properties for this panel for the specified * instance s in the scene graph. Panels have two implied * properties:
TODO Restore support for dynamic images (such as heatmaps). These were * supported in the canvas implementation using the pixel buffer API; although * SVG does not support pixel manipulation, it is possible to embed a canvas * element in SVG using foreign objects. * *
TODO Allow different modes of image placement: "scale" -- scale and * preserve aspect ratio, "tile" -- repeat the image, "center" -- center the * image, "fill" -- scale without preserving aspect ratio. * *
See {@link pv.Bar} for details on positioning properties. * * @extends pv.Bar */ pv.Image = function() { pv.Bar.call(this); }; pv.Image.prototype = pv.extend(pv.Bar) .property("url"); pv.Image.prototype.type = "image"; /** * The URL of the image to display. The set of supported image types is * browser-dependent; PNG and JPEG are recommended. * * @type string * @name pv.Image.prototype.url */ /** * Default properties for images. By default, there is no stroke or fill style. * * @type pv.Image */ pv.Image.prototype.defaults = new pv.Image() .extend(pv.Bar.prototype.defaults) .fillStyle(null); /** * Constructs a new wedge with default properties. Wedges are not typically * constructed directly, but by adding to a panel or an existing mark via * {@link pv.Mark#add}. * * @class Represents a wedge, or pie slice. Specified in terms of start and end * angle, inner and outer radius, wedges can be used to construct donut charts * and polar bar charts as well. If the {@link #angle} property is used, the end * angle is implied by adding this value to start angle. By default, the start * angle is the previously-generated wedge's end angle. This design allows * explicit control over the wedge placement if desired, while offering * convenient defaults for the construction of radial graphs. * *
The center point of the circle is positioned using the standard box model. * The wedge can be stroked and filled, similar to {link Bar}. * *
See also the Wedge guide. * * @extends pv.Mark */ pv.Wedge = function() { pv.Mark.call(this); }; pv.Wedge.prototype = pv.extend(pv.Mark) .property("startAngle") .property("endAngle") .property("angle") .property("innerRadius") .property("outerRadius") .property("lineWidth") .property("strokeStyle") .property("fillStyle"); pv.Wedge.prototype.type = "wedge"; /** * The start angle of the wedge, in radians. The start angle is measured * clockwise from the 3 o'clock position. The default value of this property is * the end angle of the previous instance (the {@link Mark#sibling}), or -PI / 2 * for the first wedge; for pie and donut charts, typically only the * {@link #angle} property needs to be specified. * * @type number * @name pv.Wedge.prototype.startAngle */ /** * The end angle of the wedge, in radians. If not specified, the end angle is * implied as the start angle plus the {@link #angle}. * * @type number * @name pv.Wedge.prototype.endAngle */ /** * The angular span of the wedge, in radians. This property is used if end angle * is not specified. * * @type number * @name pv.Wedge.prototype.angle */ /** * The inner radius of the wedge, in pixels. The default value of this property * is zero; a positive value will produce a donut slice rather than a pie slice. * The inner radius can vary per-wedge. * * @type number * @name pv.Wedge.prototype.innerRadius */ /** * The outer radius of the wedge, in pixels. This property is required. For * pies, only this radius is required; for donuts, the inner radius must be * specified as well. The outer radius can vary per-wedge. * * @type number * @name pv.Wedge.prototype.outerRadius */ /** * The width of stroked lines, in pixels; used in conjunction with * strokeStyle to stroke the wedge's border. * * @type number * @name pv.Wedge.prototype.lineWidth */ /** * The style of stroked lines; used in conjunction with lineWidth to * stroke the wedge's border. The default value of this property is null, * meaning wedges are not stroked by default. * * @type string * @name pv.Wedge.prototype.strokeStyle * @see pv.color */ /** * The wedge fill style; if non-null, the interior of the wedge is filled with * the specified color. The default value of this property is a categorical * color. * * @type string * @name pv.Wedge.prototype.fillStyle * @see pv.color */ /** * Default properties for wedges. By default, there is no stroke and the fill * style is a categorical color. * * @type pv.Wedge */ pv.Wedge.prototype.defaults = new pv.Wedge() .extend(pv.Mark.prototype.defaults) .startAngle(function() { var s = this.sibling(); return s ? s.endAngle : -Math.PI / 2; }) .innerRadius(0) .lineWidth(1.5) .strokeStyle(null) .fillStyle(defaultFillStyle.by(pv.index)); /** * Returns the mid-radius of the wedge, which is defined as half-way between the * inner and outer radii. * * @see #innerRadius * @see #outerRadius * @returns {number} the mid-radius, in pixels. */ pv.Wedge.prototype.midRadius = function() { return (this.innerRadius() + this.outerRadius()) / 2; }; /** * Returns the mid-angle of the wedge, which is defined as half-way between the * start and end angles. * * @see #startAngle * @see #endAngle * @returns {number} the mid-angle, in radians. */ pv.Wedge.prototype.midAngle = function() { return (this.startAngle() + this.endAngle()) / 2; }; /** * Constructs a new wedge anchor with default properties. Wedges support five * different anchors:
1 2 3 * 4 5 6* * should be represented as: * *
[[1, 2, 3], [4, 5, 6]]* * If your data is in column-major order, you can use {@link pv.transpose} to * transpose it. * *
This layout defines left, top, width, height and data properties. The data * property will be the associated element in the array. For example, if the * array is a two-dimensional array of values in the range [0,1], a simple * heatmap can be generated as: * *
.add(pv.Bar) * .extend(pv.Layout.grid(array)) * .fillStyle(pv.ramp("white", "black"))* * By default, the grid fills the full width and height of the parent panel. * * @param {array[]} arrays an array of arrays. * @returns {pv.Layout.grid} a grid layout. */ pv.Layout.grid = function(arrays) { var rows = arrays.length, cols = arrays[0].length; /** @private */ function w() { return this.parent.width() / cols; } /** @private */ function h() { return this.parent.height() / rows; } /* A dummy mark, like an anchor, which the caller extends. */ return new pv.Mark() .data(pv.blend(arrays)) .left(function() { return w.call(this) * (this.index % cols); }) .top(function() { return h.call(this) * Math.floor(this.index / cols); }) .width(w) .height(h); }; /** * Returns a new stack layout. * * @class A layout for stacking marks vertically or horizontally, using the * cousin instance. This layout is designed to be used for one of the * four positional properties in the box model, and changes behavior depending * on the property being evaluated:
new pv.Panel() * .width(150).height(150) * .add(pv.Panel) * .data([[1, 1.2, 1.7, 1.5, 1.7], * [.5, 1, .8, 1.1, 1.3], * [.2, .5, .8, .9, 1]]) * .add(pv.Area) * .data(function(d) d) * .bottom(pv.Layout.stack()) * .height(function(d) d * 40) * .left(function() this.index * 35) * .root.render();* * specifies a vertically-stacked area chart. * * @returns {pv.Layout.stack} a stack property function. * @see pv.Mark#cousin */ pv.Layout.stack = function() { /** @private */ var offset = function() { return 0; }; /** @private */ function layout() { /* Find the previous visible parent instance. */ var i = this.parent.index, p, c; while ((i-- > 0) && !c) { p = this.parent.scene[i]; if (p.visible) c = p.children[this.childIndex][this.index]; } if (c) { switch (property) { case "bottom": return c.bottom + c.height; case "top": return c.top + c.height; case "left": return c.left + c.width; case "right": return c.right + c.width; } } return offset.apply(this, arguments); } /** * Sets the offset for this stack layout. The offset can either be specified * as a function or as a constant. If a function, the function is invoked in * the same context as a normal property function: this refers to the * mark, and the arguments are the full data stack. By default the offset is * zero. * * @function * @name pv.Layout.stack.prototype.offset * @param {function} f offset function, or constant value. * @returns {pv.Layout.stack} this. */ layout.offset = function(f) { offset = (f instanceof Function) ? f : function() { return f; }; return this; }; return layout; }; // TODO share code with Treemap // TODO vertical / horizontal orientation? /** * Returns a new icicle tree layout. * * @class A tree layout in the form of an icicle.
This tree layout is intended to be extended (see {@link pv.Mark#extend}) * by a {@link pv.Bar}. The data property returns an array of nodes for use by * other property functions. The following node attributes are supported: * *
.add(pv.Bar) * .extend(pv.Layout.icicle(tree))* * To customize the tree to highlight leaf nodes bigger than 10,000 (1E4), you * might say: * *
.add(pv.Bar) * .extend(pv.Layout.icicle(tree)) * .fillStyle(function(n) n.data > 1e4 ? "#ff0" : "#fff")* * The format of the tree argument is any hierarchical object whose * leaf nodes are numbers corresponding to their size. For an example, and * information on how to convert tabular data into such a tree, see * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function * can be specified to override how the tree is interpreted. This size function * can also be used to transform the data. * *
By default, the icicle fills the full width and height of the parent * panel. An optional root key can be specified using {@link #root} for * convenience. * * @param tree a tree (an object) who leaf attributes have sizes. * @returns {pv.Layout.icicle} a tree layout. */ pv.Layout.icicle = function(tree) { var keys = [], sizeof = Number; /** @private */ function accumulate(map) { var node = {size: 0, children: [], keys: keys.slice()}; for (var key in map) { var child = map[key], size = sizeof(child); keys.push(key); if (isNaN(size)) { child = accumulate(child); } else { child = {size: size, data: child, keys: keys.slice()}; } node.children.push(child); node.size += child.size; keys.pop(); } node.children.sort(function(a, b) { return b.size - a.size; }); return node; } /** @private */ function scale(node, k) { node.size *= k; if (node.children) { for (var i = 0; i < node.children.length; i++) { scale(node.children[i], k); } } } /** @private */ function depth(node, i) { i = i ? (i + 1) : 1; return node.children ? pv.max(node.children, function(n) { return depth(n, i); }) : i; } /** @private */ function layout(node) { if (node.children) { icify(node); for (var i = 0; i < node.children.length; i++) { layout(node.children[i]); } } } /** @private */ function icify(node) { var left = node.left; for (var i = 0; i < node.children.length; i++) { var child = node.children[i], width = (child.size / node.size) * node.width; child.left = left; child.top = node.top + node.height; child.width = width; child.height = node.height; child.depth = node.depth + 1; left += width; if (child.children) { icify(child); } } } /** @private */ function flatten(node, array) { if (node.children) { for (var i = 0; i < node.children.length; i++) { flatten(node.children[i], array); } } array.push(node) return array; } /** @private */ function data() { var root = accumulate(tree); root.top = 0; root.left = 0; root.width = this.parent.width(); root.height = this.parent.height() / depth(root); root.depth = 0; layout(root); return flatten(root, []).reverse(); } /* A dummy mark, like an anchor, which the caller extends. */ var mark = new pv.Mark() .data(data) .left(function(n) { return n.left; }) .top(function(n) { return n.top; }) .width(function(n) { return n.width; }) .height(function(n) { return n.height; }); /** * Specifies the root key; optional. The root key is prepended to the * keys attribute for all generated nodes. This method is provided * for convenience and does not affect layout. * * @param {string} v the root key. * @function * @name pv.Layout.icicle.prototype.root * @returns {pv.Layout.icicle} this. */ mark.root = function(v) { keys = [v]; return this; }; /** * Specifies the sizing function. By default, the sizing function is * Number. The sizing function is invoked for each node in the tree * (passed to the constructor): the sizing function must return * undefined or NaN for internal nodes, and a number for * leaf nodes. The aggregate sizes of internal nodes will be automatically * computed by the layout. * *
For example, if the tree data structure represents a file system, with * files as leaf nodes, and each file has a bytes attribute, you can * specify a size function as: * *
.size(function(d) d.bytes)* * This function will return undefined for internal nodes (since * these do not have a bytes attribute), and a number for leaf nodes. * *
Note that the built-in Math.sqrt and Math.log methods
* can also be used as sizing functions. These function similarly to
* Number, except perform a root and log scale, respectively.
*
* @param {function} f the new sizing function.
* @function
* @name pv.Layout.icicle.prototype.size
* @returns {pv.Layout.icicle} this.
*/
mark.size = function(f) {
sizeof = f;
return this;
};
return mark;
};
// TODO share code with Treemap
// TODO inspect parent panel dimensions to set inner and outer radii
/**
* Returns a new sunburst tree layout.
*
* @class A tree layout in the form of a sunburst. The
* center circle corresponds to the root of the tree; subsequent rings
* correspond to each tier. Rings are subdivided into wedges based on the size
* of nodes, per {@link #size}. Within a ring, wedges are sorted by size.
*
*
The tree layout is intended to be extended (see {@link pv.Mark#extend} by * a {@link pv.Wedge}. The data property returns an array of nodes for use by * other property functions. The following node attributes are supported: * *
To produce a default sunburst layout, say: * *
.add(pv.Wedge) * .extend(pv.Layout.sunburst(tree))* * To only show nodes at a depth of two or greater, you might say: * *
.add(pv.Wedge) * .extend(pv.Layout.sunburst(tree)) * .visible(function(n) n.depth > 1)* * The format of the tree argument is a hierarchical object whose leaf * nodes are numbers corresponding to their size. For an example, and * information on how to convert tabular data into such a tree, see * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function * can be specified to override how the tree is interpreted. This size function * can also be used to transform the data. * *
By default, the sunburst fills the full width and height of the parent * panel. An optional root key can be specified using {@link #root} for * convenience. * * @param tree a tree (an object) who leaf attributes have sizes. * @returns {pv.Layout.sunburst} a tree layout. */ pv.Layout.sunburst = function(tree) { var keys = [], sizeof = Number, w, h, r; /** @private */ function accumulate(map) { var node = {size: 0, children: [], keys: keys.slice()}; for (var key in map) { var child = map[key], size = sizeof(child); keys.push(key); if (isNaN(size)) { child = accumulate(child); } else { child = {size: size, data: child, keys: keys.slice()}; } node.children.push(child); node.size += child.size; keys.pop(); } node.children.sort(function(a, b) { return b.size - a.size; }); return node; } /** @private */ function scale(node, k) { node.size *= k; if (node.children) { for (var i = 0; i < node.children.length; i++) { scale(node.children[i], k); } } } /** @private */ function depth(node, i) { i = i ? (i + 1) : 1; return node.children ? pv.max(node.children, function(n) { return depth(n, i); }) : i; } /** @private */ function layout(node) { if (node.children) { wedgify(node); for (var i = 0; i < node.children.length; i++) { layout(node.children[i]); } } } /** @private */ function wedgify(node) { var startAngle = node.startAngle; for (var i = 0; i < node.children.length; i++) { var child = node.children[i], angle = (child.size / node.size) * node.angle; child.startAngle = startAngle; child.angle = angle; child.endAngle = startAngle + angle; child.depth = node.depth + 1; child.left = w / 2; child.top = h / 2; child.innerRadius = Math.max(0, child.depth - .5) * r; child.outerRadius = (child.depth + .5) * r; startAngle += angle; if (child.children) { wedgify(child); } } } /** @private */ function flatten(node, array) { if (node.children) { for (var i = 0; i < node.children.length; i++) { flatten(node.children[i], array); } } array.push(node) return array; } /** @private */ function data() { var root = accumulate(tree); w = this.parent.width(); h = this.parent.height(); r = Math.min(w, h) / 2 / (depth(root) - .5); root.left = w / 2; root.top = h / 2; root.startAngle = 0; root.angle = 2 * Math.PI; root.endAngle = 2 * Math.PI; root.innerRadius = 0; root.outerRadius = r; root.depth = 0; layout(root); return flatten(root, []).reverse(); } /* A dummy mark, like an anchor, which the caller extends. */ var mark = new pv.Mark() .data(data) .left(function(n) { return n.left; }) .top(function(n) { return n.top; }) .startAngle(function(n) { return n.startAngle; }) .angle(function(n) { return n.angle; }) .innerRadius(function(n) { return n.innerRadius; }) .outerRadius(function(n) { return n.outerRadius; }); /** * Specifies the root key; optional. The root key is prepended to the * keys attribute for all generated nodes. This method is provided * for convenience and does not affect layout. * * @param {string} v the root key. * @function * @name pv.Layout.sunburst.prototype.root * @returns {pv.Layout.sunburst} this. */ mark.root = function(v) { keys = [v]; return this; }; /** * Specifies the sizing function. By default, the sizing function is * Number. The sizing function is invoked for each node in the tree * (passed to the constructor): the sizing function must return * undefined or NaN for internal nodes, and a number for * leaf nodes. The aggregate sizes of internal nodes will be automatically * computed by the layout. * *
For example, if the tree data structure represents a file system, with * files as leaf nodes, and each file has a bytes attribute, you can * specify a size function as: * *
.size(function(d) d.bytes)* * This function will return undefined for internal nodes (since * these do not have a bytes attribute), and a number for leaf nodes. * *
Note that the built-in Math.sqrt and Math.log methods
* can be used as sizing functions. These function similarly to
* Number, except perform a root and log scale, respectively.
*
* @param {function} f the new sizing function.
* @function
* @name pv.Layout.sunburst.prototype.size
* @returns {pv.Layout.sunburst} this.
*/
mark.size = function(f) {
sizeof = f;
return this;
};
return mark;
};
// TODO add `by` function for determining size (and children?)
/**
* Returns a new treemap tree layout.
*
* @class A tree layout in the form of an treemap. Treemaps
* are a form of space-filling layout that represents nodes as boxes, with child
* nodes placed within parent boxes. The size of each box is proportional to the
* size of the node in the tree.
*
*
This particular algorithm is taken from Bruls, D.M., C. Huizing, and * J.J. van Wijk, "Squarified * Treemaps" in Data Visualization 2000, Proceedings of the Joint * Eurographics and IEEE TCVG Sumposium on Visualization, 2000, * pp. 33-42. * *
This tree layout is intended to be extended (see {@link pv.Mark#extend}) * by a {@link pv.Bar}. The data property returns an array of nodes for use by * other property functions. The following node attributes are supported: * *
.add(pv.Bar) * .extend(pv.Layout.treemap(tree))* * To display internal nodes, and color by depth, say: * *
.add(pv.Bar) * .extend(pv.Layout.treemap(tree).inset(10)) * .fillStyle(pv.Colors.category19().by(function(n) n.depth))* * The format of the tree argument is a hierarchical object whose leaf * nodes are numbers corresponding to their size. For an example, and * information on how to convert tabular data into such a tree, see * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function * can be specified to override how the tree is interpreted. This size function * can also be used to transform the data. * *
By default, the treemap fills the full width and height of the parent * panel, and only leaf nodes are rendered. If an {@link #inset} is specified, * internal nodes will be rendered, each inset from their parent by the * specified margins. Rounding can be enabled using {@link #round}. Finally, an * optional root key can be specified using {@link #root} for convenience. * * @param tree a tree (an object) who leaf attributes have sizes. * @returns {pv.Layout.treemap} a tree layout. */ pv.Layout.treemap = function(tree) { var keys = [], round, inset, sizeof = Number; /** @private */ function rnd(i) { return round ? Math.round(i) : i; } /** @private */ function accumulate(map) { var node = {size: 0, children: [], keys: keys.slice()}; for (var key in map) { var child = map[key], size = sizeof(child); keys.push(key); if (isNaN(size)) { child = accumulate(child); } else { child = {size: size, data: child, keys: keys.slice()}; } node.children.push(child); node.size += child.size; keys.pop(); } node.children.sort(function(a, b) { return a.size - b.size; }); return node; } /** @private */ function scale(node, k) { node.size *= k; if (node.children) { for (var i = 0; i < node.children.length; i++) { scale(node.children[i], k); } } } /** @private */ function ratio(row, l) { var rmax = -Infinity, rmin = Infinity, s = 0; for (var i = 0; i < row.length; i++) { var r = row[i].size; if (r < rmin) rmin = r; if (r > rmax) rmax = r; s += r; } s = s * s; l = l * l; return Math.max(l * rmax / s, s / (l * rmin)); } /** @private */ function squarify(node) { var row = [], mink = Infinity; var x = node.left + (inset ? inset.left : 0), y = node.top + (inset ? inset.top : 0), w = node.width - (inset ? inset.left + inset.right : 0), h = node.height - (inset ? inset.top + inset.bottom : 0), l = Math.min(w, h); scale(node, w * h / node.size); function position(row) { var s = pv.sum(row, function(node) { return node.size; }), hh = (l == 0) ? 0 : rnd(s / l); for (var i = 0, d = 0; i < row.length; i++) { var n = row[i], nw = rnd(n.size / hh); if (w == l) { n.left = x + d; n.top = y; n.width = nw; n.height = hh; } else { n.left = x; n.top = y + d; n.width = hh; n.height = nw; } d += nw; } if (w == l) { if (n) n.width += w - d; // correct rounding error y += hh; h -= hh; } else { if (n) n.height += h - d; // correct rounding error x += hh; w -= hh; } l = Math.min(w, h); } var children = node.children.slice(); // copy while (children.length > 0) { var child = children[children.length - 1]; if (child.size <= 0) { children.pop(); continue; } row.push(child); var k = ratio(row, l); if (k <= mink) { children.pop(); mink = k; } else { row.pop(); position(row); row.length = 0; mink = Infinity; } } if (row.length > 0) { position(row); } /* correct rounding error */ if (w == l) { for (var i = 0; i < row.length; i++) { row[i].width += w; } } else { for (var i = 0; i < row.length; i++) { row[i].height += h; } } } /** @private */ function layout(node) { if (node.children) { squarify(node); for (var i = 0; i < node.children.length; i++) { var child = node.children[i]; child.depth = node.depth + 1; layout(child); } } } /** @private */ function flatten(node, array) { if (node.children) { for (var i = 0; i < node.children.length; i++) { flatten(node.children[i], array); } } if (inset || !node.children) { array.push(node) } return array; } /** @private */ function data() { var root = accumulate(tree); root.left = 0; root.top = 0; root.width = this.parent.width(); root.height = this.parent.height(); root.depth = 0; layout(root); return flatten(root, []).reverse(); } /* A dummy mark, like an anchor, which the caller extends. */ var mark = new pv.Mark() .data(data) .left(function(n) { return n.left; }) .top(function(n) { return n.top; }) .width(function(n) { return n.width; }) .height(function(n) { return n.height; }); /** * Enables or disables rounding. When rounding is enabled, the left, top, * width and height properties will be rounded to integer pixel values. The * rounding algorithm uses error accumulation to ensure an exact fit. * * @param {boolean} v whether rounding should be enabled. * @function * @name pv.Layout.treemap.prototype.round * @returns {pv.Layout.treemap} this. */ mark.round = function(v) { round = v; return this; }; /** * Specifies the margins to inset child nodes from their parents; as a side * effect, this also enables the display of internal nodes, which are hidden * by default. If only a single argument is specified, this value is used to * inset all four sides. * * @param {number} top the top margin. * @param {number} [right] the right margin. * @param {number} [bottom] the bottom margin. * @param {number} [left] the left margin. * @function * @name pv.Layout.treemap.prototype.inset * @returns {pv.Layout.treemap} this. */ mark.inset = function(top, right, bottom, left) { if (arguments.length == 1) right = bottom = left = top; inset = {top:top, right:right, bottom:bottom, left:left}; return this; }; /** * Specifies the root key; optional. The root key is prepended to the * keys attribute for all generated nodes. This method is provided * for convenience and does not affect layout. * * @param {string} v the root key. * @function * @name pv.Layout.treemap.prototype.root * @returns {pv.Layout.treemap} this. */ mark.root = function(v) { keys = [v]; return this; }; /** * Specifies the sizing function. By default, the sizing function is * Number. The sizing function is invoked for each node in the tree * (passed to the constructor): the sizing function must return * undefined or NaN for internal nodes, and a number for * leaf nodes. The aggregate sizes of internal nodes will be automatically * computed by the layout. * *
For example, if the tree data structure represents a file system, with * files as leaf nodes, and each file has a bytes attribute, you can * specify a size function as: * *
.size(function(d) d.bytes)* * This function will return undefined for internal nodes (since * these do not have a bytes attribute), and a number for leaf nodes. * *
Note that the built-in Math.sqrt and Math.log methods * can be used as sizing functions. These function similarly to * Number, except perform a root and log scale, respectively. * * @param {function} f the new sizing function. * @function * @name pv.Layout.treemap.prototype.size * @returns {pv.Layout.treemap} this. */ mark.size = function(f) { sizeof = f; return this; }; return mark; }; return pv;}();/* * Parses the Protovis specifications on load, allowing the use of JavaScript * 1.8 function expressions on browsers that only support JavaScript 1.6. * * @see pv.parse */ pv.listen(window, "load", function() { var scripts = document.getElementsByTagName("script"); for (var i = 0; i < scripts.length; i++) { var s = scripts[i]; if (s.type == "text/javascript+protovis") { try { pv.Panel.$dom = s; window.eval(pv.parse(s.textContent || s.innerHTML)); // IE } catch (e) { pv.error(e); } delete pv.Panel.$dom; } } });