dc.graph
Version:
Graph visualizations integrated with crossfilter and dc.js
447 lines (426 loc) • 16.6 kB
JavaScript
dc_graph.expand_collapse = function(options) {
if(typeof options === 'function') {
options = {
get_degree: arguments[0],
expand: arguments[1],
collapse: arguments[2],
dirs: arguments[3]
};
}
var _keyboard, _overNode, _overDir, _overEdge, _expanded = {};
var expanded_highlight_group = dc_graph.register_highlight_things_group(options.expanded_highlight_group || 'expanded-highlight-group');
var collapse_highlight_group = dc_graph.register_highlight_things_group(options.collapse_highlight_group || 'collapse-highlight-group');
var hide_highlight_group = dc_graph.register_highlight_things_group(options.hide_highlight_group || 'hide-highlight-group');
options.dirs = options.dirs || ['both'];
options.dirs.forEach(function(dir) {
_expanded[dir] = {};
});
options.hideKey = options.hideKey || 'Alt';
options.linkKey = options.linkKey || (is_a_mac ? 'Meta' : 'Control');
if(options.dirs.length > 2)
throw new Error('there are only two directions to expand in');
var _gradients_added = {};
function add_gradient_def(color, diagram) {
if(_gradients_added[color])
return;
_gradients_added[color] = true;
diagram.addOrRemoveDef('spike-gradient-' + color, true, 'linearGradient', function(gradient) {
gradient.attr({
x1: '0%',
y1: '0%',
x2: '100%',
y2: '0%',
spreadMethod: 'pad'
});
gradient.selectAll('stop').data([[0, color, 1], [100, color, '0']])
.enter().append('stop').attr({
offset: function(d) {
return d[0] + '%';
},
'stop-color': function(d) {
return d[1];
},
'stop-opacity': function(d) {
return d[2];
}
});
});
}
function visible_edges(diagram, edge, dir, key) {
var fil;
switch(dir) {
case 'out':
fil = function(e) {
return diagram.edgeSource.eval(e) === key;
};
break;
case 'in':
fil = function(e) {
return diagram.edgeTarget.eval(e) === key;
};
break;
case 'both':
fil = function(e) {
return diagram.edgeSource.eval(e) === key || diagram.edgeTarget.eval(e) === key;
};
break;
}
return edge.filter(fil).data();
}
function spike_directioner(rankdir, dir, N) {
if(dir==='both')
return function(i) {
return Math.PI * (2 * i / N - 0.5);
};
else {
var sweep = (N-1)*Math.PI/N, ofs;
switch(rankdir) {
case 'LR':
ofs = 0;
break;
case 'TB':
ofs = Math.PI/2;
break;
case 'RL':
ofs = Math.PI;
break;
case 'BT':
ofs = -Math.PI/2;
break;
}
if(dir === 'in')
ofs += Math.PI;
return function(i) {
return ofs + sweep * (-.5 + (N > 1 ? i / (N-1) : 0)); // avoid 0/0
};
}
}
function draw_stubs(diagram, node, edge, n, spikes) {
if(n && _expanded[spikes.dir][diagram.nodeKey.eval(n)])
spikes = null;
var spike = node
.selectAll('g.spikes')
.data(function(n2) {
return spikes && n === n2 ?
[n2] : [];
});
spike.exit().remove();
spike
.enter().insert('g', ':first-child')
.classed('spikes', true);
var rect = spike
.selectAll('rect.spike')
.data(function(n) {
var key = diagram.nodeKey.eval(n);
var dir = spikes.dir,
N = spikes.n,
af = spike_directioner(diagram.layoutEngine().rankdir(), dir, N),
ret = Array(N);
for(var i = 0; i<N; ++i) {
var a = af(i);
ret[i] = {
a: a * 180 / Math.PI,
x: Math.cos(a) * n.dcg_rx*.9,
y: Math.sin(a) * n.dcg_ry*.9,
edge: spikes.invisible ? spikes.invisible[i] : null
};
}
return ret;
});
rect
.enter().append('rect')
.classed('spike', true)
.attr({
width: 25,
height: 3,
fill: function(s) {
var color = s.edge ? dc_graph.functor_wrap(diagram.edgeStroke())(s.edge) : 'black';
add_gradient_def(color, diagram);
return 'url(#spike-gradient-' + color + ')';
},
rx: 1,
ry: 1,
x: 0,
y: 0
});
rect.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ') rotate(' + d.a + ')';
});
rect.exit().remove();
}
function clear_stubs(diagram, node, edge) {
draw_stubs(diagram, node, edge, null, null);
}
function zonedir(diagram, event, dirs, n) {
if(dirs.length === 1) // we assume it's ['out', 'in']
return dirs[0];
var bound = diagram.root().node().getBoundingClientRect();
var invert = diagram.invertCoord([event.clientX - bound.left,event.clientY - bound.top]),
x = invert[0],
y = invert[1];
switch(diagram.layoutEngine().rankdir()) {
case 'TB':
return y > n.cola.y ? 'out' : 'in';
case 'BT':
return y < n.cola.y ? 'out' : 'in';
case 'LR':
return x > n.cola.x ? 'out' : 'in';
case 'RL':
return x < n.cola.x ? 'out' : 'in';
}
throw new Error('unknown rankdir ' + diagram.layoutEngine().rankdir());
}
function detect_key(key) {
switch(key) {
case 'Alt':
return d3.event.altKey;
case 'Meta':
return d3.event.metaKey;
case 'Shift':
return d3.event.shiftKey;
case 'Control':
return d3.event.ctrlKey;
}
return false;
}
function highlight_hiding_node(diagram, n, edge) {
var nk = diagram.nodeKey.eval(n);
var hide_nodes_set = {}, hide_edges_set = {};
hide_nodes_set[nk] = true;
edge.each(function(e) {
if(diagram.edgeSource.eval(e) === nk || diagram.edgeTarget.eval(e) === nk)
hide_edges_set[diagram.edgeKey.eval(e)] = true;
});
hide_highlight_group.highlight(hide_nodes_set, hide_edges_set);
}
function highlight_hiding_edge(diagram, e) {
var hide_edges_set = {};
hide_edges_set[diagram.edgeKey.eval(e)] = true;
hide_highlight_group.highlight({}, hide_edges_set);
}
function highlight_collapse(diagram, n, node, edge, dir) {
var nk = diagram.nodeKey.eval(n);
var p;
if(options.get_edges)
p = Promise.resolve(options.get_edges(nk, dir));
else
p = Promise.resolve(options.get_degree(nk, dir));
p.then(function(de) {
var degree, edges;
if(typeof de === 'number')
degree = de;
else {
edges = de;
degree = edges.length;
}
var spikes = {
dir: dir,
visible: visible_edges(diagram, edge, dir, nk)
};
spikes.n = Math.max(0, degree - spikes.visible.length); // be tolerant of inconsistencies
if(edges) {
var shown = spikes.visible.reduce(function(p, e) {
p[diagram.edgeKey.eval(e)] = true;
return p;
}, {});
spikes.invisible = edges.filter(function(e) { return !shown[diagram.edgeKey()(e)]; });
}
draw_stubs(diagram, node, edge, n, spikes);
var collapse_nodes_set = {}, collapse_edges_set = {};
if(_expanded[dir][nk] && options.collapsibles) {
var clps = options.collapsibles(nk, dir);
collapse_nodes_set = clps.nodes;
collapse_edges_set = clps.edges;
}
collapse_highlight_group.highlight(collapse_nodes_set, collapse_edges_set);
});
}
function draw(diagram, node, edge, ehover) {
function over_node(n) {
var dir = zonedir(diagram, d3.event, options.dirs, n);
_overNode = n;
_overDir = dir;
if(options.hideNode && detect_key(options.hideKey))
highlight_hiding_node(diagram, n, edge);
else if(_mode.nodeURL.eval(_overNode) && detect_key(options.linkKey)) {
diagram.selectAllNodes()
.filter(function(n) {
return n === _overNode;
}).attr('cursor', 'pointer');
diagram.requestRefresh(0);
}
else
highlight_collapse(diagram, n, node, edge, dir);
}
function leave_node(n) {
diagram.selectAllNodes()
.filter(function(n) {
return n === _overNode;
}).attr('cursor', null);
_overNode = null;
clear_stubs(diagram, node, edge);
collapse_highlight_group.highlight({}, {});
hide_highlight_group.highlight({}, {});
}
function click_node(n) {
var nk = diagram.nodeKey.eval(n);
if(options.hideNode && detect_key(options.hideKey))
options.hideNode(nk);
else if(detect_key(options.linkKey)) {
if(_mode.nodeURL.eval(n) && _mode.urlOpener)
_mode.urlOpener()(_mode, n, _mode.nodeURL.eval(n));
} else {
clear_stubs(diagram, node, edge);
var dir = zonedir(diagram, d3.event, options.dirs, n);
expand(dir, nk, !_expanded[dir][nk]);
}
}
function enter_edge(e) {
_overEdge = e;
if(options.hideEdge && detect_key(options.hideKey))
highlight_hiding_edge(diagram, e);
}
function leave_edge(e) {
_overEdge = null;
hide_highlight_group.highlight({}, {});
}
function click_edge(e) {
if(options.hideEdge && detect_key(options.hideKey))
options.hideEdge(diagram.edgeKey.eval(e));
}
node
.on('mouseenter.expand-collapse', over_node)
.on('mousemove.expand-collapse', over_node)
.on('mouseout.expand-collapse', leave_node)
.on('click.expand-collapse', click_node)
.on('dblclick.expand-collapse', click_node);
ehover
.on('mouseenter.expand-collapse', enter_edge)
.on('mouseout.expand-collapse', leave_edge)
.on('click.expand-collapse', click_edge);
_keyboard
.on('keydown.expand-collapse', function() {
if(d3.event.key === options.hideKey && (_overNode && options.hideNode || _overEdge && options.hideEdge)) {
if(_overNode)
highlight_hiding_node(diagram, _overNode, edge);
if(_overEdge)
highlight_hiding_edge(diagram, _overEdge);
clear_stubs(diagram, node, edge);
collapse_highlight_group.highlight({}, {});
}
else if(d3.event.key === options.linkKey && _overNode) {
if(_overNode && _mode.nodeURL.eval(_overNode)) {
diagram.selectAllNodes()
.filter(function(n) {
return n === _overNode;
}).attr('cursor', 'pointer');
}
hide_highlight_group.highlight({}, {});
clear_stubs(diagram, node, edge);
collapse_highlight_group.highlight({}, {});
}
})
.on('keyup.expand_collapse', function() {
if((d3.event.key === options.hideKey || d3.event.key === options.linkKey) && (_overNode || _overEdge)) {
hide_highlight_group.highlight({}, {});
if(_overNode) {
highlight_collapse(diagram, _overNode, node, edge, _overDir);
if(_mode.nodeURL.eval(_overNode)) {
diagram.selectAllNodes()
.filter(function(n) {
return n === _overNode;
}).attr('cursor', null);
}
}
}
});
diagram.cascade(97, true, conditional_properties(
function(n) {
return n === _overNode && n.orig.value.value && n.orig.value.value.URL;
},
{
nodeLabelDecoration: 'underline'
}
));
}
function remove(diagram, node, edge, ehover) {
node
.on('mouseenter.expand-collapse', null)
.on('mousemove.expand-collapse', null)
.on('mouseout.expand-collapse', null)
.on('click.expand-collapse', null)
.on('dblclick.expand-collapse', null);
ehover
.on('mouseenter.expand-collapse', null)
.on('mouseout.expand-collapse', null)
.on('click.expand-collapse', null);
clear_stubs(diagram, node, edge);
}
function expand(dir, nk, whether) {
if(dir === 'both' && !_expanded.both)
options.dirs.forEach(function(dir2) {
_expanded[dir2][nk] = whether;
});
else
_expanded[dir][nk] = whether;
var bothmap;
if(_expanded.both)
bothmap = _expanded.both;
else {
bothmap = Object.keys(_expanded.in).filter(function(nk2) {
return _expanded.in[nk2] && _expanded.out[nk2];
}).reduce(function(p, v) {
p[v] = true;
return p;
}, {});
}
expanded_highlight_group.highlight(bothmap, {});
if(dir === 'both' && !_expanded.both)
options.dirs.forEach(function(dir2, i) {
if(whether)
options.expand(nk, dir2, i !== options.dirs.length-1);
else
options.collapse(nk, dir2, i !== options.dirs.length-1);
});
else {
if(whether)
options.expand(nk, dir);
else
options.collapse(nk, dir);
}
}
function expandNodes(nks) {
var map = nks.reduce(function(p, v) {
p[v] = true;
return p;
}, {});
options.dirs.forEach(function(dir) {
_expanded[dir] = Object.assign({}, map);
});
expanded_highlight_group.highlight(map, {});
options.expandedNodes(map);
}
var _mode = dc_graph.mode('expand-collapse', {
draw: draw,
remove: remove,
parent: function(p) {
if(p) {
_keyboard = p.child('keyboard');
if(!_keyboard)
p.child('keyboard', _keyboard = dc_graph.keyboard());
}
}
});
_mode.expand = expand;
_mode.expandNodes = expandNodes;
_mode.clickableLinks = deprecated_property("warning - clickableLinks doesn't belong in collapse_expand and will be moved", false);
_mode.nodeURL = property(function(n) {
return n.value && n.value.value && n.value.value.URL;
});
_mode.urlTargetWindow = property('dcgraphlink');
_mode.urlOpener = property(dc_graph.expand_collapse.default_url_opener);
return _mode;
};
dc_graph.expand_collapse.default_url_opener = function(mode, node, url) {
window.open(mode.nodeURL.eval(node), mode.urlTargetWindow());
};