dc.graph
Version:
Graph visualizations integrated with crossfilter and dc.js
892 lines (867 loc) • 29.6 kB
JavaScript
/*!
* metagraph.js 0.0.7
* http://gordonwoodhull.github.io/metagraph.js/
* Copyright 2019 AT&T Intellectual Property
*
* Licensed under the MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
(function() { function _metagraph() {
;
var metagraph = {
version: '0.0.7'
};
var mg = metagraph;
function as_array(a) {
return !a && [] || (Array.isArray(a) ? a : [a]);
}
function as_keyvalue(o) {
return !o && [] || (Array.isArray(o) ? o : Object.keys(o).map(function(key) {
return {key: key, value: o[key]};
}));
}
function build_map(vals, keyf, wrap) {
return vals.reduce(function(o, val) {
o[keyf(val)] = wrap(val);
return o;
}, {});
}
function graph_options(opts) {
return Object.assign({
nodeKey: function(kv) { return kv.key; },
edgeKey: function(kv) { return kv.key; },
nodeValue: function(kv) { return kv.value; },
edgeValue: function(kv) { return kv.value; },
edgeSource: function(kv) { return kv.value.source; },
edgeTarget: function(kv) { return kv.value.target; }
}, opts || {});
}
metagraph.graph = function(nodes, edges, opts) {
nodes = as_keyvalue(nodes);
edges = as_keyvalue(edges);
var options = graph_options(opts);
var _nodeMap, _edgeMap, _nodesList, _edgesList, _outsList, _insList;
function build_node_map() {
if(_nodeMap)
return;
_nodeMap = build_map(nodes, options.nodeKey, node_wrapper);
}
function build_edge_map() {
if(_edgeMap)
return;
_edgeMap = build_map(edges, options.edgeKey, edge_wrapper);
}
function build_nodes_list() {
if(_nodesList)
return;
build_node_map();
_nodesList = nodes.map(function(v) { return _graph.node(options.nodeKey(v)); });
}
function build_edges_list() {
if(_edgesList)
return;
build_edge_map();
_edgesList = edges.map(function(v) { return _graph.edge(options.edgeKey(v)); });
}
function build_directional_edge_lists(acc) {
build_edge_map();
return edges.reduce(function(o, v) {
var l = o[acc(v)] = o[acc(v)] || [];
l.push(_graph.edge(options.edgeKey(v)));
return o;
}, {});
}
function build_outs_map() {
if(_outsList)
return;
_outsList = build_directional_edge_lists(options.edgeSource);
}
function build_ins_map() {
if(_insList)
return;
_insList = build_directional_edge_lists(options.edgeTarget);
}
function node_wrapper(n) {
return {
value: function() {
return options.nodeValue(n);
},
key: function() {
return options.nodeKey(n);
},
graph: function() {
return _graph;
},
outs: function() {
build_outs_map();
return _outsList[options.nodeKey(n)] || [];
},
ins: function() {
build_ins_map();
return _insList[options.nodeKey(n)] || [];
}
};
}
function edge_wrapper(e) {
return {
value: function() {
return options.edgeValue(e);
},
key: function() {
return options.edgeKey(e);
},
graph: function() {
return _graph;
},
source: function() {
return _graph.node(options.edgeSource(e));
},
target: function() {
return _graph.node(options.edgeTarget(e));
}
};
}
var _graph = {
node: function(key) {
build_node_map();
return _nodeMap[key];
},
edge: function(key) {
build_edge_map();
return _edgeMap[key];
},
nodes: function() {
build_nodes_list();
return _nodesList;
},
edges: function() {
build_edges_list();
return _edgesList;
}
};
return _graph;
};
metagraph.graph_adjacency = metagraph.graph;
function incidence_options(opts) {
var gropts = graph_options(opts);
return Object.assign({
nodeIncidences: n => n && (n.edges || n.ins || n.outs) || [],
incidencesOutward: n => {
var v = gropts.nodeValue(n);
return !v /* doesn't matter */ || !!(v.edges || v.outs);
}
}, gropts);
}
metagraph.graph_incidence = function(nodes, opts) {
nodes = as_keyvalue(nodes);
var options = incidence_options(opts);
var edges = [];
function edge_value(outward, nk, ik) {
return outward ? {
source: nk,
target: ik
} : {
source: ik,
target: nk
};
}
function edge_key(outward, nk, ik) {
return outward ? nk + '-' + ik : ik + '-' + nk;
}
nodes.forEach(function(n) {
var nk = options.nodeKey(n),
outward = options.incidencesOutward(n);
as_array(options.nodeIncidences(options.nodeValue(n)))
.forEach(function(ik) {
edges.push({
key: edge_key(outward, nk, ik),
value: edge_value(outward, nk, ik)
});
});
});
return mg.graph_adjacency(nodes, edges, opts);
};
metagraph.graph_detect = function(spec, opts) {
if(spec.incidences)
return mg.graph_incidence(spec.incidences, opts);
else if(spec.nodes)
return mg.graph_adjacency(spec.nodes, spec.edges, opts);
throw new Error('did not recognize graph format');
};
metagraph.dataflow = function(spec, options) {
var flowgraph = mg.graph_detect(spec, options);
var _flow = {
instantiate: function(instance, inputs) {
var _inst = {
_yes_i_am_really_dataflow: true,
calc: function(id) {
if(!instance[id]) {
var n = flowgraph.node(id);
instance[id] = n.value().calc(_inst).apply(null, n.ins().map(function(e) {
return _inst.calc(e.source().key());
}));
console.assert(instance[id]);
}
return instance[id];
},
input: function(namespace, field) {
var input = inputs[namespace];
if(input._yes_i_am_really_dataflow)
return input.calc(field);
else return input[field];
}
};
return _inst;
}
};
return _flow;
};
/**
* The reason there are so many higher-order functions is that there are five
* stages of a pattern's life:
* - specification - the pattern author specifies a pattern in terms of its dataflow and
* interface. the pattern is parameterized on user-supplied data accessors
* - definition (compilation) - the pattern walks the resulting graph and
* defines the functions that will respond to data
* - instantiation - data is provided to the pattern to create objects
* - binding - if the action needs other indices built, they are built on demand
* and provided to the action before it's run
* - action - responding to user code
**/
metagraph.pattern = function(spec, flowspecs) {
var flowspec = spec.dataflow && mg.graph_detect(spec.dataflow),
interf = mg.graph_detect(spec.interface);
var defn = {node: {}, edge: {}};
interf.nodes().forEach(function(inode) {
defn.node[inode.key()] = {
members: {},
class_members: {}
};
});
function resolve(deps, funfun) {
return function(defn, flow, val) {
var action = funfun(defn, flow, val);
return function() {
return action.apply(null, deps.map(function(dep) {
var parts = dep.split('.');
if(parts.length > 1)
return flow.input(parts[0], parts[1]);
else
return flow.calc(dep);
})).apply(null, arguments);
};
};
}
interf.edges().forEach(function(iedge) {
var ekey = iedge.key(), evalue = iedge.value();
var fs = flowspec || flowspecs[ekey.split('.')[0]];
var action = evalue.member;
if(action && action.funfun) {
var funfun = action.funfun(fs, iedge, flowspecs);
var deps = as_array(evalue.deps);
funfun = resolve(deps, funfun);
defn.node[iedge.source().key()].members[evalue.name] = {defn: funfun};
}
});
interf.nodes().forEach(function(inode) {
var nkey = inode.key(), nvalue = inode.value();
var fs = flowspec || flowspecs[nkey.split('.')[0]];
as_array(inode.value()).forEach(function(spec) {
as_keyvalue(spec.class_members).forEach(function(cmemspec) {
defn.node[nkey].class_members[cmemspec.key] = cmemspec.value(fs, inode);
});
as_keyvalue(spec.members).forEach(function(memspec) {
var mem = memspec.value(fs, inode);
defn.node[nkey].members[memspec.key] = {
accessor: mem.accessor,
defn: mem.defn
};
});
});
defn.node[nkey].wrap = function(flow, val) {
var wrapper = {}, members = defn.node[nkey].members;
Object.keys(members).forEach(function(name) {
wrapper[name] = members[name].defn(defn, flow, val);
});
return wrapper;
};
});
var inodes2 = interf.nodes().map(function(n) {
var n2 = {key: n.key(), value: {}}, class_members = defn.node[n.key()].class_members;
Object.keys(class_members).forEach(function(name) {
n2.value[name] = class_members[name].defn(defn);
});
return n2;
});
var iedges2 = interf.edges().map(function(e) {
var e2 = {
key: e.key(),
value: {
source: e.source().key(),
target: e.target().key()
}
};
});
return mg.graph(inodes2, iedges2);
};
function define_dataflow(flowspec, defn) {
var flownodes = flowspec.nodes().map(function(fsn) {
return {
key: fsn.key(),
value: {
calc: fsn.value().node.calc(fsn)(defn)
}
};
});
return mg.dataflow({
nodes: flownodes,
edges: flowspec.edges().map(e => ({key: e.key(), value: e.value()}))
});
}
metagraph.input = function(path) {
return {
calc: function(fnode) {
path = path || fnode.key();
var parts = path.split('.');
var [namespace, name] = parts.length > 1 ? parts : ['data', path];
return function(defn) {
return function(flow) {
return function() {
return flow.input(namespace, name);
};
};
};
}
};
};
// pass-through
metagraph.output = function(name, namespace) {
return {
calc: function(fnode) {
return function(defn) {
return function(flow) {
return function(x) {
return x;
};
};
};
}
};
};
metagraph.map = function() {
return {
calc: function(fnode) {
var iref = as_array(fnode.value().refs)[0];
return function(defn) {
return function(flow) {
return function(data) {
return build_map(data,
defn.node[iref].members.key.accessor,
defn.node[iref].wrap.bind(null, flow));
};
};
};
}
};
};
metagraph.singleton = function() {
return {
calc: function(fnode) {
return function(defn) {
return function(flow) {
return function() {
throw new Error('singleton not initialized');
};
};
};
}
};
};
metagraph.list = function() {
return {
calc: function(fnode) {
var iref = as_array(fnode.value().refs)[0];
return function(defn) {
return function(flow) {
return function(data, map) {
return data.map(function(val) {
return map[defn.node[iref].members.key.accessor(val)];
});
};
};
};
}
};
};
metagraph.map_of_lists = function(accessor) {
return {
calc: function(fnode) {
return function(defn) {
return function(flow) {
return function(data, map) {
var iref = as_array(fnode.value().refs)[0];
return data.reduce(function(o, v) {
var key = accessor(v);
var list = o[key] = o[key] || [];
list.push(map[defn.node[iref].members.key.accessor(v)]);
return o;
}, {});
};
};
};
}
};
};
metagraph.subset = function() {
return {
calc: function(fnode) {
var iref = as_array(fnode.value().refs)[0];
return function(defn) {
return function(flow) {
return function(items, keys) {
var set = new Set(keys);
return items.filter(function(r) {
return set.has(defn.node[iref].members.key.accessor(r));
});
};
};
};
}
};
};
metagraph.createable = function(flowkey) {
return {
class_members: {
create: function(flowspec, inode) {
return {
defn: function(defn) {
var flowg = define_dataflow(flowspec, defn);
return function(data) {
var env = {};
var flow = flowg.instantiate(env, {data: data});
env[flowkey] = defn.node[inode.key()].wrap(flow, data[inode.key()]);
return env[flowkey];
};
}
};
}
}
};
};
metagraph.call = function(methodname) {
return function(f) {
return {
members: [{
key: methodname,
value: function(flowspec, inode) {
return {
accessor: f,
defn: function(defn, flow, val) {
return function() {
return f(val);
};
}
};
}
}]
};
};
};
metagraph.key = mg.call('key');
metagraph.value = mg.call('value');
// interface edges
metagraph.reference = function(inode) {
return {
reference: inode
};
};
metagraph.fetch = function() {
return {
funfun: function(flowspec, iedge) {
return function(defn, flow) {
return function(x) {
return function() {
return x;
};
};
};
}
};
};
metagraph.lookupArg = function(access) {
return {
funfun: function(flowspec, iedge) {
access = access || (x => x);
return function(defn, flow, val) {
return function(map) {
return function(key) {
return map[access(key)];
};
};
};
}
};
};
metagraph.lookupFVal = function(access) {
return {
funfun: function(flowspec, iedge) {
return function(defn, flow, val) {
return function(map) {
return function() {
return map[access(val)];
};
};
};
}
};
};
metagraph.lookupKVal = function() {
return {
funfun: function(flowspec, iedge) {
return function(defn, flow, val) {
return function(map) {
return function() {
return map[defn.node[iedge.source().key()].members.key.accessor(val)] || [];
};
};
};
}
};
};
metagraph.subgraph = function() {
return {
funfun: function(flowspec, iedge, flowspecs) {
return function(defn, flow, val) {
var subflow = define_dataflow(flowspec, defn), graflow = subflow;
var parts = iedge.target().key().split('.');
if(parts.length > 1) {
var dest = parts[0];
graflow = define_dataflow(flowspecs[dest], defn);
}
return function() {
return function(nodeKeys, edgeKeys, gdata) {
// two environments, one for the sub-pattern and one for the graph pattern
var sgflow = subflow.instantiate({}, {
data: {
nodeKeys: nodeKeys,
edgeKeys: edgeKeys
},
parent: flow});
var genv = {};
var gflow = graflow.instantiate(genv, {
data: sgflow
});
genv.graph = defn.node[iedge.target().key()].wrap(gflow, gdata);
return genv.graph;
};
};
};
}
};
};
metagraph.graph_pattern = function(opts) {
var options = graph_options(opts);
return {
dataflow: {
incidences: {
nodes: {node: mg.input()},
edges: {node: mg.input()},
node_by_key: {
node: mg.map(),
refs: 'Node',
ins: 'nodes'
},
edge_by_key: {
node: mg.map(),
refs: 'Edge',
ins: 'edges'
},
graph: {node: mg.singleton()},
node_list: {
node: mg.list(),
refs: 'Node',
ins: ['nodes', 'node_by_key']
},
edge_list: {
node: mg.list(),
refs: 'Edge',
ins: ['edges', 'edge_by_key']
},
node_outs: {
node: mg.map_of_lists(options.edgeSource),
refs: 'Node',
ins: ['edges', 'edge_by_key']
},
node_ins: {
node: mg.map_of_lists(options.edgeTarget),
refs: 'Node',
ins: ['edges', 'edge_by_key']
}
}
},
interface: {
nodes: {
Graph: mg.createable('graph'),
Node: [mg.key(options.nodeKey), mg.value(options.nodeValue)],
Edge: [mg.key(options.edgeKey), mg.value(options.edgeValue)]
},
edges: {
graph_node: {
name: 'node',
source: 'Graph', target: 'Node',
deps: 'node_by_key',
member: mg.lookupArg()
},
node_graph: {
name: 'graph',
source: 'Node', target: 'Graph',
deps: 'graph',
member: mg.fetch()
},
graph_nodes: {
name: 'nodes',
source: 'Graph', target: 'Node',
deps: 'node_list',
member: mg.fetch()
},
graph_edge: {
name: 'edge',
source: 'Graph', target: 'Edge',
deps: 'edge_by_key',
member: mg.lookupArg()
},
edge_graph: {
name: 'graph',
source: 'Edge', target: 'Graph',
deps: 'graph',
member: mg.fetch()
},
graph_edges: {
name: 'edges',
source: 'Graph', target: 'Edge',
deps: 'edge_list',
member: mg.fetch()
},
edge_source: {
name: 'source',
source: 'Edge', target: 'Node',
deps: 'node_by_key',
member: mg.lookupFVal(options.edgeSource)
},
edge_target: {
name: 'target',
source: 'Edge', target: 'Node',
deps: 'node_by_key',
member: mg.lookupFVal(options.edgeTarget)
},
node_outs: {
name: 'outs',
source: 'Node', target: 'Edge',
deps: 'node_outs',
member: mg.lookupKVal()
},
node_ins: {
name: 'ins',
source: 'Node', target: 'Edge',
deps: 'node_ins',
member: mg.lookupKVal()
}
}
}
};
};
metagraph.subgraph_pattern = function(opts) {
var options = graph_options(opts);
return {
dataflow: {
incidences: {
parent_nodes: {node: mg.input('parent.nodes')},
parent_edges: {node: mg.input('parent.edges')},
node_keys: {node: mg.input('nodeKeys')},
edge_keys: {node: mg.input('edgeKeys')},
subset_nodes: {
node: mg.subset(),
refs: 'child.Node',
ins: ['parent_nodes', 'node_keys']
},
subset_edges: {
node: mg.subset(),
refs: 'child.Edge',
ins: ['parent_edges', 'edge_keys']
},
nodes: {
node: mg.output(),
ins: 'subset_nodes'
},
edges: {
node: mg.output(),
ins: 'subset_edges'
}
}
},
interface: {
nodes: {
ParentGraph: 'parent.Graph',
ChildGraph: 'child.Graph'
},
edges: {
subgraph: {
name: 'subgraph',
source: 'ParentGraph', target: 'ChildGraph',
member: mg.subgraph()
},
subnode: {
name: 'subnode',
source: 'ParentGraph', target: 'ChildGraph',
deps: 'parent.node_by_key',
member: mg.lookupArg()
},
subedge: {
name: 'subedge',
source: 'ParentGraph', target: 'ChildGraph',
deps: 'parent.edge_by_key',
flow: mg.lookupArg()
},
subgraphS: {
name: 'subgraph',
source: 'ChildGraph', target: 'ParentGraph',
member: mg.subgraph()
},
subnodeS: {
name: 'subnode',
source: 'ChildGraph', target: 'ParentGraph',
deps: 'node_by_key', // should be child:
member: mg.lookupArg(x => x.key())
},
subedgeS: {
name: 'subedge',
source: 'ChildGraph', target: 'ParentGraph',
deps: 'edge_by_key', // should be child:
flow: mg.lookupArg(x => x.key())
}
}
}
};
};
metagraph.topological_sort = function(graph) {
// https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
var stacked = {}, marked = {}, sorted = [];
function visit(n) {
if(stacked[n.key()])
throw new Error('not a DAG');
if(!marked[n.key()]) {
stacked[n.key()] = true;
n.outs().forEach(function(e) {
visit(e.target());
});
marked[n.key()] = true;
stacked[n.key()] = false;
sorted.unshift(n);
}
}
var i = 0;
while(Object.keys(marked).length < graph.nodes().length) {
while(marked[graph.nodes()[i].key()]) ++i;
visit(graph.nodes()[i]);
}
return sorted;
};
metagraph.compose = function(composition) {
var sorted = mg.topological_sort(composition);
var built = {}, flowspecs = {};
function input_edge(patnode, name) {
return patnode.ins().find(pe => pe.value().input === name);
}
// resolve dependencies and build patterns
sorted.forEach(function(patnode) {
var flowspec = mg.graph_detect(patnode.value().dataflow);
var fnodes = flowspec.nodes().map(function(fn) {
var v2 = Object.assign({}, fn.value());
v2.refs = as_array(v2.refs).map(function(ref) {
var parts = ref.split('.');
if(parts.length > 1) {
var patedge = input_edge(patnode, parts[0]);
return patedge.source().key() + '.' + parts[1];
}
else return patnode.key() + '.' + parts[0];
});
return {
key: fn.key(),
value: v2
};
});
var fedges = flowspec.edges().map(e => ({key: e.key(), value: e.value()}));
flowspecs[patnode.key()] = mg.graph(fnodes, fedges);
var interf = patnode.value().interface;
built[patnode.key()] = mg.graph_detect({
nodes: interf.nodes,
edges: interf.edges
});
});
// unite patterns
var nodes = [], edges = [], mappings = {};
function lookup(key) {
return mappings[key] || key;
}
sorted.forEach(function(patnode) {
var pattern = built[patnode.key()];
pattern.nodes().forEach(function(inode) {
var key = patnode.key() + '.' + inode.key();
var ref = as_array(inode.value()).find(spec => typeof spec === 'string');
if(ref) {
var parts = ref.split('.');
var patedge = input_edge(patnode, parts[0]);
var key2 = lookup(patedge.source().key() + '.' + parts[1]);
mappings[key] = key2;
}
else nodes.push({
key: key,
value: inode.value()
});
});
pattern.edges().forEach(function(iedge) {
var val2 = Object.assign({}, iedge.value());
val2.source = lookup(patnode.key() + '.' + iedge.source().key());
val2.target = lookup(patnode.key() + '.' + iedge.target().key());
edges.push({
key: patnode.key() + '.' + iedge.key(),
value: val2
});
});
});
return mg.pattern({
interface: {
nodes,
edges
}
}, flowspecs);
};
return metagraph;
}
if (typeof define === 'function' && define.amd) {
define([], _metagraph);
} else if (typeof module == "object" && module.exports) {
module.exports = _metagraph();
} else {
this.metagraph = _metagraph();
}
}
)();
//# sourceMappingURL=metagraph.js.map