dc.graph
Version:
Graph visualizations integrated with crossfilter and dc.js
710 lines (688 loc) • 25.8 kB
JavaScript
function offsetx(ofsx) {
return function(p) {
return {x: p.x + ofsx, y: p.y};
};
}
dc_graph.builtin_arrows = {
box: function(open, side) {
if(!open) return {
frontRef: [8,0],
drawFunction: function(marker, ofs, stemWidth) {
marker.append('rect')
.attr({
x: ofs[0],
y: side==='right' ? -stemWidth/2 : -4,
width: 8,
height: side ? 4+stemWidth/2 : 8,
'stroke-width': 0
});
}
};
else return {
frontRef: [8,0],
drawFunction: function(marker, ofs, stemWidth) {
marker.append('rect')
.attr({
x: ofs[0] + 0.5,
y: side==='right' ? 0 : -3.5,
width: 7,
height: side ? 3.5 : 7,
'stroke-width': 1,
fill: 'none'
});
if(side)
marker.append('svg:path')
.attr({
d: ['M', ofs[0], 0, 'h',8].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
}
};
},
curve: function(open, side) {
return {
stems: [true,false],
kernstems: [0, 0.25],
frontRef: [8,0],
drawFunction: function(marker, ofs, stemWidth) {
var instrs = [];
instrs.push('M', (side==='left' ? 7.5 : 4) + ofs[0], side==='left' ? stemWidth/2 : 3.5);
if(side==='left')
instrs.push('v', -stemWidth/2);
instrs.push('A', 3.5, 3.5, 0, 0, 0,
(side==='right' ? 7.5 : 4) + ofs[0], side==='right' ? 0 : -3.5);
if(side==='right')
instrs.push('v', -stemWidth/2);
marker.append('svg:path')
.attr({
d: instrs.join(' '),
'stroke-width': 1,
fill: 'none'
});
marker.append('svg:path')
.attr({
d: ['M', 7 + ofs[0], 0,
'h -7'].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
}
};
},
icurve: function(open, side) {
return {
stems: [false,true],
kernstems: [0.25,0],
frontRef: [8,0],
drawFunction: function(marker, ofs, stemWidth) {
var instrs = [];
instrs.push('M', (side==='left' ? 0.5 : 4) + ofs[0], side==='left' ? stemWidth/2 : 3.5);
if(side==='left')
instrs.push('v', -stemWidth/2);
instrs.push('A', 3.5, 3.5, 0, 0, 1,
(side==='right' ? 0.5 : 4) + ofs[0], side==='right' ? 0 : -3.5);
if(side==='right')
instrs.push('v', -stemWidth/2);
marker.append('svg:path')
.attr({
d: instrs.join(' '),
'stroke-width': 1,
fill: 'none'
});
marker.append('svg:path')
.attr({
d: ['M', 1 + ofs[0], 0,
'h 7'].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
}
};
},
diamond: function(open, side) {
if(!open) return {
frontRef: [side ? 11.25 : 12, 0],
backRef: [side ? 0.75 : 0, 0],
viewBox: [0, -4, 12, 8],
stems: [!!side, !!side],
kernstems: function(stemWidth) {
return [side ? 0 : .75*stemWidth, side ? 0 : .75*stemWidth];
},
drawFunction: function(marker, ofs, stemWidth) {
var upoints = [{x: 0, y: 0}];
if(side !== 'left')
upoints.push({x: 6, y: 4});
else
upoints.push({x: 6, y: -4});
upoints.push({x: 12, y: 0});
if(!side)
upoints.push({x: 6, y: -4});
var points = upoints.map(offsetx(ofs[0]));
marker.append('svg:path')
.attr({
d: generate_path(points, 1, true),
'stroke-width': 0
});
if(side) {
marker.append('svg:path')
.attr({
d: ['M', 0.75 + ofs[0], 0,
'h 10.5'].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
}
}
};
else return {
frontRef: [side ? 11.25 : 12, 0],
backRef: [side ? 0.75 : 0, 0],
viewBox: [0, -4, 12, 8],
stems: [!!side, !!side],
kernstems: function(stemWidth) {
return [side ? 0 : .75*stemWidth, side ? 0 : .75*stemWidth];
},
drawFunction: function(marker, ofs, stemWidth) {
var upoints = [{x: 0.9, y: 0}];
if(side !== 'left')
upoints.push({x: 6, y: 3.4});
else
upoints.push({x: 6, y: -3.4});
upoints.push({x: 11.1, y: 0});
if(!side)
upoints.push({x: 6, y: -3.4});
var points = upoints.map(offsetx(ofs[0]));
marker.append('svg:path')
.attr({
d: generate_path(points, 1, !side),
'stroke-width': 1,
fill: 'none'
});
if(side) {
marker.append('svg:path')
.attr({
d: ['M', 0.75 + ofs[0], 0,
'h 10.5'].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
}
}
};
},
dot: function(open, side) {
if(!open) return {
frontRef: [8,0],
stems: [!!side, !!side],
drawFunction: function(marker, ofs, stemWidth) {
if(side) {
marker.append('svg:path')
.attr({
d: ['M', ofs[0], 0,
'A', 4, 4, 0, 0, side==='left'?1:0, 8 + ofs[0], 0].join(' '),
'stroke-width': 0
});
marker.append('svg:path')
.attr({
d: ['M', ofs[0], 0,
'h 8'].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
}
else {
marker.append('svg:circle')
.attr('r', 4)
.attr('cx', 4 + ofs[0])
.attr('cy', 0)
.attr('stroke-width', '0px');
}
}
};
else return {
frontRef: [8,0],
stems: [!!side, !!side],
drawFunction: function(marker, ofs, stemWidth) {
if(side) {
marker.append('svg:path')
.attr({
d: ['M', 0.5 + ofs[0], 0,
'A', 3.5, 3.5, 0, 0, side==='left'?1:0, 7.5 + ofs[0], 0].join(' '),
'stroke-width': 1,
fill: 'none'
});
marker.append('svg:path')
.attr({
d: ['M', ofs[0], 0,
'h 8'].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
} else {
marker.append('svg:circle')
.attr('r', 3.5)
.attr('cx', 4 + ofs[0])
.attr('cy', 0)
.attr('fill', 'none')
.attr('stroke-width', '1px');
}
}
};
},
normal: function(open, side) {
if(!open) return {
frontRef: [side ? 8-4/3 : 8, 0],
viewBox: [0, -3, 8, 6],
kernstems: function(stemWidth) {
return [0,stemWidth*4/3];
},
drawFunction: function(marker, ofs, stemWidth) {
var upoints = [];
if(side === 'left')
upoints.push({x: 0, y: 0});
else
upoints.push({x: 0, y: 3});
switch(side) {
case 'left':
upoints.push({x: 8 - stemWidth*4/3, y: -stemWidth/2});
break;
case 'right':
upoints.push({x: 8 - stemWidth*4/3, y: stemWidth/2});
break;
default:
upoints.push({x: 8, y: 0});
}
if(side === 'right')
upoints.push({x: 0, y: 0});
else
upoints.push({x: 0, y: -3});
var points = upoints.map(offsetx(ofs[0]));
marker.append('svg:path')
.attr('d', generate_path(points, 1, true))
.attr('stroke-width', '0px');
if(side) {
marker.append('svg:path')
.attr({
d: ['M', ofs[0], 0,
'h', 8-4*stemWidth/3].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
}
}
};
else return {
frontRef: [side ? 8-4/3 : 8, 0],
viewBox: [0, -3, 8, 6],
kernstems: function(stemWidth) {
return [0,stemWidth*4/3];
},
drawFunction: function(marker, ofs, stemWidth) {
var upoints = [];
if(!side) {
upoints = [
{x: 0.5, y: 2.28},
{x: 6.57, y: 0},
{x: 0.5, y: -2.28}
];
} else {
upoints = [
{x: 0.5, y: 0},
{x: 0.5, y: side === 'left' ? -2.28 : 2.28},
{x: 8-4/3, y: 0}
];
}
var points = upoints.map(offsetx(ofs[0]));
marker.append('svg:path')
.attr({
d: generate_path(points, 1, !side),
'stroke-width': 1,
fill: 'none'
});
if(side) {
marker.append('svg:path')
.attr({
d: ['M', ofs[0], 0,
'h', 8-4/3].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
}
}
};
},
inv: function(open, side) {
if(!open) return {
frontRef: [8,0],
backRef: [side ? 4/3 : 0, 0],
viewBox: [0, -3, 8, 6],
kernstems: function(stemWidth) {
return [stemWidth*4/3,0];
},
drawFunction: function(marker, ofs, stemWidth) {
var upoints = [];
if(side === 'left')
upoints.push({x: 8, y: 0});
else
upoints.push({x: 8, y: 3});
switch(side) {
case 'left':
upoints.push({x: stemWidth*4/3, y: -stemWidth/2});
break;
case 'right':
upoints.push({x: stemWidth*4/3, y: stemWidth/2});
break;
default:
upoints.push({x: 0, y: 0});
}
if(side === 'right')
upoints.push({x: 8, y: 0});
else
upoints.push({x: 8, y: -3});
var points = upoints.map(offsetx(ofs[0]));
marker.append('svg:path')
.attr('d', generate_path(points, 1, true))
.attr('stroke-width', '0px');
if(side) {
marker.append('svg:path')
.attr({
d: ['M', 4*stemWidth/3 + ofs[0], 0,
'h', 8-4*stemWidth/3].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
}
}
};
else return {
frontRef: [8,0],
backRef: [side ? 4/3 : 0, 0],
viewBox: [0, -3, 8, 6],
kernstems: function(stemWidth) {
return [stemWidth*4/3,0];
},
drawFunction: function(marker, ofs, stemWidth) {
var upoints = [];
if(!side) {
upoints = [
{x: 7.5, y: 2.28},
{x: 1.43, y: 0},
{x: 7.5, y: -2.28}
];
} else {
upoints = [
{x: 7.5, y: 0},
{x: 7.5, y: side === 'left' ? -2.28 : 2.28},
{x: 1.43, y: 0}
];
}
var points = upoints.map(offsetx(ofs[0]));
marker.append('svg:path')
.attr({
d: generate_path(points, 1, !side),
'stroke-width': 1,
fill: 'none'
});
if(side) {
marker.append('svg:path')
.attr({
d: ['M', 4*stemWidth/3 + ofs[0], 0,
'h', 8-4/3].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
}
}
};
},
tee: function(open, side) {
return {
frontRef: [5,0],
viewBox: [0, -5, 5, 10],
stems: [true,false],
drawFunction: function(marker, ofs, stemWidth) {
var b = side === 'right' ? 0 : -5,
t = side === 'left' ? 0 : 5;
var points = [
{x: 2, y: t},
{x: 5, y: t},
{x: 5, y: b},
{x: 2, y: b}
].map(offsetx(ofs[0]));
marker.append('svg:path')
.attr('d', generate_path(points, 1, true))
.attr('stroke-width', '0px');
marker.append('svg:path')
.attr('d', ['M', ofs[0], 0, 'h', 5].join(' '))
.attr('stroke-width', stemWidth)
.attr('fill', 'none');
}
};
},
vee: function(open, side) {
return {
stems: [true,false],
kernstems: function(stemWidth) {
return [0,stemWidth];
},
drawFunction: function(marker, ofs, stemWidth) {
var upoints = [
{x: 0, y: -5},
{x: 10, y: 0},
{x: 0, y: 5},
{x: 5, y: 0}
];
if(side==='right')
upoints.splice(0, 1,
{x: 5, y: -stemWidth/2},
{x: 10, y: -stemWidth/2});
else if(side==='left')
upoints.splice(2, 1,
{x: 10, y: stemWidth/2},
{x: 5, y: stemWidth/2});
var points = upoints.map(offsetx(ofs[0]));
marker.append('svg:path')
.attr('d', generate_path(points, 1, true))
.attr('stroke-width', '0px');
marker.append('svg:path')
.attr('d', ['M', ofs[0]+5, 0, 'h',-5].join(' '))
.attr('stroke-width', stemWidth);
}
};
},
crow: function(open, side) {
return {
stems: [false,true],
kernstems: function(stemWidth) {
return [stemWidth,0];
},
drawFunction: function(marker, ofs, stemWidth) {
var upoints = [
{x: 10, y: -5},
{x: 0, y: 0},
{x: 10, y: 5},
{x: 5, y: 0}
];
if(side==='right')
upoints.splice(0, 1,
{x: 5, y: -stemWidth/2},
{x: 0, y: -stemWidth/2});
else if(side==='left')
upoints.splice(2, 1,
{x: 0, y: stemWidth/2},
{x: 5, y: stemWidth/2});
var points = upoints.map(offsetx(ofs[0]));
marker.append('svg:path')
.attr('d', generate_path(points, 1, true))
.attr('stroke-width', '0px');
marker.append('svg:path')
.attr('d', ['M', ofs[0]+5, 0, 'h',5].join(' '))
.attr('stroke-width', stemWidth);
}
};
}
};
function arrow_def(arrdefs, shape, open, side) {
return arrdefs[shape](open, side);
}
function arrow_parts(arrdefs, desc) {
// graphviz appears to use a real parser for this
var parts = [];
while(desc && desc.length) {
var mods = /^o?(?:l|r)?/.exec(desc);
var open = false, side = null;
if(mods[0]) {
mods = mods[0];
desc = desc.slice(mods.length);
open = mods[0] === 'o';
switch(mods[mods.length-1]) {
case 'l':
side='left';
break;
case 'r':
side='right';
}
}
var ok = false;
for(var aname in arrdefs)
if(desc.substring(0, aname.length) === aname) {
ok = true;
parts.push(arrow_def(arrdefs, aname, open, side));
desc = desc.slice(aname.length);
break;
}
if(!ok) {
console.warn("couldn't find arrow name in " + desc);
break;
}
}
return parts;
}
function union_viewbox(vb1, vb2) {
var left = Math.min(vb1[0], vb2[0]),
bottom = Math.min(vb1[1], vb2[1]),
right = Math.max(vb1[0] + vb1[2], vb2[0] + vb2[2]),
top = Math.max(vb1[1] + vb1[3], vb2[1] + vb2[3]);
return [left, bottom, right - left, top - bottom];
}
function subtract_points(p1, p2) {
return [p1[0] - p2[0], p1[1] - p2[1]];
}
function add_points(p1, p2) {
return [p1[0] + p2[0], p1[1] + p2[1]];
}
function mult_point(p, s) {
return p.map(function(x) { return x*s; });
}
function defaulted(def) {
return function(x) {
return x || def;
};
}
var view_box = defaulted([0, -5, 10, 10]),
front_ref = defaulted([10, 0]),
back_ref = defaulted([0, 0]);
function arrow_offsets(parts, stemWidth) {
var frontRef = null, backRef = null;
return parts.map(function(p, i) {
var fr = front_ref(p.frontRef).slice(),
br = back_ref(p.backRef).slice();
if(p.kernstems) {
var kernstems = p.kernstems;
if(typeof kernstems === 'function')
kernstems = kernstems(stemWidth);
if(i !== 0 && kernstems[1]) {
var last = parts[i-1];
if(last.stems && last.stems[0])
fr[0] -= kernstems[1];
}
if(kernstems[0]) {
var kern = false;
if(i === parts.length-1)
kern = true;
else {
var next = parts[i+1];
if(next.stems && next.stems[1])
kern = true;
}
if(kern)
br[0] += kernstems[0];
}
}
if(i === 0) {
frontRef = fr;
backRef = br;
return {backRef: backRef, offset: [0, 0]};
} else {
var ofs = subtract_points(backRef, fr);
backRef = add_points(br, ofs);
return {backRef: backRef, offset: ofs};
}
});
}
function arrow_bounds(parts, stemWidth) {
var viewBox = null, offsets = arrow_offsets(parts, stemWidth);
parts.forEach(function(p, i) {
var vb = view_box(p.viewBox);
var ofs = offsets[i].offset;
if(!viewBox)
viewBox = vb.slice();
else
viewBox = union_viewbox(viewBox, [vb[0] + ofs[0], vb[1] + ofs[1], vb[2], vb[3]]);
});
return {offsets: offsets, viewBox: viewBox};
}
function arrow_length(parts, stemWidth) {
if(!parts.length)
return 0;
var offsets = arrow_offsets(parts, stemWidth);
return front_ref(parts[0].frontRef)[0] - offsets[parts.length-1].backRef[0];
}
function scaled_arrow_lengths(diagram, e) {
var arrowSize = diagram.edgeArrowSize.eval(e),
stemWidth = diagram.edgeStrokeWidth.eval(e) / arrowSize;
var headLength = arrowSize *
(arrow_length(arrow_parts(diagram.arrows(), diagram.edgeArrowhead.eval(e)), stemWidth) +
diagram.nodeStrokeWidth.eval(e.target) / 2),
tailLength = arrowSize *
(arrow_length(arrow_parts(diagram.arrows(), diagram.edgeArrowtail.eval(e)), stemWidth) +
diagram.nodeStrokeWidth.eval(e.source) / 2);
return {headLength: headLength, tailLength: tailLength};
}
function clip_path_to_arrows(headLength, tailLength, path) {
var points0 = as_bezier3(path),
points = chop_bezier(points0, 'head', headLength);
return {
bezDegree: 3,
points: chop_bezier(points, 'tail', tailLength),
sourcePort: path.sourcePort,
targetPort: path.targetPort
};
}
function place_arrows_on_spline(diagram, e, points) {
var alengths = scaled_arrow_lengths(diagram, e);
var path0 = {
points: points,
bezDegree: 3
};
var path = clip_path_to_arrows(alengths.headLength, alengths.tailLength, path0);
return {
path: path,
full: path0,
orienthead: angle_between_points(path.points[path.points.length-1], path0.points[path0.points.length-1]) + 'rad', //calculate_arrowhead_orientation(e.cola.points, 'head'),
orienttail: angle_between_points(path.points[0], path0.points[0]) + 'rad' //calculate_arrowhead_orientation(e.cola.points, 'tail')
};
}
// determine pre-transition orientation that won't spin a lot going to new orientation
function unsurprising_orient(oldorient, neworient) {
var oldang = +oldorient.slice(0, -3),
newang = +neworient.slice(0, -3);
if(Math.abs(oldang - newang) > Math.PI) {
if(newang > oldang)
oldang += 2*Math.PI;
else oldang -= 2*Math.PI;
}
return oldang;
}
function edgeArrow(diagram, arrdefs, e, kind, desc) {
var id = diagram.arrowId(e, kind);
var strokeOfs, edgeStroke;
function arrow_sig() {
return desc + '-' + strokeOfs + '-' + edgeStroke;
}
if(desc) {
strokeOfs = diagram.nodeStrokeWidth.eval(kind==='tail' ? e.source : e.target)/2;
edgeStroke = diagram.edgeStroke.eval(e);
if(e[kind + 'ArrowLast'] === arrow_sig())
return id;
}
var parts = arrow_parts(arrdefs, desc),
marker = diagram.addOrRemoveDef(id, !!parts.length, 'svg:marker');
if(parts.length) {
var arrowSize = diagram.edgeArrowSize.eval(e),
stemWidth = diagram.edgeStrokeWidth.eval(e) / arrowSize,
bounds = arrow_bounds(parts, stemWidth),
frontRef = front_ref(parts[0].frontRef);
bounds.viewBox[0] -= strokeOfs/arrowSize;
bounds.viewBox[3] += strokeOfs/arrowSize;
marker
.attr('viewBox', bounds.viewBox.join(' '))
.attr('refX', frontRef[0])
.attr('refY', frontRef[1])
.attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', bounds.viewBox[2]*arrowSize)
.attr('markerHeight', bounds.viewBox[3]*arrowSize)
.attr('stroke', edgeStroke)
.attr('fill', edgeStroke);
marker.html(null);
parts.forEach(function(p, i) {
marker
.call(p.drawFunction,
add_points([-strokeOfs/arrowSize,0], bounds.offsets[i].offset),
stemWidth);
});
}
e[kind + 'ArrowLast'] = arrow_sig();
return desc ? id : null;
}