From 562a891186a3716324e12099930ab6415b8cf07c Mon Sep 17 00:00:00 2001 From: Jeffrey Heer Date: Mon, 17 Aug 2015 22:11:17 -0700 Subject: [PATCH] Initial commit. --- .gitignore | 11 + LICENSE | 27 + README.md | 9 + bower.json | 38 + package.json | 59 + src/embed.js | 102 + src/param.js | 154 + src/post.js | 25 + test/data/airports.csv | 3377 ++ test/data/flights-airport.csv | 5367 ++++ test/data/jobs.json | 53552 ++++++++++++++++++++++++++++++++ test/data/miserables.json | 1 + test/data/us-10m.json | 1 + test/force_drag.json | 123 + test/index.html | 208 + test/style.css | 73 + 16 files changed, 63127 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 bower.json create mode 100644 package.json create mode 100644 src/embed.js create mode 100644 src/param.js create mode 100644 src/post.js create mode 100644 test/data/airports.csv create mode 100644 test/data/flights-airport.csv create mode 100644 test/data/jobs.json create mode 100644 test/data/miserables.json create mode 100644 test/data/us-10m.json create mode 100644 test/force_drag.json create mode 100644 test/index.html create mode 100644 test/style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..5fc8de4b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +.idea +_site +bower_components +coverage +node_modules +output +temp +vega-embed.js +vega-embed.js.map +vega-embed.min.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..0ca363042 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2015, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. 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. + +3. Neither the name of the copyright holder 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. diff --git a/README.md b/README.md index 212801f11..8e7c1558d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # vega-embed + Publish Vega visualizations as embedded web components. + +## Build Process + +To build `vega-embed.js` and view the test examples, you must have [npm](https://www.npmjs.com/) installed. + +1. Run `npm install` in the vega-embed folder to install dependencies. +2. Run `npm run build`. This will invoke [browserify](http://browserify.org/) to bundle the source files into vega-embed.js, and then [uglify-js](http://lisperator.net/uglifyjs/) to create the minified vega-embed.min.js. +3. Run a local webserver (e.g., `python -m SimpleHTTPServer 8000`) in the vega-embed folder and then point your web browser at the test directory (e.g., `http://localhost:8000/test/`). diff --git a/bower.json b/bower.json new file mode 100644 index 000000000..eb3883dd7 --- /dev/null +++ b/bower.json @@ -0,0 +1,38 @@ +{ + "name": "vega-embed", + "main": "vega-embed.js", + "homepage": "http://vega.github.io/vega/", + "description": "Publish Vega visualizations as embedded web components.", + "keywords": [ + "vega", + "data", + "visualization", + "component", + "embed" + ], + "authors": [ + "Jeffrey Heer (http://idl.cs.washington.edu)" + ], + "license": "BSD-3-Clause", + "dependencies": { + "d3": "^3.5.6", + "vega": "^2.1.1" + }, + "ignore": [ + ".DS_Store", + ".git", + ".gitignore", + ".npmignore", + ".jshintrc", + ".travis.yml", + "bin", + "coverage", + "examples", + "index.js", + "node_modules", + "output", + "scripts", + "src", + "test" + ] +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..bcf0daa67 --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "vega-embed", + "version": "0.0.1", + "description": "Publish Vega visualizations as embedded web components.", + "keywords": [ + "vega", + "data", + "visualization", + "component", + "embed" + ], + "repository": { + "type": "git", + "url": "http://github.com/vega/vega-embed.git" + }, + "author": { + "name": "Jeffrey Heer", + "url": "http://idl.cs.washington.edu" + }, + "license": "BSD-3-Clause", + "devDependencies": { + "d3": "^3.5.6", + "topojson": "^1.6.19", + "vega": "^2.1.1", + "browserify": "^10.2.6", + "browserify-shim": "^3.8.9", + "browserify-versionify": "^1.0.4", + "chai": "^3.0.0", + "istanbul": "latest", + "jshint": "^2.8.0", + "mocha": "^2.2.5", + "uglify-js": "^2.4.24" + }, + "scripts": { + "lint": "jshint src/", + "test": "mocha --timeout 30000 --recursive test/", + "cover": "istanbul cover _mocha -- --timeout 30000 --recursive test/", + "build": "browserify src/embed.js -d -s vg.embed > vega-embed.js", + "postbuild": "uglifyjs vega-embed.js -cm > vega-embed.min.js" + }, + "browser": { + "buffer": false, + "canvas": false, + "fs": false, + "http": false, + "request": false, + "sync-request": false, + "url": false + }, + "browserify": { + "transform": [ + "browserify-shim" + ] + }, + "browserify-shim": { + "d3": "global:d3", + "vega": "global:vg" + } +} diff --git a/src/embed.js b/src/embed.js new file mode 100644 index 000000000..a29100ccf --- /dev/null +++ b/src/embed.js @@ -0,0 +1,102 @@ +var d3 = require('d3'), + vg = require('vega'), + parameter = require('./param'), + post = require('./post'); + +var config = { + // URL for loading specs into editor + editor_url: 'http://vega.github.io/vega-editor/', + + // HTML to inject within view source head element + source_header: '', + + // HTML to inject before view source closing body tag + source_footer: '' +}; + +// Embed a Vega visualization component in a web page. +// el: DOM element in which to place component (DOM node or CSS selector) +// opt: Embedding specification (parsed JSON or string) +// callback: invoked with the generated Vega View instance +function embed(el, opt, callback) { + var source, spec, params = []; + + if (opt.source) { + source = opt.source; + spec = JSON.parse(source); + } else if (opt.spec) { + spec = opt.spec; + source = JSON.stringify(spec, null, 2); + } else if (opt.url) { + vg.util.load({url: opt.url}, function(err, data) { + if (err) { + console.error(err); + } else if (!data) { + console.error('No data found at ' + opt.url); + } else { + // load code, extends options, and restart + embed(el, vg.util.extend({source: data}, opt), callback); + } + }); + return; + } + + // ensure container div has class 'vega-embed' + var div = d3.select(el) + .attr('class', 'vega-embed'); + + // handle parameters + if (opt.params) { + var pdiv = div.append('div') + .attr('class', 'vega-params'); + opt.params.forEach(function(p) { + params = params.concat(parameter(pdiv, p, spec)); + }); + } + + vg.parse.spec(spec, function(chart) { + var view = chart({el: el}); + + // add child div to house action links + var ctrl = div.append('div') + .attr('class', 'vega-actions'); + + // add 'View Source' action + ctrl.append('a') + .text('View Source') + .attr('href', '#') + .on('click', function() { + viewSource(source); + d3.event.preventDefault(); + }); + + // add 'Open in Vega Editor' action + ctrl.append('a') + .text('Open in Vega Editor') + .attr('href', '#') + .on('click', function() { + post(window, embed.config.editor_url, {spec: source}); + d3.event.preventDefault(); + }); + + // bind all parameter elements + params.forEach(function(el) { el.__vega__ = view; }); + + // initialize and return visualization + view.update(); + if (callback) callback(view); + }); +} + +function viewSource(source) { + var header = '' + config.source_header + '' + '
';
+  var footer = '
' + config.source_footer + ''; + var win = window.open(''); + win.document.write(header + source + footer); + win.document.title = 'Vega JSON Source'; +} + +// make config externally visible +embed.config = config; + +module.exports = embed; diff --git a/src/param.js b/src/param.js new file mode 100644 index 000000000..2920e260c --- /dev/null +++ b/src/param.js @@ -0,0 +1,154 @@ +var d3 = require('d3'); + +module.exports = function(el, param, spec) { + return (rewrite(param, spec), handle(el, param)); +}; + +// spec re-write + +function rewrite(param, spec) { + // add signal to top-level if not defined + var sg = spec.signals || (spec.signals = []); + for (var i=0; i window:mousemove", + "expr": "{x: eventX(), y: eventY(), id: grab, update: true}" + } + ] + }, + { + "name": "dblclick", "init": null, "verbose": true, + "streams": [ + { "type": "symbol:dblclick", "expr": "datum._id" } + ] + } + ], + + "predicates": [ + { + "name": "isFixed", + "item": {"arg": "id"}, + "type": "in", + "data": "fixed", + "field": "id" + } + ], + + "data": [ + { + "name": "fixed", + "modify": [ + {"type": "toggle", "signal": "dblclick", "field": "id"} + ] + }, + { + "name": "edges", + "url": "data/miserables.json", + "format": {"type": "json", "property": "links"} + }, + { + "name": "nodes", + "url": "data/miserables.json", + "format": {"type": "json", "property": "nodes"}, + "transform": [ + { + "type": "force", + "size": [800, 500], + "links": "edges", + "linkDistance": 30, + "linkStrength": 0.5, + "charge": -80, + "interactive": true, + "fixed": "fixed", + "active": {"signal": "active"} + } + ] + } + ], + + "marks": [ + { + "type": "path", + "from": { + "data": "edges", + "transform": [ + { "type": "lookup", "on": "nodes", + "keys": ["source", "target"], + "as": ["_source", "_target"] }, + { "type": "linkpath", "shape": "line" } + ] + }, + "properties": { + "update": { + "path": {"field": "layout_path"}, + "stroke": {"value": "#ccc"}, + "strokeWidth": {"value": 0.5} + } + } + }, + { + "type": "symbol", + "from": {"data": "nodes"}, + "properties": { + "enter": { + "fillOpacity": {"value": 0.3}, + "fill": {"value": "steelblue"} + }, + "update": { + "x": {"field": "layout_x"}, + "y": {"field": "layout_y"}, + "stroke": {"rule": [ + { "predicate": {"name": "isFixed", "id": {"field": "_id"}}, + "value": "firebrick" }, + { "value": "steelblue" } + ]} + } + } + } + ] +} \ No newline at end of file diff --git a/test/index.html b/test/index.html new file mode 100644 index 000000000..e567ea411 --- /dev/null +++ b/test/index.html @@ -0,0 +1,208 @@ + + + + Vega Embed Test + + + + + + + + +
+
+
+ + + \ No newline at end of file diff --git a/test/style.css b/test/style.css new file mode 100644 index 000000000..0b4c8ea6e --- /dev/null +++ b/test/style.css @@ -0,0 +1,73 @@ +body { + max-width: 900px; + margin-left: 20px; + padding: 15px; + position: relative; + font-family: Helvetica Neue; + font-size: 16px; + line-height: 1.6; +} + +a, a:visited { + color: #4078c0; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.vega-embed .vega-actions { + font-weight: 300; + text-align: right; + font-size: 12px; +} + +.vega-embed .vega-actions a { + margin-left: 1em; + visibility: hidden; +} + +.vega-embed:hover .vega-actions a { + visibility: visible; +} + +.vega-embed .vega-params { + position: absolute; + width: 220px; + right: 0; + z-index: 1; + font-size: small; + background: rgba(255, 255, 255, 0.75); + border: 1px dashed #dedede; + border-radius: 5px; + box-shadow: 2px 2px 10px #dedede; + padding: 4px 10px; +} + +.vega-embed .vega-param { + line-height: 15px; +} + +.vega-embed .vega-param * { + vertical-align: middle; +} + +.vega-embed .vega-param-name { + display: inline-block; + width: 70px; + margin-right: 10px; + font-size: 11px; + font-weight: 500; + overflow: hidden; +} + +.vega-embed .vega-params input[type=range] { + max-width: 100px; + margin-right: 5px; +} + +.vega-embed .vega-params label { + font-size: 10px; + margin-right: 5px; +} \ No newline at end of file