├── src ├── index.js ├── sets.js ├── setcola.js └── constraints.js ├── images └── smalltree.png ├── rollup.config.js ├── package.json ├── LICENSE ├── yarn.lock ├── README.md └── dist └── setcola.js /src/index.js: -------------------------------------------------------------------------------- 1 | export * from "./setcola"; -------------------------------------------------------------------------------- /images/smalltree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwdata/setcola/HEAD/images/smalltree.png -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | 3 | export default { 4 | input: "src/index.js", 5 | output: { 6 | file: "dist/setcola.js", 7 | format: "umd", 8 | name: "setcola" 9 | }, 10 | plugins: [ 11 | resolve() 12 | ] 13 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SetCoLa", 3 | "version": "1.0.0", 4 | "description": "SetCoLa is a domain specific language for specifying graph layout constraints. The SetCoLa compiler generates constraints for WebCoLa.", 5 | "main": "index.js", 6 | "repository": "https://github.com/uwdata/setcola.git", 7 | "author": "Jane Hoffswell , Alan Borning, and Jeffrey Heer", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "rollup": "^0.58.2", 11 | "rollup-plugin-node-resolve": "^3.3.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 UW Interactive Data Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/estree@0.0.38": 6 | version "0.0.38" 7 | resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.38.tgz#c1be40aa933723c608820a99a373a16d215a1ca2" 8 | 9 | "@types/node@*": 10 | version "10.0.3" 11 | resolved "https://registry.yarnpkg.com/@types/node/-/node-10.0.3.tgz#1f89840c7aac2406cc43a2ecad98fc02a8e130e4" 12 | 13 | builtin-modules@^2.0.0: 14 | version "2.0.0" 15 | resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e" 16 | 17 | is-module@^1.0.0: 18 | version "1.0.0" 19 | resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" 20 | 21 | path-parse@^1.0.5: 22 | version "1.0.5" 23 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" 24 | 25 | resolve@^1.1.6: 26 | version "1.7.1" 27 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" 28 | dependencies: 29 | path-parse "^1.0.5" 30 | 31 | rollup-plugin-node-resolve@^3.3.0: 32 | version "3.3.0" 33 | resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.3.0.tgz#c26d110a36812cbefa7ce117cadcd3439aa1c713" 34 | dependencies: 35 | builtin-modules "^2.0.0" 36 | is-module "^1.0.0" 37 | resolve "^1.1.6" 38 | 39 | rollup@^0.58.2: 40 | version "0.58.2" 41 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.58.2.tgz#2feddea8c0c022f3e74b35c48e3c21b3433803ce" 42 | dependencies: 43 | "@types/estree" "0.0.38" 44 | "@types/node" "*" 45 | -------------------------------------------------------------------------------- /src/sets.js: -------------------------------------------------------------------------------- 1 | var _sets; 2 | 3 | export function computeSets(elements, definition, sets, index) { 4 | _sets = sets; 5 | var set = []; 6 | if(!definition) { 7 | set = [elements]; 8 | } else if(definition.partition) { 9 | set = partitionSet(elements, definition); 10 | } else if(definition.collect) { 11 | set = collectSet(elements, definition); 12 | } else if(definition.expr) { 13 | set = exprSet(elements, definition, index); 14 | if(definition.name) set._setName = definition.name; 15 | } else if(typeof(definition) === 'string') { 16 | set = existingSet(elements, definition); 17 | set._setName = definition; 18 | } else { 19 | definition.forEach(function(subdef, index) { 20 | set.push(computeSets(elements, subdef, _sets, index)); 21 | }); 22 | } 23 | return set; 24 | }; 25 | 26 | function contains(list, value) { 27 | return list.indexOf(value) !== -1; 28 | }; 29 | 30 | function partitionSet(elements, definition) { 31 | var partitionSets = {}; 32 | 33 | // Split the elements into sets based on their partition property. 34 | elements.forEach(function(element) { 35 | var partitionValue = element[definition.partition]; 36 | if(definition.partition === 'parent' && partitionValue) { 37 | partitionValue = partitionValue._id; 38 | } 39 | if(definition.exclude && contains(definition.exclude, partitionValue)) return; 40 | if(definition.include && !contains(definition.include, partitionValue)) return; 41 | if(!partitionSets[partitionValue]) partitionSets[partitionValue] = []; 42 | partitionSets[partitionValue].push(element); 43 | }); 44 | 45 | // Lift the partition property to a property of the set. 46 | Object.keys(partitionSets).forEach(function(setName) { 47 | partitionSets[setName][definition.partition] = partitionSets[setName][0][definition.partition]; 48 | }); 49 | 50 | return Object.keys(partitionSets).map(function(setName) { 51 | partitionSets[setName]._setName = setName; 52 | return partitionSets[setName]; 53 | }); 54 | }; 55 | 56 | function collectSet(elements, definition) { 57 | var collectSets = {}; 58 | elements.forEach(function(element) { 59 | var set = []; 60 | definition.collect.forEach(function(expr) { 61 | switch(expr) { 62 | case 'node': 63 | set.push(element); 64 | break; 65 | case 'node.firstchild': 66 | if(element.firstchild) set = set.concat(element.firstchild); 67 | break; 68 | case 'node.sources': 69 | set = set.concat(element.getSources()); 70 | break; 71 | case 'node.targets': 72 | set = set.concat(element.getTargets()); 73 | break; 74 | case 'node.neighbors': 75 | set = set.concat(element.getNeighbors()); 76 | break; 77 | default: 78 | if(expr.indexOf('sort') !== -1) { 79 | 80 | var children = element.getTargets(); 81 | var map = children.map(function(el) { return el.value; }); 82 | var sorted = map.sort(); 83 | var first = children.filter(function(el) { 84 | return el.value === sorted[0]; 85 | }); 86 | if(first[0]) set = set.concat(first[0]); 87 | 88 | } else if(expr.indexOf('min') !== -1) { 89 | 90 | var source = expr.split(/\(|,|\)/g)[2]; 91 | var property = expr.split(/\(|,|\)/g)[1].replace(/'/g, ''); 92 | 93 | var node; 94 | switch(source) { 95 | case 'node.children': 96 | var children = element.getTargets(); 97 | var minimum = Math.min.apply(null, children.map(function(n) { return n[property]; })); 98 | node = children.filter(function(n) { return n[property] === minimum; })[0]; 99 | if(!element[property]) { 100 | // Do nothing.... 101 | } else if(node && node[property] < element[property]) { 102 | node = null; 103 | } 104 | break; 105 | case 'node.neighbors': 106 | break; 107 | case 'node.parents': 108 | break; 109 | default: 110 | // Do nothing 111 | } 112 | if(node) { 113 | set = set.concat(node); 114 | } 115 | 116 | } else { 117 | console.error('Unknown collection parameter \'' + expr + '\''); 118 | } 119 | } 120 | }); 121 | if(set.length > 1) collectSets[element._id] = set; 122 | }); 123 | return Object.keys(collectSets).map(function(setName) { return collectSets[setName]; }); 124 | }; 125 | 126 | function exprSet(elements, definition, index) { 127 | var set = []; 128 | elements.forEach(function(element) { 129 | var matches = definition.expr.match(/node\.[a-zA-Z.0-9]+/g); 130 | var expr = definition.expr; 131 | matches.forEach(function(match) { 132 | var props = match.replace('node.', '').split('.'); 133 | var result; 134 | for (var i = 0; i < props.length; i++) { 135 | result = element[props[i]]; 136 | } 137 | expr = expr.replace(match, JSON.stringify(result)); 138 | }); 139 | if(eval(expr)) set.push(element); 140 | }); 141 | set._exprIndex = index; 142 | return set; 143 | }; 144 | 145 | function existingSet(elements, definition) { 146 | return _sets[definition]; 147 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SetCoLa 2 | SetCoLa is a domain-specific language for specifying high-level graph layout constraints relative to properties of the underlying graph. This repository contains a SetCoLa compiler that generates constraints for [WebCoLa](http://ialab.it.monash.edu/webcola/) and includes an [online graph editor](https://uwdata.github.io/setcola/). 3 | 4 | ## Citation 5 | If you are interested in this work, please see [our EuroVis 2018 research paper](http://idl.cs.washington.edu/papers/setcola/) and consider citing our work: 6 | 7 | ``` 8 | @inproceedings{2016-setcola, 9 | title = {SetCoLa: High-Level Constraints for Graph Layout}, 10 | author = {Jane Hoffswell AND Alan Borning AND Jeffrey Heer}, 11 | booktitle = {Computer Graphics Forum (Proc. EuroVis)}, 12 | year = {2018}, 13 | url = {http://idl.cs.washington.edu/files/2018-SetCoLa-EuroVis.pdf}, 14 | } 15 | ``` 16 | 17 | ## Language 18 | A SetCoLa specification is defined by multiple constraint definitions that specify the desired behavior for the graph layout and may include a set of "guides" (reference elements that serve as positional anchors), which is a list of nodes that include an "x" and/or "y" property. Each **constraint definition** includes a **set definition** and **constraint application**, which may apply one or more constraints to the element of each set. 19 | 20 | ### Set Definition 21 | There are four ways to define sets in SetCoLa: *partitioning nodes into sets*, *specifying sets with predicates*, *collecting nodes using keys*, and *composing previously defined sets*. 22 | 23 | #### Partitioning Nodes into Sets 24 | This strategy partitions all the nodes into disjoint sets based on the value of the `partitionProperty` of the node. You may also specify a list of values to `include` or `exclude`, which checks for those values explicitly when completing the partition. 25 | 26 | ```json 27 | {"partition": "partitionProperty", "include": [...], "exclude": [...]} 28 | ``` 29 | 30 | #### Specifying Sets with Predicates 31 | This strategy defines explict sets based on a predicate on the graph nodes. You may refer to properties of the `node` using dot syntax. The (optional) name for the set allows you to refer to this set later in the specificaiton. 32 | 33 | ```json 34 | [{"expr": "node.color === 'red' || node.color === 'blue'", "name": "setName"}, ...] 35 | ``` 36 | 37 | #### Collecting Nodes Using Keys 38 | This strategy generates sets by identify node keys. Sets generated in this way may not be disjoint. 39 | 40 | ```json 41 | {"collect": ["node", "node.neighbors"]} 42 | ``` 43 | 44 | In the above example, we extract the `_id` from all the identified nodes and create a set that contains all the identified nodes. In other words, for each node in the graph, we create a set that contains the node and all of its neighbors. 45 | 46 | #### Composing Previously Defined Sets 47 | This strategy allows for the hierarchical composition of sets by composing previously defined sets into a new set. 48 | 49 | ```json 50 | ["setName"] 51 | ``` 52 | 53 | In the above example, we create a new set that contains one element: the set named `"setName"`. 54 | 55 | ### Constraints 56 | There are currently 7 constraint types supported in SetCoLa: `alignment`, `position`, `order`, `circle`, `cluster`, `hull`, and `padding`. 57 | 58 | #### `alignment` 59 | 60 | ```json 61 | {"constraint": "align", "axis": "x", "orientation": "center"} 62 | ``` 63 | 64 | The `axis` along which to align the nodes can be defined as `x` or `y`. 65 | 66 | The orientation can be defined as `center`, `left`, `right`, `top`, or `bottom`. 67 | 68 | #### `position` 69 | 70 | ```json 71 | {"constraint": "position", "position": "left", "of": "right_border", "gap": 20} 72 | ``` 73 | 74 | This constraint positions all the nodes in each set to the left of the guide `right_border`. 75 | 76 | The `position` can be defined as `left`, `right`, `above`, or `below`. 77 | 78 | The (optional) `gap` property defines the minimum amount of space between the node and guide. 79 | 80 | #### `order` 81 | 82 | ```json 83 | {"constraint": "order", "axis": "x", "by": "nodeProperty", "order": [2, 3, 1, 0], "reverse": true, "band": 200} 84 | ``` 85 | 86 | The `axis` along which to order the nodes can be defined as `x` or `y`. 87 | 88 | The property `by` determines which `nodeProperty` to use for the order. 89 | 90 | The (optional) `order` property explicitly sets the order that should be used and `reverse` reverses the sort order. 91 | 92 | The (optional) `band` defines the amont of space that each section of the order should take up. 93 | 94 | #### `circle` 95 | 96 | ```json 97 | {"constraint": "circle", "around": "center", "radius": 10} 98 | ``` 99 | 100 | Adds additional edges to approximate a circle layout. `around` can be either `center` or the name of a guide. The `radius` determines the size of the circle. 101 | 102 | #### `cluster` 103 | 104 | ```json 105 | {"constraint": "cluster"} 106 | ``` 107 | 108 | Encourages the nodes to cluster together by introducing additional edges. 109 | 110 | #### `hull` 111 | 112 | ```json 113 | {"constraint": "hull"} 114 | ``` 115 | 116 | Adds an enclosing boundary around the nodes. 117 | 118 | #### `padding` 119 | 120 | ```json 121 | {"constraint": "padding", "amount": 5} 122 | ``` 123 | 124 | Adds `amount` padding around the nodes. *Note: At this time, this constraint can only apply to any given node once.* 125 | 126 | ## Usage 127 | The basic usage of the SetCoLa compiler is shown below. This behavior is demonstrated in the file `editor.js`. 128 | 129 | ```javascript 130 | var result = setcola 131 | .nodes(graph.nodes) // Set the graph nodes 132 | .links(graph.links) // Set the graph links 133 | .groups(groups) // (Optional) Set any predefined groups in the graph 134 | .guides(guides) // (Optional) Define any guides that are used by the SetCoLa layout 135 | .constraints(setcolaSpec) // Set the constraints 136 | .gap(gap) // The default gap size to use for generating the constraints (if not specified in the SetCoLa spec) 137 | .layout(); // Run the layout to convert the SetCoLa constraints to WebCoLa constraints 138 | ``` 139 | The call `setcola.layout()` returns a layout in the following form: 140 | 141 | ```javascript 142 | result = { 143 | "nodes": [...], // The output nodes (note: this may contain more nodes than originally input) 144 | "links": [...], // The output links (note: this may contain more links than originally input) 145 | "guides": [...], // The SetCoLa guides 146 | "groups": [...], // The output WebCoLa groups 147 | "constraints": [...], // The output WebCoLa constraints 148 | "constraintDefs": [...] // The original SetCoLa constraints 149 | } 150 | ``` 151 | This output can then be used to produce the actual graph layout using WebCoLa. For more information on WebCoLa, please check out the [website](http://ialab.it.monash.edu/webcola/). The basic usage of WebCoLa is shown below and demonstrated in the file `renderer.js`. 152 | 153 | ```javascript 154 | d3cola 155 | .nodes(result.nodes) 156 | .links(result.links) 157 | .constraints(result.constraints) 158 | .avoidOverlaps(true) 159 | .start(10,15,20); 160 | ``` 161 | 162 | ## Example 163 | This is a small SetCoLa example that shows how to create a simple tree layout. 164 | 165 | ```javascript 166 | // The SetCoLa constraints 167 | var setcolaSpec = [ 168 | { 169 | "name": "layer", 170 | "sets": {"partition": "depth"}, 171 | "forEach": [{"constraint": "align", "axis": "x"}] 172 | }, 173 | { 174 | "name": "sort", 175 | "sets": ["layer"], 176 | "forEach": [{"constraint": "order", "axis": "y", "by": "depth"}] 177 | } 178 | ]; 179 | ``` 180 | When applied to this graph: 181 | 182 | ```javascript 183 | var graph = { 184 | "nodes": [ 185 | {"name": "a"}, {"name": "b"}, 186 | {"name": "c"}, {"name": "d"}, 187 | {"name": "e"}, {"name": "f"} 188 | ], 189 | "links": [ 190 | {"source": 0, "target": 1}, 191 | {"source": 0, "target": 2}, 192 | {"source": 1, "target": 3}, 193 | {"source": 2, "target": 4}, 194 | {"source": 2, "target": 5} 195 | ] 196 | }; 197 | ``` 198 | 199 | SetCoLa produces the following layout: 200 | 201 | ![alt text](https://github.com/uwdata/setcola/blob/master/images/smalltree.png "SetCoLa small tree layout") 202 | 203 | For more examples, please take a look at our [online graph editor](https://uwdata.github.io/setcola/). 204 | 205 | ## Development 206 | 207 | To produce the SetCoLa compiler module on your local machine, use the following command `rollup -c`. This command will produce the file `dist/setcola.js`. You can then host the website locally on a mac using the command `python -m SimpleHTTPServer 8080`. 208 | -------------------------------------------------------------------------------- /src/setcola.js: -------------------------------------------------------------------------------- 1 | import {computeSets} from './sets.js'; 2 | import {computeConstraints} from './constraints.js'; 3 | 4 | var _nodes, _links, _sets, _gap, _guides, _guideNodes, _groups, _constraintDefs, _output; 5 | var INDEX; 6 | 7 | export function constraints(constraints) { 8 | if(constraints === undefined) { 9 | return _constraintDefs; 10 | } else { 11 | _constraintDefs = constraints; 12 | return this; 13 | } 14 | }; 15 | 16 | export function gap(gap) { 17 | if(gap === undefined) { 18 | return _gap; 19 | } else { 20 | _gap = gap; 21 | return this; 22 | } 23 | }; 24 | 25 | export function groups(groups) { 26 | if(groups === undefined) { 27 | return _groups; 28 | } else { 29 | _groups = groups; 30 | return this; 31 | } 32 | }; 33 | 34 | export function guides(guides) { 35 | if(guides === undefined) { 36 | return _guides; 37 | } else { 38 | _guides = guides; 39 | _nodes = _nodes.filter(function(node) { return !node._guide; }); // Remove previous guides. 40 | guides.map(generateGuides); 41 | return this; 42 | } 43 | }; 44 | 45 | export function links(links) { 46 | if(links === undefined) { 47 | return _links; 48 | } else { 49 | _links = links; 50 | _links.map(setLinkID); 51 | return this; 52 | } 53 | }; 54 | 55 | export function nodes(nodes) { 56 | if(nodes === undefined) { 57 | return _nodes; 58 | } else { 59 | _nodes = nodes; 60 | _nodes.map(setID); 61 | return this; 62 | } 63 | }; 64 | 65 | export function sets() { 66 | return _sets; 67 | }; 68 | 69 | export function layout() { 70 | INDEX = -1; 71 | 72 | if(!_nodes) console.error('No graph nodes defined.'); 73 | if(!_links) links([]); 74 | if(!_groups) groups([]); 75 | if(!_guides) guides([]); 76 | if(!_constraintDefs) constraints([]); 77 | if(!_gap) gap(20); 78 | 79 | // Remove previously added internal properties. 80 | _nodes = _nodes.filter(function(node) { return !node._cid; }); 81 | _links = _links.filter(function(link) { return !link._cid; }); 82 | _groups = _groups.filter(function(group) { return !group._cid; }); 83 | 84 | // Compute additional graph properties as needed 85 | computeBuiltInProperties(_constraintDefs); 86 | 87 | // Generate the SetCoLa sets 88 | _sets = {}; 89 | for (var i = 0; i < _constraintDefs.length; i++) { 90 | var result = generateSets(_constraintDefs[i]); 91 | _sets[result.name] = result.sets; 92 | } 93 | 94 | // Generate the WebCoLa constraints 95 | _constraintDefs.forEach(function() { 96 | 97 | }); 98 | var webcolaConstraints = [].concat.apply([], _constraintDefs.map(generateConstraints)); 99 | 100 | // Produce the output spec 101 | return { 102 | nodes: nodes(), 103 | links: links(), 104 | groups: groups(), 105 | guides: guides(), 106 | constraints: webcolaConstraints, 107 | constraintDefs: constraints() 108 | }; 109 | }; 110 | 111 | function generateGuides(guide) { 112 | var node = { 113 | '_guide': true, 114 | '_temp': true, 115 | 'fixed': true, 116 | 'width': 1, 117 | 'height': 1, 118 | 'padding': 0, 119 | 'x': Math.random()*100, 120 | 'y': Math.random()*100, 121 | 'boundary': '' 122 | }; 123 | 124 | // Save the position information from the guide. 125 | var complete = false; 126 | if(guide.hasOwnProperty('x')) { 127 | node.x = guide.x; 128 | node.boundary += 'x'; 129 | complete = true; 130 | } 131 | if(guide.hasOwnProperty('y')) { 132 | node.y = guide.y; 133 | node.boundary += 'y'; 134 | complete = true; 135 | } 136 | if(!complete) { 137 | console.error('Guide must have an x and/or y position: ', guide); 138 | } 139 | 140 | // Save the name from the guide. 141 | if(guide.hasOwnProperty('name')) { 142 | var found = _nodes.filter(function(node) { return node.name === guide.name; }); 143 | if(found.length > 0) { 144 | console.error('A node with the name \'' + guide.name + '\' already exists.'); 145 | } else { 146 | node.name = guide.name; 147 | } 148 | } else { 149 | console.error('Guide must have a name: ', guide); 150 | } 151 | 152 | // Save the guide and get it's index. 153 | _nodes.push(node); 154 | node._id = _nodes.indexOf(node); 155 | return node; 156 | }; 157 | 158 | function generateSets(constraintDef) { 159 | var source = _nodes.filter(function(node) { return !node._temp; }); 160 | if(constraintDef.from && typeof constraintDef.from === 'string') { 161 | source = _sets[constraintDef.from]; 162 | } else if(constraintDef.from) { 163 | source = computeSets(_nodes, constraintDef.from, _sets); 164 | } 165 | if(!constraintDef.name) constraintDef.name = '_set' + ++INDEX; 166 | return {'name': constraintDef.name, 'sets': computeSets(source, constraintDef.sets, _sets)} 167 | }; 168 | 169 | function generateConstraints(constraintDef) { 170 | var results = []; 171 | (constraintDef.forEach || []).forEach(function(constraint) { 172 | (_sets[constraintDef.name] || []).forEach(function(elements) { 173 | results = results.concat(computeConstraints(elements, constraint, constraintDef.name, _gap, nodes, links, groups)); 174 | }); 175 | }); 176 | return results; 177 | }; 178 | 179 | /**********************************************************************/ 180 | /************************** Graph Properties **************************/ 181 | /**********************************************************************/ 182 | 183 | function computeBuiltInProperties(constraints) { 184 | _nodes.forEach(setID); 185 | _links.forEach(setLinkID); 186 | 187 | // Compute numeric properties for the nodes 188 | var hasProperty = function(c, p) { return JSON.stringify(c).indexOf(p) != -1; }; 189 | if(hasProperty(constraints, 'depth')) { 190 | calculateDepths(); 191 | _nodes.forEach(function(node) { delete node.visited; }); 192 | } 193 | if(hasProperty(constraints, 'degree')) calculateDegree(); 194 | 195 | // Add accessors to get properties returning graph nodes/edges 196 | _nodes.forEach(function(node) { 197 | node.getSources = function() { return getSources(this); }; 198 | node.getTargets = function() { return getTargets(this); }; 199 | node.getNeighbors = function() { return getNeighbors(this); }; 200 | node.getIncoming = function() { return getIncoming(this); }; 201 | node.getOutgoing = function() { return getOutgoing(this); }; 202 | node.getEdges = function() { return getEdges(this); }; 203 | node.getFirstChild = function() { return getFirstChild(this); } ; 204 | }); 205 | }; 206 | 207 | function setID(node) { 208 | node._id = node._id || _nodes.indexOf(node); 209 | }; 210 | 211 | function setLinkID(link) { 212 | link._linkid = link._linkid || _links.indexOf(link); 213 | }; 214 | 215 | function graphSources() { 216 | return _nodes.filter(function(node) { 217 | if(node.hasOwnProperty('_isSource')) return node._isSource; 218 | var incoming = getIncoming(node).filter(function(n) { return n.source !== n.target; }); 219 | return incoming.length === 0; 220 | }); 221 | }; 222 | 223 | function calculateDepths() { 224 | var roots = graphSources(); 225 | if(roots.length === 0 && _nodes.length !== 0) { 226 | console.error('No roots exist, so cannot compute node depth. Please assign a \'_isSource\' property to the root and try again.'); 227 | } 228 | _nodes.forEach(getDepth); 229 | }; 230 | 231 | function calculateDegree() { 232 | _nodes.forEach(function(node) { 233 | node.degree = node.degree || getDegree(node); 234 | }); 235 | }; 236 | 237 | // The list of nodes that have edges for which the input is the target 238 | // (e.g., the node's parents). 239 | function getSources(node) { 240 | var incoming = getIncoming(node); 241 | var sources = incoming.map(function(link) { 242 | return (typeof link.source === 'object') ? link.source : _nodes[link.source]; 243 | }); 244 | return sources; 245 | }; 246 | 247 | // The list of nodes that have edges for which the input is the source 248 | // (e.g., the node's children). 249 | function getTargets(node) { 250 | var outgoing = getOutgoing(node); 251 | var targets = outgoing.map(function(link) { 252 | return (typeof link.target == 'object') ? link.target : _nodes[link.target]; 253 | }); 254 | return targets; 255 | }; 256 | 257 | // The list of nodes that have edges connected to the input (e.g., the 258 | // node's neighbors). 259 | function getNeighbors(node) { 260 | var sources = node.sources || getSources(node); 261 | var targets = node.targets || getTargets(node); 262 | return sources.concat(targets); 263 | }; 264 | 265 | // The list of edges that have the input as the target (e.g., edges 266 | // connecting the node to its parents). 267 | function getIncoming(node) { 268 | var index = node._id; 269 | var incoming = _links.filter(function(link) { 270 | var source = (typeof link.source === 'object') ? link.source._id : link.source; 271 | var target = (typeof link.target === 'object') ? link.target._id : link.target; 272 | return target == index && source !== index; 273 | }); 274 | return incoming; 275 | }; 276 | 277 | // The list of edges that have the input as the source (e.g., edges 278 | // connecting the node to its children). 279 | function getOutgoing(node) { 280 | var index = node._id; 281 | var outgoing = _links.filter(function(link) { 282 | var source = (typeof link.source === 'object') ? link.source._id : link.source; 283 | var target = (typeof link.target === 'object') ? link.target._id : link.target; 284 | return source == index && target !== index; 285 | }); 286 | return outgoing; 287 | }; 288 | 289 | // The list of edges that contain the input (e.g., edges connecting the 290 | // node to its neighbors). 291 | function getEdges(node) { 292 | var incoming = node.incoming || getIncoming(node); 293 | var outgoing = node.outgoing || getOutgoing(node); 294 | return incoming.concat(outgoing); 295 | }; 296 | 297 | // The number of neighbors for the current node. 298 | function getDegree(node) { 299 | var incoming = node.incoming || getIncoming(node); 300 | var outgoing = node.outgoing || getOutgoing(node); 301 | return incoming.length + outgoing.length; 302 | }; 303 | 304 | function getDepth(node) { 305 | if(node.hasOwnProperty('depth')) return node.depth; 306 | if(node.visited) console.error('Cannot compute the depth for a graph with cycles.'); 307 | node.visited = true; 308 | node.depth = Math.max(0, Math.max(...getSources(node).map(getDepth)) + 1); 309 | return node.depth; 310 | }; 311 | 312 | function getFirstChild(node) { 313 | var outgoing = node.outgoing || getOutgoing(node); 314 | outgoing = outgoing.sort(function(a,b) { return a._id - b._id; }); 315 | outgoing = outgoing.filter(function(n) { return n.target !== n.source; }); 316 | if(outgoing.length == 0) return null; 317 | return _nodes[outgoing[0].target]; 318 | }; 319 | -------------------------------------------------------------------------------- /src/constraints.js: -------------------------------------------------------------------------------- 1 | var _graphNodes, _graphLinks, _groups, _gap; 2 | 3 | export function computeConstraints(elements, definition, cid, gap, graphNodes, graphLinks, graphGroups) { 4 | _graphNodes = graphNodes; 5 | _graphLinks = graphLinks; 6 | _groups = graphGroups; 7 | _gap = gap; 8 | 9 | var results = []; 10 | var ID = cid + '_' + definition.constraint; 11 | switch(definition.constraint) { 12 | case 'align': 13 | results = results.concat(alignment(elements, definition, ID)); 14 | break; 15 | case 'order': 16 | results = results.concat(orderElements(elements, definition, ID)); 17 | break; 18 | case 'position': 19 | results = results.concat(position(elements, definition, ID)); 20 | break; 21 | case 'circle': 22 | circle(elements, definition, ID); 23 | break; 24 | case 'hull': 25 | hull(elements, definition, ID); 26 | break; 27 | case 'cluster': 28 | cluster(elements, definition, ID); 29 | break; 30 | case 'padding': 31 | padding(elements, definition, ID); 32 | break; 33 | default: 34 | console.error('Unknown constraint type \'' + definition.type + '\''); 35 | }; 36 | 37 | return results; 38 | }; 39 | 40 | /******************** Alignment Constraints ********************/ 41 | 42 | function alignment(elements, definition, cid) { 43 | var nodes = elements; 44 | 45 | // Compute the alignment offset 46 | var offsets = {}; 47 | nodes.forEach(function(node) { 48 | switch(definition.orientation) { 49 | case 'top': 50 | offsets[node._id] = node.height/2; 51 | break; 52 | case 'bottom': 53 | offsets[node._id] = -node.height/2; 54 | break; 55 | case 'left': 56 | offsets[node._id] = node.width/2; 57 | break; 58 | case 'right': 59 | offsets[node._id] = -node.width/2; 60 | break; 61 | default: 62 | offsets[node._id] = 0; 63 | } 64 | }); 65 | 66 | // Generate the CoLa constraints 67 | var results = []; 68 | results = results.concat(CoLaAlignment(nodes, definition.axis, offsets, cid)); 69 | return results; 70 | }; 71 | 72 | /********************** Order Constraints **********************/ 73 | 74 | function generateOrderFunc(definition) { 75 | var order; 76 | if(definition.hasOwnProperty('order')) { 77 | if(definition.hasOwnProperty('reverse') && definition.reverse) definition.order.reverse(); 78 | order = function(n1,n2) { 79 | return definition.order.indexOf(n1[definition.by]) - definition.order.indexOf(n2[definition.by]); 80 | }; 81 | } else if(definition.hasOwnProperty('reverse') && definition.reverse) { 82 | order = function(n1,n2) { 83 | return n1[definition.by] - n2[definition.by]; 84 | }; 85 | } else { 86 | order = function(n1,n2) { 87 | return n2[definition.by] - n1[definition.by]; 88 | }; 89 | } 90 | return order; 91 | }; 92 | 93 | function orderElements(elements, definition, cid) { 94 | if(elements[0] instanceof Array) { 95 | return orderSets(elements, definition, cid); 96 | } else { 97 | return orderNodes(elements, definition, cid); 98 | } 99 | }; 100 | 101 | function orderNodes(nodes, definition, cid) { 102 | // Sort the nodes into groups 103 | var order = generateOrderFunc(definition); 104 | nodes = nodes.sort(order); 105 | 106 | // Generate the CoLa constraints 107 | var results = []; 108 | var axis = definition.axis; 109 | var gap = definition.gap ? definition.gap : _gap; 110 | for(var i=0; i 1) collectSets[element._id] = set; 123 | }); 124 | return Object.keys(collectSets).map(function(setName) { return collectSets[setName]; }); 125 | } 126 | function exprSet(elements, definition, index) { 127 | var set = []; 128 | elements.forEach(function(element) { 129 | var matches = definition.expr.match(/node\.[a-zA-Z.0-9]+/g); 130 | var expr = definition.expr; 131 | matches.forEach(function(match) { 132 | var props = match.replace('node.', '').split('.'); 133 | var result; 134 | for (var i = 0; i < props.length; i++) { 135 | result = element[props[i]]; 136 | } 137 | expr = expr.replace(match, JSON.stringify(result)); 138 | }); 139 | if(eval(expr)) set.push(element); 140 | }); 141 | set._exprIndex = index; 142 | return set; 143 | } 144 | function existingSet(elements, definition) { 145 | return _sets[definition]; 146 | } 147 | 148 | var _graphNodes, _graphLinks, _groups, _gap; 149 | 150 | function computeConstraints(elements, definition, cid, gap, graphNodes, graphLinks, graphGroups) { 151 | _graphNodes = graphNodes; 152 | _graphLinks = graphLinks; 153 | _groups = graphGroups; 154 | _gap = gap; 155 | 156 | var results = []; 157 | var ID = cid + '_' + definition.constraint; 158 | switch(definition.constraint) { 159 | case 'align': 160 | results = results.concat(alignment(elements, definition, ID)); 161 | break; 162 | case 'order': 163 | results = results.concat(orderElements(elements, definition, ID)); 164 | break; 165 | case 'position': 166 | results = results.concat(position(elements, definition, ID)); 167 | break; 168 | case 'circle': 169 | circle(elements, definition, ID); 170 | break; 171 | case 'hull': 172 | hull(elements, definition, ID); 173 | break; 174 | case 'cluster': 175 | cluster(elements, definition, ID); 176 | break; 177 | case 'padding': 178 | padding(elements, definition, ID); 179 | break; 180 | default: 181 | console.error('Unknown constraint type \'' + definition.type + '\''); 182 | } 183 | return results; 184 | } 185 | /******************** Alignment Constraints ********************/ 186 | 187 | function alignment(elements, definition, cid) { 188 | var nodes = elements; 189 | 190 | // Compute the alignment offset 191 | var offsets = {}; 192 | nodes.forEach(function(node) { 193 | switch(definition.orientation) { 194 | case 'top': 195 | offsets[node._id] = node.height/2; 196 | break; 197 | case 'bottom': 198 | offsets[node._id] = -node.height/2; 199 | break; 200 | case 'left': 201 | offsets[node._id] = node.width/2; 202 | break; 203 | case 'right': 204 | offsets[node._id] = -node.width/2; 205 | break; 206 | default: 207 | offsets[node._id] = 0; 208 | } 209 | }); 210 | 211 | // Generate the CoLa constraints 212 | var results = []; 213 | results = results.concat(CoLaAlignment(nodes, definition.axis, offsets, cid)); 214 | return results; 215 | } 216 | /********************** Order Constraints **********************/ 217 | 218 | function generateOrderFunc(definition) { 219 | var order; 220 | if(definition.hasOwnProperty('order')) { 221 | if(definition.hasOwnProperty('reverse') && definition.reverse) definition.order.reverse(); 222 | order = function(n1,n2) { 223 | return definition.order.indexOf(n1[definition.by]) - definition.order.indexOf(n2[definition.by]); 224 | }; 225 | } else if(definition.hasOwnProperty('reverse') && definition.reverse) { 226 | order = function(n1,n2) { 227 | return n1[definition.by] - n2[definition.by]; 228 | }; 229 | } else { 230 | order = function(n1,n2) { 231 | return n2[definition.by] - n1[definition.by]; 232 | }; 233 | } 234 | return order; 235 | } 236 | function orderElements(elements, definition, cid) { 237 | if(elements[0] instanceof Array) { 238 | return orderSets(elements, definition, cid); 239 | } else { 240 | return orderNodes(elements, definition, cid); 241 | } 242 | } 243 | function orderNodes(nodes, definition, cid) { 244 | // Sort the nodes into groups 245 | var order = generateOrderFunc(definition); 246 | nodes = nodes.sort(order); 247 | 248 | // Generate the CoLa constraints 249 | var results = []; 250 | var axis = definition.axis; 251 | var gap = definition.gap ? definition.gap : _gap; 252 | for(var i=0; i 0) { 607 | console.error('A node with the name \'' + guide.name + '\' already exists.'); 608 | } else { 609 | node.name = guide.name; 610 | } 611 | } else { 612 | console.error('Guide must have a name: ', guide); 613 | } 614 | 615 | // Save the guide and get it's index. 616 | _nodes.push(node); 617 | node._id = _nodes.indexOf(node); 618 | return node; 619 | } 620 | function generateSets(constraintDef) { 621 | var source = _nodes.filter(function(node) { return !node._temp; }); 622 | if(constraintDef.from && typeof constraintDef.from === 'string') { 623 | source = _sets$1[constraintDef.from]; 624 | } else if(constraintDef.from) { 625 | source = computeSets(_nodes, constraintDef.from, _sets$1); 626 | } 627 | if(!constraintDef.name) constraintDef.name = '_set' + ++INDEX; 628 | return {'name': constraintDef.name, 'sets': computeSets(source, constraintDef.sets, _sets$1)} 629 | } 630 | function generateConstraints(constraintDef) { 631 | var results = []; 632 | (constraintDef.forEach || []).forEach(function(constraint) { 633 | (_sets$1[constraintDef.name] || []).forEach(function(elements) { 634 | results = results.concat(computeConstraints(elements, constraint, constraintDef.name, _gap$1, nodes, links, groups)); 635 | }); 636 | }); 637 | return results; 638 | } 639 | /**********************************************************************/ 640 | /************************** Graph Properties **************************/ 641 | /**********************************************************************/ 642 | 643 | function computeBuiltInProperties(constraints) { 644 | _nodes.forEach(setID); 645 | _links.forEach(setLinkID); 646 | 647 | // Compute numeric properties for the nodes 648 | var hasProperty = function(c, p) { return JSON.stringify(c).indexOf(p) != -1; }; 649 | if(hasProperty(constraints, 'depth')) { 650 | calculateDepths(); 651 | _nodes.forEach(function(node) { delete node.visited; }); 652 | } 653 | if(hasProperty(constraints, 'degree')) calculateDegree(); 654 | 655 | // Add accessors to get properties returning graph nodes/edges 656 | _nodes.forEach(function(node) { 657 | node.getSources = function() { return getSources(this); }; 658 | node.getTargets = function() { return getTargets(this); }; 659 | node.getNeighbors = function() { return getNeighbors(this); }; 660 | node.getIncoming = function() { return getIncoming(this); }; 661 | node.getOutgoing = function() { return getOutgoing(this); }; 662 | node.getEdges = function() { return getEdges(this); }; 663 | node.getFirstChild = function() { return getFirstChild(this); } ; 664 | }); 665 | } 666 | function setID(node) { 667 | node._id = node._id || _nodes.indexOf(node); 668 | } 669 | function setLinkID(link) { 670 | link._linkid = link._linkid || _links.indexOf(link); 671 | } 672 | function graphSources() { 673 | return _nodes.filter(function(node) { 674 | if(node.hasOwnProperty('_isSource')) return node._isSource; 675 | var incoming = getIncoming(node).filter(function(n) { return n.source !== n.target; }); 676 | return incoming.length === 0; 677 | }); 678 | } 679 | function calculateDepths() { 680 | var roots = graphSources(); 681 | if(roots.length === 0 && _nodes.length !== 0) { 682 | console.error('No roots exist, so cannot compute node depth. Please assign a \'_isSource\' property to the root and try again.'); 683 | } 684 | _nodes.forEach(getDepth); 685 | } 686 | function calculateDegree() { 687 | _nodes.forEach(function(node) { 688 | node.degree = node.degree || getDegree(node); 689 | }); 690 | } 691 | // The list of nodes that have edges for which the input is the target 692 | // (e.g., the node's parents). 693 | function getSources(node) { 694 | var incoming = getIncoming(node); 695 | var sources = incoming.map(function(link) { 696 | return (typeof link.source === 'object') ? link.source : _nodes[link.source]; 697 | }); 698 | return sources; 699 | } 700 | // The list of nodes that have edges for which the input is the source 701 | // (e.g., the node's children). 702 | function getTargets(node) { 703 | var outgoing = getOutgoing(node); 704 | var targets = outgoing.map(function(link) { 705 | return (typeof link.target == 'object') ? link.target : _nodes[link.target]; 706 | }); 707 | return targets; 708 | } 709 | // The list of nodes that have edges connected to the input (e.g., the 710 | // node's neighbors). 711 | function getNeighbors(node) { 712 | var sources = node.sources || getSources(node); 713 | var targets = node.targets || getTargets(node); 714 | return sources.concat(targets); 715 | } 716 | // The list of edges that have the input as the target (e.g., edges 717 | // connecting the node to its parents). 718 | function getIncoming(node) { 719 | var index = node._id; 720 | var incoming = _links.filter(function(link) { 721 | var source = (typeof link.source === 'object') ? link.source._id : link.source; 722 | var target = (typeof link.target === 'object') ? link.target._id : link.target; 723 | return target == index && source !== index; 724 | }); 725 | return incoming; 726 | } 727 | // The list of edges that have the input as the source (e.g., edges 728 | // connecting the node to its children). 729 | function getOutgoing(node) { 730 | var index = node._id; 731 | var outgoing = _links.filter(function(link) { 732 | var source = (typeof link.source === 'object') ? link.source._id : link.source; 733 | var target = (typeof link.target === 'object') ? link.target._id : link.target; 734 | return source == index && target !== index; 735 | }); 736 | return outgoing; 737 | } 738 | // The list of edges that contain the input (e.g., edges connecting the 739 | // node to its neighbors). 740 | function getEdges(node) { 741 | var incoming = node.incoming || getIncoming(node); 742 | var outgoing = node.outgoing || getOutgoing(node); 743 | return incoming.concat(outgoing); 744 | } 745 | // The number of neighbors for the current node. 746 | function getDegree(node) { 747 | var incoming = node.incoming || getIncoming(node); 748 | var outgoing = node.outgoing || getOutgoing(node); 749 | return incoming.length + outgoing.length; 750 | } 751 | function getDepth(node) { 752 | if(node.hasOwnProperty('depth')) return node.depth; 753 | if(node.visited) console.error('Cannot compute the depth for a graph with cycles.'); 754 | node.visited = true; 755 | node.depth = Math.max(0, Math.max(...getSources(node).map(getDepth)) + 1); 756 | return node.depth; 757 | } 758 | function getFirstChild(node) { 759 | var outgoing = node.outgoing || getOutgoing(node); 760 | outgoing = outgoing.sort(function(a,b) { return a._id - b._id; }); 761 | outgoing = outgoing.filter(function(n) { return n.target !== n.source; }); 762 | if(outgoing.length == 0) return null; 763 | return _nodes[outgoing[0].target]; 764 | } 765 | 766 | exports.constraints = constraints; 767 | exports.gap = gap; 768 | exports.groups = groups; 769 | exports.guides = guides; 770 | exports.links = links; 771 | exports.nodes = nodes; 772 | exports.sets = sets; 773 | exports.layout = layout; 774 | 775 | Object.defineProperty(exports, '__esModule', { value: true }); 776 | 777 | }))); 778 | --------------------------------------------------------------------------------