Skip to content

Commit

Permalink
1.6.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Rowell authored and Eric Rowell committed Mar 17, 2019
1 parent d4b7ed0 commit b548c25
Show file tree
Hide file tree
Showing 26 changed files with 235 additions and 184 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changlog

## v1.6.0
* new edges API (see docs)
* labels for nodes
* edge arrows for directed graphs

Expand Down
100 changes: 67 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,10 @@ let graph = new ElGrapho({
ys: [0.6, 0, 0, -0.6, -0.6, -0.6, -0.6],
colors: [0, 1, 1, 2, 2, 2, 2]
},
edges: [
0, 1,
0, 2,
1, 3,
1, 4,
2, 5,
2, 6
],
edges: {
from: [0, 0, 1, 1, 2, 2],
to: [1, 2, 3, 4, 5, 6]
},
width: 800,
height: 400
}
Expand All @@ -63,9 +59,9 @@ let graph = new ElGrapho({

* ```container``` - DOM element that will contain the El Grapho graph.

* ```model.nodes``` - object that contains information about all of the nodes in the graph (graphs are made up of nodes and edges). Each node is defined by a position (x and y), and also a color. El Grapho x and y ranges are between -1 and 1. If x is -1, then the node position is on the very left of the viewport. If x is 0 it is in the center. And if x is 1 it is on the very right of the viewport. Colors are integer values between 0 and 7. These integer values map to the El Grapho color palette.
* ```model.nodes``` - object that defines the nodes in the graph (graphs are made up of nodes and edges). Each node is defined by a position (x and y), and also a color. El Grapho x and y ranges are between -1 and 1. If x is -1, then the node position is on the very left of the viewport. If x is 0 it is in the center. And if x is 1 it is on the very right of the viewport. Colors are integer values between 0 and 7. These integer values map to the El Grapho color palette.

* ```model.edges``` - array that defines the edges between nodes based on their indices. In the example above, the first edge begins at node ```0``` and ends at node ```1```. For non directed graphs, or bi-directional graphs, the order of the first node and second node do not matter. However, for directed graphs, the first index is the *from* node, and the second index is the *to* node.
* ```model.edges``` - object that defines the edges between nodes based on their indices. Each edge is defined by a from-node-index and a to-node-index. In the example above, the first edge begins at node ```0``` and ends at node ```1```. For non directed graphs, or bi-directional graphs, ```from``` and ```to``` are interchangeable.

* ```model.width``` - number that defines the width of the El Grapho viewport in pixels.

Expand All @@ -75,9 +71,13 @@ let graph = new ElGrapho({

* ```debug``` - boolean that can be used to enable debug mode. Debug mode will show the node and edge count in the bottom right corner of the visualization. The default is false.

* ```arrows``` - boolean that enables or disables edge arrows. For non directed or bi-directional graphs, you should set ```arrows``` to ```false```. The default is true.

* ```labels``` - array of strings used to define labels for each node. If your visualization has 100 nodes, there should be 100 strings in the ```labels``` array. The default is an empty array which results in no labels.

### Models

Determining the positions of the nodes for your graph can be alot of work! While it's nice to have the power to construct custom graph shapes, most El Grapho users will want to leverage the provided El Grapho models which will generate node positions for you. Currently, ElGrapho supports ```Tree``` and ```Cluster```
Determining the positions of the nodes for your graph can be alot of work! While it's nice to have the power to construct custom graph shapes, most El Grapho users will want to leverage the provided El Grapho models which will generate node positions for you. Currently, ElGrapho supports ```Tree```, ```Ring```, ```Cluster```, and ```ForceDirectedGraph```

#### Tree Model

Expand All @@ -86,14 +86,10 @@ let modelConfig = {
nodes: {
colors: [0, 1, 1, 2, 2, 3, 3]
},
edges: [
0, 1,
0, 2,
1, 3,
1, 4,
2, 5,
2, 6
],
edges: {
from: [0, 0, 1, 1, 2, 2],
to: [1, 2, 3, 4, 5, 6]
},
width: 800,
height: 400
};
Expand All @@ -104,7 +100,26 @@ let graph = new ElGrapho({
});
```

The ```Tree``` model takes in a nested tree structure and calculates the node positions for you by adding ```xs``` and ```ys``` to the ```nodes``` object. In this example, the root node has two children, and each of those children have two children of their own. In other words, this is a simple binary tree with two levels.
#### Ring Model

```
let modelConfig = {
nodes: {
colors: [0, 1, 1, 2, 2, 3, 3]
},
edges: {
from: [0, 0, 1, 1, 2, 2],
to: [1, 2, 3, 4, 5, 6]
},
width: 800,
height: 400
};
let graph = new ElGrapho({
container: document.getElementById('container'),
model: ElGrapho.models.Ring(modelConfig)
});
```

#### Cluster Model

Expand All @@ -113,16 +128,10 @@ let modelConfig = {
nodes: {
colors: [0, 1, 1, 2, 2, 2, 2, 2]
},
edges: [
0, 1,
0, 2,
0, 3,
0, 4,
0, 5,
0, 6,
0, 7,
0, 8
],
edges: {
from: [0, 0, 0, 0, 0, 0, 0, 0],
to: [1, 2, 3, 4, 5, 6, 7, 8]
},
width: 800,
height: 400
};
Expand All @@ -133,11 +142,36 @@ let graph = new ElGrapho({
});
```

The ```Cluster``` model takes in an array of colors, and an array of edges. If a single color is used for all of the nodes, ElGrapho will generate a single centered cluster. If there are several colors used, ElGrapho will render distinct clusters. Because Cluster models can be generated in ```O(n)``` time, i.e. linear time, they are very fast to construct compared to other models such as force directed graphs which are polynomial in time.
The Cluster model clusters nodes by color. If a single color is used for all of the nodes, ElGrapho will generate a single centered cluster. If there are several colors used, ElGrapho will render distinct clusters. Because Cluster models can be generated in ```O(n)``` time, i.e. linear time, they are very fast to construct compared to other models such as force directed graphs which are polynomial in time.

#### ForceDirectedGraph Model

```
let modelConfig = {
nodes: {
colors: [0, 1, 1, 2, 2, 2, 2, 2]
},
edges: {
from: [0, 0, 0, 0, 0, 0, 0, 0],
to: [1, 2, 3, 4, 5, 6, 7, 8]
},
width: 800,
height: 400
};
let graph = new ElGrapho({
container: document.getElementById('container'),
model: ElGrapho.models.ForceDirectedGraph(modelConfig)
});
```

The ```ForceDirectedGraph``` uses a physics simulator to repel and attract nodes in order to generate natural layouts. Be warned that force directed graphs take O(n^2) time, which means they may not be appropriate for generating models on the client with lots of nodes. If it's possible to build your models in advance, it's a good idea to build the force directed graph model on the server and then cache it. If you require your models to be constructed at execution time, and the number of nodes is very high, you may consider using an O(n) model such as ```Cluster```

The ```ForceDirectedGraph``` model accepts a ```steps``` property from the modelConfig which can be used to define the accuracy of the result. This is because force directed graphs require multiple passes to obtain the final result. The default is 10. Lowering this number will result in faster model construction but less accurate results. Increasing this number will result in slower model construction but more accurate results.

### Model Polymorphism
## Model Polymorphism

You may have noticed that the model config schema is identical for ```Tree``` and ```Cluster```. In fact, all El Grapho models have the exact same schema. Thus, El Grapho visualizations are polymorphic, meaning you can pass the same data structure into different models and get different, but correct, graph visualizations. Pretty cool!
You may have noticed that the model config schema is identical for all models. As a result, El Grapho visualizations are polymorphic, meaning you can pass the same data structure into different models and get different graph visualizations. Pretty cool!

## Server Side Model Generation

Expand Down
71 changes: 37 additions & 34 deletions engine/dist/ElGrapho.js
Original file line number Diff line number Diff line change
Expand Up @@ -2263,7 +2263,7 @@ const VertexBridge = {
let colors = new Float32Array(nodes.colors);

// one edge is defined by two elements (from and to). each edge requires 2 triangles. Each triangle has 3 positions, with an x and y for each
let numEdges = edges.length / 2;
let numEdges = edges.from.length;
let numArrows = showArrows ? numEdges : 0;

let trianglePositions = new Float32Array(numEdges * 12 + numArrows * 6);
Expand All @@ -2274,9 +2274,9 @@ const VertexBridge = {
let triangleNormalsIndex = 0;
let triangleColorsIndex = 0;

for (let n=0; n<edges.length; n+=2) {
let pointIndex0 = edges[n];
let pointIndex1 = edges[n+1];
for (let n=0; n<numEdges; n++) {
let pointIndex0 = edges.from[n];
let pointIndex1 = edges.to[n];
let normalDistance = MAX_NODE_SIZE*0.1;

let x0 = nodes.xs[pointIndex0];
Expand Down Expand Up @@ -3053,9 +3053,12 @@ let Cluster = function(config) {
ys: [],
colors: config.nodes.colors.slice()
},
edges: config.edges.slice(),
width: width,
height: height
edges: {
from: config.edges.from.slice(),
to: config.edges.to.slice()
},
width: config.width,
height: config.height
};

// keys are color integers, values are arrays. The arrays contain node indices
Expand Down Expand Up @@ -3139,11 +3142,14 @@ const ForceDirectedGraph = function(config) {

let model = {
nodes: {
xs: [],
ys: [],
colors: []
xs: [],
ys: [],
colors: config.nodes.colors.slice()
},
edges: {
from: config.edges.from.slice(),
to: config.edges.to.slice()
},
edges: [],
width: config.width,
height: config.height
};
Expand All @@ -3154,11 +3160,9 @@ const ForceDirectedGraph = function(config) {
model.nodes.ys.length = numNodes;
model.nodes.ys.fill(0);

model.nodes.colors = config.nodes.colors.slice();
model.edges = config.edges.slice();

let nodes = model.nodes;
let edges = model.edges;
let numEdges = edges.from.length;

// find color counts
let colors = [];
Expand Down Expand Up @@ -3233,9 +3237,9 @@ const ForceDirectedGraph = function(config) {
}

// attractive forces between nodes sharing an edge
for (let i=0; i<edges.length; i+=2) {
let a = edges[i];
let b = edges[i+1];
for (let i=0; i<numEdges; i++) {
let a = edges.from[i];
let b = edges.to[i];

let ax = nodes.xs[a];
let ay = nodes.ys[a];
Expand Down Expand Up @@ -3300,19 +3304,18 @@ const Ring = function(config) {

let model = {
nodes: {
xs: [],
ys: [],
colors: []
xs: [],
ys: [],
colors: config.nodes.colors.slice()
},
edges: {
from: config.edges.from.slice(),
to: config.edges.to.slice()
},
edges: [],
width: config.width,
height: config.height
};

// TODO: need to sort colors first and shuffle edges
model.nodes.colors = config.nodes.colors;
model.edges = config.edges;

for (let n=0; n<numNodes; n++) {
let angle = (-1*Math.PI*2*n / numNodes) + Math.PI/2;
model.nodes.xs.push(Math.cos(angle));
Expand Down Expand Up @@ -3381,7 +3384,6 @@ let getNestedTree = function(config) {
let edges = config.edges;
let colors = config.nodes.colors;
let nodes = {};
let edgeIndex = 0;

// build nodes
for (let n=0; n<colors.length; n++) {
Expand All @@ -3393,9 +3395,9 @@ let getNestedTree = function(config) {
};
}

while(edgeIndex < edges.length) {
let fromIndex = edges[edgeIndex++];
let toIndex = edges[edgeIndex++];
for (let n=0; n<edges.from.length; n++) {
let fromIndex = edges.from[n];
let toIndex = edges.to[n];

// parent child relationship
nodes[fromIndex].children.push(nodes[toIndex]);
Expand Down Expand Up @@ -3446,22 +3448,23 @@ const Tree = function(config) {
ys: [],
colors: []
},
edges: [], // num edges = num nodes - 1
edges: {
from: [],
to: []
},
width: config.width,
height: config.height
};

let edgeIndex = 0;

// O(n)
nodes.forEach(function(node, n) {
model.nodes.xs[n] = node.x;
model.nodes.ys[n] = 1 - (2 * ((node.level - 1) / (maxLevel - 1)));
model.nodes.colors[n] = node.color;

if (node.parent) {
model.edges[edgeIndex++] = node.parent.index;
model.edges[edgeIndex++] = node.index;
model.edges.from[n] = node.parent.index;
model.edges.to[n] = node.index;
}
});

Expand Down
2 changes: 1 addition & 1 deletion engine/dist/ElGrapho.min.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions engine/src/VertexBridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const VertexBridge = {
let colors = new Float32Array(nodes.colors);

// one edge is defined by two elements (from and to). each edge requires 2 triangles. Each triangle has 3 positions, with an x and y for each
let numEdges = edges.length / 2;
let numEdges = edges.from.length;
let numArrows = showArrows ? numEdges : 0;

let trianglePositions = new Float32Array(numEdges * 12 + numArrows * 6);
Expand All @@ -40,9 +40,9 @@ const VertexBridge = {
let triangleNormalsIndex = 0;
let triangleColorsIndex = 0;

for (let n=0; n<edges.length; n+=2) {
let pointIndex0 = edges[n];
let pointIndex1 = edges[n+1];
for (let n=0; n<numEdges; n++) {
let pointIndex0 = edges.from[n];
let pointIndex1 = edges.to[n];
let normalDistance = MAX_NODE_SIZE*0.1;

let x0 = nodes.xs[pointIndex0];
Expand Down
9 changes: 6 additions & 3 deletions engine/src/models/Cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ let Cluster = function(config) {
ys: [],
colors: config.nodes.colors.slice()
},
edges: config.edges.slice(),
width: width,
height: height
edges: {
from: config.edges.from.slice(),
to: config.edges.to.slice()
},
width: config.width,
height: config.height
};

// keys are color integers, values are arrays. The arrays contain node indices
Expand Down
Loading

0 comments on commit b548c25

Please sign in to comment.