dc.graph
Version:
Graph visualizations integrated with crossfilter and dc.js
230 lines (214 loc) • 7.36 kB
JavaScript
function process_dot(callback, error, text) {
if(error) {
callback(error, null);
return;
}
var nodes, edges, node_cluster = {}, clusters = [];
if(graphlibDot.parse) { // graphlib-dot 1.1.0 (where did i get it from?)
var digraph = graphlibDot.parse(text);
var nodeNames = digraph.nodes();
nodes = new Array(nodeNames.length);
nodeNames.forEach(function (name, i) {
var node = nodes[i] = digraph._nodes[nodeNames[i]];
node.id = i;
node.name = name;
});
var edgeNames = digraph.edges();
edges = [];
edgeNames.forEach(function(e) {
var edge = digraph._edges[e];
edges.push(Object.assign({}, edge.value, {
source: digraph._nodes[edge.u].id,
target: digraph._nodes[edge.v].id,
sourcename: edge.u,
targetname: edge.v
}));
});
// TODO: if this version exists in the wild, look at how it does subgraphs/clusters
} else { // graphlib-dot 0.6
digraph = graphlibDot.read(text);
nodeNames = digraph.nodes();
nodes = new Array(nodeNames.length);
nodeNames.forEach(function (name, i) {
var node = nodes[i] = digraph._nodes[nodeNames[i]];
node.id = i;
node.name = name;
});
edges = [];
digraph.edges().forEach(function(e) {
edges.push(Object.assign({}, digraph.edge(e.v, e.w), {
source: digraph._nodes[e.v].id,
target: digraph._nodes[e.w].id,
sourcename: e.v,
targetname: e.w
}));
});
// iterative bfs for variety (recursion would work just as well)
var cluster_names = {};
var queue = digraph.children().map(function(c) { return Object.assign({parent: null, key: c}, digraph.node(c)); });
while(queue.length) {
var item = queue.shift(),
children = digraph.children(item.key);
if(children.length) {
clusters.push(item);
cluster_names[item.key] = true;
}
else
node_cluster[item.key] = item.parent;
queue = queue.concat(children.map(function(c) { return {parent: item.key, key: c}; }));
}
// clusters as nodes not currently supported
nodes = nodes.filter(function(n) {
return !cluster_names[n.name];
});
}
var graph = {nodes: nodes, links: edges, node_cluster: node_cluster, clusters: clusters};
callback(null, graph);
}
function process_dsv(callback, error, data) {
if(error) {
callback(error, null);
return;
}
var keys = Object.keys(data[0]);
var source = keys[0], target = keys[1];
var nodes = d3.set(data.map(function(r) { return r[source]; }));
data.forEach(function(r) {
nodes.add(r[target]);
});
nodes = nodes.values().map(function(k) { return {name: k}; });
callback(null, {
nodes: nodes,
links: data.map(function(r, i) {
return {
key: i,
sourcename: r[source],
targetname: r[target]
};
})
});
}
dc_graph.file_formats = [
{
exts: 'json',
mimes: 'application/json',
from_url: d3.json,
from_text: function(text, callback) {
callback(null, JSON.parse(text));
}
},
{
exts: ['gv', 'dot'],
mimes: 'text/vnd.graphviz',
from_url: function(url, callback) {
d3.text(url, process_dot.bind(null, callback));
},
from_text: function(text, callback) {
process_dot(callback, null, text);
}
},
{
exts: 'psv',
mimes: 'text/psv',
from_url: function(url, callback) {
d3.dsv('|', 'text/plain')(url, process_dsv.bind(null, callback));
},
from_text: function(text, callback) {
process_dsv(callback, null, d3.dsv('|').parse(text));
}
},
{
exts: 'csv',
mimes: 'text/csv',
from_url: function(url, callback) {
d3.csv(url, process_dsv.bind(null, callback));
},
from_text: function(text, callback) {
process_dsv(callback, null, d3.csv.parse(text));
}
}
];
dc_graph.match_file_format = function(filename) {
return dc_graph.file_formats.find(function(format) {
var exts = format.exts;
if(!Array.isArray(exts))
exts = [exts];
return exts.find(function(ext) {
return new RegExp('\.' + ext + '$').test(filename);
});
});
};
dc_graph.match_mime_type = function(mime) {
return dc_graph.file_formats.find(function(format) {
var mimes = format.mimes;
if(!Array.isArray(mimes))
mimes = [mimes];
return mimes.includes(mime);
});
};
function unknown_format_error(filename) {
var spl = filename.split('.');
if(spl.length)
return new Error('do not know how to process graph file extension ' + spl[spl.length-1]);
else
return new Error('need file extension to process graph file automatically, filename ' + filename);
}
function unknown_mime_error(mime) {
return new Error('do not know how to process mime type ' + mime);
}
// load a graph from various formats and return the data in consistent {nodes, links} format
dc_graph.load_graph = function() {
// ignore any query parameters for checking extension
function ignore_query(file) {
if(!file)
return null;
return file.replace(/\?.*/, '');
}
var file1, file2, callback;
file1 = arguments[0];
if(arguments.length===3) {
file2 = arguments[1];
callback = arguments[2];
}
else if(arguments.length===2) {
callback = arguments[1];
}
else throw new Error('need two or three arguments');
if(file2) {
// this is not general - really titan-specific
queue()
.defer(d3.json, file1)
.defer(d3.json, file2)
.await(function(error, nodes, edges) {
if(error)
callback(error, null);
else
callback(null, {nodes: nodes.results, edges: edges.results});
});
}
else {
var format;
if(/^data:/.test(file1)) {
var parts = file1.slice(5).split(/,(.+)/);
format = dc_graph.match_mime_type(parts[0]);
if(format)
format.from_text(parts[1], callback);
else callback(unknown_mime_error(parts[0]));
} else {
var file1noq = ignore_query(file1);
format = dc_graph.match_file_format(file1noq);
if(format)
format.from_url(file1, callback);
else callback(unknown_format_error(file1noq));
}
}
};
dc_graph.load_graph_text = function(text, filename, callback) {
var format = dc_graph.match_file_format(filename);
if(format)
format.from_text(text, callback);
else callback(unknown_format_error(filename));
};
dc_graph.data_url = function(data) {
return 'data:application/json,' + JSON.stringify(data);
};