/**
* Asynchronous [d3.tip](https://github.com/Caged/d3-tip) support for dc.graph.js
*
* Add tooltips to the nodes and edges of a graph using an asynchronous callback to get
* the html to show.
*
* Optional - requires separately loading the d3.tip script and CSS (which are included in
* dc.graph.js in `web/js/d3-tip/index.js` and `web/css/d3-tip/example-styles.css`)
*
* @class tip
* @memberof dc_graph
* @return {Object}
**/
dc_graph.tip = function(options) {
options = options || {};
var _namespace = options.namespace || 'tip';
var _d3tip = null;
var _showTimeout, _hideTimeout;
var _dispatch = d3.dispatch('tipped');
function init(parent) {
if(!_d3tip) {
_d3tip = d3.tip()
.attr('class', options.class || 'd3-tip')
.html(function(d) { return "<span>" + d + "</span>"; })
.direction(_mode.direction());
if(_mode.offset())
_d3tip.offset(_mode.offset());
parent.svg().call(_d3tip);
}
}
function fetch_and_show_content(d) {
if(_mode.disabled() || _mode.selection().exclude && _mode.selection().exclude(d3.event.target)) {
hide_tip.call(this);
return;
}
var target = this,
next = function() {
_mode.content()(d, function(content) {
_d3tip.show.call(target, content, target);
d3.select('div.d3-tip')
.selectAll('a.tip-link')
.on('click.' + _namespace, function() {
d3.event.preventDefault();
if(_mode.linkCallback())
_mode.linkCallback()(this.id);
});
_dispatch.tipped(d);
});
};
if(_hideTimeout)
window.clearTimeout(_hideTimeout);
if(_mode.delay()) {
window.clearTimeout(_showTimeout);
_showTimeout = window.setTimeout(next, _mode.delay());
}
else next();
}
function check_hide_tip() {
if(d3.event.relatedTarget &&
(!_mode.selection().exclude || !_mode.selection().exclude(d3.event.target)) &&
(this && this.contains(d3.event.relatedTarget) || // do not hide when mouse is still over a child
_mode.clickable() && d3.event.relatedTarget.classList.contains('d3-tip')))
return false;
return true;
}
function preempt_tip() {
if(_showTimeout) {
window.clearTimeout(_showTimeout);
_showTimeout = null;
}
}
function hide_tip() {
if(!check_hide_tip.apply(this))
return;
preempt_tip();
_d3tip.hide();
}
function hide_tip_delay() {
if(!check_hide_tip.apply(this))
return;
preempt_tip();
if(_mode.hideDelay())
_hideTimeout = window.setTimeout(function () {
_d3tip.hide();
}, _mode.hideDelay());
else
_d3tip.hide();
}
function draw(diagram, node, edge, ehover) {
init(diagram);
_mode.programmatic() || _mode.selection().select(diagram, node, edge, ehover)
.on('mouseover.' + _namespace, fetch_and_show_content)
.on('mouseout.' + _namespace, hide_tip_delay);
if(_mode.clickable()) {
d3.select('div.d3-tip')
.on('mouseover.' + _namespace, function() {
if(_hideTimeout)
window.clearTimeout(_hideTimeout);
})
.on('mouseout.' + _namespace, hide_tip_delay);
}
}
function remove(diagram, node, edge, ehover) {
_mode.programmatic() || _mode.selection().select(diagram, node, edge, ehover)
.on('mouseover.' + _namespace, null)
.on('mouseout.' + _namespace, null);
}
var _mode = dc_graph.mode(_namespace, {
draw: draw,
remove: remove,
laterDraw: true
});
/**
* Specify the direction for tooltips. Currently supports the
* [cardinal and intercardinal directions](https://en.wikipedia.org/wiki/Points_of_the_compass) supported by
* [d3.tip.direction](https://github.com/Caged/d3-tip/blob/master/docs/positioning-tooltips.md#tipdirection):
* `'n'`, `'ne'`, `'e'`, etc.
* @name direction
* @memberof dc_graph.tip
* @instance
* @param {String} [direction='n']
* @return {String}
* @return {dc_graph.tip}
**/
_mode.direction = property('n');
/**
* Specifies the function to generate content for the tooltip. This function has the
* signature `function(d, k)`, where `d` is the datum of the thing being hovered over,
* and `k` is a continuation. The function should fetch the content, asynchronously if
* needed, and then pass html forward to `k`.
* @name content
* @memberof dc_graph.tip
* @instance
* @param {Function} [content]
* @return {Function}
* @example
* // Default mode: assume it's a node, show node title
* var tip = dc_graph.tip().content(function(n, k) {
* k(_mode.parent() ? _mode.parent().nodeTitle.eval(n) : '');
* });
**/
_mode.content = property(function(n, k) {
k(_mode.parent() ? _mode.parent().nodeTitle.eval(n) : '');
});
_mode.on = function(event, f) {
return _dispatch.on(event, f);
};
_mode.disabled = property(false);
_mode.programmatic = property(false);
_mode.displayTip = function(filter, n, cb) {
if(typeof filter !== 'function') {
var d = filter;
filter = function(d2) { return d2 === d; };
}
var found = _mode.selection().select(_mode.parent(), _mode.parent().selectAllNodes(), _mode.parent().selectAllEdges(), null)
.filter(filter);
if(found.size() > 0) {
var action = fetch_and_show_content;
// we need to flatten e.g. for ports, which will have nested selections
// .nodes() does this better in D3v4
var flattened = found.reduce(function(p, v) {
return p.concat(v);
}, []);
var which = (n || 0) % flattened.length;
action.call(flattened[which], d3.select(flattened[which]).datum());
d = d3.select(flattened[which]).datum();
if(cb)
cb(d);
if(_mode.programmatic())
found.on('mouseout.' + _namespace, hide_tip_delay);
}
return _mode;
};
_mode.hideTip = function(delay) {
if(_d3tip) {
if(delay)
hide_tip_delay();
else
hide_tip();
}
return _mode;
};
_mode.selection = property(dc_graph.tip.select_node_and_edge());
_mode.showDelay = _mode.delay = property(0);
_mode.hideDelay = property(200);
_mode.offset = property(null);
_mode.clickable = property(false);
_mode.linkCallback = property(null);
return _mode;
};
/**
* Generates a handler which can be passed to `tip.content` to produce a table of the
* attributes and values of the hovered object.
*
* @name table
* @memberof dc_graph.tip
* @instance
* @return {Function}
* @example
* // show all the attributes and values in the node and edge objects
* var tip = dc_graph.tip();
* tip.content(dc_graph.tip.table());
**/
dc_graph.tip.table = function() {
var gen = function(d, k) {
d = gen.fetch()(d);
if(!d)
return; // don't display tooltip if no content
var data, keys;
if(Array.isArray(d))
data = d;
else if(typeof d === 'number' || typeof d === 'string')
data = [d];
else { // object
data = keys = Object.keys(d).filter(d3.functor(gen.filter()))
.filter(function(k) {
return d[k] !== undefined;
});
}
var table = d3.select(document.createElement('table'));
var rows = table.selectAll('tr').data(data);
var rowsEnter = rows.enter().append('tr');
rowsEnter.append('td').text(function(item) {
if(keys && typeof item === 'string')
return item;
return JSON.stringify(item);
});
if(keys)
rowsEnter.append('td').text(function(item) {
return JSON.stringify(d[item]);
});
k(table.node().outerHTML); // optimizing for clarity over speed (?)
};
gen.filter = property(true);
gen.fetch = property(function(d) {
return d.orig.value;
});
return gen;
};
dc_graph.tip.json_table = function() {
var table = dc_graph.tip.table().fetch(function(d) {
var jsontip = table.json()(d);
if(!jsontip) return null;
try {
return JSON.parse(jsontip);
} catch(xep) {
return [jsontip];
}
});
table.json = property(function(d) {
return (d.orig.value.value || d.orig.value).jsontip;
});
return table;
};
dc_graph.tip.html_or_json_table = function() {
var json_table = dc_graph.tip.json_table();
var gen = function(d, k) {
var html = gen.html()(d);
if(html)
k(html);
else
json_table(d, k);
};
gen.json = json_table.json;
gen.html = property(function(d) {
return (d.orig.value.value || d.orig.value).htmltip;
});
return gen;
};
dc_graph.tip.select_node_and_edge = function() {
return {
select: function(diagram, node, edge, ehover) {
// hack to merge selections, not supported d3v3
var selection = diagram.selectAll('.foo-this-does-not-exist');
selection[0] = node[0].concat(ehover ? ehover[0] : []);
return selection;
},
exclude: function(element) {
return ancestor_has_class(element, 'port');
}
};
};
dc_graph.tip.select_node = function() {
return {
select: function(diagram, node, edge, ehover) {
return node;
},
exclude: function(element) {
return ancestor_has_class(element, 'port');
}
};
};
dc_graph.tip.select_edge = function() {
return {
select: function(diagram, node, edge, ehover) {
return edge;
}
};
};
dc_graph.tip.select_port = function() {
return {
select: function(diagram, node, edge, ehover) {
return node.selectAll('g.port');
}
};
};