//Copyright (c) 2013, Trifacta Inc. //All rights reserved.//// //Redistribution and use in source and binary forms, with or without //modification, are permitted provided that the following conditions are met: //* Redistributions of source code must retain the above copyright notice, this // list of conditions and the following disclaimer. //* Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. //* Neither the name of Trifacta Inc. nor the names of its contributors may be // used to endorse or promote products derived from this software without // specific prior written permission. //THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" //AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE //IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE //DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE //FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL //DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR //SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER //CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, //OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE //OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. vg = (function(d3){ // take d3 instance as sole import var vg = {}; // semantic versioning vg.version = '1.2.0'; // 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.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.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(str) { return vg.isArray(str) ? "[" + str.map(vg.str) + "]" : vg.isString(str) ? ("'"+str+"'") : str; }; vg.keys = function(x) { var keys = []; for (var key in x) keys.push(key); return keys; }; vg.unique = function(data, f) { f = f || vg.identity; var results = [], v; for (var i=0; i 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.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 = stroke; g.lineWidth = lw; g.lineCap = (lc = o.strokeCap) != undefined ? lc : "butt"; g.stroke(); o.bounds.expand(lw); } } } 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 = stroke; g.lineWidth = lw; g.lineCap = (lc = o.strokeCap) != undefined ? lc : "butt"; g.strokeRect(x, y, o.width, o.height); o.bounds.expand(lw); } } } } 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 = stroke; g.lineWidth = lw; g.lineCap = (lc = o.strokeCap) != undefined ? lc : "butt"; g.beginPath(); g.moveTo(x1, y1); g.lineTo(x2, y2); g.stroke(); o.bounds.expand(lw); } } } } 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 = stroke; g.lineWidth = lw; g.strokeText(o.text, x, y); } } if (o.angle) { g.restore(); } } } function textBounds(g, o, bounds, noRotate) { var x = o.x + (o.dx || 0), y = o.y + (o.dy || 0), w = g.measureText(o.text).width, h = o.fontSize, a = o.align, b = o.baseline, angle, cos, sin, cx, cy; // 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 { // alphabetic by default y = y - 4*h/5; } bounds.set(x, y, x+w, y+h); if (!noRotate && o.angle) { bounds.rotate(o.angle*Math.PI/180, o.x, o.y); } return bounds; } 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, renderer = this, gx, gy, i, n, j, m; drawRect(g, scene, bounds); for (i=0, n=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) { // TODO... return false; } function pickRule(g, scene, x, y, gx, gy) { // TODO... return false; } function pick(test) { return function (g, scene, x, y, gx, gy) { return pickAll(test, g, scene, x, y, gx, gy); }; } var hitTests = { text: hitTestText, rect: function(g,o,x,y) { return true; }, // bounds test is sufficient image: function(g,o,x,y) { return true; }, // bounds test is sufficient 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);}, }; function hitTestText(g, o, x, y, gx, gy) { if (!o.fontSize) return false; if (!o.angle) return true; // bounds sufficient if no rotation g.font = fontString(o); var b = textBounds(g, 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); } 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: pickRule, 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; }; var prototype = renderer.prototype; prototype.initialize = function(el, width, height, pad) { this._el = el; this._width = width; this._height = height; this._padding = pad; 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"); // 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); 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; } prototype.context = function(ctx) { if (ctx) { this._ctx = ctx; return this; } else return this._ctx; }; prototype.element = function() { return this._el; }; function translatedBounds(item) { var b = new vg.Bounds(item.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)); }, 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); }; 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 = this._scene; var image = new Image(); image.onload = function() { vg.log("LOAD IMAGE: "+this.src); renderer.renderAsync(scene); }; image.src = uri; 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.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 textAlign = { "left": "start", "center": "middle", "right": "end" }; var styles = { "fill": "fill", "fillOpacity": "fill-opacity", "stroke": "stroke", "strokeWidth": "stroke-width", "strokeOpacity": "stroke-opacity", "opacity": "opacity" }; var styleProps = vg.keys(styles); function style(d) { var o = d.mark ? d : d[0], i, n, prop, name, value; for (i=0, n=styleProps.length; i "+tag).data(data), e = m.enter().append(tag); if (tag !== "g") { p.style("pointer-events", evts); e.each(function(d) { (d.mark ? d : d[0])._svg = this; }); } else { e.append("rect") .attr("class", "background") .style("pointer-events", evts); } m.exit().remove(); m.each(attr); if (tag !== "g") { m.each(style); } else { p.selectAll(id+" > "+tag+" > rect.background") .each(group_bg).each(style); } } function drawGroup(g, scene, index) { var renderer = this; drawMark(g, scene, index, "group_", "g", group); var x = g.select(".group_"+index).node(), i, n, j, m; for (i=0, n=x.childNodes.length; 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.ingest = function(datum, index) { return { data: datum, index: index }; }; vg.data.mapper = function(func) { return function(data) { data.forEach(func); return data; } }; vg.data.size = function(size, group) { size = Array.isArray(size) ? size : [0, size]; size = size.map(function(d) { return (typeof d === 'string') ? group[d] : d; }); return size; };vg.data.load = function(uri, callback) { if (vg.config.isNode) { // in node.js, consult base url and select file or http var url = vg_load_hasProtocol(uri) ? uri : vg.config.baseURL + uri, get = vg_load_isFile(url) ? vg_load_file : vg_load_http; get(url, callback); } else { // in browser, use xhr vg_load_xhr(uri, callback); } }; var vg_load_protocolRE = /^[A-Za-z]+\:\/\//; var vg_load_fileProtocol = "file://"; function vg_load_hasProtocol(url) { return vg_load_protocolRE.test(url); } function vg_load_isFile(url) { return url.indexOf(vg_load_fileProtocol) === 0; } function vg_load_xhr(url, callback) { vg.log("LOAD: " + url); d3.xhr(url, function(err, resp) { if (resp) resp = resp.responseText; callback(err, resp); }); } function vg_load_file(file, callback) { vg.log("LOAD FILE: " + file); var idx = file.indexOf(vg_load_fileProtocol); if (idx >= 0) file = file.slice(vg_load_fileProtocol.length); require("fs").readFile(file, {encoding:"utf8"}, callback); } function vg_load_http(url, callback) { vg.log("LOAD HTTP: " + url); var req = require("http").request(url, function(res) { var data = ""; res.setEncoding("utf8"); res.on("error", function(err) { callback(err, null); }); res.on("data", function(chunk) { data += chunk; }); 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; }; function parseValues(data, types) { var cols = vg.keys(types), p = cols.map(function(col) { return parsers[types[col]]; }), 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 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); return o; } function stats(data) { return (Array.isArray(data) ? [data] : data.values || []) .map(reduce); // no pun intended } stats.median = function(bool) { median = bool || false; return stats; }; stats.value = function(field) { value = vg.accessor(field); return stats; }; stats.output = function(map) { vg.keys(output).forEach(function(k) { if (map[k] !== undefined) { output[k] = map[k]; } }); return stats; }; return stats; };vg.data.treemap = function() { var layout = d3.layout.treemap() .children(function(d) { return d.values; }), value = vg.accessor("data"), size = ["width", "height"], params = ["round", "sticky", "ratio", "padding"], output = { "x": "x", "y": "y", "dx": "width", "dy": "height" }; function treemap(data, db, group) { data = layout .size(vg.data.size(size, group)) .value(value) .nodes(data); var keys = vg.keys(output), len = keys.length; data.forEach(function(d) { var key, val; for (var i=0; i 0) ? "\n " : " "; code += "o."+name+" = "+valueRef(ref)+";"; vars[name] = true; } if (vars.x2) { 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);" } if (vars.y2) { 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);" } code += "if (trans) trans.interpolate(item, o);"; return Function("item", "group", "trans", code); } // TODO security check for strings emitted into code function valueRef(ref) { if (ref == null) return null; var val = ref.value !== undefined ? vg.str(ref.value) : "item.datum.data"; // get data field value if (ref.field !== undefined) { val = "item.datum[" + vg.field(ref.field).map(vg.str).join("][") + "]"; } // run through scale function if (ref.scale !== undefined) { var scale = "group.scales['"+ref.scale+"']"; if (ref.band) { val = scale + ".rangeBand()"; } else { val = scale + "(" + val + ")"; } } // multiply, offset, return value return "(" + (ref.mult ? (ref.mult+" * ") : "") + val + ")" + (ref.offset ? " + "+ref.offset : ""); } return compile; })();vg.parse.scales = (function() { var LINEAR = "linear", ORDINAL = "ordinal", LOG = "log", POWER = "pow", TIME = "time", GROUP_PROPERTY = {width: 1, height: 1}; var SCALES = { "time": d3.time.scale, "utc": d3.time.scale.utc }; 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 = SCALES[type] || d3.scale[type]; if (!ctor) vg.error("Unrecognized scale type: " + type); (scale = ctor()).type = type; scale.scaleName = def.name; } return scale; } function ordinal(def, scale, rng, db, data) { var domain, dat, get, str; // domain domain = def.domain; if (Array.isArray(domain)) { scale.domain(domain); } else if (vg.isObject(domain)) { dat = db[domain.data] || data; get = vg.accessor(domain.field); scale.domain(vg.unique(dat, get)); } // 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, dat, interval; // domain domain = [null, null]; if (def.domain !== undefined) { if (vg.isArray(def.domain)) { domain = def.domain.slice(); } else if (vg.isObject(def.domain)) { dat = db[def.domain.data] || data; vg.array(def.domain.field).forEach(function(f,i) { f = vg.accessor(f); domain[0] = d3.min([domain[0], d3.min(dat, f)]); domain[1] = d3.max([domain[1], d3.max(dat, f)]); }); } else { domain = def.domain; } } if (def.domainMin !== undefined) { if (vg.isObject(def.domainMin)) { domain[0] = null; dat = db[def.domainMin.data] || data; vg.array(def.domainMin.field).forEach(function(f,i) { f = vg.accessor(f); domain[0] = d3.min([domain[0], d3.min(dat, f)]); }); } else { domain[0] = def.domainMin; } } if (def.domainMax !== undefined) { if (vg.isObject(def.domainMax)) { domain[1] = null; dat = db[def.domainMax.data] || data; vg.array(def.domainMax.field).forEach(function(f,i) { f = vg.accessor(f); domain[1] = d3.max([domain[1], d3.max(dat, f)]); }); } else { domain[1] = def.domainMax; } } if (def.type !== LOG && def.type !== TIME && (def.zero || def.zero===undefined)) { domain[0] = Math.min(0, domain[0]); domain[1] = Math.max(0, domain[1]); } scale.domain(domain); // range // vertical scales should flip by default, so use XOR here if (def.range=='height') rng = rng.reverse(); scale[def.round ? "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 (Array.isArray(def.range)) { rng = def.range; } else { rng = [0, def.range]; } } if (def.rangeMin !== undefined) { rng[0] = def.rangeMin; } if (def.rangeMax !== undefined) { rng[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) { 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.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.build = (function() { var GROUP = vg.scene.GROUP, ENTER = vg.scene.ENTER, UPDATE = vg.scene.UPDATE, EXIT = vg.scene.EXIT, DEFAULT= {"sentinel":1}; function build(model, db, node, parentData) { var data = vg.scene.data( model.from ? model.from(db, node, parentData) : null, parentData); // build node and items node = buildNode(model, node); node.items = buildItems(model, data, node); buildTrans(model, node); // recurse if group if (model.type === GROUP) { buildGroup(model, db, node); } return node; }; function buildNode(model, node) { node = node || {}; node.def = model; node.marktype = model.type; node.interactive = !(model.interactive === false); return node; } function buildItems(model, data, node) { var keyf = keyFunction(model.key), prev = node.items || [], next = [], map = {}, i, key, len, item, datum, enter; for (i=0, len=prev.length; i 1) f = 1; e = curr.ease(f); for (i=0, n=curr.length; i 1 ? +y : tickMajorSize; tickEndSize = n > 0 ? +arguments[n] : tickMajorSize; return axis; }; axis.tickPadding = function(x) { if (!arguments.length) return tickPadding; tickPadding = +x; return axis; }; axis.tickSubdivide = function(x) { if (!arguments.length) return tickSubdivide; tickSubdivide = +x; return axis; }; axis.offset = function(x) { if (!arguments.length) return tickValues; offset = x; return axis; }; axis.majorTickProperties = function(x) { if (!arguments.length) return majorTickStyle; majorTickStyle = x; return axis; }; axis.minorTickProperties = function(x) { if (!arguments.length) return minorTickStyle; minorTickStyle = x; return axis; }; axis.tickLabelProperties = function(x) { if (!arguments.length) return tickLabelStyle; tickLabelStyle = x; return axis; }; axis.domainProperties = function(x) { if (!arguments.length) return domainStyle; domainStyle = x; return axis; }; return axis; }; var vg_axisDefaultOrient = "bottom", 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()); } 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? 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: false, 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_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.Model = (function() { function model() { this._defs = null; this._data = {}; this._scene = null; } var prototype = model.prototype; prototype.defs = function(defs) { if (!arguments.length) return this._defs; this._defs = defs; return this; }; prototype.data = function(data) { if (!arguments.length) return this._data; var tx = this._defs.data.flow || {}, keys = this._defs.data.defs.map(vg.accessor("name")), i, j, len, k, src; for (i=0, len=keys.length; i' + svg + '' }; } function extractCanvas(view) { return {canvas: view.canvas()}; } function render(opt, callback) { function draw(chart) { try { var view = chart({ data: opt.data, renderer: opt.renderer }).update(); var extract = opt.renderer==="svg" ? extractSVG : extractCanvas; callback(null, extract(view)); } catch (err) { callback(err, null); } } vg.parse.spec(opt.spec, draw, vg.HeadlessView.Factory); } function convert(spec, type, callback) { type = type || "canvas"; var opt = { renderer: type, spec: spec, data: null }; render(opt, callback); } return { convert: convert, render: render }; })();vg.HeadlessView = (function() { var view = function(width, height, pad, type) { this._canvas = null; this._type = type; this._build = false; this._model = new vg.Model(); this._width = width || 500; this._height = height || 500; this._padding = pad || {top:0, left:0, bottom:0, right:0}; this._renderer = new vg[type].Renderer(); this.initialize(); }; var prototype = view.prototype; 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) { this._padding = pad; this.initialize(); } 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] = data[k].map(vg.data.ingest), d); }, {}); this._model.data(ingest); this._build = false; return this; }; prototype.canvas = function() { return this._canvas; }; 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.initialize(null, w, h, pad); this._renderer.context(ctx); }; prototype.initSVG = function(w, h, pad) { var tw = w + pad.left + pad.right, th = h + pad.top + pad.bottom; // the "dom" element var el = "body"; // configure renderer this._renderer.initialize(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; }; return view; })(); // headless view constructor factory // takes definitions from parsed specification as input // returns a view constructor vg.HeadlessView.Factory = function(defs) { return function(opt) { var w = defs.width, h = defs.height, p = defs.padding, r = opt.renderer || "canvas", v = new vg.HeadlessView(w, h, p, r).defs(defs); if (defs.data.load) v.data(defs.data.load); if (opt.data) v.data(opt.data); return v; }; };return vg; })(d3); // assumes availability of D3 in global namespace