├── test ├── compiler │ ├── compiler.spec.js │ ├── legend.spec.js │ ├── sort.spec.js │ ├── time.spec.js │ ├── stack.spec.js │ ├── axis.spec.js │ ├── data.spec.js │ ├── marks.spec.js │ └── scale.spec.js ├── .jshintrc ├── Encoding.spec.js ├── field.spec.js ├── schema.spec.js └── fixtures.js ├── .gitignore ├── src ├── schema │ ├── schemagen.js │ ├── instancegen.js │ └── schemautil.js ├── consts.js ├── globals.js ├── vl.js ├── compiler │ ├── group.js │ ├── subfacet.js │ ├── sort.js │ ├── style.js │ ├── stack.js │ ├── legend.js │ ├── facet.js │ ├── compiler.js │ ├── time.js │ ├── data.js │ ├── layout.js │ ├── axis.js │ ├── scale.js │ └── marks.js ├── data.js ├── logger.js ├── enc.js ├── util.js └── field.js ├── gulpfile.js ├── gulp ├── serve.js ├── lint.js ├── watch.js ├── tests.js ├── schema.js ├── bumpver.js └── build.js ├── .jshintrc ├── .travis.yml ├── editor ├── bower.json ├── style.css ├── cookies.js ├── editor.js └── jquery.autosize.js ├── gallery ├── bower.json ├── style.css └── gallery.js ├── bower.json ├── bin ├── testcases.js ├── test-output.js ├── test.js ├── shorthand2vg │ ├── birdstrikes.bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O.json │ ├── birdstrikes.bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O.color-When__Phase_of_flight-O.json │ ├── birdstrikes.bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O.row-When__Phase_of_flight-O.col-Wildlife__Size-O.json │ └── birdstrikes.bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O.row-When__Phase_of_flight-O.col-When__Time_of_day-O.color-Wildlife__Size-O.json └── gen.js ├── LICENSE ├── data ├── crimea.json ├── burtin.json ├── driving.json └── barley.json ├── scripts └── deploy.sh ├── gallery.html ├── package.json ├── index.html ├── README.md └── lib └── schema.json /test/compiler/compiler.spec.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/log 2 | test/log/difflist.json 3 | node_modules 4 | bower_components 5 | vega-lite*.js 6 | vega-lite*.map 7 | spec.json 8 | npm-debug.log 9 | instance.json 10 | coverage -------------------------------------------------------------------------------- /src/schema/schemagen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var schema = require('./schema').schema, 4 | json3 = require('../../lib/json3-compactstringify.js'); 5 | 6 | process.stdout.write(json3.stringify(schema, null, 1, 80) + '\n'); 7 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | gulp.paths = { 6 | src: 'src', 7 | test: 'test' 8 | }; 9 | 10 | require('require-dir')('./gulp'); 11 | 12 | gulp.task('default', ['bundle', 'schema', 'watch-schema', 'watch-test']); -------------------------------------------------------------------------------- /src/schema/instancegen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var schema = require('./schema').schema, 4 | schemaUtil = require('./schemautil'), 5 | json3 = require('../../lib/json3-compactstringify.js'); 6 | 7 | process.stdout.write(json3.stringify(schemaUtil.instantiate(schema), null, 1, 80) + '\n'); 8 | -------------------------------------------------------------------------------- /src/consts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./globals'); 4 | 5 | var consts = module.exports = {}; 6 | 7 | consts.encodingTypes = [X, Y, ROW, COL, SIZE, SHAPE, COLOR, TEXT, DETAIL]; 8 | 9 | consts.shorthand = { 10 | delim: '|', 11 | assign: '=', 12 | type: ',', 13 | func: '_' 14 | }; 15 | -------------------------------------------------------------------------------- /gulp/serve.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var browserSync = require('browser-sync'); 5 | 6 | gulp.task('serve', ['bundle', 'watch-schema', 'watch-test'], function() { 7 | browserSync({ 8 | server: { 9 | baseDir: './' 10 | }, 11 | browser: 'google chrome' 12 | }); 13 | }); -------------------------------------------------------------------------------- /gulp/lint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var paths = gulp.paths; 5 | var $ = require('gulp-load-plugins')(); 6 | 7 | gulp.task('jshint', function() { 8 | return gulp.src([ 9 | paths.src + '/**/*.js', 10 | paths.test + '/**/*.js', 11 | ]) 12 | .pipe($.jshint()) 13 | .pipe($.jshint.reporter('jshint-stylish')); 14 | }); 15 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": ["_", 3 | "AGGREGATE", "RAW", "STACKED", "INDEX", 4 | "X", "Y", "ROW", "COL", "SIZE", "SHAPE", "COLOR", "TEXT", "DETAIL", 5 | "O", "Q", "T", "N"], 6 | "mocha": true, 7 | "undef": true, 8 | "unused": true, 9 | "eqnull": true, 10 | "freeze": true, 11 | "noarg": true, 12 | "node": true, 13 | "expr": true 14 | } 15 | -------------------------------------------------------------------------------- /gulp/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | // watches for spec schema changes 6 | gulp.task('watch-schema', function() { 7 | gulp.watch(['src/schema/schema.js'], ['schema']); 8 | }); 9 | 10 | // watches directories and runs tests if things change 11 | gulp.task('watch-test', function() { 12 | gulp.watch(['src/**', 'test/**'], ['jshint', 'test']); 13 | }); 14 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true, 3 | "unused": true, 4 | "eqnull": true, 5 | "freeze": true, 6 | "noarg": true, 7 | "node": true, 8 | "browser": true, 9 | "quotmark": "single", 10 | "predef": [ 11 | "AGGREGATE", "RAW", "STACKED", "INDEX", 12 | "X", "Y", "ROW", "COL", "SIZE", "SHAPE", "COLOR", "TEXT", "DETAIL", 13 | "O", "Q", "T", "N"], 14 | "globalstrict": true 15 | } 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | before_install: 6 | - npm install -g gulp 7 | install: npm install 8 | after_success: ./node_modules/.bin/coveralls --verbose < coverage/lcov.info 9 | notifications: 10 | email: 11 | on_success: never 12 | on_failure: change 13 | slack: 14 | rooms: 15 | - 'uwdub:Ry6mwlUX1aZevqiqmYLiA3N1' 16 | on_success: change 17 | on_failure: always 18 | -------------------------------------------------------------------------------- /editor/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vl-editor", 3 | "main": "editor.js", 4 | "version": "0.0.0", 5 | "authors": [ 6 | "Dominik Moritz " 7 | ], 8 | "description": "Vega-lite editor", 9 | "license": "BSD-3-Clause", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "vega": "^1.5.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/globals.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // declare global constant 4 | var g = global || window; 5 | 6 | g.AGGREGATE = 'aggregate'; 7 | g.RAW = 'raw'; 8 | g.STACKED = 'stacked'; 9 | g.INDEX = 'index'; 10 | 11 | g.X = 'x'; 12 | g.Y = 'y'; 13 | g.ROW = 'row'; 14 | g.COL = 'col'; 15 | g.SIZE = 'size'; 16 | g.SHAPE = 'shape'; 17 | g.COLOR = 'color'; 18 | g.TEXT = 'text'; 19 | g.DETAIL = 'detail'; 20 | 21 | g.N = 'N'; 22 | g.O = 'O'; 23 | g.Q = 'Q'; 24 | g.T = 'T'; 25 | -------------------------------------------------------------------------------- /gallery/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vl-gallery", 3 | "main": "gallery.js", 4 | "version": "0.0.0", 5 | "authors": [ 6 | "Dominik Moritz " 7 | ], 8 | "description": "Vega-lite gallery", 9 | "license": "BSD-3-Clause", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "vega": "^1.0.0", 19 | "angular": "^1.4.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/vl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./globals'); 4 | 5 | var util = require('./util'), 6 | consts = require('./consts'); 7 | 8 | var vl = {}; 9 | 10 | util.extend(vl, consts, util); 11 | 12 | vl.Encoding = require('./Encoding'); 13 | vl.compiler = require('./compiler/compiler'); 14 | vl.compile = vl.compiler.compile; 15 | vl.data = require('./data'); 16 | vl.enc = require('./enc'); 17 | vl.field = require('./field'); 18 | vl.schema = require('./schema/schema'); 19 | vl.toShorthand = vl.Encoding.shorthand; 20 | vl.format = require('d3-format').format; 21 | 22 | module.exports = vl; -------------------------------------------------------------------------------- /gulp/tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var gutil = require('gulp-util'); 5 | var mocha = require('gulp-spawn-mocha'); 6 | 7 | // runs the tests 8 | gulp.task('coverage', function() { 9 | return gulp.src(['test/**/*.spec.js'], { read: false }) 10 | .pipe(mocha({ 11 | istanbul: true 12 | })) 13 | .on('error', gutil.log); 14 | }); 15 | 16 | // quick test 17 | gulp.task('test', function() { 18 | return gulp.src(['test/**/*.spec.js'], { read: false }) 19 | .pipe(mocha({ 20 | istanbul: false 21 | })) 22 | .on('error', gutil.log); 23 | }); -------------------------------------------------------------------------------- /gulp/schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var $ = require('gulp-load-plugins')(); 5 | 6 | // generates spec.json 7 | gulp.task('schema', function () { 8 | gulp.src('src/schema/schemagen.js') 9 | .pipe($.run('node', {silent: true, cwd: 'src/schema'})) 10 | .pipe($.rename('spec.json')) 11 | .pipe(gulp.dest('.')); 12 | }); 13 | 14 | gulp.task('instance', ['schema'], function () { 15 | gulp.src('src/schema/instancegen.js') 16 | .pipe($.run('node', {silent: true, cwd: 'src/schema'})) 17 | .pipe($.rename('instance.json')) 18 | .pipe(gulp.dest('.')); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /src/compiler/group.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | def: groupdef 5 | }; 6 | 7 | function groupdef(name, opt) { 8 | opt = opt || {}; 9 | return { 10 | _name: name || undefined, 11 | type: 'group', 12 | from: opt.from, 13 | properties: { 14 | enter: { 15 | x: opt.x || undefined, 16 | y: opt.y || undefined, 17 | width: opt.width || {group: 'width'}, 18 | height: opt.height || {group: 'height'} 19 | } 20 | }, 21 | scales: opt.scales || undefined, 22 | axes: opt.axes || undefined, 23 | marks: opt.marks || [] 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /gulp/bumpver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var $ = require('gulp-load-plugins')(); 5 | 6 | 7 | function inc(importance) { 8 | // get all the files to bump version in 9 | return gulp.src(['./package.json', './bower.json']) 10 | // bump the version number in those files 11 | .pipe($.bump({type: importance})) 12 | // save it back to filesystem 13 | .pipe(gulp.dest('./')); 14 | } 15 | 16 | gulp.task('patch', function() { return inc('patch'); }); 17 | gulp.task('feature', function() { return inc('minor'); }); 18 | gulp.task('release', function() { return inc('major'); }); -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./globals'); 4 | 5 | var stats = require('datalib/src/stats'); 6 | 7 | var vldata = module.exports = {}; 8 | 9 | /** Mapping from datalib's inferred type to Vega-lite's type */ 10 | vldata.types = { 11 | 'boolean': N, 12 | 'number': Q, 13 | 'integer': Q, 14 | 'date': T, 15 | 'string': N 16 | }; 17 | 18 | vldata.stats = function(data) { 19 | var summary = stats.summary(data); 20 | 21 | return summary.reduce(function(s, profile) { 22 | s[profile.field] = profile; 23 | return s; 24 | }, { 25 | '*': { 26 | max: data.length, 27 | min: 0 28 | } 29 | }); 30 | }; -------------------------------------------------------------------------------- /src/compiler/subfacet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../globals'); 4 | 5 | var groupdef = require('./group').def; 6 | 7 | module.exports = subfaceting; 8 | 9 | function subfaceting(group, mdef, details, stack, encoding) { 10 | var m = group.marks, 11 | g = groupdef('subfacet', {marks: m}); 12 | 13 | group.marks = [g]; 14 | g.from = mdef.from; 15 | delete mdef.from; 16 | 17 | //TODO test LOD -- we should support stack / line without color (LOD) field 18 | var trans = (g.from.transform || (g.from.transform = [])); 19 | trans.unshift({type: 'facet', keys: details}); 20 | 21 | if (stack && encoding.has(COLOR)) { 22 | trans.unshift({type: 'sort', by: encoding.fieldRef(COLOR)}); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // TODO(kanitw): chat with Vega team and possibly move this to vega-logging 4 | module.exports = function(prefix) { 5 | // Borrowed some ideas from http://stackoverflow.com/a/15653260/866989 6 | // and https://github.com/patik/console.log-wrapper/blob/master/consolelog.js 7 | var METHODS = ['error', 'info', 'debug', 'warn', 'log']; 8 | 9 | return METHODS.reduce(function(logger, fn) { 10 | var cfn = console[fn] ? fn : 'log'; 11 | if (console[cfn].bind === 'undefined') { // IE < 10 12 | logger[fn] = Function.prototype.bind.call(console[cfn], console, prefix); 13 | } 14 | else { 15 | logger[fn] = console[cfn].bind(console, prefix); 16 | } 17 | return logger; 18 | }, {}); 19 | }; -------------------------------------------------------------------------------- /test/compiler/legend.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | var legend = require('../../src/compiler/legend'), 6 | Encoding = require('../../src/Encoding'); 7 | 8 | describe('Legend', function() { 9 | describe('title()', function () { 10 | it('should add explicitly specified title', function () { 11 | var title = legend.title('color', Encoding.fromSpec({ 12 | encoding: { 13 | color: {name: 'a', legend: {title: 'Custom'}} 14 | } 15 | })); 16 | expect(title).to.eql('Custom'); 17 | }); 18 | 19 | it('should add return fieldTitle by default', function () { 20 | var encoding = Encoding.fromSpec({ 21 | encoding: { 22 | color: {name: 'a', legend: {}} 23 | } 24 | }); 25 | 26 | var title = legend.title('color', encoding); 27 | expect(title).to.eql('a'); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vega-lite", 3 | "main": "vega-lite.js", 4 | "version": "0.7.13", 5 | "homepage": "https://github.com/uwdata/vega-lite", 6 | "authors": [ 7 | "Kanit Wongsuphasawat (http://kanitw.yellowpigz.com)", 8 | "Dominik Moritz (http://domoritz.de)", 9 | "Jeffrey Heer (http://jheer.org)" 10 | ], 11 | "description": "Vega-lite provides a higher-level grammar for visual analysis, comparable to ggplot or Tableau, that generates complete Vega specifications.", 12 | "moduleType": [ 13 | "amd", 14 | "globals", 15 | "node" 16 | ], 17 | "keywords": [ 18 | "visualization", 19 | "grammar" 20 | ], 21 | "license": "BSD-3-Clause", 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "test", 27 | "tests" 28 | ], 29 | "dependencies": { 30 | "datalib": "^1.3.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gallery/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px; 3 | padding: 15px; 4 | font-family: Avenir, Helvetica Neue, Helvetica, Arial; 5 | } 6 | 7 | strong { 8 | font-weight: 400; 9 | } 10 | 11 | a { 12 | color: #8A5ED3; 13 | text-decoration: none; 14 | font-weight: 500; 15 | } 16 | 17 | a:hover { 18 | color: #d5a928; 19 | } 20 | 21 | .logo { 22 | font-weight: 600; 23 | font-size: 60px; 24 | line-height: 72px; 25 | margin-bottom: 20px; 26 | margin-top: 0; 27 | } 28 | 29 | .visualizations { 30 | display: flex; 31 | flex-direction: row; 32 | flex-wrap: wrap; 33 | align-content: flex-start; 34 | align-items: stretch; 35 | } 36 | 37 | .viz { 38 | border: 1px solid #CECECE; 39 | padding: 10px; 40 | margin: 10px; 41 | max-height: 500px; 42 | overflow: scroll; 43 | } 44 | 45 | .viz > div { 46 | display: flex; 47 | } 48 | 49 | .viz h3 { 50 | margin: 0; 51 | } 52 | 53 | .viz p { 54 | color: gray; 55 | max-width: 400px; 56 | } -------------------------------------------------------------------------------- /bin/testcases.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var testcases = { 4 | 'data/birdstrikes.json': [{ 5 | n: 'Bar', 6 | e: 'bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O' 7 | },{ 8 | n: 'Bar Chart of bin(Q) x avg(Q)', 9 | e: 'bar.x-bin_Cost__Total_$-Q.y-avg_Speed_IAS_in_knots-Q' 10 | },{ 11 | n: 'Bar Chart of bin(Q) x Q', 12 | e: 'point.x-Cost__Total_$-Q.y-bin_Speed_IAS_in_knots-Q' 13 | },{ 14 | n: 'Stack Bar', 15 | e: 'bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O.color-When__Phase_of_flight-O' 16 | },{ 17 | n: 'Small Multiples of Stack Bar', 18 | e: 'bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O.row-When__Phase_of_flight-O.col-When__Time_of_day-O.color-Wildlife__Size-O' 19 | },{ 20 | n: 'Small Multiples of Bar Chart', 21 | e: 'bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O.row-When__Phase_of_flight-O.col-Wildlife__Size-O' 22 | }], 23 | 'data/movies.json': [{ 24 | n: 'Small Multiples without y-axes #82', 25 | e: 'point.x-US_Gross-Q.row-Major_Genre-O', 26 | i: 82 27 | },{ 28 | n: 'table', 29 | e: 'text.row-Major_Genre-O.col-Creative_Type-O.text-avg_US_Gross-Q' 30 | }] 31 | }; 32 | 33 | 34 | module.exports = testcases; 35 | -------------------------------------------------------------------------------- /gulp/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var $ = require('gulp-load-plugins')(); 5 | 6 | var buffer = require('vinyl-buffer'); 7 | var browserify = require('browserify'); 8 | var browserSync = require('browser-sync'); 9 | var source = require('vinyl-source-stream'); 10 | var watchify = require('watchify'); 11 | 12 | var bundleDef = { 13 | entries: ['./src/vl'], 14 | standalone: 'vl', 15 | debug: true 16 | }; 17 | 18 | var browserBundler = browserify(bundleDef); 19 | var watchBundler = watchify(browserify(bundleDef)); 20 | 21 | // builds Vega-lite with watcher 22 | function bundle() { 23 | return build(watchBundler.bundle()); 24 | } 25 | 26 | function build(bundle) { 27 | return bundle 28 | .pipe(source('vega-lite.js')) 29 | .pipe(buffer()) 30 | .pipe(gulp.dest('.')) 31 | .pipe($.sourcemaps.init({loadMaps: true})) 32 | // This will minify and rename to vega-lite.min.js 33 | .pipe($.uglify()) 34 | .pipe($.rename({ extname: '.min.js' })) 35 | .pipe($.sourcemaps.write('./')) 36 | .pipe(gulp.dest('.')) 37 | .pipe(browserSync.reload({stream:true})); 38 | } 39 | 40 | // builds Vega-lite and schema 41 | gulp.task('build', ['schema'], function() { 42 | build(browserBundler.bundle()); 43 | }); 44 | 45 | watchBundler.on('update', bundle); 46 | gulp.task('bundle', bundle); -------------------------------------------------------------------------------- /test/Encoding.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | var Encoding = require('../src/Encoding'); 6 | 7 | describe('Encoding.fromShorthand()', function () { 8 | it('should parse shorthand correctly', function () { 9 | var shorthand = 'mark=point|x=Effect__Amount_of_damage,O|y=avg_Cost__Total_$,Q'; 10 | var encoding = Encoding.fromShorthand(shorthand); 11 | expect(encoding.has('y')).ok; 12 | expect(encoding.has('x')).ok; 13 | 14 | }); 15 | }); 16 | 17 | describe('encoding.filter()', function () { 18 | var spec = { 19 | marktype: 'point', 20 | encoding: { 21 | y: {name: 'Q', type:'Q'}, 22 | x: {name: 'T', type:'T'}, 23 | color: {name: 'O', type:'O'} 24 | } 25 | }; 26 | it('should add filterNull for Q and T by default', function () { 27 | var encoding = Encoding.fromSpec(spec), 28 | filter = encoding.filter(); 29 | expect(filter.length).to.equal(2); 30 | expect(filter.indexOf({name: 'O', type:'O'})).to.equal(-1); 31 | }); 32 | 33 | it('should add filterNull for O when specified', function () { 34 | var encoding = Encoding.fromSpec(spec, { 35 | config: { 36 | filterNull: {O: true} 37 | } 38 | }); 39 | var filter = encoding.filter(); 40 | expect(filter.length).to.equal(3); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /bin/test-output.js: -------------------------------------------------------------------------------- 1 | // script for test/index.html -- for rendering failed test case 2 | 3 | function getParams() { 4 | var params = location.search.slice(1); 5 | 6 | // remove trailing slash that chrome adds automatically 7 | if (params[params.length - 1] == '/') params = params.substring(0, params.length - 1); 8 | 9 | return params.split('&') 10 | .map(function(x) { return x.split('='); }) 11 | .reduce(function(a, b) { 12 | a[b[0]] = b[1]; return a; 13 | }, {}); 14 | } 15 | 16 | function renderList(data, main) { 17 | var div = main.selectAll('div').data(data).enter() 18 | .append('div'); 19 | div.append('h2').text(function(d) { return d.filename; }); 20 | var split = div.append('div'); 21 | 22 | var code = split.append('div').attr('class', 'inline') 23 | .append('textarea').attr('class', 'code') 24 | .text(function(d) { return JSON.stringify(d.spec, null, ' ', 80); }); 25 | 26 | split.append('div').attr({ 27 | 'class': 'output inline', 28 | 'id': function(d, i) { return 'vis-'+ i; } 29 | }); 30 | 31 | div.each(function(d, i) { 32 | vg.parse.spec(d.spec, function(chart) { 33 | self.vis = chart({el: '#vis-'+ i, renderer: 'svg'}); 34 | vis.update(); 35 | }); 36 | }); 37 | } 38 | 39 | function load(filename) { 40 | d3.json('test/log/'+ filename, function(err, data) { 41 | renderList(data.bad, d3.select('#bad')); 42 | renderList(data.good, d3.select('#good')); 43 | }); 44 | } 45 | 46 | load(getParams().filename || 'difflist.json'); 47 | -------------------------------------------------------------------------------- /src/compiler/sort.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../globals'); 4 | 5 | var vlfield = require('../field'); 6 | 7 | module.exports = sort; 8 | 9 | // adds new transforms that produce sorted fields 10 | function sort(data, encoding, stats, opt) { 11 | // jshint unused:false 12 | 13 | var datasetMapping = {}; 14 | 15 | encoding.forEach(function(field, encType) { 16 | var sortBy = encoding.sort(encType, stats); 17 | if (sortBy.length > 0) { 18 | var fields = sortBy.map(function(d) { 19 | return { 20 | op: d.aggregate, 21 | field: vlfield.fieldRef(d, {nofn: true, data: !encoding._vega2}) 22 | }; 23 | }); 24 | 25 | var byClause = sortBy.map(function(d) { 26 | var reverse = (d.reverse ? '-' : ''); 27 | return reverse + vlfield.fieldRef(d, {data: !encoding._vega2}); 28 | }); 29 | 30 | var dataName = sort.getDataName(encType); 31 | 32 | var transforms = [ 33 | { 34 | type: 'aggregate', 35 | groupby: [ encoding.fieldRef(encType) ], 36 | fields: fields 37 | }, 38 | { 39 | type: 'sort', 40 | by: byClause 41 | } 42 | ]; 43 | 44 | data.push({ 45 | name: dataName, 46 | source: RAW, 47 | transform: transforms 48 | }); 49 | 50 | datasetMapping[encType] = dataName; 51 | } 52 | }); 53 | 54 | return data; 55 | } 56 | 57 | sort.getDataName = function(encType) { 58 | return 'sorted-' + encType; 59 | }; 60 | 61 | -------------------------------------------------------------------------------- /test/compiler/sort.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | var vlsort = require('../../src/compiler/sort'), 6 | Encoding = require('../../src/Encoding'); 7 | 8 | describe('Sort', function() { 9 | var encoding = Encoding.fromSpec({ 10 | encoding: { 11 | x: {name: 'foo', type: 'O', sort: [{ 12 | name: 'bar', aggregate: 'avg' 13 | }]}, 14 | y: {name: 'bar', type: 'Q'}, 15 | color: {name: 'baz', type: 'O', sort: [{ 16 | name: 'bar', aggregate: 'sum' 17 | }, { 18 | name: 'foo', aggregate: 'max', reverse: true 19 | }]} 20 | } 21 | }), 22 | data = [{name: RAW}, {name: AGGREGATE}]; 23 | 24 | vlsort(data, encoding, {}); 25 | 26 | it('should add new data and transform', function() { 27 | expect(data.length).to.equal(4); 28 | 29 | expect(data[2].transform).to.deep.equal([ 30 | { 31 | type: 'aggregate', 32 | groupby: [ 'data.foo' ], 33 | fields: [{ 34 | field: 'data.bar', 35 | op: 'avg' 36 | }] 37 | }, 38 | { type: 'sort', by: [ 'data.avg_bar' ] } 39 | ]); 40 | 41 | expect(data[3].transform).to.deep.equal([ 42 | { 43 | type: 'aggregate', 44 | groupby: [ 'data.baz' ], 45 | fields: [{ 46 | field: 'data.bar', 47 | op: 'sum' 48 | }, { 49 | field: 'data.foo', 50 | op: 'max' 51 | }] 52 | }, 53 | { type: 'sort', by: [ 'data.sum_bar', '-data.max_foo' ] } 54 | ]); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, University of Washington Interactive Data Lab. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the University of Washington Interactive Data Lab 15 | nor the names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /test/field.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../src/globals'); 4 | 5 | var expect = require('chai').expect; 6 | var vlfield = require('../src/field'); 7 | 8 | describe('vl.field.cardinality()', function () { 9 | describe('for Q', function () { 10 | it('should return cardinality', function() { 11 | var field = {name:2, type:'Q'}; 12 | var stats = {2:{distinct: 10, min:0, max:150}}; 13 | var cardinality = vlfield.cardinality(field, stats); 14 | expect(cardinality).to.equal(10); 15 | }); 16 | }); 17 | 18 | describe('for B(Q)', function(){ 19 | it('should return cardinality', function() { 20 | var field = {name:2, type:'Q', bin: {maxbins: 15}}; 21 | var stats = {2:{distinct: 10, min:0, max:150}}; 22 | var cardinality = vlfield.cardinality(field, stats); 23 | expect(cardinality).to.equal(15); 24 | }); 25 | }); 26 | }); 27 | 28 | describe('vl.field.isType', function () { 29 | it('should return correct type checking', function() { 30 | var qField = {name: 'number', type:'Q'}; 31 | expect(vlfield.isType(qField, Q)).to.eql(true); 32 | expect(vlfield.isTypes(qField, N)).to.eql(false); 33 | }); 34 | }); 35 | 36 | describe('vl.field.isTypes', function () { 37 | it('should return correct type checking', function() { 38 | var qField = {name: 'number', type:'Q'}; 39 | expect(vlfield.isType(qField, Q)).to.eql(true); 40 | expect(vlfield.isTypes(qField, [Q])).to.eql(true); 41 | expect(vlfield.isTypes(qField, [Q, O])).to.eql(true); 42 | expect(vlfield.isTypes(qField, [O, Q])).to.eql(true); 43 | expect(vlfield.isTypes(qField, [Q, N])).to.eql(true); 44 | expect(vlfield.isTypes(qField, [N])).to.eql(false); 45 | }); 46 | }); -------------------------------------------------------------------------------- /test/compiler/time.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | var time = require('../../src/compiler/time'), 6 | Encoding = require('../../src/Encoding'); 7 | 8 | describe('time', function() { 9 | var fieldName = 'a', 10 | timeUnit = 'month', 11 | encoding = Encoding.fromSpec({ 12 | encoding: { 13 | x: {name: fieldName, type: 'T', timeUnit: timeUnit} 14 | } 15 | }), 16 | scales = time.scales(encoding); 17 | 18 | 19 | it('should add custom axis scale', function() { 20 | expect(scales.filter(function(scale) { 21 | return scale.name == 'time-'+ timeUnit; 22 | }).length).to.equal(1); 23 | }); 24 | 25 | describe('maxLength', function(){ 26 | it('should return max length based on time format', function () { 27 | expect(time.maxLength(undefined /*no timeUnit*/, { 28 | config: function(){ return '%A %B %e %H:%M:%S %Y';} 29 | })) 30 | .to.eql('Wednesday September 17 04:00:00 2014'.length); 31 | }); 32 | 33 | it('should return max length of the month custom scale', function () { 34 | expect(time.maxLength('month', Encoding.fromSpec({mark: 'point'}))) 35 | .to.eql(3); 36 | }); 37 | 38 | it('should return max length of the day custom scale', function () { 39 | expect(time.maxLength('day', Encoding.fromSpec({mark: 'point'}))) 40 | .to.eql(3); 41 | }); 42 | 43 | it('should return max length of the month custom scale', function () { 44 | expect(time.maxLength('month', Encoding.fromSpec({ 45 | mark: 'point', 46 | config: { 47 | timeScaleLabelLength: 0 48 | } 49 | }))).to.eql(9); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /data/crimea.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "date": "4/1854", "wounds": 0, "other": 110, "disease": 110 }, 3 | { "date": "5/1854", "wounds": 0, "other": 95, "disease": 105 }, 4 | { "date": "6/1854", "wounds": 0, "other": 40, "disease": 95 }, 5 | { "date": "7/1854", "wounds": 0, "other": 140, "disease": 520 }, 6 | { "date": "8/1854", "wounds": 20, "other": 150, "disease": 800 }, 7 | { "date": "9/1854", "wounds": 220, "other": 230, "disease": 740 }, 8 | { "date": "10/1854", "wounds": 305, "other": 310, "disease": 600 }, 9 | { "date": "11/1854", "wounds": 480, "other": 290, "disease": 820 }, 10 | { "date": "12/1854", "wounds": 295, "other": 310, "disease": 1100 }, 11 | { "date": "1/1855", "wounds": 230, "other": 460, "disease": 1440 }, 12 | { "date": "2/1855", "wounds": 180, "other": 520, "disease": 1270 }, 13 | { "date": "3/1855", "wounds": 155, "other": 350, "disease": 935 }, 14 | { "date": "4/1855", "wounds": 195, "other": 195, "disease": 560 }, 15 | { "date": "5/1855", "wounds": 180, "other": 155, "disease": 550 }, 16 | { "date": "6/1855", "wounds": 330, "other": 130, "disease": 650 }, 17 | { "date": "7/1855", "wounds": 260, "other": 130, "disease": 430 }, 18 | { "date": "8/1855", "wounds": 290, "other": 110, "disease": 490 }, 19 | { "date": "9/1855", "wounds": 355, "other": 100, "disease": 290 }, 20 | { "date": "10/1855", "wounds": 135, "other": 95, "disease": 245 }, 21 | { "date": "11/1855", "wounds": 100, "other": 140, "disease": 325 }, 22 | { "date": "12/1855", "wounds": 40, "other": 120, "disease": 215 }, 23 | { "date": "1/1856", "wounds": 0, "other": 160, "disease": 160 }, 24 | { "date": "2/1856", "wounds": 0, "other": 100, "disease": 100 }, 25 | { "date": "3/1856", "wounds": 0, "other": 125, "disease": 90 } 26 | ] -------------------------------------------------------------------------------- /editor/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 3 | font-size: 11pt; 4 | } 5 | 6 | html, body { 7 | height: 100%; 8 | } 9 | 10 | body { 11 | margin: 0px; 12 | } 13 | 14 | #content { 15 | height: 100%; 16 | vertical-align: bottom; 17 | } 18 | 19 | .pre { 20 | font-family: Monaco, Courier New; 21 | font-size: 10px; 22 | white-space: pre; 23 | overflow: hidden; 24 | resize: none; 25 | width: 478px; 26 | /* margin: 8px; */ 27 | } 28 | 29 | .mod { 30 | position: relative; 31 | vertical-align: top; 32 | /* height: 100%; */ 33 | } 34 | 35 | .mod_header { 36 | padding: 4px; 37 | background-color: #444; 38 | color: #fff; 39 | } 40 | 41 | .mod_title { 42 | line-height: 14pt; 43 | } 44 | 45 | .mod_title a { 46 | color: #fff; 47 | cursor: pointer; 48 | margin-right: 5px; 49 | } 50 | .mod_title a.active { 51 | color: #fff; 52 | } 53 | .mod_title a:hover { 54 | color: #fff; 55 | } 56 | 57 | .mod_ctrls { 58 | position: absolute; 59 | right: 8px; 60 | top: 2px; 61 | } 62 | 63 | .panel_header { 64 | margin: 12px 0 4px 0; 65 | padding: 4px; 66 | position: relative; 67 | } 68 | 69 | .panel_header h2 { 70 | margin: 0; 71 | } 72 | 73 | #mod_vis { 74 | margin-left: 515px; 75 | } 76 | 77 | #mod_spec { 78 | position: absolute; 79 | left: 0; 80 | top: 0; 81 | width: 500px; 82 | background-color: #eee; 83 | } 84 | 85 | #spec { 86 | width: 498px; 87 | min-height: 200px; 88 | } 89 | 90 | #vis div { 91 | border: 1px dashed #ccc; 92 | } 93 | 94 | .mod_content { 95 | padding: 0 8px 8px 8px; 96 | } 97 | 98 | input { 99 | font-size: 10px; 100 | } 101 | 102 | #shorthand { 103 | width: 478px; 104 | } -------------------------------------------------------------------------------- /src/compiler/style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../globals'); 4 | 5 | var vlfield = require('../field'); 6 | 7 | module.exports = function(encoding, stats) { 8 | return { 9 | opacity: estimateOpacity(encoding, stats), 10 | }; 11 | }; 12 | 13 | function estimateOpacity(encoding,stats) { 14 | if (!stats) { 15 | return 1; 16 | } 17 | 18 | var numPoints = 0; 19 | 20 | if (encoding.isAggregate()) { // aggregate plot 21 | numPoints = 1; 22 | 23 | // get number of points in each "cell" 24 | // by calculating product of cardinality 25 | // for each non faceting and non-ordinal X / Y fields 26 | // note that ordinal x,y are not include since we can 27 | // consider that ordinal x are subdividing the cell into subcells anyway 28 | encoding.forEach(function(field, encType) { 29 | 30 | if (encType !== ROW && encType !== COL && 31 | !((encType === X || encType === Y) && 32 | vlfield.isOrdinalScale(field)) 33 | ) { 34 | numPoints *= encoding.cardinality(encType, stats); 35 | } 36 | }); 37 | 38 | } else { // raw plot 39 | 40 | // TODO: error handling 41 | if (!stats['*']) 42 | return 1; 43 | 44 | numPoints = stats['*'].max; // count 45 | 46 | // small multiples divide number of points 47 | var numMultiples = 1; 48 | if (encoding.has(ROW)) { 49 | numMultiples *= encoding.cardinality(ROW, stats); 50 | } 51 | if (encoding.has(COL)) { 52 | numMultiples *= encoding.cardinality(COL, stats); 53 | } 54 | numPoints /= numMultiples; 55 | } 56 | 57 | var opacity = 0; 58 | if (numPoints <= 25) { 59 | opacity = 1; 60 | } else if (numPoints < 200) { 61 | opacity = 0.8; 62 | } else if (numPoints < 1000 || encoding.is('tick')) { 63 | opacity = 0.7; 64 | } else { 65 | opacity = 0.3; 66 | } 67 | 68 | return opacity; 69 | } 70 | 71 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | # define color 4 | RED='\033[0;31m' 5 | NC='\033[0m' # No Color 6 | 7 | 8 | # 0.1 Check if jq has been installed 9 | type jq >/dev/null 2>&1 || { echo >&2 "I require jq but it's not installed. Aborting."; exit 1; } 10 | 11 | # 0.2 check if on master 12 | if [ "$(git rev-parse --abbrev-ref HEAD)" != "master" ]; then 13 | echo "${RED}Not on master, please checkout master branch before running this script${NC}" 14 | exit 1 15 | fi 16 | 17 | # 0.3 Check if all files are committed 18 | if [ -z "$(git status --porcelain)" ]; then 19 | echo "All tracked files are committed. Publishing on npm and bower. \n" 20 | else 21 | echo "${RED}There are uncommitted files. Please commit or stash first!${NC} \n\n" 22 | git status 23 | exit 1 24 | fi 25 | 26 | # # 1. BOWER PUBLISH 27 | 28 | # read version 29 | gitsha=$(git rev-parse HEAD) 30 | version=$(cat package.json | jq .version | sed -e 's/^"//' -e 's/"$//') 31 | 32 | # remove all the compiled files, so we can checkout gh-pages without errors 33 | rm -f vega-lite* 34 | rm -f spec.json 35 | 36 | # update github pages 37 | git checkout gh-pages 38 | git pull 39 | git merge master --no-edit 40 | 41 | gulp build 42 | 43 | # add the compiled files, commit and tag! 44 | git add vega-lite* -f 45 | git add spec.json -f 46 | 47 | # add bower_components for editor 48 | cd editor 49 | bower install 50 | git add bower_components/* -f 51 | cd .. 52 | 53 | # add bower_components for gallery 54 | cd gallery 55 | bower install 56 | git add bower_components/* -f 57 | cd .. 58 | 59 | # commit, tag and push to gh-pages and swap back to master 60 | set +e 61 | git commit -m "release $version $gitsha" 62 | set -e 63 | git push 64 | git tag -am "Release v$version." "v$version" 65 | git push --tags 66 | git checkout master 67 | gulp build # rebuild so that vega-lite.js are back for linked bower/npm 68 | 69 | # 2. NPM PUBLISH 70 | 71 | npm publish 72 | # exit if npm publish failed 73 | rc=$? 74 | if [[ $rc != 0 ]]; then 75 | echo "${RED} npm publish failed. Publishing canceled. ${NC} \n\n" 76 | exit $rc; 77 | fi 78 | -------------------------------------------------------------------------------- /src/compiler/stack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../globals'); 4 | 5 | var marks = require('./marks'); 6 | 7 | module.exports = stacking; 8 | 9 | function stacking(data, encoding, mdef) { 10 | if (!marks[encoding.marktype()].stack) return false; 11 | 12 | // TODO: add || encoding.has(LOD) here once LOD is implemented 13 | if (!encoding.has(COLOR)) return false; 14 | 15 | var dim=null, val=null, idx =null, 16 | isXMeasure = encoding.isMeasure(X), 17 | isYMeasure = encoding.isMeasure(Y), 18 | facets = encoding.facets(); 19 | 20 | if (isXMeasure && !isYMeasure) { 21 | dim = Y; 22 | val = X; 23 | idx = 0; 24 | } else if (isYMeasure && !isXMeasure) { 25 | dim = X; 26 | val = Y; 27 | idx = 1; 28 | } else { 29 | return null; // no stack encoding 30 | } 31 | 32 | // add transform to compute sums for scale 33 | var stacked = { 34 | name: STACKED, 35 | source: encoding.dataTable(), 36 | transform: [{ 37 | type: 'aggregate', 38 | groupby: [encoding.fieldRef(dim)].concat(facets), // dim and other facets 39 | fields: [{op: 'sum', field: encoding.fieldRef(val)}] // TODO check if field with aggregate is correct? 40 | }] 41 | }; 42 | 43 | if (facets && facets.length > 0) { 44 | stacked.transform.push({ //calculate max for each facet 45 | type: 'aggregate', 46 | groupby: facets, 47 | fields: [{ 48 | op: 'max', 49 | field: encoding.fieldName(val, {fn: 'sum'}) 50 | }] 51 | }); 52 | } 53 | 54 | data.push(stacked); 55 | 56 | // add stack transform to mark 57 | mdef.from.transform = [{ 58 | type: 'stack', 59 | point: encoding.fieldRef(dim), 60 | height: encoding.fieldRef(val), 61 | output: {y1: val, y0: val + '2'} 62 | }]; 63 | 64 | // TODO: This is super hack-ish -- consolidate into modular mark properties? 65 | mdef.properties.update[val] = mdef.properties.enter[val] = {scale: val, field: val}; 66 | mdef.properties.update[val + '2'] = mdef.properties.enter[val + '2'] = {scale: val, field: val + '2'}; 67 | 68 | return val; //return stack encoding 69 | } 70 | -------------------------------------------------------------------------------- /gallery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vega-lite Gallery 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

14 | Vega-lite 15 |

16 | 17 |

This is a gallery of Vega-lite visualizations. Uses Vega version {{vegaVersion}}.

18 | 19 |
20 | 21 | 22 |
23 |
24 |

{{viz.title}}

25 |

{{viz.description}}

26 |
27 |
{{viz.spec | compactJSON}}
28 | 29 |
30 |
31 |
32 |
33 | 34 | Fork me on GitHub 35 | 36 | 45 | 46 | -------------------------------------------------------------------------------- /bin/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var VEGA_DIR = 'shorthand2vg'; 4 | 5 | var program = require('commander'); 6 | program.version('0.0.1') 7 | .description('Generate Vega specs from Vega-lite object in testcases.js and compare output with testcases in '+ VEGA_DIR) 8 | .parse(process.argv); 9 | 10 | var fs = require('fs'), 11 | vl = require('../src/vl'), 12 | stringify = require('../lib/json3-compactstringify').stringify, 13 | deepDiff = require('deep-diff').diff; 14 | 15 | var badList = [], goodList = []; 16 | 17 | var testcases = require('./testcases'); 18 | vl.keys(testcases).forEach(function(dataUrl) { 19 | testcases[dataUrl].forEach(function(tc) { 20 | var encoding = vl.Encoding.parseShorthand(tc.e, {dataUrl: dataUrl}), 21 | filename = encoding.toShorthand(); 22 | test(filename, encoding); 23 | }); 24 | }); 25 | 26 | function test(filename, encoding) { 27 | var dataUrl = '../' + encoding.config('dataUrl'), 28 | data = require(dataUrl), 29 | stats = vl.getStats(encoding, data), 30 | spec = vl.toVegaSpec(encoding, stats); 31 | 32 | var dataname = dataUrl.split('/').pop().split('.'); 33 | dataname = dataname.slice(0, dataname.length - 1).join('.'); 34 | var vgPath = VEGA_DIR + '/'+ dataname + '.'+ filename + '.json', 35 | testSpec = require(vgPath); 36 | 37 | var diff = deepDiff(spec, testSpec); 38 | 39 | if (diff) { 40 | console.log('Bad:', filename, diff); 41 | badList.push({ 42 | filename: filename, 43 | spec: spec, 44 | testSpec: testSpec 45 | }); 46 | }else { 47 | console.log('Good:', filename); 48 | goodList.push({ 49 | filename: filename, 50 | spec: spec 51 | }); 52 | } 53 | } 54 | 55 | function writeErrorHandler(path) { 56 | return function(err) { 57 | if (err) { 58 | console.log(err); 59 | }else { 60 | console.log('File ', path, 'is saved!'); 61 | } 62 | }; 63 | } 64 | 65 | function log() { 66 | var out = stringify({bad: badList, good: goodList}, null, ' ', 80); 67 | fs.writeFile('log/difflist.json', out, writeErrorHandler('log/difflist.json')); 68 | fs.writeFile('log/difflist_' + (new Date()).toJSON() + '.json', out, writeErrorHandler('log/difflist_' + (new Date()).toJSON() + '.json')); 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vega-lite", 3 | "author": "Jeffrey Heer, Dominik Moritz, Kanit \"Ham\" Wongsuphasawat", 4 | "version": "0.7.13", 5 | "collaborators": [ 6 | "Kanit Wongsuphasawat (http://kanitw.yellowpigz.com)", 7 | "Dominik Moritz (http://domoritz.de)", 8 | "Jeffrey Heer (http://jheer.org)" 9 | ], 10 | "description": "Vega-lite provides a higher-level grammar for visual analysis, comparable to ggplot or Tableau, that generates complete Vega specifications.", 11 | "main": "src/vl.js", 12 | "directories": { 13 | "test": "test" 14 | }, 15 | "scripts": { 16 | "deploy": "npm run lint && npm run test && scripts/deploy.sh", 17 | "lint": "gulp jshint", 18 | "test": "gulp test" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/uwdata/vega-lite.git" 23 | }, 24 | "license": "BSD-3-Clause", 25 | "bugs": { 26 | "url": "https://github.com/uwdata/vega-lite/issues" 27 | }, 28 | "homepage": "https://github.com/uwdata/vega-lite", 29 | "devDependencies": { 30 | "browser-sync": "^2.8.2", 31 | "browserify": "^11.0.1", 32 | "browserify-shim": "^3.8.10", 33 | "chai": "^3.2.0", 34 | "commander": "^2.8.1", 35 | "coveralls": "^2.11.4", 36 | "d3": "^3.5.6", 37 | "deep-diff": "^0.3.2", 38 | "gulp": "^3.9.0", 39 | "gulp-bump": "^0.3.1", 40 | "gulp-git": "^1.2.4", 41 | "gulp-jshint": "^1.11.2", 42 | "gulp-load-plugins": "^1.0.0-rc", 43 | "gulp-rename": "^1.2.2", 44 | "gulp-run": "^1.6.10", 45 | "gulp-sourcemaps": "^1.5.2", 46 | "gulp-spawn-mocha": "^2.2.1", 47 | "gulp-tag-version": "^1.3.0", 48 | "gulp-uglify": "^1.3.0", 49 | "gulp-util": "^3.0.6", 50 | "jshint-stylish": "^2.0.1", 51 | "lodash": "^3.10.1", 52 | "mocha": "^2.2.5", 53 | "require-dir": "^0.3.0", 54 | "vinyl-buffer": "^1.0.0", 55 | "vinyl-source-stream": "^1.1.0", 56 | "watchify": "^3.3.1", 57 | "z-schema": "^3.12.4" 58 | }, 59 | "dependencies": { 60 | "colorbrewer": "0.0.2", 61 | "d3-color": "^0.2.4", 62 | "d3-format": "^0.3.0", 63 | "d3-time-format": "0.1.0", 64 | "datalib": "^1.4.5" 65 | }, 66 | "browserify": { 67 | "transform": [ 68 | "browserify-shim" 69 | ] 70 | }, 71 | "browserify-shim": {} 72 | } 73 | -------------------------------------------------------------------------------- /bin/shorthand2vg/birdstrikes.bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O.json: -------------------------------------------------------------------------------- 1 | { 2 | "width": 200, 3 | "height": 147, 4 | "padding": "auto", 5 | "data": [ 6 | { 7 | "name": "table", 8 | "format": {"type": "json","parse": {"Cost__Total_$": "number"}}, 9 | "url": "data/birdstrikes.json", 10 | "transform": [ 11 | { 12 | "type": "aggregate", 13 | "groupby": ["data.Effect__Amount_of_damage"], 14 | "fields": [{"op": "sum","field": "data.Cost__Total_$"}] 15 | } 16 | ] 17 | } 18 | ], 19 | "marks": [ 20 | { 21 | "_name": "cell", 22 | "type": "group", 23 | "properties": {"enter": {"width": {"value": 200},"height": {"value": 147}}}, 24 | "scales": [ 25 | { 26 | "name": "x", 27 | "type": "linear", 28 | "domain": {"data": "table","field": "data.sum_Cost__Total_$"}, 29 | "range": "width", 30 | "zero": true, 31 | "reverse": false, 32 | "round": true, 33 | "nice": true 34 | }, 35 | { 36 | "name": "y", 37 | "type": "ordinal", 38 | "domain": {"data": "table","field": "data.Effect__Amount_of_damage"}, 39 | "sort": true, 40 | "bandWidth": 21, 41 | "round": true, 42 | "nice": true, 43 | "points": true, 44 | "padding": 1 45 | } 46 | ], 47 | "axes": [ 48 | {"type": "x","scale": "x","ticks": 3}, 49 | {"type": "y","scale": "y","ticks": 3} 50 | ], 51 | "marks": [ 52 | { 53 | "type": "rect", 54 | "from": {"data": "table"}, 55 | "properties": { 56 | "enter": { 57 | "x": {"scale": "x","field": "data.sum_Cost__Total_$"}, 58 | "x2": {"scale": "x","value": 0}, 59 | "yc": {"scale": "y","field": "data.Effect__Amount_of_damage"}, 60 | "height": {"value": 21,"offset": -1}, 61 | "fill": {"value": "steelblue"} 62 | }, 63 | "update": { 64 | "x": {"scale": "x","field": "data.sum_Cost__Total_$"}, 65 | "x2": {"scale": "x","value": 0}, 66 | "yc": {"scale": "y","field": "data.Effect__Amount_of_damage"}, 67 | "height": {"value": 21,"offset": -1}, 68 | "fill": {"value": "steelblue"} 69 | } 70 | } 71 | } 72 | ] 73 | } 74 | ] 75 | } -------------------------------------------------------------------------------- /src/schema/schemautil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var schemautil = module.exports = {}, 4 | util = require('../util'); 5 | 6 | var isEmpty = function(obj) { 7 | return Object.keys(obj).length === 0; 8 | }; 9 | 10 | schemautil.extend = function(instance, schema) { 11 | return schemautil.merge(schemautil.instantiate(schema), instance); 12 | }; 13 | 14 | // instantiate a schema 15 | schemautil.instantiate = function(schema) { 16 | var val; 17 | if (schema === undefined) { 18 | return undefined; 19 | } else if ('default' in schema) { 20 | val = schema.default; 21 | return util.isObject(val) ? util.duplicate(val) : val; 22 | } else if (schema.type === 'object') { 23 | var instance = {}; 24 | for (var name in schema.properties) { 25 | val = schemautil.instantiate(schema.properties[name]); 26 | if (val !== undefined) { 27 | instance[name] = val; 28 | } 29 | } 30 | return instance; 31 | } else if (schema.type === 'array') { 32 | return []; 33 | } 34 | return undefined; 35 | }; 36 | 37 | // remove all defaults from an instance 38 | schemautil.subtract = function(instance, defaults) { 39 | var changes = {}; 40 | for (var prop in instance) { 41 | var def = defaults[prop]; 42 | var ins = instance[prop]; 43 | // Note: does not properly subtract arrays 44 | if (!defaults || def !== ins) { 45 | if (typeof ins === 'object' && !util.isArray(ins) && def) { 46 | var c = schemautil.subtract(ins, def); 47 | if (!isEmpty(c)) 48 | changes[prop] = c; 49 | } else if (!util.isArray(ins) || ins.length > 0) { 50 | changes[prop] = ins; 51 | } 52 | } 53 | } 54 | return changes; 55 | }; 56 | 57 | schemautil.merge = function(/*dest*, src0, src1, ...*/){ 58 | var dest = arguments[0]; 59 | for (var i=1 ; i 2 | 3 | 4 | Vega-lite Editor 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | Vega-lite 18 |
19 |
20 |
21 |
22 |

Shorthand

23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |

Vega-lite spec

32 |
33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 |

Vega spec

41 |
42 | 43 |
44 |
45 |
46 |
47 |
Visualization
48 |
49 |
50 |
51 |
52 |
53 | Fork me on GitHub 54 | 55 | 56 | 57 | 58 | 59 | 64 | 73 | 74 | -------------------------------------------------------------------------------- /bin/gen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var VEGA_DIR = 'vega'; 4 | 5 | var program = require('commander'); 6 | program.version('0.0.1') 7 | .description('Generate Test Cases. Regenerate all testcases in ' + VEGA_DIR + ' by default. Use -f or -j to generate single test case.') 8 | .option('-j, --json [string]', 'Create test from json strings [null]', null) 9 | .option('-f, --file [path]', 'Create test from file [null]', null) 10 | .option('-d, --data [path]', 'Data file path (otherwise, path will be parsed from dataUrl config.) [null]', null) 11 | .option('-n, --note [String]', 'Add _info.description property to the vega-lite json file.', null) 12 | .parse(process.argv); 13 | 14 | var fs = require('fs'), 15 | vl = require('../src/vl.js'), 16 | stringify = require('../lib/json3-compactstringify').stringify; 17 | 18 | if (program.json || program.file) { 19 | var json = program.json ? JSON.parse(program.json) : require(program.file), 20 | encoding = vl.Encoding.fromSpec(json); 21 | 22 | if (program.note) { 23 | (encoding._info = encoding._info || {}).description = program.note; 24 | } 25 | 26 | generate(encoding, 'specs'); 27 | }else { 28 | var testcases = require('./testcases'); 29 | 30 | vl.keys(testcases).forEach(function(dataUrl) { 31 | testcases[dataUrl].forEach(function(tc) { 32 | var encoding = vl.Encoding.parseShorthand(tc.e, {dataUrl: dataUrl}); 33 | encoding._info = { 34 | description: tc.n 35 | }; 36 | generate(encoding, 'shorthand2vg'); 37 | }); 38 | }); 39 | //TODO read from testcases.js instead 40 | 41 | // fs.readdir(vegaliteDir, function(err, files){ 42 | // files.filter(function(f){ 43 | // return f.lastIndexOf(".json") == f.length - 5; //filter .DSStore and other unrelated files 44 | // }).forEach(function(f){ 45 | // generate(require(vegaliteDir+"/"+f)); 46 | // }); 47 | // }); 48 | } 49 | 50 | function writeErrorHandler(path) { 51 | return function(err) { 52 | if (err) { 53 | console.log(err); 54 | }else { 55 | console.log('File ', path, 'is saved!'); 56 | } 57 | }; 58 | } 59 | 60 | function generate(encoding, vlDir, vgDir) { 61 | if (program.note) { 62 | encoding._note = program.note; 63 | } 64 | var dataUrl = program.data || (encoding.data('url') ? '../' + encoding.data('url') : null); 65 | 66 | if (!dataUrl) { 67 | console.log('no data provided neither as argument or as dataUrl config.'); 68 | process.exit(1); 69 | } 70 | 71 | var data = require(dataUrl); 72 | 73 | var dataname = dataUrl.split('/').pop().split('.'); 74 | dataname = dataname.slice(0, dataname.length - 1).join('.'); 75 | 76 | var filename = encoding.toShorthand(); 77 | 78 | var stats = vl.data.stats(data), 79 | spec = vl.compile.encoding(encoding, stats); 80 | 81 | var vlPath = vlDir + '/'+ dataname + '.'+ filename + '.json', 82 | vgPath = vgDir + '/'+ dataname + '.'+ filename + '.json'; 83 | 84 | if (vlDir) { 85 | fs.writeFile(vlPath, stringify(json, null, ' ', 80), writeErrorHandler(vlPath)); 86 | } 87 | if (vgDir) { 88 | fs.writeFile(vgPath, stringify(spec, null, ' ', 80), writeErrorHandler(vgPath)); 89 | }else { 90 | //output in console 91 | console.log(stringify(spec, null, ' ', 80)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/compiler/legend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../globals'); 4 | 5 | var time = require('./time'), 6 | util = require('../util'), 7 | setter = util.setter, 8 | getter = util.getter; 9 | 10 | var legend = module.exports = {}; 11 | 12 | legend.defs = function(encoding, style) { 13 | var defs = []; 14 | 15 | if (encoding.has(COLOR) && encoding.field(COLOR).legend) { 16 | defs.push(legend.def(COLOR, encoding, { 17 | fill: COLOR, 18 | orient: 'right' 19 | }, style)); 20 | } 21 | 22 | if (encoding.has(SIZE) && encoding.field(SIZE).legend) { 23 | defs.push(legend.def(SIZE, encoding, { 24 | size: SIZE, 25 | orient: defs.length === 1 ? 'left' : 'right' 26 | }, style)); 27 | } 28 | 29 | if (encoding.has(SHAPE) && encoding.field(SHAPE).legend) { 30 | if (defs.length === 2) { 31 | console.error('Vega-lite currently only supports two legends'); 32 | } 33 | defs.push(legend.def(SHAPE, encoding, { 34 | shape: SHAPE, 35 | orient: defs.length === 1 ? 'left' : 'right' 36 | }, style)); 37 | } 38 | return defs; 39 | }; 40 | 41 | legend.def = function(name, encoding, def, style) { 42 | var timeUnit = encoding.field(name).timeUnit; 43 | 44 | def.title = legend.title(name, encoding); 45 | def = legend.style(name, encoding, def, style); 46 | 47 | if (encoding.isType(name, T) && 48 | timeUnit && 49 | time.hasScale(timeUnit) 50 | ) { 51 | setter(def, ['properties', 'labels', 'text', 'scale'], 'time-'+ timeUnit); 52 | } 53 | 54 | return def; 55 | }; 56 | 57 | legend.style = function(name, e, def, style) { 58 | var symbols = getter(def, ['properties', 'symbols']), 59 | marktype = e.marktype(); 60 | 61 | switch (marktype) { 62 | case 'bar': 63 | case 'tick': 64 | case 'text': 65 | symbols.stroke = {value: 'transparent'}; 66 | symbols.shape = {value: 'square'}; 67 | break; 68 | 69 | case 'circle': 70 | case 'square': 71 | symbols.shape = {value: marktype}; 72 | /* fall through */ 73 | case 'point': 74 | // fill or stroke 75 | if (e.field(SHAPE).filled) { 76 | if (e.has(COLOR) && name === COLOR) { 77 | symbols.fill = {scale: COLOR, field: 'data'}; 78 | } else { 79 | symbols.fill = {value: e.value(COLOR)}; 80 | } 81 | symbols.stroke = {value: 'transparent'}; 82 | } else { 83 | if (e.has(COLOR) && name === COLOR) { 84 | symbols.stroke = {scale: COLOR, field: 'data'}; 85 | } else { 86 | symbols.stroke = {value: e.value(COLOR)}; 87 | } 88 | symbols.fill = {value: 'transparent'}; 89 | symbols.strokeWidth = {value: e.config('strokeWidth')}; 90 | } 91 | 92 | break; 93 | case 'line': 94 | case 'area': 95 | // TODO use shape here after implementing #508 96 | break; 97 | } 98 | 99 | var opacity = e.field(COLOR).opacity || style.opacity; 100 | if (opacity) { 101 | symbols.opacity = {value: opacity}; 102 | } 103 | return def; 104 | }; 105 | 106 | legend.title = function(name, encoding) { 107 | var leg = encoding.field(name).legend; 108 | 109 | if (leg.title) return leg.title; 110 | 111 | return encoding.fieldTitle(name); 112 | }; 113 | -------------------------------------------------------------------------------- /data/burtin.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Bacteria":"Aerobacter aerogenes", 4 | "Penicilin":870, 5 | "Streptomycin":1, 6 | "Neomycin":1.6, 7 | "Gram_Staining":"negative", 8 | "Genus": "other" 9 | }, 10 | { 11 | "Bacteria":"Brucella abortus", 12 | "Penicilin":1, 13 | "Streptomycin":2, 14 | "Neomycin":0.02, 15 | "Gram_Staining":"negative", 16 | "Genus": "other" 17 | }, 18 | { 19 | "Bacteria":"Brucella anthracis", 20 | "Penicilin":0.001, 21 | "Streptomycin":0.01, 22 | "Neomycin":0.007, 23 | "Gram_Staining":"positive", 24 | "Genus": "other" 25 | }, 26 | { 27 | "Bacteria":"Diplococcus pneumoniae", 28 | "Penicilin":0.005, 29 | "Streptomycin":11, 30 | "Neomycin":10, 31 | "Gram_Staining":"positive", 32 | "Genus": "other" 33 | }, 34 | { 35 | "Bacteria":"Escherichia coli", 36 | "Penicilin":100, 37 | "Streptomycin":0.4, 38 | "Neomycin":0.1, 39 | "Gram_Staining":"negative", 40 | "Genus": "other" 41 | }, 42 | { 43 | "Bacteria":"Klebsiella pneumoniae", 44 | "Penicilin":850, 45 | "Streptomycin":1.2, 46 | "Neomycin":1, 47 | "Gram_Staining":"negative", 48 | "Genus": "other" 49 | }, 50 | { 51 | "Bacteria":"Mycobacterium tuberculosis", 52 | "Penicilin":800, 53 | "Streptomycin":5, 54 | "Neomycin":2, 55 | "Gram_Staining":"negative", 56 | "Genus": "other" 57 | }, 58 | { 59 | "Bacteria":"Proteus vulgaris", 60 | "Penicilin":3, 61 | "Streptomycin":0.1, 62 | "Neomycin":0.1, 63 | "Gram_Staining":"negative", 64 | "Genus": "other" 65 | }, 66 | { 67 | "Bacteria":"Pseudomonas aeruginosa", 68 | "Penicilin":850, 69 | "Streptomycin":2, 70 | "Neomycin":0.4, 71 | "Gram_Staining":"negative", 72 | "Genus": "other" 73 | }, 74 | { 75 | "Bacteria":"Salmonella (Eberthella) typhosa", 76 | "Penicilin":1, 77 | "Streptomycin":0.4, 78 | "Neomycin":0.008, 79 | "Gram_Staining":"negative", 80 | "Genus": "Salmonella" 81 | }, 82 | { 83 | "Bacteria":"Salmonella schottmuelleri", 84 | "Penicilin":10, 85 | "Streptomycin":0.8, 86 | "Neomycin":0.09, 87 | "Gram_Staining":"negative", 88 | "Genus": "Salmonella" 89 | }, 90 | { 91 | "Bacteria":"Staphylococcus albus", 92 | "Penicilin":0.007, 93 | "Streptomycin":0.1, 94 | "Neomycin":0.001, 95 | "Gram_Staining":"positive", 96 | "Genus": "Staphylococcus" 97 | }, 98 | { 99 | "Bacteria":"Staphylococcus aureus", 100 | "Penicilin":0.03, 101 | "Streptomycin":0.03, 102 | "Neomycin":0.001, 103 | "Gram_Staining":"positive", 104 | "Genus": "Staphylococcus" 105 | }, 106 | { 107 | "Bacteria":"Streptococcus fecalis", 108 | "Penicilin":1, 109 | "Streptomycin":1, 110 | "Neomycin":0.1, 111 | "Gram_Staining":"positive", 112 | "Genus": "Streptococcus" 113 | }, 114 | { 115 | "Bacteria":"Streptococcus hemolyticus", 116 | "Penicilin":0.001, 117 | "Streptomycin":14, 118 | "Neomycin":10, 119 | "Gram_Staining":"positive", 120 | "Genus": "Streptococcus" 121 | }, 122 | { 123 | "Bacteria":"Streptococcus viridans", 124 | "Penicilin":0.005, 125 | "Streptomycin":10, 126 | "Neomycin":40, 127 | "Gram_Staining":"positive", 128 | "Genus": "Streptococcus" 129 | } 130 | ] -------------------------------------------------------------------------------- /test/schema.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'), 4 | ZSchema = require("z-schema"), 5 | inspect = require('util').inspect; 6 | 7 | var schema = require('../lib/schema.json'), 8 | util = require('../src/schema/schemautil.js'), 9 | specSchema = require('../src/schema/schema.js').schema; 10 | 11 | /*jshint -W121 */ 12 | String.prototype.endsWith = function(suffix) { 13 | return this.indexOf(suffix, this.length - suffix.length) !== -1; 14 | }; 15 | 16 | describe('Schema', function() { 17 | it('should be valid', function() { 18 | var validator = new ZSchema(); 19 | 20 | // now validate our data against the schema 21 | var valid = validator.validate(specSchema, schema); 22 | 23 | if (!valid) { 24 | var errors = validator.getLastErrors(); 25 | console.log(inspect(errors, { depth: 10, colors: true })); 26 | } 27 | assert.equal(valid, true); 28 | }); 29 | 30 | it('field def should have supportedMarktypes', function() { 31 | var encProps = specSchema.properties.encoding.properties; 32 | for (var k in encProps) { 33 | assert.notEqual(encProps[k].supportedMarktypes, undefined); 34 | } 35 | }); 36 | }); 37 | 38 | describe('Util', function() { 39 | it('instantiate simple schema', function() { 40 | var simpleSchema = { 41 | type: 'object', required: ['fooBaz'], 42 | properties: { 43 | foo: {type: 'array'}, 44 | fooBar: {type: 'string', default: 'baz'}, 45 | fooBaz: {type: 'string', enum: ['a', 'b']}}}; 46 | assert.deepEqual( 47 | util.instantiate(simpleSchema), 48 | {foo: [], fooBar: 'baz'}); 49 | }); 50 | 51 | it('remove defaults', function() { 52 | var spec = { 53 | marktype: 'point', 54 | encoding: { 55 | x: { name: 'dsp', type: 'Q', scale: {type: 'linear'} 56 | }, 57 | color: { name: 'cyl', type: 'O' } 58 | }, 59 | data: { 60 | formatType: 'json', 61 | url: 'data/cars.json' 62 | } 63 | }; 64 | 65 | var expected = { 66 | marktype: 'point', 67 | encoding: { 68 | x: { name: 'dsp', type: 'Q' }, 69 | color: { name: 'cyl', type: 'O' } 70 | }, 71 | data: { 72 | url: 'data/cars.json' 73 | } 74 | }; 75 | 76 | var actual = util.subtract(spec, util.instantiate(specSchema)); 77 | assert.deepEqual(actual, expected); 78 | }); 79 | 80 | it('subtract with different types', function() { 81 | var a = {a: 'foo', b: 'bar', 'baz': [1, 2, 3]}; 82 | var b = {a: 'foo', b: 'hi', 'baz': 'hi'}; 83 | 84 | assert.deepEqual( 85 | util.subtract(a, b), 86 | {b: 'bar', baz: [1, 2, 3]}); 87 | 88 | assert.equal(util.subtract(a, b).baz instanceof Array, true); 89 | }); 90 | 91 | it('merge objects', function() { 92 | var a = {a: 'foo', b: {'bar': 0, 'baz': [], 'qux': [1, 2, 3]}}; 93 | var b = {a: 'fuu'}; 94 | 95 | assert.deepEqual( 96 | util.merge(a, b), 97 | {a: 'fuu', b: {'bar': 0, 'baz': [], 'qux': [1, 2, 3]}}); 98 | }); 99 | 100 | it('merge objects reversed', function() { 101 | var a = {a: 'foo', b: {'bar': 0, 'baz': [], 'qux': [1, 2, 3]}}; 102 | var b = {a: 'fuu'}; 103 | 104 | assert.deepEqual( 105 | util.merge(b, a), 106 | {a: 'foo', b: {'bar': 0, 'baz': [], 'qux': [1, 2, 3]}}); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /data/driving.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"side": "left", "year": 1956, "miles": 3675, "gas": 2.38}, 3 | {"side": "right", "year": 1957, "miles": 3706, "gas": 2.40}, 4 | {"side": "bottom", "year": 1958, "miles": 3766, "gas": 2.26}, 5 | {"side": "top", "year": 1959, "miles": 3905, "gas": 2.31}, 6 | {"side": "right", "year": 1960, "miles": 3935, "gas": 2.27}, 7 | {"side": "bottom", "year": 1961, "miles": 3977, "gas": 2.25}, 8 | {"side": "right", "year": 1962, "miles": 4085, "gas": 2.22}, 9 | {"side": "bottom", "year": 1963, "miles": 4218, "gas": 2.12}, 10 | {"side": "bottom", "year": 1964, "miles": 4369, "gas": 2.11}, 11 | {"side": "bottom", "year": 1965, "miles": 4538, "gas": 2.14}, 12 | {"side": "top", "year": 1966, "miles": 4676, "gas": 2.14}, 13 | {"side": "bottom", "year": 1967, "miles": 4827, "gas": 2.14}, 14 | {"side": "right", "year": 1968, "miles": 5038, "gas": 2.13}, 15 | {"side": "right", "year": 1969, "miles": 5207, "gas": 2.07}, 16 | {"side": "right", "year": 1970, "miles": 5376, "gas": 2.01}, 17 | {"side": "bottom", "year": 1971, "miles": 5617, "gas": 1.93}, 18 | {"side": "bottom", "year": 1972, "miles": 5973, "gas": 1.87}, 19 | {"side": "right", "year": 1973, "miles": 6154, "gas": 1.90}, 20 | {"side": "left", "year": 1974, "miles": 5943, "gas": 2.34}, 21 | {"side": "bottom", "year": 1975, "miles": 6111, "gas": 2.31}, 22 | {"side": "bottom", "year": 1976, "miles": 6389, "gas": 2.32}, 23 | {"side": "top", "year": 1977, "miles": 6630, "gas": 2.36}, 24 | {"side": "bottom", "year": 1978, "miles": 6883, "gas": 2.23}, 25 | {"side": "left", "year": 1979, "miles": 6744, "gas": 2.68}, 26 | {"side": "left", "year": 1980, "miles": 6672, "gas": 3.30}, 27 | {"side": "right", "year": 1981, "miles": 6732, "gas": 3.30}, 28 | {"side": "right", "year": 1982, "miles": 6835, "gas": 2.92}, 29 | {"side": "right", "year": 1983, "miles": 6943, "gas": 2.66}, 30 | {"side": "right", "year": 1984, "miles": 7130, "gas": 2.48}, 31 | {"side": "right", "year": 1985, "miles": 7323, "gas": 2.36}, 32 | {"side": "left", "year": 1986, "miles": 7558, "gas": 1.76}, 33 | {"side": "top", "year": 1987, "miles": 7770, "gas": 1.76}, 34 | {"side": "bottom", "year": 1988, "miles": 8089, "gas": 1.68}, 35 | {"side": "left", "year": 1989, "miles": 8397, "gas": 1.75}, 36 | {"side": "top", "year": 1990, "miles": 8529, "gas": 1.88}, 37 | {"side": "right", "year": 1991, "miles": 8535, "gas": 1.78}, 38 | {"side": "right", "year": 1992, "miles": 8662, "gas": 1.69}, 39 | {"side": "left", "year": 1993, "miles": 8855, "gas": 1.60}, 40 | {"side": "bottom", "year": 1994, "miles": 8909, "gas": 1.59}, 41 | {"side": "bottom", "year": 1995, "miles": 9150, "gas": 1.60}, 42 | {"side": "top", "year": 1996, "miles": 9192, "gas": 1.67}, 43 | {"side": "right", "year": 1997, "miles": 9416, "gas": 1.65}, 44 | {"side": "bottom", "year": 1998, "miles": 9590, "gas": 1.39}, 45 | {"side": "right", "year": 1999, "miles": 9687, "gas": 1.50}, 46 | {"side": "top", "year": 2000, "miles": 9717, "gas": 1.89}, 47 | {"side": "left", "year": 2001, "miles": 9699, "gas": 1.77}, 48 | {"side": "bottom", "year": 2002, "miles": 9814, "gas": 1.64}, 49 | {"side": "right", "year": 2003, "miles": 9868, "gas": 1.86}, 50 | {"side": "left", "year": 2004, "miles": 9994, "gas": 2.14}, 51 | {"side": "left", "year": 2005, "miles": 10067, "gas": 2.53}, 52 | {"side": "right", "year": 2006, "miles": 10037, "gas": 2.79}, 53 | {"side": "right", "year": 2007, "miles": 10025, "gas": 2.95}, 54 | {"side": "left", "year": 2008, "miles": 9880, "gas": 3.31}, 55 | {"side": "bottom", "year": 2009, "miles": 9657, "gas": 2.38}, 56 | {"side": "left", "year": 2010, "miles": 9596, "gas": 2.61} 57 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vega-lite 2 | 3 | [![Build Status](https://travis-ci.org/uwdata/vega-lite.svg)](https://travis-ci.org/uwdata/vega-lite) 4 | [![npm dependencies](https://david-dm.org/uwdata/vega-lite.svg)](https://www.npmjs.com/package/vega-lite) 5 | [![npm version](https://img.shields.io/npm/v/vega-lite.svg)](https://www.npmjs.com/package/vega-lite) 6 | [![Coverage Status](https://coveralls.io/repos/uwdata/vega-lite/badge.svg)](https://coveralls.io/r/uwdata/vega-lite) 7 | 8 | **Vega-lite is work in progress and we are working on improving the code and documentation.** 9 | 10 | Provides a higher-level grammar for visual analysis, comparable to ggplot or Tableau, that generates complete [Vega](https://vega.github.io/) specifications. 11 | 12 | Vega-lite specifications consist of simple mappings of variables in a data set to visual encoding channels such as position (`x`,`y`), `size`, `color` and `shape`. These mappings are then translated into full visualization specifications using the Vega visualization grammar. These resulting visualizations can then be exported or further modified to customize the display. 13 | 14 | If you are using Vega-lite for your project(s), please let us know by emailing us at [Vega-lite \[at\] cs.washington.edu](mailto:vega-lite@cs.washington.edu). Feedbacks are also welcomed. 15 | If you find a bug or have a feature request, please [create an issue](https://github.com/uwdata/vega-lite/issues/new). 16 | 17 | Use Vega-lite in the [online editor](https://uwdata.github.io/vega-lite/). 18 | 19 | The complete schema for specifications as [JSON schema](http://json-schema.org/) is at [spec.json](https://uwdata.github.io/vega-lite/spec.json). 20 | 21 | ## Example specification 22 | 23 | We have more example visualizations in our [gallery](https://uwdata.github.io/vega-lite/gallery.html). 24 | 25 | ### Barleys 26 | 27 | ```json 28 | { 29 | "data": {"url": "data/barley.json"}, 30 | "marktype": "point", 31 | "encoding": { 32 | "x": {"type": "Q","name": "yield","aggregate": "avg"}, 33 | "y": { 34 | "sort": [{"name": "yield","aggregate": "avg","reverse": false}], 35 | "type": "O", 36 | "name": "variety" 37 | }, 38 | "row": {"type": "O","name": "site"}, 39 | "color": {"type": "O","name": "year"} 40 | } 41 | } 42 | ``` 43 | 44 | ### Simple bar chart 45 | 46 | This is a similar chart as one of the Vega examples in https://github.com/trifacta/vega/wiki/Tutorial. See how much simpler it is. 47 | 48 | ```json 49 | { 50 | "data": { 51 | "values": [ 52 | {"x":"A", "y":28}, {"x":"B", "y":55}, {"x":"C", "y":43}, 53 | {"x":"D", "y":91}, {"x":"E", "y":81}, {"x":"F", "y":53}, 54 | {"x":"G", "y":19}, {"x":"H", "y":87}, {"x":"I", "y":52} 55 | ] 56 | }, 57 | "marktype": "bar", 58 | "encoding": { 59 | "y": {"type": "Q","name": "y"}, 60 | "x": {"type": "O","name": "x"} 61 | } 62 | } 63 | ``` 64 | 65 | ## Setup Instructions 66 | 67 | Make sure you have node.js. (We recommend using [homebrew](http://brew.sh) and simply run `brew install node`.) 68 | 69 | Install gulp globally by running 70 | 71 | ```sh 72 | npm install -g gulp 73 | ``` 74 | 75 | Then install all the npm dependencies: 76 | 77 | ```sh 78 | npm install 79 | ``` 80 | 81 | You can run `gulp` to compile vega-lite or run `gulp serve` to open the live vega-lite editor. 82 | 83 | ### Developing Vega-lite and Datalib 84 | 85 | Vega-lite depends on [Datalib](https://github.com/uwdata/datalib). 86 | If you plan to make changes to datalib and test Vega-lite without publishing / copying compiled datalib all the time, use npm's [link](http://justjs.com/posts/npm-link-developing-your-own-npm-modules-without-tears) function. 87 | 88 | 89 | ``` 90 | # first link datalib global npm 91 | cd path/to/datalib 92 | npm link 93 | # then link vega-lite to datalib 94 | cd path/to/vega-lite 95 | npm link datalib 96 | ``` 97 | 98 | Now all the changes you make in Datalib are reflected in your Vega-lite automatically. 99 | 100 | -------------------------------------------------------------------------------- /bin/shorthand2vg/birdstrikes.bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O.color-When__Phase_of_flight-O.json: -------------------------------------------------------------------------------- 1 | { 2 | "width": 200, 3 | "height": 147, 4 | "padding": "auto", 5 | "data": [ 6 | { 7 | "name": "table", 8 | "format": {"type": "json","parse": {"Cost__Total_$": "number"}}, 9 | "url": "data/birdstrikes.json", 10 | "transform": [ 11 | { 12 | "type": "aggregate", 13 | "groupby": ["data.Effect__Amount_of_damage","data.When__Phase_of_flight"], 14 | "fields": [{"op": "sum","field": "data.Cost__Total_$"}] 15 | } 16 | ] 17 | }, 18 | { 19 | "name": "stacked", 20 | "source": "table", 21 | "transform": [ 22 | { 23 | "type": "aggregate", 24 | "groupby": ["data.Effect__Amount_of_damage"], 25 | "fields": [{"op": "sum","field": "data.sum_Cost__Total_$"}] 26 | } 27 | ] 28 | } 29 | ], 30 | "marks": [ 31 | { 32 | "_name": "cell", 33 | "type": "group", 34 | "properties": {"enter": {"width": {"value": 200},"height": {"value": 147}}}, 35 | "scales": [ 36 | { 37 | "name": "x", 38 | "type": "linear", 39 | "domain": {"data": "stacked","field": "data.sum_sum_Cost__Total_$"}, 40 | "range": "width", 41 | "zero": true, 42 | "reverse": false, 43 | "round": true, 44 | "nice": true 45 | }, 46 | { 47 | "name": "y", 48 | "type": "ordinal", 49 | "domain": {"data": "table","field": "data.Effect__Amount_of_damage"}, 50 | "sort": true, 51 | "bandWidth": 21, 52 | "round": true, 53 | "nice": true, 54 | "points": true, 55 | "padding": 1 56 | }, 57 | { 58 | "name": "color", 59 | "type": "ordinal", 60 | "domain": {"data": "table","field": "data.When__Phase_of_flight"}, 61 | "sort": true, 62 | "range": "category10", 63 | "points": true, 64 | "padding": 1 65 | } 66 | ], 67 | "axes": [ 68 | {"type": "x","scale": "x","ticks": 3}, 69 | {"type": "y","scale": "y","ticks": 3} 70 | ], 71 | "marks": [ 72 | { 73 | "_name": "subfacet", 74 | "type": "group", 75 | "from": { 76 | "data": "table", 77 | "transform": [ 78 | {"type": "sort","by": "data.When__Phase_of_flight"}, 79 | {"type": "facet","keys": ["data.When__Phase_of_flight"]}, 80 | { 81 | "type": "stack", 82 | "point": "data.Effect__Amount_of_damage", 83 | "height": "data.sum_Cost__Total_$", 84 | "output": {"y1": "x","y0": "x2"} 85 | } 86 | ] 87 | }, 88 | "properties": {"enter": {"width": {"group": "width"},"height": {"group": "height"}}}, 89 | "marks": [ 90 | { 91 | "type": "rect", 92 | "properties": { 93 | "enter": { 94 | "x": {"scale": "x","field": "x"}, 95 | "x2": {"scale": "x","field": "x2"}, 96 | "yc": {"scale": "y","field": "data.Effect__Amount_of_damage"}, 97 | "height": {"value": 21,"offset": -1}, 98 | "fill": {"scale": "color","field": "data.When__Phase_of_flight"} 99 | }, 100 | "update": { 101 | "x": {"scale": "x","field": "x"}, 102 | "x2": {"scale": "x","field": "x2"}, 103 | "yc": {"scale": "y","field": "data.Effect__Amount_of_damage"}, 104 | "height": {"value": 21,"offset": -1}, 105 | "fill": {"scale": "color","field": "data.When__Phase_of_flight"} 106 | } 107 | } 108 | } 109 | ] 110 | } 111 | ] 112 | } 113 | ] 114 | } -------------------------------------------------------------------------------- /src/compiler/facet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../globals'); 4 | 5 | var util = require('../util'); 6 | 7 | var axis = require('./axis'), 8 | groupdef = require('./group').def, 9 | scale = require('./scale'); 10 | 11 | module.exports = faceting; 12 | 13 | function faceting(group, encoding, layout, spec, singleScaleNames, stack, stats) { 14 | var enter = group.properties.enter; 15 | var facetKeys = [], cellAxes = [], from, axesGrp; 16 | 17 | var hasRow = encoding.has(ROW), hasCol = encoding.has(COL); 18 | 19 | enter.fill = {value: encoding.config('cellBackgroundColor')}; 20 | 21 | //move "from" to cell level and add facet transform 22 | group.from = {data: group.marks[0].from.data}; 23 | 24 | // Hack, this needs to be refactored 25 | for (var i = 0; i < group.marks.length; i++) { 26 | var mark = group.marks[i]; 27 | if (mark.from.transform) { 28 | delete mark.from.data; //need to keep transform for subfacetting case 29 | } else { 30 | delete mark.from; 31 | } 32 | } 33 | 34 | if (hasRow) { 35 | if (!encoding.isDimension(ROW)) { 36 | util.error('Row encoding should be ordinal.'); 37 | } 38 | enter.y = {scale: ROW, field: 'keys.' + facetKeys.length}; 39 | enter.height = {'value': layout.cellHeight}; // HACK 40 | 41 | facetKeys.push(encoding.fieldRef(ROW)); 42 | 43 | if (hasCol) { 44 | from = util.duplicate(group.from); 45 | from.transform = from.transform || []; 46 | from.transform.unshift({type: 'facet', keys: [encoding.fieldRef(COL)]}); 47 | } 48 | 49 | axesGrp = groupdef('x-axes', { 50 | axes: encoding.has(X) ? [axis.def(X, encoding, layout, stats)] : undefined, 51 | x: hasCol ? {scale: COL, field: 'keys.0'} : {value: 0}, 52 | width: hasCol && {'value': layout.cellWidth}, //HACK? 53 | from: from 54 | }); 55 | 56 | spec.marks.unshift(axesGrp); // need to prepend so it appears under the plots 57 | (spec.axes = spec.axes || []); 58 | spec.axes.push(axis.def(ROW, encoding, layout, stats)); 59 | } else { // doesn't have row 60 | if (encoding.has(X)) { 61 | //keep x axis in the cell 62 | cellAxes.push(axis.def(X, encoding, layout, stats)); 63 | } 64 | } 65 | 66 | if (hasCol) { 67 | if (!encoding.isDimension(COL)) { 68 | util.error('Col encoding should be ordinal.'); 69 | } 70 | enter.x = {scale: COL, field: 'keys.' + facetKeys.length}; 71 | enter.width = {'value': layout.cellWidth}; // HACK 72 | 73 | facetKeys.push(encoding.fieldRef(COL)); 74 | 75 | if (hasRow) { 76 | from = util.duplicate(group.from); 77 | from.transform = from.transform || []; 78 | from.transform.unshift({type: 'facet', keys: [encoding.fieldRef(ROW)]}); 79 | } 80 | 81 | axesGrp = groupdef('y-axes', { 82 | axes: encoding.has(Y) ? [axis.def(Y, encoding, layout, stats)] : undefined, 83 | y: hasRow && {scale: ROW, field: 'keys.0'}, 84 | x: hasRow && {value: 0}, 85 | height: hasRow && {'value': layout.cellHeight}, //HACK? 86 | from: from 87 | }); 88 | 89 | spec.marks.unshift(axesGrp); // need to prepend so it appears under the plots 90 | (spec.axes = spec.axes || []); 91 | spec.axes.push(axis.def(COL, encoding, layout, stats)); 92 | } else { // doesn't have col 93 | if (encoding.has(Y)) { 94 | cellAxes.push(axis.def(Y, encoding, layout, stats)); 95 | } 96 | } 97 | 98 | // assuming equal cellWidth here 99 | // TODO: support heterogenous cellWidth (maybe by using multiple scales?) 100 | spec.scales = (spec.scales || []).concat(scale.defs( 101 | scale.names(enter).concat(singleScaleNames), 102 | encoding, 103 | layout, 104 | stats, 105 | {stack: stack, facet: true} 106 | )); // row/col scales + cell scales 107 | 108 | if (cellAxes.length > 0) { 109 | group.axes = cellAxes; 110 | } 111 | 112 | // add facet transform 113 | var trans = (group.from.transform || (group.from.transform = [])); 114 | trans.unshift({type: 'facet', keys: facetKeys}); 115 | 116 | return spec; 117 | } 118 | -------------------------------------------------------------------------------- /test/fixtures.js: -------------------------------------------------------------------------------- 1 | var f = {}; 2 | 3 | // BARS 4 | 5 | f.bars = {}; 6 | 7 | f.bars.log_ver = { 8 | "marktype": "bar", 9 | "encoding": { 10 | "x": {"bin": {"maxbins": 15},"type": "Q","name": "IMDB_Rating"}, 11 | "y": {"scale": {"type": "log"},"type": "Q","name": "US_Gross","aggregate": "avg"} 12 | }, 13 | "data": {"url": "data/movies.json"} 14 | }; 15 | 16 | f.bars.log_hor = { 17 | "marktype": "bar", 18 | "encoding": { 19 | "y": {"bin": {"maxbins": 15},"type": "Q","name": "IMDB_Rating"}, 20 | "x": {"scale": {"type": "log"},"type": "Q","name": "US_Gross","aggregate": "avg"} 21 | }, 22 | "data": {"url": "data/movies.json"} 23 | }; 24 | 25 | f.bars['1d_hor'] = { 26 | "marktype": "bar", 27 | "encoding": {"x": {"type": "Q","name": "US_Gross","aggregate": "sum"}}, 28 | "data": {"url": "data/movies.json"} 29 | }; 30 | 31 | 32 | f.bars['1d_ver'] = { 33 | "marktype": "bar", 34 | "encoding": {"y": {"type": "Q","name": "US_Gross","aggregate": "sum"}}, 35 | "data": {"url": "data/movies.json"} 36 | }; 37 | 38 | // STACK 39 | 40 | f.stack = {}; 41 | 42 | f.stack.binY = { 43 | "marktype": "bar", 44 | "encoding": { 45 | "x": {"type": "Q","name": "Cost__Other","aggregate": "avg"}, 46 | "y": {"bin": true,"type": "Q","name": "Cost__Total_$"}, 47 | "color": {"type": "O","name": "Effect__Amount_of_damage"} 48 | } 49 | }; 50 | f.stack.binX = { 51 | "marktype": "bar", 52 | "encoding": { 53 | "y": {"type": "Q","name": "Cost__Other","aggregate": "avg"}, 54 | "x": {"bin": true,"type": "Q","name": "Cost__Total_$"}, 55 | "color": {"type": "O","name": "Effect__Amount_of_damage"} 56 | } 57 | }; 58 | 59 | // POINT 60 | 61 | f.points = {}; 62 | 63 | f.points['1d_hor'] = { 64 | "marktype": "point", 65 | "encoding": {"x": {"name": "year","type": "O"}}, 66 | "data": {"url": "data/barley.json"} 67 | }; 68 | 69 | f.points['1d_ver'] = { 70 | "marktype": "point", 71 | "encoding": {"y": {"name": "year","type": "O"}}, 72 | "data": {"url": "data/barley.json"} 73 | }; 74 | 75 | f.points['x,y'] = { 76 | "marktype": "point", 77 | "encoding": {"x": {"name": "year","type": "O"},"y": {"name": "yield","type": "Q"}}, 78 | "data": {"url": "data/barley.json"} 79 | }; 80 | 81 | f.points['x,y,size'] = { 82 | "marktype": "point", 83 | "encoding": { 84 | "x": {"name": "year","type": "O"}, 85 | "y": {"name": "yield","type": "Q"}, 86 | "size": {"name": "*","type": "Q","aggregate": "count"} 87 | }, 88 | "data": {"url": "data/barley.json"} 89 | }; 90 | 91 | f.points['x,y,stroke'] = { 92 | "marktype": "point", 93 | "encoding": { 94 | "x": {"name": "year","type": "O"}, 95 | "y": {"name": "yield","type": "Q"}, 96 | "color": {"name": "yield","type": "Q"} 97 | }, 98 | "data": {"url": "data/barley.json"} 99 | }; 100 | 101 | f.points['x,y,shape'] = { 102 | "marktype": "point", 103 | "encoding": { 104 | "x": {"name": "year","type": "O"}, 105 | "y": {"name": "yield","type": "Q"}, 106 | "shape": {"bin": {"maxbins": 15},"name": "yield","type": "Q"} 107 | }, 108 | "data": {"url": "data/barley.json"} 109 | }; 110 | 111 | // LINE 112 | 113 | f.lines = {}; 114 | 115 | f.lines['x,y'] = { 116 | "marktype": "line", 117 | "encoding": {"x": {"name": "year","type": "O"},"y": {"name": "yield","type": "Q"}}, 118 | "data": {"url": "data/barley.json"} 119 | }; 120 | 121 | f.lines['x,y,stroke'] = { 122 | "marktype": "line", 123 | "encoding": { 124 | "x": {"name": "name","type": "N"}, 125 | "y": {"name": "Cylinders","type": "O"}, 126 | "color": {"name": "Acceleration","type": "Q"} 127 | }, 128 | "data": {"url": "data/cars.json"} 129 | }; 130 | 131 | // AREA 132 | 133 | f.area = {}; 134 | 135 | f.area['x,y'] = { 136 | "marktype": "area", 137 | "encoding": { 138 | "x": {"name": "Displacement","type": "Q"}, 139 | "y": {"name": "Acceleration","type": "Q"} 140 | }, 141 | "data": {"url": "data/cars.json"} 142 | }; 143 | 144 | f.area['x,y,stroke'] = { 145 | "marktype": "area", 146 | "encoding": { 147 | "x": {"name": "Displacement","type": "Q"}, 148 | "y": {"name": "Acceleration","type": "Q"}, 149 | "color": {"name": "Miles_per_Gallon","type": "Q"} 150 | }, 151 | "data": {"url": "data/cars.json"} 152 | }; 153 | 154 | module.exports = f; -------------------------------------------------------------------------------- /src/compiler/compiler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var summary = module.exports = require('datalib/src/stats').summary; 4 | 5 | require('../globals'); 6 | 7 | var compiler = module.exports = {}; 8 | 9 | var Encoding = require('../Encoding'), 10 | axis = compiler.axis = require('./axis'), 11 | legend = compiler.legend = require('./legend'), 12 | marks = compiler.marks = require('./marks'), 13 | scale = compiler.scale = require('./scale'); 14 | 15 | compiler.data = require('./data'); 16 | compiler.facet = require('./facet'); 17 | compiler.group = require('./group'); 18 | compiler.layout = require('./layout'); 19 | compiler.sort = require('./sort'); 20 | compiler.stack = require('./stack'); 21 | compiler.style = require('./style'); 22 | compiler.subfacet = require('./subfacet'); 23 | compiler.time = require('./time'); 24 | 25 | compiler.compile = function (spec, stats, theme) { 26 | return compiler.compileEncoding(Encoding.fromSpec(spec, theme), stats); 27 | }; 28 | 29 | compiler.shorthand = function (shorthand, stats, config, theme) { 30 | return compiler.compileEncoding(Encoding.fromShorthand(shorthand, config, theme), stats); 31 | }; 32 | 33 | 34 | compiler.compileEncoding = function (encoding, stats) { 35 | // no need to pass stats if you pass in the data 36 | if (!stats && encoding.hasValues()) { 37 | stats = summary(encoding.data().values).reduce(function(s, p) { 38 | s[p.field] = p; 39 | return s; 40 | }, {}); 41 | } 42 | 43 | var layout = compiler.layout(encoding, stats); 44 | 45 | var spec = { 46 | width: layout.width, 47 | height: layout.height, 48 | padding: 'auto', 49 | data: compiler.data(encoding), 50 | // global scales contains only time unit scales 51 | scales: compiler.time.scales(encoding) 52 | }; 53 | 54 | // FIXME remove compiler.sort after migrating to vega 2. 55 | spec.data = compiler.sort(spec.data, encoding, stats); // append new data 56 | 57 | // marks 58 | 59 | // TODO this line is temporary and should be refactored 60 | spec.marks = [compiler.group.def('cell', { 61 | width: layout.cellWidth ? {value: layout.cellWidth} : undefined, 62 | height: layout.cellHeight ? {value: layout.cellHeight} : undefined 63 | })]; 64 | 65 | var style = compiler.style(encoding, stats), 66 | group = spec.marks[0], 67 | mdefs = marks.def(encoding, layout, style, stats), 68 | mdef = mdefs[mdefs.length - 1]; // TODO: remove this dirty hack by refactoring the whole flow 69 | 70 | for (var i = 0; i < mdefs.length; i++) { 71 | group.marks.push(mdefs[i]); 72 | } 73 | 74 | var lineType = marks[encoding.marktype()].line; 75 | 76 | // handle subfacets 77 | 78 | var details = encoding.details(), 79 | stack = encoding.isAggregate() && details.length > 0 && compiler.stack(spec.data, encoding, mdef); // modify spec.data, mdef.{from,properties} 80 | 81 | if (details.length > 0 && (stack || lineType)) { 82 | //subfacet to group stack / line together in one group 83 | compiler.subfacet(group, mdef, details, stack, encoding); 84 | } 85 | 86 | // auto-sort line/area values 87 | if (lineType && encoding.config('autoSortLine')) { 88 | var f = (encoding.isMeasure(X) && encoding.isDimension(Y)) ? Y : X; 89 | if (!mdef.from) mdef.from = {}; 90 | // TODO: why - ? 91 | mdef.from.transform = [{type: 'sort', by: '-' + encoding.fieldRef(f)}]; 92 | } 93 | 94 | // get a flattened list of all scale names that are used in the vl spec 95 | var singleScaleNames = [].concat.apply([], mdefs.map(function(markProps) { 96 | return scale.names(markProps.properties.update); 97 | })); 98 | 99 | // Small Multiples 100 | if (encoding.has(ROW) || encoding.has(COL)) { 101 | spec = compiler.facet(group, encoding, layout, spec, singleScaleNames, stack, stats); 102 | spec.legends = legend.defs(encoding, style); 103 | } else { 104 | group.scales = scale.defs(singleScaleNames, encoding, layout, stats, {stack: stack}); 105 | 106 | group.axes = []; 107 | if (encoding.has(X)) group.axes.push(axis.def(X, encoding, layout, stats)); 108 | if (encoding.has(Y)) group.axes.push(axis.def(Y, encoding, layout, stats)); 109 | 110 | group.legends = legend.defs(encoding, style); 111 | } 112 | 113 | 114 | 115 | return spec; 116 | }; 117 | 118 | -------------------------------------------------------------------------------- /src/compiler/time.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('../util'), 4 | d3_time_format = require('d3-time-format'); 5 | 6 | var time = module.exports = {}; 7 | 8 | var LONG_DATE = new Date(2014, 8, 17); 9 | 10 | time.cardinality = function(field, stats, filterNull, type) { 11 | var timeUnit = field.timeUnit; 12 | switch (timeUnit) { 13 | case 'seconds': return 60; 14 | case 'minutes': return 60; 15 | case 'hours': return 24; 16 | case 'day': return 7; 17 | case 'date': return 31; 18 | case 'month': return 12; 19 | case 'year': 20 | var stat = stats[field.name], 21 | yearstat = stats['year_'+field.name]; 22 | 23 | if (!yearstat) { return null; } 24 | 25 | return yearstat.distinct - 26 | (stat.nulls > 0 && filterNull[type] ? 1 : 0); 27 | } 28 | 29 | return null; 30 | }; 31 | 32 | time.formula = function(timeUnit, fieldRef) { 33 | // TODO(kanitw): add formula to other time format 34 | var fn = 'utc' + timeUnit; 35 | return fn + '(' + fieldRef + ')'; 36 | }; 37 | 38 | time.maxLength = function(timeUnit, encoding) { 39 | switch (timeUnit) { 40 | case 'seconds': 41 | case 'minutes': 42 | case 'hours': 43 | case 'date': 44 | return 2; 45 | case 'month': 46 | case 'day': 47 | var range = time.range(timeUnit, encoding); 48 | if (range) { 49 | // return the longest name in the range 50 | return Math.max.apply(null, range.map(function(r) {return r.length;})); 51 | } 52 | return 2; 53 | case 'year': 54 | return 4; //'1998' 55 | } 56 | // no time unit 57 | var timeFormat = encoding.config('timeFormat'); 58 | return d3_time_format.utcFormat(timeFormat)(LONG_DATE).length; 59 | }; 60 | 61 | time.range = function(timeUnit, encoding) { 62 | var labelLength = encoding.config('timeScaleLabelLength'), 63 | scaleLabel; 64 | switch (timeUnit) { 65 | case 'day': 66 | scaleLabel = encoding.config('dayScaleLabel'); 67 | break; 68 | case 'month': 69 | scaleLabel = encoding.config('monthScaleLabel'); 70 | break; 71 | } 72 | if (scaleLabel) { 73 | return labelLength ? scaleLabel.map( 74 | function(s) { return s.substr(0, labelLength);} 75 | ) : scaleLabel; 76 | } 77 | return; 78 | }; 79 | 80 | 81 | /** 82 | * @param {Object} encoding 83 | * @return {Array} scales for time unit names 84 | */ 85 | time.scales = function(encoding) { 86 | var scales = encoding.reduce(function(scales, field) { 87 | var timeUnit = field.timeUnit; 88 | if (field.type === T && timeUnit && !scales[timeUnit]) { 89 | var scale = time.scale.def(field.timeUnit, encoding); 90 | if (scale) scales[timeUnit] = scale; 91 | } 92 | return scales; 93 | }, {}); 94 | 95 | return util.vals(scales); 96 | }; 97 | 98 | 99 | time.scale = {}; 100 | 101 | /** append custom time scales for axis label */ 102 | time.scale.def = function(timeUnit, encoding) { 103 | var range = time.range(timeUnit, encoding); 104 | 105 | if (range) { 106 | return { 107 | name: 'time-'+timeUnit, 108 | type: 'ordinal', 109 | domain: time.scale.domain(timeUnit), 110 | range: range 111 | }; 112 | } 113 | return null; 114 | }; 115 | 116 | time.isOrdinalFn = function(timeUnit) { 117 | switch (timeUnit) { 118 | case 'seconds': 119 | case 'minutes': 120 | case 'hours': 121 | case 'day': 122 | case 'date': 123 | case 'month': 124 | return true; 125 | } 126 | return false; 127 | }; 128 | 129 | time.scale.type = function(timeUnit, name) { 130 | if (name === COLOR) { 131 | return 'linear'; // time has order, so use interpolated ordinal color scale. 132 | } 133 | 134 | return time.isOrdinalFn(timeUnit) || name === COL || name === ROW ? 'ordinal' : 'linear'; 135 | }; 136 | 137 | time.scale.domain = function(timeUnit, name) { 138 | var isColor = name === COLOR; 139 | switch (timeUnit) { 140 | case 'seconds': 141 | case 'minutes': return isColor ? [0,59] : util.range(0, 60); 142 | case 'hours': return isColor ? [0,23] : util.range(0, 24); 143 | case 'day': return isColor ? [0,6] : util.range(0, 7); 144 | case 'date': return isColor ? [1,31] : util.range(1, 32); 145 | case 'month': return isColor ? [0,11] : util.range(0, 12); 146 | } 147 | return null; 148 | }; 149 | 150 | /** whether a particular time function has custom scale for labels implemented in time.scale */ 151 | time.hasScale = function(timeUnit) { 152 | switch (timeUnit) { 153 | case 'day': 154 | case 'month': 155 | return true; 156 | } 157 | return false; 158 | }; 159 | -------------------------------------------------------------------------------- /lib/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://json-schema.org/draft-04/schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "positiveInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "positiveIntegerDefault0": { 16 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 17 | }, 18 | "simpleTypes": { 19 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 20 | }, 21 | "stringArray": { 22 | "type": "array", 23 | "items": { "type": "string" }, 24 | "minItems": 1, 25 | "uniqueItems": true 26 | } 27 | }, 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "string", 32 | "format": "uri" 33 | }, 34 | "$schema": { 35 | "type": "string", 36 | "format": "uri" 37 | }, 38 | "title": { 39 | "type": "string" 40 | }, 41 | "description": { 42 | "type": "string" 43 | }, 44 | "default": {}, 45 | "multipleOf": { 46 | "type": "number", 47 | "minimum": 0, 48 | "exclusiveMinimum": true 49 | }, 50 | "maximum": { 51 | "type": "number" 52 | }, 53 | "exclusiveMaximum": { 54 | "type": "boolean", 55 | "default": false 56 | }, 57 | "minimum": { 58 | "type": "number" 59 | }, 60 | "exclusiveMinimum": { 61 | "type": "boolean", 62 | "default": false 63 | }, 64 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 65 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 66 | "pattern": { 67 | "type": "string", 68 | "format": "regex" 69 | }, 70 | "additionalItems": { 71 | "anyOf": [ 72 | { "type": "boolean" }, 73 | { "$ref": "#" } 74 | ], 75 | "default": {} 76 | }, 77 | "items": { 78 | "anyOf": [ 79 | { "$ref": "#" }, 80 | { "$ref": "#/definitions/schemaArray" } 81 | ], 82 | "default": {} 83 | }, 84 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 85 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 86 | "uniqueItems": { 87 | "type": "boolean", 88 | "default": false 89 | }, 90 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 91 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 92 | "required": { "$ref": "#/definitions/stringArray" }, 93 | "additionalProperties": { 94 | "anyOf": [ 95 | { "type": "boolean" }, 96 | { "$ref": "#" } 97 | ], 98 | "default": {} 99 | }, 100 | "definitions": { 101 | "type": "object", 102 | "additionalProperties": { "$ref": "#" }, 103 | "default": {} 104 | }, 105 | "properties": { 106 | "type": "object", 107 | "additionalProperties": { "$ref": "#" }, 108 | "default": {} 109 | }, 110 | "patternProperties": { 111 | "type": "object", 112 | "additionalProperties": { "$ref": "#" }, 113 | "default": {} 114 | }, 115 | "dependencies": { 116 | "type": "object", 117 | "additionalProperties": { 118 | "anyOf": [ 119 | { "$ref": "#" }, 120 | { "$ref": "#/definitions/stringArray" } 121 | ] 122 | } 123 | }, 124 | "enum": { 125 | "type": "array", 126 | "minItems": 1, 127 | "uniqueItems": true 128 | }, 129 | "type": { 130 | "anyOf": [ 131 | { "$ref": "#/definitions/simpleTypes" }, 132 | { 133 | "type": "array", 134 | "items": { "$ref": "#/definitions/simpleTypes" }, 135 | "minItems": 1, 136 | "uniqueItems": true 137 | } 138 | ] 139 | }, 140 | "allOf": { "$ref": "#/definitions/schemaArray" }, 141 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 142 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 143 | "not": { "$ref": "#" } 144 | }, 145 | "dependencies": { 146 | "exclusiveMaximum": [ "maximum" ], 147 | "exclusiveMinimum": [ "minimum" ] 148 | }, 149 | "default": {} 150 | } 151 | -------------------------------------------------------------------------------- /bin/shorthand2vg/birdstrikes.bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O.row-When__Phase_of_flight-O.col-Wildlife__Size-O.json: -------------------------------------------------------------------------------- 1 | { 2 | "width": 640, 3 | "height": 1117.2, 4 | "padding": "auto", 5 | "data": [ 6 | { 7 | "name": "table", 8 | "format": {"type": "json","parse": {"Cost__Total_$": "number"}}, 9 | "url": "data/birdstrikes.json", 10 | "transform": [ 11 | { 12 | "type": "aggregate", 13 | "groupby": [ 14 | "data.Effect__Amount_of_damage", 15 | "data.When__Phase_of_flight", 16 | "data.Wildlife__Size" 17 | ], 18 | "fields": [{"op": "sum","field": "data.Cost__Total_$"}] 19 | } 20 | ] 21 | } 22 | ], 23 | "marks": [ 24 | { 25 | "_name": "cell", 26 | "type": "group", 27 | "from": { 28 | "data": "table", 29 | "transform": [ 30 | { 31 | "type": "facet", 32 | "keys": ["data.When__Phase_of_flight","data.Wildlife__Size"] 33 | } 34 | ] 35 | }, 36 | "properties": { 37 | "enter": { 38 | "x": {"scale": "col","field": "keys.1","offset": 80}, 39 | "y": {"scale": "row","field": "keys.0"}, 40 | "width": {"value": 200}, 41 | "height": {"value": 147}, 42 | "fill": {"value": "#fdfdfd"} 43 | } 44 | }, 45 | "marks": [ 46 | { 47 | "type": "rect", 48 | "properties": { 49 | "enter": { 50 | "x": {"scale": "x","field": "data.sum_Cost__Total_$"}, 51 | "x2": {"scale": "x","value": 0}, 52 | "yc": {"scale": "y","field": "data.Effect__Amount_of_damage"}, 53 | "height": {"value": 21,"offset": -1}, 54 | "fill": {"value": "steelblue"} 55 | }, 56 | "update": { 57 | "x": {"scale": "x","field": "data.sum_Cost__Total_$"}, 58 | "x2": {"scale": "x","value": 0}, 59 | "yc": {"scale": "y","field": "data.Effect__Amount_of_damage"}, 60 | "height": {"value": 21,"offset": -1}, 61 | "fill": {"value": "steelblue"} 62 | } 63 | } 64 | } 65 | ] 66 | }, 67 | { 68 | "_name": "x-axes", 69 | "type": "group", 70 | "from": { 71 | "data": "table", 72 | "transform": [{"type": "facet","keys": ["data.Wildlife__Size"]}] 73 | }, 74 | "properties": { 75 | "enter": { 76 | "x": {"scale": "col","field": "keys.0","offset": 80}, 77 | "width": {"value": 200}, 78 | "height": {"group": "height"} 79 | } 80 | }, 81 | "axes": [{"type": "x","scale": "x","ticks": 3}], 82 | "marks": [] 83 | }, 84 | { 85 | "_name": "y-axes", 86 | "type": "group", 87 | "from": { 88 | "data": "table", 89 | "transform": [{"type": "facet","keys": ["data.When__Phase_of_flight"]}] 90 | }, 91 | "properties": { 92 | "enter": { 93 | "x": {"value": 80}, 94 | "y": {"scale": "row","field": "keys.0"}, 95 | "width": {"group": "width"}, 96 | "height": {"value": 147} 97 | } 98 | }, 99 | "axes": [{"type": "y","scale": "y","ticks": 3}], 100 | "marks": [] 101 | } 102 | ], 103 | "axes": [ 104 | { 105 | "type": "y", 106 | "scale": "row", 107 | "ticks": 3, 108 | "properties": { 109 | "ticks": {"opacity": {"value": 0}}, 110 | "majorTicks": {"opacity": {"value": 0}}, 111 | "axis": {"opacity": {"value": 0}} 112 | } 113 | }, 114 | { 115 | "type": "x", 116 | "scale": "col", 117 | "ticks": 3, 118 | "properties": { 119 | "ticks": {"opacity": {"value": 0}}, 120 | "majorTicks": {"opacity": {"value": 0}}, 121 | "axis": {"opacity": {"value": 0}} 122 | }, 123 | "offset": [80,0], 124 | "orient": "top" 125 | } 126 | ], 127 | "scales": [ 128 | { 129 | "name": "col", 130 | "type": "ordinal", 131 | "domain": {"data": "table","field": "data.Wildlife__Size"}, 132 | "sort": true, 133 | "bandWidth": 200, 134 | "round": true, 135 | "nice": true, 136 | "padding": 0.1, 137 | "outerPadding": 0 138 | }, 139 | { 140 | "name": "row", 141 | "type": "ordinal", 142 | "domain": {"data": "table","field": "data.When__Phase_of_flight"}, 143 | "sort": true, 144 | "bandWidth": 147, 145 | "round": true, 146 | "nice": true, 147 | "padding": 0.1, 148 | "outerPadding": 0 149 | }, 150 | { 151 | "name": "x", 152 | "type": "linear", 153 | "domain": {"data": "table","field": "data.sum_Cost__Total_$"}, 154 | "range": [0,200], 155 | "zero": true, 156 | "reverse": false, 157 | "round": true, 158 | "nice": true 159 | }, 160 | { 161 | "name": "y", 162 | "type": "ordinal", 163 | "domain": {"data": "table","field": "data.Effect__Amount_of_damage"}, 164 | "sort": true, 165 | "bandWidth": 21, 166 | "round": true, 167 | "nice": true, 168 | "points": true, 169 | "padding": 1 170 | } 171 | ] 172 | } -------------------------------------------------------------------------------- /src/field.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // utility for field 4 | 5 | require('./globals'); 6 | 7 | var consts = require('./consts'), 8 | c = consts.shorthand, 9 | time = require('./compiler/time'), 10 | util = require('./util'), 11 | schema = require('./schema/schema'); 12 | 13 | var vlfield = module.exports = {}; 14 | 15 | /** 16 | * @param field 17 | * @param opt 18 | * opt.nofn -- exclude bin, aggregate, timeUnit 19 | * opt.data - include 'data.' 20 | * opt.d - include 'd.' 21 | * opt.fn - replace fn with custom function prefix 22 | * opt.prefn - prepend fn with custom function prefix 23 | 24 | * @return {[type]} [description] 25 | */ 26 | vlfield.fieldRef = function(field, opt) { 27 | opt = opt || {}; 28 | 29 | var f = (opt.d ? 'd.' : '') + 30 | (opt.data ? 'data.' : '') + 31 | (opt.prefn || ''), 32 | nofn = opt.nofn || opt.fn, 33 | name = field.name; 34 | 35 | if (vlfield.isCount(field)) { 36 | return f + 'count'; 37 | } else if (!nofn && field.bin) { 38 | return f + 'bin_' + name; 39 | } else if (!nofn && field.aggregate) { 40 | return f + field.aggregate + '_' + name; 41 | } else if (!nofn && field.timeUnit) { 42 | return f + field.timeUnit + '_' + name; 43 | } else if (opt.fn) { 44 | return f + opt.fn + '_' + name; 45 | } else { 46 | return f + name; 47 | } 48 | }; 49 | 50 | vlfield.shorthand = function(f) { 51 | var c = consts.shorthand; 52 | return (f.aggregate ? f.aggregate + c.func : '') + 53 | (f.timeUnit ? f.timeUnit + c.func : '') + 54 | (f.bin ? 'bin' + c.func : '') + 55 | (f.name || '') + c.type + f.type; 56 | }; 57 | 58 | vlfield.shorthands = function(fields, delim) { 59 | delim = delim || c.delim; 60 | return fields.map(vlfield.shorthand).join(delim); 61 | }; 62 | 63 | vlfield.fromShorthand = function(shorthand) { 64 | var split = shorthand.split(c.type), i; 65 | var o = { 66 | name: split[0].trim(), 67 | type: split[1].trim() 68 | }; 69 | 70 | // check aggregate type 71 | for (i in schema.aggregate.enum) { 72 | var a = schema.aggregate.enum[i]; 73 | if (o.name.indexOf(a + '_') === 0) { 74 | o.name = o.name.substr(a.length + 1); 75 | if (a == 'count' && o.name.length === 0) o.name = '*'; 76 | o.aggregate = a; 77 | break; 78 | } 79 | } 80 | 81 | // check time timeUnit 82 | for (i in schema.timefns) { 83 | var tu = schema.timefns[i]; 84 | if (o.name && o.name.indexOf(tu + '_') === 0) { 85 | o.name = o.name.substr(o.length + 1); 86 | o.timeUnit = tu; 87 | break; 88 | } 89 | } 90 | 91 | // check bin 92 | if (o.name && o.name.indexOf('bin_') === 0) { 93 | o.name = o.name.substr(4); 94 | o.bin = true; 95 | } 96 | 97 | return o; 98 | }; 99 | 100 | var isType = vlfield.isType = function (fieldDef, type) { 101 | return fieldDef.type === type; 102 | }; 103 | 104 | var isTypes = vlfield.isTypes = function (fieldDef, types) { 105 | for (var t=0; t 0 && filterNull[type] ? 1 : 0); 176 | }; 177 | -------------------------------------------------------------------------------- /src/compiler/data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../globals'); 4 | 5 | module.exports = data; 6 | 7 | var vlfield = require('../field'), 8 | util = require('../util'), 9 | time = require('./time'); 10 | 11 | function data(encoding) { 12 | var def = [data.raw(encoding)]; 13 | 14 | var aggregate = data.aggregate(encoding); 15 | if (aggregate) def.push(data.aggregate(encoding)); 16 | 17 | // TODO add "having" filter here 18 | 19 | // append non-positive filter at the end for the data table 20 | data.filterNonPositive(def[def.length - 1], encoding); 21 | 22 | return def; 23 | } 24 | 25 | data.raw = function(encoding) { 26 | var raw = {name: RAW}; 27 | 28 | // Data source (url or inline) 29 | if (encoding.hasValues()) { 30 | raw.values = encoding.data().values; 31 | } else { 32 | raw.url = encoding.data().url; 33 | raw.format = {type: encoding.data().formatType}; 34 | } 35 | 36 | // Set format.parse if needed 37 | var parse = data.raw.formatParse(encoding); 38 | if (parse) { 39 | raw.format = raw.format || {}; 40 | raw.format.parse = parse; 41 | } 42 | 43 | raw.transform = data.raw.transform(encoding); 44 | return raw; 45 | }; 46 | 47 | data.raw.formatParse = function(encoding) { 48 | var parse; 49 | 50 | encoding.forEach(function(field) { 51 | if (field.type == T) { 52 | parse = parse || {}; 53 | parse[field.name] = 'date'; 54 | } else if (field.type == Q) { 55 | if (vlfield.isCount(field)) return; 56 | parse = parse || {}; 57 | parse[field.name] = 'number'; 58 | } 59 | }); 60 | 61 | return parse; 62 | }; 63 | 64 | data.raw.transform = function(encoding) { 65 | // time and bin should come before filter so we can filter by time and bin 66 | return data.raw.transform.time(encoding).concat( 67 | data.raw.transform.bin(encoding), 68 | data.raw.transform.filter(encoding) 69 | ); 70 | }; 71 | 72 | var BINARY = { 73 | '>': true, 74 | '>=': true, 75 | '=': true, 76 | '!=': true, 77 | '<': true, 78 | '<=': true 79 | }; 80 | 81 | data.raw.transform.time = function(encoding) { 82 | return encoding.reduce(function(transform, field, encType) { 83 | if (field.type === T && field.timeUnit) { 84 | transform.push({ 85 | type: 'formula', 86 | field: encoding.fieldRef(encType), 87 | expr: time.formula(field.timeUnit, encoding.fieldRef(encType, {nofn: true, d: true})) 88 | }); 89 | } 90 | return transform; 91 | }, []); 92 | }; 93 | 94 | data.raw.transform.bin = function(encoding) { 95 | return encoding.reduce(function(transform, field, encType) { 96 | if (encoding.bin(encType)) { 97 | transform.push({ 98 | type: 'bin', 99 | field: encoding.fieldRef(encType, {nofn: true}), 100 | output: encoding.fieldRef(encType), 101 | maxbins: encoding.bin(encType).maxbins 102 | }); 103 | } 104 | return transform; 105 | }, []); 106 | }; 107 | 108 | data.raw.transform.filter = function(encoding) { 109 | var filters = encoding.filter().reduce(function(f, filter) { 110 | var condition = ''; 111 | var operator = filter.operator; 112 | var operands = filter.operands; 113 | 114 | var d = 'd.' + (encoding._vega2 ? '' : 'data.'); 115 | 116 | if (BINARY[operator]) { 117 | // expects a field and a value 118 | if (operator === '=') { 119 | operator = '=='; 120 | } 121 | 122 | var op1 = operands[0]; 123 | var op2 = operands[1]; 124 | condition = d + op1 + ' ' + operator + ' ' + op2; 125 | } else if (operator === 'notNull') { 126 | // expects a number of fields 127 | for (var j=0; j 0) { 170 | return { 171 | name: AGGREGATE, 172 | source: RAW, 173 | transform: [{ 174 | type: 'aggregate', 175 | groupby: dims, 176 | fields: meas 177 | }] 178 | }; 179 | } 180 | 181 | return null; 182 | }; 183 | 184 | data.filterNonPositive = function(dataTable, encoding) { 185 | encoding.forEach(function(field, encType) { 186 | if (encoding.scale(encType).type === 'log') { 187 | dataTable.transform.push({ 188 | type: 'filter', 189 | test: encoding.fieldRef(encType, {d: 1}) + ' > 0' 190 | }); 191 | } 192 | }); 193 | }; 194 | -------------------------------------------------------------------------------- /src/compiler/layout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../globals'); 4 | 5 | var util = require('../util'), 6 | setter = util.setter, 7 | time = require('./time'), 8 | d3_format = require('d3-format'); 9 | 10 | module.exports = vllayout; 11 | 12 | function vllayout(encoding, stats) { 13 | var layout = box(encoding, stats); 14 | layout = offset(encoding, stats, layout); 15 | return layout; 16 | } 17 | 18 | /* 19 | HACK to set chart size 20 | NOTE: this fails for plots driven by derived values (e.g., aggregates) 21 | One solution is to update Vega to support auto-sizing 22 | In the meantime, auto-padding (mostly) does the trick 23 | */ 24 | function box(encoding, stats) { 25 | var hasRow = encoding.has(ROW), 26 | hasCol = encoding.has(COL), 27 | hasX = encoding.has(X), 28 | hasY = encoding.has(Y), 29 | marktype = encoding.marktype(); 30 | 31 | // FIXME/HACK we need to take filter into account 32 | var xCardinality = hasX && encoding.isDimension(X) ? encoding.cardinality(X, stats) : 1, 33 | yCardinality = hasY && encoding.isDimension(Y) ? encoding.cardinality(Y, stats) : 1; 34 | 35 | var useSmallBand = xCardinality > encoding.config('largeBandMaxCardinality') || 36 | yCardinality > encoding.config('largeBandMaxCardinality'); 37 | 38 | var cellWidth, cellHeight, cellPadding = encoding.config('cellPadding'); 39 | 40 | // set cellWidth 41 | if (hasX) { 42 | if (encoding.isOrdinalScale(X)) { 43 | // for ordinal, hasCol or not doesn't matter -- we scale based on cardinality 44 | cellWidth = (xCardinality + encoding.field(X).band.padding) * encoding.bandSize(X, useSmallBand); 45 | } else { 46 | cellWidth = hasCol || hasRow ? encoding.field(COL).width : encoding.config('singleWidth'); 47 | } 48 | } else { 49 | if (marktype === TEXT) { 50 | cellWidth = encoding.config('textCellWidth'); 51 | } else { 52 | cellWidth = encoding.bandSize(X); 53 | } 54 | } 55 | 56 | // set cellHeight 57 | if (hasY) { 58 | if (encoding.isOrdinalScale(Y)) { 59 | // for ordinal, hasCol or not doesn't matter -- we scale based on cardinality 60 | cellHeight = (yCardinality + encoding.field(Y).band.padding) * encoding.bandSize(Y, useSmallBand); 61 | } else { 62 | cellHeight = hasCol || hasRow ? encoding.field(ROW).height : encoding.config('singleHeight'); 63 | } 64 | } else { 65 | cellHeight = encoding.bandSize(Y); 66 | } 67 | 68 | // Cell bands use rangeBands(). There are n-1 padding. Outerpadding = 0 for cells 69 | 70 | var width = cellWidth, height = cellHeight; 71 | if (hasCol) { 72 | var colCardinality = encoding.cardinality(COL, stats); 73 | width = cellWidth * ((1 + cellPadding) * (colCardinality - 1) + 1); 74 | } 75 | if (hasRow) { 76 | var rowCardinality = encoding.cardinality(ROW, stats); 77 | height = cellHeight * ((1 + cellPadding) * (rowCardinality - 1) + 1); 78 | } 79 | 80 | return { 81 | // width and height of the whole cell 82 | cellWidth: cellWidth, 83 | cellHeight: cellHeight, 84 | cellPadding: cellPadding, 85 | // width and height of the chart 86 | width: width, 87 | height: height, 88 | // information about x and y, such as band size 89 | x: {useSmallBand: useSmallBand}, 90 | y: {useSmallBand: useSmallBand} 91 | }; 92 | } 93 | 94 | 95 | // FIXME fieldStats.max isn't always the longest 96 | function getMaxNumberLength(encoding, et, fieldStats) { 97 | var format = encoding.numberFormat(et, fieldStats); 98 | 99 | return d3_format.format(format)(fieldStats.max).length; 100 | } 101 | 102 | function getMaxLength(encoding, stats, et) { 103 | var field = encoding.field(et), 104 | fieldStats = stats[field.name]; 105 | 106 | if (field.bin) { 107 | // TODO once bin support range, need to update this 108 | return getMaxNumberLength(encoding, et, fieldStats); 109 | } if (encoding.isType(et, Q)) { 110 | return getMaxNumberLength(encoding, et, fieldStats); 111 | } else if (encoding.isType(et, T)) { 112 | return time.maxLength(encoding.field(et).timeUnit, encoding); 113 | } else if (encoding.isTypes(et, [N, O])) { 114 | if(fieldStats.type === 'number') { 115 | return getMaxNumberLength(encoding, et, fieldStats); 116 | } else { 117 | return Math.min(fieldStats.max, encoding.axis(et).maxLabelLength || Infinity); 118 | } 119 | } 120 | } 121 | 122 | function offset(encoding, stats, layout) { 123 | [X, Y].forEach(function (et) { 124 | // TODO(kanitw): Jul 19, 2015 - create a set of visual test for extraOffset 125 | var extraOffset = et === X ? 20 : 22, 126 | maxLength; 127 | if (encoding.isDimension(et) || encoding.isType(et, T)) { 128 | maxLength = getMaxLength(encoding, stats, et); 129 | } else if ( 130 | // TODO once we have #512 (allow using inferred type) 131 | // Need to adjust condition here. 132 | encoding.isType(et, Q) || 133 | encoding.aggregate(et) === 'count' 134 | ) { 135 | if ( 136 | et===Y 137 | // || (et===X && false) 138 | // FIXME determine when X would rotate, but should move this to axis.js first #506 139 | ) { 140 | maxLength = getMaxLength(encoding, stats, et); 141 | } 142 | } else { 143 | // nothing 144 | } 145 | 146 | if (maxLength) { 147 | setter(layout,[et, 'axisTitleOffset'], encoding.config('characterWidth') * maxLength + extraOffset); 148 | } else { 149 | // if no max length (no rotation case), use maxLength = 3 150 | setter(layout,[et, 'axisTitleOffset'], encoding.config('characterWidth') * 3 + extraOffset); 151 | } 152 | 153 | }); 154 | return layout; 155 | } 156 | -------------------------------------------------------------------------------- /gallery/gallery.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*global vl, d3, vg, angular, alert */ 4 | 5 | var EXAMPLES = [ 6 | { 7 | title: 'Simple Bar Chart', 8 | description: 'A simple bar chart with embedded data.', 9 | spec: { 10 | data: { 11 | values: [ 12 | {'x':'A', 'y':28}, {'x':'B', 'y':55}, {'x':'C', 'y':43}, 13 | {'x':'D', 'y':91}, {'x':'E', 'y':81}, {'x':'F', 'y':53}, 14 | {'x':'G', 'y':19}, {'x':'H', 'y':87}, {'x':'I', 'y':52} 15 | ] 16 | }, 17 | marktype: 'bar', 18 | encoding: { 19 | y: {type: 'Q', name: 'y'}, 20 | x: {type: 'O', name: 'x'} 21 | } 22 | } 23 | },{ 24 | title: 'Horse power and miles per gallon', 25 | description: 'A scatter plot.', 26 | spec: { 27 | marktype: 'point', 28 | encoding: { 29 | x: {'name': 'Horsepower','type': 'Q'}, 30 | y: {'name': 'Miles_per_Gallon','type': 'Q'} 31 | }, 32 | data: {'url': 'data/cars.json'} 33 | } 34 | },{ 35 | title: 'Horse power over time', 36 | spec: { 37 | marktype: 'line', 38 | encoding: { 39 | x: {'name': 'Year','type': 'T','timeUnit': 'year'}, 40 | y: {'name': 'Horsepower','type': 'Q','aggregate': 'avg'} 41 | }, 42 | data: {'url': 'data/cars.json'} 43 | } 44 | },{ 45 | title: 'Horse power histogram', 46 | description: 'Simple histogram with bars broken down by the number of cylinders. Also has a legend.', 47 | spec: { 48 | marktype: 'bar', 49 | encoding: { 50 | x: {'bin': {'maxbins': 15},'name': 'Horsepower','type': 'Q'}, 51 | y: {'name': '*','type': 'Q','aggregate': 'count'}, 52 | color: {'name': 'Cylinders','type': 'N'} 53 | }, 54 | data: {'url': 'data/cars.json'} 55 | } 56 | },{ 57 | title: 'Barleys', 58 | spec: { 59 | data: {url: 'data/barley.json'}, 60 | marktype: 'point', 61 | encoding: { 62 | x: {type: 'Q',name: 'yield',aggregate: 'avg'}, 63 | y: { 64 | sort: [{name: 'yield',aggregate: 'avg',reverse: false}], 65 | type: 'O', 66 | name: 'variety' 67 | }, 68 | row: {type: 'O',name: 'site'}, 69 | color: {type: 'N',name: 'year'} 70 | } 71 | } 72 | },{ 73 | title: 'Binned plots', 74 | spec: { 75 | 'marktype': 'point', 76 | 'encoding': { 77 | 'x': {'bin': true,'name': 'Displacement','type': 'Q'}, 78 | 'y': {'bin': true,'name': 'Miles_per_Gallon','type': 'Q'}, 79 | 'size': { 80 | 'name': '*', 81 | 'aggregate': 'count', 82 | 'type': 'Q', 83 | 'displayName': 'Number of Records' 84 | } 85 | }, 86 | 'data': {'url': 'data/cars.json'} 87 | } 88 | },{ 89 | title: 'Small Multiples', 90 | spec: { 91 | 'marktype': 'point', 92 | 'encoding': { 93 | 'x': {'name': 'Worldwide_Gross','type': 'Q'}, 94 | 'y': {'name': 'US_DVD_Sales','type': 'Q'}, 95 | 'col': {'axis': {'maxLabelLength': 25},'name': 'MPAA_Rating','type': 'O'} 96 | }, 97 | 'data': {'url': 'data/movies.json'} 98 | } 99 | },{ 100 | title: 'Ordinal on Top', 101 | spec: { 102 | 'marktype': 'point', 103 | 'encoding': { 104 | 'x': {'name': 'MPAA_Rating','type': 'N'}, 105 | 'y': {'name': 'Release_Date','type': 'N'} 106 | }, 107 | 'data': {'url': 'data/movies.json'} 108 | } 109 | },{ 110 | title: 'Text Heatmap', 111 | spec: { 112 | 'marktype': 'text', 113 | 'encoding': { 114 | 'row': {'name': 'Origin','type': 'O'}, 115 | 'col': {'axis': {'maxLabelLength': 25},'name': 'Cylinders','type': 'O'}, 116 | 'color': {'name': 'Horsepower','type': 'Q','aggregate': 'avg'}, 117 | 'text': {'name': '*','type': 'Q','aggregate': 'count'} 118 | }, 119 | 'data': {'url': 'data/cars.json'} 120 | } 121 | },{ 122 | title: 'Area chart', 123 | spec: { 124 | 'marktype': 'area', 125 | 'encoding': { 126 | 'x': {'name': 'Year','type': 'T','timeUnit': 'year'}, 127 | 'y': {'name': 'Weight_in_lbs','type': 'Q','aggregate': 'sum'}, 128 | 'color': { 129 | 'scale': {'quantitativeRange': ['#AFC6A3','#09622A']}, 130 | 'name': 'Cylinders', 131 | 'type': 'O' 132 | } 133 | }, 134 | 'data': {'url': 'data/cars.json'} 135 | } 136 | } 137 | ]; 138 | 139 | var app = angular.module('app', []); 140 | 141 | app.controller('GalleryCtrl', function ($scope) { 142 | $scope.visualizations = EXAMPLES; 143 | $scope.vegaVersion = vg.version; 144 | }); 145 | 146 | app.filter('compactJSON', function() { 147 | return function(input) { 148 | return JSON.stringify(input, null, ' ', 80); 149 | }; 150 | }); 151 | 152 | app.directive('vlPlot', function() { 153 | return { 154 | scope: { 155 | vlSpec: '=' 156 | }, 157 | template: '', 158 | link: function(scope, element) { 159 | 160 | var vlElement = element[0]; 161 | 162 | var callback = function(stats) { 163 | var spec = vl.compile(scope.vlSpec, stats); 164 | vg.parse.spec(spec, function(chart) { 165 | var view = chart({el: vlElement, renderer: 'svg'}); 166 | view.update(); 167 | }); 168 | }; 169 | 170 | if (!scope.vlSpec.data.values) { 171 | d3.json(scope.vlSpec.data.url, function(err, data) { 172 | if (err) return alert('Error loading data ' + err.statusText); 173 | var stats = vl.data.stats(data); 174 | callback(stats); 175 | }); 176 | } else { 177 | callback(); 178 | } 179 | } 180 | }; 181 | }); 182 | -------------------------------------------------------------------------------- /test/compiler/axis.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | var axis = require('../../src/compiler/axis'), 6 | Encoding = require('../../src/Encoding'); 7 | 8 | describe('Axis', function() { 9 | var stats = {a: {distinct: 5}, b: {distinct: 32}}, 10 | layout = { 11 | cellWidth: 60, // default characterWidth = 6 12 | cellHeight: 60 13 | }; 14 | 15 | describe('(X) for Time Data', function() { 16 | var fieldName = 'a', 17 | timeUnit = 'month', 18 | encoding = Encoding.fromSpec({ 19 | encoding: { 20 | x: {name: fieldName, type: 'T', timeUnit: timeUnit} 21 | } 22 | }); 23 | var _axis = axis.def('x', encoding, { 24 | width: 200, 25 | height: 200, 26 | cellWidth: 200, 27 | cellHeight: 200, 28 | x: { 29 | axisTitleOffset: 60 30 | }, 31 | y: { 32 | axisTitleOffset: 60 33 | } 34 | }, stats); 35 | 36 | //FIXME decouple the test here 37 | 38 | it('should use custom label', function() { 39 | expect(_axis.properties.labels.text.scale).to.equal('time-'+ timeUnit); 40 | }); 41 | it('should rotate label', function() { 42 | expect(_axis.properties.labels.angle.value).to.equal(270); 43 | }); 44 | }); 45 | 46 | 47 | describe('grid()', function () { 48 | // FIXME(kanitw): Jul 19, 2015 - write test 49 | }); 50 | 51 | describe('hideTicks()', function () { 52 | var def = axis.hideTicks({properties:{}}); 53 | it('should adjust ticks', function () { 54 | expect(def.properties.ticks).to.eql({opacity: {value: 0}}); 55 | }); 56 | it('should adjust majorTicks', function () { 57 | expect(def.properties.majorTicks).to.eql({opacity: {value: 0}}); 58 | }); 59 | it('should adjust axis', function () { 60 | expect(def.properties.axis).to.eql({opacity: {value: 0}}); 61 | }); 62 | }); 63 | 64 | describe('labels.scale()', function () { 65 | // FIXME(kanitw): Jul 19, 2015 - write test 66 | }); 67 | 68 | describe('labels.format()', function () { 69 | // FIXME(kanitw): Jul 19, 2015 - write test 70 | }); 71 | 72 | describe('labels.angle()', function () { 73 | it('should set explicitly specified angle', function () { 74 | var def = axis.labels.angle({}, Encoding.fromSpec({ 75 | encoding: { 76 | x: {name: 'a', type: 'T', axis:{labelAngle: 90}} 77 | } 78 | }), 'x'); 79 | expect(def.properties.labels.angle).to.eql({value: 90}); 80 | }); 81 | }); 82 | 83 | describe('labels.rotate()', function () { 84 | // FIXME(kanitw): Jul 19, 2015 - write test 85 | }); 86 | 87 | describe('orient()', function () { 88 | it('should return specified orient', function () { 89 | var orient = axis.orient('x', Encoding.fromSpec({ 90 | encoding: { 91 | x: {name: 'a', axis:{orient: 'bottom'}} 92 | } 93 | }), stats); 94 | expect(orient).to.eql('bottom'); 95 | }); 96 | 97 | it('should return undefined by default', function () { 98 | var orient = axis.orient('x', Encoding.fromSpec({ 99 | encoding: { 100 | x: {name: 'a'} 101 | } 102 | }), stats); 103 | expect(orient).to.eql(undefined); 104 | }); 105 | 106 | it('should return top for COL', function () { 107 | var orient = axis.orient('col', Encoding.fromSpec({ 108 | encoding: { 109 | x: {name: 'a'}, 110 | col: {name: 'a'} 111 | } 112 | }), stats); 113 | expect(orient).to.eql('top'); 114 | }); 115 | 116 | it('should return top for X with high cardinality, ordinal Y', function () { 117 | var orient = axis.orient('x', Encoding.fromSpec({ 118 | encoding: { 119 | x: {name: 'a'}, 120 | y: {name: 'b', type: 'O'} 121 | } 122 | }), stats); 123 | expect(orient).to.eql('top'); 124 | }); 125 | }); 126 | 127 | describe('title()', function () { 128 | it('should add explicitly specified title', function () { 129 | var def = axis.title({}, 'x', Encoding.fromSpec({ 130 | encoding: { 131 | x: {name: 'a', axis: {title: 'Custom'}} 132 | } 133 | }), stats, layout); 134 | expect(def.title).to.eql('Custom'); 135 | }); 136 | 137 | it('should add return fieldTitle by default', function () { 138 | var encoding = Encoding.fromSpec({ 139 | encoding: { 140 | x: {name: 'a', axis: {titleMaxLength: '3'}} 141 | } 142 | }); 143 | 144 | var def = axis.title({}, 'x', encoding, layout); 145 | expect(def.title).to.eql('a'); 146 | }); 147 | 148 | it('should add return fieldTitle by default', function () { 149 | var encoding = Encoding.fromSpec({ 150 | encoding: { 151 | x: {name: 'a', aggregate: 'sum', axis: {titleMaxLength: '10'}} 152 | } 153 | }); 154 | 155 | var def = axis.title({}, 'x', encoding, layout); 156 | expect(def.title).to.eql('SUM(a)'); 157 | }); 158 | 159 | it('should add return fieldTitle by default and truncate', function () { 160 | var encoding = Encoding.fromSpec({ 161 | encoding: { 162 | x: {name: 'a', aggregate: 'sum', axis: {titleMaxLength: '3'}} 163 | } 164 | }); 165 | 166 | var def = axis.title({}, 'x', encoding, layout); 167 | expect(def.title).to.eql('SU…'); 168 | }); 169 | 170 | 171 | it('should add return fieldTitle by default and truncate', function () { 172 | var encoding = Encoding.fromSpec({ 173 | encoding: { 174 | x: {name: 'abcdefghijkl'} 175 | } 176 | }); 177 | 178 | var def = axis.title({}, 'x', encoding, layout); 179 | expect(def.title).to.eql('abcdefghi…'); 180 | }); 181 | }); 182 | 183 | describe('titleOffset()', function () { 184 | // FIXME(kanitw): Jul 19, 2015 - write test 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /editor/editor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*global location, d3, vl, vg, docCookies, document, $, alert */ 4 | 5 | var DATASETS = [ 6 | { 7 | name: 'Barley', 8 | url: 'data/barley.json' 9 | },{ 10 | name: 'Cars', 11 | url: 'data/cars.json' 12 | },{ 13 | name: 'Crimea', 14 | url: 'data/crimea.json' 15 | },{ 16 | name: 'Driving', 17 | url: 'data/driving.json' 18 | },{ 19 | name: 'Iris', 20 | url: 'data/iris.json' 21 | },{ 22 | name: 'Jobs', 23 | url: 'data/jobs.json' 24 | },{ 25 | name: 'Population', 26 | url: 'data/population.json' 27 | },{ 28 | name: 'Movies', 29 | url: 'data/movies.json' 30 | },{ 31 | name: 'Birdstrikes', 32 | url: 'data/birdstrikes.json' 33 | },{ 34 | name: 'Burtin', 35 | url: 'data/burtin.json' 36 | },{ 37 | name: 'Budget 2016', 38 | url: 'data/budget.json' 39 | },{ 40 | name: 'Climate Normals', 41 | url: 'data/climate.json' 42 | },{ 43 | name: 'Campaigns', 44 | url: 'data/weball26.json' 45 | } 46 | ]; 47 | 48 | var vled = { 49 | version: 0.1, 50 | spec: {} 51 | }; 52 | 53 | function getParams() { 54 | var params = location.search.slice(1); 55 | 56 | // remove trailing slash that chrome adds automatically 57 | if (params[params.length-1] == '/') params = params.substring(0, params.length-1); 58 | 59 | return params.split('&') 60 | .map(function(x) { 61 | // don't gobble up any equals within the query value 62 | var idx = x.indexOf('='); 63 | return [x.slice(0,idx), x.slice(idx+1)]; 64 | }) 65 | .reduce(function(a, b) { 66 | a[b[0]] = b[1]; return a; 67 | }, {}); 68 | } 69 | 70 | vled.format = function() { 71 | var el = d3.select('#vlspec'), 72 | spec = JSON.parse(el.property('value')), 73 | text = JSON.stringify(spec, null, ' ', 60); 74 | el.property('value', text); 75 | }; 76 | 77 | vled.parse = function() { 78 | var spec; 79 | try { 80 | spec = JSON.parse(d3.select('#vlspec').property('value')); 81 | } catch (e) { 82 | console.warn(e); 83 | return; 84 | } 85 | 86 | var datasetIndex; 87 | for (var i = 0; spec.data && i < DATASETS.length; i++) { 88 | if (DATASETS[i].url === spec.data.url) { 89 | datasetIndex = i; 90 | break; 91 | } 92 | } 93 | 94 | var done = function() { 95 | // only add url if data is not provided explicitly 96 | var theme = (spec.data && spec.data.values) ? {} : { 97 | data: { 98 | url: vled.dataset.url 99 | } 100 | }; 101 | vled.loadSpec(spec, theme); 102 | }; 103 | 104 | if (!vled.dataset && !datasetIndex) { 105 | datasetIndex = 0; 106 | } 107 | 108 | if (datasetIndex !== undefined) { 109 | document.getElementById('sel_spec').selectedIndex = datasetIndex; 110 | vled.datasetChanged(DATASETS[datasetIndex], function() { 111 | done(); 112 | }); 113 | } else { 114 | done(); 115 | } 116 | }; 117 | 118 | vled.parseShorthand = function() { 119 | var shorthand = d3.select('#shorthand').property('value'); 120 | 121 | var spec = vl.compile(shorthand, vled.dataset.stats); 122 | d3.select('#vlspec').node().value = JSON.stringify(spec, null, ' ', 60); 123 | vled.parse(); 124 | }; 125 | 126 | vled.loadSpec = function(vlspec, theme) { 127 | var spec = vl.compile(vlspec, vled.dataset.stats, theme); 128 | 129 | d3.select('#shorthand').node().value = vl.toShorthand(vlspec); 130 | d3.select('#vgspec').node().value = JSON.stringify(spec, null, ' ', 60); 131 | 132 | // store spec in cookie for a day 133 | docCookies.setItem('vlspec', JSON.stringify(vlspec), 86400); 134 | 135 | $('textarea').trigger('autosize.resize'); 136 | 137 | vled.vis = null; // DEBUG 138 | vg.parse.spec(spec, function(chart) { 139 | vled.vis = chart({el:'#vis', renderer: 'svg'}); 140 | 141 | vled.vis.update(); 142 | vled.vis.on('mouseover', function(ev, item) { 143 | console.log(item); 144 | }); 145 | }); 146 | }; 147 | 148 | vled.datasetChanged = function(dataset, callback) { 149 | vled.dataset = dataset; 150 | 151 | if (dataset.stats) { 152 | callback(); 153 | return; 154 | } 155 | 156 | d3.json(dataset.url, function(err, data) { 157 | if (err) return alert('Error loading data ' + err.statusText); 158 | dataset.stats = vl.data.stats(data); 159 | callback(); 160 | }); 161 | }; 162 | 163 | vled.init = function() { 164 | var params = getParams(); 165 | 166 | // Specification drop-down menu 167 | var sel = d3.select('#sel_spec'); 168 | sel.selectAll('option.spec') 169 | .data(DATASETS) 170 | .enter().append('option') 171 | .text(function(d) { return d.name; }); 172 | 173 | sel.on('change', function() { 174 | var item = this.options[this.selectedIndex].__data__; 175 | vled.datasetChanged(item, function() { 176 | d3.select('#vgspec').node().value = ''; 177 | d3.select('#vis').node().innerHTML = ''; 178 | }); 179 | }); 180 | 181 | // Initialize application 182 | d3.select('#btn_spec_format').on('click', vled.format); 183 | d3.select('#btn_spec_parse').on('click', vled.parse); 184 | d3.select('#btn_shorthand_parse').on('click', vled.parseShorthand); 185 | 186 | var shorthand = params.shortHand; 187 | if (shorthand) { 188 | document.getElementById('shorthand').value = shorthand; 189 | vled.datasetChanged(DATASETS[0], function() { 190 | vled.parseShorthand(); 191 | }); 192 | } else if (docCookies.hasItem('vlspec')) { 193 | document.getElementById('vlspec').value = docCookies.getItem('vlspec'); 194 | vled.parse(); 195 | vled.format(); 196 | } else { 197 | document.getElementById('vlspec').value = JSON.stringify({ 198 | marktype: 'point', 199 | encoding: { 200 | x: {type: 'Q',name: 'yield',aggr: 'avg'}, 201 | y: { 202 | sort: [{name: 'yield', aggr: 'avg', reverse: false}], 203 | type: 'N', 204 | name: 'variety' 205 | }, 206 | row: {type: 'N', name: 'site'}, 207 | color: {type: 'N', name: 'year'} 208 | } 209 | }); 210 | 211 | vled.parse(); 212 | vled.format(); 213 | } 214 | }; 215 | 216 | window.onload = vled.init; 217 | -------------------------------------------------------------------------------- /test/compiler/data.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | var data = require('../../src/compiler/data'), 6 | Encoding = require('../../src/Encoding'); 7 | 8 | describe('data', function () { 9 | describe('for aggregate encoding', function () { 10 | it('should contain two tables', function() { 11 | var encoding = Encoding.fromSpec({ 12 | encoding: { 13 | x: {name: 'a', type: 'T'}, 14 | y: {name: 'b', type: 'Q', scale: {type: 'log'}, aggregate: 'sum'} 15 | } 16 | }); 17 | 18 | var _data = data(encoding); 19 | expect(_data.length).to.equal(2); 20 | }); 21 | }); 22 | 23 | describe('when contains log in non-aggregate', function () { 24 | var rawEncodingWithLog = Encoding.fromSpec({ 25 | encoding: { 26 | x: {name: 'a', type: 'T'}, 27 | y: {name: 'b', type: 'Q', scale: {type: 'log'}} 28 | } 29 | }); 30 | 31 | var _data = data(rawEncodingWithLog); 32 | it('should contains one table', function() { 33 | expect(_data.length).to.equal(1); 34 | }); 35 | it('should have filter non-positive in raw', function() { 36 | var rawTransform = _data[0].transform; 37 | expect(rawTransform[rawTransform.length - 1]).to.eql({ 38 | type: 'filter', 39 | test: 'd.data.b > 0' 40 | }); 41 | }); 42 | }); 43 | }); 44 | 45 | describe('data.raw', function() { 46 | describe('with explicit values', function() { 47 | var encoding = Encoding.fromSpec({ 48 | data: { 49 | values: [{a: 1, b:2, c:3}, {a: 4, b:5, c:6}] 50 | } 51 | }); 52 | 53 | var raw = data.raw(encoding, {}); 54 | 55 | it('should have values', function() { 56 | expect(raw.name).to.equal('raw'); 57 | expect(raw.values).to.deep.equal([{a: 1, b:2, c:3}, {a: 4, b:5, c:6}]); 58 | }); 59 | 60 | it('should have raw.format if not required', function(){ 61 | expect(raw.format).to.eql(undefined); 62 | }); 63 | }); 64 | 65 | describe('with link to url', function() { 66 | var encoding = Encoding.fromSpec({ 67 | data: { 68 | url: 'http://foo.bar' 69 | } 70 | }); 71 | 72 | var raw = data.raw(encoding); 73 | 74 | it('should have format json', function() { 75 | expect(raw.name).to.equal('raw'); 76 | expect(raw.format.type).to.equal('json'); 77 | }); 78 | it('should have correct url', function() { 79 | expect(raw.url).to.equal('http://foo.bar'); 80 | }); 81 | }); 82 | 83 | describe('formatParse', function () { 84 | it('should have correct parse', function() { 85 | var encoding = Encoding.fromSpec({ 86 | encoding: { 87 | x: {name: 'a', type: 'T'}, 88 | y: {name: 'b', type: 'Q'}, 89 | color: {name: '*', type: 'Q', aggregate: 'count'} 90 | } 91 | }); 92 | 93 | var raw = data.raw(encoding); 94 | expect(raw.format.parse).to.eql({ 95 | 'a': 'date', 96 | 'b': 'number' 97 | }); 98 | }); 99 | }); 100 | 101 | describe('transform', function () { 102 | var encoding = Encoding.fromSpec({ 103 | encoding: { 104 | x: {name: 'a', type:'T', timeUnit: 'year'}, 105 | y: { 106 | 'bin': {'maxbins': 15}, 107 | 'name': 'Acceleration', 108 | 'type': 'Q' 109 | } 110 | }, 111 | filter: [{ 112 | operator: '>', 113 | operands: ['a', 'b'] 114 | },{ 115 | operator: '=', 116 | operands: ['c', 'd'] 117 | }] 118 | }); 119 | 120 | describe('bin', function() { 121 | it('should add bin transform', function() { 122 | var transform = data.raw.transform.bin(encoding); 123 | expect(transform[0]).to.eql({ 124 | type: 'bin', 125 | field: 'data.Acceleration', 126 | output: 'data.bin_Acceleration', 127 | maxbins: 15 128 | }); 129 | }); 130 | }); 131 | 132 | describe('filter', function () { 133 | it('should return filter transform that include filter null', function () { 134 | var transform = data.raw.transform.filter(encoding); 135 | 136 | expect(transform[0]).to.eql({ 137 | type: 'filter', 138 | test: '(d.data.a!==null) && (d.data.Acceleration!==null)' + 139 | ' && (d.data.a > b) && (d.data.c == d)' 140 | }); 141 | }); 142 | 143 | it('should exclude unsupported operator', function () { 144 | var badEncoding = Encoding.fromSpec({ 145 | filter: [{ 146 | operator: '*', 147 | operands: ['a', 'b'] 148 | }] 149 | }); 150 | 151 | var transform = data.raw.transform.filter(badEncoding); 152 | 153 | expect(transform.length).to.equal(0); 154 | }); 155 | }); 156 | 157 | describe('time', function() { 158 | it('should add formula transform', function() { 159 | var transform = data.raw.transform.time(encoding); 160 | expect(transform[0]).to.eql({ 161 | type: 'formula', 162 | field: 'data.year_a', 163 | expr: 'utcyear(d.data.a)' 164 | }); 165 | }); 166 | }); 167 | 168 | it('should time and bin before filter', function () { 169 | var transform = data.raw.transform(encoding); 170 | expect(transform[0].type).to.eql('formula'); 171 | expect(transform[1].type).to.eql('bin'); 172 | expect(transform[2].type).to.eql('filter'); 173 | }); 174 | 175 | }); 176 | }); 177 | 178 | 179 | describe('data.aggregated', function () { 180 | it('should return correct aggregation', function() { 181 | var encoding = Encoding.fromSpec({ 182 | encoding: { 183 | 'y': { 184 | 'aggregate': 'sum', 185 | 'name': 'Acceleration', 186 | 'type': 'Q' 187 | }, 188 | 'x': { 189 | 'name': 'origin', 190 | "type": "O" 191 | }, 192 | color: {name: '*', type: 'Q', aggregate: 'count'} 193 | } 194 | }); 195 | 196 | var aggregated = data.aggregate(encoding); 197 | expect(aggregated ).to.eql({ 198 | "name": AGGREGATE, 199 | "source": "raw", 200 | "transform": [{ 201 | "type": "aggregate", 202 | "groupby": ["data.origin"], 203 | "fields": [{ 204 | "op": "sum", 205 | "field": "data.Acceleration" 206 | },{ 207 | "op": "count", 208 | "field": "*" 209 | }] 210 | }] 211 | }); 212 | }); 213 | }); -------------------------------------------------------------------------------- /test/compiler/marks.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var fixtures = require('../fixtures'); 5 | 6 | var marks = require('../../src/compiler/marks'), 7 | Encoding = require('../../src/Encoding'); 8 | 9 | var mockLayout = { 10 | x: {useSmallBand: false}, 11 | y: {useSmallBand: false} 12 | }; 13 | 14 | describe('compile.marks', function() { 15 | describe('bar', function() { 16 | describe('vertical, with log', function() { 17 | var f = fixtures.bars.log_ver, 18 | e = Encoding.fromSpec(f), 19 | def = marks.bar.prop(e, mockLayout, {}); 20 | it('should end on axis', function() { 21 | expect(def.y2).to.eql({group: 'height'}); 22 | }); 23 | it('should has no height', function(){ 24 | expect(def.height).to.be.undefined; 25 | }); 26 | }); 27 | 28 | describe('horizontal, with log', function() { 29 | var f = fixtures.bars.log_hor, 30 | e = Encoding.fromSpec(f), 31 | def = marks.bar.prop(e, mockLayout, {}); 32 | it('should end on axis', function() { 33 | expect(def.x2).to.eql({value: 0}); 34 | }); 35 | it('should have no width', function(){ 36 | expect(def.width).to.be.undefined; 37 | }); 38 | }); 39 | 40 | describe('1D, vertical', function() { 41 | var f = fixtures.bars['1d_ver'], 42 | e = Encoding.fromSpec(f), 43 | def = marks.bar.prop(e, mockLayout, {}); 44 | it('should end on axis', function() { 45 | expect(def.y2).to.eql({group: 'height'}); 46 | }); 47 | it('should have no height', function(){ 48 | expect(def.height).to.be.undefined; 49 | }); 50 | it('should have x-offset', function(){ 51 | expect(def.x.offset).to.eql(5); // config.singleBarOffset 52 | }); 53 | }); 54 | 55 | describe('1D, horizontal', function() { 56 | var f = fixtures.bars['1d_hor'], 57 | e = Encoding.fromSpec(f), 58 | def = marks.bar.prop(e, mockLayout, {}); 59 | it('should end on axis', function() { 60 | expect(def.x2).to.eql({value: 0}); 61 | }); 62 | it('should have no width', function(){ 63 | expect(def.width).to.be.undefined; 64 | }); 65 | it('should have y-offset', function(){ 66 | expect(def.y2).to.eql({ 67 | group: 'height', 68 | offset: -5 // -config.singleBarOffset 69 | }); 70 | }); 71 | }); 72 | }); 73 | 74 | describe('point', function() { 75 | describe('1D, horizontal', function() { 76 | var f = fixtures.points['1d_hor'], 77 | e = Encoding.fromSpec(f), 78 | def = marks.point.prop(e, mockLayout, {}); 79 | it('should be centered', function() { 80 | expect(def.y).to.eql({value: e.bandSize(Y, mockLayout.y.useSmallBand) / 2}); 81 | }); 82 | it('should scale on x', function() { 83 | expect(def.x).to.eql({scale: X, field: "data.year"}); 84 | }); 85 | }); 86 | 87 | describe('1D, vertical', function() { 88 | var f = fixtures.points['1d_ver'], 89 | e = Encoding.fromSpec(f), 90 | def = marks.point.prop(e, mockLayout, {}); 91 | it('should be centered', function() { 92 | expect(def.x).to.eql({value: e.bandSize(X, mockLayout.x.useSmallBand) / 2}); 93 | }); 94 | it('should scale on y', function() { 95 | expect(def.y).to.eql({scale: Y, field: "data.year"}); 96 | }); 97 | }); 98 | 99 | describe('2D, x and y', function() { 100 | var f = fixtures.points['x,y'], 101 | e = Encoding.fromSpec(f), 102 | def = marks.point.prop(e, mockLayout, {}); 103 | it('should scale on x', function() { 104 | expect(def.x).to.eql({scale: X, field: "data.year"}); 105 | }); 106 | it('should scale on y', function(){ 107 | expect(def.y).to.eql({scale: Y, field: "data.yield"}); 108 | }); 109 | }); 110 | 111 | describe('3D', function() { 112 | describe('x,y,size', function () { 113 | var f = fixtures.points['x,y,size'], 114 | e = Encoding.fromSpec(f), 115 | def = marks.point.prop(e, mockLayout, {}); 116 | it('should have scale for size', function () { 117 | expect(def.size).to.eql({scale: SIZE, field: "data.count"}); 118 | }); 119 | }); 120 | 121 | describe('x,y,color', function () { 122 | var f = fixtures.points['x,y,stroke'], 123 | e = Encoding.fromSpec(f), 124 | def = marks.point.prop(e, mockLayout, {}); 125 | it('should have scale for color', function () { 126 | expect(def.stroke).to.eql({scale: COLOR, field: "data.yield"}); 127 | }); 128 | }); 129 | 130 | describe('x,y,shape', function () { 131 | var f = fixtures.points['x,y,shape'], 132 | e = Encoding.fromSpec(f), 133 | def = marks.point.prop(e, mockLayout, {}); 134 | it('should have scale for shape', function () { 135 | expect(def.shape).to.eql({scale: SHAPE, field: "data.bin_yield"}); 136 | }); 137 | }); 138 | }); 139 | }); 140 | 141 | describe('line', function() { 142 | describe('2D, x and y', function() { 143 | var f = fixtures.lines['x,y'], 144 | e = Encoding.fromSpec(f), 145 | def = marks.line.prop(e, mockLayout, {}); 146 | it('should have scale for x', function() { 147 | expect(def.x).to.eql({scale: X, field: "data.year"}); 148 | }); 149 | it('should have scale for y', function(){ 150 | expect(def.y).to.eql({scale: Y, field: "data.yield"}); 151 | }); 152 | }); 153 | 154 | describe('3D', function() { 155 | describe('x,y,color', function () { 156 | var f = fixtures.lines['x,y,stroke'], 157 | e = Encoding.fromSpec(f), 158 | def = marks.line.prop(e, mockLayout, {}); 159 | it('should have scale for color', function () { 160 | expect(def.stroke).to.eql({scale: COLOR, field: "data.Acceleration"}); 161 | }); 162 | }); 163 | }); 164 | }); 165 | 166 | describe('area', function() { 167 | describe('2D, x and y', function() { 168 | var f = fixtures.area['x,y'], 169 | e = Encoding.fromSpec(f), 170 | def = marks.area.prop(e, mockLayout, {}); 171 | it('should have scale for x', function() { 172 | expect(def.x).to.eql({scale: X, field: "data.Displacement"}); 173 | }); 174 | it('should have scale for y', function(){ 175 | expect(def.y).to.eql({scale: Y, field: "data.Acceleration"}); 176 | }); 177 | }); 178 | 179 | describe('3D', function() { 180 | describe('x,y,color', function () { 181 | var f = fixtures.area['x,y,stroke'], 182 | e = Encoding.fromSpec(f), 183 | def = marks.area.prop(e, mockLayout, {}); 184 | it('should have scale for color', function () { 185 | expect(def.fill).to.eql({scale: COLOR, field: "data.Miles_per_Gallon"}); 186 | }); 187 | }); 188 | }); 189 | }); 190 | 191 | // TODO add other type of marks 192 | }); -------------------------------------------------------------------------------- /bin/shorthand2vg/birdstrikes.bar.x-sum_Cost__Total_$-Q.y-Effect__Amount_of_damage-O.row-When__Phase_of_flight-O.col-When__Time_of_day-O.color-Wildlife__Size-O.json: -------------------------------------------------------------------------------- 1 | { 2 | "width": 860.0000000000001, 3 | "height": 1117.2, 4 | "padding": "auto", 5 | "data": [ 6 | { 7 | "name": "table", 8 | "format": {"type": "json","parse": {"Cost__Total_$": "number"}}, 9 | "url": "data/birdstrikes.json", 10 | "transform": [ 11 | { 12 | "type": "aggregate", 13 | "groupby": [ 14 | "data.Effect__Amount_of_damage", 15 | "data.When__Phase_of_flight", 16 | "data.When__Time_of_day", 17 | "data.Wildlife__Size" 18 | ], 19 | "fields": [{"op": "sum","field": "data.Cost__Total_$"}] 20 | } 21 | ] 22 | }, 23 | { 24 | "name": "stacked", 25 | "source": "table", 26 | "transform": [ 27 | { 28 | "type": "aggregate", 29 | "groupby": [ 30 | "data.Effect__Amount_of_damage", 31 | "data.When__Phase_of_flight", 32 | "data.When__Time_of_day" 33 | ], 34 | "fields": [{"op": "sum","field": "data.sum_Cost__Total_$"}] 35 | }, 36 | { 37 | "type": "aggregate", 38 | "groupby": ["data.When__Phase_of_flight","data.When__Time_of_day"], 39 | "fields": [{"op": "max","field": "data.sum_sum_Cost__Total_$"}] 40 | } 41 | ] 42 | } 43 | ], 44 | "marks": [ 45 | { 46 | "_name": "cell", 47 | "type": "group", 48 | "from": { 49 | "data": "table", 50 | "transform": [ 51 | { 52 | "type": "facet", 53 | "keys": ["data.When__Phase_of_flight","data.When__Time_of_day"] 54 | } 55 | ] 56 | }, 57 | "properties": { 58 | "enter": { 59 | "x": {"scale": "col","field": "keys.1","offset": 80}, 60 | "y": {"scale": "row","field": "keys.0"}, 61 | "width": {"value": 200}, 62 | "height": {"value": 147}, 63 | "fill": {"value": "#fdfdfd"} 64 | } 65 | }, 66 | "marks": [ 67 | { 68 | "_name": "subfacet", 69 | "type": "group", 70 | "from": { 71 | "transform": [ 72 | {"type": "sort","by": "data.Wildlife__Size"}, 73 | {"type": "facet","keys": ["data.Wildlife__Size"]}, 74 | { 75 | "type": "stack", 76 | "point": "data.Effect__Amount_of_damage", 77 | "height": "data.sum_Cost__Total_$", 78 | "output": {"y1": "x","y0": "x2"} 79 | } 80 | ] 81 | }, 82 | "properties": {"enter": {"width": {"group": "width"},"height": {"group": "height"}}}, 83 | "marks": [ 84 | { 85 | "type": "rect", 86 | "properties": { 87 | "enter": { 88 | "x": {"scale": "x","field": "x"}, 89 | "x2": {"scale": "x","field": "x2"}, 90 | "yc": {"scale": "y","field": "data.Effect__Amount_of_damage"}, 91 | "height": {"value": 21,"offset": -1}, 92 | "fill": {"scale": "color","field": "data.Wildlife__Size"} 93 | }, 94 | "update": { 95 | "x": {"scale": "x","field": "x"}, 96 | "x2": {"scale": "x","field": "x2"}, 97 | "yc": {"scale": "y","field": "data.Effect__Amount_of_damage"}, 98 | "height": {"value": 21,"offset": -1}, 99 | "fill": {"scale": "color","field": "data.Wildlife__Size"} 100 | } 101 | } 102 | } 103 | ] 104 | } 105 | ] 106 | }, 107 | { 108 | "_name": "x-axes", 109 | "type": "group", 110 | "from": { 111 | "data": "table", 112 | "transform": [{"type": "facet","keys": ["data.When__Time_of_day"]}] 113 | }, 114 | "properties": { 115 | "enter": { 116 | "x": {"scale": "col","field": "keys.0","offset": 80}, 117 | "width": {"value": 200}, 118 | "height": {"group": "height"} 119 | } 120 | }, 121 | "axes": [{"type": "x","scale": "x","ticks": 3}], 122 | "marks": [] 123 | }, 124 | { 125 | "_name": "y-axes", 126 | "type": "group", 127 | "from": { 128 | "data": "table", 129 | "transform": [{"type": "facet","keys": ["data.When__Phase_of_flight"]}] 130 | }, 131 | "properties": { 132 | "enter": { 133 | "x": {"value": 80}, 134 | "y": {"scale": "row","field": "keys.0"}, 135 | "width": {"group": "width"}, 136 | "height": {"value": 147} 137 | } 138 | }, 139 | "axes": [{"type": "y","scale": "y","ticks": 3}], 140 | "marks": [] 141 | } 142 | ], 143 | "axes": [ 144 | { 145 | "type": "y", 146 | "scale": "row", 147 | "ticks": 3, 148 | "properties": { 149 | "ticks": {"opacity": {"value": 0}}, 150 | "majorTicks": {"opacity": {"value": 0}}, 151 | "axis": {"opacity": {"value": 0}} 152 | } 153 | }, 154 | { 155 | "type": "x", 156 | "scale": "col", 157 | "ticks": 3, 158 | "properties": { 159 | "ticks": {"opacity": {"value": 0}}, 160 | "majorTicks": {"opacity": {"value": 0}}, 161 | "axis": {"opacity": {"value": 0}} 162 | }, 163 | "offset": [80,0], 164 | "orient": "top" 165 | } 166 | ], 167 | "scales": [ 168 | { 169 | "name": "col", 170 | "type": "ordinal", 171 | "domain": {"data": "table","field": "data.When__Time_of_day"}, 172 | "sort": true, 173 | "bandWidth": 200, 174 | "round": true, 175 | "nice": true, 176 | "padding": 0.1, 177 | "outerPadding": 0 178 | }, 179 | { 180 | "name": "row", 181 | "type": "ordinal", 182 | "domain": {"data": "table","field": "data.When__Phase_of_flight"}, 183 | "sort": true, 184 | "bandWidth": 147, 185 | "round": true, 186 | "nice": true, 187 | "padding": 0.1, 188 | "outerPadding": 0 189 | }, 190 | { 191 | "name": "x", 192 | "type": "linear", 193 | "domain": {"data": "stacked","field": "data.max_sum_sum_Cost__Total_$"}, 194 | "range": [0,200], 195 | "zero": true, 196 | "reverse": false, 197 | "round": true, 198 | "nice": true 199 | }, 200 | { 201 | "name": "y", 202 | "type": "ordinal", 203 | "domain": {"data": "table","field": "data.Effect__Amount_of_damage"}, 204 | "sort": true, 205 | "bandWidth": 21, 206 | "round": true, 207 | "nice": true, 208 | "points": true, 209 | "padding": 1 210 | }, 211 | { 212 | "name": "color", 213 | "type": "ordinal", 214 | "domain": {"data": "table","field": "data.Wildlife__Size"}, 215 | "sort": true, 216 | "range": "category10", 217 | "points": true, 218 | "padding": 1 219 | } 220 | ] 221 | } -------------------------------------------------------------------------------- /src/compiler/axis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../globals'); 4 | 5 | var util = require('../util'), 6 | setter = util.setter, 7 | getter = util.getter, 8 | time = require('./time'); 9 | 10 | var axis = module.exports = {}; 11 | 12 | axis.def = function(name, encoding, layout, stats, opt) { 13 | var isCol = name == COL, 14 | isRow = name == ROW, 15 | type = isCol ? 'x' : isRow ? 'y' : name; 16 | 17 | var def = { 18 | type: type, 19 | scale: name, 20 | properties: {}, 21 | layer: encoding.field(name).axis.layer, 22 | orient: axis.orient(name, encoding, stats) 23 | }; 24 | 25 | // Add axis label custom scale (for bin / time) 26 | def = axis.labels.scale(def, encoding, name); 27 | def = axis.labels.format(def, name, encoding, stats); 28 | def = axis.labels.angle(def, encoding, name); 29 | 30 | // for x-axis, set ticks for Q or rotate scale for ordinal scale 31 | if (name == X) { 32 | if ((encoding.isDimension(X) || encoding.isType(X, T)) && 33 | !('angle' in getter(def, ['properties', 'labels']))) { 34 | // TODO(kanitw): Jul 19, 2015 - #506 add condition for rotation 35 | def = axis.labels.rotate(def); 36 | } else { // Q 37 | def.ticks = encoding.field(name).axis.ticks; 38 | } 39 | } 40 | 41 | // TitleOffset depends on labels rotation 42 | def.titleOffset = axis.titleOffset(encoding, layout, name); 43 | 44 | //def.offset is used in axis.grid 45 | if(isRow) def.offset = axis.titleOffset(encoding, layout, Y) + 20; 46 | // FIXME(kanitw): Jul 19, 2015 - offset for column when x is put on top 47 | 48 | def = axis.grid(def, name, encoding, layout); 49 | def = axis.title(def, name, encoding, layout, opt); 50 | 51 | if (isRow || isCol) def = axis.hideTicks(def); 52 | 53 | return def; 54 | }; 55 | 56 | axis.orient = function(name, encoding, stats) { 57 | var orient = encoding.field(name).axis.orient; 58 | if (orient) return orient; 59 | 60 | if (name===COL) return 'top'; 61 | 62 | // x-axis for long y - put on top 63 | if (name===X && encoding.has(Y) && encoding.isOrdinalScale(Y) && encoding.cardinality(Y, stats) > 30) { 64 | return 'top'; 65 | } 66 | 67 | return undefined; 68 | }; 69 | 70 | axis.grid = function(def, name, encoding, layout) { 71 | var cellPadding = layout.cellPadding, 72 | isCol = name == COL, 73 | isRow = name == ROW; 74 | 75 | if (encoding.axis(name).grid) { 76 | def.grid = true; 77 | 78 | if (isCol) { 79 | // set grid property -- put the lines on the right the cell 80 | def.properties.grid = { 81 | x: { 82 | offset: layout.cellWidth * (1+ cellPadding/2.0), 83 | // default value(s) -- vega doesn't do recursive merge 84 | scale: 'col' 85 | }, 86 | y: { 87 | value: -layout.cellHeight * (cellPadding/2), 88 | }, 89 | stroke: { value: encoding.config('cellGridColor') }, 90 | opacity: { value: encoding.config('cellGridOpacity') } 91 | }; 92 | } else if (isRow) { 93 | // set grid property -- put the lines on the top 94 | def.properties.grid = { 95 | y: { 96 | offset: -layout.cellHeight * (cellPadding/2), 97 | // default value(s) -- vega doesn't do recursive merge 98 | scale: 'row' 99 | }, 100 | x: { 101 | value: def.offset 102 | }, 103 | x2: { 104 | offset: def.offset + (layout.cellWidth * 0.05), 105 | // default value(s) -- vega doesn't do recursive merge 106 | group: 'mark.group.width', 107 | mult: 1 108 | }, 109 | stroke: { value: encoding.config('cellGridColor') }, 110 | opacity: { value: encoding.config('cellGridOpacity') } 111 | }; 112 | } else { 113 | def.properties.grid = { 114 | stroke: { value: encoding.config('gridColor') }, 115 | opacity: { value: encoding.config('gridOpacity') } 116 | }; 117 | } 118 | } 119 | return def; 120 | }; 121 | 122 | axis.hideTicks = function(def) { 123 | def.properties.ticks = {opacity: {value: 0}}; 124 | def.properties.majorTicks = {opacity: {value: 0}}; 125 | def.properties.axis = {opacity: {value: 0}}; 126 | return def; 127 | }; 128 | 129 | axis.title = function (def, name, encoding, layout) { 130 | var ax = encoding.field(name).axis; 131 | 132 | if (ax.title) { 133 | def.title = ax.title; 134 | } else { 135 | // if not defined, automatically determine axis title from field def 136 | var fieldTitle = encoding.fieldTitle(name), 137 | maxLength; 138 | 139 | if (ax.titleMaxLength) { 140 | maxLength = ax.titleMaxLength; 141 | } else if (name===X) { 142 | maxLength = layout.cellWidth / encoding.config('characterWidth'); 143 | } else if (name === Y) { 144 | maxLength = layout.cellHeight / encoding.config('characterWidth'); 145 | } 146 | 147 | def.title = maxLength ? util.truncate(fieldTitle, maxLength) : fieldTitle; 148 | } 149 | 150 | if (name === ROW) { 151 | def.properties.title = { 152 | angle: {value: 0}, 153 | align: {value: 'right'}, 154 | baseline: {value: 'middle'}, 155 | dy: {value: (-layout.height/2) -20} 156 | }; 157 | } 158 | 159 | return def; 160 | }; 161 | 162 | axis.labels = {}; 163 | 164 | /** add custom label for time type and bin */ 165 | axis.labels.scale = function(def, encoding, name) { 166 | // time 167 | var timeUnit = encoding.field(name).timeUnit; 168 | if (encoding.isType(name, T) && timeUnit && (time.hasScale(timeUnit))) { 169 | setter(def, ['properties','labels','text','scale'], 'time-'+ timeUnit); 170 | } 171 | // FIXME bin 172 | return def; 173 | }; 174 | 175 | /** 176 | * Determine number format or truncate if maxLabel length is presented. 177 | */ 178 | axis.labels.format = function (def, name, encoding, stats) { 179 | var fieldStats = stats[encoding.field(name).name]; 180 | 181 | if (encoding.axis(name).format) { 182 | def.format = encoding.axis(name).format; 183 | } else if (encoding.isType(name, Q) || fieldStats.type === 'number') { 184 | def.format = encoding.numberFormat(fieldStats); 185 | } else if (encoding.isType(name, T)) { 186 | var timeUnit = encoding.field(name).timeUnit; 187 | if (!timeUnit) { 188 | def.format = encoding.config('timeFormat'); 189 | } else if (timeUnit === 'year') { 190 | def.format = 'd'; 191 | } 192 | } else if (encoding.isTypes(name, [N, O]) && encoding.axis(name).maxLabelLength) { 193 | setter(def, 194 | ['properties','labels','text','template'], 195 | '{{data | truncate:' + encoding.axis(name).maxLabelLength + '}}' 196 | ); 197 | } 198 | 199 | return def; 200 | }; 201 | 202 | axis.labels.angle = function(def, encoding, name) { 203 | var angle = encoding.axis(name).labelAngle; 204 | if (typeof angle === 'undefined') return def; 205 | 206 | setter(def, ['properties', 'labels', 'angle', 'value'], angle); 207 | return def; 208 | }; 209 | 210 | axis.labels.rotate = function(def) { 211 | var align = def.orient ==='top' ? 'left' : 'right'; 212 | setter(def, ['properties','labels', 'angle', 'value'], 270); 213 | setter(def, ['properties','labels', 'align', 'value'], align); 214 | setter(def, ['properties','labels', 'baseline', 'value'], 'middle'); 215 | return def; 216 | }; 217 | 218 | axis.titleOffset = function (encoding, layout, name) { 219 | // return specified value if specified 220 | var value = encoding.axis(name).titleOffset; 221 | if (value) return value; 222 | 223 | switch (name) { 224 | //FIXME make this adjustable 225 | case ROW: return 0; 226 | case COL: return 35; 227 | } 228 | return getter(layout, [name, 'axisTitleOffset']); 229 | }; 230 | -------------------------------------------------------------------------------- /test/compiler/scale.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var d3 = require('d3'); 4 | var expect = require('chai').expect; 5 | 6 | var util = require('../../src/util'), 7 | Encoding = require('../../src/Encoding'), 8 | vlscale = require('../../src/compiler/scale'), 9 | colorbrewer = require('colorbrewer'); 10 | 11 | describe('vl.compile.scale', function() { 12 | describe('sort()', function() { 13 | it('should return true for any ordinal or binned field', function() { 14 | var encoding = Encoding.fromSpec({ 15 | encoding: { 16 | x: { name: 'origin', type: O}, 17 | y: { bin: true, name: 'origin', type: Q} 18 | } 19 | }); 20 | 21 | expect(vlscale.sort({type: 'ordinal'}, encoding, 'x')) 22 | .to.eql(true); 23 | expect(vlscale.sort({type: 'ordinal'}, encoding, 'y')) 24 | .to.eql(true); 25 | }); 26 | 27 | }); 28 | 29 | describe('domain()', function() { 30 | it('should return correct stack', function() { 31 | var domain = vlscale.domain('y', Encoding.fromSpec({ 32 | encoding: { 33 | y: { 34 | name: 'origin' 35 | } 36 | } 37 | }), {}, { 38 | stack: 'y', 39 | facet: true 40 | }); 41 | 42 | expect(domain).to.eql({ 43 | data: 'stacked', 44 | field: 'data.max_sum_origin' 45 | }); 46 | }); 47 | 48 | it('should return correct aggregated stack', function() { 49 | var domain = vlscale.domain('y', Encoding.fromSpec({ 50 | encoding: { 51 | y: { 52 | aggregate: 'sum', 53 | name: 'origin' 54 | } 55 | } 56 | }), {}, { 57 | stack: 'y', 58 | facet: true 59 | }); 60 | 61 | expect(domain).to.eql({ 62 | data: 'stacked', 63 | field: 'data.max_sum_sum_origin' 64 | }); 65 | }); 66 | 67 | it('should return the right domain if binned Q', 68 | function() { 69 | var domain = vlscale.domain('y', Encoding.fromSpec({ 70 | encoding: { 71 | y: { 72 | bin: {maxbins: 15}, 73 | name: 'origin', 74 | scale: {useRawDomain: true}, 75 | type: Q 76 | } 77 | } 78 | }), {origin: {min: -5, max:48}}, {}); 79 | 80 | expect(domain).to.eql([-5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45]); 81 | }); 82 | 83 | it('should return the raw domain if useRawDomain is true for non-bin, non-sum Q', 84 | function() { 85 | var domain = vlscale.domain('y', Encoding.fromSpec({ 86 | encoding: { 87 | y: { 88 | aggregate: 'mean', 89 | name: 'origin', 90 | scale: {useRawDomain: true}, 91 | type: Q 92 | } 93 | } 94 | }), {}, {}); 95 | 96 | expect(domain.data).to.eql(RAW); 97 | }); 98 | 99 | it('should return the aggregate domain for sum Q', 100 | function() { 101 | var domain = vlscale.domain('y', Encoding.fromSpec({ 102 | encoding: { 103 | y: { 104 | aggregate: 'sum', 105 | name: 'origin', 106 | scale: {useRawDomain: true}, 107 | type: Q 108 | } 109 | } 110 | }), {}, {}); 111 | 112 | expect(domain.data).to.eql(AGGREGATE); 113 | }); 114 | 115 | it('should return the raw domain if useRawDomain is true for raw T', 116 | function() { 117 | var domain = vlscale.domain('y', Encoding.fromSpec({ 118 | encoding: { 119 | y: { 120 | name: 'origin', 121 | scale: {useRawDomain: true}, 122 | type: T 123 | } 124 | } 125 | }), {}, {}); 126 | 127 | expect(domain.data).to.eql(RAW); 128 | }); 129 | 130 | it('should return the raw domain if useRawDomain is true for year T', 131 | function() { 132 | var domain = vlscale.domain('y', Encoding.fromSpec({ 133 | encoding: { 134 | y: { 135 | name: 'origin', 136 | scale: {useRawDomain: true}, 137 | type: T, 138 | timeUnit: 'year' 139 | } 140 | } 141 | }), {}, {}); 142 | 143 | expect(domain.data).to.eql(RAW); 144 | expect(domain.field.indexOf('year')).to.gt(-1); 145 | }); 146 | 147 | it('should return the correct domain for month T', 148 | function() { 149 | var domain = vlscale.domain('y', Encoding.fromSpec({ 150 | encoding: { 151 | y: { 152 | name: 'origin', 153 | scale: {useRawDomain: true}, 154 | type: T, 155 | timeUnit: 'month' 156 | } 157 | } 158 | }), {}, {}); 159 | 160 | expect(domain).to.eql([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); 161 | }); 162 | 163 | it('should return the aggregated domain if useRawDomain is false', function() { 164 | var domain = vlscale.domain('y', Encoding.fromSpec({ 165 | encoding: { 166 | y: { 167 | aggregate: 'min', 168 | name: 'origin', 169 | scale: {useRawDomain: false}, 170 | type: Q 171 | } 172 | } 173 | }), {}, {}); 174 | 175 | expect(domain.data).to.eql(AGGREGATE); 176 | }); 177 | 178 | // TODO test other cases 179 | }); 180 | 181 | describe('color.palette', function() { 182 | it('should return tableau categories', function() { 183 | expect(vlscale.color.palette('category10k')).to.eql( 184 | ['#2ca02c', '#e377c2', '#7f7f7f', '#17becf', '#8c564b', '#d62728', '#bcbd22', 185 | '#9467bd', '#ff7f0e', '#1f77b4' 186 | ] 187 | ); 188 | }); 189 | 190 | it('should return pre-defined brewer palette if low cardinality', function() { 191 | var brewerPalettes = util.keys(colorbrewer); 192 | brewerPalettes.forEach(function(palette) { 193 | util.range(3, 9).forEach(function(cardinality) { 194 | expect(vlscale.color.palette(palette, cardinality)).to.eql( 195 | colorbrewer[palette][cardinality] 196 | ); 197 | }); 198 | }); 199 | }); 200 | 201 | it('should return pre-defined brewer palette if high cardinality N', function() { 202 | var brewerPalettes = util.keys(colorbrewer); 203 | brewerPalettes.forEach(function(palette) { 204 | var cardinality = 20; 205 | expect(vlscale.color.palette(palette, cardinality, 'N')).to.eql( 206 | colorbrewer[palette][Math.max.apply(null, util.keys(colorbrewer[palette]))] 207 | ); 208 | }); 209 | }); 210 | 211 | it('should return interpolated scale if high cardinality ordinal', function() { 212 | var brewerPalettes = util.keys(colorbrewer); 213 | brewerPalettes.forEach(function(palette) { 214 | var cardinality = 20, 215 | p = colorbrewer[palette], 216 | ps = Math.max.apply(null, util.keys(p)), 217 | interpolator = d3.interpolateHsl(p[ps][0], p[ps][ps - 1]); 218 | expect(vlscale.color.palette(palette, cardinality, 'O')).to.eql( 219 | util.range(cardinality).map(function(i) { 220 | return interpolator(i * 1.0 / (cardinality - 1)); 221 | }) 222 | ); 223 | }); 224 | }); 225 | }); 226 | 227 | describe('color.interpolate', function() { 228 | it('should interpolate color along the hsl space', function() { 229 | var interpolator = d3.interpolateHsl('#ffffff', '#000000'), 230 | cardinality = 8; 231 | 232 | expect(vlscale.color.interpolate('#ffffff', '#000000', cardinality)) 233 | .to.eql( 234 | util.range(cardinality).map(function(i) { 235 | return interpolator(i * 1.0 / (cardinality - 1)); 236 | }) 237 | ); 238 | }); 239 | }); 240 | }); -------------------------------------------------------------------------------- /data/barley.json: -------------------------------------------------------------------------------- 1 | [{"yield":27,"variety":"Manchuria","year":1931,"site":"University Farm"}, 2 | {"yield":48.86667,"variety":"Manchuria","year":1931,"site":"Waseca"}, 3 | {"yield":27.43334,"variety":"Manchuria","year":1931,"site":"Morris"}, 4 | {"yield":39.93333,"variety":"Manchuria","year":1931,"site":"Crookston"}, 5 | {"yield":32.96667,"variety":"Manchuria","year":1931,"site":"Grand Rapids"}, 6 | {"yield":28.96667,"variety":"Manchuria","year":1931,"site":"Duluth"}, 7 | {"yield":43.06666,"variety":"Glabron","year":1931,"site":"University Farm"}, 8 | {"yield":55.2,"variety":"Glabron","year":1931,"site":"Waseca"}, 9 | {"yield":28.76667,"variety":"Glabron","year":1931,"site":"Morris"}, 10 | {"yield":38.13333,"variety":"Glabron","year":1931,"site":"Crookston"}, 11 | {"yield":29.13333,"variety":"Glabron","year":1931,"site":"Grand Rapids"}, 12 | {"yield":29.66667,"variety":"Glabron","year":1931,"site":"Duluth"}, 13 | {"yield":35.13333,"variety":"Svansota","year":1931,"site":"University Farm"}, 14 | {"yield":47.33333,"variety":"Svansota","year":1931,"site":"Waseca"}, 15 | {"yield":25.76667,"variety":"Svansota","year":1931,"site":"Morris"}, 16 | {"yield":40.46667,"variety":"Svansota","year":1931,"site":"Crookston"}, 17 | {"yield":29.66667,"variety":"Svansota","year":1931,"site":"Grand Rapids"}, 18 | {"yield":25.7,"variety":"Svansota","year":1931,"site":"Duluth"}, 19 | {"yield":39.9,"variety":"Velvet","year":1931,"site":"University Farm"}, 20 | {"yield":50.23333,"variety":"Velvet","year":1931,"site":"Waseca"}, 21 | {"yield":26.13333,"variety":"Velvet","year":1931,"site":"Morris"}, 22 | {"yield":41.33333,"variety":"Velvet","year":1931,"site":"Crookston"}, 23 | {"yield":23.03333,"variety":"Velvet","year":1931,"site":"Grand Rapids"}, 24 | {"yield":26.3,"variety":"Velvet","year":1931,"site":"Duluth"}, 25 | {"yield":36.56666,"variety":"Trebi","year":1931,"site":"University Farm"}, 26 | {"yield":63.8333,"variety":"Trebi","year":1931,"site":"Waseca"}, 27 | {"yield":43.76667,"variety":"Trebi","year":1931,"site":"Morris"}, 28 | {"yield":46.93333,"variety":"Trebi","year":1931,"site":"Crookston"}, 29 | {"yield":29.76667,"variety":"Trebi","year":1931,"site":"Grand Rapids"}, 30 | {"yield":33.93333,"variety":"Trebi","year":1931,"site":"Duluth"}, 31 | {"yield":43.26667,"variety":"No. 457","year":1931,"site":"University Farm"}, 32 | {"yield":58.1,"variety":"No. 457","year":1931,"site":"Waseca"}, 33 | {"yield":28.7,"variety":"No. 457","year":1931,"site":"Morris"}, 34 | {"yield":45.66667,"variety":"No. 457","year":1931,"site":"Crookston"}, 35 | {"yield":32.16667,"variety":"No. 457","year":1931,"site":"Grand Rapids"}, 36 | {"yield":33.6,"variety":"No. 457","year":1931,"site":"Duluth"}, 37 | {"yield":36.6,"variety":"No. 462","year":1931,"site":"University Farm"}, 38 | {"yield":65.7667,"variety":"No. 462","year":1931,"site":"Waseca"}, 39 | {"yield":30.36667,"variety":"No. 462","year":1931,"site":"Morris"}, 40 | {"yield":48.56666,"variety":"No. 462","year":1931,"site":"Crookston"}, 41 | {"yield":24.93334,"variety":"No. 462","year":1931,"site":"Grand Rapids"}, 42 | {"yield":28.1,"variety":"No. 462","year":1931,"site":"Duluth"}, 43 | {"yield":32.76667,"variety":"Peatland","year":1931,"site":"University Farm"}, 44 | {"yield":48.56666,"variety":"Peatland","year":1931,"site":"Waseca"}, 45 | {"yield":29.86667,"variety":"Peatland","year":1931,"site":"Morris"}, 46 | {"yield":41.6,"variety":"Peatland","year":1931,"site":"Crookston"}, 47 | {"yield":34.7,"variety":"Peatland","year":1931,"site":"Grand Rapids"}, 48 | {"yield":32,"variety":"Peatland","year":1931,"site":"Duluth"}, 49 | {"yield":24.66667,"variety":"No. 475","year":1931,"site":"University Farm"}, 50 | {"yield":46.76667,"variety":"No. 475","year":1931,"site":"Waseca"}, 51 | {"yield":22.6,"variety":"No. 475","year":1931,"site":"Morris"}, 52 | {"yield":44.1,"variety":"No. 475","year":1931,"site":"Crookston"}, 53 | {"yield":19.7,"variety":"No. 475","year":1931,"site":"Grand Rapids"}, 54 | {"yield":33.06666,"variety":"No. 475","year":1931,"site":"Duluth"}, 55 | {"yield":39.3,"variety":"Wisconsin No. 38","year":1931,"site":"University Farm"}, 56 | {"yield":58.8,"variety":"Wisconsin No. 38","year":1931,"site":"Waseca"}, 57 | {"yield":29.46667,"variety":"Wisconsin No. 38","year":1931,"site":"Morris"}, 58 | {"yield":49.86667,"variety":"Wisconsin No. 38","year":1931,"site":"Crookston"}, 59 | {"yield":34.46667,"variety":"Wisconsin No. 38","year":1931,"site":"Grand Rapids"}, 60 | {"yield":31.6,"variety":"Wisconsin No. 38","year":1931,"site":"Duluth"}, 61 | {"yield":26.9,"variety":"Manchuria","year":1932,"site":"University Farm"}, 62 | {"yield":33.46667,"variety":"Manchuria","year":1932,"site":"Waseca"}, 63 | {"yield":34.36666,"variety":"Manchuria","year":1932,"site":"Morris"}, 64 | {"yield":32.96667,"variety":"Manchuria","year":1932,"site":"Crookston"}, 65 | {"yield":22.13333,"variety":"Manchuria","year":1932,"site":"Grand Rapids"}, 66 | {"yield":22.56667,"variety":"Manchuria","year":1932,"site":"Duluth"}, 67 | {"yield":36.8,"variety":"Glabron","year":1932,"site":"University Farm"}, 68 | {"yield":37.73333,"variety":"Glabron","year":1932,"site":"Waseca"}, 69 | {"yield":35.13333,"variety":"Glabron","year":1932,"site":"Morris"}, 70 | {"yield":26.16667,"variety":"Glabron","year":1932,"site":"Crookston"}, 71 | {"yield":14.43333,"variety":"Glabron","year":1932,"site":"Grand Rapids"}, 72 | {"yield":25.86667,"variety":"Glabron","year":1932,"site":"Duluth"}, 73 | {"yield":27.43334,"variety":"Svansota","year":1932,"site":"University Farm"}, 74 | {"yield":38.5,"variety":"Svansota","year":1932,"site":"Waseca"}, 75 | {"yield":35.03333,"variety":"Svansota","year":1932,"site":"Morris"}, 76 | {"yield":20.63333,"variety":"Svansota","year":1932,"site":"Crookston"}, 77 | {"yield":16.63333,"variety":"Svansota","year":1932,"site":"Grand Rapids"}, 78 | {"yield":22.23333,"variety":"Svansota","year":1932,"site":"Duluth"}, 79 | {"yield":26.8,"variety":"Velvet","year":1932,"site":"University Farm"}, 80 | {"yield":37.4,"variety":"Velvet","year":1932,"site":"Waseca"}, 81 | {"yield":38.83333,"variety":"Velvet","year":1932,"site":"Morris"}, 82 | {"yield":32.06666,"variety":"Velvet","year":1932,"site":"Crookston"}, 83 | {"yield":32.23333,"variety":"Velvet","year":1932,"site":"Grand Rapids"}, 84 | {"yield":22.46667,"variety":"Velvet","year":1932,"site":"Duluth"}, 85 | {"yield":29.06667,"variety":"Trebi","year":1932,"site":"University Farm"}, 86 | {"yield":49.2333,"variety":"Trebi","year":1932,"site":"Waseca"}, 87 | {"yield":46.63333,"variety":"Trebi","year":1932,"site":"Morris"}, 88 | {"yield":41.83333,"variety":"Trebi","year":1932,"site":"Crookston"}, 89 | {"yield":20.63333,"variety":"Trebi","year":1932,"site":"Grand Rapids"}, 90 | {"yield":30.6,"variety":"Trebi","year":1932,"site":"Duluth"}, 91 | {"yield":26.43334,"variety":"No. 457","year":1932,"site":"University Farm"}, 92 | {"yield":42.2,"variety":"No. 457","year":1932,"site":"Waseca"}, 93 | {"yield":43.53334,"variety":"No. 457","year":1932,"site":"Morris"}, 94 | {"yield":34.33333,"variety":"No. 457","year":1932,"site":"Crookston"}, 95 | {"yield":19.46667,"variety":"No. 457","year":1932,"site":"Grand Rapids"}, 96 | {"yield":22.7,"variety":"No. 457","year":1932,"site":"Duluth"}, 97 | {"yield":25.56667,"variety":"No. 462","year":1932,"site":"University Farm"}, 98 | {"yield":44.7,"variety":"No. 462","year":1932,"site":"Waseca"}, 99 | {"yield":47,"variety":"No. 462","year":1932,"site":"Morris"}, 100 | {"yield":30.53333,"variety":"No. 462","year":1932,"site":"Crookston"}, 101 | {"yield":19.9,"variety":"No. 462","year":1932,"site":"Grand Rapids"}, 102 | {"yield":22.5,"variety":"No. 462","year":1932,"site":"Duluth"}, 103 | {"yield":28.06667,"variety":"Peatland","year":1932,"site":"University Farm"}, 104 | {"yield":36.03333,"variety":"Peatland","year":1932,"site":"Waseca"}, 105 | {"yield":43.2,"variety":"Peatland","year":1932,"site":"Morris"}, 106 | {"yield":25.23333,"variety":"Peatland","year":1932,"site":"Crookston"}, 107 | {"yield":26.76667,"variety":"Peatland","year":1932,"site":"Grand Rapids"}, 108 | {"yield":31.36667,"variety":"Peatland","year":1932,"site":"Duluth"}, 109 | {"yield":30,"variety":"No. 475","year":1932,"site":"University Farm"}, 110 | {"yield":41.26667,"variety":"No. 475","year":1932,"site":"Waseca"}, 111 | {"yield":44.23333,"variety":"No. 475","year":1932,"site":"Morris"}, 112 | {"yield":32.13333,"variety":"No. 475","year":1932,"site":"Crookston"}, 113 | {"yield":15.23333,"variety":"No. 475","year":1932,"site":"Grand Rapids"}, 114 | {"yield":27.36667,"variety":"No. 475","year":1932,"site":"Duluth"}, 115 | {"yield":38,"variety":"Wisconsin No. 38","year":1932,"site":"University Farm"}, 116 | {"yield":58.16667,"variety":"Wisconsin No. 38","year":1932,"site":"Waseca"}, 117 | {"yield":47.16667,"variety":"Wisconsin No. 38","year":1932,"site":"Morris"}, 118 | {"yield":35.9,"variety":"Wisconsin No. 38","year":1932,"site":"Crookston"}, 119 | {"yield":20.66667,"variety":"Wisconsin No. 38","year":1932,"site":"Grand Rapids"}, 120 | {"yield":29.33333,"variety":"Wisconsin No. 38","year":1932,"site":"Duluth"}] -------------------------------------------------------------------------------- /editor/jquery.autosize.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Autosize 1.18.17 3 | license: MIT 4 | http://www.jacklmoore.com/autosize 5 | */ 6 | (function ($) { 7 | var 8 | defaults = { 9 | className: 'autosizejs', 10 | id: 'autosizejs', 11 | append: '\n', 12 | callback: false, 13 | resizeDelay: 10, 14 | placeholder: true 15 | }, 16 | 17 | // border:0 is unnecessary, but avoids a bug in Firefox on OSX 18 | copy = '