d3-sankey-diagram
Version:
Sankey diagram d3 plugin
333 lines (291 loc) • 9.45 kB
JavaScript
// function defaultSegments (d) {
// return d.segments
// }
function defaultMinWidth (d) {
return (d.dy === 0) ? 0 : 2
}
export default function sankeyLink () {
// var segments = defaultSegments
let minWidth = defaultMinWidth
function radiusBounds (d) {
const Dx = d.x1 - d.x0
const Dy = d.y1 - d.y0
const Rmin = d.dy / 2
const Rmax = (Dx * Dx + Dy * Dy) / Math.abs(4 * Dy)
return [Rmin, Rmax]
}
function link (d) {
if (d.points.length === 1) {
return toOrFromElsewherePath(d)
}
let path = ''
let seg
for (let 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) {
const 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
const h = Math.max(minWidth(d), d.dy) / 2
let x0 = d.x0
let x1 = d.x1
let y0 = d.y0
let y1 = d.y1
if (x1 < x0) {
[x0, x1] = [x1, x0];
[y0, y1] = [y1, y0]
}
const f = y1 > y0 ? 1 : -1
const fx = 1 // dir === 'll' ? -1 : 1;
const Rlim = radiusBounds(d)
const 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)))
let r1 = Math.max(Rlim[0], Math.min(Rlim[1], (d.r1 || defaultRadius)))
const dcx = (x1 - x0)
const dcy = (y1 - y0) - f * (r0 + r1)
const D = Math.sqrt(dcx * dcx + dcy * dcy)
const phi = -f * Math.acos(Math.min(1, (r0 + r1) / D))
const psi = Math.atan2(dcy, dcx)
let theta = Math.PI / 2 + f * (psi + phi)
let hs = h * f * Math.sin(theta)
let hc = h * Math.cos(theta)
let x2 = x0 + fx * r0 * Math.sin(Math.abs(theta))
let x3 = x1 - fx * r1 * Math.sin(Math.abs(theta))
let y2 = y0 + r0 * f * (1 - Math.cos(theta))
let 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) {
const f = (dir * (y1 - y0) > 0) ? 1 : 0
let 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 + ' '
}
// if (fx * (x2 - x3) < 0 || Math.abs(y1 - y0) > 4*h) {
// XXX this causes juddering during transitions
const 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) {
const h = Math.max(minWidth(d), d.dy) / 2
const r = h * 1.5
const theta = 2 * Math.PI
const x0 = d.x0
const y0 = d.y0
function arc (dir) {
const f = (dir > 0) ? 1 : 0
const 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
const h = Math.max(minWidth(d), d.dy) / 2
const x0 = d.x0
const x1 = d.x1
const y0 = d.y0
const y1 = d.y1
const Dx = d.x1 - d.x0
const Dy = d.y1 - d.y0
// Rlim = radiusBounds(d),
const defaultRadius = ((d.r0 + d.r1) / 2) || (5 + h) // Math.max(Rlim[0], Math.min(Rlim[1], Dx/3)),
const r = Math.min(Math.abs(y1 - y0) / 2.1, defaultRadius) // 2*(d.r || defaultRadius),
const theta = Math.atan2(Dy - 2 * r, Dx)
const f = d.y1 > d.y0 ? 1 : -1
const hs = h * Math.sin(theta)
const hc = h * Math.cos(theta)
const x2 = d.x0 + r * Math.sin(Math.abs(theta))
const x3 = d.x1 + r * Math.sin(Math.abs(theta))
const y2 = d.y0 + r * f * (1 - Math.cos(theta))
const y3 = d.y1 - r * f * (1 - Math.cos(theta))
function arc (dir) {
const f = (dir * theta > 0) ? 1 : 0
let 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
const h = Math.max(minWidth(d), d.dy) / 2
const x0 = d.x0
const x1 = d.x1
const y0 = d.y0
const y1 = d.y1
const theta = Math.PI / 2
const r = Math.max(0, x1 - x0)
const y2 = y0 + r
function arc (dir) {
const f = (dir * theta > 0) ? 1 : 0
let 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
const h = Math.max(minWidth(d), d.dy) / 2
const x0 = d.x0
const x1 = d.x1
const y0 = d.y0
const y1 = d.y1
const theta = Math.PI / 2
const r = Math.max(0, x1 - x0)
const y2 = y1 - r
function arc (dir) {
const f = (dir * theta > 0) ? 1 : 0
let 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
const h = Math.max(minWidth(d), d.dy) / 2
const x0 = d.x0
const x1 = d.x1
const y0 = d.y0
const y1 = d.y1
const Dx = d.x1 - d.x0
const Dy = d.y1 - d.y0
// Rlim = radiusBounds(d),
const defaultRadius = ((d.r0 + d.r1) / 2) || (5 + h) // Math.max(Rlim[0], Math.min(Rlim[1], Dx/3)),
const r = Math.min(Math.abs(Dy) / 2.1, defaultRadius) // 2*(d.r || defaultRadius),
const theta = Math.atan2(Dy - 2 * r, Dx)
// const l = Math.sqrt(Math.max(0, Dx * Dx + (Dy - 2 * r) * (Dy - 2 * r)))
const f = d.y1 > d.y0 ? 1 : -1
const hs = h * Math.sin(theta)
const hc = h * Math.cos(theta)
const x2 = d.x0 - r * Math.sin(Math.abs(theta))
const x3 = d.x1 - r * Math.sin(Math.abs(theta))
const y2 = d.y0 + r * f * (1 - Math.cos(theta))
const y3 = d.y1 - r * f * (1 - Math.cos(theta))
function arc (dir) {
const f = (dir * theta > 0) ? 1 : 0
let 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
}