d3-sankey-diagram
Version:
Sankey diagram d3 plugin
341 lines (298 loc) • 9.77 kB
JavaScript
import { interpolate } from 'd3-interpolate'
// function defaultSegments (d) {
// return d.segments
// }
function defaultMinWidth (d) {
return (d.dy === 0) ? 0 : 2
}
export default function sankeyLink() {
// var segments = defaultSegments
var minWidth = defaultMinWidth
function radiusBounds(d) {
var Dx = d.x1 - d.x0,
Dy = d.y1 - d.y0,
Rmin = d.dy / 2,
Rmax = (Dx*Dx + Dy*Dy) / Math.abs(4*Dy);
return [Rmin, Rmax];
}
function link(d) {
if (d.points.length === 1) {
return toOrFromElsewherePath(d)
}
var path = ''
var seg
for (var i = 0; i < d.points.length - 1; ++i) {
seg = {
x0: d.points[i].x,
y0: d.points[i].y,
x1: d.points[i + 1].x,
y1: d.points[i + 1].y,
r0: d.points[i].ro,
r1: d.points[i + 1].ri,
d0: d.points[i].d,
d1: d.points[i + 1].d,
dy: d.dy
}
path += segmentPath(seg)
}
return path
}
function segmentPath (d) {
var dir = (d.d0 || 'r') + (d.d1 || 'r');
if (d.source && d.source === d.target) {
return selfLink(d);
}
if (dir === 'rl') {
return fbLink(d);
}
if (dir === 'rd') {
return fdLink(d);
}
if (dir === 'dr') {
return dfLink(d);
}
if (dir === 'lr') {
return bfLink(d);
}
// Minimum thickness 2px
var h = Math.max(minWidth(d), d.dy) / 2,
x0 = d.x0,
x1 = d.x1,
y0 = d.y0,
y1 = d.y1;
if (x1 < x0) {
[x0, x1] = [x1, x0];
[y0, y1] = [y1, y0];
}
let f = y1 > y0 ? 1 : -1,
fx = 1; // dir === 'll' ? -1 : 1;
const Rlim = radiusBounds(d),
defaultRadius = Math.max(Rlim[0], Math.min(Rlim[1], (x1 - x0)/3));
let r0 = Math.max(Rlim[0], Math.min(Rlim[1], (d.r0 || defaultRadius))),
r1 = Math.max(Rlim[0], Math.min(Rlim[1], (d.r1 || defaultRadius)));
const dcx = (x1 - x0),
dcy = (y1 - y0) - f * (r0 + r1),
D = Math.sqrt(dcx*dcx + dcy*dcy);
const phi = -f * Math.acos(Math.min(1, (r0 + r1) / D)),
psi = Math.atan2(dcy, dcx);
let theta = Math.PI/2 + f * (psi + phi);
let hs = h * f * Math.sin(theta),
hc = h * Math.cos(theta),
x2 = x0 + fx * r0 * Math.sin(Math.abs(theta)),
x3 = x1 - fx * r1 * Math.sin(Math.abs(theta)),
y2 = y0 + r0 * f * (1 - Math.cos(theta)),
y3 = y1 - r1 * f * (1 - Math.cos(theta));
if (isNaN(theta) || Math.abs(theta) < 1e-3) {
theta = r0 = r1 = 0;
x2 = x0;
x3 = x1;
y2 = y0;
y3 = y1;
hs = 0;
hc = h;
}
function arc(dir, r) {
var f = ( dir * (y1-y0) > 0) ? 1 : 0,
rr = (fx * dir * (y1-y0) > 0) ? (r + h) : (r - h);
// straight line
if (theta === 0) { rr = r; }
return "A" + rr + " " + rr + " " + Math.abs(theta) + " 0 " + f + " ";
}
var path;
// if (fx * (x2 - x3) < 0 || Math.abs(y1 - y0) > 4*h) {
// XXX this causes juddering during transitions
path = ("M" + [x0, y0-h ] + " " +
arc(+1, r0) + [x2+hs, y2-hc] + " " +
"L" + [x3+hs, y3-hc] + " " +
arc(-1, r1) + [x1, y1-h ] + " " +
"L" + [x1, y1+h ] + " " +
arc(+1, r1) + [x3-hs, y3+hc] + " " +
"L" + [x2-hs, y2+hc] + " " +
arc(-1, r0) + [x0, y0+h ] + " " +
"Z");
if (/NaN/.test(path)) {
console.error('path NaN', d, path);
}
return path;
}
function selfLink(d) {
var h = Math.max(minWidth(d), d.dy) / 2,
r = h*1.5,
theta = 2 * Math.PI,
x0 = d.x0,
y0 = d.y0;
function arc(dir) {
var f = (dir > 0) ? 1 : 0,
rr = (dir > 0) ? (r + h) : (r - h);
return "A" + rr + " " + rr + " " + Math.abs(theta) + " 1 " + f + " ";
}
return ("M" + [x0+0.1, y0-h] + " " +
arc(+1) + [x0-0.1, y0-h] + " " +
"L" + [x0-0.1, y0+h] + " " +
arc(-1) + [x0+0.1, y0+h] + " " +
"Z");
}
function fbLink(d) {
// Minimum thickness 2px
var h = Math.max(minWidth(d), d.dy) / 2,
x0 = d.x0,
x1 = d.x1,
y0 = d.y0,
y1 = d.y1,
Dx = d.x1 - d.x0,
Dy = d.y1 - d.y0,
//Rlim = radiusBounds(d),
defaultRadius = ((d.r0 + d.r1) / 2) || (5 + h), //Math.max(Rlim[0], Math.min(Rlim[1], Dx/3)),
r = Math.min(Math.abs(y1-y0)/2.1, defaultRadius), //2*(d.r || defaultRadius),
theta = Math.atan2(Dy - 2*r, Dx),
l = Math.sqrt(Math.max(0, Dx*Dx + (Dy-2*r)*(Dy-2*r))),
f = d.y1 > d.y0 ? 1 : -1,
hs = h * Math.sin(theta),
hc = h * Math.cos(theta),
x2 = d.x0 + r * Math.sin(Math.abs(theta)),
x3 = d.x1 + r * Math.sin(Math.abs(theta)),
y2 = d.y0 + r * f * (1 - Math.cos(theta)),
y3 = d.y1 - r * f * (1 - Math.cos(theta));
function arc(dir) {
var f = (dir * theta > 0) ? 1 : 0,
rr = (dir * theta > 0) ? (r + h) : (r - h);
// straight line
if (theta === 0) { rr = r; }
return "A" + rr + " " + rr + " " + Math.abs(theta) + " 0 " + f + " ";
}
return ("M" + [x0, y0-h ] + " " +
arc(+1) + [x2+hs, y2-hc] + " " +
"L" + [x3+hs, y3-hc] + " " +
arc(+1) + [x1, y1+h ] + " " +
"L" + [x1, y1-h ] + " " +
arc(-1) + [x3-hs, y3+hc] + " " +
"L" + [x2-hs, y2+hc] + " " +
arc(-1) + [x0, y0+h ] + " " +
"Z");
}
function fdLink(d) {
// Minimum thickness 2px
var h = Math.max(minWidth(d), d.dy) / 2,
x0 = d.x0,
x1 = d.x1,
y0 = d.y0,
y1 = d.y1,
Dx = d.x1 - d.x0,
Dy = d.y1 - d.y0,
theta = Math.PI / 2,
r = Math.max(0, x1 - x0),
f = d.y1 > d.y0, // = 1
y2 = y0 + r;
function arc(dir) {
var f = (dir * theta > 0) ? 1 : 0,
rr = (dir * theta > 0) ? (r + h) : (r - h);
// straight line
if (theta === 0) { rr = r; }
return "A" + rr + " " + rr + " " + Math.abs(theta) + " 0 " + f + " ";
}
return ("M" + [x0, y0-h ] + " " +
arc(+1) + [x1+h, y2 ] + " " +
"L" + [x1+h, y1 ] + " " +
"" + [x1-h, y1 ] + " " +
"" + [x1-h, y2 ] + " " +
arc(-1) + [x0, y0+h ] + " " +
"Z");
}
function dfLink(d) {
// Minimum thickness 2px
var h = Math.max(minWidth(d), d.dy) / 2,
x0 = d.x0,
x1 = d.x1,
y0 = d.y0,
y1 = d.y1,
Dx = d.x1 - d.x0,
Dy = d.y1 - d.y0,
theta = Math.PI / 2,
r = Math.max(0, x1 - x0),
f = d.y1 > d.y0, // = 1
y2 = y1 - r;
function arc(dir) {
var f = (dir * theta > 0) ? 1 : 0,
rr = (dir * theta > 0) ? (r + h) : (r - h);
// straight line
if (theta === 0) { rr = r; }
return "A" + rr + " " + rr + " " + Math.abs(theta) + " 0 " + f + " ";
}
return ("M" + [x0-h, y0 ] + " " +
"L" + [x0+h, y0 ] + " " +
"" + [x0+h, y2 ] + " " +
arc(-1) + [x1 , y1-h ] + " " +
"L" + [x1 , y1+h ] + " " +
arc(+1) + [x0-h, y2 ] + " " +
"Z");
}
function bfLink(d) {
// Minimum thickness 2px
var h = Math.max(minWidth(d), d.dy) / 2,
x0 = d.x0,
x1 = d.x1,
y0 = d.y0,
y1 = d.y1,
Dx = d.x1 - d.x0,
Dy = d.y1 - d.y0,
//Rlim = radiusBounds(d),
defaultRadius = ((d.r0 + d.r1) / 2) || (5 + h), //Math.max(Rlim[0], Math.min(Rlim[1], Dx/3)),
r = Math.min(Math.abs(Dy)/2.1, defaultRadius), //2*(d.r || defaultRadius),
theta = Math.atan2(Dy - 2*r, Dx),
l = Math.sqrt(Math.max(0, Dx*Dx + (Dy-2*r)*(Dy-2*r))),
f = d.y1 > d.y0 ? 1 : -1,
hs = h * Math.sin(theta),
hc = h * Math.cos(theta),
x2 = d.x0 - r * Math.sin(Math.abs(theta)),
x3 = d.x1 - r * Math.sin(Math.abs(theta)),
y2 = d.y0 + r * f * (1 - Math.cos(theta)),
y3 = d.y1 - r * f * (1 - Math.cos(theta));
function arc(dir) {
var f = (dir * theta > 0) ? 1 : 0,
rr = (-dir * theta > 0) ? (r + h) : (r - h);
// straight line
if (theta === 0) { rr = r; }
return "A" + rr + " " + rr + " " + Math.abs(theta) + " 0 " + f + " ";
}
return ("M" + [x0, y0-h ] + " " +
arc(-1) + [x2-hs, y2-hc] + " " +
"L" + [x3-hs, y3-hc] + " " +
arc(-1) + [x1, y1+h ] + " " +
"L" + [x1, y1-h ] + " " +
arc(+1) + [x3+hs, y3-hc] + " " +
"L" + [x2+hs, y2-hc] + " " +
arc(+1) + [x0, y0+h ] + " " +
"Z");
}
function toOrFromElsewherePath(d) {
const p = d.points[0]
const h = Math.max(minWidth(d), d.dy) / 2
// XXX draw these properly with curves and appropriate radii
if (p.style === "down-right") {
return ("M" + [p.x - 20, p.y-h] + " " +
"L" + [p.x, p.y-h] + " " +
"L" + [p.x, p.y+h] + " " +
"L" + [p.x - 20, p.y+h] + " " +
"Z")
}
if (p.style === "right-down") {
return ("M" + [p.x, p.y-h] + " " +
"L" + [p.x + 20, p.y-h] + " " +
"L" + [p.x + 20, p.y+h] + " " +
"L" + [p.x, p.y+h] + " " +
"Z")
}
}
link.minWidth = function (x) {
if (arguments.length) {
minWidth = required(x)
return link
}
return minWidth
}
return link;
}
function required (f) {
if (typeof f !== 'function') throw new Error()
return f
}