dc.graph
Version:
Graph visualizations integrated with crossfilter and dc.js
483 lines (417 loc) • 18.5 kB
JavaScript
dc_graph.spline_paths = function(pathreader, pathprops, hoverprops, selectprops, pathsgroup) {
var highlight_paths_group = dc_graph.register_highlight_paths_group(pathsgroup || 'highlight-paths-group');
pathprops = pathprops || {};
hoverprops = hoverprops || {};
var _paths = null, _hoverpaths = null, _selected = null;
var _anchor;
var _layer = null;
var _savedPositions = null;
function paths_changed(nop, eop, paths) {
_paths = paths;
var engine = _mode.parent().layoutEngine(),
localPaths = paths.filter(pathIsPresent);
if(localPaths.length) {
var nidpaths = localPaths.map(function(lpath) {
var strength = pathreader.pathStrength.eval(lpath);
if(typeof strength !== 'number')
strength = 1;
if(_selected && _selected.indexOf(lpath) !== -1)
strength *= _mode.selectedStrength();
return {
nodes: path_keys(lpath),
strength: strength
};
});
engine.paths(nidpaths);
} else {
engine.paths(null);
if(_savedPositions)
engine.restorePositions(_savedPositions);
}
if(_selected)
_selected = _selected.filter(function(p) { return localPaths.indexOf(p) !== -1; });
_mode.parent().redraw();
}
function select_changed(sp) {
if(sp !== _selected) {
_selected = sp;
paths_changed(null, null, _paths);
}
}
function path_keys(path, unique) {
unique = unique !== false;
var keys = pathreader.elementList.eval(path).filter(function(elem) {
return pathreader.elementType.eval(elem) === 'node';
}).map(function(elem) {
return pathreader.nodeKey.eval(elem);
});
return unique ? uniq(keys) : keys;
}
// check if entire path is present in this view
function pathIsPresent(path) {
return pathreader.elementList.eval(path).every(function(element) {
return pathreader.elementType.eval(element) !== 'node' ||
_mode.parent().getWholeNode(pathreader.nodeKey.eval(element));
});
}
// get the positions of nodes on path
function getNodePositions(path, old) {
return path_keys(path, false).map(function(key) {
var node = _mode.parent().getWholeNode(key);
return {x: old && node.prevX !== undefined ? node.prevX : node.cola.x,
y: old && node.prevY !== undefined ? node.prevY : node.cola.y};
});
};
// insert fake nodes to avoid sharp turns
function insertDummyNodes(path_coord) {
function _distance(node1, node2) {
return Math.sqrt(Math.pow((node1.x-node2.x),2) + Math.pow((node1.y-node2.y),2));
}
var new_path_coord = [];
for(var i = 0; i < path_coord.length; i ++) {
if (i-1 >= 0 && i+1 < path_coord.length) {
if (path_coord[i-1].x === path_coord[i+1].x &&
path_coord[i-1].y === path_coord[i+1].y ) {
// insert node when the previous and next nodes are the same
var x1 = path_coord[i-1].x, y1 = path_coord[i-1].y;
var x2 = path_coord[i].x, y2 = path_coord[i].y;
var dx = x1 - x2, dy = y1 - y2;
var v1 = dy / Math.sqrt(dx*dx + dy*dy);
var v2 = - dx / Math.sqrt(dx*dx + dy*dy);
var insert_p1 = {'x': null, 'y': null};
var insert_p2 = {'x': null, 'y': null};
var offset = 10;
insert_p1.x = (x1+x2)/2.0 + offset*v1;
insert_p1.y = (y1+y2)/2.0 + offset*v2;
insert_p2.x = (x1+x2)/2.0 - offset*v1;
insert_p2.y = (y1+y2)/2.0 - offset*v2;
new_path_coord.push(insert_p1);
new_path_coord.push(path_coord[i]);
new_path_coord.push(insert_p2);
} else if (_distance(path_coord[i-1], path_coord[i+1]) < pathprops.nearNodesDistance){
// insert node when the previous and next nodes are very close
// first node
var x1 = path_coord[i-1].x, y1 = path_coord[i-1].y;
var x2 = path_coord[i].x, y2 = path_coord[i].y;
var dx = x1 - x2, dy = y1 - y2;
var v1 = dy / Math.sqrt(dx*dx + dy*dy);
var v2 = - dx / Math.sqrt(dx*dx + dy*dy);
var insert_p1 = {'x': null, 'y': null};
var offset = 10;
insert_p1.x = (x1+x2)/2.0 + offset*v1;
insert_p1.y = (y1+y2)/2.0 + offset*v2;
// second node
x1 = path_coord[i].x;
y1 = path_coord[i].y;
x2 = path_coord[i+1].x;
y2 = path_coord[i+1].y;
dx = x1 - x2;
dy = y1 - y2;
v1 = dy / Math.sqrt(dx*dx + dy*dy);
v2 = - dx / Math.sqrt(dx*dx + dy*dy);
var insert_p2 = {'x': null, 'y': null};
insert_p2.x = (x1+x2)/2.0 + offset*v1;
insert_p2.y = (y1+y2)/2.0 + offset*v2;
new_path_coord.push(insert_p1);
new_path_coord.push(path_coord[i]);
new_path_coord.push(insert_p2);
}
else {
new_path_coord.push(path_coord[i]);
}
} else {
new_path_coord.push(path_coord[i]);
}
}
return new_path_coord;
}
// helper functions
var vecDot = function(v0, v1) { return v0.x*v1.x+v0.y*v1.y; };
var vecMag = function(v) { return Math.sqrt(v.x*v.x + v.y*v.y); };
var l2Dist = function(p1, p2) {
return Math.sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y));
};
function drawCardinalSpline(points, lineTension, avoidSharpTurn, angleThreshold) {
var c = lineTension || 0;
avoidSharpTurn = avoidSharpTurn !== false;
angleThreshold = angleThreshold || 0.02;
// get the path without self loops
var path_list = [points[0]];
for(var i = 1; i < points.length; i ++) {
if(l2Dist(points[i], path_list[path_list.length-1]) > 1e-6) {
path_list.push(points[i]);
}
}
// repeat first and last node
points = [path_list[0]];
points = points.concat(path_list);
points.push(path_list[path_list.length-1]);
// a segment is a list of three points: [c0, c1, p1],
// representing the coordinates in "C x0,y0,x1,y1,x,y" in svg:path
var segments = []; // control points
for(var i = 1; i < points.length-2; i ++) {
// generate svg:path
var m_0_x = (1-c)*(points[i+1].x - points[i-1].x)/2;
var m_0_y = (1-c)*(points[i+1].y - points[i-1].y)/2;
var m_1_x = (1-c)*(points[i+2].x - points[i].x)/2;
var m_1_y = (1-c)*(points[i+2].y - points[i].y)/2;
var p0 = points[i];
var p1 = points[i+1];
var c0 = p0;
if(i !== 1) {
c0 = {x: p0.x+(m_0_x/3), y:p0.y+(m_0_y/3)};
}
var c1 = p1;
if(i !== points.length-3) {
c1 = {x: p1.x-(m_1_x/3), y:p1.y-(m_1_y/3)};
}
// detect special case by calculating the angle
if(avoidSharpTurn) {
var v0 = {x:points[i-1].x - points[i].x, y:points[i-1].y - points[i].y};
var v1 = {x:points[i+1].x - points[i].x, y:points[i+1].y - points[i].y};
var acosValue = vecDot(v0,v1) / (vecMag(v0)*vecMag(v1));
acosValue = Math.max(-1, Math.min(1, acosValue));
var angle = Math.acos( acosValue );
if(angle <= angleThreshold ){
var m_x = (1-c)*(points[i].x - points[i-1].x)/2;
var m_y = (1-c)*(points[i].y - points[i-1].y)/2;
var k = 2;
var cp1 = {x: p0.x+k*(-m_y/3), y:p0.y+k*(m_x/3)};
var cp2 = {x: p0.x-k*(-m_y/3), y:p0.y-k*(m_x/3)};
// CP_1CP_2
var vCP = {x: cp1.x-cp2.x, y:cp1.y-cp2.y}; // vector cp1->cp2
var vPN = {x: points[i-2].x - points[i+2].x, y:points[i-2].y-points[i+2].y}; // vector Previous->Next
if(vecDot(vCP, vPN) > 0) {
c0 = cp1;
segments[segments.length-1][1] = cp2;
} else {
c0 = cp2;
segments[segments.length-1][1] = cp1;
}
}
}
segments.push([c0,c1,p1]);
}
var path_d = "M"+points[0].x+","+points[0].y;
for(var i = 0; i < segments.length; i ++) {
var s = segments[i];
path_d += "C"+s[0].x+","+s[0].y;
path_d += ","+s[1].x+","+s[1].y;
path_d += ","+s[2].x+","+s[2].y;
}
return path_d;
}
function drawDedicatedLoops(points, lineTension, avoidSharpTurn, angleThreshold) {
// get loops as segments
var p1 = 0, p2 = 1;
var seg_list = []; // (start, end)
while(p1 < points.length-1 && p2 < points.length) {
if(l2Dist(points[p1], points[p2]) < 1e-6) {
var repeated = points[p2];
while(p2 < points.length && l2Dist(points[p2], repeated) < 1e-6) p2++;
seg_list.push({'start': Math.max(0, p1-1), 'end': Math.min(points.length-1, p2)});
p1 = p2;
p2 = p1+1;
} else {
p1++;
p2++;
}
}
var loopCurves = "";
for(var i = 0; i < seg_list.length; i ++) {
var segment = seg_list[i];
var loopCount = segment.end - segment.start - 2;
var anchorPoint = points[segment.start+1];
// the vector from previous node to next node
var vec_pre_next = {
x: points[segment.end].x-points[segment.start].x,
y: points[segment.end].y-points[segment.start].y
};
// when previous node and next node are the same node, we need to handle
// them differently.
// e.g. for a loop segment A->B->B->A, we use the perpendicular vector perp_AB
// instead of vector AA(which is vec_pre_next in this case).
if(vecMag(vec_pre_next) == 0) {
vec_pre_next = {
x: -(points[segment.end].y-anchorPoint.y),
y: points[segment.end].x-anchorPoint.x
};
}
// unit length vector
var vec_pre_next_unit = {
x: vec_pre_next.x / vecMag(vec_pre_next),
y: vec_pre_next.y / vecMag(vec_pre_next)
};
var vec_pre_next_perp = {
x: -vec_pre_next.y / vecMag(vec_pre_next),
y: vec_pre_next.x / vecMag(vec_pre_next)
};
var insertP;
for(var j = 0; j < loopCount; j ++) {
var c1,c2,c3,c4;
// change the control points every time this loop appears
var cp_k = 15+2*j;
// calculate c1 and c4, their tangent match the tangent at anchorPoint
c1 = {
x: anchorPoint.x + cp_k*vec_pre_next_unit.x,
y: anchorPoint.y + cp_k*vec_pre_next_unit.y
};
c4 = {
x: anchorPoint.x - cp_k*vec_pre_next_unit.x,
y: anchorPoint.y - cp_k*vec_pre_next_unit.y
};
// change the location of inserted virtual point every time this loop appears
var control_k = 25+5*j;
var insertP1 = {
x: anchorPoint.x+vec_pre_next_perp.x*control_k,
y: anchorPoint.y+vec_pre_next_perp.y*control_k
};
var insertP2 = {
x: anchorPoint.x-vec_pre_next_perp.x*control_k,
y: anchorPoint.y-vec_pre_next_perp.y*control_k
};
var vec_i_to_next = {
x: points[segment.end].x - anchorPoint.x,
y: points[segment.end].y - anchorPoint.y
};
var vec_i_to_insert = {
x: insertP1.x - anchorPoint.x,
y: insertP1.y - anchorPoint.y
};
insertP = insertP1;
if(vecDot(vec_i_to_insert, vec_i_to_next) > 0) {
insertP = insertP2;
}
// calculate c2 and c3 based on insertP
c2 = {
x: insertP.x + cp_k*vec_pre_next_unit.x,
y: insertP.y + cp_k*vec_pre_next_unit.y
};
c3 = {
x: insertP.x - cp_k*vec_pre_next_unit.x,
y: insertP.y - cp_k*vec_pre_next_unit.y
};
var curve = "M"+anchorPoint.x+","+anchorPoint.y;
curve += "C"+c1.x+","+c1.y+","+c2.x+","+c2.y+","+insertP.x+","+insertP.y;
curve += "C"+c3.x+","+c3.y+","+c4.x+","+c4.y+","+anchorPoint.x+","+anchorPoint.y;
loopCurves += curve;
}
}
return loopCurves;
}
// convert original path data into <d>
function genPath(originalPoints, old, lineTension, avoidSharpTurn, angleThreshold) {
// get coordinates
var path_coord = getNodePositions(originalPoints, old);
if(path_coord.length < 2) return "";
var result = "";
// process the points and treat them differently:
// 1. sub-path without self loop
result += drawCardinalSpline(path_coord, lineTension, avoidSharpTurn, angleThreshold);
// 2. a list of loop segments
result += drawDedicatedLoops(path_coord, lineTension, avoidSharpTurn, angleThreshold);
return result;
}
// draw the spline for paths
function drawSpline(paths) {
if(paths === null) {
_savedPositions = _mode.parent().layoutEngine().savePositions();
return;
}
paths = paths.filter(pathIsPresent);
var hoverpaths = _hoverpaths || [],
selected = _selected || [];
// edge spline
var edge = _layer.selectAll(".spline-edge").data(paths, function(path) { return path_keys(path).join(','); });
edge.exit().remove();
var edgeEnter = edge.enter().append("svg:path")
.attr('class', 'spline-edge')
.attr('id', function(d, i) { return "spline-path-"+i; })
.attr('stroke-width', pathprops.edgeStrokeWidth || 1)
.attr('fill', 'none')
.attr('d', function(d) { return genPath(d, true, pathprops.lineTension, _mode.avoidSharpTurns()); });
edge
.attr('stroke', function(p) {
return selected.indexOf(p) !== -1 && selectprops.edgeStroke ||
hoverpaths.indexOf(p) !== -1 && hoverprops.edgeStroke ||
pathprops.edgeStroke || 'black';
})
.attr('opacity', function(p) {
return selected.indexOf(p) !== -1 && selectprops.edgeOpacity ||
hoverpaths.indexOf(p) !== -1 && hoverprops.edgeOpacity ||
pathprops.edgeOpacity || 1;
});
function path_order(p) {
return hoverpaths.indexOf(p) !== -1 ? 2 :
selected.indexOf(p) !== -1 ? 1 :
0;
}
edge.sort(function(a, b) {
return path_order(a) - path_order(b);
});
_layer.selectAll('.spline-edge-hover')
.each(function() {this.parentNode.appendChild(this);});
edge.transition().duration(_mode.parent().transitionDuration())
.attr('d', function(d) { return genPath(d, false, pathprops.lineTension, _mode.avoidSharpTurns()); });
// another wider copy of the edge just for hover events
var edgeHover = _layer.selectAll('.spline-edge-hover')
.data(paths, function(path) { return path_keys(path).join(','); });
edgeHover.exit().remove();
var edgeHoverEnter = edgeHover.enter().append('svg:path')
.attr('class', 'spline-edge-hover')
.attr('d', function(d) { return genPath(d, true, pathprops.lineTension, _mode.avoidSharpTurns()); })
.attr('opacity', 0)
.attr('stroke', 'green')
.attr('stroke-width', (pathprops.edgeStrokeWidth || 1) + 4)
.attr('fill', 'none')
.on('mouseover.spline-paths', function(d) {
highlight_paths_group.hover_changed([d]);
})
.on('mouseout.spline-paths', function(d) {
highlight_paths_group.hover_changed(null);
})
.on('click.spline-paths', function(d) {
var selected = _selected && _selected.slice(0) || [],
i = selected.indexOf(d);
if(i !== -1)
selected.splice(i, 1);
else if(d3.event.shiftKey)
selected.push(d);
else
selected = [d];
highlight_paths_group.select_changed(selected);
});
edgeHover.transition().duration(_mode.parent().transitionDuration())
.attr('d', function(d) { return genPath(d, false, pathprops.lineTension, _mode.avoidSharpTurns()); });
};
function draw(diagram, node, edge, ehover) {
_layer = _mode.parent().select('g.draw').selectAll('g.spline-layer').data([0]);
_layer.enter().append('g').attr('class', 'spline-layer');
drawSpline(_paths);
}
function remove(diagram, node, edge, ehover) {
}
var _mode = dc_graph.mode('draw-spline-paths', {
laterDraw: true,
draw: draw,
remove: function(diagram, node, edge, ehover) {
remove(diagram, node, edge, ehover);
return this;
},
parent: function(p) {
if(p)
_anchor = p.anchorName();
highlight_paths_group
.on('paths_changed.draw-spline-paths-' + _anchor, p ? paths_changed : null)
.on('select_changed.draw-spline-paths-' + _anchor, p ? select_changed : null)
.on('hover_changed.draw-spline-paths-' + _anchor, p ? function(hpaths) {
_hoverpaths = hpaths;
drawSpline(_paths);
} : null);
}
});
_mode.selectedStrength = property(1);
_mode.avoidSharpTurns = property(true);
return _mode;
};
dc_graph.draw_spline_paths = deprecate_function("draw_spline_paths has been renamed spline_paths, please update", dc_graph.spline_paths);