From 689de466dead34b9ee81882c6ea65aa9aa2830ba Mon Sep 17 00:00:00 2001 From: Ed Rooth Date: Mon, 14 Apr 2014 10:49:30 -0700 Subject: [PATCH] chore(dashboard/stats): add vega depencency --- mod/dashboard/app/vega.js | 6970 +++++++++++++++++++++++++++++++++++++ 1 file changed, 6970 insertions(+) create mode 100644 mod/dashboard/app/vega.js diff --git a/mod/dashboard/app/vega.js b/mod/dashboard/app/vega.js new file mode 100644 index 000000000..c1f564745 --- /dev/null +++ b/mod/dashboard/app/vega.js @@ -0,0 +1,6970 @@ +vg = (function(d3, topojson) { // take d3 & topojson as imports + var vg = { + version: "1.3.2", // semantic versioning + d3: d3, // stash d3 for use in property functions + topojson: topojson // stash topojson similarly + }; +// type checking functions +var toString = Object.prototype.toString; + +vg.isObject = function(obj) { + return obj === Object(obj); +}; + +vg.isFunction = function(obj) { + return toString.call(obj) == '[object Function]'; +}; + +vg.isString = function(obj) { + return toString.call(obj) == '[object String]'; +}; + +vg.isArray = Array.isArray || function(obj) { + return toString.call(obj) == '[object Array]'; +}; + +vg.isNumber = function(obj) { + return toString.call(obj) == '[object Number]'; +}; + +vg.isBoolean = function(obj) { + return toString.call(obj) == '[object Boolean]'; +}; + +vg.isTree = function(obj) { + return vg.isArray(obj) && obj.__vgtree__; +}; + +vg.number = function(s) { return +s; }; + +vg.boolean = function(s) { return !!s; }; + +// utility functions + +vg.identity = function(x) { return x; }; + +vg.extend = function(obj) { + for (var x, name, i=1, len=arguments.length; i 1 + ? function(x) { return s.reduce(function(x,f) { return x[f]; }, x); } + : function(x) { return x[f]; }; +}; + +vg.comparator = function(sort) { + var sign = []; + if (sort === undefined) sort = []; + sort = vg.array(sort).map(function(f) { + var s = 1; + if (f[0] === "-") { s = -1; f = f.slice(1); } + else if (f[0] === "+") { s = +1; f = f.slice(1); } + sign.push(s); + return vg.accessor(f); + }); + return function(a,b) { + var i, n, f, x, y; + for (i=0, n=sort.length; i y) return sign[i]; + } + return 0; + }; +}; + +vg.cmp = function(a, b) { return ab ? 1 : 0; }; + +vg.numcmp = function(a, b) { return a - b; }; + +vg.array = function(x) { + return x != null ? (vg.isArray(x) ? x : [x]) : []; +}; + +vg.values = function(x) { + return (vg.isObject(x) && !vg.isArray(x) && x.values) ? x.values : x; +}; + +vg.str = function(x) { + return vg.isArray(x) ? "[" + x.map(vg.str) + "]" + : vg.isObject(x) ? JSON.stringify(x) + : vg.isString(x) ? ("'"+vg_escape_str(x)+"'") : x; +}; + +var escape_str_re = /(^|[^\\])'/g; + +function vg_escape_str(x) { + return x.replace(escape_str_re, "$1\\'"); +} + +vg.keys = function(x) { + var keys = []; + for (var key in x) keys.push(key); + return keys; +}; + +vg.unique = function(data, f, results) { + if (!vg.isArray(data) || data.length==0) return []; + f = f || vg.identity; + results = results || []; + for (var v, i=0, n=data.length; i max) { max = v; idx = i; } + } + return idx; +}; + +vg.truncate = function(s, length, pos, word, ellipsis) { + var len = s.length; + if (len <= length) return s; + ellipsis = ellipsis || "..."; + var l = Math.max(0, length - ellipsis.length); + + switch (pos) { + case "left": + return ellipsis + (word ? vg_truncateOnWord(s,l,1) : s.slice(len-l)); + case "middle": + case "center": + var l1 = Math.ceil(l/2), l2 = Math.floor(l/2); + return (word ? vg_truncateOnWord(s,l1) : s.slice(0,l1)) + ellipsis + + (word ? vg_truncateOnWord(s,l2,1) : s.slice(len-l2)); + default: + return (word ? vg_truncateOnWord(s,l) : s.slice(0,l)) + ellipsis; + } +} + +function vg_truncateOnWord(s, len, rev) { + var cnt = 0, tok = s.split(vg_truncate_word_re); + if (rev) { + s = (tok = tok.reverse()) + .filter(function(w) { cnt += w.length; return cnt <= len; }) + .reverse(); + } else { + s = tok.filter(function(w) { cnt += w.length; return cnt <= len; }); + } + return s.length ? s.join("").trim() : tok[0].slice(0, len); +} + +var vg_truncate_word_re = /([\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u2028\u2029\u3000\uFEFF])/; + +// Logging + +function vg_write(msg) { + vg.config.isNode + ? process.stderr.write(msg + "\n") + : console.log(msg); +} + +vg.log = function(msg) { + vg_write("[Vega Log] " + msg); +}; + +vg.error = function(msg) { + msg = "[Vega Err] " + msg; + vg_write(msg); + if (typeof alert !== "undefined") alert(msg); +};vg.config = {}; + +// are we running in node.js? +// via timetler.com/2012/10/13/environment-detection-in-javascript/ +vg.config.isNode = typeof exports !== 'undefined' && this.exports !== exports; + +// base url for loading external data files +// used only for server-side operation +vg.config.baseURL = ""; + +// version and namepsaces for exported svg +vg.config.svgNamespace = + 'version="1.1" xmlns="http://www.w3.org/2000/svg" ' + + 'xmlns:xlink="http://www.w3.org/1999/xlink"'; + +// inset padding for automatic padding calculation +vg.config.autopadInset = 5; + +// extensible scale lookup table +// all d3.scale.* instances also supported +vg.config.scale = { + time: d3.time.scale, + utc: d3.time.scale.utc +}; + +// default rendering settings +vg.config.render = { + lineWidth: 1, + lineCap: "butt", + font: "sans-serif", + fontSize: 11 +}; + +// default axis properties +vg.config.axis = { + orient: "bottom", + ticks: 10, + padding: 3, + axisColor: "#000", + gridColor: "#d8d8d8", + tickColor: "#000", + tickLabelColor: "#000", + axisWidth: 1, + tickWidth: 1, + tickSize: 6, + tickLabelFontSize: 11, + tickLabelFont: "sans-serif", + titleColor: "#000", + titleFont: "sans-serif", + titleFontSize: 11, + titleFontWeight: "bold", + titleOffset: 35 +}; + +// default legend properties +vg.config.legend = { + orient: "right", + offset: 10, + padding: 3, + gradientStrokeColor: "#888", + gradientStrokeWidth: 1, + gradientHeight: 16, + gradientWidth: 100, + labelColor: "#000", + labelFontSize: 10, + labelFont: "sans-serif", + labelAlign: "left", + labelBaseline: "middle", + labelOffset: 8, + symbolShape: "circle", + symbolSize: 50, + symbolColor: "#888", + symbolStrokeWidth: 1, + titleColor: "#000", + titleFont: "sans-serif", + titleFontSize: 11, + titleFontWeight: "bold" +}; + +// default color values +vg.config.color = { + rgb: [128, 128, 128], + lab: [50, 0, 0], + hcl: [0, 0, 50], + hsl: [0, 0, 0.5] +}; + +// default scale ranges +vg.config.range = { + category10: [ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf" + ], + category20: [ + "#1f77b4", + "#aec7e8", + "#ff7f0e", + "#ffbb78", + "#2ca02c", + "#98df8a", + "#d62728", + "#ff9896", + "#9467bd", + "#c5b0d5", + "#8c564b", + "#c49c94", + "#e377c2", + "#f7b6d2", + "#7f7f7f", + "#c7c7c7", + "#bcbd22", + "#dbdb8d", + "#17becf", + "#9edae5" + ], + shapes: [ + "circle", + "cross", + "diamond", + "square", + "triangle-down", + "triangle-up" + ] +};vg.Bounds = (function() { + var bounds = function(b) { + this.clear(); + if (b) this.union(b); + }; + + var prototype = bounds.prototype; + + prototype.clear = function() { + this.x1 = +Number.MAX_VALUE; + this.y1 = +Number.MAX_VALUE; + this.x2 = -Number.MAX_VALUE; + this.y2 = -Number.MAX_VALUE; + return this; + }; + + prototype.set = function(x1, y1, x2, y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + return this; + }; + + prototype.add = function(x, y) { + if (x < this.x1) this.x1 = x; + if (y < this.y1) this.y1 = y; + if (x > this.x2) this.x2 = x; + if (y > this.y2) this.y2 = y; + return this; + }; + + prototype.expand = function(d) { + this.x1 -= d; + this.y1 -= d; + this.x2 += d; + this.y2 += d; + return this; + }; + + prototype.round = function() { + this.x1 = Math.floor(this.x1); + this.y1 = Math.floor(this.y1); + this.x2 = Math.ceil(this.x2); + this.y2 = Math.ceil(this.y2); + return this; + }; + + prototype.translate = function(dx, dy) { + this.x1 += dx; + this.x2 += dx; + this.y1 += dy; + this.y2 += dy; + return this; + }; + + prototype.rotate = function(angle, x, y) { + var cos = Math.cos(angle), + sin = Math.sin(angle), + cx = x - x*cos + y*sin, + cy = y - x*sin - y*cos, + x1 = this.x1, x2 = this.x2, + y1 = this.y1, y2 = this.y2; + + return this.clear() + .add(cos*x1 - sin*y1 + cx, sin*x1 + cos*y1 + cy) + .add(cos*x1 - sin*y2 + cx, sin*x1 + cos*y2 + cy) + .add(cos*x2 - sin*y1 + cx, sin*x2 + cos*y1 + cy) + .add(cos*x2 - sin*y2 + cx, sin*x2 + cos*y2 + cy); + } + + prototype.union = function(b) { + if (b.x1 < this.x1) this.x1 = b.x1; + if (b.y1 < this.y1) this.y1 = b.y1; + if (b.x2 > this.x2) this.x2 = b.x2; + if (b.y2 > this.y2) this.y2 = b.y2; + return this; + }; + + prototype.encloses = function(b) { + return b && ( + this.x1 <= b.x1 && + this.x2 >= b.x2 && + this.y1 <= b.y1 && + this.y2 >= b.y2 + ); + }; + + prototype.intersects = function(b) { + return b && !( + this.x2 < b.x1 || + this.x1 > b.x2 || + this.y2 < b.y1 || + this.y1 > b.y2 + ); + }; + + prototype.contains = function(x, y) { + return !( + x < this.x1 || + x > this.x2 || + y < this.y1 || + y > this.y2 + ); + }; + + prototype.width = function() { + return this.x2 - this.x1; + }; + + prototype.height = function() { + return this.y2 - this.y1; + }; + + return bounds; +})();vg.Gradient = (function() { + + function gradient(type) { + this.id = "grad_" + (vg_gradient_id++); + this.type = type || "linear"; + this.stops = []; + this.x1 = 0; + this.x2 = 1; + this.y1 = 0; + this.y2 = 0; + }; + + var prototype = gradient.prototype; + + prototype.stop = function(offset, color) { + this.stops.push({ + offset: offset, + color: color + }); + return this; + }; + + return gradient; +})(); + +var vg_gradient_id = 0;vg.canvas = {};vg.canvas.path = (function() { + + // Path parsing and rendering code taken from fabric.js -- Thanks! + var cmdLength = { m:2, l:2, h:1, v:1, c:6, s:4, q:4, t:2, a:7 }, + re = [/([MLHVCSQTAZmlhvcsqtaz])/g, /###/, /(\d)-/g, /\s|,|###/]; + + function parse(path) { + var result = [], + currentPath, + chunks, + parsed; + + // First, break path into command sequence + path = path.slice().replace(re[0], '###$1').split(re[1]).slice(1); + + // Next, parse each command in turn + for (var i=0, j, chunksParsed, len=path.length; i commandLength) { + for (var k = 1, klen = chunksParsed.length; k < klen; k += commandLength) { + result.push([ chunksParsed[0] ].concat(chunksParsed.slice(k, k + commandLength))); + } + } + else { + result.push(chunksParsed); + } + } + + return result; + } + + function drawArc(g, x, y, coords, bounds, l, t) { + var rx = coords[0]; + var ry = coords[1]; + var rot = coords[2]; + var large = coords[3]; + var sweep = coords[4]; + var ex = coords[5]; + var ey = coords[6]; + var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); + for (var i=0; i 1) { + pl = Math.sqrt(pl); + rx *= pl; + ry *= pl; + } + + var a00 = cos_th / rx; + var a01 = sin_th / rx; + var a10 = (-sin_th) / ry; + var a11 = (cos_th) / ry; + var x0 = a00 * ox + a01 * oy; + var y0 = a10 * ox + a11 * oy; + var x1 = a00 * x + a01 * y; + var y1 = a10 * x + a11 * y; + + var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0); + var sfactor_sq = 1 / d - 0.25; + if (sfactor_sq < 0) sfactor_sq = 0; + var sfactor = Math.sqrt(sfactor_sq); + if (sweep == large) sfactor = -sfactor; + var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0); + var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0); + + var th0 = Math.atan2(y0-yc, x0-xc); + var th1 = Math.atan2(y1-yc, x1-xc); + + var th_arc = th1-th0; + if (th_arc < 0 && sweep == 1){ + th_arc += 2*Math.PI; + } else if (th_arc > 0 && sweep == 0) { + th_arc -= 2 * Math.PI; + } + + var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); + var result = []; + for (var i=0; i 0) { + g.globalAlpha = opac * (o.strokeOpacity==null ? 1 : o.strokeOpacity); + g.strokeStyle = color(g, o, stroke); + g.lineWidth = lw; + g.lineCap = (lc = o.strokeCap) != null ? lc : vg.config.render.lineCap; + g.vgLineDash(o.strokeDash || null); + g.vgLineDashOffset(o.strokeDashOffset || 0); + g.stroke(); + } + } + } + + function drawPathAll(path, g, scene, bounds) { + var i, len, item; + for (i=0, len=scene.items.length; i 0) { + g.globalAlpha = opac * (o.strokeOpacity==null ? 1 : o.strokeOpacity); + g.strokeStyle = color(g, o, stroke); + g.lineWidth = lw; + g.lineCap = (lc = o.strokeCap) != null ? lc : vg.config.render.lineCap; + g.vgLineDash(o.strokeDash || null); + g.vgLineDashOffset(o.strokeDashOffset || 0); + g.strokeRect(x, y, w, h); + } + } + } + } + + function drawRule(g, scene, bounds) { + if (!scene.items.length) return; + var items = scene.items, + o, stroke, opac, lc, lw, x1, y1, x2, y2; + + for (var i=0, len=items.length; i 0) { + g.globalAlpha = opac * (o.strokeOpacity==null ? 1 : o.strokeOpacity); + g.strokeStyle = color(g, o, stroke); + g.lineWidth = lw; + g.lineCap = (lc = o.strokeCap) != null ? lc : vg.config.render.lineCap; + g.vgLineDash(o.strokeDash || null); + g.vgLineDashOffset(o.strokeDashOffset || 0); + g.beginPath(); + g.moveTo(x1, y1); + g.lineTo(x2, y2); + g.stroke(); + } + } + } + } + + function drawImage(g, scene, bounds) { + if (!scene.items.length) return; + var renderer = this, + items = scene.items, o; + + for (var i=0, len=items.length; i 0) { + g.globalAlpha = opac * (o.strokeOpacity==null ? 1 : o.strokeOpacity); + g.strokeStyle = color(o, stroke); + g.lineWidth = lw; + g.strokeText(o.text, x, y); + } + } + + if (o.angle) g.restore(); + } + } + + function drawAll(pathFunc) { + return function(g, scene, bounds) { + drawPathAll(pathFunc, g, scene, bounds); + } + } + + function drawOne(pathFunc) { + return function(g, scene, bounds) { + if (!scene.items.length) return; + if (bounds && !bounds.intersects(scene.items[0].bounds)) + return; // bounds check + drawPathOne(pathFunc, g, scene.items[0], scene.items); + } + } + + function drawGroup(g, scene, bounds) { + if (!scene.items.length) return; + var items = scene.items, group, axes, legends, + renderer = this, gx, gy, gb, i, n, j, m; + + drawRect(g, scene, bounds); + + for (i=0, n=items.length; i=0;) { + group = items[i]; + dx = group.x || 0; + dy = group.y || 0; + + g.save(); + g.translate(dx, dy); + for (j=group.items.length; --j >= 0;) { + subscene = group.items[j]; + if (subscene.interactive === false) continue; + hit = handler.pick(subscene, x, y, gx-dx, gy-dy); + if (hit) { + g.restore(); + return hit; + } + } + g.restore(); + } + + return scene.interactive + ? pickAll(hitTests.rect, g, scene, x, y, gx, gy) + : false; + } + + function pickAll(test, g, scene, x, y, gx, gy) { + if (!scene.items.length) return false; + var o, b, i; + + if (g._ratio !== 1) { + x *= g._ratio; + y *= g._ratio; + } + + for (i=scene.items.length; --i >= 0;) { + o = scene.items[i]; b = o.bounds; + // first hit test against bounding box + if ((b && !b.contains(gx, gy)) || !b) continue; + // if in bounding box, perform more careful test + if (test(g, o, x, y, gx, gy)) return o; + } + return false; + } + + function pickArea(g, scene, x, y, gx, gy) { + if (!scene.items.length) return false; + var items = scene.items, + o, b, i, di, dd, od, dx, dy; + + b = items[0].bounds; + if (b && !b.contains(gx, gy)) return false; + if (g._ratio !== 1) { + x *= g._ratio; + y *= g._ratio; + } + if (!hitTests.area(g, items, x, y)) return false; + return items[0]; + } + + function pickLine(g, scene, x, y, gx, gy) { + if (!scene.items.length) return false; + var items = scene.items, + o, b, i, di, dd, od, dx, dy; + + b = items[0].bounds; + if (b && !b.contains(gx, gy)) return false; + if (g._ratio !== 1) { + x *= g._ratio; + y *= g._ratio; + } + if (!hitTests.line(g, items, x, y)) return false; + return items[0]; + } + + function pick(test) { + return function (g, scene, x, y, gx, gy) { + return pickAll(test, g, scene, x, y, gx, gy); + }; + } + + function textHit(g, o, x, y, gx, gy) { + if (!o.fontSize) return false; + if (!o.angle) return true; // bounds sufficient if no rotation + + var b = vg.scene.bounds.text(o, tmpBounds, true), + a = -o.angle * Math.PI / 180, + cos = Math.cos(a), + sin = Math.sin(a), + x = o.x, + y = o.y, + px = cos*gx - sin*gy + (x - x*cos + y*sin), + py = sin*gx + cos*gy + (y - x*sin - y*cos); + + return b.contains(px, py); + } + + var hitTests = { + text: textHit, + rect: function(g,o,x,y) { return true; }, // bounds test is sufficient + image: function(g,o,x,y) { return true; }, // bounds test is sufficient + rule: function(g,o,x,y) { + if (!g.isPointInStroke) return false; + ruleStroke(g,o); return g.isPointInStroke(x,y); + }, + line: function(g,s,x,y) { + if (!g.isPointInStroke) return false; + lineStroke(g,s); return g.isPointInStroke(x,y); + }, + arc: function(g,o,x,y) { arcPath(g,o); return g.isPointInPath(x,y); }, + area: function(g,s,x,y) { areaPath(g,s); return g.isPointInPath(x,y); }, + path: function(g,o,x,y) { pathPath(g,o); return g.isPointInPath(x,y); }, + symbol: function(g,o,x,y) { symbolPath(g,o); return g.isPointInPath(x,y); } + }; + + return { + draw: { + group: drawGroup, + area: drawOne(areaPath), + line: drawOne(linePath), + arc: drawAll(arcPath), + path: drawAll(pathPath), + symbol: drawAll(symbolPath), + rect: drawRect, + rule: drawRule, + text: drawText, + image: drawImage, + drawOne: drawOne, // expose for extensibility + drawAll: drawAll // expose for extensibility + }, + pick: { + group: pickGroup, + area: pickArea, + line: pickLine, + arc: pick(hitTests.arc), + path: pick(hitTests.path), + symbol: pick(hitTests.symbol), + rect: pick(hitTests.rect), + rule: pick(hitTests.rule), + text: pick(hitTests.text), + image: pick(hitTests.image), + pickAll: pickAll // expose for extensibility + } + }; + +})();vg.canvas.Renderer = (function() { + var renderer = function() { + this._ctx = null; + this._el = null; + this._imgload = 0; + }; + + var prototype = renderer.prototype; + + prototype.initialize = function(el, width, height, pad) { + this._el = el; + + if (!el) return this; // early exit if no DOM element + + // select canvas element + var canvas = d3.select(el) + .selectAll("canvas.marks") + .data([1]); + + // create new canvas element if needed + canvas.enter() + .append("canvas") + .attr("class", "marks"); + + // remove extraneous canvas if needed + canvas.exit().remove(); + + return this.resize(width, height, pad); + }; + + prototype.resize = function(width, height, pad) { + this._width = width; + this._height = height; + this._padding = pad; + + if (this._el) { + var canvas = d3.select(this._el).select("canvas.marks"); + + // initialize canvas attributes + canvas + .attr("width", width + pad.left + pad.right) + .attr("height", height + pad.top + pad.bottom); + + // get the canvas graphics context + var s; + this._ctx = canvas.node().getContext("2d"); + this._ctx._ratio = (s = scaleCanvas(canvas.node(), this._ctx) || 1); + this._ctx.setTransform(s, 0, 0, s, s*pad.left, s*pad.top); + } + + initializeLineDash(this._ctx); + return this; + }; + + function scaleCanvas(canvas, ctx) { + // get canvas pixel data + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = ( + ctx.webkitBackingStorePixelRatio || + ctx.mozBackingStorePixelRatio || + ctx.msBackingStorePixelRatio || + ctx.oBackingStorePixelRatio || + ctx.backingStorePixelRatio) || 1, + ratio = devicePixelRatio / backingStoreRatio; + + if (devicePixelRatio !== backingStoreRatio) { + var w = canvas.width, h = canvas.height; + // set actual and visible canvas size + canvas.setAttribute("width", w * ratio); + canvas.setAttribute("height", h * ratio); + canvas.style.width = w + 'px'; + canvas.style.height = h + 'px'; + } + return ratio; + } + + function initializeLineDash(ctx) { + if (ctx.vgLineDash) return; // already set + + var NODASH = []; + if (ctx.setLineDash) { + ctx.vgLineDash = function(dash) { this.setLineDash(dash || NODASH); }; + ctx.vgLineDashOffset = function(off) { this.lineDashOffset = off; }; + } else if (ctx.webkitLineDash !== undefined) { + ctx.vgLineDash = function(dash) { this.webkitLineDash = dash || NODASH; }; + ctx.vgLineDashOffset = function(off) { this.webkitLineDashOffset = off; }; + } else if (ctx.mozDash !== undefined) { + ctx.vgLineDash = function(dash) { this.mozDash = dash; }; + ctx.vgLineDashOffset = function(off) { /* unsupported */ }; + } else { + ctx.vgLineDash = function(dash) { /* unsupported */ }; + ctx.vgLineDashOffset = function(off) { /* unsupported */ }; + } + } + + prototype.context = function(ctx) { + if (ctx) { this._ctx = ctx; return this; } + else return this._ctx; + }; + + prototype.element = function() { + return this._el; + }; + + prototype.pendingImages = function() { + return this._imgload; + }; + + function translatedBounds(item, bounds) { + var b = new vg.Bounds(bounds); + while ((item = item.mark.group) != null) { + b.translate(item.x || 0, item.y || 0); + } + return b; + } + + function getBounds(items) { + return !items ? null : + vg.array(items).reduce(function(b, item) { + return b.union(translatedBounds(item, item.bounds)) + .union(translatedBounds(item, item['bounds:prev'])); + }, new vg.Bounds()); + } + + function setBounds(g, bounds) { + var bbox = null; + if (bounds) { + bbox = (new vg.Bounds(bounds)).round(); + g.beginPath(); + g.rect(bbox.x1, bbox.y1, bbox.width(), bbox.height()); + g.clip(); + } + return bbox; + } + + prototype.render = function(scene, items) { + var g = this._ctx, + pad = this._padding, + w = this._width + pad.left + pad.right, + h = this._height + pad.top + pad.bottom, + bb = null, bb2; + + // setup + this._scene = scene; + g.save(); + bb = setBounds(g, getBounds(items)); + g.clearRect(-pad.left, -pad.top, w, h); + + // render + this.draw(g, scene, bb); + + // render again to handle possible bounds change + if (items) { + g.restore(); + g.save(); + bb2 = setBounds(g, getBounds(items)); + if (!bb.encloses(bb2)) { + g.clearRect(-pad.left, -pad.top, w, h); + this.draw(g, scene, bb2); + } + } + + // takedown + g.restore(); + this._scene = null; + }; + + prototype.draw = function(ctx, scene, bounds) { + var marktype = scene.marktype, + renderer = vg.canvas.marks.draw[marktype]; + renderer.call(this, ctx, scene, bounds); + + // compute mark-level bounds + scene.bounds = scene.items.reduce(function(b, item) { + return item.bounds ? b.union(item.bounds) : b; + }, scene.bounds || new vg.Bounds()); + }; + + prototype.renderAsync = function(scene) { + // TODO make safe for multiple scene rendering? + var renderer = this; + if (renderer._async_id) { + clearTimeout(renderer._async_id); + } + renderer._async_id = setTimeout(function() { + renderer.render(scene); + delete renderer._async_id; + }, 50); + }; + + prototype.loadImage = function(uri) { + var renderer = this, + scene = renderer._scene, + image = null, url; + + renderer._imgload += 1; + if (vg.config.isNode) { + image = new (require("canvas").Image)(); + vg.data.load(uri, function(err, data) { + if (err) { vg.error(err); return; } + image.src = data; + image.loaded = true; + renderer._imgload -= 1; + }); + } else { + image = new Image(); + url = vg.config.baseURL + uri; + image.onload = function() { + vg.log("LOAD IMAGE: "+url); + image.loaded = true; + renderer._imgload -= 1; + renderer.renderAsync(scene); + }; + image.src = url; + } + + return image; + }; + + return renderer; +})();vg.canvas.Handler = (function() { + var handler = function(el, model) { + this._active = null; + this._handlers = {}; + if (el) this.initialize(el); + if (model) this.model(model); + }; + + var prototype = handler.prototype; + + prototype.initialize = function(el, pad, obj) { + this._el = d3.select(el).node(); + this._canvas = d3.select(el).select("canvas.marks").node(); + this._padding = pad; + this._obj = obj || null; + + // add event listeners + var canvas = this._canvas, that = this; + events.forEach(function(type) { + canvas.addEventListener(type, function(evt) { + prototype[type].call(that, evt); + }); + }); + + return this; + }; + + prototype.padding = function(pad) { + this._padding = pad; + return this; + }; + + prototype.model = function(model) { + if (!arguments.length) return this._model; + this._model = model; + return this; + }; + + prototype.handlers = function() { + var h = this._handlers; + return vg.keys(h).reduce(function(a, k) { + return h[k].reduce(function(a, x) { return (a.push(x), a); }, a); + }, []); + }; + + // setup events + var events = [ + "mousedown", + "mouseup", + "click", + "dblclick", + "wheel", + "keydown", + "keypress", + "keyup", + "mousewheel" + ]; + events.forEach(function(type) { + prototype[type] = function(evt) { + this.fire(type, evt); + }; + }); + events.push("mousemove"); + events.push("mouseout"); + + function eventName(name) { + var i = name.indexOf("."); + return i < 0 ? name : name.slice(0,i); + } + + prototype.mousemove = function(evt) { + var pad = this._padding, + b = evt.target.getBoundingClientRect(), + x = evt.clientX - b.left, + y = evt.clientY - b.top, + a = this._active, + p = this.pick(this._model.scene(), x, y, x-pad.left, y-pad.top); + + if (p === a) { + this.fire("mousemove", evt); + return; + } else if (a) { + this.fire("mouseout", evt); + } + this._active = p; + if (p) { + this.fire("mouseover", evt); + } + }; + + prototype.mouseout = function(evt) { + if (this._active) { + this.fire("mouseout", evt); + } + this._active = null; + }; + + // to keep firefox happy + prototype.DOMMouseScroll = function(evt) { + this.fire("mousewheel", evt); + }; + + // fire an event + prototype.fire = function(type, evt) { + var a = this._active, + h = this._handlers[type]; + if (a && h) { + for (var i=0, len=h.length; i=0;) { + if (h[i].type !== type) continue; + if (!handler || h[i].handler === handler) h.splice(i, 1); + } + return this; + }; + + // retrieve the current canvas context + prototype.context = function() { + return this._canvas.getContext("2d"); + }; + + // find the scenegraph item at the current mouse position + // returns an array of scenegraph items, from leaf node up to the root + // x, y -- the absolute x, y mouse coordinates on the canvas element + // gx, gy -- the relative coordinates within the current group + prototype.pick = function(scene, x, y, gx, gy) { + var g = this.context(), + marktype = scene.marktype, + picker = vg.canvas.marks.pick[marktype]; + return picker.call(this, g, scene, x, y, gx, gy); + }; + + return handler; +})();vg.svg = {};vg.svg.marks = (function() { + + function x(o) { return o.x || 0; } + function y(o) { return o.y || 0; } + function yh(o) { return o.y + o.height || 0; } + function key(o) { return o.key; } + function size(o) { return o.size==null ? 100 : o.size; } + function shape(o) { return o.shape || "circle"; } + + var arc_path = d3.svg.arc(), + area_path = d3.svg.area().x(x).y1(y).y0(yh), + line_path = d3.svg.line().x(x).y(y), + symbol_path = d3.svg.symbol().type(shape).size(size); + + var mark_id = 0; + + var textAlign = { + "left": "start", + "center": "middle", + "right": "end" + }; + + var styles = { + "fill": "fill", + "fillOpacity": "fill-opacity", + "stroke": "stroke", + "strokeWidth": "stroke-width", + "strokeOpacity": "stroke-opacity", + "strokeCap": "stroke-linecap", + "strokeDash": "stroke-dasharray", + "strokeDashOffset": "stroke-dashoffset", + "opacity": "opacity" + }; + var styleProps = vg.keys(styles); + + function style(d) { + var i, n, prop, name, value, + o = d.mark ? d : d.length ? d[0] : null; + if (o === null) return; + + for (i=0, n=styleProps.length; i " + tag, + m = p.selectAll(s).data(data), + e = m.enter().append(tag); + + if (notG) { + p.style("pointer-events", evts); + e.each(function(d) { + if (d.mark) d._svg = this; + else if (d.length) d[0]._svg = this; + }); + } else { + e.append("rect").attr("class","background").style("pointer-events",evts); + } + + m.exit().remove(); + m.each(attr); + if (notG) m.each(style); + else p.selectAll(s+" > rect.background").each(group_bg).each(style); + + return p; + } + + function drawGroup(g, scene, index, prefix) { + var p = drawMark(g, scene, index, prefix || "group_", "g", group), + c = p.node().childNodes, n = c.length, i, j, m; + + for (i=0; i=0;) { + if (h[i].type !== type) continue; + if (!handler || h[i].handler === handler) { + dom.removeEventListener(name, h[i].svg); + h.splice(i, 1); + } + } + return this; + }; + + return handler; +})();vg.data = {}; + +vg.data.ingestAll = function(data) { + return vg.isTree(data) + ? vg_make_tree(vg.data.ingestTree(data[0], data.children)) + : data.map(vg.data.ingest); +}; + +vg.data.ingest = function(datum, index) { + return { + data: datum, + index: index + }; +}; + +vg.data.ingestTree = function(node, children) { + var d = vg.data.ingest(node), + c = node[children], n, i; + if (c && (n = c.length)) { + d.values = Array(n); + for (i=0; i= 0) file = file.slice(vg_load_fileProtocol.length); + require("fs").readFile(file, callback); +} + +function vg_load_http(url, callback) { + vg.log("LOAD HTTP: " + url); + var req = require("http").request(url, function(res) { + var pos=0, data = new Buffer(parseInt(res.headers['content-length'],10)); + res.on("error", function(err) { callback(err, null); }); + res.on("data", function(x) { x.copy(data, pos); pos += x.length; }); + res.on("end", function() { callback(null, data); }); + }); + req.on("error", function(err) { callback(err); }); + req.end(); +}vg.data.read = (function() { + var formats = {}, + parsers = { + "number": vg.number, + "boolean": vg.boolean, + "date": Date.parse + }; + + function read(data, format) { + var type = (format && format.type) || "json"; + data = formats[type](data, format); + if (format && format.parse) parseValues(data, format.parse); + return data; + } + + formats.json = function(data, format) { + var d = JSON.parse(data); + if (format && format.property) { + d = vg.accessor(format.property)(d); + } + return d; + }; + + formats.csv = function(data, format) { + var d = d3.csv.parse(data); + return d; + }; + + formats.tsv = function(data, format) { + var d = d3.tsv.parse(data); + return d; + }; + + formats.topojson = function(data, format) { + if (topojson == null) { + vg.error("TopoJSON library not loaded."); + return []; + } + var t = JSON.parse(data), obj = []; + + if (format && format.feature) { + obj = (obj = t.objects[format.feature]) + ? topojson.feature(t, obj).features + : (vg.error("Invalid TopoJSON object: "+format.feature), []); + } else if (format && format.mesh) { + obj = (obj = t.objects[format.mesh]) + ? [topojson.mesh(t, t.objects[format.mesh])] + : (vg.error("Invalid TopoJSON object: " + format.mesh), []); + } + else { vg.error("Missing TopoJSON feature or mesh parameter."); } + + return obj; + }; + + formats.treejson = function(data, format) { + var d = [JSON.parse(data)]; + d.__vgtree__ = true; + d.children = format.children || "children"; + return d; + }; + + function parseValues(data, types) { + var cols = vg.keys(types), + p = cols.map(function(col) { return parsers[types[col]]; }), + tree = vg.isTree(data); + vg_parseArray(tree ? [data] : data, cols, p, tree); + } + + function vg_parseArray(data, cols, p, tree) { + var d, i, j, len, clen; + for (i=0, len=data.length; i0 ? "|" : "") + String(kv); + } + obj = map[kstr]; + if (obj === undefined) { + vals.push(obj = map[kstr] = { + key: kstr, + keys: klist, + index: vals.length, + values: [] + }); + } + obj.values.push(data[i]); + } + + if (sort) { + for (i=0, len=vals.length; i b ? 1 : 0; + }); + data = [data[~~(list.length/2)]]; + } else { + var idx = vg.array(by); + data = data.slice(idx[0], idx[1]); + } + return data; + } + + slice.by = function(x) { + by = x; + return slice; + }; + + slice.field = function(f) { + field = vg.accessor(f); + return slice; + }; + + return slice; +};vg.data.sort = function() { + var by = null; + + function sort(data) { + data = (vg.isArray(data) ? data : data.values || []); + data.sort(by); + for (var i=0, n=data.length; ib.x ? 1 : (a.zb.z ? 1 : 0); + }); + + // emit data series for stack layout + for (x=points[0].x, i=0, j=0, k=0, n=points.length; k i) series[i++].push({x:j, y:0}); + p.x = j; + series[i++].push(p); + } + while (i < series.length) series[i++].push({x:j, y:0}); + + return series; + } + + stack.point = function(field) { + point = vg.accessor(field); + return stack; + }; + + stack.height = function(field) { + height = vg.accessor(field); + return stack; + }; + + params.forEach(function(name) { + stack[name] = function(x) { + layout[name](x); + return stack; + } + }); + + stack.output = function(map) { + d3.keys(output).forEach(function(k) { + if (map[k] !== undefined) { + output[k] = map[k]; + } + }); + return stack; + }; + + return stack; +};vg.data.stats = function() { + var value = vg.accessor("data"), + assign = false, + median = false, + output = { + "count": "count", + "min": "min", + "max": "max", + "sum": "sum", + "mean": "mean", + "variance": "variance", + "stdev": "stdev", + "median": "median" + }; + + function reduce(data) { + var min = +Infinity, + max = -Infinity, + sum = 0, + mean = 0, + M2 = 0, + i, len, v, delta; + + var list = (vg.isArray(data) ? data : data.values || []).map(value); + + // compute aggregates + for (i=0, len=list.length; i max) max = v; + sum += v; + delta = v - mean; + mean = mean + delta / (i+1); + M2 = M2 + delta * (v - mean); + } + M2 = M2 / (len - 1); + + var o = vg.isArray(data) ? {} : data; + if (median) { + list.sort(vg.numcmp); + i = list.length >> 1; + o[output.median] = list.length % 2 + ? list[i] + : (list[i-1] + list[i])/2; + } + o[output.count] = len; + o[output.min] = min; + o[output.max] = max; + o[output.sum] = sum; + o[output.mean] = mean; + o[output.variance] = M2; + o[output.stdev] = Math.sqrt(M2); + + if (assign) { + list = (vg.isArray(data) ? data : data.values); + v = {}; + v[output.count] = len; + v[output.min] = min; + v[output.max] = max; + v[output.sum] = sum; + v[output.mean] = mean; + v[output.variance] = M2; + v[output.stdev] = Math.sqrt(M2); + if (median) v[output.median] = o[output.median]; + for (i=0, len=list.length; i\~\&\|\?\:\+\-\/\*\%\!\^\,\;\[\]\{\}\(\) ]+)/; + + return function(x) { + var tokens = x.split(lexer), + t, v, i, n, sq, dq; + + for (sq=0, dq=0, i=0, n=tokens.length; i 0) ? "\n " : " "; + code += "o."+name+" = "+valueRef(name, ref)+";"; + vars[name] = true; + } + + if (vars.x2) { + if (vars.x) { + code += "\n if (o.x > o.x2) { " + + "var t = o.x; o.x = o.x2; o.x2 = t; };"; + code += "\n o.width = (o.x2 - o.x);"; + } else if (vars.width && !vars.x1) { + code += "\n o.x = (o.x2 - o.width);"; + } + } + + if (vars.y2) { + if (vars.y) { + code += "\n if (o.y > o.y2) { " + + "var t = o.y; o.y = o.y2; o.y2 = t; };"; + code += "\n o.height = (o.y2 - o.y);"; + } else if (vars.height && !vars.y1) { + code += "\n o.y = (o.y2 - o.height);"; + } + } + + if (hasPath(mark, vars)) { + code += "\n if (o['path:parsed']) o['path:parsed'] = null;" + } + code += "\n if (trans) trans.interpolate(item, o);"; + + try { + return Function("item", "group", "trans", code); + } catch (e) { + vg.error(e); + vg.log(code); + } + } + + function hasPath(mark, vars) { + return vars.path || + ((mark==="area" || mark==="line") && + (vars.x || vars.x2 || vars.width || + vars.y || vars.y2 || vars.height || + vars.tension || vars.interpolate)); + } + + var GROUP_VARS = { + "width": 1, + "height": 1, + "mark.group.width": 1, + "mark.group.height": 1 + }; + + function valueRef(name, ref) { + if (ref == null) return null; + var isColor = name==="fill" || name==="stroke"; + + if (isColor) { + if (ref.c) { + return colorRef("hcl", ref.h, ref.c, ref.l); + } else if (ref.h || ref.s) { + return colorRef("hsl", ref.h, ref.s, ref.l); + } else if (ref.l || ref.a) { + return colorRef("lab", ref.l, ref.a, ref.b); + } else if (ref.r || ref.g || ref.b) { + return colorRef("rgb", ref.r, ref.g, ref.b); + } + } + + // initialize value + var val = "item.datum.data"; + if (ref.value !== undefined) { + val = vg.str(ref.value); + } + + // get field reference for enclosing group + if (ref.group != null) { + var grp = ""; + if (vg.isString(ref.group)) { + grp = GROUP_VARS[ref.group] + ? "group." + ref.group + : "group.datum["+vg.field(ref.group).map(vg.str).join("][")+"]"; + } + } + + // get data field value + if (ref.field != null) { + if (vg.isString(ref.field)) { + val = "item.datum["+vg.field(ref.field).map(vg.str).join("][")+"]"; + if (ref.group != null) { val = grp+"["+val+"]"; } + } else { + val = "this.accessor(group.datum[" + + vg.field(ref.field.group).map(vg.str).join("][") + + "])(item.datum.data)"; + } + } else if (ref.group != null) { + val = grp; + } + + // run through scale function + if (ref.scale != null) { + var scale = vg.isString(ref.scale) + ? vg.str(ref.scale) + : (ref.scale.group ? "group" : "item") + + ".datum[" + vg.str(ref.scale.group || ref.scale.field) + "]"; + scale = "group.scales[" + scale + "]"; + val = scale + (ref.band ? ".rangeBand()" : "("+val+")"); + } + + // multiply, offset, return value + val = "(" + (ref.mult?(vg.number(ref.mult)+" * "):"") + val + ")" + + (ref.offset ? " + " + vg.number(ref.offset) : ""); + if (isColor) val = '('+val+')+""'; + return val; + } + + function colorRef(type, x, y, z) { + var xx = x ? valueRef("", x) : vg.config.color[type][0], + yy = y ? valueRef("", y) : vg.config.color[type][1], + zz = z ? valueRef("", z) : vg.config.color[type][2]; + return "(this.d3." + type + "(" + [xx,yy,zz].join(",") + ') + "")'; + } + + return compile; +})();vg.parse.scales = (function() { + var LINEAR = "linear", + ORDINAL = "ordinal", + LOG = "log", + POWER = "pow", + TIME = "time", + GROUP_PROPERTY = {width: 1, height: 1}; + + function scales(spec, scales, db, group) { + return (spec || []).reduce(function(o, def) { + var name = def.name, prev = name + ":prev"; + o[name] = scale(def, o[name], db, group); + o[prev] = o[prev] || o[name]; + return o; + }, scales || {}); + } + + function scale(def, scale, db, group) { + var s = instance(def, scale), + m = s.type===ORDINAL ? ordinal : quantitative, + rng = range(def, group), + data = vg.values(group.datum); + + m(def, s, rng, db, data); + return s; + } + + function instance(def, scale) { + var type = def.type || LINEAR; + if (!scale || type !== scale.type) { + var ctor = vg.config.scale[type] || d3.scale[type]; + if (!ctor) vg.error("Unrecognized scale type: " + type); + (scale = ctor()).type = scale.type || type; + scale.scaleName = def.name; + } + return scale; + } + + function ordinal(def, scale, rng, db, data) { + var domain, refs, values, str; + + // domain + domain = def.domain; + if (vg.isArray(domain)) { + scale.domain(domain); + } else if (vg.isObject(domain)) { + refs = def.domain.fields || vg.array(def.domain); + values = refs.reduce(function(values, r) { + var dat = vg.values(db[r.data] || data), + get = vg.accessor(vg.isString(r.field) + ? r.field : "data." + vg.accessor(r.field.group)(data)); + return vg.unique(dat, get, values); + }, []); + if (def.sort) values.sort(vg.cmp); + scale.domain(values); + } + + // range + str = typeof rng[0] === 'string'; + if (str || rng.length > 2) { + scale.range(rng); // color or shape values + } else if (def.points) { + scale.rangePoints(rng, def.padding||0); + } else if (def.round || def.round===undefined) { + scale.rangeRoundBands(rng, def.padding||0); + } else { + scale.rangeBands(rng, def.padding||0); + } + } + + function quantitative(def, scale, rng, db, data) { + var domain, refs, interval, z; + + // domain + domain = [null, null]; + function extract(ref, min, max, z) { + var dat = vg.values(db[ref.data] || data); + var fields = vg.array(ref.field).map(function(f) { + return vg.isString(f) ? f + : "data." + vg.accessor(f.group)(data); + }); + + fields.forEach(function(f,i) { + f = vg.accessor(f); + if (min) domain[0] = d3.min([domain[0], d3.min(dat, f)]); + if (max) domain[z] = d3.max([domain[z], d3.max(dat, f)]); + }); + } + if (def.domain !== undefined) { + if (vg.isArray(def.domain)) { + domain = def.domain.slice(); + } else if (vg.isObject(def.domain)) { + refs = def.domain.fields || vg.array(def.domain); + refs.forEach(function(r) { extract(r,1,1,1); }); + } else { + domain = def.domain; + } + } + z = domain.length - 1; + if (def.domainMin !== undefined) { + if (vg.isObject(def.domainMin)) { + domain[0] = null; + refs = def.domainMin.fields || vg.array(def.domainMin); + refs.forEach(function(r) { extract(r,1,0,z); }); + } else { + domain[0] = def.domainMin; + } + } + if (def.domainMax !== undefined) { + if (vg.isObject(def.domainMax)) { + domain[z] = null; + refs = def.domainMax.fields || vg.array(def.domainMax); + refs.forEach(function(r) { extract(r,0,1,z); }); + } else { + domain[z] = def.domainMax; + } + } + if (def.type !== LOG && def.type !== TIME && (def.zero || def.zero===undefined)) { + domain[0] = Math.min(0, domain[0]); + domain[z] = Math.max(0, domain[z]); + } + scale.domain(domain); + + // range + // vertical scales should flip by default, so use XOR here + if (def.range === "height") rng = rng.reverse(); + scale[def.round && scale.rangeRound ? "rangeRound" : "range"](rng); + + if (def.exponent && def.type===POWER) scale.exponent(def.exponent); + if (def.clamp) scale.clamp(true); + if (def.nice) { + if (def.type === TIME) { + interval = d3.time[def.nice]; + if (!interval) vg.error("Unrecognized interval: " + interval); + scale.nice(interval); + } else { + scale.nice(); + } + } + } + + function range(def, group) { + var rng = [null, null]; + + if (def.range !== undefined) { + if (typeof def.range === 'string') { + if (GROUP_PROPERTY[def.range]) { + rng = [0, group[def.range]]; + } else if (vg.config.range[def.range]) { + rng = vg.config.range[def.range]; + } else { + vg.error("Unrecogized range: "+def.range); + return rng; + } + } else if (vg.isArray(def.range)) { + rng = def.range; + } else { + rng = [0, def.range]; + } + } + if (def.rangeMin !== undefined) { + rng[0] = def.rangeMin; + } + if (def.rangeMax !== undefined) { + rng[rng.length-1] = def.rangeMax; + } + + if (def.reverse !== undefined) { + var rev = def.reverse; + if (vg.isObject(rev)) { + rev = vg.accessor(rev.field)(group.datum); + } + if (rev) rng = rng.reverse(); + } + + return rng; + } + + return scales; +})(); +vg.parse.spec = function(spec, callback, viewFactory) { + + viewFactory = viewFactory || vg.ViewFactory; + + function parse(spec) { + // protect against subsequent spec modification + spec = vg.duplicate(spec); + + var width = spec.width || 500, + height = spec.height || 500, + viewport = spec.viewport || null; + + var defs = { + width: width, + height: height, + viewport: viewport, + padding: vg.parse.padding(spec.padding), + marks: vg.parse.marks(spec, width, height), + data: vg.parse.data(spec.data, function() { callback(viewConstructor); }) + }; + + var viewConstructor = viewFactory(defs); + } + + vg.isObject(spec) ? parse(spec) : + d3.json(spec, function(error, json) { + error ? vg.error(error) : parse(json); + }); +};vg.parse.transform = function(def) { + var tx = vg.data[def.type](); + + vg.keys(def).forEach(function(k) { + if (k === 'type') return; + (tx[k])(def[k]); + }); + + return tx; +};vg.scene = {}; + +vg.scene.GROUP = "group", +vg.scene.ENTER = 0, +vg.scene.UPDATE = 1, +vg.scene.EXIT = 2; + +vg.scene.DEFAULT_DATA = {"sentinel":1} + +vg.scene.data = function(data, parentData) { + var DEFAULT = vg.scene.DEFAULT_DATA; + + // if data is undefined, inherit or use default + data = vg.values(data || parentData || [DEFAULT]); + + // if inheriting default data, ensure its in an array + if (data === DEFAULT) data = [DEFAULT]; + + return data; +}; + +vg.scene.fontString = function(o) { + return (o.fontStyle ? o.fontStyle + " " : "") + + (o.fontVariant ? o.fontVariant + " " : "") + + (o.fontWeight ? o.fontWeight + " " : "") + + (o.fontSize != null ? o.fontSize : vg.config.render.fontSize) + "px " + + (o.font || vg.config.render.font); +};vg.scene.Item = (function() { + function item(mark) { + this.mark = mark; + } + + var prototype = item.prototype; + + prototype.hasPropertySet = function(name) { + var props = this.mark.def.properties; + return props && props[name] != null; + }; + + prototype.cousin = function(offset, index) { + if (offset === 0) return this; + offset = offset || -1; + var mark = this.mark, + group = mark.group, + iidx = index==null ? mark.items.indexOf(this) : index, + midx = group.items.indexOf(mark) + offset; + return group.items[midx].items[iidx]; + }; + + prototype.sibling = function(offset) { + if (offset === 0) return this; + offset = offset || -1; + var mark = this.mark, + iidx = mark.items.indexOf(this) + offset; + return mark.items[iidx]; + }; + + prototype.remove = function() { + var item = this, + list = item.mark.items, + i = list.indexOf(item); + if (i >= 0) (i===list.length-1) ? list.pop() : list.splice(i, 1); + return item; + }; + + return item; +})(); + +vg.scene.item = function(mark) { + return new vg.scene.Item(mark); +};vg.scene.visit = function(node, func) { + var i, n, items; + if (func(node)) return true; + if (items = node.items) { + for (i=0, n=items.length; i0) s += "|"; + s += String(f[i](d)); + } + return s; + } + } + + return build; +})();vg.scene.bounds = (function() { + + var parse = vg.canvas.path.parse, + boundPath = vg.canvas.path.bounds, + areaPath = vg.canvas.path.area, + linePath = vg.canvas.path.line, + halfpi = Math.PI / 2, + sqrt3 = Math.sqrt(3), + tan30 = Math.tan(30 * Math.PI / 180), + gfx = null; + + function context() { + return gfx || (gfx = (vg.config.isNode + ? new (require("canvas"))(1,1) + : d3.select("body").append("canvas") + .attr("class", "vega_hidden") + .attr("width", 1) + .attr("height", 1) + .style("display", "none") + .node()) + .getContext("2d")); + } + + function pathBounds(o, path, bounds) { + if (path == null) { + bounds.set(0, 0, 0, 0); + } else { + boundPath(path, bounds); + if (o.stroke && o.opacity !== 0 && o.strokeWidth > 0) { + bounds.expand(o.strokeWidth); + } + } + return bounds; + } + + function path(o, bounds) { + var p = o.path + ? o["path:parsed"] || (o["path:parsed"] = parse(o.path)) + : null; + return pathBounds(o, p, bounds); + } + + function area(o, bounds) { + var items = o.mark.items, o = items[0]; + var p = o["path:parsed"] || (o["path:parsed"]=parse(areaPath(items))); + return pathBounds(items[0], p, bounds); + } + + function line(o, bounds) { + var items = o.mark.items, o = items[0]; + var p = o["path:parsed"] || (o["path:parsed"]=parse(linePath(items))); + return pathBounds(items[0], p, bounds); + } + + function rect(o, bounds) { + var x = o.x || 0, + y = o.y || 0, + w = (x + o.width) || 0, + h = (y + o.height) || 0; + bounds.set(x, y, w, h); + if (o.stroke && o.opacity !== 0 && o.strokeWidth > 0) { + bounds.expand(o.strokeWidth); + } + return bounds; + } + + function image(o, bounds) { + var w = o.width || 0, + h = o.height || 0, + x = (o.x||0) - (o.align === "center" + ? w/2 : (o.align === "right" ? w : 0)), + y = (o.y||0) - (o.baseline === "middle" + ? h/2 : (o.baseline === "bottom" ? h : 0)); + return bounds.set(x, y, x+w, y+h); + } + + function rule(o, bounds) { + var x1, y1; + bounds.set( + x1 = o.x || 0, + y1 = o.y || 0, + o.x2 != null ? o.x2 : x1, + o.y2 != null ? o.y2 : y1 + ); + if (o.stroke && o.opacity !== 0 && o.strokeWidth > 0) { + bounds.expand(o.strokeWidth); + } + return bounds; + } + + function arc(o, bounds) { + var cx = o.x || 0, + cy = o.y || 0, + ir = o.innerRadius || 0, + or = o.outerRadius || 0, + sa = (o.startAngle || 0) - halfpi, + ea = (o.endAngle || 0) - halfpi, + xmin = Infinity, xmax = -Infinity, + ymin = Infinity, ymax = -Infinity, + a, i, n, x, y, ix, iy, ox, oy; + + var angles = [sa, ea], + s = sa - (sa%halfpi); + for (i=0; i<4 && s 0) { + bounds.expand(o.strokeWidth); + } + return bounds; + } + + function symbol(o, bounds) { + var size = o.size != null ? o.size : 100, + x = o.x || 0, + y = o.y || 0, + r, t, rx, ry; + + switch (o.shape) { + case "cross": + r = Math.sqrt(size / 5) / 2; + t = 3*r; + bounds.set(x-t, y-t, x+y, y+t); + break; + + case "diamond": + ry = Math.sqrt(size / (2 * tan30)); + rx = ry * tan30; + bounds.set(x-rx, y-ry, x+rx, y+ry); + break; + + case "square": + t = Math.sqrt(size); + r = t / 2; + bounds.set(x-r, y-r, x+r, y+r); + break; + + case "triangle-down": + rx = Math.sqrt(size / sqrt3); + ry = rx * sqrt3 / 2; + bounds.set(x-rx, y-ry, x+rx, y+ry); + break; + + case "triangle-up": + rx = Math.sqrt(size / sqrt3); + ry = rx * sqrt3 / 2; + bounds.set(x-rx, y-ry, x+rx, y+ry); + break; + + default: + r = Math.sqrt(size/Math.PI); + bounds.set(x-r, y-r, x+r, y+r); + } + if (o.stroke && o.opacity !== 0 && o.strokeWidth > 0) { + bounds.expand(o.strokeWidth); + } + return bounds; + } + + function text(o, bounds, noRotate) { + var x = (o.x || 0) + (o.dx || 0), + y = (o.y || 0) + (o.dy || 0), + h = o.fontSize || vg.config.render.fontSize, + a = o.align, + b = o.baseline, + g = context(), w; + + g.font = vg.scene.fontString(o); + g.textAlign = a || "left"; + g.textBaseline = b || "alphabetic"; + w = g.measureText(o.text || "").width; + + // horizontal + if (a === "center") { + x = x - (w / 2); + } else if (a === "right") { + x = x - w; + } else { + // left by default, do nothing + } + + /// TODO find a robust solution for heights. + /// These offsets work for some but not all fonts. + + // vertical + if (b === "top") { + y = y + (h/5); + } else if (b === "bottom") { + y = y - h; + } else if (b === "middle") { + y = y - (h/2) + (h/10); + } else { + y = y - 4*h/5; // alphabetic by default + } + + bounds.set(x, y, x+w, y+h); + if (o.angle && !noRotate) { + bounds.rotate(o.angle*Math.PI/180, o.x||0, o.y||0); + } + return bounds.expand(noRotate ? 0 : 1); + } + + function group(g, bounds, includeLegends) { + var axes = g.axisItems || [], + legends = g.legendItems || [], j, m; + + for (j=0, m=axes.length; j 1) f = 1; + e = curr.ease(f); + + for (i=0, n=curr.length; i 1 ? +y : tickMajorSize, + end = n > 0 ? +arguments[n] : tickMajorSize; + + if (tickMajorSize !== major || + tickMinorSize !== minor || + tickEndSize !== end) { + reset(); + } + + tickMajorSize = major; + tickMinorSize = minor; + tickEndSize = end; + return axis; + }; + + axis.tickSubdivide = function(x) { + if (!arguments.length) return tickSubdivide; + tickSubdivide = +x; + return axis; + }; + + axis.offset = function(x) { + if (!arguments.length) return offset; + offset = vg.isObject(x) ? x : +x; + return axis; + }; + + axis.tickPadding = function(x) { + if (!arguments.length) return tickPadding; + if (tickPadding !== +x) { tickPadding = +x; reset(); } + return axis; + }; + + axis.titleOffset = function(x) { + if (!arguments.length) return titleOffset; + if (titleOffset !== +x) { titleOffset = +x; reset(); } + return axis; + }; + + axis.layer = function(x) { + if (!arguments.length) return layer; + if (layer !== x) { layer = x; reset(); } + return axis; + }; + + axis.grid = function(x) { + if (!arguments.length) return grid; + if (grid !== x) { grid = x; reset(); } + return axis; + }; + + axis.gridLineProperties = function(x) { + if (!arguments.length) return gridLineStyle; + if (gridLineStyle !== x) { gridLineStyle = x; } + return axis; + }; + + axis.majorTickProperties = function(x) { + if (!arguments.length) return majorTickStyle; + if (majorTickStyle !== x) { majorTickStyle = x; } + return axis; + }; + + axis.minorTickProperties = function(x) { + if (!arguments.length) return minorTickStyle; + if (minorTickStyle !== x) { minorTickStyle = x; } + return axis; + }; + + axis.tickLabelProperties = function(x) { + if (!arguments.length) return tickLabelStyle; + if (tickLabelStyle !== x) { tickLabelStyle = x; } + return axis; + }; + + axis.titleProperties = function(x) { + if (!arguments.length) return titleStyle; + if (titleStyle !== x) { titleStyle = x; } + return axis; + }; + + axis.domainProperties = function(x) { + if (!arguments.length) return domainStyle; + if (domainStyle !== x) { domainStyle = x; } + return axis; + }; + + axis.reset = function() { reset(); }; + + return axis; +}; + +var vg_axisOrients = {top: 1, right: 1, bottom: 1, left: 1}; + +function vg_axisSubdivide(scale, ticks, m) { + subticks = []; + if (m && ticks.length > 1) { + var extent = vg_axisScaleExtent(scale.domain()), + subticks, + i = -1, + n = ticks.length, + d = (ticks[1] - ticks[0]) / ++m, + j, + v; + while (++i < n) { + for (j = m; --j > 0;) { + if ((v = +ticks[i] - j * d) >= extent[0]) { + subticks.push(v); + } + } + } + for (--i, j = 0; ++j < m && (v = +ticks[i] + j * d) < extent[1];) { + subticks.push(v); + } + } + return subticks; +} + +function vg_axisScaleExtent(domain) { + var start = domain[0], stop = domain[domain.length - 1]; + return start < stop ? [start, stop] : [stop, start]; +} + +function vg_axisScaleRange(scale) { + return scale.rangeExtent + ? scale.rangeExtent() + : vg_axisScaleExtent(scale.range()); +} + +var vg_axisAlign = { + bottom: "center", + top: "center", + left: "right", + right: "left" +}; + +var vg_axisBaseline = { + bottom: "top", + top: "bottom", + left: "middle", + right: "middle" +}; + +function vg_axisLabelExtend(orient, labels, oldScale, newScale, size, pad) { + size = Math.max(size, 0) + pad; + if (orient === "left" || orient === "top") { + size *= -1; + } + if (orient === "top" || orient === "bottom") { + vg.extend(labels.properties.enter, { + x: oldScale, + y: {value: size}, + }); + vg.extend(labels.properties.update, { + x: newScale, + y: {value: size}, + align: {value: "center"}, + baseline: {value: vg_axisBaseline[orient]} + }); + } else { + vg.extend(labels.properties.enter, { + x: {value: size}, + y: oldScale, + }); + vg.extend(labels.properties.update, { + x: {value: size}, + y: newScale, + align: {value: vg_axisAlign[orient]}, + baseline: {value: "middle"} + }); + } +} + +function vg_axisTicksExtend(orient, ticks, oldScale, newScale, size) { + var sign = (orient === "left" || orient === "top") ? -1 : 1; + if (size === Infinity) { + size = (orient === "top" || orient === "bottom") + ? {group: "mark.group.height", mult: -sign} + : {group: "mark.group.width", mult: -sign}; + } else { + size = {value: sign * size}; + } + if (orient === "top" || orient === "bottom") { + vg.extend(ticks.properties.enter, { + x: oldScale, + y: {value: 0}, + y2: size + }); + vg.extend(ticks.properties.update, { + x: newScale, + y: {value: 0}, + y2: size + }); + vg.extend(ticks.properties.exit, { + x: newScale, + }); + } else { + vg.extend(ticks.properties.enter, { + x: {value: 0}, + x2: size, + y: oldScale + }); + vg.extend(ticks.properties.update, { + x: {value: 0}, + x2: size, + y: newScale + }); + vg.extend(ticks.properties.exit, { + y: newScale, + }); + } +} + +function vg_axisTitleExtend(orient, title, range, offset) { + var mid = ~~((range[1] - range[0]) / 2), + sign = (orient === "top" || orient === "left") ? -1 : 1; + + if (orient === "bottom" || orient === "top") { + vg.extend(title.properties.update, { + x: {value: mid}, + y: {value: sign*offset}, + angle: {value: 0} + }); + } else { + vg.extend(title.properties.update, { + x: {value: sign*offset}, + y: {value: mid}, + angle: {value: -90} + }); + } +} + +function vg_axisDomainExtend(orient, domain, range, size) { + var path; + if (orient === "top" || orient === "left") { + size = -1 * size; + } + if (orient === "bottom" || orient === "top") { + path = "M" + range[0] + "," + size + "V0H" + range[1] + "V" + size; + } else { + path = "M" + size + "," + range[0] + "H0V" + range[1] + "H" + size; + } + domain.properties.update.path = {value: path}; +} + +function vg_axisUpdate(item, group, trans) { + var o = trans ? {} : item, + offset = item.mark.def.offset, + orient = item.mark.def.orient, + width = group.width, + height = group.height; // TODO fallback to global w,h? + + if (vg.isObject(offset)) { + offset = -group.scales[offset.scale](offset.value); + } + + switch (orient) { + case "left": { o.x = -offset; o.y = 0; break; } + case "right": { o.x = width + offset; o.y = 0; break; } + case "bottom": { o.x = 0; o.y = height + offset; break; } + case "top": { o.x = 0; o.y = -offset; break; } + default: { o.x = 0; o.y = 0; } + } + + if (trans) trans.interpolate(item, o); +} + +function vg_axisTicks() { + return { + type: "rule", + interactive: false, + key: "data", + properties: { + enter: { + stroke: {value: vg.config.axis.tickColor}, + strokeWidth: {value: vg.config.axis.tickWidth}, + opacity: {value: 1e-6} + }, + exit: { opacity: {value: 1e-6} }, + update: { opacity: {value: 1} } + } + }; +} + +function vg_axisTickLabels() { + return { + type: "text", + interactive: true, + key: "data", + properties: { + enter: { + fill: {value: vg.config.axis.tickLabelColor}, + font: {value: vg.config.axis.tickLabelFont}, + fontSize: {value: vg.config.axis.tickLabelFontSize}, + opacity: {value: 1e-6}, + text: {field: "label"} + }, + exit: { opacity: {value: 1e-6} }, + update: { opacity: {value: 1} } + } + }; +} + +function vg_axisTitle() { + return { + type: "text", + interactive: true, + properties: { + enter: { + font: {value: vg.config.axis.titleFont}, + fontSize: {value: vg.config.axis.titleFontSize}, + fontWeight: {value: vg.config.axis.titleFontWeight}, + fill: {value: vg.config.axis.titleColor}, + align: {value: "center"}, + baseline: {value: "middle"}, + text: {field: "data"} + }, + update: {} + } + }; +} + +function vg_axisDomain() { + return { + type: "path", + interactive: false, + properties: { + enter: { + x: {value: 0.5}, + y: {value: 0.5}, + stroke: {value: vg.config.axis.axisColor}, + strokeWidth: {value: vg.config.axis.axisWidth} + }, + update: {} + } + }; +} +vg.scene.legend = function() { + var size = null, + shape = null, + fill = null, + stroke = null, + spacing = null, + values = null, + format = null, + title = undefined, + orient = "right", + offset = vg.config.legend.offset, + padding = vg.config.legend.padding, + legendDef, + tickArguments = [5], + legendStyle = {}, + symbolStyle = {}, + gradientStyle = {}, + titleStyle = {}, + labelStyle = {}; + + var legend = {}, + legendDef = null; + + function reset() { legendDef = null; } + + legend.def = function() { + var scale = size || shape || fill || stroke; + if (!legendDef) { + legendDef = (scale===fill || scale===stroke) && !discrete(scale.type) + ? quantDef(scale) + : ordinalDef(scale); + } + legendDef.orient = orient; + legendDef.offset = offset; + legendDef.padding = padding; + return legendDef; + }; + + function discrete(type) { + return type==="ordinal" || type==="quantize" + || type==="quantile" || type==="threshold"; + } + + function ordinalDef(scale) { + var def = o_legend_def(size, shape, fill, stroke); + + // generate data + var data = (values == null + ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()) + : values).map(vg.data.ingest); + var fmt = format==null ? (scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments) : String) : format; + + // determine spacing between legend entries + var fs, range, offset, pad=5, domain = d3.range(data.length); + if (size) { + range = data.map(function(x) { return Math.sqrt(size(x.data)); }); + offset = d3.max(range); + range = range.reduce(function(a,b,i,z) { + if (i > 0) a[i] = a[i-1] + z[i-1]/2 + pad; + return (a[i] += b/2, a); }, [0]).map(Math.round); + } else { + offset = Math.round(Math.sqrt(vg.config.legend.symbolSize)); + range = spacing + || (fs = labelStyle.fontSize) && (fs.value + pad) + || (vg.config.legend.labelFontSize + pad); + range = domain.map(function(d,i) { + return Math.round(offset/2 + i*range); + }); + } + + // account for padding and title size + var sz = padding, ts; + if (title) { + ts = titleStyle.fontSize; + sz += 5 + ((ts && ts.value) || vg.config.legend.titleFontSize); + } + for (var i=0, n=range.length; i this._width ? Math.ceil(+b.x2 - this._width) + inset : 0, + b = b.y2 > this._height ? Math.ceil(+b.y2 - this._height) + inset : 0; + pad = {left:l, top:t, right:r, bottom:b}; + + if (this._strict) { + this._autopad = 0; + this._padding = pad; + this._width = Math.max(0, this.__width - (l+r)); + this._height = Math.max(0, this.__height - (t+b)); + this._model.width(this._width); + this._model.height(this._height); + if (this._el) this.initialize(this._el.parentNode); + this.update({props:"enter"}).update({props:"update"}); + } else { + this.padding(pad).update(opt); + } + return this; + }; + + prototype.viewport = function(size) { + if (!arguments.length) return this._viewport; + if (this._viewport !== size) { + this._viewport = size; + if (this._el) this.initialize(this._el.parentNode); + } + return this; + }; + + prototype.renderer = function(type) { + if (!arguments.length) return this._io; + if (type === "canvas") type = vg.canvas; + if (type === "svg") type = vg.svg; + if (this._io !== type) { + this._io = type; + this._renderer = null; + if (this._el) this.initialize(this._el.parentNode); + if (this._build) this.render(); + } + return this; + }; + + prototype.defs = function(defs) { + if (!arguments.length) return this._model.defs(); + this._model.defs(defs); + return this; + }; + + prototype.data = function(data) { + if (!arguments.length) return this._model.data(); + var ingest = vg.keys(data).reduce(function(d, k) { + return (d[k] = vg.data.ingestAll(data[k]), d); + }, {}); + this._model.data(ingest); + this._build = false; + return this; + }; + + prototype.model = function(model) { + if (!arguments.length) return this._model; + if (this._model !== model) { + this._model = model; + if (this._handler) this._handler.model(model); + } + return this; + }; + + prototype.initialize = function(el) { + var v = this, prevHandler, + w = v._width, h = v._height, pad = v._padding; + + // clear pre-existing container + d3.select(el).select("div.vega").remove(); + + // add div container + this._el = el = d3.select(el) + .append("div") + .attr("class", "vega") + .style("position", "relative") + .node(); + if (v._viewport) { + d3.select(el) + .style("width", (v._viewport[0] || w)+"px") + .style("height", (v._viewport[1] || h)+"px") + .style("overflow", "auto"); + } + + // renderer + v._renderer = (v._renderer || new this._io.Renderer()) + .initialize(el, w, h, pad); + + // input handler + prevHandler = v._handler; + v._handler = new this._io.Handler() + .initialize(el, pad, v) + .model(v._model); + + if (prevHandler) { + prevHandler.handlers().forEach(function(h) { + v._handler.on(h.type, h.handler); + }); + } + + return this; + }; + + prototype.render = function(items) { + this._renderer.render(this._model.scene(), items); + return this; + }; + + prototype.on = function() { + this._handler.on.apply(this._handler, arguments); + return this; + }; + + prototype.off = function() { + this._handler.off.apply(this._handler, arguments); + return this; + }; + + prototype.update = function(opt) { + opt = opt || {}; + var view = this, + trans = opt.duration + ? vg.scene.transition(opt.duration, opt.ease) + : null; + + view._build = view._build || (view._model.build(), true); + view._model.encode(trans, opt.props, opt.items); + + if (trans) { + trans.start(function(items) { + view._renderer.render(view._model.scene(), items); + }); + } + else view.render(opt.items); + + return view.autopad(opt); + }; + + return view; +})(); + +// view constructor factory +// takes definitions from parsed specification as input +// returns a view constructor +vg.ViewFactory = function(defs) { + return function(opt) { + opt = opt || {}; + var v = new vg.View() + .width(defs.width) + .height(defs.height) + .padding(defs.padding) + .viewport(defs.viewport) + .renderer(opt.renderer || "canvas") + .defs(defs); + + if (defs.data.load) v.data(defs.data.load); + if (opt.data) v.data(opt.data); + if (opt.el) v.initialize(opt.el); + + if (opt.hover !== false) { + v.on("mouseover", function(evt, item) { + if (item.hasPropertySet("hover")) { + this.update({props:"hover", items:item}); + } + }) + .on("mouseout", function(evt, item) { + if (item.hasPropertySet("hover")) { + this.update({props:"update", items:item}); + } + }); + } + + return v; + }; +}; +vg.Spec = (function() { + var spec = function(s) { + this.spec = { + width: 500, + height: 500, + padding: 0, + data: [], + scales: [], + axes: [], + marks: [] + }; + if (s) vg.extend(this.spec, s); + }; + + var prototype = spec.prototype; + + prototype.width = function(w) { + this.spec.width = w; + return this; + }; + + prototype.height = function(h) { + this.spec.height = h; + return this; + }; + + prototype.padding = function(p) { + this.spec.padding = p; + return this; + }; + + prototype.viewport = function(v) { + this.spec.viewport = v; + return this; + }; + + prototype.data = function(name, params) { + if (!params) params = vg.isString(name) ? {name: name} : name; + else params.name = name; + this.spec.data.push(params); + return this; + }; + + prototype.scale = function(name, params) { + if (!params) params = vg.isString(name) ? {name: name} : name; + else params.name = name; + this.spec.scales.push(params); + return this; + }; + + prototype.axis = function(params) { + this.spec.axes.push(params); + return this; + }; + + prototype.mark = function(type, mark) { + if (!mark) mark = {type: type}; + else mark.type = type; + mark.properties = {}; + this.spec.marks.push(mark); + + var that = this; + return { + from: function(name, obj) { + mark.from = obj + ? (obj.data = name, obj) + : vg.isString(name) ? {data: name} : name; + return this; + }, + prop: function(name, obj) { + mark.properties[name] = vg.keys(obj).reduce(function(o,k) { + var v = obj[k]; + return (o[k] = vg.isObject(v) ? v : {value: v}, o); + }, {}); + return this; + }, + done: function() { return that; } + }; + }; + + prototype.parse = function(callback) { + vg.parse.spec(this.spec, callback); + }; + + prototype.json = function() { + return this.spec; + }; + + return spec; +})(); + +vg.spec = function(s) { + return new vg.Spec(s); +}; +vg.headless = {};vg.headless.View = (function() { + + var view = function(width, height, pad, type) { + this._canvas = null; + this._type = type; + this._el = "body"; + this._build = false; + this._model = new vg.Model(); + this._width = this.__width = width || 500; + this._height = this.__height = height || 500; + this._autopad = 1; + this._padding = pad || {top:0, left:0, bottom:0, right:0}; + this._renderer = new vg[type].Renderer(); + this.initialize(); + }; + + var prototype = view.prototype; + + prototype.el = function(el) { + if (!arguments.length) return this._el; + if (this._el !== el) { + this._el = el; + this.initialize(); + } + return this; + }; + + prototype.width = function(width) { + if (!arguments.length) return this._width; + if (this._width !== width) { + this._width = width; + this.initialize(); + this._model.width(width); + } + return this; + }; + + prototype.height = function(height) { + if (!arguments.length) return this._height; + if (this._height !== height) { + this._height = height; + this.initialize(); + this._model.height(this._height); + } + return this; + }; + + prototype.padding = function(pad) { + if (!arguments.length) return this._padding; + if (this._padding !== pad) { + if (vg.isString(pad)) { + this._autopad = 1; + this._padding = {top:0, left:0, bottom:0, right:0}; + this._strict = (pad === "strict"); + } else { + this._autopad = 0; + this._padding = pad; + this._strict = false; + } + this.initialize(); + } + return this; + }; + + prototype.autopad = function(opt) { + if (this._autopad < 1) return this; + else this._autopad = 0; + + var pad = this._padding, + b = this._model.scene().bounds, + inset = vg.config.autopadInset, + l = b.x1 < 0 ? Math.ceil(-b.x1) + inset : 0, + t = b.y1 < 0 ? Math.ceil(-b.y1) + inset : 0, + r = b.x2 > this._width ? Math.ceil(+b.x2 - this._width) + inset : 0, + b = b.y2 > this._height ? Math.ceil(+b.y2 - this._height) + inset : 0; + pad = {left:l, top:t, right:r, bottom:b}; + + if (this._strict) { + this._autopad = 0; + this._padding = pad; + this._width = Math.max(0, this.__width - (l+r)); + this._height = Math.max(0, this.__height - (t+b)); + this._model.width(this._width); + this._model.height(this._height); + if (this._el) this.initialize(); + this.update({props:"enter"}).update({props:"update"}); + } else { + this.padding(pad).update(opt); + } + return this; + }; + + prototype.viewport = function() { + if (!arguments.length) return null; + return this; + }; + + prototype.defs = function(defs) { + if (!arguments.length) return this._model.defs(); + this._model.defs(defs); + return this; + }; + + prototype.data = function(data) { + if (!arguments.length) return this._model.data(); + var ingest = vg.keys(data).reduce(function(d, k) { + return (d[k] = vg.data.ingestAll(data[k]), d); + }, {}); + this._model.data(ingest); + this._build = false; + return this; + }; + + prototype.renderer = function() { + return this._renderer; + }; + + prototype.canvas = function() { + return this._canvas; + }; + + prototype.canvasAsync = function(callback) { + var r = this._renderer, view = this; + + function wait() { + if (r.pendingImages() === 0) { + view.render(); // re-render with all images + callback(view._canvas); + } else { + setTimeout(wait, 10); + } + } + + // if images loading, poll until ready + (r.pendingImages() > 0) ? wait() : callback(this._canvas); + }; + + prototype.svg = function() { + if (this._type !== "svg") return null; + + var p = this._padding, + w = this._width + (p ? p.left + p.right : 0), + h = this._height + (p ? p.top + p.bottom : 0); + + // build svg text + var svg = d3.select(this._el) + .select("svg").node().innerHTML + .replace(/ href=/g, " xlink:href="); // ns hack. sigh. + + return '' + svg + '' + }; + + prototype.initialize = function() { + var w = this._width, + h = this._height, + pad = this._padding; + + if (this._type === "svg") { + this.initSVG(w, h, pad); + } else { + this.initCanvas(w, h, pad); + } + + return this; + }; + + prototype.initCanvas = function(w, h, pad) { + var Canvas = require("canvas"), + tw = w + pad.left + pad.right, + th = h + pad.top + pad.bottom, + canvas = this._canvas = new Canvas(tw, th), + ctx = canvas.getContext("2d"); + + // setup canvas context + ctx.setTransform(1, 0, 0, 1, pad.left, pad.top); + + // configure renderer + this._renderer.context(ctx); + this._renderer.resize(w, h, pad); + }; + + prototype.initSVG = function(w, h, pad) { + var tw = w + pad.left + pad.right, + th = h + pad.top + pad.bottom; + + // configure renderer + this._renderer.initialize(this._el, w, h, pad); + } + + prototype.render = function(items) { + this._renderer.render(this._model.scene(), items); + return this; + }; + + prototype.update = function(opt) { + opt = opt || {}; + var view = this; + view._build = view._build || (view._model.build(), true); + view._model.encode(null, opt.props, opt.items); + view.render(opt.items); + return view.autopad(opt); + }; + + return view; +})(); + +// headless view constructor factory +// takes definitions from parsed specification as input +// returns a view constructor +vg.headless.View.Factory = function(defs) { + return function(opt) { + opt = opt || {}; + var w = defs.width, + h = defs.height, + p = defs.padding, + r = opt.renderer || "canvas", + v = new vg.headless.View(w, h, p, r).defs(defs); + if (defs.data.load) v.data(defs.data.load); + if (opt.data) v.data(opt.data); + return v; + }; +};vg.headless.render = function(opt, callback) { + function draw(chart) { + try { + // create and render view + var view = chart({ + data: opt.data, + renderer: opt.renderer + }).update(); + + if (opt.renderer === "svg") { + // extract rendered svg + callback(null, {svg: view.svg()}); + } else { + // extract rendered canvas, waiting for any images to load + view.canvasAsync(function(canvas) { + callback(null, {canvas: canvas}); + }); + } + } catch (err) { + callback(err, null); + } + } + + vg.parse.spec(opt.spec, draw, vg.headless.View.Factory); +}; return vg; +})(d3, typeof topojson === "undefined" ? null : topojson); +// assumes D3 and topojson in global namespace