/** * @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 following conversion specifications are unsupported (for now): * * @see strptime * documentation. * @param {string} s the string to parse as a date. * @param {string} [format] an optional format string. * @returns {Date} the parsed date. */ Date.parse = function(s, format) { if (arguments.length == 1) { return Date.__parse__(s); } var year = 1970, month = 0, date = 1, hour = 0, minute = 0, second = 0; var fields = [function() {}]; format = format.replace(/[\\\^\$\*\+\?\[\]\(\)\.\{\}]/g, "\\$&"); format = format.replace(/%[a-zA-Z0-9]/g, function(s) { switch (s) { // TODO %a: day of week, either abbreviated or full name // TODO %A: same as %a case '%b': { fields.push(function(x) { month = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 }[x]; }); return "([A-Za-z]+)"; } case '%h': case '%B': { fields.push(function(x) { month = { January: 0, February: 1, March: 2, April: 3, May: 4, June: 5, July: 6, August: 7, September: 8, October: 9, November: 10, December: 11 }[x]; }); return "([A-Za-z]+)"; } // TODO %c: locale's appropriate date and time // TODO %C: century number[0,99] case '%e': case '%d': { fields.push(function(x) { date = x; }); return "([0-9]+)"; } // TODO %D: same as %m/%d/%y case '%H': { fields.push(function(x) { hour = x; }); return "([0-9]+)"; } // TODO %I: hour (12-hour clock) [1,12] // TODO %j: day number [1,366] case '%m': { fields.push(function(x) { month = x - 1; }); return "([0-9]+)"; } case '%M': { fields.push(function(x) { minute = x; }); return "([0-9]+)"; } // TODO %n: any white space // TODO %p: locale's equivalent of a.m. or p.m. // TODO %r: %I:%M:%S %p // TODO %R: %H:%M case '%S': { fields.push(function(x) { second = x; }); return "([0-9]+)"; } // TODO %t: any white space // TODO %T: %H:%M:%S // TODO %U: week number [00,53] // TODO %w: weekday [0,6] // TODO %W: week number [00, 53] // TODO %x: locale date (%m/%d/%y) // TODO %X: locale time (%I:%M:%S %p) case '%y': { fields.push(function(x) { x = Number(x); year = x + (((0 <= x) && (x < 69)) ? 2000 : (((x >= 69) && (x < 100) ? 1900 : 0))); }); return "([0-9]+)"; } case '%Y': { fields.push(function(x) { year = x; }); return "([0-9]+)"; } case '%%': { fields.push(function() {}); return "%"; } } return s; }); var match = s.match(format); if (match) match.forEach(function(m, i) { fields[i](m); }); return new Date(year, month, date, hour, minute, second); }; if (Date.prototype.toLocaleFormat) { Date.prototype.format = Date.prototype.toLocaleFormat; } else { /** * Converts a date to a string using the specified formatting. If the * Date object already supports the toLocaleFormat method, as * in Firefox, this is simply an alias to the built-in method. * *

The format string is in the same format expected by the strftime * function in C. The following conversion specifications are supported:

The following conversion specifications are unsupported (for now): * * @see Date.toLocaleFormat * documentation. * @see strftime * documentation. * @param {string} format a format string. * @returns {string} the formatted date. */ Date.prototype.format = function(format) { function pad(n, p) { return (n < 10) ? (p || "0") + n : n; } var d = this; return format.replace(/%[a-zA-Z0-9]/g, function(s) { switch (s) { case '%a': return [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ][d.getDay()]; case '%A': return [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ][d.getDay()]; case '%h': case '%b': return [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ][d.getMonth()]; case '%B': return [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ][d.getMonth()]; case '%c': return d.toLocaleString(); case '%C': return pad(Math.floor(d.getFullYear() / 100) % 100); case '%d': return pad(d.getDate()); case '%x': case '%D': return pad(d.getMonth() + 1) + "/" + pad(d.getDate()) + "/" + pad(d.getFullYear() % 100); case '%e': return pad(d.getDate(), " "); case '%H': return pad(d.getHours()); case '%I': { var h = d.getHours() % 12; return h ? pad(h) : 12; } // TODO %j: day of year as a decimal number [001,366] case '%m': return pad(d.getMonth() + 1); case '%M': return pad(d.getMinutes()); case '%n': return "\n"; case '%p': return d.getHours() < 12 ? "AM" : "PM"; case '%T': case '%X': case '%r': { var h = d.getHours() % 12; return (h ? pad(h) : 12) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds()) + " " + (d.getHours() < 12 ? "AM" : "PM"); } case '%R': return pad(d.getHours()) + ":" + pad(d.getMinutes()); case '%S': return pad(d.getSeconds()); case '%t': return "\t"; case '%u': { var w = d.getDay(); return w ? w : 1; } // TODO %U: week number (sunday first day) [00,53] // TODO %V: week number (monday first day) [01,53] ... with weirdness case '%w': return d.getDay(); // TODO %W: week number (monday first day) [00,53] ... with weirdness case '%y': return pad(d.getFullYear() % 100); case '%Y': return d.getFullYear(); // TODO %Z: timezone name or abbreviation case '%%': return "%"; } return s; }); }; } var pv = function() {/** * The top-level Protovis namespace. All public methods and fields should be * registered on this object. Note that core Protovis source is surrounded by an * anonymous function, so any other declared globals will not be visible outside * of core methods. This also allows multiple versions of Protovis to coexist, * since each version will see their own pv namespace. * * @namespace The top-level Protovis namespace, pv. */ var pv = {}; /** * @private Returns a prototype object suitable for extending the given class * f. Rather than constructing a new instance of f to serve as * the prototype (which unnecessarily runs the constructor on the created * prototype object, potentially polluting it), an anonymous function is * generated internally that shares the same prototype: * *
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:

return [1, 2, 3, "a", "b", "c"]. * * @param {array[]} arrays an array of arrays. * @returns {array} an array containing all the elements of each array in * arrays. */ pv.blend = function(arrays) { return Array.prototype.concat.apply([], arrays); }; /** * Given the specified array of arrays, transposes each element * arrayij with arrayji. If the array has dimensions * n×m, it will have dimensions m×n * after this method returns. This method transposes the elements of the array * in place, mutating the array, and returning a reference to the array. * * @param {array[]} arrays an array of arrays. * @returns {array[]} the passed-in array, after transposing the elements. */ pv.transpose = function(arrays) { var n = arrays.length, m = pv.max(arrays, function(d) { return d.length; }); if (m > n) { arrays.length = m; for (var i = n; i < m; i++) { arrays[i] = new Array(n); } for (var i = 0; i < n; i++) { for (var j = i + 1; j < m; j++) { var t = arrays[i][j]; arrays[i][j] = arrays[j][i]; arrays[j][i] = t; } } } else { for (var i = 0; i < m; i++) { arrays[i].length = n; } for (var i = 0; i < n; i++) { for (var j = 0; j < i; j++) { var t = arrays[i][j]; arrays[i][j] = arrays[j][i]; arrays[j][i] = t; } } } arrays.length = m; for (var i = 0; i < m; i++) { arrays[i].length = n; } return arrays; }; /** * Returns all of the property names (keys) of the specified object (a map). The * order of the returned array is not defined. * * @param map an object. * @returns {string[]} an array of strings corresponding to the keys. * @see #entries */ pv.keys = function(map) { var array = []; for (var key in map) { array.push(key); } return array; }; /** * Returns all of the entries (key-value pairs) of the specified object (a * map). The order of the returned array is not defined. Each key-value pair is * represented as an object with key and value attributes, * e.g., {key: "foo", value: 42}. * * @param map an object. * @returns {array} an array of key-value pairs corresponding to the keys. */ pv.entries = function(map) { var array = []; for (var key in map) { array.push({ key: key, value: map[key] }); } return array; }; /** * Returns all of the values (attribute values) of the specified object (a * map). The order of the returned array is not defined. * * @param map an object. * @returns {array} an array of objects corresponding to the values. * @see #entries */ pv.values = function(map) { var array = []; for (var key in map) { array.push(map[key]); } return array; }; /** * @private A private variant of Array.prototype.map that supports the index * property. */ function map(array, f) { var o = {}; return f ? array.map(function(d, i) { o.index = i; return f.call(o, d); }) : array.slice(); }; /** * Returns a normalized copy of the specified array, such that the sum of the * returned elements sum to one. If the specified array is not an array of * numbers, an optional accessor function f can be specified to map the * elements to numbers. For example, if array is an array of objects, * and each object has a numeric property "foo", the expression * *
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
* f(x) = x / 100 * 640
* f(x) = x * 6.4
*
* * Thus, saying * *
.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. Most commonly, a log scale represents * a 1-dimensional log 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) = (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
* f(x) = log(x) / 2 * 640
* f(x) = log(x) * 320
*
* * Thus, saying * *
.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:

The SVG 1.0 color keywords names are also supported, such as "aliceblue" * and "yellowgreen". The "transparent" keyword is supported for a * fully-transparent color. * *

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: * *
#1f77b4
*
#ff7f0e
*
#2ca02c
*
#d62728
*
#9467bd
*
#8c564b
*
#e377c2
*
#7f7f7f
*
#bcbd22
*
#17becf
* * @param {number...} domain... domain values. * @returns {pv.Scale.ordinal} a new ordinal color scale. * @see pv.color */ pv.Colors.category10 = function() { var scale = pv.colors( "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"); scale.domain.apply(scale, arguments); return scale; }; /** * Returns a new 20-color scheme. The arguments to this constructor are * optional, and equivalent to calling {@link pv.Scale.OrdinalScale#domain}. The * following colors are used: * *
#1f77b4
*
#aec7e8
*
#ff7f0e
*
#ffbb78
*
#2ca02c
*
#98df8a
*
#d62728
*
#ff9896
*
#9467bd
*
#c5b0d5
*
#8c564b
*
#c49c94
*
#e377c2
*
#f7b6d2
*
#7f7f7f
*
#c7c7c7
*
#bcbd22
*
#dbdb8d
*
#17becf
*
#9edae5
* * @param {number...} domain... domain values. * @returns {pv.Scale.ordinal} a new ordinal color scale. * @see pv.color */ pv.Colors.category20 = function() { var scale = pv.colors( "#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c", "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5", "#8c564b", "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", "#c7c7c7", "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"); scale.domain.apply(scale, arguments); return scale; }; /** * Returns a new alternative 19-color scheme. The arguments to this constructor * are optional, and equivalent to calling * {@link pv.Scale.OrdinalScale#domain}. The following colors are used: * *
#9c9ede
*
#7375b5
*
#4a5584
*
#cedb9c
*
#b5cf6b
*
#8ca252
*
#637939
*
#e7cb94
*
#e7ba52
*
#bd9e39
*
#8c6d31
*
#e7969c
*
#d6616b
*
#ad494a
*
#843c39
*
#de9ed6
*
#ce6dbd
*
#a55194
*
#7b4173
* * @param {number...} domain... domain values. * @returns {pv.Scale.ordinal} a new ordinal color scale. * @see pv.color */ pv.Colors.category19 = function() { var scale = pv.colors( "#9c9ede", "#7375b5", "#4a5584", "#cedb9c", "#b5cf6b", "#8ca252", "#637939", "#e7cb94", "#e7ba52", "#bd9e39", "#8c6d31", "#e7969c", "#d6616b", "#ad494a", "#843c39", "#de9ed6", "#ce6dbd", "#a55194", "#7b4173"); scale.domain.apply(scale, arguments); return scale; }; /** * Returns a linear color ramp from the specified start color to the * specified end color. The color arguments may be specified either as * strings or as {@link pv.Color}s. * * @param {string} start the start color; may be a pv.Color. * @param {string} end the end color; may be a pv.Color. * @returns {Function} a color ramp from start to end. * @see pv.Scale.linear */ pv.ramp = function(start, end) { var scale = pv.Scale.linear(); scale.range.apply(scale, arguments); return scale; }; // TODO don't populate default attributes? /** * @private * @namespace */ pv.Scene = pv.SvgScene = {}; /** * Updates the display for the specified array of scene nodes. * * @param scenes {array} an array of scene nodes. */ pv.SvgScene.updateAll = function(scenes) { if (!scenes.length) return; if ((scenes[0].reverse) && (scenes.type != "line") && (scenes.type != "area")) { var reversed = pv.extend(scenes); for (var i = 0, j = scenes.length - 1; j >= 0; i++, j--) { reversed[i] = scenes[j]; } scenes = reversed; } this.removeSiblings(this[scenes.type](scenes)); }; /** * Creates a new SVG element of the specified type. * * @param type {string} an SVG element type, such as "rect". * @return a new SVG element. */ pv.SvgScene.create = function(type) { return document.createElementNS(pv.ns.svg, type); }; /** * Expects the element e to be the specified type. If the element does * not exist, a new one is created. If the element does exist but is the wrong * type, it is replaced with the specified element. * * @param type {string} an SVG element type, such as "rect". * @return a new SVG element. */ pv.SvgScene.expect = function(type, e) { if (!e) return this.create(type); if (e.tagName == "a") e = e.firstChild; if (e.tagName == type) return e; var n = this.create(type); e.parentNode.replaceChild(n, e); return n; }; /** TODO */ pv.SvgScene.append = function(e, scenes, index) { e.$scene = {scenes:scenes, index:index}; e = this.title(e, scenes[index]); if (!e.parentNode) scenes.$g.appendChild(e); return e.nextSibling; }; /** * Applies a title tooltip to the specified element e, using the * title property of the specified scene node s. Note that * this implementation does not create an SVG title element as a child * of e; although this is the recommended standard, it is only * supported in Opera. Instead, an anchor element is created around the element * e, and the xlink:title attribute is set accordingly. * * @param e an SVG element. * @param s a scene node. */ pv.SvgScene.title = function(e, s) { var a = e.parentNode, t = String(s.title); if (a && (a.tagName != "a")) a = null; if (t) { if (!a) { a = this.create("a"); if (e.parentNode) e.parentNode.replaceChild(a, e); a.appendChild(e); } a.setAttributeNS(pv.ns.xlink, "title", t); return a; } if (a) a.parentNode.replaceChild(e, a); return e; }; /** TODO */ pv.SvgScene.dispatch = function(e) { var t = e.target.$scene; if (t) { t.scenes.mark.dispatch(e.type, t.scenes, t.index); e.preventDefault(); } }; /** TODO */ pv.SvgScene.removeSiblings = function(e) { while (e) { var n = e.nextSibling; e.parentNode.removeChild(e); e = n; } }; // TODO strokeStyle for areaSegment? pv.SvgScene.area = function(scenes) { var e = scenes.$g.firstChild; if (!scenes.length) return e; var s = scenes[0]; /* segmented */ if (s.segmented) return this.areaSegment(scenes); /* visible */ if (!s.visible) return e; var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); if (!fill.opacity && !stroke.opacity) return e; /* points */ var p1 = "", p2 = ""; for (var i = 0, j = scenes.length - 1; j >= 0; i++, j--) { var si = scenes[i], sj = scenes[j]; p1 += si.left + "," + si.top + " "; p2 += (sj.left + sj.width) + "," + (sj.top + sj.height) + " "; /* interpolate (assume linear by default) */ if (i < scenes.length - 1) { var sk = scenes[i + 1], sl = scenes[j - 1]; switch (s.interpolate) { case "step-before": { p1 += si.left + "," + sk.top + " "; p2 += (sl.left + sl.width) + "," + (sj.top + sj.height) + " "; break; } case "step-after": { p1 += sk.left + "," + si.top + " "; p2 += (sj.left + sj.width) + "," + (sl.top + sl.height) + " "; break; } } } } e = this.expect("polygon", e); e.setAttribute("cursor", s.cursor); e.setAttribute("points", p1 + p2); var fill = pv.color(s.fillStyle); e.setAttribute("fill", fill.color); e.setAttribute("fill-opacity", fill.opacity); var stroke = pv.color(s.strokeStyle); e.setAttribute("stroke", stroke.color); e.setAttribute("stroke-opacity", stroke.opacity); e.setAttribute("stroke-width", s.lineWidth); return this.append(e, scenes, 0); }; pv.SvgScene.areaSegment = function(scenes) { var e = scenes.$g.firstChild; for (var i = 0, n = scenes.length - 1; i < n; i++) { var s1 = scenes[i], s2 = scenes[i + 1]; /* visible */ if (!s1.visible || !s2.visible) continue; var fill = pv.color(s1.fillStyle), stroke = pv.color(s1.strokeStyle); if (!fill.opacity && !stroke.opacity) continue; /* points */ var p = s1.left + "," + s1.top + " " + s2.left + "," + s2.top + " " + (s2.left + s2.width) + "," + (s2.top + s2.height) + " " + (s1.left + s1.width) + "," + (s1.top + s1.height); e = this.expect("polygon", e); e.setAttribute("cursor", s1.cursor); e.setAttribute("points", p); e.setAttribute("fill", fill.color); e.setAttribute("fill-opacity", fill.opacity); e.setAttribute("stroke", stroke.color); e.setAttribute("stroke-opacity", stroke.opacity); e.setAttribute("stroke-width", s1.lineWidth); e = this.append(e, scenes, i); } return e; }; pv.SvgScene.bar = function(scenes) { var e = scenes.$g.firstChild; for (var i = 0; i < scenes.length; i++) { var s = scenes[i]; /* visible */ if (!s.visible) continue; var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); if (!fill.opacity && !stroke.opacity) continue; e = this.expect("rect", e); e.setAttribute("cursor", s.cursor); e.setAttribute("x", s.left); e.setAttribute("y", s.top); e.setAttribute("width", Math.max(1E-10, s.width)); e.setAttribute("height", Math.max(1E-10, s.height)); e.setAttribute("fill", fill.color); e.setAttribute("fill-opacity", fill.opacity); e.setAttribute("stroke", stroke.color); e.setAttribute("stroke-opacity", stroke.opacity); e.setAttribute("stroke-width", s.lineWidth); e = this.append(e, scenes, i); } return e; }; pv.SvgScene.dot = function(scenes) { var e = scenes.$g.firstChild; for (var i = 0; i < scenes.length; i++) { var s = scenes[i]; /* visible */ if (!s.visible) continue; var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); if (!fill.opacity && !stroke.opacity) continue; /* points */ var radius = Math.sqrt(s.size), fillPath = "", strokePath = ""; switch (s.shape) { case "cross": { fillPath = "M" + -radius + "," + -radius + "L" + radius + "," + radius + "M" + radius + "," + -radius + "L" + -radius + "," + radius; break; } case "triangle": { var h = radius, w = radius * 2 / Math.sqrt(3); fillPath = "M0," + h + "L" + w +"," + -h + " " + -w + "," + -h + "Z"; break; } case "diamond": { radius *= Math.sqrt(2); fillPath = "M0," + -radius + "L" + radius + ",0" + " 0," + radius + " " + -radius + ",0" + "Z"; break; } case "square": { fillPath = "M" + -radius + "," + -radius + "L" + radius + "," + -radius + " " + radius + "," + radius + " " + -radius + "," + radius + "Z"; break; } case "tick": { fillPath = "M0,0L0," + -s.size; break; } default: { function circle(r) { return "M0," + r + "A" + r + "," + r + " 0 1,1 0," + (-r) + "A" + r + "," + r + " 0 1,1 0," + r + "Z"; } if (s.lineWidth / 2 > radius) strokePath = circle(s.lineWidth); fillPath = circle(radius); break; } } /* transform */ var transform = "translate(" + s.left + "," + s.top + ")" + (s.angle ? " rotate(" + 180 * s.angle / Math.PI + ")" : ""); /* The normal fill path. */ e = this.expect("path", e); e.setAttribute("d", fillPath); e.setAttribute("transform", transform); e.setAttribute("fill", fill.color); e.setAttribute("fill-opacity", fill.opacity); e.setAttribute("cursor", s.cursor); if (strokePath) { e.setAttribute("stroke", "none"); } else { e.setAttribute("stroke", stroke.color); e.setAttribute("stroke-opacity", stroke.opacity); e.setAttribute("stroke-width", s.lineWidth); } e = this.append(e, scenes, i); /* The special-case stroke path. */ if (strokePath) { e = this.expect("path", e); e.setAttribute("d", strokePath); e.setAttribute("transform", transform); e.setAttribute("fill", stroke.color); e.setAttribute("fill-opacity", stroke.opacity); e.setAttribute("cursor", s.cursor); e = this.append(e, scenes, i); } } return e; }; pv.SvgScene.image = function(scenes) { var e = scenes.$g.firstChild; for (var i = 0; i < scenes.length; i++) { var s = scenes[i]; /* visible */ if (!s.visible) continue; /* fill */ e = this.fill(e, scenes, i); /* image */ e = this.expect("image", e); e.setAttribute("preserveAspectRatio", "none"); e.setAttribute("x", s.left); e.setAttribute("y", s.top); e.setAttribute("width", s.width); e.setAttribute("height", s.height); e.setAttribute("cursor", s.cursor); e.setAttributeNS(pv.ns.xlink, "href", s.url); e = this.append(e, scenes, i); /* stroke */ e = this.stroke(e, scenes, i); } return e; }; pv.SvgScene.label = function(scenes) { var e = scenes.$g.firstChild; for (var i = 0; i < scenes.length; i++) { var s = scenes[i]; /* visible */ if (!s.visible) continue; var fill = pv.color(s.textStyle); if (!fill.opacity) continue; /* text-baseline, text-align */ var x = 0, y = 0, dy = 0, anchor = "start"; switch (s.textBaseline) { case "middle": dy = ".35em"; break; case "top": dy = ".71em"; y = s.textMargin; break; case "bottom": y = "-" + s.textMargin; break; } switch (s.textAlign) { case "right": anchor = "end"; x = "-" + s.textMargin; break; case "center": anchor = "middle"; break; case "left": x = s.textMargin; break; } e = this.expect("text", e); e.setAttribute("pointer-events", "none"); e.setAttribute("x", x); e.setAttribute("y", y); e.setAttribute("dy", dy); e.setAttribute("text-anchor", anchor); e.setAttribute("transform", "translate(" + s.left + "," + s.top + ")" + (s.textAngle ? " rotate(" + 180 * s.textAngle / Math.PI + ")" : "")); e.setAttribute("fill", fill.color); e.setAttribute("fill-opacity", fill.opacity); e.style.font = s.font; e.style.textShadow = s.textShadow; if (e.firstChild) e.firstChild.nodeValue = s.text; else e.appendChild(document.createTextNode(s.text)); e = this.append(e, scenes, i); } return e; }; // TODO fillStyle for lineSegment? // TODO lineOffset for flow maps? pv.SvgScene.line = function(scenes) { var e = scenes.$g.firstChild; if (scenes.length < 2) return e; var s = scenes[0]; /* segmented */ if (s.segmented) return this.lineSegment(scenes); /* visible */ if (!s.visible) return e; var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); if (!fill.opacity && !stroke.opacity) return e; /* points */ var p = ""; for (var i = 0; i < scenes.length; i++) { var si = scenes[i]; p += si.left + "," + si.top + " "; /* interpolate (assume linear by default) */ if (i < scenes.length - 1) { var sj = scenes[i + 1]; switch (s.interpolate) { case "step-before": { p += si.left + "," + sj.top + " "; break; } case "step-after": { p += sj.left + "," + si.top + " "; break; } } } } e = this.expect("polyline", e); e.setAttribute("cursor", s.cursor); e.setAttribute("points", p); e.setAttribute("fill", fill.color); e.setAttribute("fill-opacity", fill.opacity); e.setAttribute("stroke", stroke.color); e.setAttribute("stroke-opacity", stroke.opacity); e.setAttribute("stroke-width", s.lineWidth); return this.append(e, scenes, 0); }; pv.SvgScene.lineSegment = function(scenes) { var e = scenes.$g.firstChild; for (var i = 0, n = scenes.length - 1; i < n; i++) { var s1 = scenes[i], s2 = scenes[i + 1]; /* visible */ if (!s1.visible || !s2.visible) continue; var stroke = pv.color(s1.strokeStyle); if (!stroke.opacity) continue; /* Line-line intersection, per Akenine-Moller 16.16.1. */ function intersect(o1, d1, o2, d2) { return o1.plus(d1.times(o2.minus(o1).dot(d2.perp()) / d1.dot(d2.perp()))); } /* * P1-P2 is the current line segment. V is a vector that is perpendicular to * the line segment, and has length lineWidth / 2. ABCD forms the initial * bounding box of the line segment (i.e., the line segment if we were to do * no joins). */ var p1 = pv.vector(s1.left, s1.top), p2 = pv.vector(s2.left, s2.top), p = p2.minus(p1), v = p.perp().norm(), w = v.times(s1.lineWidth / 2), a = p1.plus(w), b = p2.plus(w), c = p2.minus(w), d = p1.minus(w); /* * Start join. P0 is the previous line segment's start point. We define the * cutting plane as the average of the vector perpendicular to P0-P1, and * the vector perpendicular to P1-P2. This insures that the cross-section of * the line on the cutting plane is equal if the line-width is unchanged. * Note that we don't implement miter limits, so these can get wild. */ if (i > 0) { var s0 = scenes[i - 1]; if (s0.visible) { var v1 = p1.minus(s0.left, s0.top).perp().norm().plus(v); d = intersect(p1, v1, d, p); a = intersect(p1, v1, a, p); } } /* Similarly, for end join. */ if (i < (n - 1)) { var s3 = scenes[i + 2]; if (s3.visible) { var v2 = pv.vector(s3.left, s3.top).minus(p2).perp().norm().plus(v); c = intersect(p2, v2, c, p); b = intersect(p2, v2, b, p); } } /* points */ var p = a.x + "," + a.y + " " + b.x + "," + b.y + " " + c.x + "," + c.y + " " + d.x + "," + d.y; e = this.expect("polygon", e); e.setAttribute("cursor", s1.cursor); e.setAttribute("points", p); e.setAttribute("fill", stroke.color); e.setAttribute("fill-opacity", stroke.opacity); e = this.append(e, scenes, i); } return e; }; var guid = 0; pv.SvgScene.panel = function(scenes) { var g = scenes.$g, e = g && g.firstChild; for (var i = 0; i < scenes.length; i++) { var s = scenes[i]; /* visible */ if (!s.visible) continue; /* svg */ if (!scenes.parent) { s.canvas.style.display = "inline-block"; g = s.canvas.firstChild; if (!g) { g = s.canvas.appendChild(this.create("svg")); g.onclick = g.onmousedown = g.onmouseup = g.onmousemove = g.onmouseout = g.onmouseover = pv.SvgScene.dispatch; } scenes.$g = g; g.setAttribute("width", s.width + s.left + s.right); g.setAttribute("height", s.height + s.top + s.bottom); if (typeof e == "undefined") e = g.firstChild; } /* clip (nest children) */ if (s.overflow == "hidden") { var c = this.expect("g", e), id = (guid++).toString(36); c.setAttribute("clip-path", "url(#" + id + ")"); if (!c.parentNode) g.appendChild(c); scenes.$g = g = c; e = c.firstChild; e = this.expect("clipPath", e); e.setAttribute("id", id); var r = e.firstChild || e.appendChild(this.create("rect")); r.setAttribute("x", s.left); r.setAttribute("y", s.top); r.setAttribute("width", s.width); r.setAttribute("height", s.height); if (!e.parentNode) g.appendChild(e); e = e.nextSibling; } /* fill */ e = this.fill(e, scenes, i); /* children */ for (var j = 0; j < s.children.length; j++) { s.children[j].$g = e = this.expect("g", e); e.setAttribute("transform", "translate(" + s.left + "," + s.top + ")"); this.updateAll(s.children[j]); if (!e.parentNode) g.appendChild(e); e = e.nextSibling; } /* stroke */ e = this.stroke(e, scenes, i); /* clip (restore group) */ if (s.overflow == "hidden") { scenes.$g = g = c.parentNode; e = c.nextSibling; } } return e; }; pv.SvgScene.fill = function(e, scenes, i) { var s = scenes[i], fill = pv.color(s.fillStyle); if (fill.opacity) { e = this.expect("rect", e); e.setAttribute("x", s.left); e.setAttribute("y", s.top); e.setAttribute("width", s.width); e.setAttribute("height", s.height); e.setAttribute("cursor", s.cursor); e.setAttribute("fill", fill.color); e.setAttribute("fill-opacity", fill.opacity); e = this.append(e, scenes, i); } return e; }; pv.SvgScene.stroke = function(e, scenes, i) { var s = scenes[i], stroke = pv.color(s.strokeStyle); if (stroke.opacity) { e = this.expect("rect", e); e.setAttribute("x", s.left); e.setAttribute("y", s.top); e.setAttribute("width", Math.max(1E-10, s.width)); e.setAttribute("height", Math.max(1E-10, s.height)); e.setAttribute("cursor", s.cursor); e.setAttribute("fill", "none"); e.setAttribute("stroke", stroke.color); e.setAttribute("stroke-opacity", stroke.opacity); e.setAttribute("stroke-width", s.lineWidth); e = this.append(e, scenes, i); } return e; }; pv.SvgScene.rule = function(scenes) { var e = scenes.$g.firstChild; for (var i = 0; i < scenes.length; i++) { var s = scenes[i]; /* visible */ if (!s.visible) continue; var stroke = pv.color(s.strokeStyle); if (!stroke.opacity) continue; e = this.expect("line", e); e.setAttribute("cursor", s.cursor); e.setAttribute("x1", s.left); e.setAttribute("y1", s.top); e.setAttribute("x2", s.left + s.width); e.setAttribute("y2", s.top + s.height); e.setAttribute("stroke", stroke.color); e.setAttribute("stroke-opacity", stroke.opacity); e.setAttribute("stroke-width", s.lineWidth); e = this.append(e, scenes, i); } return e; }; pv.SvgScene.wedge = function(scenes) { var e = scenes.$g.firstChild; for (var i = 0; i < scenes.length; i++) { var s = scenes[i]; /* visible */ if (!s.visible) continue; var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); if (!fill.opacity && !stroke.opacity) continue; /* points */ var r1 = s.innerRadius, r2 = s.outerRadius, a = Math.abs(s.angle), p; if (a >= 2 * Math.PI) { if (r1) { p = "M0," + r2 + "A" + r2 + "," + r2 + " 0 1,1 0," + (-r2) + "A" + r2 + "," + r2 + " 0 1,1 0," + r2 + "M0," + r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + (-r1) + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + "Z"; } else { p = "M0," + r2 + "A" + r2 + "," + r2 + " 0 1,1 0," + (-r2) + "A" + r2 + "," + r2 + " 0 1,1 0," + r2 + "Z"; } } else { var sa = Math.min(s.startAngle, s.endAngle), ea = Math.max(s.startAngle, s.endAngle), c1 = Math.cos(sa), c2 = Math.cos(ea), s1 = Math.sin(sa), s2 = Math.sin(ea); if (r1) { p = "M" + r2 * c1 + "," + r2 * s1 + "A" + r2 + "," + r2 + " 0 " + ((a < Math.PI) ? "0" : "1") + ",1 " + r2 * c2 + "," + r2 * s2 + "L" + r1 * c2 + "," + r1 * s2 + "A" + r1 + "," + r1 + " 0 " + ((a < Math.PI) ? "0" : "1") + ",0 " + r1 * c1 + "," + r1 * s1 + "Z"; } else { p = "M" + r2 * c1 + "," + r2 * s1 + "A" + r2 + "," + r2 + " 0 " + ((a < Math.PI) ? "0" : "1") + ",1 " + r2 * c2 + "," + r2 * s2 + "L0,0Z"; } } e = this.expect("path", e); e.setAttribute("fill-rule", "evenodd"); e.setAttribute("cursor", s.cursor); e.setAttribute("transform", "translate(" + s.left + "," + s.top + ")"); e.setAttribute("d", p); e.setAttribute("fill", fill.color); e.setAttribute("fill-opacity", fill.opacity); e.setAttribute("stroke", stroke.color); e.setAttribute("stroke-opacity", stroke.opacity); e.setAttribute("stroke-width", s.lineWidth); e = this.append(e, scenes, i); } return e; }; /** * Constructs a new mark with default properties. Marks, with the exception of * the root panel, are not typically constructed directly; instead, they are * added to a panel or an existing mark via {@link pv.Mark#add}. * * @class Represents a data-driven graphical mark. The Mark class is * the base class for all graphical marks in Protovis; it does not provide any * specific rendering functionality, but together with {@link Panel} establishes * the core framework. * *

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:

    * *
  1. If the width property is not specified (i.e., null), its value * is the width of the parent panel, minus this mark's left and right margins; * the left and right margins are zero if not specified. * *
  2. Otherwise, if the right margin is not specified, its value is * the width of the parent panel, minus this mark's width and left margin; the * left margin is zero if not specified. * *
  3. Otherwise, if the left property is not specified, its value is * the width of the parent panel, minus this mark's width and the right margin. * *
This prioritization is then duplicated for the height, * bottom and top properties, respectively. * *

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:

    * *
  1. If invoked with a Function argument, this function is evaluated * for each associated datum. The return value of the function is used as the * computed property value. The context of the function (this) is this * mark. The arguments to the function are the associated data of this mark and * any enclosing panels. For example, a linear encoding of numerical data to * height is specified as * *
    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)).

    * *

  2. If invoked with a non-function argument, the property is treated as a * constant. The return value of the property method (e.g., m.height) * is this mark.

    * *

  3. If invoked with no arguments, the computed property value for the current * mark instance in the scene graph is returned. This facilitates property * chaining, where one mark's properties are defined in terms of another's. * For example, to offset a mark's location from its prototype, you might say * *
    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:

Since Protovis does not specify any concept of focus, it does not * support key events; these should be handled outside the visualization using * standard JavaScript. In the future, support for interaction may be extended * to support additional event types, particularly those most relevant to * interactive visualization, such as selection. * *

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:

In addition to positioning properties (left, right, top bottom), the * anchors support text rendering properties (text-align, text-baseline). Text is * rendered to appear inside the area polygon. * *

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:

In addition to positioning properties (left, right, top bottom), the * anchors support text rendering properties (text-align, text-baseline). Text * is rendered to appear inside the bar. * *

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:

These shapes can be further changed using the {@link #angle} property; * for instance, a cross can be turned into a plus by rotating. Similarly, the * tick, which is vertical by default, can be rotated horizontally. Note that * some shapes (cross and tick) do not have interior areas, and thus do not * support fill style meaningfully. * *

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:

In addition to positioning properties (left, right, top bottom), the * anchors support text rendering properties (text-align, text-baseline). Text is * rendered to appear outside the dot. Note that this behavior is different from * other mark anchors, which default to rendering text inside the mark. * *

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:

The default horizontal alignment is left. * * @type string * @name pv.Label.prototype.textAlign */ /** * The vertical text alignment. One of:The default vertical alignment is bottom. * * @type string * @name pv.Label.prototype.textBaseline */ /** * The text margin; may be specified in pixels, or in font-dependent units (such * as ".1ex"). The margin can be used to pad text away from its anchor location, * in a direction dependent on the horizontal and vertical alignment * properties. For example, if the text is left- and middle-aligned, the margin * shifts the text to the right. The default margin is 3 pixels. * * @type number * @name pv.Label.prototype.textMargin */ /** * A list of shadow effects to be applied to text, per the CSS Text Level 3 * text-shadow property. An example specification is "0.1em 0.1em 0.1em * rgba(0,0,0,.5)"; the first length is the horizontal offset, the second the * vertical offset, and the third the blur radius. * * @see CSS3 text * @type string * @name pv.Label.prototype.textShadow */ /** * Default properties for labels. See the individual properties for the default * values. * * @type pv.Label */ pv.Label.prototype.defaults = new pv.Label() .extend(pv.Mark.prototype.defaults) .text(pv.identity) .font("10px sans-serif") .textAngle(0) .textStyle("black") .textAlign("left") .textBaseline("bottom") .textMargin(3); /** * Constructs a new line mark with default properties. Lines are not typically * constructed directly, but by adding to a panel or an existing mark via * {@link pv.Mark#add}. * * @class Represents a series of connected line segments, or polyline, * that can be stroked with a configurable color and thickness. Each * articulation point in the line corresponds to a datum; for n points, * n-1 connected line segments are drawn. The point is positioned using * the box model. Arbitrary paths are also possible, allowing radar plots and * other custom visualizations. * *

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: * * * * * * * * * * * * * * * * * * * * * *
PropertiesOrientation
leftvertical
rightvertical
left, bottom, topvertical
right, bottom, topvertical
tophorizontal
bottomhorizontal
top, left, righthorizontal
bottom, left, righthorizontal
left, top, heightvertical
left, bottom, heightvertical
right, top, heightvertical
right, bottom, heightvertical
left, top, widthhorizontal
left, bottom, widthhorizontal
right, top, widthhorizontal
right, bottom, widthhorizontal
* *

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:

In addition to positioning properties (left, right, top bottom), the * anchors support text rendering properties (text-align, text-baseline). Text is * rendered to appear outside the rule. Note that this behavior is different * from other mark anchors, which default to rendering text inside the * mark. * *

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:

The current implementation creates the SVG element, if necessary, during * the build phase; in the future, it may be preferrable to move this to the * update phase, although then the canvas property would be undefined. In * addition, DOM inspection is necessary to define the implied width and height * properties that may be inferred from the DOM. * * @param s a node in the scene graph; the instance of the panel to build. */ pv.Panel.prototype.buildImplied = function(s) { if (!this.parent) { var c = s.canvas; if (c) { if (typeof c == "string") c = document.getElementById(c); /* Clear the container if it's not associated with this panel. */ if (c.$panel != this) { c.$panel = this; c.innerHTML = ""; } /* If width and height weren't specified, inspect the container. */ var w, h; if (s.width == null) { w = parseFloat(pv.css(c, "width")); s.width = w - s.left - s.right; } if (s.height == null) { h = parseFloat(pv.css(c, "height")); s.height = h - s.top - s.bottom; } } else if (s.$canvas) { /* * If the canvas property is null, and we previously created a canvas for * this scene node, reuse the previous canvas rather than creating a new * one. */ c = s.$canvas; } else { /** * Returns the last element in the current document's body. The canvas * element is appended to this last element if another DOM element has not * already been specified via the $dom field. */ function lastElement() { var node = document.body; while (node.lastChild && node.lastChild.tagName) { node = node.lastChild; } return (node == document.body) ? node : node.parentNode; } /* Insert a new container into the DOM. */ c = s.$canvas = document.createElement("span"); this.$dom // script element for text/javascript+protovis ? this.$dom.parentNode.insertBefore(c, this.$dom) : lastElement().appendChild(c); } s.canvas = c; } pv.Bar.prototype.buildImplied.call(this, s); }; /** * Constructs a new dot mark with default properties. Images are not typically * constructed directly, but by adding to a panel or an existing mark via * {@link pv.Mark#add}. * * @class Represents an image. Images share the same layout and style properties as * bars, in conjunction with an external image such as PNG or JPEG. The image is * specified via the {@link #url} property. The fill, if specified, appears * beneath the image, while the optional stroke appears above the image. * *

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:

In addition to positioning properties (left, right, top bottom), the * anchors support text rendering properties (text-align, text-baseline, * textAngle). Text is rendered to appear inside the wedge. * * @param {string} name the anchor name; either a string or a property function. * @returns {pv.Anchor} */ pv.Wedge.prototype.anchor = function(name) { var w = this; return pv.Mark.prototype.anchor.call(this, name) .left(function() { switch (this.name()) { case "outer": return w.left() + w.outerRadius() * Math.cos(w.midAngle()); case "inner": return w.left() + w.innerRadius() * Math.cos(w.midAngle()); case "start": return w.left() + w.midRadius() * Math.cos(w.startAngle()); case "center": return w.left() + w.midRadius() * Math.cos(w.midAngle()); case "end": return w.left() + w.midRadius() * Math.cos(w.endAngle()); } }) .right(function() { switch (this.name()) { case "outer": return w.right() + w.outerRadius() * Math.cos(w.midAngle()); case "inner": return w.right() + w.innerRadius() * Math.cos(w.midAngle()); case "start": return w.right() + w.midRadius() * Math.cos(w.startAngle()); case "center": return w.right() + w.midRadius() * Math.cos(w.midAngle()); case "end": return w.right() + w.midRadius() * Math.cos(w.endAngle()); } }) .top(function() { switch (this.name()) { case "outer": return w.top() + w.outerRadius() * Math.sin(w.midAngle()); case "inner": return w.top() + w.innerRadius() * Math.sin(w.midAngle()); case "start": return w.top() + w.midRadius() * Math.sin(w.startAngle()); case "center": return w.top() + w.midRadius() * Math.sin(w.midAngle()); case "end": return w.top() + w.midRadius() * Math.sin(w.endAngle()); } }) .bottom(function() { switch (this.name()) { case "outer": return w.bottom() + w.outerRadius() * Math.sin(w.midAngle()); case "inner": return w.bottom() + w.innerRadius() * Math.sin(w.midAngle()); case "start": return w.bottom() + w.midRadius() * Math.sin(w.startAngle()); case "center": return w.bottom() + w.midRadius() * Math.sin(w.midAngle()); case "end": return w.bottom() + w.midRadius() * Math.sin(w.endAngle()); } }) .textAlign(function() { switch (this.name()) { case "outer": return pv.Wedge.upright(w.midAngle()) ? "right" : "left"; case "inner": return pv.Wedge.upright(w.midAngle()) ? "left" : "right"; } return "center"; }) .textBaseline(function() { switch (this.name()) { case "start": return pv.Wedge.upright(w.startAngle()) ? "top" : "bottom"; case "end": return pv.Wedge.upright(w.endAngle()) ? "bottom" : "top"; } return "middle"; }) .textAngle(function() { var a = 0; switch (this.name()) { case "center": case "inner": case "outer": a = w.midAngle(); break; case "start": a = w.startAngle(); break; case "end": a = w.endAngle(); break; } return pv.Wedge.upright(a) ? a : (a + Math.PI); }); }; /** * Returns true if the specified angle is considered "upright", as in, text * rendered at that angle would appear upright. If the angle is not upright, * text is rotated 180 degrees to be upright, and the text alignment properties * are correspondingly changed. * * @param {number} angle an angle, in radius. * @returns {boolean} true if the specified angle is upright. */ pv.Wedge.upright = function(angle) { angle = angle % (2 * Math.PI); angle = (angle < 0) ? (2 * Math.PI + angle) : angle; return (angle < Math.PI / 2) || (angle > 3 * Math.PI / 2); }; /** * @private Overrides the default behavior of {@link pv.Mark.buildImplied} such * that the end angle is computed from the start angle and angle (angular span) * if not specified. * * @param s a node in the scene graph; the instance of the wedge to build. */ pv.Wedge.prototype.buildImplied = function(s) { pv.Mark.prototype.buildImplied.call(this, s); /* * TODO If the angle or endAngle is updated by an event handler, the implied * properties won't recompute correctly, so this will lead to potentially * buggy redraw. How to re-evaluate implied properties on update? */ if (s.endAngle == null) s.endAngle = s.startAngle + s.angle; if (s.angle == null) s.angle = s.endAngle - s.startAngle; }; /** * @ignore * @namespace */ pv.Layout = {}; /** * Returns a new grid layout. * * @class A grid layout with regularly-sized rows and columns. The number of rows * and columns are determined from the array, which should be in row-major * order. For example, the 2×3 array: * *
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:If no cousin instance is available (for example, for first instance), * the specified offset is used. If no offset is specified, zero is used. For * example, * *
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. The first row corresponds to the root * of the tree; subsequent rows correspond to each tier. Rows are subdivided * into cells based on the size of nodes, per {@link #size}. Within a row, cells * are sorted by size. * *

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: * *

* * To produce a default icicle layout, say: * *
.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: * *

* * To produce a default treemap layout, say: * *
.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; } } });