Browse Source

Implemented subgraphs in the DOT parser

css_transitions
josdejong 11 years ago
parent
commit
b0dda31e6d
4 changed files with 399 additions and 180 deletions
  1. +183
    -78
      src/graph/dotparser.js
  2. +30
    -21
      test/dotparser.js
  3. +183
    -78
      vis.js
  4. +3
    -3
      vis.min.js

+ 183
- 78
src/graph/dotparser.js View File

@ -187,6 +187,30 @@
} }
} }
/**
* Create an edge to a graph object
* @param {Object} graph
* @param {String | Number | Object} from
* @param {String | Number | Object} to
* @param {String} type
* @param {Object | null} attr
* @return {Object} edge
*/
function createEdge(graph, from, to, type, attr) {
var edge = {
from: from,
to: to,
type: type
};
if (graph.edge) {
edge.attr = merge({}, graph.edge); // clone global attributes
}
edge.attr = merge(edge.attr || {}, attr); // merge attributes
return edge;
}
/** /**
* Get next token in the current dot file. * Get next token in the current dot file.
* The token and token type are available as token and tokenType * The token and token type are available as token and tokenType
@ -345,7 +369,7 @@
getToken(); getToken();
} }
// graph id
// optional graph id
if (tokenType == TOKENTYPE.IDENTIFIER) { if (tokenType == TOKENTYPE.IDENTIFIER) {
graph.id = token; graph.id = token;
getToken(); getToken();
@ -385,10 +409,6 @@
*/ */
function parseStatements (graph) { function parseStatements (graph) {
while (token !== '' && token != '}') { while (token !== '' && token != '}') {
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Identifier expected');
}
parseStatement(graph); parseStatement(graph);
if (token == ';') { if (token == ';') {
getToken(); getToken();
@ -403,7 +423,14 @@
* @param {Object} graph * @param {Object} graph
*/ */
function parseStatement(graph) { function parseStatement(graph) {
// TODO: parse subgraph
// parse subgraph
var subgraph = parseSubgraph(graph);
if (subgraph) {
// edge statements
parseEdge(graph, subgraph);
return;
}
// parse an attribute statement // parse an attribute statement
var attr = parseAttributeStatement(graph); var attr = parseAttributeStatement(graph);
@ -418,14 +445,75 @@
if (token == '=') { if (token == '=') {
// id statement // id statement
getToken(); getToken();
graph[id] = token;
if (!graph.attr) {
graph.attr = {};
}
graph.attr[id] = token;
getToken(); getToken();
// TODO: implement comma separated list with "ID=ID"
} }
else { else {
parseNodeStatement(graph, id); parseNodeStatement(graph, id);
} }
} }
/**
* Parse a subgraph
* @param {Object} graph parent graph object
* @return {Object | null} subgraph
*/
function parseSubgraph (graph) {
var subgraph = null;
// optional subgraph keyword
if (token == 'subgraph') {
subgraph = {};
subgraph.type = token;
getToken();
// optional graph id
if (tokenType == TOKENTYPE.IDENTIFIER) {
subgraph.id = token;
getToken();
}
}
// open angle bracket
if (token == '{') {
getToken();
if (!subgraph) {
subgraph = {
type: 'subgraph'
};
}
// TODO: copy global node and edge attributes into subgraph?
// statements
parseStatements(subgraph);
// close angle bracket
if (token != '}') {
throw newSyntaxError('Angle bracket } expected');
}
getToken();
// remove temporary global properties
delete subgraph.node;
delete subgraph.edge;
// register at the parent graph
if (!graph.subgraphs) {
graph.subgraphs = [];
}
graph.subgraphs.push(subgraph);
graph.nodes = (graph.nodes || []).concat(subgraph.nodes || []);
}
return subgraph;
}
/** /**
* parse an attribute statement like "node [shape=circle fontSize=16]". * parse an attribute statement like "node [shape=circle fontSize=16]".
* Available keywords are 'node', 'edge', 'graph' * Available keywords are 'node', 'edge', 'graph'
@ -494,89 +582,29 @@
*/ */
function parseEdge(graph, from) { function parseEdge(graph, from) {
while (token == '->' || token == '--') { while (token == '->' || token == '--') {
var to;
var type = token; var type = token;
getToken(); getToken();
if (token == '{') {
// parse a set of nodes, like "node1 -> {node2, node3}"
parseEdgeSet(graph, from, type);
break;
var subgraph = parseSubgraph(graph);
if (subgraph) {
to = subgraph;
} }
else { else {
// parse a single edge, like "node1 -> node2 -> node3"
var to = token;
to = token;
addNode(graph, { addNode(graph, {
id: to id: to
}); });
getToken(); getToken();
var attr = parseAttributeList(); var attr = parseAttributeList();
// create edge
var edge = {
from: from,
to: to,
type: type
};
if (attr) {
edge.attr = attr;
}
addEdge(graph, edge);
from = to;
} }
}
}
/**
* Parse a set of nodes, like "{node1; node2; node3}"
* @param {Object} graph
* @param {String | Number} from Id of the from node
* @param {String} type Edge type, '--' or '->'
* @return {Node[] | null} nodes
*/
function parseEdgeSet(graph, from, type) {
var nodes = null;
// create edge
var edge = createEdge(graph, from, to, type, attr);
addEdge(graph, edge);
if (token == '{') {
getToken();
while (token !== '' && token != '}') {
// create to node
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Identifier expected');
}
var to = token;
addNode(graph, {
id: to
});
getToken();
// create edge
var edge = {
from: from,
to: to,
type: type
};
var attr = parseAttributeList();
if (attr) {
edge.attr = attr;
}
addEdge(graph, edge);
// separator
if (token == ';') {
getToken();
}
}
// closing bracket
if (token != '}') {
throw newSyntaxError('bracket } expected');
}
getToken();
from = to;
} }
return nodes;
} }
/** /**
@ -642,6 +670,37 @@
return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
} }
/**
* Execute a function fn for each pair of elements in two arrays
* @param {Array | *} array1
* @param {Array | *} array2
* @param {function} fn
*/
function forEach2(array1, array2, fn) {
if (array1 instanceof Array) {
array1.forEach(function (elem1) {
if (array2 instanceof Array) {
array2.forEach(function (elem2) {
fn(elem1, elem2);
});
}
else {
fn(elem1, array2);
}
});
}
else {
if (array2 instanceof Array) {
array2.forEach(function (elem2) {
fn(array1, elem2);
});
}
else {
fn(array1, array2);
}
}
}
/** /**
* Convert a string containing a graph in DOT language into a map containing * Convert a string containing a graph in DOT language into a map containing
* with nodes and edges in the format of graph. * with nodes and edges in the format of graph.
@ -674,14 +733,60 @@
// copy the edges // copy the edges
if (dotData.edges) { if (dotData.edges) {
dotData.edges.forEach(function (dotEdge) {
/**
* Convert an edge in DOT format to an edge with VisGraph format
* @param {Object} dotEdge
* @returns {Object} graphEdge
*/
function convertEdge(dotEdge) {
var graphEdge = { var graphEdge = {
from: dotEdge.from, from: dotEdge.from,
to: dotEdge.to to: dotEdge.to
}; };
merge(graphEdge, dotEdge.attr); merge(graphEdge, dotEdge.attr);
graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
graphData.edges.push(graphEdge);
return graphEdge;
}
dotData.edges.forEach(function (dotEdge) {
var from, to;
if (dotEdge.from instanceof Object) {
from = dotEdge.from.nodes;
}
else {
from = {
id: dotEdge.from
}
}
if (dotEdge.to instanceof Object) {
to = dotEdge.to.nodes;
}
else {
to = {
id: dotEdge.to
}
}
if (dotEdge.from instanceof Object && dotEdge.from.edges) {
dotEdge.from.edges.forEach(function (subEdge) {
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
forEach2(from, to, function (from, to) {
var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
if (dotEdge.to instanceof Object && dotEdge.to.edges) {
dotEdge.to.edges.forEach(function (subEdge) {
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
}); });
} }

+ 30
- 21
test/dotparser.js View File

@ -10,10 +10,10 @@ fs.readFile('test/dot.txt', function (err, data) {
assert.deepEqual(graph, { assert.deepEqual(graph, {
"type": "digraph", "type": "digraph",
"id": "test_graph", "id": "test_graph",
"rankdir": "LR",
"size": "8,5",
"font": "arial",
"attr": { "attr": {
"rankdir": "LR",
"size": "8,5",
"font": "arial",
"attr1": "another\" attr" "attr1": "another\" attr"
}, },
"nodes": [ "nodes": [
@ -63,16 +63,10 @@ fs.readFile('test/dot.txt', function (err, data) {
} }
}, },
{ {
"id": "B",
"attr": {
"shape": "circle"
}
"id": "B"
}, },
{ {
"id": "C",
"attr": {
"shape": "circle"
}
"id": "C"
} }
], ],
"edges": [ "edges": [
@ -136,22 +130,37 @@ fs.readFile('test/dot.txt', function (err, data) {
}, },
{ {
"from": "A", "from": "A",
"to": "B",
"type": "->",
"attr": {
"length": 170,
"fontSize": 12
}
},
{
"from": "A",
"to": "C",
"to": {
"type": "subgraph",
"nodes": [
{
"id": "B"
},
{
"id": "C"
}
]
},
"type": "->", "type": "->",
"attr": { "attr": {
"length": 170, "length": 170,
"fontSize": 12 "fontSize": 12
} }
} }
],
"subgraphs" : [
{
"type": "subgraph",
"nodes": [
{
"id": "B"
},
{
"id": "C"
}
]
}
] ]
}); });
}); });

+ 183
- 78
vis.js View File

@ -6995,6 +6995,30 @@ Timeline.prototype.getItemRange = function getItemRange() {
} }
} }
/**
* Create an edge to a graph object
* @param {Object} graph
* @param {String | Number | Object} from
* @param {String | Number | Object} to
* @param {String} type
* @param {Object | null} attr
* @return {Object} edge
*/
function createEdge(graph, from, to, type, attr) {
var edge = {
from: from,
to: to,
type: type
};
if (graph.edge) {
edge.attr = merge({}, graph.edge); // clone global attributes
}
edge.attr = merge(edge.attr || {}, attr); // merge attributes
return edge;
}
/** /**
* Get next token in the current dot file. * Get next token in the current dot file.
* The token and token type are available as token and tokenType * The token and token type are available as token and tokenType
@ -7153,7 +7177,7 @@ Timeline.prototype.getItemRange = function getItemRange() {
getToken(); getToken();
} }
// graph id
// optional graph id
if (tokenType == TOKENTYPE.IDENTIFIER) { if (tokenType == TOKENTYPE.IDENTIFIER) {
graph.id = token; graph.id = token;
getToken(); getToken();
@ -7193,10 +7217,6 @@ Timeline.prototype.getItemRange = function getItemRange() {
*/ */
function parseStatements (graph) { function parseStatements (graph) {
while (token !== '' && token != '}') { while (token !== '' && token != '}') {
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Identifier expected');
}
parseStatement(graph); parseStatement(graph);
if (token == ';') { if (token == ';') {
getToken(); getToken();
@ -7211,7 +7231,14 @@ Timeline.prototype.getItemRange = function getItemRange() {
* @param {Object} graph * @param {Object} graph
*/ */
function parseStatement(graph) { function parseStatement(graph) {
// TODO: parse subgraph
// parse subgraph
var subgraph = parseSubgraph(graph);
if (subgraph) {
// edge statements
parseEdge(graph, subgraph);
return;
}
// parse an attribute statement // parse an attribute statement
var attr = parseAttributeStatement(graph); var attr = parseAttributeStatement(graph);
@ -7226,14 +7253,75 @@ Timeline.prototype.getItemRange = function getItemRange() {
if (token == '=') { if (token == '=') {
// id statement // id statement
getToken(); getToken();
graph[id] = token;
if (!graph.attr) {
graph.attr = {};
}
graph.attr[id] = token;
getToken(); getToken();
// TODO: implement comma separated list with "ID=ID"
} }
else { else {
parseNodeStatement(graph, id); parseNodeStatement(graph, id);
} }
} }
/**
* Parse a subgraph
* @param {Object} graph parent graph object
* @return {Object | null} subgraph
*/
function parseSubgraph (graph) {
var subgraph = null;
// optional subgraph keyword
if (token == 'subgraph') {
subgraph = {};
subgraph.type = token;
getToken();
// optional graph id
if (tokenType == TOKENTYPE.IDENTIFIER) {
subgraph.id = token;
getToken();
}
}
// open angle bracket
if (token == '{') {
getToken();
if (!subgraph) {
subgraph = {
type: 'subgraph'
};
}
// TODO: copy global node and edge attributes into subgraph?
// statements
parseStatements(subgraph);
// close angle bracket
if (token != '}') {
throw newSyntaxError('Angle bracket } expected');
}
getToken();
// remove temporary global properties
delete subgraph.node;
delete subgraph.edge;
// register at the parent graph
if (!graph.subgraphs) {
graph.subgraphs = [];
}
graph.subgraphs.push(subgraph);
graph.nodes = (graph.nodes || []).concat(subgraph.nodes || []);
}
return subgraph;
}
/** /**
* parse an attribute statement like "node [shape=circle fontSize=16]". * parse an attribute statement like "node [shape=circle fontSize=16]".
* Available keywords are 'node', 'edge', 'graph' * Available keywords are 'node', 'edge', 'graph'
@ -7302,89 +7390,29 @@ Timeline.prototype.getItemRange = function getItemRange() {
*/ */
function parseEdge(graph, from) { function parseEdge(graph, from) {
while (token == '->' || token == '--') { while (token == '->' || token == '--') {
var to;
var type = token; var type = token;
getToken(); getToken();
if (token == '{') {
// parse a set of nodes, like "node1 -> {node2, node3}"
parseEdgeSet(graph, from, type);
break;
var subgraph = parseSubgraph(graph);
if (subgraph) {
to = subgraph;
} }
else { else {
// parse a single edge, like "node1 -> node2 -> node3"
var to = token;
to = token;
addNode(graph, { addNode(graph, {
id: to id: to
}); });
getToken(); getToken();
var attr = parseAttributeList(); var attr = parseAttributeList();
// create edge
var edge = {
from: from,
to: to,
type: type
};
if (attr) {
edge.attr = attr;
}
addEdge(graph, edge);
from = to;
} }
}
}
/**
* Parse a set of nodes, like "{node1; node2; node3}"
* @param {Object} graph
* @param {String | Number} from Id of the from node
* @param {String} type Edge type, '--' or '->'
* @return {Node[] | null} nodes
*/
function parseEdgeSet(graph, from, type) {
var nodes = null;
if (token == '{') {
getToken();
while (token !== '' && token != '}') {
// create to node
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Identifier expected');
}
var to = token;
addNode(graph, {
id: to
});
getToken();
// create edge
var edge = createEdge(graph, from, to, type, attr);
addEdge(graph, edge);
// create edge
var edge = {
from: from,
to: to,
type: type
};
var attr = parseAttributeList();
if (attr) {
edge.attr = attr;
}
addEdge(graph, edge);
// separator
if (token == ';') {
getToken();
}
}
// closing bracket
if (token != '}') {
throw newSyntaxError('bracket } expected');
}
getToken();
from = to;
} }
return nodes;
} }
/** /**
@ -7450,6 +7478,37 @@ Timeline.prototype.getItemRange = function getItemRange() {
return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
} }
/**
* Execute a function fn for each pair of elements in two arrays
* @param {Array | *} array1
* @param {Array | *} array2
* @param {function} fn
*/
function forEach2(array1, array2, fn) {
if (array1 instanceof Array) {
array1.forEach(function (elem1) {
if (array2 instanceof Array) {
array2.forEach(function (elem2) {
fn(elem1, elem2);
});
}
else {
fn(elem1, array2);
}
});
}
else {
if (array2 instanceof Array) {
array2.forEach(function (elem2) {
fn(array1, elem2);
});
}
else {
fn(array1, array2);
}
}
}
/** /**
* Convert a string containing a graph in DOT language into a map containing * Convert a string containing a graph in DOT language into a map containing
* with nodes and edges in the format of graph. * with nodes and edges in the format of graph.
@ -7482,14 +7541,60 @@ Timeline.prototype.getItemRange = function getItemRange() {
// copy the edges // copy the edges
if (dotData.edges) { if (dotData.edges) {
dotData.edges.forEach(function (dotEdge) {
/**
* Convert an edge in DOT format to an edge with VisGraph format
* @param {Object} dotEdge
* @returns {Object} graphEdge
*/
function convertEdge(dotEdge) {
var graphEdge = { var graphEdge = {
from: dotEdge.from, from: dotEdge.from,
to: dotEdge.to to: dotEdge.to
}; };
merge(graphEdge, dotEdge.attr); merge(graphEdge, dotEdge.attr);
graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
graphData.edges.push(graphEdge);
return graphEdge;
}
dotData.edges.forEach(function (dotEdge) {
var from, to;
if (dotEdge.from instanceof Object) {
from = dotEdge.from.nodes;
}
else {
from = {
id: dotEdge.from
}
}
if (dotEdge.to instanceof Object) {
to = dotEdge.to.nodes;
}
else {
to = {
id: dotEdge.to
}
}
if (dotEdge.from instanceof Object && dotEdge.from.edges) {
dotEdge.from.edges.forEach(function (subEdge) {
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
forEach2(from, to, function (from, to) {
var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
if (dotEdge.to instanceof Object && dotEdge.to.edges) {
dotEdge.to.edges.forEach(function (subEdge) {
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
}); });
} }

+ 3
- 3
vis.min.js
File diff suppressed because it is too large
View File


Loading…
Cancel
Save