react-occult
Version:
Layered Information Visualization based on React and D3
257 lines (217 loc) • 6.62 kB
JavaScript
import React from 'react';
import { curveLinearClosed, line } from 'd3-shape';
const dedupeRibbonPoints = (weight = 1) => (p, c) => {
const l = p[p.length - 1];
if (
!l ||
Math.round(l.x / weight) !== Math.round(c.x / weight) ||
Math.round(l.y / weight) !== Math.round(c.y / weight)
) {
p.push(c);
}
return p;
};
// FROM d3-svg-ribbon
const linearRibbon = () => {
const _lineConstructor = line();
let _xAccessor = function(d) {
return d.x;
};
let _yAccessor = function(d) {
return d.y;
};
let _rAccessor = function(d) {
return d.r;
};
let _interpolator = curveLinearClosed;
function _ribbon(pathData) {
if (pathData.multiple) {
const original_r = _rAccessor;
const parallelTotal = pathData.multiple.reduce((p, c) => p + c.weight, 0);
_rAccessor = () => parallelTotal;
const totalPoints = buildRibbon(pathData.points);
let currentPoints = totalPoints
.filter(d => d.direction === 'forward')
.reduce(dedupeRibbonPoints(), []);
const allRibbons = [];
pathData.multiple.forEach((siblingPath, siblingI) => {
_rAccessor = () => siblingPath.weight;
const currentRibbon = buildRibbon(currentPoints);
allRibbons.push(currentRibbon);
const nextSibling = pathData.multiple[siblingI + 1];
if (nextSibling) {
const currentLeftSide = currentRibbon
.reverse()
.filter(d => d.direction === 'back')
.reduce(dedupeRibbonPoints(), []);
_rAccessor = () => nextSibling.weight;
const leftHandInflatedRibbon = buildRibbon(currentLeftSide);
currentPoints = leftHandInflatedRibbon
.reverse()
.filter(d => d.direction === 'back')
.reduce(dedupeRibbonPoints(), []);
}
});
_rAccessor = original_r;
return allRibbons.map(d =>
_lineConstructor
.x(_xAccessor)
.y(_yAccessor)
.curve(_interpolator)(d)
);
}
const bothPoints = buildRibbon(pathData).reduce(dedupeRibbonPoints(), []);
return _lineConstructor
.x(_xAccessor)
.y(_yAccessor)
.curve(_interpolator)(bothPoints);
}
_ribbon.x = function(_value) {
if (!arguments.length) return _xAccessor;
_xAccessor = _value;
return _ribbon;
};
_ribbon.y = function(_value) {
if (!arguments.length) return _yAccessor;
_yAccessor = _value;
return _ribbon;
};
_ribbon.r = function(_value) {
if (!arguments.length) return _rAccessor;
_rAccessor = _value;
return _ribbon;
};
_ribbon.interpolate = function(_value) {
if (!arguments.length) return _interpolator;
_interpolator = _value;
return _ribbon;
};
return _ribbon;
function offsetEdge(d) {
const diffX = _yAccessor(d.target) - _yAccessor(d.source);
const diffY = _xAccessor(d.target) - _xAccessor(d.source);
const angle0 = Math.atan2(diffY, diffX) + Math.PI / 2;
const angle1 = angle0 + Math.PI * 0.5;
const angle2 = angle0 + Math.PI * 0.5;
const x1 = _xAccessor(d.source) + _rAccessor(d.source) * Math.cos(angle1);
const y1 = _yAccessor(d.source) - _rAccessor(d.source) * Math.sin(angle1);
const x2 = _xAccessor(d.target) + _rAccessor(d.target) * Math.cos(angle2);
const y2 = _yAccessor(d.target) - _rAccessor(d.target) * Math.sin(angle2);
return { x1: x1, y1: y1, x2: x2, y2: y2 };
}
function buildRibbon(points) {
const bothCode = [];
let x = 0;
let transformedPoints = { x1: 0, y1: 0, x2: 0, y2: 0 };
while (x < points.length) {
if (x !== points.length - 1) {
transformedPoints = offsetEdge({
source: points[x],
target: points[x + 1]
});
const p1 = {
x: transformedPoints.x1,
y: transformedPoints.y1,
direction: 'forward'
};
const p2 = {
x: transformedPoints.x2,
y: transformedPoints.y2,
direction: 'forward'
};
bothCode.push(p1, p2);
if (bothCode.length > 3) {
const l = bothCode.length - 1;
const lineA = { a: bothCode[l - 3], b: bothCode[l - 2] };
const lineB = { a: bothCode[l - 1], b: bothCode[l] };
const intersect = findIntersect(
lineA.a.x,
lineA.a.y,
lineA.b.x,
lineA.b.y,
lineB.a.x,
lineB.a.y,
lineB.b.x,
lineB.b.y
);
if (intersect.found === true) {
lineA.b.x = intersect.x;
lineA.b.y = intersect.y;
lineB.a.x = intersect.x;
lineB.a.y = intersect.y;
}
}
}
x++;
}
x--;
//Back
while (x >= 0) {
if (x !== 0) {
transformedPoints = offsetEdge({
source: points[x],
target: points[x - 1]
});
const p1 = {
x: transformedPoints.x1,
y: transformedPoints.y1,
direction: 'back'
};
const p2 = {
x: transformedPoints.x2,
y: transformedPoints.y2,
direction: 'back'
};
bothCode.push(p1, p2);
if (bothCode.length > 3) {
const l = bothCode.length - 1;
const lineA = { a: bothCode[l - 3], b: bothCode[l - 2] };
const lineB = { a: bothCode[l - 1], b: bothCode[l] };
const intersect = findIntersect(
lineA.a.x,
lineA.a.y,
lineA.b.x,
lineA.b.y,
lineB.a.x,
lineB.a.y,
lineB.b.x,
lineB.b.y
);
if (intersect.found === true) {
lineA.b.x = intersect.x;
lineA.b.y = intersect.y;
lineB.a.x = intersect.x;
lineB.a.y = intersect.y;
}
}
}
x--;
}
return bothCode;
}
function findIntersect(l1x1, l1y1, l1x2, l1y2, l2x1, l2y1, l2x2, l2y2) {
let a, b;
const result = {
x: null,
y: null,
found: false
};
const d = (l2y2 - l2y1) * (l1x2 - l1x1) - (l2x2 - l2x1) * (l1y2 - l1y1);
if (d === 0) {
return result;
}
a = l1y1 - l2y1;
b = l1x1 - l2x1;
const n1 = (l2x2 - l2x1) * a - (l2y2 - l2y1) * b;
const n2 = (l1x2 - l1x1) * a - (l1y2 - l1y1) * b;
a = n1 / d;
b = n2 / d;
result.x = l1x1 + a * (l1x2 - l1x1);
result.y = l1y1 + a * (l1y2 - l1y1);
if (a > 0 && a < 1 && b > 0 && b < 1) {
result.found = true;
}
return result;
}
};
export default linearRibbon;