├── app ├── lib ├── js │ ├── sequence-worker.js │ ├── application.js │ └── sequence-index.js ├── css │ └── sequence-index.css ├── bower.json ├── data │ ├── specs.json │ ├── burtin.json │ ├── admission.json │ ├── transitions.json │ └── barley.json └── index.html ├── .gitignore ├── src ├── editOp │ ├── editOp.sh │ ├── genEditOpSet.js │ ├── editOpSet.js │ └── def.js ├── index.js ├── constants.js ├── path │ ├── index.js │ ├── evaluate.js │ ├── evaluateRules.js │ └── enumerate.js ├── sequence │ ├── TieBreaker.js │ ├── PatternOptimizer.js │ └── sequence.js ├── transition │ ├── neighbor.js │ └── apply.js └── util.js ├── test ├── exampleLoader.js ├── util.test.js ├── path │ ├── index.test.js │ ├── enumerate.test.js │ └── evaluate.test.js ├── examples │ ├── filter_sort.json │ ├── filter_modifyX_aggregate.json │ ├── input-mergedScale.json │ ├── filter_aggregate.json │ ├── addY_addColor.json │ ├── addY_aggregate_scale.json │ └── filter_and_filter.json ├── data │ ├── specs.json │ ├── burtin.json │ ├── admission.json │ ├── transitions.json │ └── barley.json ├── transition │ ├── neighbor.test.js │ └── apply.test.js └── editOpSetForTest.js ├── rollup.config.js ├── package.json ├── lib ├── TSP.js └── BEA.js └── README.md /app/lib: -------------------------------------------------------------------------------- 1 | ../lib -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | bower_components 4 | app/bower_components 5 | temp/*.json 6 | *.pptx 7 | temp/ -------------------------------------------------------------------------------- /app/js/sequence-worker.js: -------------------------------------------------------------------------------- 1 | self.onmessage = function(e) { 2 | importScripts('../js/graphscape.js'); 3 | var result = graphscape.sequence(e.data.specs, e.data.options); 4 | self.postMessage(result); 5 | }; 6 | -------------------------------------------------------------------------------- /app/css/sequence-index.css: -------------------------------------------------------------------------------- 1 | textarea { 2 | margin-bottom: 10px; 3 | } 4 | #sort{ 5 | margin: 0 10px; 6 | 7 | } 8 | .result { 9 | margin: 3 10px; 10 | } 11 | 12 | #control-result{ 13 | margin: 5px 0; 14 | } -------------------------------------------------------------------------------- /src/editOp/editOp.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | # define color 4 | RED='\033[0;31m' 5 | NC='\033[0m' # No Color 6 | 7 | 8 | node lp.js 9 | matlab -nodesktop < lp.m 10 | node genEditOpSet.js 11 | rm idMap.json encodingCeiling.json costs.json lp.m 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | module.exports = { 3 | sequence: require('./sequence/sequence.js').sequence, 4 | transition: require('./transition/trans.js').transition, 5 | apply: require('./transition/apply').apply, 6 | path: require('./path').path 7 | } 8 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | exports.TYPES = { 2 | QUANTITATIVE: 'quantitative', 3 | ORDINAL: 'ordinal', 4 | TEMPORAL: 'temporal', 5 | NOMINAL: 'nominal', 6 | GEOJSON: 'geojson' 7 | }; 8 | 9 | exports.CHANNELS = ["x", "y", "color", "shape", "size", "text", "row", "column"]; 10 | exports.OPS = ["equal", "lt", "lte", "gt", "gte", "range", "oneOf", "valid"]; 11 | exports.LOGIC_OPS = ["and", "or", "not"]; -------------------------------------------------------------------------------- /app/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vega-lite-sequence-recommender", 3 | "authors": [ 4 | "Younghoon Kim " 5 | ], 6 | "description": "", 7 | "main": "", 8 | "license": "BSD-3-Clause", 9 | "private": true, 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "vega-lite": "1.2.0", 19 | "vega": "~2.6.3", 20 | "d3": "3.*", 21 | "bootstrap": "^3.3.7", 22 | "jquery": "^1.12.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/exampleLoader.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const {copy} = require("../src/util"); 3 | 4 | const examples = {}; 5 | function loadExample (path, sub) { 6 | let collection = examples; 7 | if (sub ) { 8 | examples[sub] = examples[sub] || {}; 9 | collection = examples[sub]; 10 | } 11 | 12 | fs.readdirSync(__dirname + `/examples${path}`) 13 | .filter(filename => filename.indexOf(".json") >= 0) 14 | .forEach(filename => { 15 | 16 | let example = JSON.parse(fs.readFileSync(__dirname + `/examples${path}/` + filename)); 17 | if (example.data) { 18 | example.sSpec.data.find(dataObj => dataObj.name === "source_0").values = copy(example.data); 19 | example.eSpec.data.find(dataObj => dataObj.name === "source_0").values = copy(example.data); 20 | } 21 | 22 | collection[filename.replace(".json","")] = example; 23 | }); 24 | } 25 | 26 | loadExample(""); 27 | 28 | exports.examples = examples; -------------------------------------------------------------------------------- /test/util.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const expect = require('chai').expect; 3 | const {partition, permutate} = require("../src/util"); 4 | 5 | describe("partition", () => { 6 | it("Should enumerate all possible partitions properly", () => { 7 | let parts = partition([0,1,2,3], 1); 8 | expect(parts).to.deep.eq([[[0,1,2,3]]]); 9 | 10 | let partsBy2 = [ 11 | [[0],[1,2,3]], 12 | [[1],[0,2,3]], 13 | [[2],[0,1,3]], 14 | [[3],[0,1,2]], 15 | [[0,1],[2,3]], 16 | [[0,3],[1,2]], 17 | [[0,2],[1,3]], 18 | ] 19 | expect(partition([0,1,2,3], 2).length).to.deep.eq(partsBy2.length); 20 | }) 21 | }) 22 | describe("permute", () => { 23 | it("Should enumerate all possible permutations properly", () => { 24 | let permutations = permutate([0,1]); 25 | 26 | 27 | expect(permutations.length).to.deep.eq(2); 28 | 29 | permutations = permutate([0]); 30 | expect(permutations).to.deep.eq([[0]]); 31 | }) 32 | }) -------------------------------------------------------------------------------- /test/path/index.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const expect = require('chai').expect; 3 | const EXAMPLES = require("../exampleLoader").examples; //TODO! 4 | const { path, validateInput } = require("../../src/path/index"); 5 | 6 | describe("path", () => { 7 | it("should recommend all possible sequences for given start and end VL specs", async () => { 8 | 9 | const {start, end} = EXAMPLES.filter_aggregate; 10 | let sequences = await path(start, end); 11 | 12 | expect(sequences["2"].length).to.eq(2); 13 | expect(sequences["3"]).to.eq(undefined); 14 | }) 15 | 16 | it("should return an error if the given charts are invalid VL charts.", async () => { 17 | const {start, end} = EXAMPLES.filter_aggregate; 18 | let result = validateInput({ 19 | hconcat: [ 20 | {mark: "point", encode: {x: {field: "X"}}} 21 | ] 22 | }, end); 23 | expect(!!result.error).to.eq(true); 24 | 25 | }) 26 | 27 | it("should return specs with not merged scale when there are sort edit operations. ", async () => { 28 | 29 | const {start, end} = EXAMPLES.filter_sort; 30 | let sequences = await path(start, end); 31 | let scale = sequences['2'][0].sequence[1].encoding.x.scale; 32 | expect(sequences['2'].length).to.eq(2); 33 | expect(scale).to.eq(undefined); 34 | }) 35 | }) -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import json from '@rollup/plugin-json'; 4 | import resolve from '@rollup/plugin-node-resolve'; 5 | import bundleSize from 'rollup-plugin-bundle-size'; 6 | import {terser} from 'rollup-plugin-terser'; 7 | 8 | const extensions = ['.js']; 9 | const outputs = [ 10 | { 11 | input: 'src/index.js', 12 | output: [ 13 | { 14 | file: `graphscape.js`, 15 | format: 'umd', 16 | sourcemap: true, 17 | name: 'graphscape', 18 | globals: { 19 | vega: "vega", 20 | "vega-lite": "vl", 21 | d3: "d3" 22 | } 23 | }, 24 | { 25 | file: `graphscape.min.js`, 26 | format: 'umd', 27 | sourcemap: true, 28 | name: 'graphscape', 29 | plugins: [terser()], 30 | globals: { 31 | vega: "vega", 32 | "vega-lite": "vl", 33 | d3: "d3" 34 | } 35 | } 36 | ], 37 | plugins: [ 38 | resolve({browser: true, extensions}), 39 | commonjs(), 40 | json(), 41 | babel({ 42 | extensions, 43 | babelHelpers: 'bundled', 44 | presets: [ 45 | [ 46 | '@babel/env', 47 | { targets: 'defaults and not IE 11'} 48 | ] 49 | ] 50 | }), 51 | bundleSize() 52 | ], 53 | external: ['d3', "vega-lite", "vega"] 54 | } 55 | ]; 56 | 57 | export default outputs; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphscape", 3 | "version": "1.1.0", 4 | "description": "Measure cognitive distances among visualizations.", 5 | "main": "graphscape.js", 6 | "scripts": { 7 | "test": "mocha ./test --recursive --watch", 8 | "build": "rollup -c" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/uwdata/GraphScape.git" 13 | }, 14 | "author": { 15 | "name": "UW Interactive Data Lab", 16 | "url": "http://idl.cs.washington.edu" 17 | }, 18 | "collaborators": [ 19 | "Younghoon Kim ", 20 | "Kanit Wongsuphasawat (http://kanitw.yellowpigz.com)", 21 | "Jeffrey Heer (http://jheer.org)" 22 | ], 23 | "license": "BSD-3-Clause", 24 | "bugs": { 25 | "url": "https://github.com/uwdata/GraphScape/issues" 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "^7.12.13", 29 | "@babel/core": "^7.12.13", 30 | "@babel/preset-env": "^7.12.13", 31 | "@rollup/plugin-babel": "^5.2.3", 32 | "@rollup/plugin-commonjs": "^15.1.0", 33 | "@rollup/plugin-json": "^4.1.0", 34 | "@rollup/plugin-node-resolve": "^9.0.0", 35 | "chai": "~3.5.0", 36 | "mocha": "^8.2.1", 37 | "rollup": "^2.38.5", 38 | "rollup-plugin-bundle-size": "^1.0.3", 39 | "rollup-plugin-sourcemaps": "^0.6.3", 40 | "rollup-plugin-terser": "^7.0.2" 41 | }, 42 | "dependencies": { 43 | "d3": "^6.5.0", 44 | "vega": "^5.19.1", 45 | "vega-expression": "^3.0.1", 46 | "vega-lite": "^4.17.0" 47 | }, 48 | "homepage": "https://github.com/uwdata/GraphScape#readme" 49 | } 50 | -------------------------------------------------------------------------------- /test/examples/filter_sort.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": { 3 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 4 | "description": "A simple bar chart with embedded data.", 5 | "data": { 6 | "values": [ 7 | {"a": "A", "b": 28}, 8 | {"a": "B", "b": 55}, 9 | {"a": "C", "b": 43}, 10 | {"a": "D", "b": 91}, 11 | {"a": "E", "b": 81}, 12 | {"a": "F", "b": 53}, 13 | {"a": "G", "b": 19}, 14 | {"a": "H", "b": 87}, 15 | {"a": "I", "b": 52} 16 | ] 17 | }, 18 | "mark": "bar", 19 | "encoding": { 20 | "x": {"field": "a", "type": "nominal", "axis": {"labelAngle": 0}}, 21 | "y": {"field": "b", "type": "quantitative"} 22 | } 23 | }, 24 | "end": { 25 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 26 | "description": "A simple bar chart with embedded data.", 27 | "data": { 28 | "values": [ 29 | {"a": "A", "b": 28}, 30 | {"a": "B", "b": 55}, 31 | {"a": "C", "b": 43}, 32 | {"a": "D", "b": 91}, 33 | {"a": "E", "b": 81}, 34 | {"a": "F", "b": 53}, 35 | {"a": "G", "b": 19}, 36 | {"a": "H", "b": 87}, 37 | {"a": "I", "b": 52} 38 | ] 39 | }, 40 | "transform": [ 41 | {"filter": {"field": "a", "oneOf": ["G", "A", "C", "I", "F", "B"]}} 42 | ], 43 | "mark": "bar", 44 | "encoding": { 45 | "x": { 46 | "field": "a", 47 | "type": "nominal", 48 | "axis": {"labelAngle": 0}, 49 | "sort": {"order": "ascending", "field": "b"} 50 | }, 51 | "y": {"field": "b", "type": "quantitative"} 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/editOp/genEditOpSet.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | function keys(obj) { 3 | var k = [], x; 4 | for (x in obj) { 5 | k.push(x); 6 | } 7 | return k; 8 | }; 9 | 10 | // Generated from lp_yh01.m 11 | var costs = JSON.parse(fs.readFileSync('costs.json','utf8')); 12 | //Generated from lp_yh01.js 13 | var map = JSON.parse(fs.readFileSync('idMap.json','utf8')); 14 | var encodingCeiling = JSON.parse(fs.readFileSync('encodingCeiling.json','utf8')); 15 | 16 | 17 | var maxEncodingCost = 0; 18 | //Imports the lp result 19 | var editOpNames = keys(map); 20 | var editOpSet = { 21 | markEditOps: {}, 22 | transformEditOps: {}, 23 | encodingEditOps: {} 24 | }; 25 | 26 | for (var i = 0; i < editOpNames.length; i++) { 27 | if ( i <= 14 ) { 28 | editOpSet.markEditOps[editOpNames[i]] = { name: editOpNames[i], cost: costs[i] }; 29 | } 30 | else if( i <= 21 ){ 31 | editOpSet.transformEditOps[editOpNames[i]] = { name: editOpNames[i], cost: costs[i] }; 32 | } 33 | else{ 34 | if (maxEncodingCost < costs[i] ) { 35 | maxEncodingCost = costs[i]; 36 | } 37 | editOpSet.encodingEditOps[editOpNames[i]] = { name: editOpNames[i], cost: costs[i] }; 38 | } 39 | }; 40 | 41 | editOpSet.encodingEditOps['ceiling'] = { 42 | cost: maxEncodingCost * encodingCeiling.depth, 43 | alternatingCost: maxEncodingCost * ( encodingCeiling.depth + 1 ) 44 | }; 45 | // for (var i = 0; i < encodingExceptions.length; i++) { 46 | // editOpSet.encodingEditOps[encodingExceptions[i].name] = encodingExceptions[i]; 47 | // }; 48 | 49 | 50 | fs.writeFileSync('editOpSet.js', 'exports.DEFAULT_EDIT_OPS = ' + JSON.stringify(editOpSet)); 51 | 52 | -------------------------------------------------------------------------------- /app/js/application.js: -------------------------------------------------------------------------------- 1 | function draw(selector, spec, specAdjustment, callback){ 2 | if(specAdjustment){ 3 | for (var i = 0; i < specAdjustment.length; i++) { 4 | spec[specAdjustment[i].key] = specAdjustment[i].value 5 | } 6 | } 7 | if (spec.mark === "null") { 8 | $(selector).append(""); 9 | return; 10 | }; 11 | var vgSpec = vl.compile(spec).spec; 12 | vgSpec.description = spec.description 13 | 14 | if (spec.encoding.x && spec.encoding.x.axis && spec.encoding.x.axis.labelAngle) { 15 | vgSpec.marks[0].axes[0].properties.labels.align = "left"; 16 | }; 17 | 18 | vg.parse.spec(vgSpec, function(chart) { 19 | chart({el: selector, renderer:"svg"}).update(); 20 | 21 | if (vgSpec.description) { 22 | var yOffset = 30; 23 | var yOffsetText = 15; 24 | 25 | if(spec.encoding.column && !(typeof(spec.encoding.column.axis) !== "undefined" && spec.encoding.column.axis.labels === false)){ 26 | yOffset = 30; 27 | yOffsetText = -20; 28 | } 29 | 30 | var svg = d3.select(selector+" svg"); 31 | svg.attr("height", Number(svg.attr("height")) + yOffset) 32 | .select("g") 33 | .append("text") 34 | .text(vgSpec.description) 35 | .attr("y", yOffsetText) 36 | .attr("x",-40) 37 | .style("font-size","14px") 38 | .style("font-weight","bold") 39 | .style("font-family", "sans-serif") 40 | .style("fill", 'rgb(0, 0, 0)'); 41 | 42 | svg.select("g.mark-group") 43 | .attr("transform","translate(0," + yOffset + ")"); 44 | } 45 | 46 | if(callback){ 47 | callback(selector, spec, specAdjustment); 48 | } 49 | }); 50 | } 51 | 52 | function isEmpty( el ){ 53 | return el.length === 0; 54 | } -------------------------------------------------------------------------------- /src/path/index.js: -------------------------------------------------------------------------------- 1 | const {copy} = require("../util"); 2 | const { enumerate } = require( "./enumerate"); 3 | const { evaluate } = require( "./evaluate"); 4 | const getTransition = require('../transition/trans.js').transition 5 | 6 | async function path(sSpec, eSpec, transM) { 7 | validateInput(sSpec, eSpec); 8 | 9 | const transition = await getTransition(copy(sSpec), copy(eSpec)) 10 | const editOps = [ 11 | ...transition.mark, 12 | ...transition.transform, 13 | ...transition.encoding 14 | ]; 15 | let result = {} 16 | if (transM === undefined ) { 17 | for (let m = 1; m <= editOps.length; m++) { 18 | result[m] = await enumAndEval(sSpec, eSpec, editOps, m) 19 | } 20 | return result; 21 | } 22 | 23 | return await enumAndEval(sSpec, eSpec, editOps, transM) 24 | } 25 | exports.path = path; 26 | 27 | async function enumAndEval(sSpec, eSpec, editOps, transM) { 28 | let result = await enumerate(sSpec, eSpec, editOps, transM) 29 | return result.map((seq) => { 30 | return { 31 | ...seq, 32 | eval: evaluate(seq.editOpPartition) 33 | } 34 | }).sort((a,b) => { return b.eval.score - a.eval.score}) 35 | } 36 | 37 | function validateInput(sSpec, eSpec) { 38 | //check if specs are single-view vega-lite chart 39 | if (!isValidVLSpec(sSpec) || !isValidVLSpec(eSpec)) { 40 | return { error: "Gemini++ cannot recommend keyframes for the given Vega-Lite charts."} 41 | } 42 | } 43 | exports.validateInput = validateInput; 44 | 45 | function isValidVLSpec(spec) { 46 | if (spec.layer || spec.hconcat || spec.vconcat || spec.concat || spec.spec) { 47 | return false; 48 | } 49 | if (spec.$schema && (spec.$schema.indexOf("https://vega.github.io/schema/vega-lite") >= 0)){ 50 | return true 51 | } 52 | return false 53 | 54 | } 55 | exports.isValidVLSpec = isValidVLSpec; -------------------------------------------------------------------------------- /src/sequence/TieBreaker.js: -------------------------------------------------------------------------------- 1 | function TieBreaker(result, transitionSetsFromEmptyVis) { 2 | var TBScore = 0; 3 | var reasons = {}; 4 | //rule#1 FILTER_MODIFY, same field, same op, ascending numeric values > descending 5 | var continued = { }; 6 | var filterState = {}; 7 | var filterScore = []; 8 | var filterSequenceCost = 0; 9 | for (var i = 0; i < result.charts.length; i++) { 10 | let spec = result.charts[i]; 11 | if (spec.transform ) { 12 | let filters = spec.transform.filter(trsfm => trsfm.filter).map(trsfm => trsfm.filter); 13 | for (var j = 0; j < filters.length; j++) { 14 | let filter = filters[j]; 15 | if ( filter.hasOwnProperty("field") && filter.hasOwnProperty("equal") ) { 16 | if (filterState[filter.field]) { 17 | filterState[filter.field].push(filter.equal); 18 | } else { 19 | filterState[filter.field] = [filter.equal] ; 20 | filterScore.push({ "field": filter.field, "score": 0}); 21 | } 22 | } 23 | 24 | } 25 | } 26 | } 27 | 28 | for (var i = 0; i < filterScore.length; i++) { 29 | for (var j = 1; j < filterState[filterScore[i].field].length; j++) { 30 | if ( filterState[filterScore[i].field][j-1] < filterState[filterScore[i].field][j] ) { 31 | filterScore[i].score += 1; 32 | } else if (filterState[filterScore[i].field][j-1] > filterState[filterScore[i].field][j] ){ 33 | filterScore[i].score -= 1; 34 | } 35 | } 36 | 37 | filterSequenceCost += Math.abs(filterScore[i].score + 0.1) / ( filterState[filterScore[i].field].length - 1 + 0.1 ); 38 | } 39 | 40 | filterSequenceCost = filterScore.length > 0 ? 1 - filterSequenceCost / filterScore.length : 0; 41 | 42 | return { 'tiebreakCost' : filterSequenceCost, 'reasons': filterScore }; 43 | } 44 | 45 | 46 | 47 | module.exports = { 48 | TieBreaker: TieBreaker 49 | }; 50 | -------------------------------------------------------------------------------- /app/data/specs.json: -------------------------------------------------------------------------------- 1 | [{"description":"USA cars in 1976","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1976 && datum.Origin === 'A'"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}},{"description":"European cars in 1976","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1976 && datum.Origin === 'B'"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}},{"description":"USA cars in 1980","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1980 && datum.Origin === 'A'"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}},{"description":"European cars in 1980","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1980 && datum.Origin === 'B'"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}},{"description":"Cars in 1976","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1976"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}},{"description":"Cars in 1980","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1980"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}}] -------------------------------------------------------------------------------- /test/data/specs.json: -------------------------------------------------------------------------------- 1 | [{"description":"USA cars in 1976","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1976 && datum.Origin === 'A'"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}},{"description":"European cars in 1976","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1976 && datum.Origin === 'B'"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}},{"description":"USA cars in 1980","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1980 && datum.Origin === 'A'"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}},{"description":"European cars in 1980","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1980 && datum.Origin === 'B'"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}},{"description":"Cars in 1976","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1976"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}},{"description":"Cars in 1980","data":{"url":"data/cars.json"},"transform":{"filter":" datum.Year === 1980"},"mark":"point","encoding":{"x":{"field":"Horsepower","type":"quantitative","bin":true},"y":{"field":"Weight_in_lbs","type":"quantitative","aggregate":"mean"},"size":{"field":"*","type":"quantitative","aggregate":"count"}}}] -------------------------------------------------------------------------------- /lib/TSP.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | function TSP(matrix, value, fixFirst){ 3 | var head,sequences; 4 | function enumSequences(arr){ 5 | var out = []; 6 | if (arr.length === 1) { 7 | out.push(arr); 8 | return out; 9 | } 10 | else { 11 | for (var i = 0; i < arr.length; i++) { 12 | var arrTemp = JSON.parse(JSON.stringify(arr)); 13 | var head = arrTemp.splice(i,1); 14 | enumSequences(arrTemp).map(function(seq){ 15 | out.push(head.concat(seq)); 16 | }); 17 | } 18 | return out; 19 | } 20 | } 21 | 22 | var sequence = matrix[0].map(function(elem, i){ 23 | return i; 24 | }); 25 | 26 | if (!isNaN(fixFirst)) { 27 | head = sequence.splice(fixFirst,1); 28 | sequences = enumSequences(sequence).map(function(elem){ 29 | return head.concat(elem); 30 | }); 31 | 32 | } 33 | else{ 34 | sequences = enumSequences(sequence); 35 | } 36 | 37 | var minDistance = Infinity; 38 | var argMin = 0; 39 | var distance = 0; 40 | var out = []; 41 | var all = []; 42 | for (var i = 0; i < sequences.length; i++) { 43 | if (i*100/sequences.length %10 === 0) { 44 | // process.std.out(i*100/sequences.length); 45 | } 46 | 47 | 48 | for (var j = 0; j < sequences[i].length-1; j++) { 49 | distance += matrix[sequences[i][j]][sequences[i][j+1]][value]; 50 | } 51 | distance = Math.round(distance*10000)/10000; 52 | all.push({sequence: sequences[i], distance: distance}); 53 | 54 | if (distance <= minDistance ) { 55 | 56 | if (distance === minDistance) { 57 | out.push({sequence: sequences[i], distance: minDistance}); 58 | } 59 | else{ 60 | out = []; 61 | out.push({sequence: sequences[i], distance: distance}); 62 | } 63 | argMin = i; 64 | minDistance = distance; 65 | // console.log(i,minDistance); 66 | } 67 | distance = 0; 68 | } 69 | 70 | return {out: out, all: all}; 71 | } 72 | 73 | 74 | // var matrix = JSON.parse(fs.readFileSync(process.argv[2],'utf8')); 75 | // var fixFirst = Number(process.argv[3]); 76 | 77 | // console.log(TSP(matrix,"rank",fixFirst)); 78 | 79 | module.exports = { 80 | TSP: TSP 81 | }; 82 | -------------------------------------------------------------------------------- /test/examples/filter_modifyX_aggregate.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "sequence": [ 4 | { 5 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 6 | "data": {"url": "test/data/sample_kc_house_data.json"}, 7 | "mark": "point", 8 | "transform": [ 9 | {"calculate": "datum.price/datum.sqft_living", "as": "price_per_sqft"} 10 | ], 11 | "encoding": { 12 | "x": {"field": "price", "type": "quantitative", "scale": {"zero": false}}, 13 | "y": { 14 | "field": "sqft_living", 15 | "type": "quantitative", 16 | "scale": {"zero": false}, 17 | "axis": {"labelFlush": true} 18 | }, 19 | "color": {"field": "bedrooms", "type": "nominal"} 20 | } 21 | }, 22 | { 23 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 24 | "data": {"url": "test/data/sample_kc_house_data.json"}, 25 | "mark": "bar", 26 | "transform": [ 27 | {"calculate": "datum.price/datum.sqft_living", "as": "price_per_sqft"} 28 | ], 29 | "encoding": { 30 | "x": {"field": "bedrooms", "type": "nominal", "scale": {"zero": false}}, 31 | "y": { 32 | "field": "sqft_living", 33 | "type": "quantitative", 34 | "scale": {"zero": false}, 35 | "aggregate": "mean" 36 | }, 37 | "color": {"field": "bedrooms", "type": "nominal"} 38 | } 39 | } 40 | ], 41 | "opt": { 42 | "totalDuration": 4000, 43 | "marks": { 44 | "marks": {"change": {"data": ["name"]}} 45 | }, 46 | "axes": { 47 | "x": {"change": { "scale": {"domainDimension": "same"} }}, 48 | "y": {"change": { "scale": {"domainDimension": "same"} }} 49 | }, 50 | "scales": {"x": {"domainDimension": "same"}, "y": {"domainDimension": "same"}} 51 | }, 52 | "gemSpecs": [ 53 | { 54 | "timeline": { 55 | "sync": [ 56 | { 57 | "component": {"axis": "x"}, 58 | "change": {"scale": {"domainDimension": "same"}}, 59 | "timing": {"duration": 4000} 60 | }, 61 | { 62 | "component": {"axis": "y"}, 63 | "timing": {"duration": 4000} 64 | }, 65 | { 66 | "component": "view", 67 | "timing": {"duration": 4000} 68 | }, 69 | { 70 | "component": {"mark": "marks"}, 71 | "change": {"data": ["name"]}, 72 | "timing": {"duration": 4000} 73 | } 74 | ] 75 | } 76 | } 77 | ] 78 | } -------------------------------------------------------------------------------- /test/examples/input-mergedScale.json: -------------------------------------------------------------------------------- 1 | { 2 | "charts":[ 3 | { 4 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 5 | "data": { 6 | "values": [ 7 | {"Hungry": 10, "Name": "Gemini"}, 8 | {"Hungry": 60, "Name": "Cordelia"}, 9 | {"Hungry": 80, "Name": "Gemini"}, 10 | {"Hungry": 100, "Name": "Cordelia"}, 11 | {"Hungry": 40, "Name": "Mango"}, 12 | {"Hungry": 100, "Name": "Mango"} 13 | ] 14 | }, 15 | "mark": "point", 16 | "encoding": { 17 | "x": { "field": "Hungry", "type": "quantitative"}, 18 | "y": { "field": "Name", "type": "nominal"} 19 | } 20 | }, 21 | { 22 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 23 | "data": { 24 | "values": [ 25 | {"Hungry": 10, "Name": "Gemini"}, 26 | {"Hungry": 60, "Name": "Cordelia"}, 27 | {"Hungry": 80, "Name": "Gemini"}, 28 | {"Hungry": 100, "Name": "Cordelia"}, 29 | {"Hungry": 40, "Name": "Mango"}, 30 | {"Hungry": 100, "Name": "Mango"} 31 | ] 32 | }, 33 | "transform": [{"filter": {"field": "Name", "equal": "Gemini"}}], 34 | "mark": "point", 35 | "encoding": { 36 | "x": { "field": "Hungry", "type": "quantitative", "aggregate": "mean"}, 37 | "y": { "field": "Name", "type": "nominal"} 38 | } 39 | } 40 | ], 41 | "gemSpecs": [ 42 | { 43 | "timeline": { 44 | "sync": [ 45 | { 46 | "component": {"axis": "x"}, 47 | "change": {"scale": {"domainDimension": "same"}}, 48 | "timing": {"duration": 1000} 49 | }, 50 | { 51 | "component": {"axis": "y"}, 52 | "change": {"scale": {"domainDimension": "same"}}, 53 | "timing": {"duration": 1000} 54 | }, 55 | { 56 | "component": "view", "timing": {"duration": 1000} 57 | }, 58 | { 59 | "component": {"mark": "marks"}, 60 | "change": {"data": ["Name"]}, 61 | "timing": {"duration": 1000} 62 | } 63 | ] 64 | } 65 | }, 66 | { 67 | "timeline": { 68 | "sync": [ 69 | { 70 | "component": {"axis": "x"}, 71 | "change": {"scale": {"domainDimension": "same"}}, 72 | "timing": {"duration": 1000} 73 | }, 74 | { 75 | "component": "view", "timing": {"duration": 1000} 76 | }, 77 | { 78 | "component": {"axis": "y"}, 79 | "change": {"scale": {"domainDimension": "same"}}, 80 | "timing": {"duration": 1000} 81 | }, 82 | { 83 | "component": {"mark": "marks"}, 84 | "change": {"data": ["Name"]}, 85 | "timing": {"duration": 1000} 86 | } 87 | ] 88 | } 89 | } 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /app/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/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 | ] -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Sequence 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 |
33 | 34 | 35 | 36 |
37 | 41 |
42 |
43 |
44 | 45 | 55 |
56 |
57 | 58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 68 |
69 |
70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /test/transition/neighbor.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var expect = require('chai').expect; 3 | var editOpSet = require('../editOpSetForTest'); 4 | 5 | const { TYPES } = require('../../src/constants'); 6 | var neighbor = require('../../src/transition/neighbor'); 7 | 8 | describe('transition.neighbor', function () { 9 | it('should return all neighbors linked by encdoeTransition.', function () { 10 | var testVL = { 11 | "data": { "url": "data/cars.json" }, 12 | "mark": "tick", 13 | "encoding": { 14 | "x": { "field": "Horsepower", "type": "quantitative" } 15 | } 16 | }; 17 | var additionalFields = [{ "field": "Origin", "type": TYPES.ORDINAL }]; 18 | var additionalChannels = ["y"]; 19 | 20 | var result = neighbor.neighbors(testVL, additionalFields, additionalChannels, editOpSet.DEFAULT_EDIT_OPS.encodingEditOps); 21 | expect(result.length).to.eq(4); 22 | }); 23 | it('should return neighbors with _COUNT edit operations correctly', function () { 24 | var testVL = { 25 | "data": { "url": "data/cars.json" }, 26 | "mark": "tick", 27 | "encoding": { 28 | "x": { "field": "*", "type": "quantitative", "aggregate": "count" } 29 | } 30 | }; 31 | var additionalFields = [{ "field": "*", "type": TYPES.QUANTITATIVE, "aggregate": "count" }]; 32 | var additionalChannels = ["y"]; 33 | var result = neighbor.neighbors(testVL, additionalFields, additionalChannels, editOpSet.DEFAULT_EDIT_OPS.encodingEditOps); 34 | expect(result[0].editOp.name).to.eq("REMOVE_X_COUNT"); 35 | expect(result[2].editOp.name).to.eq("ADD_Y_COUNT"); 36 | }); 37 | it('should return only a neighbor with SWAP_X_Y edit operations', function () { 38 | var testVL = { 39 | "encoding": { 40 | "column": { "field": "Cylinders", "type": "ordinal" }, 41 | "x": { 42 | "field": "Origin", "type": "nominal", 43 | "axis": { "labels": false, "title": "", "tickSize": 0 } 44 | }, 45 | "y": { "scale": 'hey', "aggregate": "mean", "field": "Acceleration", "type": "quantitative" }, 46 | } 47 | }; 48 | var additionalFields = []; 49 | var additionalChannels = []; 50 | var result = neighbor.neighbors(testVL, additionalFields, additionalChannels, editOpSet.DEFAULT_EDIT_OPS.encodingEditOps); 51 | expect(result[2].editOp.name).to.eq("SWAP_X_Y"); 52 | expect(result.length).to.eq(4); 53 | }); 54 | it('should return neighbors regardless redundant additionalFields', function () { 55 | var testVL = { 56 | "encoding": { 57 | "x": { 58 | "type": "quantitative", 59 | "field": "Acceleration", 60 | "bin": true 61 | }, 62 | "y": { 63 | "type": "quantitative", 64 | "field": "*", 65 | "scale": { "type": "log" }, 66 | "aggregate": "count" 67 | } 68 | } 69 | }; 70 | var additionalFields = [ 71 | { "field": "Acceleration", "type": TYPES.QUANTITATIVE }, 72 | { "field": "Horsepower", "type": TYPES.QUANTITATIVE }]; 73 | var additionalChannels = []; 74 | var result = neighbor.neighbors(testVL, additionalFields, additionalChannels, editOpSet.DEFAULT_EDIT_OPS.encodingEditOps); 75 | expect(result.length).to.eq(6); 76 | }); 77 | }); 78 | //# sourceMappingURL=neighbor.test.js.map -------------------------------------------------------------------------------- /src/path/evaluate.js: -------------------------------------------------------------------------------- 1 | const RULES = require("./evaluateRules").HEURISTIC_RULES 2 | const {copy, intersection} = require("../util"); 3 | 4 | function evaluate(editOpPartition) { 5 | let satisfiedRules = findRules(editOpPartition, RULES); 6 | let score = satisfiedRules.reduce((score, rule) => { 7 | return score + rule.score 8 | }, 0) 9 | return {score, satisfiedRules} 10 | } 11 | exports.evaluate = evaluate; 12 | 13 | function findRules(editOpPartition, rules = RULES) { 14 | return rules.filter(_rule => { 15 | let rule = copy(_rule); 16 | for (let j = 0; j < rule.editOps.length; j++) { 17 | const ruleEditOp = rule.editOps[j]; 18 | rule[ruleEditOp] = []; 19 | 20 | for (let i = 0; i < editOpPartition.length; i++) { 21 | const editOpPart = editOpPartition[i]; 22 | let newFoundEditOps = findEditOps(editOpPart, ruleEditOp) 23 | 24 | if (newFoundEditOps.length > 0) { 25 | rule[ruleEditOp] = [ 26 | ...rule[ruleEditOp], 27 | ...newFoundEditOps.map(eo => { 28 | return {...eo, position: i} 29 | }) 30 | ]; 31 | } 32 | } 33 | 34 | if (rule[ruleEditOp].length === 0) { 35 | return false; // when there is no corresponding edit op for the rule in given editOp partition. 36 | } 37 | } 38 | 39 | if (rule.type === "A-With-B"){ 40 | let foundEditOps = rule.editOps.map(eo => rule[eo]); 41 | 42 | if (foundEditOps.filter(eo => !eo).length !== 0) { 43 | return false; 44 | } 45 | let positions = rule.editOps.reduce((positions, eo, i) => { 46 | let currPositions = rule[eo].map(d => d.position); 47 | if (i === 0){ 48 | return currPositions 49 | } 50 | return intersection(positions, currPositions) 51 | }, []) 52 | 53 | 54 | 55 | if (positions.length === 0) { 56 | return false 57 | } else if (_rule.condition) { 58 | let mappedFoundEditOps = rule.editOps.reduce((acc, eo) => { 59 | acc[eo] = rule[eo] 60 | return acc; 61 | }, {}); 62 | 63 | return _rule.condition(mappedFoundEditOps); 64 | } 65 | return true; 66 | 67 | } else { 68 | for (let i = 0; i < rule[rule.editOps[0]].length; i++) { 69 | const followed = rule[rule.editOps[0]][i]; 70 | for (let j = 0; j < rule[rule.editOps[1]].length; j++) { 71 | const following = rule[rule.editOps[1]][j]; 72 | if (followed.position >= following.position) { 73 | return false 74 | } 75 | 76 | if (_rule.condition && !_rule.condition(followed, following)) { 77 | return false; 78 | } 79 | } 80 | } 81 | return true; 82 | } 83 | 84 | }); 85 | } 86 | exports.findRules = findRules; 87 | 88 | function findEditOps(editOps, query) { 89 | return editOps.filter(eo => { 90 | if (query === "TRANSFORM") { 91 | return eo.type === "transform" 92 | } else if (query === "ENCODING") { 93 | return eo.type === "encoding" 94 | } else if (query === "MARK") { 95 | return eo.type === "mark" 96 | } else if (query === "ENCODING.MODIFY") { 97 | return eo.type === "encoding" && eo.name.indexOf("MODIFY") >= 0 98 | } 99 | return (eo.name.indexOf(query) >= 0) 100 | }) 101 | } -------------------------------------------------------------------------------- /app/data/admission.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Status":"Admit", 4 | "Gender":"Male", 5 | "Department":"Astronomy", 6 | "Count":512, 7 | "Rate":62.06 8 | }, 9 | { 10 | "Status":"Reject", 11 | "Gender":"Male", 12 | "Department":"Astronomy", 13 | "Count":313, 14 | "Rate":37.94 15 | }, 16 | { 17 | "Status":"Admit", 18 | "Gender":"Female", 19 | "Department":"Astronomy", 20 | "Count":89, 21 | "Rate":82.41 22 | }, 23 | { 24 | "Status":"Reject", 25 | "Gender":"Female", 26 | "Department":"Astronomy", 27 | "Count":19, 28 | "Rate":17.59 29 | }, 30 | { 31 | "Status":"Admit", 32 | "Gender":"Male", 33 | "Department":"Biology", 34 | "Count":22, 35 | "Rate":5.9 36 | }, 37 | { 38 | "Status":"Reject", 39 | "Gender":"Male", 40 | "Department":"Biology", 41 | "Count":351, 42 | "Rate":94.1 43 | }, 44 | { 45 | "Status":"Admit", 46 | "Gender":"Female", 47 | "Department":"Biology", 48 | "Count":24, 49 | "Rate":7.04 50 | }, 51 | { 52 | "Status":"Reject", 53 | "Gender":"Female", 54 | "Department":"Biology", 55 | "Count":317, 56 | "Rate":92.96 57 | }, 58 | { 59 | "Status":"Admit", 60 | "Gender":"Male", 61 | "Department":"Law", 62 | "Count":138, 63 | "Rate":33.09 64 | }, 65 | { 66 | "Status":"Reject", 67 | "Gender":"Male", 68 | "Department":"Law", 69 | "Count":279, 70 | "Rate":66.91 71 | }, 72 | { 73 | "Status":"Admit", 74 | "Gender":"Female", 75 | "Department":"Law", 76 | "Count":131, 77 | "Rate":34.93 78 | }, 79 | { 80 | "Status":"Reject", 81 | "Gender":"Female", 82 | "Department":"Law", 83 | "Count":244, 84 | "Rate":65.07 85 | }, 86 | { 87 | "Status":"Admit", 88 | "Gender":"Male", 89 | "Department":"Physics", 90 | "Count":353, 91 | "Rate":63.04 92 | }, 93 | { 94 | "Status":"Reject", 95 | "Gender":"Male", 96 | "Department":"Physics", 97 | "Count":207, 98 | "Rate":36.96 99 | }, 100 | { 101 | "Status":"Admit", 102 | "Gender":"Female", 103 | "Department":"Physics", 104 | "Count":17, 105 | "Rate":68 106 | }, 107 | { 108 | "Status":"Reject", 109 | "Gender":"Female", 110 | "Department":"Physics", 111 | "Count":8, 112 | "Rate":32 113 | }, 114 | { 115 | "Status":"Admit", 116 | "Gender":"Male", 117 | "Department":"Psychology", 118 | "Count":120, 119 | "Rate":36.92 120 | }, 121 | { 122 | "Status":"Reject", 123 | "Gender":"Male", 124 | "Department":"Psychology", 125 | "Count":205, 126 | "Rate":63.08 127 | }, 128 | { 129 | "Status":"Admit", 130 | "Gender":"Female", 131 | "Department":"Psychology", 132 | "Count":202, 133 | "Rate":34.06 134 | }, 135 | { 136 | "Status":"Reject", 137 | "Gender":"Female", 138 | "Department":"Psychology", 139 | "Count":391, 140 | "Rate":65.94 141 | }, 142 | { 143 | "Status":"Admit", 144 | "Gender":"Male", 145 | "Department":"Sociology", 146 | "Count":53, 147 | "Rate":27.75 148 | }, 149 | { 150 | "Status":"Reject", 151 | "Gender":"Male", 152 | "Department":"Sociology", 153 | "Count":138, 154 | "Rate":72.25 155 | }, 156 | { 157 | "Status":"Admit", 158 | "Gender":"Female", 159 | "Department":"Sociology", 160 | "Count":94, 161 | "Rate":23.92 162 | }, 163 | { 164 | "Status":"Reject", 165 | "Gender":"Female", 166 | "Department":"Sociology", 167 | "Count":299, 168 | "Rate":76.08 169 | } 170 | ] 171 | -------------------------------------------------------------------------------- /test/data/admission.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Status":"Admit", 4 | "Gender":"Male", 5 | "Department":"Astronomy", 6 | "Count":512, 7 | "Rate":62.06 8 | }, 9 | { 10 | "Status":"Reject", 11 | "Gender":"Male", 12 | "Department":"Astronomy", 13 | "Count":313, 14 | "Rate":37.94 15 | }, 16 | { 17 | "Status":"Admit", 18 | "Gender":"Female", 19 | "Department":"Astronomy", 20 | "Count":89, 21 | "Rate":82.41 22 | }, 23 | { 24 | "Status":"Reject", 25 | "Gender":"Female", 26 | "Department":"Astronomy", 27 | "Count":19, 28 | "Rate":17.59 29 | }, 30 | { 31 | "Status":"Admit", 32 | "Gender":"Male", 33 | "Department":"Biology", 34 | "Count":22, 35 | "Rate":5.9 36 | }, 37 | { 38 | "Status":"Reject", 39 | "Gender":"Male", 40 | "Department":"Biology", 41 | "Count":351, 42 | "Rate":94.1 43 | }, 44 | { 45 | "Status":"Admit", 46 | "Gender":"Female", 47 | "Department":"Biology", 48 | "Count":24, 49 | "Rate":7.04 50 | }, 51 | { 52 | "Status":"Reject", 53 | "Gender":"Female", 54 | "Department":"Biology", 55 | "Count":317, 56 | "Rate":92.96 57 | }, 58 | { 59 | "Status":"Admit", 60 | "Gender":"Male", 61 | "Department":"Law", 62 | "Count":138, 63 | "Rate":33.09 64 | }, 65 | { 66 | "Status":"Reject", 67 | "Gender":"Male", 68 | "Department":"Law", 69 | "Count":279, 70 | "Rate":66.91 71 | }, 72 | { 73 | "Status":"Admit", 74 | "Gender":"Female", 75 | "Department":"Law", 76 | "Count":131, 77 | "Rate":34.93 78 | }, 79 | { 80 | "Status":"Reject", 81 | "Gender":"Female", 82 | "Department":"Law", 83 | "Count":244, 84 | "Rate":65.07 85 | }, 86 | { 87 | "Status":"Admit", 88 | "Gender":"Male", 89 | "Department":"Physics", 90 | "Count":353, 91 | "Rate":63.04 92 | }, 93 | { 94 | "Status":"Reject", 95 | "Gender":"Male", 96 | "Department":"Physics", 97 | "Count":207, 98 | "Rate":36.96 99 | }, 100 | { 101 | "Status":"Admit", 102 | "Gender":"Female", 103 | "Department":"Physics", 104 | "Count":17, 105 | "Rate":68 106 | }, 107 | { 108 | "Status":"Reject", 109 | "Gender":"Female", 110 | "Department":"Physics", 111 | "Count":8, 112 | "Rate":32 113 | }, 114 | { 115 | "Status":"Admit", 116 | "Gender":"Male", 117 | "Department":"Psychology", 118 | "Count":120, 119 | "Rate":36.92 120 | }, 121 | { 122 | "Status":"Reject", 123 | "Gender":"Male", 124 | "Department":"Psychology", 125 | "Count":205, 126 | "Rate":63.08 127 | }, 128 | { 129 | "Status":"Admit", 130 | "Gender":"Female", 131 | "Department":"Psychology", 132 | "Count":202, 133 | "Rate":34.06 134 | }, 135 | { 136 | "Status":"Reject", 137 | "Gender":"Female", 138 | "Department":"Psychology", 139 | "Count":391, 140 | "Rate":65.94 141 | }, 142 | { 143 | "Status":"Admit", 144 | "Gender":"Male", 145 | "Department":"Sociology", 146 | "Count":53, 147 | "Rate":27.75 148 | }, 149 | { 150 | "Status":"Reject", 151 | "Gender":"Male", 152 | "Department":"Sociology", 153 | "Count":138, 154 | "Rate":72.25 155 | }, 156 | { 157 | "Status":"Admit", 158 | "Gender":"Female", 159 | "Department":"Sociology", 160 | "Count":94, 161 | "Rate":23.92 162 | }, 163 | { 164 | "Status":"Reject", 165 | "Gender":"Female", 166 | "Department":"Sociology", 167 | "Count":299, 168 | "Rate":76.08 169 | } 170 | ] 171 | -------------------------------------------------------------------------------- /src/sequence/PatternOptimizer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | //PatternOptimizer 3 | function score(coverage, uniqTransitionSets, appear, patternArray){ 4 | var R = patternArray.reduce(function(prev, curr){ 5 | prev += uniqTransitionSets[curr].tr.cost 6 | return prev; 7 | },0); 8 | R = patternArray.length * 4 / R; 9 | R = R > 1 ? 1 : R; 10 | 11 | return Math.round(1000000*(1-1/appear.length)*R*coverage)/1000000; 12 | } 13 | 14 | function scoreSimple(coverage, patternLength, inputLength){ 15 | var w_c = 1, w_l = 0; 16 | 17 | return (coverage * w_c + patternLength / inputLength * w_l) / (w_c + w_l); 18 | } 19 | 20 | function PatternOptimizer(inputArray, uniqTransitionSets) { 21 | var Optimized = [], maxScore = 0; 22 | // var inputDistance = distance(inputArray, uniqTransitionSets); 23 | 24 | for (var l = 1; l <= inputArray.length; l++) { 25 | 26 | for (var i = 0; i < inputArray.length-l+1; i++) { 27 | var appear = [i]; 28 | for (var j = 0; j < inputArray.length-l+1; j++) { 29 | if ( i !== j && isSameSub(inputArray, i, i + (l-1), j, j + (l-1))) { 30 | appear.push(j); 31 | } 32 | } 33 | var overlap = false; 34 | 35 | var rythmic = true; 36 | var period = 0; 37 | for (var k = 0; k < appear.length-1; k++) { 38 | if(appear[k+1] - appear[k] < l){ 39 | overlap = true; 40 | break; 41 | } 42 | // if(period !== 0 && period !== appear[k+1] - appear[k]){ 43 | // rythmic = false; 44 | // break; 45 | // } 46 | // period = appear[k+1] - appear[k]; 47 | } 48 | 49 | // if (appear.length > 1 && !overlap && rythmic ){ 50 | if (appear.length > 1 && !overlap){ 51 | 52 | var newPattern = dup(inputArray).splice(i,l); 53 | var RPcoverage = coverage(inputArray, l, appear); 54 | 55 | if( !Optimized.find(function(rp){ return s(rp.pattern) === s(newPattern); }) ){ 56 | newPattern = { 'pattern': newPattern, 'appear': appear, 'coverage': RPcoverage }; 57 | newPattern.patternScore = scoreSimple(newPattern.coverage, l, inputArray.length ); 58 | 59 | if (newPattern.patternScore > maxScore) { 60 | maxScore = newPattern.patternScore; 61 | Optimized = [ newPattern ]; 62 | } else if ( newPattern.patternScore === maxScore ) { 63 | Optimized.push(newPattern); 64 | } 65 | 66 | } 67 | } 68 | } 69 | } 70 | 71 | 72 | return Optimized; 73 | } 74 | function distance(trArray, uniqTransitionSets){ 75 | return trArray.reduce(function(prev,curr){ 76 | prev += uniqTransitionSets[curr].tr.cost 77 | return prev; },0); 78 | } 79 | function coverage(array, Patternlength, appear){ 80 | var s, coverage = 0; 81 | for (var i = 0; i < appear.length-1; i++) { 82 | s=i; 83 | while ( appear[i] + Patternlength > appear[i+1] ) { 84 | i++; 85 | } 86 | coverage += appear[i] + Patternlength - appear[s]; 87 | 88 | }; 89 | if (i===appear.length-1) { 90 | coverage += Patternlength; 91 | }; 92 | 93 | return coverage / array.length; 94 | } 95 | 96 | function isSameSub(array, i1, f1, i2, f2) { 97 | for (var i = 0; i < (f1-i1+1); i++) { 98 | if (array[i1+i] !== array[i2+i]) { 99 | return false; 100 | } 101 | } 102 | return true; 103 | } 104 | function s(a) { 105 | return JSON.stringify(a); 106 | } 107 | function dup(a) { 108 | return JSON.parse(s(a)); 109 | } 110 | 111 | // console.log(PatternOptimizer("231111".split(''),[1,1,1,1])); 112 | // console.log(coverage("sdsdxxxasdsdsdaasdsdsdsdsdsdsdsd".split(''), 2, [ 0, 2, 8, 10, 12, 16, 18, 20, 22, 24, 26, 28, 30 ])) 113 | module.exports = { 114 | PatternOptimizer: PatternOptimizer 115 | }; 116 | -------------------------------------------------------------------------------- /lib/BEA.js: -------------------------------------------------------------------------------- 1 | function Matrix(valueAttr, rowN, colN){ 2 | var that = this; 3 | var data; 4 | if (arguments[1] !== undefined && arguments[2] !== undefined) { 5 | data = new Array(rowN); 6 | for (var i = 0; i < data.length; i++) { 7 | data[i] = new Array(colN); 8 | for (var j = 0; j < data[i].length; j++) { 9 | data[i][j] = {}; 10 | data[i][j][valueAttr] = Math.floor(Math.random()*100)%2; 11 | } 12 | } 13 | } 14 | else { 15 | data =[]; 16 | } 17 | this.valueAttr = valueAttr; 18 | this.import = function(doubleArray){ 19 | data = doubleArray; 20 | that.rows = data; 21 | } 22 | this.col = function(j){ 23 | return data.map(function(row){ 24 | return row[j]; 25 | }); 26 | } 27 | this.colN = function(j){ 28 | return data[0].length; 29 | } 30 | this.rows = data; 31 | this.row = function(i){ 32 | return data[i]; 33 | } 34 | this.rowN = function(){ 35 | return data.length; 36 | } 37 | this.display = function(){ 38 | for (var i = 0; i < data.length; i++) { 39 | var line = []; 40 | for (var j = 0; j < data[i].length; j++) { 41 | line.push(data[i][j][valueAttr]); 42 | } 43 | console.log(line.join(" ")); 44 | } 45 | return this 46 | } 47 | this.pushRow = function(row){ 48 | data.push(row); 49 | } 50 | this.transpose = function(){ 51 | var newData = []; 52 | for (var j = 0; j < that.colN(); j++) { 53 | newData.push(that.col(j)) 54 | } 55 | data = JSON.parse(JSON.stringify(newData)); 56 | that.rows = data; 57 | return this; 58 | } 59 | } 60 | 61 | 62 | function BEA(M, options){ 63 | function bondEnergy(v1, v2, valueAttr){ 64 | var BE = 0; 65 | for (var i = 0; i < v1.length; i++) { 66 | BE += Math.pow(v1[i][valueAttr] - v2[i][valueAttr],2); 67 | } 68 | return BE 69 | } 70 | function BEArow(RM,options){ 71 | var CM = new Matrix(RM.valueAttr); 72 | var fixedFirst; 73 | if (options.fixFirst) { 74 | fixedFirst = RM.rows.splice(0,1); 75 | } 76 | 77 | var selectedRow = RM.rows.splice(0,1); 78 | CM.pushRow(selectedRow[0]); 79 | 80 | var minBE = 0, minBE_CM_i = 0, minRemained_i = 0; 81 | 82 | while ( RM.rowN() > 0) { 83 | minBE = Infinity; 84 | minBE_CM_i = 0; 85 | minRemained_i = 0; 86 | 87 | for (var remained_i = 0; remained_i < RM.rowN(); remained_i++) { 88 | var remainedRow = RM.row(remained_i); 89 | for (var CM_i = 0; CM_i <= CM.rowN(); CM_i++) { 90 | var BE = 0; 91 | 92 | if (CM_i === 0) { 93 | BE = bondEnergy(CM.row(CM_i), remainedRow, RM.valueAttr); 94 | 95 | if (options.fixFirst) { 96 | BE += bondEnergy(fixedFirst, remainedRow, RM.valueAttr); 97 | } 98 | } 99 | else if( CM_i === CM.rowN()) { 100 | BE = bondEnergy(CM.row(CM_i-1), remainedRow, RM.valueAttr); 101 | } 102 | else { 103 | BE = bondEnergy(CM.row(CM_i-1), remainedRow, RM.valueAttr) + bondEnergy(CM.row(CM_i), remainedRow, RM.valueAttr) - bondEnergy(CM.row(CM_i), CM.row(CM_i-1), RM.valueAttr) 104 | } 105 | 106 | if (minBE > BE) { 107 | minBE = BE; 108 | minBE_CM_i = CM_i; 109 | minRemained_i = remained_i; 110 | } 111 | } 112 | } 113 | 114 | CM.rows.splice(minBE_CM_i, 0, RM.rows.splice(minRemained_i,1)[0]); 115 | } 116 | if(options.fixFirst){ 117 | CM.rows.splice(0,0,fixedFirst[0]); 118 | } 119 | return CM; 120 | } 121 | var CM1 = BEArow(M, options); 122 | return BEArow(CM1.transpose(), options).transpose(); 123 | } 124 | 125 | 126 | // module.exports = { 127 | // Matrix: Matrix, 128 | // BEA: BEA 129 | // }; 130 | -------------------------------------------------------------------------------- /app/data/transitions.json: -------------------------------------------------------------------------------- 1 | [[{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":6.22},{"marktype":[],"transform":[],"filter":[{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":3.13},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":6.24}],[{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":6.22},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":3.13},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":6.24}],[{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":6.22},{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":6.24},{"marktype":[],"transform":[],"filter":[{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":3.13}],[{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":6.22},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":6.24},{"marktype":[],"transform":[],"filter":[{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":3.13}],[{"marktype":[],"transform":[],"filter":[{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":3.12},{"marktype":[],"transform":[],"filter":[{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":3.12},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":6.23},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":6.23},{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11}],[{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":6.23},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":6.23},{"marktype":[],"transform":[],"filter":[{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":3.12},{"marktype":[],"transform":[],"filter":[{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":3.12},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0}]] -------------------------------------------------------------------------------- /test/data/transitions.json: -------------------------------------------------------------------------------- 1 | [[{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":6.22},{"marktype":[],"transform":[],"filter":[{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":3.13},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":6.24}],[{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":6.22},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":3.13},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":6.24}],[{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":6.22},{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":6.24},{"marktype":[],"transform":[],"filter":[{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":3.13}],[{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":6.22},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":6.24},{"marktype":[],"transform":[],"filter":[{"name":"REMOVE_FILTER","cost":3.13}],"encoding":[],"cost":3.13}],[{"marktype":[],"transform":[],"filter":[{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":3.12},{"marktype":[],"transform":[],"filter":[{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":3.12},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":6.23},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":6.23},{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11}],[{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":6.23},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11},{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":6.23},{"marktype":[],"transform":[],"filter":[{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":3.12},{"marktype":[],"transform":[],"filter":[{"name":"ADD_FILTER","cost":3.12}],"encoding":[],"cost":3.12},{"marktype":[],"transform":[],"filter":[{"name":"MODIFY_FILTER","cost":3.11}],"encoding":[],"cost":3.11},{"marktype":[],"transform":[],"filter":[],"encoding":[],"cost":0}]] -------------------------------------------------------------------------------- /test/examples/filter_aggregate.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": { 3 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 4 | "data": { 5 | "values": [ 6 | {"Hungry": 10, "Name": "Gemini"}, 7 | {"Hungry": 60, "Name": "Cordelia"}, 8 | {"Hungry": 80, "Name": "Gemini"}, 9 | {"Hungry": 100, "Name": "Cordelia"} 10 | ] 11 | }, 12 | "mark": "point", 13 | "encoding": { 14 | "x": { "field": "Hungry", "type": "quantitative"} 15 | } 16 | }, 17 | "end": { 18 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 19 | "data": { 20 | "values": [ 21 | {"Hungry": 10, "Name": "Gemini"}, 22 | {"Hungry": 60, "Name": "Cordelia"}, 23 | {"Hungry": 80, "Name": "Gemini"}, 24 | {"Hungry": 100, "Name": "Cordelia"} 25 | ] 26 | }, 27 | "transform": [{"filter": {"field": "Name", "equal": "Gemini"}}], 28 | "mark": "point", 29 | "encoding": { 30 | "x": { "field": "Hungry", "type": "quantitative", "aggregate": "mean"} 31 | } 32 | }, 33 | "gemSpecs": [ 34 | { 35 | "timeline": { 36 | "sync": [ 37 | { 38 | "component": {"axis": "x"}, 39 | "change": {"scale": {"domainDimension": "same"}}, 40 | "timing": {"duration": 1000} 41 | }, 42 | { 43 | "component": {"mark": "marks"}, 44 | "change": {"data": ["Name"]}, 45 | "timing": {"duration": 1000} 46 | } 47 | ] 48 | } 49 | }, 50 | { 51 | "timeline": { 52 | "sync": [ 53 | { 54 | "component": {"axis": "x"}, 55 | "change": {"scale": {"domainDimension": "same"}}, 56 | "timing": {"duration": 1000} 57 | }, 58 | { 59 | "component": {"mark": "marks"}, 60 | "change": {"data": ["Name"]}, 61 | "timing": {"duration": 1000} 62 | } 63 | ] 64 | } 65 | } 66 | ], 67 | "sequence":[ 68 | { 69 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 70 | "data": { 71 | "values": [ 72 | {"Hungry": 10, "Name": "Gemini"}, 73 | {"Hungry": 60, "Name": "Cordelia"}, 74 | {"Hungry": 80, "Name": "Gemini"}, 75 | {"Hungry": 100, "Name": "Cordelia"}, 76 | {"Hungry": 40, "Name": "Mango"}, 77 | {"Hungry": 100, "Name": "Mango"} 78 | ] 79 | }, 80 | "mark": "point", 81 | "encoding": { 82 | "x": { "field": "Hungry", "type": "quantitative"}, 83 | "y": { "field": "Name", "type": "nominal"} 84 | } 85 | }, 86 | { 87 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 88 | "data": { 89 | "values": [ 90 | {"Hungry": 10, "Name": "Gemini"}, 91 | {"Hungry": 60, "Name": "Cordelia"}, 92 | {"Hungry": 80, "Name": "Gemini"}, 93 | {"Hungry": 100, "Name": "Cordelia"}, 94 | {"Hungry": 40, "Name": "Mango"}, 95 | {"Hungry": 100, "Name": "Mango"} 96 | ] 97 | }, 98 | "transform": [{"filter": {"field": "Name", "equal": "Gemini"}}], 99 | "mark": "point", 100 | "encoding": { 101 | "x": { "field": "Hungry", "type": "quantitative"}, 102 | "y": { "field": "Name", "type": "nominal"} 103 | } 104 | }, 105 | { 106 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 107 | "data": { 108 | "values": [ 109 | {"Hungry": 10, "Name": "Gemini"}, 110 | {"Hungry": 60, "Name": "Cordelia"}, 111 | {"Hungry": 80, "Name": "Gemini"}, 112 | {"Hungry": 100, "Name": "Cordelia"}, 113 | {"Hungry": 40, "Name": "Mango"}, 114 | {"Hungry": 100, "Name": "Mango"} 115 | ] 116 | }, 117 | "transform": [{"filter": {"field": "Name", "equal": "Gemini"}}], 118 | "mark": "point", 119 | "encoding": { 120 | "x": { "field": "Hungry", "type": "quantitative", "aggregate": "mean"}, 121 | "y": { "field": "Name", "type": "nominal"} 122 | } 123 | } 124 | ], 125 | "opt": { 126 | "totalDuration": 2000 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/examples/addY_addColor.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": { 3 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 4 | "data": { 5 | "values": [ 6 | {"Hungry": 10, "Name": "Gemini", "t": 1}, 7 | {"Hungry": 60, "Name": "Cordelia", "t": 1}, 8 | {"Hungry": 80, "Name": "Gemini", "t": 2}, 9 | {"Hungry": 100, "Name": "Cordelia", "t": 2} 10 | ] 11 | }, 12 | "mark": "point", 13 | "encoding": { 14 | "x": { "field": "Hungry", "type": "quantitative"} 15 | } 16 | }, 17 | "end": { 18 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 19 | "data": { 20 | "values": [ 21 | {"Hungry": 10, "Name": "Gemini", "t": 1}, 22 | {"Hungry": 60, "Name": "Cordelia", "t": 1}, 23 | {"Hungry": 80, "Name": "Gemini", "t": 2}, 24 | {"Hungry": 100, "Name": "Cordelia", "t": 2} 25 | ] 26 | }, 27 | "mark": "point", 28 | "encoding": { 29 | "x": { "field": "Hungry", "type": "quantitative"}, 30 | "y": { "field": "Name", "type": "nominal"}, 31 | "color": { "field": "t", "type": "nominal"} 32 | } 33 | }, 34 | "gemSpecs": [ 35 | { 36 | "timeline": { 37 | "sync": [ 38 | { 39 | "component": {"axis": "x"}, 40 | "change": {"scale": {"domainDimension": "same"}}, 41 | "timing": {"duration": 1000} 42 | }, 43 | { 44 | "component": {"mark": "marks"}, 45 | "change": {"data": ["Name"]}, 46 | "timing": {"duration": 1000} 47 | } 48 | ] 49 | } 50 | }, 51 | { 52 | "timeline": { 53 | "sync": [ 54 | { 55 | "component": {"axis": "x"}, 56 | "change": {"scale": {"domainDimension": "same"}}, 57 | "timing": {"duration": 1000} 58 | }, 59 | { 60 | "component": {"mark": "marks"}, 61 | "change": {"data": ["Name"]}, 62 | "timing": {"duration": 1000} 63 | } 64 | ] 65 | } 66 | } 67 | ], 68 | "sequence":[ 69 | { 70 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 71 | "data": { 72 | "values": [ 73 | {"Hungry": 10, "Name": "Gemini"}, 74 | {"Hungry": 60, "Name": "Cordelia"}, 75 | {"Hungry": 80, "Name": "Gemini"}, 76 | {"Hungry": 100, "Name": "Cordelia"}, 77 | {"Hungry": 40, "Name": "Mango"}, 78 | {"Hungry": 100, "Name": "Mango"} 79 | ] 80 | }, 81 | "mark": "point", 82 | "encoding": { 83 | "x": { "field": "Hungry", "type": "quantitative"}, 84 | "y": { "field": "Name", "type": "nominal"} 85 | } 86 | }, 87 | { 88 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 89 | "data": { 90 | "values": [ 91 | {"Hungry": 10, "Name": "Gemini"}, 92 | {"Hungry": 60, "Name": "Cordelia"}, 93 | {"Hungry": 80, "Name": "Gemini"}, 94 | {"Hungry": 100, "Name": "Cordelia"}, 95 | {"Hungry": 40, "Name": "Mango"}, 96 | {"Hungry": 100, "Name": "Mango"} 97 | ] 98 | }, 99 | "transform": [{"filter": {"field": "Name", "equal": "Gemini"}}], 100 | "mark": "point", 101 | "encoding": { 102 | "x": { "field": "Hungry", "type": "quantitative"}, 103 | "y": { "field": "Name", "type": "nominal"} 104 | } 105 | }, 106 | { 107 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 108 | "data": { 109 | "values": [ 110 | {"Hungry": 10, "Name": "Gemini"}, 111 | {"Hungry": 60, "Name": "Cordelia"}, 112 | {"Hungry": 80, "Name": "Gemini"}, 113 | {"Hungry": 100, "Name": "Cordelia"}, 114 | {"Hungry": 40, "Name": "Mango"}, 115 | {"Hungry": 100, "Name": "Mango"} 116 | ] 117 | }, 118 | "transform": [{"filter": {"field": "Name", "equal": "Gemini"}}], 119 | "mark": "point", 120 | "encoding": { 121 | "x": { "field": "Hungry", "type": "quantitative", "aggregate": "mean"}, 122 | "y": { "field": "Name", "type": "nominal"} 123 | } 124 | } 125 | ], 126 | "opt": { 127 | "totalDuration": 2000 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/examples/addY_aggregate_scale.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "[Penguin] ADD_X(f1) & AGGREGATE", 3 | "gemSpecs": [ 4 | { 5 | "timeline": { 6 | "concat": [ 7 | { 8 | "sync": [ 9 | { 10 | "component": {"mark": "marks"}, 11 | "change": { 12 | "scale": ["x", "y"], 13 | "data": ["Flipper Length (mm)", "Body Mass (g)"], 14 | "encode": {"update": true, "enter": true, "exit": true} 15 | }, 16 | "timing": {"duration": {"ratio": 1}} 17 | }, 18 | { 19 | "component": {"axis": "x"}, 20 | "change": {"scale": {"domainDimension": "same"}}, 21 | "timing": {"duration": {"ratio": 1}} 22 | }, 23 | { 24 | "component": {"axis": "y"}, 25 | "change": {"scale": {"domainDimension": "same"}}, 26 | "timing": {"duration": {"ratio": 1}} 27 | }, 28 | { 29 | "component": "view", 30 | "change": {"signal": ["width", "height"]}, 31 | "timing": {"duration": {"ratio": 1}} 32 | } 33 | ] 34 | } 35 | ] 36 | }, 37 | "totalDuration": 2000 38 | }, 39 | { 40 | "timeline": { 41 | "concat": [ 42 | { 43 | "sync": [ 44 | { 45 | "component": {"mark": "marks"}, 46 | "change": { 47 | "scale": ["x"], 48 | "data": ["Flipper Length (mm)", "Body Mass (g)"], 49 | "encode": {"update": true, "enter": true, "exit": true} 50 | }, 51 | "timing": {"duration": {"ratio": 1}} 52 | }, 53 | { 54 | "component": {"axis": "x"}, 55 | "change": {"scale": {"domainDimension": "same"}}, 56 | "timing": {"duration": {"ratio": 1}} 57 | } 58 | ] 59 | } 60 | ] 61 | }, 62 | "totalDuration": 2000 63 | } 64 | ], 65 | "sequence":[ 66 | { 67 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 68 | "data": { 69 | "url": "test/data/penguins.json" 70 | }, 71 | "mark": "point", 72 | "encoding": { 73 | "x": { 74 | "field": "Flipper Length (mm)", 75 | "type": "quantitative", 76 | "scale": {"zero": false} 77 | } 78 | } 79 | }, 80 | { 81 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 82 | "data": {"url": "test/data/penguins.json"}, 83 | "mark": "point", 84 | "encoding": { 85 | "x": { 86 | "field": "Flipper Length (mm)", 87 | "type": "quantitative", 88 | "scale": {"zero": false, "domain": [0, 235]}, 89 | "aggregate": "mean" 90 | }, 91 | "y": {"field": "Species", "type": "nominal"} 92 | } 93 | }, 94 | { 95 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 96 | "data": {"url": "test/data/penguins.json"}, 97 | "mark": "point", 98 | "encoding": { 99 | "x": { 100 | "field": "Flipper Length (mm)", 101 | "type": "quantitative", 102 | "aggregate": "mean" 103 | }, 104 | "y": {"field": "Species", "type": "nominal"} 105 | } 106 | } 107 | 108 | ], 109 | "start": { 110 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 111 | "data": { 112 | "url": "test/data/penguins.json" 113 | }, 114 | "mark": "point", 115 | "encoding": { 116 | "x": { 117 | "field": "Flipper Length (mm)", 118 | "type": "quantitative", 119 | "scale": {"zero": false} 120 | } 121 | } 122 | }, 123 | "end": { 124 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 125 | "data": { 126 | "url": "test/data/penguins.json" 127 | }, 128 | "mark": "point", 129 | "encoding": { 130 | "x": { 131 | "field": "Flipper Length (mm)", 132 | "type": "quantitative", 133 | "aggregate": "mean" 134 | }, 135 | "y": {"field": "Species", "type": "nominal"} 136 | } 137 | }, 138 | "opt": { 139 | "totalDuration": 4000, 140 | "marks": { 141 | "marks": {"change": {"data": ["Flipper Length (mm)", "Body Mass (g)", "Species"]}} 142 | }, 143 | "axes": { 144 | "x": {"change": { "scale": {"domainDimension": "same"} }}, 145 | "y": {"change": { "scale": {"domainDimension": "same"} }} 146 | }, 147 | "scales": {"x": {"domainDimension": "same"}, "y": {"domainDimension": "same"}} 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /test/examples/filter_and_filter.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "title": "[COVID] Filter & Filter", 4 | "sequence": [{ 5 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 6 | "data": {"url": "test/data/covid-us-states.json"}, 7 | "mark": "area", 8 | "transform": [ 9 | {"filter": {"field": "deaths_delta", "gt": 0}}, 10 | {"calculate": "datum.state=='New York' ? 'New York' : 'Others'", "as": "isNY"}, 11 | { 12 | "aggregate": [ 13 | {"op": "sum", "field": "cases_delta", "as": "_cases_delta"}, 14 | {"op": "sum", "field": "deaths_delta", "as": "_deaths_delta"} 15 | ], 16 | "groupby": ["date", "isNY"] 17 | }, 18 | { 19 | "window": [ 20 | {"op": "mean", "field": "_cases_delta", "as": "cases_7dayAvg"}, 21 | {"op": "mean", "field": "_deaths_delta", "as": "deaths_7dayAvg"} 22 | ], 23 | "sort": [{"field": "date", "order": "ascending"}], 24 | "groupby": ["isNY"], 25 | "frame": [-3, 3] 26 | }, 27 | {"filter": {"field": "isNY", "equal": "New York"}}, 28 | { 29 | "filter": { 30 | "field": "date", 31 | "range": [ 32 | {"year": 2020, "month": "Mar"}, 33 | {"year": 2020, "month": "jul"} 34 | ] 35 | } 36 | } 37 | ], 38 | "encoding": { 39 | "x": {"field": "date", "type": "temporal", "title": "Date"}, 40 | "y": { 41 | "field": "deaths_7dayAvg", 42 | "type": "quantitative", 43 | "stack": true, 44 | "axis": {"labelFlush": true, "title": "Daily Deaths (7-day Avg.)"} 45 | }, 46 | "color": { 47 | "field": "isNY", 48 | "type": "nominal", 49 | "scale": {"domain": ["New York", "Others"]}, 50 | "legend": {"title": "Region", "direction": "horizontal", "orient": "top"} 51 | }, 52 | "order": {"field": "isNY"} 53 | }, 54 | "height": 150, 55 | "width": 450 56 | }, 57 | { 58 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 59 | "data": {"url": "test/data/covid-us-states.json"}, 60 | "mark": "area", 61 | "transform": [ 62 | {"filter": {"field": "deaths_delta", "gt": 0}}, 63 | {"calculate": "datum.state=='New York' ? 'New York' : 'Others'", "as": "isNY"}, 64 | { 65 | "aggregate": [ 66 | {"op": "sum", "field": "cases_delta", "as": "_cases_delta"}, 67 | {"op": "sum", "field": "deaths_delta", "as": "_deaths_delta"} 68 | ], 69 | "groupby": ["date", "isNY"] 70 | }, 71 | { 72 | "window": [ 73 | {"op": "mean", "field": "_cases_delta", "as": "cases_7dayAvg"}, 74 | {"op": "mean", "field": "_deaths_delta", "as": "deaths_7dayAvg"} 75 | ], 76 | "sort": [{"field": "date", "order": "ascending"}], 77 | "groupby": ["isNY"], 78 | "frame": [-3, 3] 79 | }, 80 | { 81 | "filter": { 82 | "field": "date", 83 | "range": [ 84 | {"year": 2020, "month": "mar"}, 85 | {"year": 2021, "month": "jul"} 86 | ] 87 | } 88 | } 89 | ], 90 | "encoding": { 91 | "x": {"field": "date", "type": "temporal", "title": "Date"}, 92 | "y": { 93 | "field": "deaths_7dayAvg", 94 | "type": "quantitative", 95 | "stack": true, 96 | "axis": {"labelFlush": true, "title": "Daily Deaths (7-day Avg.)"} 97 | }, 98 | "color": { 99 | "field": "isNY", 100 | "type": "nominal", 101 | "scale": {"domain": ["New York", "Others"]}, 102 | "legend": {"title": "Region", "direction": "horizontal", "orient": "top"} 103 | }, 104 | "order": {"field": "isNY"} 105 | }, 106 | "height": 150, 107 | "width": 450 108 | } 109 | ], 110 | "opt": { 111 | "totalDuration": 4000, 112 | "marks": { 113 | "marks": {"change": {"data": ["isNY"]}} 114 | }, 115 | "axes": { 116 | "x": {"change": { "scale": {"domainDimension": "same"} }}, 117 | "y": {"change": { "scale": {"domainDimension": "same"} }} 118 | }, 119 | "scales": {"x": {"domainDimension": "same"}, "y": {"domainDimension": "same"}} 120 | }, 121 | "gemSpecs": [ 122 | { 123 | "timeline": { 124 | "sync": [ 125 | { 126 | "component": {"axis": "x"}, 127 | "change": {"scale": {"domainDimension": "same"}}, 128 | "timing": {"duration": 4000} 129 | }, 130 | { 131 | "component": {"axis": "y"}, 132 | "timing": {"duration": 4000} 133 | }, 134 | { 135 | "component": "view", 136 | "timing": {"duration": 4000} 137 | }, 138 | { 139 | "component": {"mark": "marks"}, 140 | "change": {"data": ["name"]}, 141 | "timing": {"duration": 4000} 142 | } 143 | ] 144 | } 145 | } 146 | ] 147 | } -------------------------------------------------------------------------------- /src/path/evaluateRules.js: -------------------------------------------------------------------------------- 1 | const { filter } = require("d3-array") 2 | const { unique } = require("../util") 3 | 4 | exports.HEURISTIC_RULES = [ 5 | { 6 | name: "filter-then-aggregate", 7 | type: "A-Then-B", 8 | editOps: ["FILTER", "AGGREGATE"], 9 | condition: (filter, aggregate) => { 10 | return aggregate.detail && aggregate.detail.find(dt => dt.how === "added") 11 | }, 12 | score: 1 13 | }, 14 | { 15 | name: "disaggregate-then-filter", 16 | type: "A-Then-B", 17 | editOps: ["AGGREGATE", "FILTER"], 18 | condition: (aggregate, filter) => { 19 | return aggregate.detail && aggregate.detail.find(dt => dt.how === "removed") 20 | }, 21 | score: 1 22 | }, 23 | { 24 | name: "filter-then-bin", 25 | type: "A-Then-B", 26 | editOps: ["FILTER", "BIN"], 27 | condition: (filter, bin) => { 28 | return bin.detail && bin.detail.find(dt => dt.how === "added") 29 | }, 30 | score: 1 31 | }, 32 | { 33 | name: "unbin-then-filter", 34 | type: "A-Then-B", 35 | editOps: ["BIN", "FILTER"], 36 | condition: (bin, filter) => { 37 | return bin.detail && bin.detail.find(dt => dt.how === "removed") 38 | }, 39 | score: 1 40 | }, 41 | { 42 | name: "no-aggregate-then-bin", 43 | type: "A-Then-B", 44 | editOps: ["AGGREGATE", "BIN"], 45 | condition: (aggregate, bin) => { 46 | return aggregate.detail && aggregate.detail.find(dt => dt.how === "added") 47 | }, 48 | score: -1 49 | }, 50 | { 51 | name: "no-unbin-then-disaggregate", 52 | type: "A-Then-B", 53 | editOps: ["BIN", "AGGREGATE"], 54 | condition: (bin, aggregate) => { 55 | return aggregate.detail && aggregate.detail.find(dt => dt.how === "removed") 56 | }, 57 | score: -1 58 | }, 59 | 60 | { 61 | name: "encoding(MODIFY)-then-aggregate", 62 | type: "A-Then-B", 63 | editOps: ["ENCODING", "AGGREGATE"], 64 | condition: (encoding, aggregate) => { 65 | return encoding.name.indexOf("MODIFY") >= 0 66 | && aggregate.detail 67 | && aggregate.detail.find(dt => dt.how === "added") 68 | }, 69 | score: 1 70 | }, 71 | { 72 | name: "disaggregate-then-encoding(MODIFY)", 73 | type: "A-Then-B", 74 | editOps: ["AGGREGATE", "ENCODING"], 75 | condition: (aggregate, encoding) => { 76 | return encoding.name.indexOf("MODIFY") >= 0 77 | && aggregate.detail 78 | && aggregate.detail.find(dt => dt.how === "removed") 79 | }, 80 | score: 1 81 | }, 82 | 83 | { 84 | name: "encoding(add)-then-aggregate", 85 | type: "A-Then-B", 86 | editOps: ["ENCODING", "AGGREGATE"], 87 | condition: (encoding, aggregate) => { 88 | return encoding.name.indexOf("ADD") >= 0 89 | && aggregate.detail 90 | && aggregate.detail.find(dt => dt.how === "added") 91 | }, 92 | score: 1 93 | }, 94 | { 95 | name: "disaggregate-then-encoding(remove)", 96 | type: "A-Then-B", 97 | editOps: ["AGGREGATE", "ENCODING"], 98 | condition: (aggregate, encoding) => { 99 | return encoding.name.indexOf("REMOVE") >= 0 100 | && aggregate.detail 101 | && aggregate.detail.find(dt => dt.how === "removed") 102 | }, 103 | score: 1 104 | }, 105 | { 106 | name: "no-mark-then-aggregate", 107 | type: "A-Then-B", 108 | editOps: ["MARK", "AGGREGATE" ], 109 | condition: (mark, aggregate) => { 110 | 111 | return aggregate.detail && aggregate.detail.find(dt => dt.how === "added") 112 | }, 113 | score: -1 114 | }, 115 | { 116 | name: "no-disaggregate-then-mark", 117 | type: "A-Then-B", 118 | editOps: ["AGGREGATE", "MARK"], 119 | condition: (aggregate, mark ) => { 120 | 121 | return aggregate.detail && aggregate.detail.find(dt => dt.how === "removed") 122 | }, 123 | score: -1 124 | }, 125 | { 126 | name: "modifying-with-scale", 127 | type: "A-With-B", 128 | editOps: ["ENCODING.MODIFY", "SCALE"], 129 | score: 1 130 | }, 131 | { 132 | name: "no-filtering-with-filtering", 133 | type: "A-With-B", 134 | editOps: ["FILTER"], 135 | condition: (editOps) => { 136 | 137 | return unique(editOps.FILTER, f => f.position).length < editOps.FILTER.length 138 | }, 139 | score: -1 140 | }, 141 | { 142 | name: "bin-with-aggregate", 143 | type: "A-With-B", 144 | editOps: ["AGGREGATE", "BIN"], 145 | score: 1 146 | }, 147 | // { 148 | // editOps: [TRANSFORM, ENCODING.REMOVE], 149 | // condition: (transform, remove) => { 150 | // return transform.detail.field === remove.detail.before.field 151 | // }, 152 | // score: 1 153 | // }, 154 | // { 155 | // editOps: [TRANSFORM, ENCODING.MODIFY], 156 | // condition: (transform, modify) => { 157 | // return transform.detail.field === modify.detail.after.field 158 | // }, 159 | // score: 1 160 | // }, 161 | // { 162 | // editOps: [TRANSFORM, ENCODING.ADD], 163 | // condition: (transform, add) => { 164 | // return transform.detail.field === add.detail.after.field 165 | // }, 166 | // score: 1 167 | // }, 168 | // { 169 | // editOps: [ENCODING.MODIFY, TRANSFORM], 170 | // condition: (transform, modify) => { 171 | // return transform.detail.field === modify.detail.before.field 172 | // }, 173 | // score: 1 174 | // } 175 | ] -------------------------------------------------------------------------------- /src/sequence/sequence.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var trans = require('./../transition/trans.js'); 4 | var editOp = require('./../editOp/editOpSet.js'); 5 | var TSP = require('../../lib/TSP.js'); 6 | var d3 = require('d3'); 7 | var PO = require('./PatternOptimizer.js'); 8 | var tb = require('./TieBreaker.js'); 9 | 10 | async function sequence(specs, options, editOpSet, callback){ 11 | if (!editOpSet) { 12 | editOpSet = editOp.DEFAULT_EDIT_OPS; 13 | } 14 | 15 | function distanceWithPattern(dist, globalWeightingTerm, filterCost){ 16 | return (dist + filterCost / 1000) * globalWeightingTerm; 17 | } 18 | 19 | var transitionSetsFromEmptyVis = await getTransitionSetsFromSpec({ "mark":"null", "encoding": {} }, specs, editOpSet); 20 | 21 | if (!options.fixFirst) { 22 | var startingSpec = { "mark":"null", "encoding": {} }; 23 | specs = [ startingSpec ].concat(specs); 24 | } 25 | 26 | var transitions = await getTransitionSets(specs, editOpSet); 27 | transitions = extendTransitionSets(transitions); 28 | 29 | var TSPResult = TSP.TSP(transitions, "cost", options.fixFirst===true ? 0 : undefined); 30 | var TSPResultAll = TSPResult.all.filter(function(seqWithDist){ 31 | return seqWithDist.sequence[0] === 0; 32 | }).map(function(tspR){ 33 | 34 | var sequence = tspR.sequence; 35 | var transitionSet = []; 36 | for (var i = 0; i < sequence.length-1; i++) { 37 | transitionSet.push(transitions[sequence[i]][sequence[i+1]]); 38 | }; 39 | var pattern = transitionSet.map(function(r){ return r.id; }); 40 | var POResult = PO.PatternOptimizer(pattern, transitions.uniq); 41 | 42 | var result = { 43 | "sequence" : sequence, 44 | "transitions" : transitionSet, 45 | "sumOfTransitionCosts" : tspR.distance, 46 | "patterns" : POResult, 47 | "globalWeightingTerm" : !!POResult[0] ? 1 - POResult[0].patternScore : 1, 48 | "charts" : sequence.map(function(index){ 49 | return specs[index]; 50 | }) 51 | }; 52 | var tbResult = tb.TieBreaker(result, transitionSetsFromEmptyVis); 53 | result.filterSequenceCost = tbResult.tiebreakCost; 54 | result.filterSequenceCostReasons = tbResult.reasons; 55 | result.sequenceCost = distanceWithPattern(result.sumOfTransitionCosts, result.globalWeightingTerm, tbResult.tiebreakCost); 56 | return result; 57 | }).sort(function(a,b){ 58 | if (a.sequenceCost > b.sequenceCost) { 59 | return 1; 60 | } 61 | if (a.sequenceCost < b.sequenceCost) { 62 | return -1; 63 | } else { 64 | return a.sequence.join(',') > b.sequence.join(',') ? 1 : -1; 65 | } 66 | return 0; 67 | }); 68 | 69 | var sequencedSpecs = []; 70 | var minSequenceCost = TSPResultAll[0].sequenceCost; 71 | for (var i = 0; i < TSPResultAll.length; i++) { 72 | if(TSPResultAll[i].sequenceCost === minSequenceCost ){ 73 | TSPResultAll[i].isOptimum = true; 74 | } 75 | else { 76 | break; 77 | } 78 | } 79 | var returnValue = TSPResultAll; 80 | 81 | 82 | if(callback){ 83 | callback(returnValue); 84 | } 85 | return returnValue; 86 | } 87 | async function getTransitionSetsFromSpec( spec, specs, editOpSet){ 88 | var transitions = []; 89 | for (var i = 0; i < specs.length; i++) { 90 | transitions.push(await trans.transition(specs[i], spec, editOpSet, { omitIncludeRawDomin: true })); 91 | } 92 | return transitions; 93 | } 94 | 95 | async function getTransitionSets(specs, editOpSet){ 96 | var transitions = []; 97 | for (var i = 0; i < specs.length; i++) { 98 | transitions.push([]); 99 | for (var j = 0; j < specs.length; j++) { 100 | transitions[i].push(await trans.transition(specs[i], specs[j], editOpSet, { omitIncludeRawDomin: true })); 101 | 102 | } 103 | } 104 | return transitions; 105 | } 106 | 107 | function extendTransitionSets(transitions){ 108 | var uniqTransitionSets = []; 109 | var flatCosts = transitions.reduce(function(prev,curr){ 110 | for (var i = 0; i < curr.length; i++) { 111 | prev.push(curr[i].cost); 112 | var transitionSetSH = transitionShorthand(curr[i]); 113 | var index = uniqTransitionSets.map(function(tr){ return tr.shorthand; }).indexOf(transitionSetSH); 114 | 115 | if ( index === -1) { 116 | curr[i]["id"] = uniqTransitionSets.push({tr: curr[i], shorthand: transitionSetSH}) - 1; 117 | } else { 118 | curr[i]["id"] = index; 119 | } 120 | 121 | }; 122 | return prev; 123 | }, []); 124 | 125 | var uniqueCosts = [...new Set(flatCosts)] 126 | .map(function(val){ return Number(val); }) 127 | .sort(function(a,b){ return a-b;}); 128 | 129 | var rank = d3.scaleOrdinal() 130 | .domain(uniqueCosts) 131 | .range([0,uniqueCosts.length]); 132 | 133 | for (var i = 0; i < transitions.length; i++) { 134 | for (var j = 0; j < transitions[i].length; j++) { 135 | transitions[i][j]["start"] = i; 136 | transitions[i][j]["destination"] = j; 137 | transitions[i][j]["rank"] = Math.floor(rank(transitions[i][j].cost)); 138 | } 139 | } 140 | transitions.uniq = uniqTransitionSets; 141 | return transitions 142 | } 143 | function transitionShorthand(transition){ 144 | return transition.mark 145 | .concat(transition.transform) 146 | .concat(transition.encoding) 147 | .map(function(tr){ 148 | if (tr.detail) { 149 | if (tr.name === "MODIFY_FILTER") { 150 | return tr.name + '(' + JSON.stringify(tr.detail.id) + ')'; 151 | } 152 | return tr.name + '(' + JSON.stringify(tr.detail) + ')'; 153 | } 154 | return tr.name; 155 | }) 156 | .sort() 157 | .join('|'); 158 | 159 | } 160 | exports.sequence = sequence; 161 | -------------------------------------------------------------------------------- /src/path/enumerate.js: -------------------------------------------------------------------------------- 1 | 2 | // import {default as vl2vg4gemini} from "../../util/vl2vg4gemini" 3 | const vl = require("vega-lite"); 4 | const vega = require("vega"); 5 | const { copy, deepEqual, partition, permutate, union, intersection} = require("../util"); 6 | const apply = require("../transition/apply").apply; 7 | 8 | // Take two vega-lite specs and enumerate paths [{sequence, editOpPartition (aka transition)}]: 9 | async function enumerate(sVLSpec, eVLSpec, editOps, transM, withExcluded = false) { 10 | if (editOps.length < transM) { 11 | throw new CannotEnumStagesMoreThanTransitions(editOps.length, transM) 12 | } 13 | 14 | const editOpPartitions = partition(editOps, transM) 15 | 16 | const orderedEditOpPartitions = editOpPartitions.reduce((ordered, pt) => { 17 | return ordered.concat(permutate(pt)); 18 | }, []) 19 | const sequences = []; 20 | 21 | let excludedPaths = []; 22 | 23 | for (const editOpPartition of orderedEditOpPartitions) { 24 | let sequence = [copy(sVLSpec)]; 25 | let currSpec = copy(sVLSpec); 26 | let valid = true; 27 | for (let i = 0; i < editOpPartition.length; i++) { 28 | const editOps = editOpPartition[i]; 29 | if (i===(editOpPartition.length - 1)) { 30 | sequence.push(eVLSpec); 31 | break; // The last spec should be the same as eVLSpec; 32 | } 33 | 34 | try { 35 | currSpec = apply(copy(currSpec), eVLSpec, editOps); 36 | } catch(e) { 37 | if (["UnapplicableEditOPError", "InvalidVLSpecError", "UnapplicableEditOpsError"].indexOf(e.name) < 0) { 38 | throw e; 39 | } else { 40 | valid = false; 41 | excludedPaths.push({info: e, editOpPartition, invalidSpec: currSpec}) 42 | break; 43 | } 44 | } 45 | 46 | sequence.push(copy(currSpec)); 47 | } 48 | 49 | const mergedScaleDomain = await getMergedScale(sequence); 50 | 51 | sequence = sequence.map((currSpec, i) => { 52 | if (i===0 || i===sequence.length-1) { 53 | return currSpec 54 | } 55 | return applyMergedScale(currSpec, mergedScaleDomain, editOpPartition[i-1]) 56 | }) 57 | 58 | if (valid && validate(sequence)) { 59 | sequences.push({sequence, editOpPartition}); 60 | } 61 | } 62 | if (withExcluded) { 63 | return {sequences, excludedPaths} 64 | } 65 | 66 | return sequences 67 | } 68 | exports.enumerate = enumerate; 69 | 70 | 71 | function applyMergedScale(vlSpec, mergedScaleDomain, currEditOps) { 72 | let currSpec = copy(vlSpec); 73 | let sortEditOp = currEditOps.find(eo => eo.name === "SORT"); 74 | 75 | for (const channel in mergedScaleDomain) { 76 | // When sort editOps are applied, do not change the corresponding scale domain. 77 | if (sortEditOp && sortEditOp.detail.find(dt => dt.channel === channel)) { 78 | continue; 79 | }; 80 | 81 | if (mergedScaleDomain.hasOwnProperty(channel)) { 82 | 83 | if (currSpec.encoding[channel]) { 84 | if (!currSpec.encoding[channel].scale) { 85 | currSpec.encoding[channel].scale = {}; 86 | } 87 | currSpec.encoding[channel].scale.domain = mergedScaleDomain[channel]; 88 | if (currSpec.encoding[channel].scale.zero !== undefined) { 89 | 90 | delete currSpec.encoding[channel].scale.zero 91 | } 92 | } 93 | } 94 | } 95 | return currSpec 96 | } 97 | 98 | // Get the scales including all data points while doing transitions. 99 | async function getMergedScale(sequence) { 100 | 101 | const views = await Promise.all(sequence.map(vlSpec => { 102 | return new vega.View(vega.parse(vl.compile(vlSpec).spec), {renderer: "svg"}).runAsync() 103 | })) 104 | 105 | let commonEncoding = sequence.reduce((commonEncoding, vlSpec, i) => { 106 | let encoding = Object.keys(vlSpec.encoding).map(channel => { 107 | return { 108 | channel, 109 | ...vlSpec.encoding[channel], 110 | runtimeScale: views[i]._runtime.scales[channel] 111 | }; 112 | }); 113 | if (i===0){ 114 | return encoding; 115 | } 116 | return intersection(encoding, commonEncoding, ch => { 117 | return [ch.channel, ch.field||"", ch.type||"", ch.runtimeScale ? ch.runtimeScale.type : ""].join("_"); 118 | }) 119 | }, []).map(encoding => { 120 | return { 121 | ...encoding, 122 | domains: views.map(view => { 123 | return view._runtime.scales[encoding.channel] ? view._runtime.scales[encoding.channel].value.domain() : undefined 124 | }) 125 | } 126 | }) 127 | 128 | commonEncoding = commonEncoding.filter(encoding => { 129 | //if all the domains are the same, then don't need to merge 130 | return !encoding.domains 131 | .filter(d => d) 132 | .reduce((accDomain, domain) => { 133 | if (deepEqual(domain, accDomain)) { 134 | return domain; 135 | } 136 | return undefined; 137 | }, encoding.domains[0]) 138 | }) 139 | 140 | return commonEncoding.reduce((mergedScaleDomains, encoding) => { 141 | if (!encoding.runtimeScale) { 142 | return mergedScaleDomains; 143 | } 144 | const vlType = encoding.type, 145 | domains = encoding.domains; 146 | 147 | if (vlType === "quantitative") { 148 | mergedScaleDomains[encoding.channel] = [ 149 | Math.min(...domains.map(domain => domain[0])), 150 | Math.max(...domains.map(domain => domain[1])) 151 | ] 152 | } else if (vlType === "nominal" || vlType === "ordinal") { 153 | mergedScaleDomains[encoding.channel] = domains.reduce((merged, domain) => { 154 | return union(merged, domain) 155 | }, []) 156 | } else if (vlType==="temporal") { 157 | mergedScaleDomains[encoding.channel] = [ 158 | Math.min(...domains.map(domain => domain[0])), 159 | Math.max(...domains.map(domain => domain[1])) 160 | ] 161 | } 162 | 163 | return mergedScaleDomains; 164 | }, {}) 165 | } 166 | exports.getMergedScale = getMergedScale 167 | 168 | 169 | function validate(sequence) { 170 | //Todo: check if the sequence is a valid vega-lite spec. 171 | let prevChart = sequence[0]; 172 | for (let i = 1; i < sequence.length; i++) { 173 | const currChart = sequence[i]; 174 | if (deepEqual(prevChart, currChart)) { 175 | return false; 176 | } 177 | prevChart = sequence[i]; 178 | } 179 | return true; 180 | } 181 | exports.validate = validate 182 | 183 | 184 | class CannotEnumStagesMoreThanTransitions extends Error { 185 | constructor(editOpsN, transM) { 186 | super(`Cannot enumerate ${transM} transitions for ${editOpsN} edit operations. The number of transitions should lesser than the number of possible edit operations.`) 187 | this.name = "CannotEnumStagesMoreThanTransitions" 188 | } 189 | } -------------------------------------------------------------------------------- /src/editOp/editOpSet.js: -------------------------------------------------------------------------------- 1 | exports.DEFAULT_EDIT_OPS = { 2 | "markEditOps":{"AREA_BAR":{"name":"AREA_BAR","cost":0.03},"AREA_LINE":{"name":"AREA_LINE","cost":0.02},"AREA_POINT":{"name":"AREA_POINT","cost":0.04},"AREA_TEXT":{"name":"AREA_TEXT","cost":0.08},"AREA_TICK":{"name":"AREA_TICK","cost":0.04},"BAR_LINE":{"name":"BAR_LINE","cost":0.04},"BAR_POINT":{"name":"BAR_POINT","cost":0.02},"BAR_TEXT":{"name":"BAR_TEXT","cost":0.06},"BAR_TICK":{"name":"BAR_TICK","cost":0.02},"LINE_POINT":{"name":"LINE_POINT","cost":0.03},"LINE_TEXT":{"name":"LINE_TEXT","cost":0.07},"LINE_TICK":{"name":"LINE_TICK","cost":0.03},"POINT_TEXT":{"name":"POINT_TEXT","cost":0.05},"POINT_TICK":{"name":"POINT_TICK","cost":0.01},"TEXT_TICK":{"name":"TEXT_TICK","cost":0.05}}, 3 | "transformEditOps":{"SCALE":{"name":"SCALE","cost":0.6},"SORT":{"name":"SORT","cost":0.61},"BIN":{"name":"BIN","cost":0.62},"AGGREGATE":{"name":"AGGREGATE","cost":0.63},"ADD_FILTER":{"name":"ADD_FILTER","cost":0.65},"REMOVE_FILTER":{"name":"REMOVE_FILTER","cost":0.65},"MODIFY_FILTER":{"name":"MODIFY_FILTER","cost":0.64}}, 4 | "encodingEditOps":{"ADD_X":{"name":"ADD_X","cost":4.59},"ADD_Y":{"name":"ADD_Y","cost":4.59},"ADD_COLOR":{"name":"ADD_COLOR","cost":4.55},"ADD_SHAPE":{"name":"ADD_SHAPE","cost":4.51},"ADD_SIZE":{"name":"ADD_SIZE","cost":4.53},"ADD_ROW":{"name":"ADD_ROW","cost":4.57},"ADD_COLUMN":{"name":"ADD_COLUMN","cost":4.57},"ADD_TEXT":{"name":"ADD_TEXT","cost":4.49},"ADD_X_COUNT":{"name":"ADD_X_COUNT","cost":4.58},"ADD_Y_COUNT":{"name":"ADD_Y_COUNT","cost":4.58},"ADD_COLOR_COUNT":{"name":"ADD_COLOR_COUNT","cost":4.54},"ADD_SHAPE_COUNT":{"name":"ADD_SHAPE_COUNT","cost":4.5},"ADD_SIZE_COUNT":{"name":"ADD_SIZE_COUNT","cost":4.52},"ADD_ROW_COUNT":{"name":"ADD_ROW_COUNT","cost":4.56},"ADD_COLUMN_COUNT":{"name":"ADD_COLUMN_COUNT","cost":4.56},"ADD_TEXT_COUNT":{"name":"ADD_TEXT_COUNT","cost":4.48},"REMOVE_X_COUNT":{"name":"REMOVE_X_COUNT","cost":4.58},"REMOVE_Y_COUNT":{"name":"REMOVE_Y_COUNT","cost":4.58},"REMOVE_COLOR_COUNT":{"name":"REMOVE_COLOR_COUNT","cost":4.54},"REMOVE_SHAPE_COUNT":{"name":"REMOVE_SHAPE_COUNT","cost":4.5},"REMOVE_SIZE_COUNT":{"name":"REMOVE_SIZE_COUNT","cost":4.52},"REMOVE_ROW_COUNT":{"name":"REMOVE_ROW_COUNT","cost":4.56},"REMOVE_COLUMN_COUNT":{"name":"REMOVE_COLUMN_COUNT","cost":4.56},"REMOVE_TEXT_COUNT":{"name":"REMOVE_TEXT_COUNT","cost":4.48},"REMOVE_X":{"name":"REMOVE_X","cost":4.59},"REMOVE_Y":{"name":"REMOVE_Y","cost":4.59},"REMOVE_COLOR":{"name":"REMOVE_COLOR","cost":4.55},"REMOVE_SHAPE":{"name":"REMOVE_SHAPE","cost":4.51},"REMOVE_SIZE":{"name":"REMOVE_SIZE","cost":4.53},"REMOVE_ROW":{"name":"REMOVE_ROW","cost":4.57},"REMOVE_COLUMN":{"name":"REMOVE_COLUMN","cost":4.57},"REMOVE_TEXT":{"name":"REMOVE_TEXT","cost":4.49},"MODIFY_X":{"name":"MODIFY_X","cost":4.71},"MODIFY_Y":{"name":"MODIFY_Y","cost":4.71},"MODIFY_COLOR":{"name":"MODIFY_COLOR","cost":4.67},"MODIFY_SHAPE":{"name":"MODIFY_SHAPE","cost":4.63},"MODIFY_SIZE":{"name":"MODIFY_SIZE","cost":4.65},"MODIFY_ROW":{"name":"MODIFY_ROW","cost":4.69},"MODIFY_COLUMN":{"name":"MODIFY_COLUMN","cost":4.69},"MODIFY_TEXT":{"name":"MODIFY_TEXT","cost":4.61},"MODIFY_X_ADD_COUNT":{"name":"MODIFY_X_ADD_COUNT","cost":4.7},"MODIFY_Y_ADD_COUNT":{"name":"MODIFY_Y_ADD_COUNT","cost":4.7},"MODIFY_COLOR_ADD_COUNT":{"name":"MODIFY_COLOR_ADD_COUNT","cost":4.66},"MODIFY_SHAPE_ADD_COUNT":{"name":"MODIFY_SHAPE_ADD_COUNT","cost":4.62},"MODIFY_SIZE_ADD_COUNT":{"name":"MODIFY_SIZE_ADD_COUNT","cost":4.64},"MODIFY_ROW_ADD_COUNT":{"name":"MODIFY_ROW_ADD_COUNT","cost":4.68},"MODIFY_COLUMN_ADD_COUNT":{"name":"MODIFY_COLUMN_ADD_COUNT","cost":4.68},"MODIFY_TEXT_ADD_COUNT":{"name":"MODIFY_TEXT_ADD_COUNT","cost":4.6},"MODIFY_X_REMOVE_COUNT":{"name":"MODIFY_X_REMOVE_COUNT","cost":4.7},"MODIFY_Y_REMOVE_COUNT":{"name":"MODIFY_Y_REMOVE_COUNT","cost":4.7},"MODIFY_COLOR_REMOVE_COUNT":{"name":"MODIFY_COLOR_REMOVE_COUNT","cost":4.66},"MODIFY_SHAPE_REMOVE_COUNT":{"name":"MODIFY_SHAPE_REMOVE_COUNT","cost":4.62},"MODIFY_SIZE_REMOVE_COUNT":{"name":"MODIFY_SIZE_REMOVE_COUNT","cost":4.64},"MODIFY_ROW_REMOVE_COUNT":{"name":"MODIFY_ROW_REMOVE_COUNT","cost":4.68},"MODIFY_COLUMN_REMOVE_COUNT":{"name":"MODIFY_COLUMN_REMOVE_COUNT","cost":4.68},"MODIFY_TEXT_REMOVE_COUNT":{"name":"MODIFY_TEXT_REMOVE_COUNT","cost":4.6},"MOVE_X_ROW":{"name":"MOVE_X_ROW","cost":4.45},"MOVE_X_COLUMN":{"name":"MOVE_X_COLUMN","cost":4.43},"MOVE_X_SIZE":{"name":"MOVE_X_SIZE","cost":4.46},"MOVE_X_SHAPE":{"name":"MOVE_X_SHAPE","cost":4.46},"MOVE_X_COLOR":{"name":"MOVE_X_COLOR","cost":4.46},"MOVE_X_Y":{"name":"MOVE_X_Y","cost":4.44},"MOVE_X_TEXT":{"name":"MOVE_X_TEXT","cost":4.46},"MOVE_Y_ROW":{"name":"MOVE_Y_ROW","cost":4.43},"MOVE_Y_COLUMN":{"name":"MOVE_Y_COLUMN","cost":4.45},"MOVE_Y_SIZE":{"name":"MOVE_Y_SIZE","cost":4.46},"MOVE_Y_SHAPE":{"name":"MOVE_Y_SHAPE","cost":4.46},"MOVE_Y_COLOR":{"name":"MOVE_Y_COLOR","cost":4.46},"MOVE_Y_X":{"name":"MOVE_Y_X","cost":4.44},"MOVE_Y_TEXT":{"name":"MOVE_Y_TEXT","cost":4.46},"MOVE_COLOR_ROW":{"name":"MOVE_COLOR_ROW","cost":4.47},"MOVE_COLOR_COLUMN":{"name":"MOVE_COLOR_COLUMN","cost":4.47},"MOVE_COLOR_SIZE":{"name":"MOVE_COLOR_SIZE","cost":4.43},"MOVE_COLOR_SHAPE":{"name":"MOVE_COLOR_SHAPE","cost":4.43},"MOVE_COLOR_Y":{"name":"MOVE_COLOR_Y","cost":4.46},"MOVE_COLOR_X":{"name":"MOVE_COLOR_X","cost":4.46},"MOVE_COLOR_TEXT":{"name":"MOVE_COLOR_TEXT","cost":4.43},"MOVE_SHAPE_ROW":{"name":"MOVE_SHAPE_ROW","cost":4.47},"MOVE_SHAPE_COLUMN":{"name":"MOVE_SHAPE_COLUMN","cost":4.47},"MOVE_SHAPE_SIZE":{"name":"MOVE_SHAPE_SIZE","cost":4.43},"MOVE_SHAPE_COLOR":{"name":"MOVE_SHAPE_COLOR","cost":4.43},"MOVE_SHAPE_Y":{"name":"MOVE_SHAPE_Y","cost":4.46},"MOVE_SHAPE_X":{"name":"MOVE_SHAPE_X","cost":4.46},"MOVE_SHAPE_TEXT":{"name":"MOVE_SHAPE_TEXT","cost":4.43},"MOVE_SIZE_ROW":{"name":"MOVE_SIZE_ROW","cost":4.47},"MOVE_SIZE_COLUMN":{"name":"MOVE_SIZE_COLUMN","cost":4.47},"MOVE_SIZE_SHAPE":{"name":"MOVE_SIZE_SHAPE","cost":4.43},"MOVE_SIZE_COLOR":{"name":"MOVE_SIZE_COLOR","cost":4.43},"MOVE_SIZE_Y":{"name":"MOVE_SIZE_Y","cost":4.46},"MOVE_SIZE_X":{"name":"MOVE_SIZE_X","cost":4.46},"MOVE_SIZE_TEXT":{"name":"MOVE_SIZE_TEXT","cost":4.43},"MOVE_TEXT_ROW":{"name":"MOVE_TEXT_ROW","cost":4.47},"MOVE_TEXT_COLUMN":{"name":"MOVE_TEXT_COLUMN","cost":4.47},"MOVE_TEXT_SHAPE":{"name":"MOVE_TEXT_SHAPE","cost":4.43},"MOVE_TEXT_COLOR":{"name":"MOVE_TEXT_COLOR","cost":4.43},"MOVE_TEXT_Y":{"name":"MOVE_TEXT_Y","cost":4.46},"MOVE_TEXT_X":{"name":"MOVE_TEXT_X","cost":4.46},"MOVE_TEXT_SIZE":{"name":"MOVE_TEXT_SIZE","cost":4.43},"MOVE_COLUMN_ROW":{"name":"MOVE_COLUMN_ROW","cost":4.44},"MOVE_COLUMN_SIZE":{"name":"MOVE_COLUMN_SIZE","cost":4.47},"MOVE_COLUMN_SHAPE":{"name":"MOVE_COLUMN_SHAPE","cost":4.47},"MOVE_COLUMN_COLOR":{"name":"MOVE_COLUMN_COLOR","cost":4.47},"MOVE_COLUMN_Y":{"name":"MOVE_COLUMN_Y","cost":4.45},"MOVE_COLUMN_X":{"name":"MOVE_COLUMN_X","cost":4.43},"MOVE_COLUMN_TEXT":{"name":"MOVE_COLUMN_TEXT","cost":4.47},"MOVE_ROW_COLUMN":{"name":"MOVE_ROW_COLUMN","cost":4.44},"MOVE_ROW_SIZE":{"name":"MOVE_ROW_SIZE","cost":4.47},"MOVE_ROW_SHAPE":{"name":"MOVE_ROW_SHAPE","cost":4.47},"MOVE_ROW_COLOR":{"name":"MOVE_ROW_COLOR","cost":4.47},"MOVE_ROW_Y":{"name":"MOVE_ROW_Y","cost":4.43},"MOVE_ROW_X":{"name":"MOVE_ROW_X","cost":4.45},"MOVE_ROW_TEXT":{"name":"MOVE_ROW_TEXT","cost":4.47},"SWAP_X_Y":{"name":"SWAP_X_Y","cost":4.42},"SWAP_ROW_COLUMN":{"name":"SWAP_ROW_COLUMN","cost":4.41},"ceiling":{"cost":47.1,"alternatingCost":51.81}} 5 | } -------------------------------------------------------------------------------- /src/transition/neighbor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var util = require('../util'); 3 | var def = require('../editOp/editOpSet'); 4 | function neighbors(spec, additionalFields, additionalChannels, importedEncodingEditOps) { 5 | var neighbors = []; 6 | var encodingEditOps = importedEncodingEditOps || def.DEFAULT_ENCODING_EDIT_OPS; 7 | var inChannels = util.keys(spec.encoding); 8 | var exChannels = additionalChannels; 9 | 10 | inChannels.forEach(function (channel) { 11 | var newNeighbor = util.duplicate(spec); 12 | var editOpType = "REMOVE_" + channel.toUpperCase(); 13 | editOpType += (spec.encoding[channel].field === "*") ? "_COUNT" : ""; 14 | var editOp = util.duplicate(encodingEditOps[editOpType]); 15 | var newAdditionalFields = util.duplicate(additionalFields); 16 | if (util.find(newAdditionalFields, util.rawEqual, newNeighbor.encoding[channel]) === -1) { 17 | newAdditionalFields.push(newNeighbor.encoding[channel]); 18 | } 19 | var newAdditionalChannels = util.duplicate(additionalChannels); 20 | editOp.detail = { 21 | "before": {"field": newNeighbor.encoding[channel].field, channel}, 22 | "after": undefined 23 | }; 24 | 25 | newAdditionalChannels.push(channel); 26 | delete newNeighbor.encoding[channel]; 27 | if (validate(newNeighbor)) { 28 | newNeighbor.editOp = editOp; 29 | newNeighbor.additionalFields = newAdditionalFields; 30 | newNeighbor.additionalChannels = newAdditionalChannels; 31 | neighbors.push(newNeighbor); 32 | } 33 | ; 34 | additionalFields.forEach(function (field, index) { 35 | if ((field.field !== spec.encoding[channel].field) || 36 | (field.type !== spec.encoding[channel].type)) { 37 | newNeighbor = util.duplicate(spec); 38 | editOpType = "MODIFY_" + channel.toUpperCase(); 39 | if (spec.encoding[channel].field === "*" && field.field !== "*") { 40 | editOpType += "_REMOVE_COUNT"; 41 | } 42 | else if (spec.encoding[channel].field !== "*" && field.field === "*") { 43 | editOpType += "_ADD_COUNT"; 44 | } 45 | editOp = util.duplicate(encodingEditOps[editOpType]); 46 | newAdditionalFields = util.duplicate(additionalFields); 47 | newAdditionalFields.splice(index, 1); 48 | if (util.find(newAdditionalFields, util.rawEqual, newNeighbor.encoding[channel]) === -1) { 49 | newAdditionalFields.push(newNeighbor.encoding[channel]); 50 | } 51 | newAdditionalChannels = util.duplicate(additionalChannels); 52 | newNeighbor.encoding[channel] = field; 53 | editOp.detail = { 54 | "before": { ...spec.encoding[channel], channel}, 55 | "after": { ...field, channel} 56 | }; 57 | 58 | if (validate(newNeighbor)) { 59 | newNeighbor.editOp = editOp; 60 | newNeighbor.additionalFields = newAdditionalFields; 61 | newNeighbor.additionalChannels = newAdditionalChannels; 62 | neighbors.push(newNeighbor); 63 | } 64 | ; 65 | } 66 | }); 67 | inChannels.forEach(function (anotherChannel) { 68 | if (anotherChannel === channel 69 | || (["x", "y"].indexOf(channel) < 0 || ["x", "y"].indexOf(anotherChannel) < 0)) { 70 | return; 71 | } 72 | newNeighbor = util.duplicate(spec); 73 | editOp = util.duplicate(encodingEditOps["SWAP_X_Y"]); 74 | newAdditionalFields = util.duplicate(additionalFields); 75 | newAdditionalChannels = util.duplicate(additionalChannels); 76 | var tempChannel = util.duplicate(newNeighbor.encoding[channel]); 77 | newNeighbor.encoding[channel] = newNeighbor.encoding[anotherChannel]; 78 | newNeighbor.encoding[anotherChannel] = tempChannel; 79 | editOp.detail = { 80 | "before": {"field": spec.encoding["x"].field, "channel": "x"}, 81 | "after": {"field": spec.encoding["y"].field, "channel": "y"} 82 | }; 83 | 84 | if (validate(newNeighbor)) { 85 | newNeighbor.editOp = editOp; 86 | newNeighbor.additionalFields = newAdditionalFields; 87 | newNeighbor.additionalChannels = newAdditionalChannels; 88 | neighbors.push(newNeighbor); 89 | }; 90 | }); 91 | exChannels.forEach(function (exChannel, index) { 92 | newNeighbor = util.duplicate(spec); 93 | var newNeighborChannels = (channel + "_" + exChannel).toUpperCase(); 94 | editOp = util.duplicate(encodingEditOps["MOVE_" + newNeighborChannels]); 95 | newAdditionalFields = util.duplicate(additionalFields); 96 | newAdditionalChannels = util.duplicate(additionalChannels); 97 | newAdditionalChannels.splice(index, 1); 98 | newAdditionalChannels.push(channel); 99 | newNeighbor.encoding[exChannel] = util.duplicate(newNeighbor.encoding[channel]); 100 | delete newNeighbor.encoding[channel]; 101 | editOp.detail = { 102 | "before": {channel}, 103 | "after": {"channel": exChannel} 104 | }; 105 | 106 | if (validate(newNeighbor)) { 107 | newNeighbor.editOp = editOp; 108 | newNeighbor.additionalFields = newAdditionalFields; 109 | newNeighbor.additionalChannels = newAdditionalChannels; 110 | neighbors.push(newNeighbor); 111 | } 112 | ; 113 | }); 114 | }); 115 | exChannels.forEach(function (channel, chIndex) { 116 | additionalFields.forEach(function (field, index) { 117 | var newNeighbor = util.duplicate(spec); 118 | var editOpType = "ADD_" + channel.toUpperCase(); 119 | editOpType += (field.field === "*") ? "_COUNT" : ""; 120 | var editOp = util.duplicate(encodingEditOps[editOpType]); 121 | var newAdditionalFields = util.duplicate(additionalFields); 122 | var newAdditionalChannels = util.duplicate(additionalChannels); 123 | newAdditionalFields.splice(index, 1); 124 | newNeighbor.encoding[channel] = field; 125 | newAdditionalChannels.splice(chIndex, 1); 126 | 127 | editOp.detail = { 128 | "before": undefined, 129 | "after": {"field": field.field, channel} 130 | }; 131 | 132 | if (validate(newNeighbor)) { 133 | newNeighbor.editOp = editOp; 134 | newNeighbor.additionalFields = newAdditionalFields; 135 | newNeighbor.additionalChannels = newAdditionalChannels; 136 | neighbors.push(newNeighbor); 137 | } 138 | ; 139 | }); 140 | }); 141 | for (var i = 0; i < neighbors.length; i += 1) { 142 | for (var j = i + 1; j < neighbors.length; j += 1) { 143 | if (sameEncoding(neighbors[i].encoding, neighbors[j].encoding)) { 144 | neighbors.splice(j, 1); 145 | j -= 1; 146 | } 147 | } 148 | } 149 | return neighbors; 150 | } 151 | exports.neighbors = neighbors; 152 | function validate(spec) { 153 | return true; 154 | } 155 | function sameEncoding(a, b) { 156 | var aKeys = util.keys(a); 157 | var bKeys = util.keys(b); 158 | if (aKeys.length !== bKeys.length) { 159 | return false; 160 | } 161 | var allKeys = util.union(aKeys, bKeys); 162 | for (var i = 0; i < allKeys.length; i += 1) { 163 | var key = allKeys[i]; 164 | if (!(a[key] && b[key])) { 165 | return false; 166 | } 167 | if ((a[key].field !== b[key].field) || a[key].type !== b[key].type) { 168 | return false; 169 | } 170 | } 171 | return true; 172 | } 173 | exports.sameEncoding = sameEncoding; -------------------------------------------------------------------------------- /app/js/sequence-index.js: -------------------------------------------------------------------------------- 1 | $(document).on('ready page:load', function () { 2 | 3 | var results,uniqDP,uniqD, rankSequenceCost, rankTransitionCosts, rankSequenceCostTie, rankTransitionCostsTie; 4 | 5 | 6 | var worker = new Worker('js/sequence-worker.js'); 7 | 8 | 9 | 10 | $('#toggle-show-all').on('click',function(e){ 11 | if (!$(this).hasClass('active')) { 12 | $('#sorted-result .not.optimum').removeClass('hidden'); 13 | $(this).addClass('active'); 14 | $(this).text('Show Optimum Only'); 15 | } else { 16 | $('#sorted-result .not.optimum').addClass('hidden'); 17 | $(this).removeClass('active'); 18 | $(this).text('Show All'); 19 | } 20 | }); 21 | 22 | $('#toggle-pattern-score').on('click',function(e){ 23 | if ($(this).hasClass('active')) { 24 | results = results.sort(function(a,b){ 25 | if (a.sumOfTransitionCosts > b.sumOfTransitionCosts) { 26 | return 1; 27 | } 28 | if (a.sumOfTransitionCosts < b.sumOfTransitionCosts) { 29 | return -1; 30 | } else { 31 | return a.sequence.join(',') > b.sequence.join(',') ? 1 : -1; 32 | } 33 | return 0; 34 | }); 35 | var minSumOfTransitionCosts = results[0].sumOfTransitionCosts; 36 | for (var i = 0; i < results.length; i++) { 37 | if(results[i].sumOfTransitionCosts === minSumOfTransitionCosts){ 38 | results[i].isOptimum = true; 39 | } 40 | else { 41 | results[i].isOptimum = false; 42 | } 43 | } 44 | listButtons(results) 45 | $(this).removeClass('active'); 46 | $(this).text('On Pattern Score'); 47 | 48 | } else { 49 | results = results.sort(function(a,b){ 50 | if (a.sequenceCost > b.sequenceCost) { 51 | return 1; 52 | } 53 | if (a.sequenceCost < b.sequenceCost) { 54 | return -1; 55 | } else { 56 | return a.sequence.join(',') > b.sequence.join(',') ? 1 : -1; 57 | } 58 | return 0; 59 | }); 60 | var maxSequenceCost = results[0].sequenceCost; 61 | for (var i = 0; i < results.length; i++) { 62 | if(results[i].sequenceCost === maxSequenceCost){ 63 | results[i].isOptimum = true; 64 | } 65 | else { 66 | results[i].isOptimum = false; 67 | } 68 | } 69 | 70 | listButtons(results); 71 | $(this).addClass('active'); 72 | $(this).text('Off Pattern Score'); 73 | } 74 | 75 | }); 76 | 77 | 78 | $('#sort').on('click', function(e){ 79 | 80 | 81 | var specs = JSON.parse($('#specs').val()); 82 | var fixFirst = $('#fixfirst').is(':checked'); 83 | 84 | // Run 85 | $('#current-status').show(500, function(){ 86 | 87 | worker.onmessage = function(e) { 88 | 89 | 90 | 91 | 92 | setTimeout(function(){ 93 | $('#current-status').hide(500); 94 | }, 1); 95 | results = e.data; 96 | 97 | allDP = results.map(overallDP).sort(function(a,b){ return a-b;}); 98 | uniqDP = d3.set(results.map(overallDP)) 99 | .values() 100 | .map(function(val){ return Number(val); }) 101 | .sort(function(a,b){ return a-b;}); 102 | 103 | 104 | allD = results.map(overallD).sort(function(a,b){ return a-b;}); 105 | uniqD = d3.set(results.map(overallD)) 106 | .values() 107 | .map(function(val){ return Number(val); }) 108 | .sort(function(a,b){ return a-b;}); 109 | 110 | 111 | 112 | 113 | rankSequenceCost = d3.scale.ordinal() 114 | .domain(uniqDP) 115 | .rangePoints([1, uniqDP.length]); 116 | 117 | rankTransitionCosts = d3.scale.ordinal() 118 | .domain(uniqD) 119 | .rangePoints([1, uniqD.length]); 120 | 121 | 122 | if (!$('#toggle-pattern-score').hasClass('active')) { 123 | $('#toggle-pattern-score').addClass('active'); 124 | $('#toggle-pattern-score').text('Off Pattern Score'); 125 | } 126 | if ($('#toggle-show-all').hasClass('active')) { 127 | $('#toggle-show-all').removeClass('active'); 128 | $('#toggle-show-all').text('Show All'); 129 | } 130 | listButtons(results, fixFirst); 131 | 132 | } 133 | worker.postMessage({specs: specs, options: {"fixFirst": fixFirst}}); // Start the worker. 134 | }); 135 | 136 | }) 137 | function listButtons(results, fixFirst){ 138 | 139 | $('#sorted-result .optimum').children().remove(); 140 | $('#sorted-result .not.optimum').children().remove(); 141 | $('#control-result').removeClass("hidden"); 142 | 143 | for (var i = 0; i < results.length; i++) { 144 | var sequence = results[i].sequence; 145 | if (!fixFirst) { 146 | sequence = sequence.map(function(chart){ return chart - 1;}).splice(1); 147 | }; 148 | 149 | var link = $('') 150 | .html(sequence.join(',') + ' | ' + Math.round(results[i].sequenceCost*100)/100 ) 151 | .addClass('result btn btn-xs') 152 | .data('result', results[i]); 153 | 154 | if (results[i].isOptimum) { 155 | link.addClass('btn-primary'); 156 | $('#sorted-result .optimum').not('.not').append(link); 157 | } 158 | else { 159 | link.addClass('btn-default'); 160 | $('#sorted-result .not.optimum').append(link); 161 | }; 162 | 163 | }; 164 | 165 | $('.result').on('click', function(){ 166 | drawingByOrder(results[$(this).data('id')]); 167 | console.log(results[$(this).data('id')]); 168 | }); 169 | } 170 | 171 | 172 | 173 | function drawingByOrder(result){ 174 | var specs = result.charts; 175 | 176 | var metaInfo = "Rank(Pattern) : " + rankSequenceCost(overallDP(result)) + "
" 177 | + "Rank(Simple Sum) : " + rankTransitionCosts(overallD(result)) + "
" 178 | + "Rank(Pattern inc. ties) : " + (allDP.indexOf(overallDP(result))+1) + "
" 179 | + "Rank(Simple Sum inc. ties) : " + (allD.indexOf(overallD(result))+1) + "
" 180 | + "Sequence Cost : " + result.sequenceCost + "
" 181 | + "Sum of Transition Costs : " + result.sumOfTransitionCosts + "
" 182 | + "Global Weighting Term : " + result.globalWeightingTerm + "
" 183 | + "Patterns : " + JSON.stringify(result.patterns) + "
" 184 | + "Filter Sequence Cost : " + result.filterSequenceCost + "
" 185 | + (Object.keys(result.filterSequenceCostReasons).length > 0 ? "Filter Sequence Cost Details : " + JSON.stringify(result.filterSequenceCostReasons) + "
" : "" ); 186 | 187 | $('#sequence-meta-info').html(metaInfo); 188 | 189 | 190 | var specsDiv = $('#sequence'); 191 | specsDiv.children().remove(); 192 | if (!isEmpty(specsDiv)) { 193 | for (var i = 0; i < specs.length; i++) { 194 | var newRowDiv = $("
"); 195 | var VLdiv = $("
").attr("class","col-xs-6"); 196 | newRowDiv.append(VLdiv); 197 | draw("#vega-lite-" + i, specs[i]); 198 | if (i>0) { 199 | var TRdiv = $("
").attr("class","col-xs-6"); 200 | var TRinfo = "Distance : " + result.transitions[i-1].cost + "
" 201 | + "Marktype Transitions : " + JSON.stringify(result.transitions[i-1].mark) + "
" 202 | + "Encoding Transitions : " + JSON.stringify(result.transitions[i-1].encoding) + "
" 203 | + "Transform Transitions : " + JSON.stringify(result.transitions[i-1].transform); 204 | TRdiv.html(TRinfo); 205 | newRowDiv.append(TRdiv); 206 | 207 | }; 208 | specsDiv.append(newRowDiv); 209 | } 210 | } 211 | } 212 | }); 213 | 214 | function overallDP(d){ 215 | return d.sequenceCost; 216 | } 217 | function overallD(d){ 218 | return d.sumOfTransitionCosts; 219 | } 220 | -------------------------------------------------------------------------------- /src/transition/apply.js: -------------------------------------------------------------------------------- 1 | const util = require('../util'); 2 | const vega = require('vega'); 3 | const vl = require('vega-lite') 4 | const {parsePredicateFilter} = require('./trans'); 5 | const { OPS, LOGIC_OPS } = require('../constants'); 6 | function apply (sSpec, eSpec, editOps) { 7 | checkApplyingEditOps(editOps); 8 | 9 | let resultSpec = editOps.reduce((resultSpec, editOp) => { 10 | if (editOp.type === "mark") { 11 | resultSpec = applyMarkEditOp(resultSpec, eSpec, editOp); 12 | } else if (editOp.type === "transform") { 13 | resultSpec = applyTransformEditOp(resultSpec, eSpec, editOp); 14 | } else if (editOp.type === "encoding") { 15 | resultSpec = applyEncodingEditOp(resultSpec, eSpec, editOp); 16 | } 17 | return resultSpec; 18 | }, util.duplicate(sSpec))//an intermediate spec by applying edit operations on the sSpec 19 | 20 | checkSpec(resultSpec); 21 | return resultSpec; 22 | } 23 | exports.apply = apply 24 | 25 | function applyMarkEditOp(targetSpec, eSpec, editOp) { 26 | let resultSpec = util.duplicate(targetSpec); 27 | resultSpec.mark = eSpec.mark 28 | return resultSpec; 29 | } 30 | exports.applyMarkEditOp = applyMarkEditOp; 31 | 32 | function applyTransformEditOp(targetSpec, eSpec, editOp){ 33 | let resultSpec = util.duplicate(targetSpec); 34 | const transformType = editOp.name.toLowerCase(); 35 | const details = !util.isArray(editOp.detail) ? [editOp.detail] : editOp.detail; 36 | 37 | if (transformType.indexOf("filter") >= 0) { 38 | if (editOp.name === "REMOVE_FILTER" || editOp.name === "MODIFY_FILTER") { 39 | 40 | resultSpec.transform.filter(tfm => { 41 | return tfm.filter && ((parsePredicateFilter(tfm.filter)[0].id === editOp.detail.id)) 42 | }).forEach(filter => { 43 | if (resultSpec.transform) { 44 | 45 | let i = resultSpec.transform.findIndex(trsfm => util.deepEqual(trsfm, filter)) 46 | 47 | resultSpec.transform.splice(i, 1); 48 | } 49 | }) 50 | } 51 | if (editOp.name === "ADD_FILTER" || editOp.name === "MODIFY_FILTER") { 52 | eSpec.transform.filter(tfm => { 53 | return tfm.filter && ((parsePredicateFilter(tfm.filter)[0].id === editOp.detail.id)) 54 | }).forEach(filter => { 55 | if (!resultSpec.transform) { 56 | resultSpec.transform = [filter]; 57 | } else if (!resultSpec.transform.find(trsfm => util.deepEqual(filter, trsfm))) { 58 | resultSpec.transform.push(filter); 59 | } 60 | }) 61 | } 62 | } else { 63 | details.forEach(detail => { 64 | let fieldDef = resultSpec.encoding[detail.channel]; 65 | if (fieldDef) { 66 | //Todo: cannot apply SCALE if the channel has a different type. 67 | if (detail.how === "removed"){ 68 | delete fieldDef[transformType] 69 | } else { 70 | // console.log(fieldDef.type, detail.fieldType) 71 | if (transformType === "scale" && fieldDef.type !== detail.fieldType) { 72 | throw new UnapplicableEditOPError(`Cannot apply ${editOp.name} since it requires "${detail.fieldType}" field instead of "${fieldDef.type}".`) 73 | } 74 | fieldDef[transformType] = eSpec.encoding[detail.channel][transformType] 75 | } 76 | } else { 77 | throw new UnapplicableEditOPError(`Cannot apply ${editOp.name} since there is no "${detail.channel}" channel.`) 78 | } 79 | }) 80 | 81 | } 82 | return resultSpec; 83 | } 84 | exports.applyTransformEditOp = applyTransformEditOp; 85 | 86 | 87 | function applyEncodingEditOp(targetSpec, eSpec, editOp){ 88 | let resultSpec = util.duplicate(targetSpec); 89 | if (editOp.name.indexOf("REMOVE") === 0) { 90 | let channel = editOp.detail.before.channel; 91 | if (resultSpec.encoding[channel]) { 92 | delete resultSpec.encoding[channel]; 93 | } else { 94 | throw new UnapplicableEditOPError(`Cannot apply ${editOp.name} since there is no "${channel}" channel.`); 95 | } 96 | } else if (editOp.name.indexOf("ADD") === 0) { 97 | let channel = editOp.detail.after.channel; 98 | if (resultSpec.encoding[channel]) { 99 | throw new UnapplicableEditOPError(`Cannot apply ${editOp.name} since "${channel}" already exists.`); 100 | } else { 101 | resultSpec.encoding[channel] = util.duplicate(eSpec.encoding[channel]); 102 | } 103 | } else if (editOp.name.indexOf("MOVE") === 0) { 104 | let sChannel = editOp.detail.before.channel, 105 | dChannel = editOp.detail.after.channel; 106 | if (!resultSpec.encoding[sChannel]) { 107 | throw new UnapplicableEditOPError(`Cannot apply ${editOp.name} since there is no "${sChannel}" channel.`); 108 | } else if (resultSpec.encoding[dChannel]) { 109 | throw new UnapplicableEditOPError(`Cannot apply ${editOp.name} since "${dChannel}" already exists.`); 110 | } else { 111 | resultSpec.encoding[dChannel] = util.duplicate(resultSpec.encoding[sChannel]) 112 | delete resultSpec.encoding[sChannel]; 113 | } 114 | } else if (editOp.name.indexOf("MODIFY") === 0) { 115 | let channel = editOp.detail.before.channel, 116 | field = editOp.detail.after.field, 117 | type = editOp.detail.after.type; 118 | if (!resultSpec.encoding[channel]) { 119 | throw new UnapplicableEditOPError(`Cannot apply ${editOp.name} since there is no "${channel}" channel.`); 120 | } else { 121 | resultSpec.encoding[channel].field = field; 122 | resultSpec.encoding[channel].type = type; 123 | } 124 | } else if (editOp.name.indexOf("SWAP_X_Y") === 0) { 125 | if (!resultSpec.encoding.x || !resultSpec.encoding.y) { 126 | throw new UnapplicableEditOPError(`Cannot apply ${editOp.name} since there is no "x" and "y" channels.`); 127 | } else { 128 | let temp = util.duplicate(resultSpec.encoding.y); 129 | resultSpec.encoding.y = util.duplicate(resultSpec.encoding.x); 130 | resultSpec.encoding.x = temp; 131 | } 132 | } 133 | 134 | return resultSpec; 135 | } 136 | exports.applyEncodingEditOp = applyEncodingEditOp; 137 | 138 | function checkSpec(spec) { 139 | let lg = vega.logger(); 140 | const warnings = [], errors = []; 141 | lg.warn = (m) => { 142 | warnings.push(m); 143 | } 144 | lg.error = (m) => { 145 | errors.push(m); 146 | } 147 | vl.compile(spec, {logger: lg}) 148 | 149 | let hasAggregate = false; 150 | for (const key in spec.encoding) { 151 | if (spec.encoding.hasOwnProperty(key)) { 152 | const fieldDef = spec.encoding[key]; 153 | if (fieldDef.aggregate) { 154 | hasAggregate = true; 155 | } 156 | if (fieldDef.field === "*" && !fieldDef.aggregate) { 157 | warnings.push("'*' field should innclude aggregate.") 158 | } 159 | } 160 | } 161 | if (hasAggregate) { 162 | const hasNoAggOnQField = Object.keys(spec.encoding) 163 | .filter(ch => { 164 | return spec.encoding[ch].type === "quantitative" && !spec.encoding[ch].aggregate 165 | }).length > 0 166 | if (hasNoAggOnQField) { 167 | warnings.push("Aggregate should be applied on all quantitative fields.") 168 | } 169 | } 170 | 171 | 172 | if ((warnings.length > 0) || (errors.length > 0)) { 173 | throw new InvalidVLSpecError(`The resulted spec is not valid Vega-Lite Spec.`, {warnings, errors}) 174 | } 175 | } 176 | function checkApplyingEditOps(editOps) { 177 | // _COUNT encodig should be applied with AGGREGATE 178 | if ( 179 | editOps.find(eo => eo.name.indexOf("_COUNT") >= 0) && 180 | !editOps.find(eo => eo.name === "AGGREGATE") 181 | ) { 182 | throw new UnapplicableEditOpsError("_COUNT encoding edit operations cannot be applied without AGGREGATE."); 183 | } 184 | } 185 | class UnapplicableEditOPError extends Error { 186 | constructor(message) { 187 | super(message); 188 | this.name = "UnapplicableEditOPError" 189 | } 190 | } 191 | 192 | class InvalidVLSpecError extends Error { 193 | constructor(message, info) { 194 | super(message); 195 | this.name = "InvalidVLSpecError" 196 | this.info = info; 197 | } 198 | } 199 | class UnapplicableEditOpsError extends Error { 200 | constructor(message) { 201 | super(message); 202 | this.name = "UnapplicableEditOpsError" 203 | } 204 | } 205 | 206 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.isArray = Array.isArray || function (obj) { 3 | return {}.toString.call(obj) === '[object Array]'; 4 | }; 5 | function isString(item) { 6 | return typeof item === 'string' || item instanceof String; 7 | } 8 | exports.isString = isString; 9 | function isin(item, array) { 10 | return array.indexOf(item) !== -1; 11 | } 12 | exports.isin = isin; 13 | 14 | function json(s, sp) { 15 | return JSON.stringify(s, null, sp); 16 | } 17 | exports.json = json; 18 | 19 | function keys(obj) { 20 | var k = [], x; 21 | for (x in obj) { 22 | k.push(x); 23 | } 24 | return k; 25 | } 26 | exports.keys = keys; 27 | 28 | function duplicate(obj) { 29 | if (obj === undefined) { 30 | return undefined; 31 | } 32 | return JSON.parse(JSON.stringify(obj)); 33 | } 34 | exports.duplicate = duplicate; 35 | exports.copy = duplicate; 36 | 37 | function forEach(obj, f, thisArg) { 38 | if (obj.forEach) { 39 | obj.forEach.call(thisArg, f); 40 | } 41 | else { 42 | for (var k in obj) { 43 | f.call(thisArg, obj[k], k, obj); 44 | } 45 | } 46 | } 47 | exports.forEach = forEach; 48 | 49 | function any(arr, f) { 50 | var i = 0, k; 51 | for (k in arr) { 52 | if (f(arr[k], k, i++)) { 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | exports.any = any; 59 | 60 | function nestedMap(collection, f, level, filter) { 61 | return level === 0 ? 62 | collection.map(f) : 63 | collection.map(function (v) { 64 | var r = nestedMap(v, f, level - 1); 65 | return filter ? r.filter(nonEmpty) : r; 66 | }); 67 | } 68 | exports.nestedMap = nestedMap; 69 | 70 | function nestedReduce(collection, f, level, filter) { 71 | return level === 0 ? 72 | collection.reduce(f, []) : 73 | collection.map(function (v) { 74 | var r = nestedReduce(v, f, level - 1); 75 | return filter ? r.filter(nonEmpty) : r; 76 | }); 77 | } 78 | exports.nestedReduce = nestedReduce; 79 | 80 | function nonEmpty(grp) { 81 | return !exports.isArray(grp) || grp.length > 0; 82 | } 83 | exports.nonEmpty = nonEmpty; 84 | 85 | function traverse(node, arr) { 86 | if (node.value !== undefined) { 87 | arr.push(node.value); 88 | } 89 | else { 90 | if (node.left) { 91 | traverse(node.left, arr); 92 | } 93 | if (node.right) { 94 | traverse(node.right, arr); 95 | } 96 | } 97 | return arr; 98 | } 99 | exports.traverse = traverse; 100 | 101 | function extend(obj, b) { 102 | var rest = []; 103 | for (var _i = 2; _i < arguments.length; _i++) { 104 | rest[_i - 2] = arguments[_i]; 105 | } 106 | for (var x, name, i = 1, len = arguments.length; i < len; ++i) { 107 | x = arguments[i]; 108 | for (name in x) { 109 | obj[name] = x[name]; 110 | } 111 | } 112 | return obj; 113 | } 114 | exports.extend = extend; 115 | 116 | function union(arr1, arr2, accessor = (d) => d) { 117 | let result = [...arr1]; 118 | return result.concat( 119 | arr2.filter(x => !arr1.find(y => accessor(x) === accessor(y))) 120 | ); 121 | } 122 | exports.union = union; 123 | 124 | var gen; 125 | (function (gen) { 126 | function getOpt(opt) { 127 | return (opt ? keys(opt) : []).reduce(function (c, k) { 128 | c[k] = opt[k]; 129 | return c; 130 | }, Object.create({})); 131 | } 132 | gen.getOpt = getOpt; 133 | ; 134 | })(gen = exports.gen || (exports.gen = {})); 135 | function powerset(list) { 136 | var ps = [ 137 | [] 138 | ]; 139 | for (var i = 0; i < list.length; i++) { 140 | for (var j = 0, len = ps.length; j < len; j++) { 141 | ps.push(ps[j].concat(list[i])); 142 | } 143 | } 144 | return ps; 145 | } 146 | exports.powerset = powerset; 147 | 148 | function chooseKorLess(list, k) { 149 | var subset = [[]]; 150 | for (var i = 0; i < list.length; i++) { 151 | for (var j = 0, len = subset.length; j < len; j++) { 152 | var sub = subset[j].concat(list[i]); 153 | if (sub.length <= k) { 154 | subset.push(sub); 155 | } 156 | } 157 | } 158 | return subset; 159 | } 160 | exports.chooseKorLess = chooseKorLess; 161 | 162 | function chooseK(list, k) { 163 | var subset = [[]]; 164 | var kArray = []; 165 | for (var i = 0; i < list.length; i++) { 166 | for (var j = 0, len = subset.length; j < len; j++) { 167 | var sub = subset[j].concat(list[i]); 168 | if (sub.length < k) { 169 | subset.push(sub); 170 | } 171 | else if (sub.length === k) { 172 | kArray.push(sub); 173 | } 174 | } 175 | } 176 | return kArray; 177 | } 178 | exports.chooseK = chooseK; 179 | 180 | function cross(a, b) { 181 | var x = []; 182 | for (var i = 0; i < a.length; i++) { 183 | for (var j = 0; j < b.length; j++) { 184 | x.push(a[i].concat(b[j])); 185 | } 186 | } 187 | return x; 188 | } 189 | exports.cross = cross; 190 | 191 | function find(array, f, obj) { 192 | for (var i = 0; i < array.length; i += 1) { 193 | if (f(obj) === f(array[i])) { 194 | return i; 195 | } 196 | } 197 | return -1; 198 | } 199 | exports.find = find; 200 | function rawEqual(a, b) { 201 | return JSON.stringify(a) === JSON.stringify(b); 202 | } 203 | exports.rawEqual = rawEqual; 204 | function arrayDiff(a, b, f) { 205 | return a.filter(function (x) { 206 | if (!f) { 207 | return b.findIndex(y => deepEqual(x,y)) < 0; 208 | } 209 | else 210 | return find(b, f, x) < 0; 211 | }); 212 | } 213 | exports.arrayDiff = arrayDiff; 214 | function unionObjectArray(a, b, f) { 215 | return arrayDiff(a, b, f).concat(b); 216 | } 217 | exports.unionObjectArray = unionObjectArray; 218 | 219 | function deepEqual(obj1, obj2) { 220 | if (obj1 === obj2) { 221 | return true; 222 | } 223 | if (isDate(obj1) && isDate(obj2)) { 224 | return Number(obj1) === Number(obj2); 225 | } 226 | if ( 227 | typeof obj1 === "object" && 228 | obj1 !== undefined && 229 | typeof obj2 === "object" && 230 | obj2 !== undefined 231 | ) { 232 | const props1 = Object.keys(obj1); 233 | const props2 = Object.keys(obj2); 234 | if (props1.length !== props2.length) { 235 | return false; 236 | } 237 | 238 | for (let i = 0; i < props1.length; i++) { 239 | const prop = props1[i]; 240 | 241 | if (!Object.prototype.hasOwnProperty.call(obj2, prop) || !deepEqual(obj1[prop], obj2[prop])) { 242 | return false; 243 | } 244 | } 245 | return true; 246 | } 247 | return false; 248 | } 249 | exports.deepEqual = deepEqual; 250 | 251 | 252 | function isDate(o) { 253 | return o !== undefined && typeof o.getMonth === "function"; 254 | } 255 | 256 | // partitioning the array into N_p arrays 257 | function partition(arr, N_p) { 258 | if (arr.length === N_p) { 259 | return [arr.map(item => [item])] 260 | } else if (N_p === 1) { 261 | return [[arr]] 262 | } else if (N_p > arr.length) { 263 | throw new Error(`Cannot partition the array of ${arr.length} into ${N_p}.`); 264 | } else if (arr.length === 0) { 265 | return; 266 | } 267 | let item = [arr[0]]; 268 | let newArr = arr.slice(1); 269 | let results = partition(newArr, N_p - 1).map(pt => { 270 | let newPt = duplicate(pt); 271 | newPt.push(item) 272 | return newPt 273 | }); 274 | return partition(newArr, N_p).reduce((results, currPt) => { 275 | 276 | return results.concat(currPt.map((p, i, currPt) => { 277 | let newPt = duplicate(currPt); 278 | let newP = duplicate(p); 279 | newP.push(item[0]); 280 | newPt[i] = newP; 281 | return newPt; 282 | })); 283 | }, results) 284 | } 285 | exports.partition = partition; 286 | 287 | function permutate(arr) { 288 | if (arr.length === 1) { 289 | return [arr]; 290 | } 291 | if (arr.length === 2) { 292 | return [arr, [arr[1], arr[0]]]; 293 | } 294 | return arr.reduce((acc, anchor, i) => { 295 | const workingArr = duplicate(arr); 296 | workingArr.splice(i, 1); 297 | 298 | acc = acc.concat( 299 | permutate(workingArr).map(newArr => { 300 | return [anchor].concat(newArr); 301 | }) 302 | ); 303 | return acc; 304 | }, []); 305 | } 306 | exports.permutate = permutate; 307 | 308 | function intersection(arr1, arr2, accessor = (d) => d) { 309 | return arr2.filter(x => arr1.filter(y => accessor(x) === accessor(y)).length > 0) 310 | } 311 | exports.intersection = intersection 312 | 313 | function unique(arr, accessor = (d) => d) { 314 | let maps = arr.map(accessor).reduce((acc, curr) => { 315 | acc[curr] = true; 316 | return acc; 317 | }, {}); 318 | return Object.keys(maps) 319 | } 320 | exports.unique = unique; 321 | //# sourceMappingURL=util.js.map -------------------------------------------------------------------------------- /app/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"}] -------------------------------------------------------------------------------- /test/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"}] -------------------------------------------------------------------------------- /test/editOpSetForTest.js: -------------------------------------------------------------------------------- 1 | exports.DEFAULT_EDIT_OPS = { 2 | "markEditOps": { 3 | "AREA_BAR": {"name": "AREA_BAR","cost": 0.03}, 4 | "AREA_LINE": {"name": "AREA_LINE","cost": 0.02}, 5 | "AREA_POINT": {"name": "AREA_POINT","cost": 0.04}, 6 | "AREA_TEXT": {"name": "AREA_TEXT","cost": 0.08}, 7 | "AREA_TICK": {"name": "AREA_TICK","cost": 0.04}, 8 | "BAR_LINE": {"name": "BAR_LINE","cost": 0.04}, 9 | "BAR_POINT": {"name": "BAR_POINT","cost": 0.02}, 10 | "BAR_TEXT": {"name": "BAR_TEXT","cost": 0.06}, 11 | "BAR_TICK": {"name": "BAR_TICK","cost": 0.02}, 12 | "LINE_POINT": {"name": "LINE_POINT","cost": 0.03}, 13 | "LINE_TEXT": {"name": "LINE_TEXT","cost": 0.07}, 14 | "LINE_TICK": {"name": "LINE_TICK","cost": 0.03}, 15 | "POINT_TEXT": {"name": "POINT_TEXT","cost": 0.05}, 16 | "POINT_TICK": {"name": "POINT_TICK","cost": 0.01}, 17 | "TEXT_TICK": {"name": "TEXT_TICK","cost": 0.05} 18 | }, 19 | "transformEditOps": { 20 | "SCALE": {"name": "SCALE","cost": 0.6}, 21 | "SORT": {"name": "SORT","cost": 0.61}, 22 | "BIN": {"name": "BIN","cost": 0.62}, 23 | "AGGREGATE": {"name": "AGGREGATE","cost": 0.63}, 24 | "ADD_FILTER": {"name": "ADD_FILTER","cost": 0.65}, 25 | "REMOVE_FILTER": {"name": "REMOVE_FILTER","cost": 0.65}, 26 | "MODIFY_FILTER": {"name": "MODIFY_FILTER","cost": 0.64} 27 | }, 28 | "encodingEditOps": { 29 | "ADD_X": {"name": "ADD_X","cost": 4.59}, 30 | "ADD_Y": {"name": "ADD_Y","cost": 4.59}, 31 | "ADD_COLOR": {"name": "ADD_COLOR","cost": 4.55}, 32 | "ADD_SHAPE": {"name": "ADD_SHAPE","cost": 4.51}, 33 | "ADD_SIZE": {"name": "ADD_SIZE","cost": 4.53}, 34 | "ADD_ROW": {"name": "ADD_ROW","cost": 4.57}, 35 | "ADD_COLUMN": {"name": "ADD_COLUMN","cost": 4.57}, 36 | "ADD_TEXT": {"name": "ADD_TEXT","cost": 4.49}, 37 | "ADD_X_COUNT": {"name": "ADD_X_COUNT","cost": 4.58}, 38 | "ADD_Y_COUNT": {"name": "ADD_Y_COUNT","cost": 4.58}, 39 | "ADD_COLOR_COUNT": {"name": "ADD_COLOR_COUNT","cost": 4.54}, 40 | "ADD_SHAPE_COUNT": {"name": "ADD_SHAPE_COUNT","cost": 4.5}, 41 | "ADD_SIZE_COUNT": {"name": "ADD_SIZE_COUNT","cost": 4.52}, 42 | "ADD_ROW_COUNT": {"name": "ADD_ROW_COUNT","cost": 4.56}, 43 | "ADD_COLUMN_COUNT": {"name": "ADD_COLUMN_COUNT","cost": 4.56}, 44 | "ADD_TEXT_COUNT": {"name": "ADD_TEXT_COUNT","cost": 4.48}, 45 | "REMOVE_X_COUNT": {"name": "REMOVE_X_COUNT","cost": 4.58}, 46 | "REMOVE_Y_COUNT": {"name": "REMOVE_Y_COUNT","cost": 4.58}, 47 | "REMOVE_COLOR_COUNT": {"name": "REMOVE_COLOR_COUNT","cost": 4.54}, 48 | "REMOVE_SHAPE_COUNT": {"name": "REMOVE_SHAPE_COUNT","cost": 4.5}, 49 | "REMOVE_SIZE_COUNT": {"name": "REMOVE_SIZE_COUNT","cost": 4.52}, 50 | "REMOVE_ROW_COUNT": {"name": "REMOVE_ROW_COUNT","cost": 4.56}, 51 | "REMOVE_COLUMN_COUNT": {"name": "REMOVE_COLUMN_COUNT","cost": 4.56}, 52 | "REMOVE_TEXT_COUNT": {"name": "REMOVE_TEXT_COUNT","cost": 4.48}, 53 | "REMOVE_X": {"name": "REMOVE_X","cost": 4.59}, 54 | "REMOVE_Y": {"name": "REMOVE_Y","cost": 4.59}, 55 | "REMOVE_COLOR": {"name": "REMOVE_COLOR","cost": 4.55}, 56 | "REMOVE_SHAPE": {"name": "REMOVE_SHAPE","cost": 4.51}, 57 | "REMOVE_SIZE": {"name": "REMOVE_SIZE","cost": 4.53}, 58 | "REMOVE_ROW": {"name": "REMOVE_ROW","cost": 4.57}, 59 | "REMOVE_COLUMN": {"name": "REMOVE_COLUMN","cost": 4.57}, 60 | "REMOVE_TEXT": {"name": "REMOVE_TEXT","cost": 4.49}, 61 | "MODIFY_X": {"name": "MODIFY_X","cost": 4.71}, 62 | "MODIFY_Y": {"name": "MODIFY_Y","cost": 4.71}, 63 | "MODIFY_COLOR": {"name": "MODIFY_COLOR","cost": 4.67}, 64 | "MODIFY_SHAPE": {"name": "MODIFY_SHAPE","cost": 4.63}, 65 | "MODIFY_SIZE": {"name": "MODIFY_SIZE","cost": 4.65}, 66 | "MODIFY_ROW": {"name": "MODIFY_ROW","cost": 4.69}, 67 | "MODIFY_COLUMN": {"name": "MODIFY_COLUMN","cost": 4.69}, 68 | "MODIFY_TEXT": {"name": "MODIFY_TEXT","cost": 4.61}, 69 | "MODIFY_X_ADD_COUNT": {"name": "MODIFY_X_ADD_COUNT","cost": 4.7}, 70 | "MODIFY_Y_ADD_COUNT": {"name": "MODIFY_Y_ADD_COUNT","cost": 4.7}, 71 | "MODIFY_COLOR_ADD_COUNT": {"name": "MODIFY_COLOR_ADD_COUNT","cost": 4.66}, 72 | "MODIFY_SHAPE_ADD_COUNT": {"name": "MODIFY_SHAPE_ADD_COUNT","cost": 4.62}, 73 | "MODIFY_SIZE_ADD_COUNT": {"name": "MODIFY_SIZE_ADD_COUNT","cost": 4.64}, 74 | "MODIFY_ROW_ADD_COUNT": {"name": "MODIFY_ROW_ADD_COUNT","cost": 4.68}, 75 | "MODIFY_COLUMN_ADD_COUNT": {"name": "MODIFY_COLUMN_ADD_COUNT","cost": 4.68}, 76 | "MODIFY_TEXT_ADD_COUNT": {"name": "MODIFY_TEXT_ADD_COUNT","cost": 4.6}, 77 | "MODIFY_X_REMOVE_COUNT": {"name": "MODIFY_X_REMOVE_COUNT","cost": 4.7}, 78 | "MODIFY_Y_REMOVE_COUNT": {"name": "MODIFY_Y_REMOVE_COUNT","cost": 4.7}, 79 | "MODIFY_COLOR_REMOVE_COUNT": {"name": "MODIFY_COLOR_REMOVE_COUNT","cost": 4.66}, 80 | "MODIFY_SHAPE_REMOVE_COUNT": {"name": "MODIFY_SHAPE_REMOVE_COUNT","cost": 4.62}, 81 | "MODIFY_SIZE_REMOVE_COUNT": {"name": "MODIFY_SIZE_REMOVE_COUNT","cost": 4.64}, 82 | "MODIFY_ROW_REMOVE_COUNT": {"name": "MODIFY_ROW_REMOVE_COUNT","cost": 4.68}, 83 | "MODIFY_COLUMN_REMOVE_COUNT": {"name": "MODIFY_COLUMN_REMOVE_COUNT","cost": 4.68}, 84 | "MODIFY_TEXT_REMOVE_COUNT": {"name": "MODIFY_TEXT_REMOVE_COUNT","cost": 4.6}, 85 | "MOVE_X_ROW": {"name": "MOVE_X_ROW","cost": 4.45}, 86 | "MOVE_X_COLUMN": {"name": "MOVE_X_COLUMN","cost": 4.43}, 87 | "MOVE_X_SIZE": {"name": "MOVE_X_SIZE","cost": 4.46}, 88 | "MOVE_X_SHAPE": {"name": "MOVE_X_SHAPE","cost": 4.46}, 89 | "MOVE_X_COLOR": {"name": "MOVE_X_COLOR","cost": 4.46}, 90 | "MOVE_X_Y": {"name": "MOVE_X_Y","cost": 4.44}, 91 | "MOVE_X_TEXT": {"name": "MOVE_X_TEXT","cost": 4.46}, 92 | "MOVE_Y_ROW": {"name": "MOVE_Y_ROW","cost": 4.43}, 93 | "MOVE_Y_COLUMN": {"name": "MOVE_Y_COLUMN","cost": 4.45}, 94 | "MOVE_Y_SIZE": {"name": "MOVE_Y_SIZE","cost": 4.46}, 95 | "MOVE_Y_SHAPE": {"name": "MOVE_Y_SHAPE","cost": 4.46}, 96 | "MOVE_Y_COLOR": {"name": "MOVE_Y_COLOR","cost": 4.46}, 97 | "MOVE_Y_X": {"name": "MOVE_Y_X","cost": 4.44}, 98 | "MOVE_Y_TEXT": {"name": "MOVE_Y_TEXT","cost": 4.46}, 99 | "MOVE_COLOR_ROW": {"name": "MOVE_COLOR_ROW","cost": 4.47}, 100 | "MOVE_COLOR_COLUMN": {"name": "MOVE_COLOR_COLUMN","cost": 4.47}, 101 | "MOVE_COLOR_SIZE": {"name": "MOVE_COLOR_SIZE","cost": 4.43}, 102 | "MOVE_COLOR_SHAPE": {"name": "MOVE_COLOR_SHAPE","cost": 4.43}, 103 | "MOVE_COLOR_Y": {"name": "MOVE_COLOR_Y","cost": 4.46}, 104 | "MOVE_COLOR_X": {"name": "MOVE_COLOR_X","cost": 4.46}, 105 | "MOVE_COLOR_TEXT": {"name": "MOVE_COLOR_TEXT","cost": 4.43}, 106 | "MOVE_SHAPE_ROW": {"name": "MOVE_SHAPE_ROW","cost": 4.47}, 107 | "MOVE_SHAPE_COLUMN": {"name": "MOVE_SHAPE_COLUMN","cost": 4.47}, 108 | "MOVE_SHAPE_SIZE": {"name": "MOVE_SHAPE_SIZE","cost": 4.43}, 109 | "MOVE_SHAPE_COLOR": {"name": "MOVE_SHAPE_COLOR","cost": 4.43}, 110 | "MOVE_SHAPE_Y": {"name": "MOVE_SHAPE_Y","cost": 4.46}, 111 | "MOVE_SHAPE_X": {"name": "MOVE_SHAPE_X","cost": 4.46}, 112 | "MOVE_SHAPE_TEXT": {"name": "MOVE_SHAPE_TEXT","cost": 4.43}, 113 | "MOVE_SIZE_ROW": {"name": "MOVE_SIZE_ROW","cost": 4.47}, 114 | "MOVE_SIZE_COLUMN": {"name": "MOVE_SIZE_COLUMN","cost": 4.47}, 115 | "MOVE_SIZE_SHAPE": {"name": "MOVE_SIZE_SHAPE","cost": 4.43}, 116 | "MOVE_SIZE_COLOR": {"name": "MOVE_SIZE_COLOR","cost": 4.43}, 117 | "MOVE_SIZE_Y": {"name": "MOVE_SIZE_Y","cost": 4.46}, 118 | "MOVE_SIZE_X": {"name": "MOVE_SIZE_X","cost": 4.46}, 119 | "MOVE_SIZE_TEXT": {"name": "MOVE_SIZE_TEXT","cost": 4.43}, 120 | "MOVE_TEXT_ROW": {"name": "MOVE_TEXT_ROW","cost": 4.47}, 121 | "MOVE_TEXT_COLUMN": {"name": "MOVE_TEXT_COLUMN","cost": 4.47}, 122 | "MOVE_TEXT_SHAPE": {"name": "MOVE_TEXT_SHAPE","cost": 4.43}, 123 | "MOVE_TEXT_COLOR": {"name": "MOVE_TEXT_COLOR","cost": 4.43}, 124 | "MOVE_TEXT_Y": {"name": "MOVE_TEXT_Y","cost": 4.46}, 125 | "MOVE_TEXT_X": {"name": "MOVE_TEXT_X","cost": 4.46}, 126 | "MOVE_TEXT_SIZE": {"name": "MOVE_TEXT_SIZE","cost": 4.43}, 127 | "MOVE_COLUMN_ROW": {"name": "MOVE_COLUMN_ROW","cost": 4.44}, 128 | "MOVE_COLUMN_SIZE": {"name": "MOVE_COLUMN_SIZE","cost": 4.47}, 129 | "MOVE_COLUMN_SHAPE": {"name": "MOVE_COLUMN_SHAPE","cost": 4.47}, 130 | "MOVE_COLUMN_COLOR": {"name": "MOVE_COLUMN_COLOR","cost": 4.47}, 131 | "MOVE_COLUMN_Y": {"name": "MOVE_COLUMN_Y","cost": 4.45}, 132 | "MOVE_COLUMN_X": {"name": "MOVE_COLUMN_X","cost": 4.43}, 133 | "MOVE_COLUMN_TEXT": {"name": "MOVE_COLUMN_TEXT","cost": 4.47}, 134 | "MOVE_ROW_COLUMN": {"name": "MOVE_ROW_COLUMN","cost": 4.44}, 135 | "MOVE_ROW_SIZE": {"name": "MOVE_ROW_SIZE","cost": 4.47}, 136 | "MOVE_ROW_SHAPE": {"name": "MOVE_ROW_SHAPE","cost": 4.47}, 137 | "MOVE_ROW_COLOR": {"name": "MOVE_ROW_COLOR","cost": 4.47}, 138 | "MOVE_ROW_Y": {"name": "MOVE_ROW_Y","cost": 4.43}, 139 | "MOVE_ROW_X": {"name": "MOVE_ROW_X","cost": 4.45}, 140 | "MOVE_ROW_TEXT": {"name": "MOVE_ROW_TEXT","cost": 4.47}, 141 | "SWAP_X_Y": {"name": "SWAP_X_Y","cost": 4.42}, 142 | "SWAP_ROW_COLUMN": {"name": "SWAP_ROW_COLUMN","cost": 4.41}, 143 | "ceiling": {"cost": 47.1,"alternatingCost": 51.81} 144 | } 145 | } -------------------------------------------------------------------------------- /test/path/enumerate.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const expect = require('chai').expect; 3 | const EXAMPLES = require("../exampleLoader").examples; //TODO! 4 | const { enumerate, getMergedScale } = require("../../src/path/enumerate"); 5 | const {copy} = require("../../src/util"); 6 | const getTransition = require('../../src/transition/trans.js').transition 7 | 8 | 9 | describe("enumerate", () => { 10 | it("Should enumerate the keyframe sets of N keyframes for the given start and end specs.", async () => { 11 | const sSpec = { 12 | "mark": "bar", 13 | "encoding": {"x": {"field": "A", "type": "quantitative", "aggregate": "mean"}} 14 | } 15 | const eSpec = { 16 | "mark": "point", 17 | "transform": [{"filter": {"field": "A", "gt": 10}}], 18 | "encoding": {"x": {"field": "A", "type": "quantitative"}} 19 | } 20 | const transition = await getTransition(copy(sSpec), copy(eSpec)) 21 | 22 | const editOps = [ 23 | ...transition.mark, 24 | ...transition.transform, 25 | ...transition.encoding 26 | ]; 27 | 28 | let sequences = await enumerate(sSpec, eSpec, editOps, 2) 29 | 30 | expect(sequences.length).to.eq(6) 31 | }); 32 | 33 | it("Should enumerate 1 transition for give specs.", async () => { 34 | const sSpec = { 35 | "mark": "bar", 36 | "encoding": {"x": {"field": "A", "type": "quantitative", "aggregate": "mean"}} 37 | } 38 | const eSpec = { 39 | "mark": "point", 40 | "transform": [{"filter": {"field": "A", "gt": 10}}], 41 | "encoding": {"x": {"field": "A", "type": "quantitative"}} 42 | } 43 | const transition = await getTransition(copy(sSpec), copy(eSpec)) 44 | 45 | const editOps = [ 46 | ...transition.mark, 47 | ...transition.transform, 48 | ...transition.encoding 49 | ]; 50 | 51 | let sequences = await enumerate(sSpec, eSpec, editOps, 1) 52 | 53 | expect(sequences.length).to.eq(1) 54 | }); 55 | 56 | 57 | it("Should enumerate the keyframe with merged scales.", async () => { 58 | const {start, end} = EXAMPLES.filter_aggregate; 59 | const transition = await getTransition(copy(start), copy(end)) 60 | 61 | const editOps = [ 62 | ...transition.mark, 63 | ...transition.transform, 64 | ...transition.encoding 65 | ]; 66 | 67 | let sequences = await enumerate(start, end, editOps, 2) 68 | expect(sequences.length).to.eq(2) 69 | expect(sequences[0].sequence[1].encoding.x.scale.domain).to.deep.eq([0,100]) 70 | expect(sequences[1].sequence[1].encoding.x.scale.domain).to.deep.eq([0,100]) 71 | }); 72 | 73 | it("Should enumerate the keyframe with merged scales for temporal data.", async () => { 74 | const {sequence} = EXAMPLES.filter_and_filter; 75 | const transition = await getTransition(copy(sequence[0]), copy(sequence[1])) 76 | 77 | const editOps = [ 78 | ...transition.mark, 79 | ...transition.transform, 80 | ...transition.encoding 81 | ]; 82 | 83 | let sequences = await enumerate(sequence[0], sequence[1], editOps, 2) 84 | 85 | expect(sequences[0].sequence[1].encoding.x.scale.domain).to.deep.eq([ 1583107200000, 1613260800000 ]) 86 | // expect(sequences[1].sequence[1].encoding.x.scale.domain).to.deep.eq([0,100]) 87 | }); 88 | 89 | it("Should enumerate valid sequences.", async () => { 90 | const {start, end} = EXAMPLES.addY_aggregate_scale; 91 | const transition = await getTransition(copy(start), copy(end)) 92 | 93 | const editOps = [ 94 | ...transition.mark, 95 | ...transition.transform, 96 | ...transition.encoding 97 | ]; 98 | 99 | let sequences = await enumerate(start, end, editOps, 2) 100 | expect(sequences.length).to.eq(6) // applying only "SCALE" edit op should be ignored. 101 | 102 | }); 103 | 104 | it("Should only enumerate valid sequences having valid Vega-Lite specs.", async () => { 105 | const start = { 106 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 107 | "data": {"url": "data/penguins.json"}, 108 | "mark": "point", 109 | "encoding": { 110 | "x": { 111 | "field": "Flipper Length (mm)", 112 | "type": "quantitative" 113 | }, 114 | "y": { 115 | "field": "Body Mass (g)", 116 | "type": "quantitative" 117 | }, 118 | "color": {"field": "Species", "type": "nominal"} 119 | } 120 | }; 121 | const end = { 122 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 123 | "data": {"url": "data/penguins.json"}, 124 | "mark": "point", 125 | "encoding": { 126 | "x": { 127 | "field": "Flipper Length (mm)", 128 | "type": "quantitative", 129 | "bin": true 130 | }, 131 | "y": { 132 | "field": "Body Mass (g)", 133 | "type": "quantitative", 134 | "bin": true 135 | }, 136 | "size": {"field": "*", "type": "quantitative", "aggregate": "count"} 137 | } 138 | } 139 | const transition = await getTransition(copy(start), copy(end)) 140 | 141 | const editOps = [ 142 | ...transition.mark, 143 | ...transition.transform, 144 | ...transition.encoding 145 | ]; 146 | 147 | 148 | let sequences = await enumerate(start, end, editOps, 2) 149 | expect(sequences.length).to.eq(1) // applying only "SCALE" edit op should be ignored. 150 | 151 | }); 152 | 153 | it("Should enumerate proper paths having 3 edit ops for 2 stages.", async () => { 154 | const sSpec = { 155 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 156 | "data": {"url": "test/data/sample_kc_house_data.json"}, 157 | "mark": "point", 158 | "transform": [ 159 | {"calculate": "datum.price/datum.sqft_living", "as": "price_per_sqft"} 160 | ], 161 | "encoding": { 162 | "x": {"field": "price", "type": "quantitative"}, 163 | "y": { 164 | "field": "sqft_living", 165 | "type": "quantitative", 166 | "scale": {"zero": false}, 167 | "axis": {"labelFlush": true} 168 | }, 169 | "color": {"field": "bedrooms", "type": "nominal"} 170 | } 171 | } 172 | const eSpec = { 173 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 174 | "data": {"url": "test/data/sample_kc_house_data.json"}, 175 | "mark": "bar", 176 | "transform": [ 177 | {"calculate": "datum.price/datum.sqft_living", "as": "price_per_sqft"} 178 | ], 179 | "encoding": { 180 | "x": {"field": "bedrooms", "type": "nominal"}, 181 | "y": { 182 | "field": "sqft_living", 183 | "type": "quantitative", 184 | "scale": {"zero": false}, 185 | "aggregate": "mean" 186 | }, 187 | "color": {"field": "bedrooms", "type": "nominal"} 188 | } 189 | } 190 | const transition = await getTransition(copy(sSpec), copy(eSpec)) 191 | 192 | const editOps = [ 193 | ...transition.mark, 194 | ...transition.transform, 195 | ...transition.encoding 196 | ]; 197 | 198 | let {sequences, excludedPaths} = await enumerate(sSpec, eSpec, editOps, 2, true) 199 | 200 | expect(sequences.length).to.eq(4) 201 | 202 | expect(!!excludedPaths[0].editOpPartition[0].find(eo => eo.name === "AGGREGATE")).to.eq(true) 203 | expect(!!excludedPaths[0].editOpPartition[0].find(eo => eo.name === "MODIFY_X")).to.eq(false) 204 | 205 | expect(!!excludedPaths[1].editOpPartition[0].find(eo => eo.name === "AGGREGATE")).to.eq(true) 206 | expect(!!excludedPaths[1].editOpPartition[0].find(eo => eo.name === "MODIFY_X")).to.eq(false) 207 | 208 | }); 209 | 210 | 211 | }); 212 | 213 | 214 | describe("getMergedScale", () => { 215 | it("Should return the scale domains that covering the start and end visualizations.", async () => { 216 | const sSpec = { 217 | "mark": "bar", 218 | "data": {"values": [{"A": 10}, {"A": 20}]}, 219 | "encoding": { 220 | "x": {"field": "A", "type": "quantitative", "scale": {"zero": false}} 221 | } 222 | } 223 | const eSpec = { 224 | "mark": "bar", 225 | "data": {"values": [{"A": 10}, {"A": 20}]}, 226 | "encoding": { 227 | "x": {"field": "A", "type": "quantitative"} 228 | } 229 | } 230 | 231 | let newScaleDomains = await getMergedScale([sSpec, eSpec]) 232 | expect(newScaleDomains.x).to.deep.eq([0, 20]) 233 | }); 234 | 235 | it("Should return the scale domains that covering the start and end visualizations.", async () => { 236 | const sSpec = { 237 | "mark": "bar", 238 | "data": {"values": [{"A": 10}, {"A": 20}]}, 239 | "encoding": { 240 | "x": { 241 | "field": "A", 242 | "type": "nominal", 243 | "scale": {"domain": ["NY"]} 244 | } 245 | } 246 | } 247 | const eSpec = { 248 | "mark": "bar", 249 | "data": {"values": [{"A": 10}, {"A": 20}]}, 250 | "encoding": { 251 | "x": { 252 | "field": "A", 253 | "type": "nominal" 254 | } 255 | } 256 | } 257 | 258 | let newScaleDomains = await getMergedScale([sSpec, eSpec]) 259 | 260 | expect(newScaleDomains.x).to.deep.eq([ "NY", 10, 20]) 261 | }); 262 | 263 | 264 | }); 265 | 266 | -------------------------------------------------------------------------------- /test/path/evaluate.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const expect = require('chai').expect; 3 | const EXAMPLES = require("../exampleLoader").examples; //TODO! 4 | const { findRules, evaluate } = require("../../src/path/evaluate"); 5 | const { enumerate } = require("../../src/path/enumerate"); 6 | const {copy} = require("../../src/util"); 7 | const getTransition = require('../../src/transition/trans.js').transition 8 | 9 | describe("findRules", () => { 10 | it("Should find satisfying A-Then-B rules.", async () => { 11 | 12 | let found = findRules([ 13 | [{name: "AGGREGATE", type: "transform"}], 14 | [{name: "FILTER", type: "transform"}] 15 | ], [ 16 | { editOps: ["FILTER", "AGGREGATE"] } 17 | ]) 18 | expect(found.length).to.eq(0); 19 | 20 | 21 | let rule2 = [{ editOps: ["FILTER", "AGGREGATE"] }]; 22 | let found2 = findRules([ 23 | [{name: "FILTER", type: "transform"}], 24 | [{name: "AGGREGATE", type: "transform"}] 25 | ], rule2) 26 | expect(found2.map(r => r.editOps)).to.deep.eq(rule2.map(r => r.editOps)) 27 | 28 | let rule3 = [{ editOps: ["ENCODING", "TRANSFORM"] }]; 29 | let found3 = findRules([ 30 | [{name: "ADD_X", type: "encoding"}], 31 | [{name: "AGGREGATE", type: "transform"}] 32 | ], rule3) 33 | expect(found3.map(r => r.editOps)).to.deep.eq(rule3.map(r => r.editOps)) 34 | 35 | let rule4 = [ 36 | { editOps: ["ENCODING", "TRANSFORM"] }, 37 | { editOps: ["FILTER", "AGGREGATE"], condition: (filter, aggregate) => { 38 | return aggregate.detail && aggregate.detail.how === "added"; 39 | } } 40 | ]; 41 | 42 | let found4 = findRules([ 43 | [{name: "FILTER", type: "transform"}], 44 | [{name: "AGGREGATE", type: "transform", detail: {"how": "added"}}] 45 | ], rule4) 46 | expect(found4.map(r => r.editOps)).to.deep.eq(rule4.slice(1,2).map(r => r.editOps)) 47 | 48 | }) 49 | it("Should find satisfying A-With-B rules.", async () => { 50 | 51 | let found = findRules([ 52 | [{name: "FILTER", type: "transform"}], 53 | [{name: "AGGREGATE", type: "transform"}] 54 | ], [ 55 | { editOps: ["FILTER", "AGGREGATE"], type: "A-With-B" } 56 | ]) 57 | expect(found.length).to.eq(0); 58 | 59 | found = findRules( 60 | [[ 61 | {name: "FILTER", type: "transform"}, 62 | {name: "AGGREGATE", type: "transform"} 63 | ]], 64 | [{ editOps: ["FILTER", "AGGREGATE"], type: "A-With-B" }] 65 | ) 66 | expect(found.length).to.eq(1); 67 | 68 | found = findRules( 69 | [[ 70 | {name: "FILTER", type: "transform"}, 71 | {name: "MODIFY_X", type: "encoding"} 72 | ]], 73 | [{ editOps: ["FILTER", "ENCODING.MODIFY"], type: "A-With-B" }] 74 | ) 75 | expect(found.length).to.eq(1); 76 | }) 77 | }) 78 | 79 | describe("evaluate", () => { 80 | const sSpec = { 81 | "mark": "bar", 82 | "encoding": {"x": {"field": "A", "type": "quantitative", "aggregate": "mean"}} 83 | } 84 | const eSpec = { 85 | "mark": "point", 86 | "transform": [{"filter": {"field": "A", "gt": 10}}], 87 | "encoding": {"x": {"field": "A", "type": "quantitative"}} 88 | } 89 | 90 | 91 | it("should promote the one filtering after dis-aggregating", async () => { 92 | const transition = await getTransition(copy(sSpec), copy(eSpec)) 93 | 94 | const editOps = [ 95 | ...transition.mark, 96 | ...transition.transform, 97 | ...transition.encoding 98 | ]; 99 | 100 | let sequences = await enumerate(sSpec, eSpec, editOps, 3) 101 | sequences = sequences.map((seq) => { 102 | return { 103 | ...seq, 104 | eval: evaluate(seq.editOpPartition) 105 | } 106 | }).sort((a,b) => { return b.eval.score - a.eval.score}) 107 | 108 | expect((sequences[0].eval.satisfiedRules.length)).to.eq(1); 109 | expect((sequences[0].eval.satisfiedRules[0].name)).to.eq("disaggregate-then-filter") 110 | 111 | expect((sequences[2].eval.satisfiedRules[1].name)).to.eq("no-disaggregate-then-mark") 112 | }) 113 | 114 | 115 | it("should promote the one adding encoding before aggregating", async () => { 116 | const sSpec = { 117 | "mark": "point", 118 | "encoding": {"x": {"field": "A", "type": "quantitative"}} 119 | } 120 | const eSpec = { 121 | "mark": "point", 122 | "encoding": { 123 | "x": {"field": "A", "type": "quantitative", "aggregate": "mean"}, 124 | "y": {"field": "B", "type": "quantitative"} 125 | } 126 | } 127 | const transition = await getTransition(sSpec, eSpec) 128 | 129 | const editOps = [ 130 | ...transition.mark, 131 | ...transition.transform, 132 | ...transition.encoding 133 | ]; 134 | 135 | let sequences = await enumerate(sSpec, eSpec, editOps, 2) 136 | 137 | sequences = sequences.map((seq) => { 138 | return { 139 | ...seq, 140 | eval: evaluate(seq.editOpPartition) 141 | } 142 | }).sort((a,b) => { return b.eval.score - a.eval.score}) 143 | 144 | expect((sequences[0].eval.satisfiedRules.length)).to.eq(1); 145 | expect((sequences[0].eval.satisfiedRules[0].name)).to.eq("encoding(add)-then-aggregate") 146 | }) 147 | 148 | it("should promote the one filtering before binning", async () => { 149 | const sSpec = { 150 | "mark": "point", 151 | "encoding": {"x": {"field": "A", "type": "quantitative"}} 152 | } 153 | const eSpec = { 154 | "mark": "point", 155 | "transform": [{"filter": {"field": "A", "gt": 10}}], 156 | "encoding": { 157 | "x": {"field": "A", "type": "quantitative", "bin": true} 158 | } 159 | } 160 | const transition = await getTransition(sSpec, eSpec) 161 | 162 | const editOps = [ 163 | ...transition.mark, 164 | ...transition.transform, 165 | ...transition.encoding 166 | ]; 167 | 168 | let sequences = await enumerate(sSpec, eSpec, editOps, 2) 169 | 170 | sequences = sequences.map((seq) => { 171 | return { 172 | ...seq, 173 | eval: evaluate(seq.editOpPartition) 174 | } 175 | }).sort((a,b) => { return b.eval.score - a.eval.score}) 176 | 177 | expect((sequences[0].eval.satisfiedRules.length)).to.eq(1); 178 | expect((sequences[0].eval.satisfiedRules[0].name)).to.eq("filter-then-bin") 179 | }) 180 | 181 | it("Should promote the one modifying and scaling together.", async () => { 182 | const sSpec = { 183 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 184 | "data": {"url": "data/r2d3.json"}, 185 | "transform": [{"calculate": "datum.in_sf ? 'SF' : 'NY'", "as": "Location"}], 186 | "mark": {"type": "tick", "width": 100}, 187 | "encoding": { 188 | "color": { 189 | "field": "Location", 190 | "type": "nominal", 191 | "scale": {"domain": ["SF", "NY"]} 192 | }, 193 | "y": { 194 | "field": "elevation", 195 | "type": "quantitative", 196 | "axis": {"title": "Elevation (ft)"} 197 | }, 198 | "x": { 199 | "field": "Location", 200 | "type": "nominal", 201 | "scale": {"domain": ["SF", "NY"]} 202 | } 203 | }, 204 | "width": 200 205 | } 206 | const eSpec = { 207 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 208 | "data": {"url": "data/r2d3.json"}, 209 | "transform": [{"calculate": "datum.in_sf ? 'SF' : 'NY'", "as": "Location"}], 210 | "mark": "point", 211 | "encoding": { 212 | "color": { 213 | "field": "Location", 214 | "type": "nominal", 215 | "scale": {"domain": ["SF", "NY"]} 216 | }, 217 | "y": { 218 | "field": "elevation", 219 | "type": "quantitative", 220 | "axis": {"title": "Elevation (ft)"} 221 | }, 222 | "x": { 223 | "field": "price_per_sqft", 224 | "type": "quantitative", 225 | "axis": {"title": "Price / Sqft"} 226 | } 227 | } 228 | } 229 | const transition = await getTransition(copy(sSpec), copy(eSpec)) 230 | 231 | const editOps = [ 232 | ...transition.mark, 233 | ...transition.transform, 234 | ...transition.encoding 235 | ]; 236 | 237 | let {sequences, excludedPaths} = await enumerate(sSpec, eSpec, editOps, 2, true) 238 | 239 | sequences = sequences.map((seq) => { 240 | return { 241 | ...seq, 242 | eval: evaluate(seq.editOpPartition) 243 | } 244 | }).sort((a,b) => { return b.eval.score - a.eval.score}) 245 | 246 | 247 | expect(sequences[0].eval.score).to.eq(1) 248 | expect(sequences[0].eval.satisfiedRules.length).to.eq(1) 249 | expect(sequences[0].eval.satisfiedRules[0].name).to.eq('modifying-with-scale') 250 | 251 | }); 252 | 253 | it("Should demote the one applying 2+ filters.", async () => { 254 | const {sequence} = EXAMPLES.filter_and_filter; 255 | const sSpec = sequence[0], eSpec = sequence[1] 256 | const transition = await getTransition(copy(sSpec), copy(eSpec)) 257 | 258 | const editOps = [ 259 | ...transition.mark, 260 | ...transition.transform, 261 | ...transition.encoding 262 | ]; 263 | 264 | let {sequences, excludedPaths} = await enumerate(copy(sSpec), copy(eSpec), copy(editOps), 1, true) 265 | 266 | sequences = sequences.map((seq) => { 267 | return { 268 | ...seq, 269 | eval: evaluate(seq.editOpPartition) 270 | } 271 | }).sort((a,b) => { return b.eval.score - a.eval.score}) 272 | 273 | 274 | 275 | let seqs2 = (await enumerate(sSpec, eSpec, editOps, 2, true)).sequences 276 | .map((seq) => { 277 | return { ...seq, eval: evaluate(seq.editOpPartition) } 278 | }).sort((a,b) => { return b.eval.score - a.eval.score}) 279 | 280 | expect(seqs2[0].eval.score).to.eq(0); 281 | }) 282 | }) -------------------------------------------------------------------------------- /src/editOp/def.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.DEFAULT_MARK_EDIT_OP_LIST = [ 3 | "AREA_BAR", 4 | "AREA_LINE", 5 | "AREA_POINT", 6 | "AREA_TEXT", 7 | "AREA_TICK", 8 | "BAR_LINE", 9 | "BAR_POINT", 10 | "BAR_TEXT", 11 | "BAR_TICK", 12 | "LINE_POINT", 13 | "LINE_TEXT", 14 | "LINE_TICK", 15 | "POINT_TEXT", 16 | "POINT_TICK", 17 | "TEXT_TICK"]; 18 | exports.DEFAULT_TRANSFORM_EDIT_OP_LIST = [ 19 | "SCALE", 20 | "SORT", 21 | "BIN", 22 | "AGGREGATE", 23 | "SETTYPE", 24 | "ADD_FILTER", 25 | "REMOVE_FILTER", 26 | "MODIFY_FILTER" 27 | ]; 28 | exports.CHANNELS_WITH_TRANSITION_ORDER = [ 29 | "x", "y", "color", "shape", "size", "row", "column", "text" 30 | ]; 31 | exports.DEFAULT_EDIT_OPS = { 32 | "markEditOps": { 33 | "AREA_BAR": { "name": "AREA_BAR", "cost": 0.03 }, 34 | "AREA_LINE": { "name": "AREA_LINE", "cost": 0.02 }, 35 | "AREA_POINT": { "name": "AREA_POINT", "cost": 0.04 }, 36 | "AREA_TEXT": { "name": "AREA_TEXT", "cost": 0.08 }, 37 | "AREA_TICK": { "name": "AREA_TICK", "cost": 0.04 }, 38 | "BAR_LINE": { "name": "BAR_LINE", "cost": 0.04 }, 39 | "BAR_POINT": { "name": "BAR_POINT", "cost": 0.02 }, 40 | "BAR_TEXT": { "name": "BAR_TEXT", "cost": 0.06 }, 41 | "BAR_TICK": { "name": "BAR_TICK", "cost": 0.02 }, 42 | "LINE_POINT": { "name": "LINE_POINT", "cost": 0.03 }, 43 | "LINE_TEXT": { "name": "LINE_TEXT", "cost": 0.07 }, 44 | "LINE_TICK": { "name": "LINE_TICK", "cost": 0.03 }, 45 | "POINT_TEXT": { "name": "POINT_TEXT", "cost": 0.05 }, 46 | "POINT_TICK": { "name": "POINT_TICK", "cost": 0.01 }, 47 | "TEXT_TICK": { "name": "TEXT_TICK", "cost": 0.05 } 48 | }, 49 | "transformEditOps": { 50 | "SCALE": { "name": "SCALE", "cost": 0.6 }, 51 | "SORT": { "name": "SORT", "cost": 0.61 }, 52 | "BIN": { "name": "BIN", "cost": 0.62 }, 53 | "AGGREGATE": { "name": "AGGREGATE", "cost": 0.63 }, 54 | "ADD_FILTER": { "name": "ADD_FILTER", "cost": 0.65 }, 55 | "REMOVE_FILTER": { "name": "REMOVE_FILTER", "cost": 0.65 }, 56 | "MODIFY_FILTER": { "name": "MODIFY_FILTER", "cost": 0.64 } 57 | }, 58 | "encodingEditOps": { 59 | "ADD_X": { "name": "ADD_X", "cost": 4.59 }, 60 | "ADD_Y": { "name": "ADD_Y", "cost": 4.59 }, 61 | "ADD_COLOR": { "name": "ADD_COLOR", "cost": 4.55 }, 62 | "ADD_SHAPE": { "name": "ADD_SHAPE", "cost": 4.51 }, 63 | "ADD_SIZE": { "name": "ADD_SIZE", "cost": 4.53 }, 64 | "ADD_ROW": { "name": "ADD_ROW", "cost": 4.57 }, 65 | "ADD_COLUMN": { "name": "ADD_COLUMN", "cost": 4.57 }, 66 | "ADD_TEXT": { "name": "ADD_TEXT", "cost": 4.49 }, 67 | "ADD_X_COUNT": { "name": "ADD_X_COUNT", "cost": 4.58 }, 68 | "ADD_Y_COUNT": { "name": "ADD_Y_COUNT", "cost": 4.58 }, 69 | "ADD_COLOR_COUNT": { "name": "ADD_COLOR_COUNT", "cost": 4.54 }, 70 | "ADD_SHAPE_COUNT": { "name": "ADD_SHAPE_COUNT", "cost": 4.5 }, 71 | "ADD_SIZE_COUNT": { "name": "ADD_SIZE_COUNT", "cost": 4.52 }, 72 | "ADD_ROW_COUNT": { "name": "ADD_ROW_COUNT", "cost": 4.56 }, 73 | "ADD_COLUMN_COUNT": { "name": "ADD_COLUMN_COUNT", "cost": 4.56 }, 74 | "ADD_TEXT_COUNT": { "name": "ADD_TEXT_COUNT", "cost": 4.48 }, 75 | "REMOVE_X_COUNT": { "name": "REMOVE_X_COUNT", "cost": 4.58 }, 76 | "REMOVE_Y_COUNT": { "name": "REMOVE_Y_COUNT", "cost": 4.58 }, 77 | "REMOVE_COLOR_COUNT": { "name": "REMOVE_COLOR_COUNT", "cost": 4.54 }, 78 | "REMOVE_SHAPE_COUNT": { "name": "REMOVE_SHAPE_COUNT", "cost": 4.5 }, 79 | "REMOVE_SIZE_COUNT": { "name": "REMOVE_SIZE_COUNT", "cost": 4.52 }, 80 | "REMOVE_ROW_COUNT": { "name": "REMOVE_ROW_COUNT", "cost": 4.56 }, 81 | "REMOVE_COLUMN_COUNT": { "name": "REMOVE_COLUMN_COUNT", "cost": 4.56 }, 82 | "REMOVE_TEXT_COUNT": { "name": "REMOVE_TEXT_COUNT", "cost": 4.48 }, 83 | "REMOVE_X": { "name": "REMOVE_X", "cost": 4.59 }, 84 | "REMOVE_Y": { "name": "REMOVE_Y", "cost": 4.59 }, 85 | "REMOVE_COLOR": { "name": "REMOVE_COLOR", "cost": 4.55 }, 86 | "REMOVE_SHAPE": { "name": "REMOVE_SHAPE", "cost": 4.51 }, 87 | "REMOVE_SIZE": { "name": "REMOVE_SIZE", "cost": 4.53 }, 88 | "REMOVE_ROW": { "name": "REMOVE_ROW", "cost": 4.57 }, 89 | "REMOVE_COLUMN": { "name": "REMOVE_COLUMN", "cost": 4.57 }, 90 | "REMOVE_TEXT": { "name": "REMOVE_TEXT", "cost": 4.49 }, 91 | "MODIFY_X": { "name": "MODIFY_X", "cost": 4.71 }, 92 | "MODIFY_Y": { "name": "MODIFY_Y", "cost": 4.71 }, 93 | "MODIFY_COLOR": { "name": "MODIFY_COLOR", "cost": 4.67 }, 94 | "MODIFY_SHAPE": { "name": "MODIFY_SHAPE", "cost": 4.63 }, 95 | "MODIFY_SIZE": { "name": "MODIFY_SIZE", "cost": 4.65 }, 96 | "MODIFY_ROW": { "name": "MODIFY_ROW", "cost": 4.69 }, 97 | "MODIFY_COLUMN": { "name": "MODIFY_COLUMN", "cost": 4.69 }, 98 | "MODIFY_TEXT": { "name": "MODIFY_TEXT", "cost": 4.61 }, 99 | "MODIFY_X_ADD_COUNT": { "name": "MODIFY_X_ADD_COUNT", "cost": 4.7 }, 100 | "MODIFY_Y_ADD_COUNT": { "name": "MODIFY_Y_ADD_COUNT", "cost": 4.7 }, 101 | "MODIFY_COLOR_ADD_COUNT": { "name": "MODIFY_COLOR_ADD_COUNT", "cost": 4.66 }, 102 | "MODIFY_SHAPE_ADD_COUNT": { "name": "MODIFY_SHAPE_ADD_COUNT", "cost": 4.62 }, 103 | "MODIFY_SIZE_ADD_COUNT": { "name": "MODIFY_SIZE_ADD_COUNT", "cost": 4.64 }, 104 | "MODIFY_ROW_ADD_COUNT": { "name": "MODIFY_ROW_ADD_COUNT", "cost": 4.68 }, 105 | "MODIFY_COLUMN_ADD_COUNT": { "name": "MODIFY_COLUMN_ADD_COUNT", "cost": 4.68 }, 106 | "MODIFY_TEXT_ADD_COUNT": { "name": "MODIFY_TEXT_ADD_COUNT", "cost": 4.6 }, 107 | "MODIFY_X_REMOVE_COUNT": { "name": "MODIFY_X_REMOVE_COUNT", "cost": 4.7 }, 108 | "MODIFY_Y_REMOVE_COUNT": { "name": "MODIFY_Y_REMOVE_COUNT", "cost": 4.7 }, 109 | "MODIFY_COLOR_REMOVE_COUNT": { "name": "MODIFY_COLOR_REMOVE_COUNT", "cost": 4.66 }, 110 | "MODIFY_SHAPE_REMOVE_COUNT": { "name": "MODIFY_SHAPE_REMOVE_COUNT", "cost": 4.62 }, 111 | "MODIFY_SIZE_REMOVE_COUNT": { "name": "MODIFY_SIZE_REMOVE_COUNT", "cost": 4.64 }, 112 | "MODIFY_ROW_REMOVE_COUNT": { "name": "MODIFY_ROW_REMOVE_COUNT", "cost": 4.68 }, 113 | "MODIFY_COLUMN_REMOVE_COUNT": { "name": "MODIFY_COLUMN_REMOVE_COUNT", "cost": 4.68 }, 114 | "MODIFY_TEXT_REMOVE_COUNT": { "name": "MODIFY_TEXT_REMOVE_COUNT", "cost": 4.6 }, 115 | "MOVE_X_ROW": { "name": "MOVE_X_ROW", "cost": 4.45 }, 116 | "MOVE_X_COLUMN": { "name": "MOVE_X_COLUMN", "cost": 4.43 }, 117 | "MOVE_X_SIZE": { "name": "MOVE_X_SIZE", "cost": 4.46 }, 118 | "MOVE_X_SHAPE": { "name": "MOVE_X_SHAPE", "cost": 4.46 }, 119 | "MOVE_X_COLOR": { "name": "MOVE_X_COLOR", "cost": 4.46 }, 120 | "MOVE_X_Y": { "name": "MOVE_X_Y", "cost": 4.44 }, 121 | "MOVE_X_TEXT": { "name": "MOVE_X_TEXT", "cost": 4.46 }, 122 | "MOVE_Y_ROW": { "name": "MOVE_Y_ROW", "cost": 4.43 }, 123 | "MOVE_Y_COLUMN": { "name": "MOVE_Y_COLUMN", "cost": 4.45 }, 124 | "MOVE_Y_SIZE": { "name": "MOVE_Y_SIZE", "cost": 4.46 }, 125 | "MOVE_Y_SHAPE": { "name": "MOVE_Y_SHAPE", "cost": 4.46 }, 126 | "MOVE_Y_COLOR": { "name": "MOVE_Y_COLOR", "cost": 4.46 }, 127 | "MOVE_Y_X": { "name": "MOVE_Y_X", "cost": 4.44 }, 128 | "MOVE_Y_TEXT": { "name": "MOVE_Y_TEXT", "cost": 4.46 }, 129 | "MOVE_COLOR_ROW": { "name": "MOVE_COLOR_ROW", "cost": 4.47 }, 130 | "MOVE_COLOR_COLUMN": { "name": "MOVE_COLOR_COLUMN", "cost": 4.47 }, 131 | "MOVE_COLOR_SIZE": { "name": "MOVE_COLOR_SIZE", "cost": 4.43 }, 132 | "MOVE_COLOR_SHAPE": { "name": "MOVE_COLOR_SHAPE", "cost": 4.43 }, 133 | "MOVE_COLOR_Y": { "name": "MOVE_COLOR_Y", "cost": 4.46 }, 134 | "MOVE_COLOR_X": { "name": "MOVE_COLOR_X", "cost": 4.46 }, 135 | "MOVE_COLOR_TEXT": { "name": "MOVE_COLOR_TEXT", "cost": 4.43 }, 136 | "MOVE_SHAPE_ROW": { "name": "MOVE_SHAPE_ROW", "cost": 4.47 }, 137 | "MOVE_SHAPE_COLUMN": { "name": "MOVE_SHAPE_COLUMN", "cost": 4.47 }, 138 | "MOVE_SHAPE_SIZE": { "name": "MOVE_SHAPE_SIZE", "cost": 4.43 }, 139 | "MOVE_SHAPE_COLOR": { "name": "MOVE_SHAPE_COLOR", "cost": 4.43 }, 140 | "MOVE_SHAPE_Y": { "name": "MOVE_SHAPE_Y", "cost": 4.46 }, 141 | "MOVE_SHAPE_X": { "name": "MOVE_SHAPE_X", "cost": 4.46 }, 142 | "MOVE_SHAPE_TEXT": { "name": "MOVE_SHAPE_TEXT", "cost": 4.43 }, 143 | "MOVE_SIZE_ROW": { "name": "MOVE_SIZE_ROW", "cost": 4.47 }, 144 | "MOVE_SIZE_COLUMN": { "name": "MOVE_SIZE_COLUMN", "cost": 4.47 }, 145 | "MOVE_SIZE_SHAPE": { "name": "MOVE_SIZE_SHAPE", "cost": 4.43 }, 146 | "MOVE_SIZE_COLOR": { "name": "MOVE_SIZE_COLOR", "cost": 4.43 }, 147 | "MOVE_SIZE_Y": { "name": "MOVE_SIZE_Y", "cost": 4.46 }, 148 | "MOVE_SIZE_X": { "name": "MOVE_SIZE_X", "cost": 4.46 }, 149 | "MOVE_SIZE_TEXT": { "name": "MOVE_SIZE_TEXT", "cost": 4.43 }, 150 | "MOVE_TEXT_ROW": { "name": "MOVE_TEXT_ROW", "cost": 4.47 }, 151 | "MOVE_TEXT_COLUMN": { "name": "MOVE_TEXT_COLUMN", "cost": 4.47 }, 152 | "MOVE_TEXT_SHAPE": { "name": "MOVE_TEXT_SHAPE", "cost": 4.43 }, 153 | "MOVE_TEXT_COLOR": { "name": "MOVE_TEXT_COLOR", "cost": 4.43 }, 154 | "MOVE_TEXT_Y": { "name": "MOVE_TEXT_Y", "cost": 4.46 }, 155 | "MOVE_TEXT_X": { "name": "MOVE_TEXT_X", "cost": 4.46 }, 156 | "MOVE_TEXT_SIZE": { "name": "MOVE_TEXT_SIZE", "cost": 4.43 }, 157 | "MOVE_COLUMN_ROW": { "name": "MOVE_COLUMN_ROW", "cost": 4.44 }, 158 | "MOVE_COLUMN_SIZE": { "name": "MOVE_COLUMN_SIZE", "cost": 4.47 }, 159 | "MOVE_COLUMN_SHAPE": { "name": "MOVE_COLUMN_SHAPE", "cost": 4.47 }, 160 | "MOVE_COLUMN_COLOR": { "name": "MOVE_COLUMN_COLOR", "cost": 4.47 }, 161 | "MOVE_COLUMN_Y": { "name": "MOVE_COLUMN_Y", "cost": 4.45 }, 162 | "MOVE_COLUMN_X": { "name": "MOVE_COLUMN_X", "cost": 4.43 }, 163 | "MOVE_COLUMN_TEXT": { "name": "MOVE_COLUMN_TEXT", "cost": 4.47 }, 164 | "MOVE_ROW_COLUMN": { "name": "MOVE_ROW_COLUMN", "cost": 4.44 }, 165 | "MOVE_ROW_SIZE": { "name": "MOVE_ROW_SIZE", "cost": 4.47 }, 166 | "MOVE_ROW_SHAPE": { "name": "MOVE_ROW_SHAPE", "cost": 4.47 }, 167 | "MOVE_ROW_COLOR": { "name": "MOVE_ROW_COLOR", "cost": 4.47 }, 168 | "MOVE_ROW_Y": { "name": "MOVE_ROW_Y", "cost": 4.43 }, 169 | "MOVE_ROW_X": { "name": "MOVE_ROW_X", "cost": 4.45 }, 170 | "MOVE_ROW_TEXT": { "name": "MOVE_ROW_TEXT", "cost": 4.47 }, 171 | "SWAP_X_Y": { "name": "SWAP_X_Y", "cost": 4.42 }, 172 | "SWAP_ROW_COLUMN": { "name": "SWAP_ROW_COLUMN", "cost": 4.41 }, 173 | "ceiling": { "cost": 14.13, "alternatingCost": 18.84 } 174 | } 175 | }; 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphScape 2 | 3 | (Last Update: 2021-05-20) 4 | 5 | GraphScape([paper](http://idl.cs.washington.edu/papers/graphscape/)) is a directed graph model of the visualization design space that supports automated reasoning about visualization similarity and sequencing. It uses the [Vega-Lite](https://vega.github.io/vega-lite) language to model individual charts. This repository contains source code for building GraphScape models and automatically recommending sequences of charts. 6 | 7 | - [APIs](#apis) 8 | - [`.sequence`](#sequence) 9 | - [`.transition`](#transition) 10 | - [`.apply`](#apply) 11 | - [`.path`](#path) 12 | - [Sequence Recommender Web Application](#sequence-recommender-web-application) 13 | - [Development Instructions](#development-instructions) 14 | - [Cite Us!](#cite-us) 15 | 16 | ## APIs 17 | 18 | # 19 | graphscape.sequence(charts, options[, editOpSet, callback]) 20 | [<>](https://github.com/uwdata/graphscape/blob/master/src/sequence/sequence.js "Source") 21 | 22 | Generate recommended sequence orders for a collection of Vega-Lite *charts*. The return value is a ranked array of potential sequences and associated metadata. 23 | 24 | ### Input 25 | 26 | | Parameter | Type | Description | 27 | | :-------- |:-------------:| :------------- | 28 | | charts | Array | An array of [Vega-Lite](https://vega.github.io/vega-lite/) unit charts. | 29 | | options | Object | `{ "fixFirst": true / false }`
*fixFirst*: indicates whether the first chart in *charts* should be pinned as the first chart of the recommended sequence (`true`) or not (`false`).| 30 | | editOpSet | Object | (*Optional*) Specifies custom rules for calculating sequence costss | 31 | | callback | Function | (*Optional*) `function(result) { ... }`
A callback function to invoke with the results. | 32 | 33 | 34 | ### Output 35 | 36 | The output is a ranked array of objects, each containing a sequence ordering and related metadata. 37 | 38 | | Property | Type | Description | 39 | | :-------- |:-------------:| :------------- | 40 | | charts | Array | The given input charts.
If `options.fixFirst` was `false`, a *null specification* for an empty chart is included as the first entry. | 41 | | sequence | Array | Order of indexes of input charts. | 42 | | transitions | Array | Transitions between each pair of two adjacent charts with `id`. | 43 | | sequenceCost | Number| Final GraphScape sequence cost. | 44 | | sumOfTransitionCosts | Number | Sum of transition costs. | 45 | | patterns | Array | Observed patterns of the sequence.
Each pattern is consist of `pattern`, `appear`, `coverage`, and `patternScore`.
`pattern` : An array of transition `id`s composing the pattern.
`appear` : An array of indexes of `transitions` where the pattern appears in the sequence.
`coverage` : How much the pattern cover the sequence.
`patternScore` : Final pattern score, which is the same as coverage now. | 46 | | globalWeightingTerm | Number | Global weighting term. | 47 | | filterSequenceCost | Number | Filter sequence cost. | 48 | | filterSequenceCostReasons | Array | Sum of filter value change score
Increment of value : +1
Decrement of value : -1
Otherwise : 0| 49 | 50 | 51 | ### Sample Code (node.js) 52 | 53 | ```js 54 | const gs = require('./graphscape.js') 55 | const charts = []; // an array of Vega-Lite charts 56 | charts.push({ 57 | "data": {"url": "data/cars.json"}, 58 | "mark": "point", 59 | "encoding": { 60 | "x": {"field": "Horsepower","type": "quantitative"}, 61 | } 62 | }); 63 | charts.push({ 64 | "data": {"url": "data/cars.json"}, 65 | "mark": "point", 66 | "encoding": { 67 | "x": {"field": "Horsepower","type": "quantitative"}, 68 | "y": {"field": "Miles_per_Gallon","type": "quantitative"} 69 | } 70 | }); 71 | const options = { "fixFirst": false }; 72 | console.log(gs.sequence(charts, options)); 73 | ``` 74 | 75 | 76 | # 77 | graphscape.transition(source chart, target chart) 78 | [<>](https://github.com/uwdata/graphscape/blob/master/src/transition/trans.js "Source") 79 | 80 | Generate a transition from a source Vega-Lite chart to a target Vega-Lite chart. The transition has the minimum edit operation costs. 81 | 82 | ### Input 83 | 84 | | Parameter | Type | Description | 85 | | :-------- |:-------------:| :------------- | 86 | | source chart | Object | A [Vega-Lite](https://vega.github.io/vega-lite/) unit chart. | 87 | | target chart | Object | A [Vega-Lite](https://vega.github.io/vega-lite/) unit chart. | 88 | 89 | ### Output 90 | 91 | The output is a ranked array of objects, each containing a sequence ordering and related metadata. 92 | 93 | | Property | Type | Description | 94 | | :-------- |:-------------:| :------------- | 95 | | mark | Array | Edit operations in *mark* category. | 96 | | transform | Array | Edit operations in *transform* category. | 97 | | encoding | Array | Edit operations in *encoding* category. | 98 | | cost | Number | Sum of all costs of edit operations in this transition. | 99 | 100 | 101 | ### Sample Code (node.js) 102 | 103 | ```js 104 | const gs = require('./graphscape.js') 105 | const source = { 106 | "data": {"url": "data/cars.json"}, 107 | "mark": "point", 108 | "encoding": { 109 | "x": {"field": "Horsepower","type": "quantitative"}, 110 | } 111 | }; 112 | const target = { 113 | "data": {"url": "data/cars.json"}, 114 | "mark": "point", 115 | "encoding": { 116 | "x": {"field": "Horsepower","type": "quantitative"}, 117 | "y": {"field": "Miles_per_Gallon","type": "quantitative"} 118 | } 119 | }; 120 | 121 | console.log(gs.transition(source, target)); 122 | ``` 123 | 124 | # 125 | graphscape.apply(startChart, endChart, editOps) 126 | [<>](https://github.com/uwdata/graphscape/blob/master/src/transition/apply.js "Source") 127 | 128 | Applies edit operations on the start chart to synthesize the intermeidate chart between the start and end. The edit operations should be provided with the corresponding end chart. 129 | 130 | ### Input 131 | 132 | | Parameter | Type | Description | 133 | | :-------- |:-------------:| :------------- | 134 | | startChart | Vega-Lite Spec | The start chart that the edit operations are applied to. | 135 | | editOps | Array | Edit operations between the start and end charts. Users can get these by `.transition`.| 136 | | endChart | Vega-Lite Spec | The end chart. The given edit operations should be extracted from the transition between the start and end. | 137 | 138 | 139 | ### Output 140 | 141 | A Vega-Lite Spec. 142 | 143 | ### Sample Code (node.js) 144 | 145 | ```js 146 | const gs = require('./graphscape.js') 147 | const startVL = { 148 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 149 | "data": {"url": "data/penguins.json"}, 150 | "mark": "point", 151 | "encoding": {"x": {"field": "A", "type": "nominal"}} 152 | }; 153 | const endVL = { 154 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 155 | "data": {"url": "data/penguins.json"}, 156 | "mark": "point", 157 | "encoding": { 158 | "y": {"field": "A", "type": "quantitative", "aggregate": "mean" } 159 | } 160 | }; 161 | const transition = await gs.transition(startVL, endVL); 162 | const chart = gs.apply(startVL, endVL, transition.encoding) 163 | console.log(chart); 164 | /* 165 | { 166 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 167 | "data": {"url": "data/penguins.json"}, 168 | "mark": "point", 169 | "encoding": {"y": {"field": "A", "type": "quantitative"}} 170 | } 171 | */ 172 | ``` 173 | 174 | # 175 | graphscape.path(startChart, endChart, M) 176 | [<>](https://github.com/uwdata/graphscape/blob/master/src/path/ "Source") 177 | 178 | Recommends paths (chart sequences) from the start to the end with M transitions. Each path will have M+1 charts. Unlike `.sequence`, it generates intermediate charts for given two ends. 179 | 180 | ### Input 181 | 182 | | Parameter | Type | Description | 183 | | :-------- |:-------------:| :------------- | 184 | | startChart | Vega-Lite Spec | The start chart for paths. | 185 | | endChart | Vega-Lite Spec | The end chart for paths. | 186 | | M | Inteager | The number of transitions for the paths. If M is undefined, it returns all possible paths. | 187 | 188 | 189 | ### Output 190 | 191 | If M is specified, it returns a path array (`Array`). If not, it returns object having possible Ms and corresponding paths as keys and values(`{ "1": Array, "2": ..., ...}`) 192 | 193 | Each path has these properties: 194 | ```js 195 | { 196 | "sequence": [startChart, ..., endChart ], 197 | // The partition of the edit operations from the start and the end. 198 | "editOpPartition": [editOpArray1, ..., editOpArrayM], 199 | 200 | "eval": { 201 | // GraphScape's heuristic evaluation score for this path. Higher means better. 202 | "score": 1, //Number 203 | "satisfiedRules": ... // The reasons for the scores. 204 | } 205 | } 206 | ``` 207 | 208 | ### Sample Code (node.js) 209 | 210 | ```js 211 | const gs = require('./graphscape.js') 212 | const startVL = { 213 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 214 | "data": {"url": "data/penguins.json"}, 215 | "mark": "point", 216 | "encoding": {"x": {"field": "A", "type": "nominal"}} 217 | }; 218 | const endVL = { 219 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 220 | "data": {"url": "data/penguins.json"}, 221 | "mark": "point", 222 | "encoding": { 223 | "y": {"field": "A", "type": "quantitative", "aggregate": "mean" } 224 | } 225 | }; 226 | const paths = await gs.path(startVL, endVL); 227 | console.log(paths) 228 | ``` 229 | 230 | *More details of the implementation will be available in here(TBD). 231 | 232 | ## Sequence Recommender Web Application 233 | 234 | The `app/` folder contains a *sequence recommender* web application. Given a set of input [Vega-Lite](https://vega.github.io/vega-lite/) specifications, it produces a recommended sequence intended to improve chart reading and comprehension. To run this app, first you should install bower components: 235 | ```console 236 | $ cd app 237 | $ bower install 238 | ``` 239 | Next, launch a local webserver to run the application. For example: 240 | 241 | ```console 242 | $ python -m SimpleHTTPServer 9000 # for Python 2 243 | $ python -m http.server 9000 # for Python 3 244 | ``` 245 | 246 | To use a custom build of `graphscape.js`, copy your new `graphscape.js` file and paste it into the `app/js` folder. 247 | 248 | ## Development Instructions 249 | 250 | 1. MATLAB is required to solve `lp.m`. 251 | 2. Install npm dependencies via `npm install`. 252 | 3. You can customize rankings of edit operations by modifying `lp.js` and running the following commands: 253 | 254 | ```console 255 | $ cd src/rule 256 | $ node lp.js 257 | $ matlab < lp.m 258 | $ node genEditOpSet.js # This will generate editOpSet.js. 259 | 260 | # After creating your rankings, you must re-build `graphscape.js` to apply changes. 261 | $ cd 262 | $ npm run test 263 | $ npm run build 264 | ``` 265 | 266 | ## Cite us! 267 | 268 | If you use GraphScpae in published research, please cite [this paper](http://idl.cs.washington.edu/papers/graphscape/). 269 | -------------------------------------------------------------------------------- /test/transition/apply.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const expect = require('chai').expect; 3 | const editOpSet = require('../editOpSetForTest'); 4 | const trans = require('../../src/transition/trans'); 5 | const {apply, applyEncodingEditOp, applyTransformEditOp, applyMarkEditOp } = require('../../src/transition/apply'); 6 | const util = require('../../src/util'); 7 | var startVL = { 8 | "data": { "url": "/data/cars.json" }, 9 | "mark": "area", 10 | "transform": [{ "filter": "datum.Year > 1970 " }], 11 | "encoding": { 12 | "x": { "type": "temporal", "field": "Year", "timeUnit": "year" }, 13 | "y": { "type": "quantitative", 14 | "field": "*", 15 | "aggregate": "count" 16 | }, 17 | "color": { "type": "nominal", "field": "Origin" } 18 | } 19 | }; 20 | var destinationVL = { 21 | "data": { "url": "/data/cars.json" }, 22 | "mark": "point", 23 | "encoding": { 24 | "x": { "type": "quantitative", "field": "Horsepower", "scale": { "type": "log" } }, 25 | "y": { 26 | "type": "quantitative", 27 | "field": "Acceleration", 28 | "scale": { "type": "log" } 29 | }, 30 | "color": { "type": "ordinal", "field": "Origin" } 31 | } 32 | }; 33 | describe('transition.apply', function () { 34 | describe('applyMarkEditOp', function () { 35 | it('should apply a mark edit operation correctly.', function () { 36 | const markEditOp = trans.markEditOps(startVL, destinationVL, editOpSet.DEFAULT_EDIT_OPS["markEditOps"])[0]; 37 | const iSpec = applyMarkEditOp(startVL, destinationVL, markEditOp) 38 | 39 | expect(iSpec.mark).to.eq(destinationVL.mark); 40 | 41 | }); 42 | }); 43 | 44 | describe('applyTransformEditOp', function () { 45 | it('should apply SCALE and AGGREGATE edit operations correctly.', async function () { 46 | const scaleEditOp = await trans.scaleEditOps(startVL, destinationVL, "y", editOpSet.DEFAULT_EDIT_OPS["transformEditOps"].SCALE); 47 | const aggEditOp = trans.transformBasic(startVL, destinationVL, "y", "AGGREGATE", editOpSet.DEFAULT_EDIT_OPS["transformEditOps"]); 48 | 49 | let iSpec = applyTransformEditOp(startVL, destinationVL, scaleEditOp) 50 | expect(iSpec.encoding.y.scale).to.eq(destinationVL.encoding.y.scale); 51 | 52 | iSpec = applyTransformEditOp(iSpec, destinationVL, aggEditOp) 53 | expect(iSpec.encoding.y.aggregate).to.eq(destinationVL.encoding.y.aggregate); 54 | 55 | }); 56 | 57 | it('should apply ADD_FILTER/REMOVE_FILTER/MODIFY_FILTER edit operations correctly.', function () { 58 | const filterEditOps = { 59 | "MODIFY_FILTER": editOpSet.DEFAULT_EDIT_OPS["transformEditOps"]["MODIFY_FILTER"], 60 | "ADD_FILTER": editOpSet.DEFAULT_EDIT_OPS["transformEditOps"]["ADD_FILTER"], 61 | "REMOVE_FILTER": editOpSet.DEFAULT_EDIT_OPS["transformEditOps"]["REMOVE_FILTER"] 62 | }; 63 | let _startVL = { 64 | "transform": [ 65 | {"filter": {"not": {"field": "x", "oneOf": [1,2]}}}, 66 | { 67 | "filter": { 68 | "and": [ 69 | {"field": "y", "oneOf": [1,2]}, 70 | {"field": "y", "oneOf": [3,2]} 71 | ] 72 | } 73 | } 74 | ] 75 | 76 | } 77 | let _endVL = { 78 | "transform": [ 79 | {"filter": {"field": "z", "oneOf": [1,2]}}, 80 | {"filter": {"not": {"field": "x", "oneOf": [1,22]}}} 81 | ] 82 | } 83 | const editOps = trans.filterEditOps(_startVL, _endVL, filterEditOps); 84 | 85 | expect(editOps.length).to.eq(3); 86 | let addFilterOp = editOps.find(eo => eo.name === "ADD_FILTER") 87 | let iSpec = applyTransformEditOp(_startVL, _endVL, addFilterOp) 88 | expect(iSpec.transform.length).to.eq(3); 89 | let removeFilterOp = editOps.find(eo => eo.name === "REMOVE_FILTER") 90 | iSpec = applyTransformEditOp(iSpec, _endVL, removeFilterOp) 91 | expect(iSpec.transform.find(trsfm=> trsfm.filter.and)).to.eq(undefined); 92 | 93 | let modifyFilterOp = editOps.find(eo => eo.name === "MODIFY_FILTER") 94 | iSpec = applyTransformEditOp(iSpec, _endVL, modifyFilterOp) 95 | 96 | expect(iSpec.transform.find(trsfm=> trsfm.filter.not).filter.not.oneOf[1]).to.eq(22); 97 | 98 | 99 | }); 100 | 101 | it('should apply SORT edit operations correctly.', function () { 102 | const sortEditOp = { 103 | "SORT": editOpSet.DEFAULT_EDIT_OPS["transformEditOps"]["SORT"] 104 | }; 105 | let _startVL = { 106 | "encoding": { 107 | "x": {"field": "A", "type": "Q"}, 108 | "y": {"field": "B", "type": "N", "sort": {"field": "A", "order": "ascending"}} 109 | } 110 | } 111 | let _endVL = { 112 | "encoding": { 113 | "x": {"field": "A", "type": "Q"}, 114 | "y": {"field": "B", "type": "N"} 115 | } 116 | } 117 | const editOp = trans.transformBasic(_startVL, _endVL, "y", "SORT", sortEditOp); 118 | 119 | expect(editOp.name).to.eq("SORT"); 120 | expect(editOp.detail).to.deep.eq({how: "removed", channel: "y"}); 121 | 122 | let iSpec = applyTransformEditOp(_startVL, _endVL, editOp) 123 | expect(iSpec.encoding.y).to.deep.eq(_endVL.encoding.y); 124 | 125 | }); 126 | }); 127 | 128 | 129 | describe('applyEncodingEditOp', function () { 130 | it('should apply a REMOVE_Channel edit operation correctly.', function () { 131 | let startVL = {"encoding": {"x": {"field": "A", "type": "nominal"}}}; 132 | const removeEditOp = trans.encodingEditOps( 133 | util.duplicate(startVL), 134 | {"encoding": {}}, 135 | editOpSet.DEFAULT_EDIT_OPS["encodingEditOps"])[0]; 136 | 137 | let iSpec = applyEncodingEditOp(startVL, destinationVL, removeEditOp) 138 | 139 | expect(JSON.stringify(iSpec.encoding)).to.eq("{}") 140 | expect(() => { 141 | applyEncodingEditOp(iSpec, destinationVL, removeEditOp); 142 | }).to.throw(); 143 | }); 144 | 145 | it('should apply a ADD edit operation correctly.', function () { 146 | let startVL = {"encoding": {}}; 147 | let endVL = {"encoding": {"x": {"field": "A", "type": "nominal"}}} 148 | const addEditoOp = trans.encodingEditOps( 149 | util.duplicate(startVL), 150 | util.duplicate(endVL), 151 | editOpSet.DEFAULT_EDIT_OPS["encodingEditOps"])[0]; 152 | 153 | let iSpec = applyEncodingEditOp(startVL, endVL, addEditoOp) 154 | 155 | expect(iSpec.encoding.x.field).to.eq("A") 156 | expect(() => { 157 | applyEncodingEditOp(iSpec, endVL, addEditoOp); 158 | }).to.throw(); 159 | }); 160 | 161 | it('should apply a MOVE edit operation correctly.', function () { 162 | let startVL = {"encoding": {"y": {"field": "A", "type": "nominal"}}}; 163 | let endVL = {"encoding": {"x": {"field": "A", "type": "nominal"}}} 164 | const moveEditoOp = trans.encodingEditOps( 165 | util.duplicate(startVL), 166 | util.duplicate(endVL), 167 | editOpSet.DEFAULT_EDIT_OPS["encodingEditOps"])[0]; 168 | 169 | let iSpec = applyEncodingEditOp(startVL, endVL, moveEditoOp) 170 | 171 | expect(iSpec.encoding.x.field).to.eq("A") 172 | expect(iSpec.encoding.y).to.eq(undefined) 173 | expect(() => { 174 | applyEncodingEditOp(iSpec, endVL, moveEditoOp); 175 | }).to.throw(); 176 | }); 177 | 178 | it('should apply a MODIFY edit operation correctly.', function () { 179 | let startVL = {"encoding": {"y": {"field": "A", "type": "nominal"}}}; 180 | let endVL = {"encoding": {"y": {"field": "B", "type": "nominal", "aggregate": "mean"}}} 181 | const modifyEditOp = trans.encodingEditOps( 182 | util.duplicate(startVL), 183 | util.duplicate(endVL), 184 | editOpSet.DEFAULT_EDIT_OPS["encodingEditOps"])[0]; 185 | 186 | let iSpec = applyEncodingEditOp(startVL, endVL, modifyEditOp) 187 | 188 | expect(iSpec.encoding.y.field).to.eq("B") 189 | expect(iSpec.encoding.y.aggregate).to.eq(undefined) 190 | }); 191 | 192 | it('should apply a SWAP edit operation correctly.', function () { 193 | let startVL = { 194 | "encoding": { 195 | "x": {"field": "A", "type": "nominal"}, 196 | "y": {"field": "B", "type": "nominal", "aggregate": "mean"} 197 | } 198 | }; 199 | let endVL = { 200 | "encoding": { 201 | "y": {"field": "A", "type": "nominal"}, 202 | "x": {"field": "B", "type": "nominal", "aggregate": "mean"} 203 | } 204 | } 205 | const swapEditOp = trans.encodingEditOps( 206 | util.duplicate(startVL), 207 | util.duplicate(endVL), 208 | editOpSet.DEFAULT_EDIT_OPS["encodingEditOps"])[0]; 209 | 210 | let iSpec = applyEncodingEditOp(startVL, endVL, swapEditOp) 211 | 212 | expect(iSpec.encoding.x.field).to.eq("B") 213 | expect(iSpec.encoding.y.aggregate).to.eq(undefined) 214 | }); 215 | 216 | }); 217 | describe('apply', function () { 218 | it('should apply a REMOVE_Channel edit operation correctly.', async function () { 219 | let startVL = { 220 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 221 | "data": {"url": "data/penguins.json"}, 222 | "mark": "point", 223 | "encoding": {"x": {"field": "A", "type": "nominal"}} 224 | }; 225 | let endVL = { 226 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 227 | "data": {"url": "data/penguins.json"}, 228 | "mark": "point", 229 | "encoding": { 230 | "y": {"field": "A", "type": "quantitative", "aggregate": "mean" } 231 | } 232 | }; 233 | const editOps = await trans.transition( 234 | util.duplicate(startVL), 235 | util.duplicate(endVL), 236 | editOpSet.DEFAULT_EDIT_OPS 237 | ); 238 | 239 | expect(() => { 240 | apply(startVL, endVL, editOps.transform); 241 | }).to.throw(); 242 | 243 | let iSpec = apply(startVL, endVL, editOps.encoding) 244 | expect(iSpec).to.deep.equal({ 245 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 246 | "data": {"url": "data/penguins.json"}, 247 | "mark": "point", 248 | "encoding": {"y": {"field": "A", "type": "quantitative"}} 249 | }) 250 | 251 | 252 | expect(apply(startVL, endVL, editOps.encoding.concat(editOps.transform))) 253 | .to.deep.equal(endVL) 254 | 255 | }); 256 | 257 | it('should return an error if the resulted spec is invalid.', async function () { 258 | let startVL = { 259 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 260 | "data": {"url": "data/penguins.json"}, 261 | "mark": "point", 262 | "encoding": { 263 | "x": { "field": "A", "type": "quantitative"}, 264 | "color": {"field": "Species", "type": "nominal"} 265 | } 266 | }; 267 | let endVL = { 268 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 269 | "data": {"url": "data/penguins.json"}, 270 | "mark": "point", 271 | "encoding": { 272 | "x": { "field": "A", "type": "quantitative", "bin": true }, 273 | "size": {"field": "*", "type": "nominal", "aggregate": "count"} 274 | } 275 | }; 276 | const editOps = await trans.transition( 277 | util.duplicate(startVL), 278 | util.duplicate(endVL), 279 | editOpSet.DEFAULT_EDIT_OPS 280 | ); 281 | 282 | const eos = [...editOps.encoding, editOps.transform.find(eo => eo.name === 'AGGREGATE')]; 283 | expect(() => { 284 | apply(util.duplicate(startVL), endVL, eos); 285 | }).to.throw(`The resulted spec is not valid Vega-Lite Spec.`); 286 | 287 | const eos2 = [...editOps.encoding]; 288 | 289 | expect(() => { 290 | apply(util.duplicate(startVL), endVL, eos2); 291 | }).to.throw(`_COUNT encoding edit operations cannot be applied without AGGREGATE.`); 292 | 293 | }); 294 | }); 295 | 296 | }); --------------------------------------------------------------------------------