Skip to content

Commit

Permalink
Add support for curved lines
Browse files Browse the repository at this point in the history
  • Loading branch information
vasturiano committed May 16, 2018
1 parent 77246da commit c420197
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 5 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Check out the examples:
* [Large size graph (~75k elements)](https://vasturiano.github.io/force-graph/example/large-graph/) ([source](https://github.com/vasturiano/force-graph/blob/master/example/large-graph/index.html))
* [Text as nodes](https://vasturiano.github.io/force-graph/example/text-nodes/) ([source](https://github.com/vasturiano/force-graph/blob/master/example/text-nodes/index.html))
* [Directional links](https://vasturiano.github.io/force-graph/example/directional-links/) ([source](https://github.com/vasturiano/force-graph/blob/master/example/directional-links/index.html))
* [Curved lines and self links](https://vasturiano.github.io/force-graph/example/curved-links/) ([source](https://github.com/vasturiano/force-graph/blob/master/example/curved-links/index.html))
* [Highlight nodes/links](https://vasturiano.github.io/force-graph/example/highlight/) ([source](https://github.com/vasturiano/force-graph/blob/master/example/highlight/index.html))
* [Auto-colored nodes/links](https://vasturiano.github.io/force-graph/example/auto-colored/) ([source](https://github.com/vasturiano/force-graph/blob/master/example/auto-colored/index.html))
* [Custom node shapes](https://vasturiano.github.io/force-graph/example/custom-node-shape/) ([source](https://github.com/vasturiano/force-graph/blob/master/example/custom-node-shape/index.html))
Expand Down Expand Up @@ -85,6 +86,7 @@ myGraph(<myDOMElement>)
| <b>linkColor</b>([<i>str</i> or <i>fn</i>]) | Link object accessor function or attribute for line color. | `color` |
| <b>linkAutoColorBy</b>([<i>str</i> or <i>fn</i>]) | Link object accessor function (`fn(link)`) or attribute (e.g. `'type'`) to automatically group colors by. Only affects links without a color attribute. | |
| <b>linkWidth</b>([<i>num</i>, <i>str</i> or <i>fn</i>]) | Link object accessor function, attribute or a numeric constant for the link line width. Keep in mind that link widths remain visually contant through various zoom levels, where as node sizes scale relatively. | 1 |
| <b>linkCurvature</b>([<i>num</i>, <i>str</i> or <i>fn</i>]) | Link object accessor function, attribute or a numeric constant for the curvature radius of the link line. Curved lines are represented as bezier curves, and any numeric value is accepted. A value of `0` renders a straight line. `1` indicates a radius equal to half of the line length, causing the curve to approximate a semi-circle. For self-referencing links (`source` equal to `target`) the curve is represented as a loop around the node, with length proportional to the curvature value. Lines are curved clockwise for positive values, and counter-clockwise for negative values. Note that rendering curved lines is purely a visual effect and does not affect the behavior of the underlying forces. | 0 |
| <b>linkDirectionalParticles</b>([<i>num</i>, <i>str</i> or <i>fn</i>]) | Link object accessor function, attribute or a numeric constant for the number of particles (small circles) to display over the link line. The particles are distributed equi-spaced along the line, travel in the direction `source` > `target`, and can be used to indicate link directionality. | 0 |
| <b>linkDirectionalParticleSpeed</b>([<i>num</i>, <i>str</i> or <i>fn</i>]) | Link object accessor function, attribute or a numeric constant for the directional particles speed, expressed as the ratio of the link length to travel per frame. Values above `0.5` are discouraged. | 0.01 |
| <b>linkDirectionalParticleWidth</b>([<i>num</i>, <i>str</i> or <i>fn</i>]) | Link object accessor function, attribute or a numeric constant for the directional particles width (diameter). | 4 |
Expand Down
37 changes: 37 additions & 0 deletions example/curved-links/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<head>
<style> body { margin: 0; } </style>

<script src="//unpkg.com/force-graph"></script>
<!--<script src="../../dist/force-graph.js"></script>-->
</head>

<body>
<div id="graph"></div>

<script>
const gData = {
nodes: [...Array(9).keys()].map(i => ({ id: i })),
links: [
{ source: 1, target: 4, curvature: 0 },
{ source: 1, target: 4, curvature: 0.5 },
{ source: 1, target: 4, curvature: -0.5 },
{ source: 5, target: 2, curvature: 0.3 },
{ source: 2, target: 5, curvature: 0.3 },
{ source: 0, target: 3, curvature: 0 },
{ source: 3, target: 3, curvature: 0.5 },
{ source: 0, target: 4, curvature: 0.2 },
{ source: 4, target: 5, curvature: 0.5 },
{ source: 5, target: 6, curvature: 0.7 },
{ source: 6, target: 7, curvature: 1 },
{ source: 7, target: 8, curvature: 2 },
{ source: 8, target: 0, curvature: 0.5 }
]
};

const Graph = ForceGraph()
(document.getElementById('graph'))
.linkDirectionalParticles(2)
.linkCurvature('curvature')
.graphData(gData);
</script>
</body>
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"dependencies": {
"accessor-fn": "^1.2.2",
"bezier-js": "^2.2.5",
"canvas-color-tracker": "^1.0.0",
"d3-drag": "^1.2.1",
"d3-force": "^1.1.0",
Expand Down
50 changes: 45 additions & 5 deletions src/canvas-force-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
forceCenter as d3ForceCenter
} from 'd3-force';

import { default as Bezier } from 'bezier-js';

import Kapsule from 'kapsule';
import accessorFn from 'accessor-fn';
import indexBy from 'index-array-by';
Expand Down Expand Up @@ -34,6 +36,7 @@ export default Kapsule({
linkColor: { default: 'color', triggerUpdate: false },
linkAutoColorBy: {},
linkWidth: { default: 1, triggerUpdate: false },
linkCurvature: { default: 0, triggerUpdate: false },
linkDirectionalParticles: { default: 0 }, // animate photons travelling in the link direction
linkDirectionalParticleSpeed: { default: 0.01, triggerUpdate: false }, // in link length ratio per frame
linkDirectionalParticleWidth: { default: 4, triggerUpdate: false },
Expand Down Expand Up @@ -116,6 +119,7 @@ export default Kapsule({
function paintLinks() {
const getColor = accessorFn(state.linkColor);
const getWidth = accessorFn(state.linkWidth);
const getCurvature = accessorFn(state.linkCurvature);
const ctx = state.ctx;

// Draw wider lines by 2px on shadow canvas for more precise hovering (due to boundary anti-aliasing)
Expand All @@ -137,8 +141,36 @@ export default Kapsule({
const end = link.target;
if (!start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link

const curvature = getCurvature(link);

ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);

if (!curvature) { // Straight line
ctx.lineTo(end.x, end.y);
link.__controlPoints = null;
return;
}

const l = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)); // line length

if (l > 0) {
const a = Math.atan2(end.y - start.y, end.x - start.x); // line angle
const d = l * curvature; // control point distance

const cp = { // control point
x: (start.x + end.x) / 2 + d * Math.cos(a - Math.PI / 2),
y: (start.y + end.y) / 2 + d * Math.sin(a - Math.PI / 2)
};
ctx.quadraticCurveTo(cp.x, cp.y, end.x, end.y);

link.__controlPoints = [cp.x, cp.y];
} else { // Same point, draw a loop
const d = curvature * 70;
const cps = [end.x, end.y - d, end.x + d, end.y];
ctx.bezierCurveTo(...cps, end.x, end.y);

link.__controlPoints = cps;
}
});
ctx.strokeStyle = lineColor;
ctx.lineWidth = lineWidth;
Expand Down Expand Up @@ -172,16 +204,24 @@ export default Kapsule({

ctx.fillStyle = photonColor;

// Construct bezier for curved lines
const bzLine = link.__controlPoints
? new Bezier(start.x, start.y, ...link.__controlPoints, end.x, end.y)
: null;

photons.forEach((photon, idx) => {
const photonPosRatio = photon.__progressRatio =
((photon.__progressRatio || (idx / photons.length)) + particleSpeed) % 1;

const coords = ['x', 'y'].map(dim =>
start[dim] + (end[dim] - start[dim]) * photonPosRatio || 0
);
const coords = bzLine
? bzLine.get(photonPosRatio) // get position along bezier line
: { // straight line: interpolate linearly
x: start.x + (end.x - start.x) * photonPosRatio || 0,
y: start.y + (end.y - start.y) * photonPosRatio || 0
};

ctx.beginPath();
ctx.arc(coords[0], coords[1], photonR, 0, 2 * Math.PI, false);
ctx.arc(coords.x, coords.y, photonR, 0, 2 * Math.PI, false);
ctx.fill();
});
});
Expand Down
1 change: 1 addition & 0 deletions src/force-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const linkedProps = Object.assign(
'linkColor',
'linkAutoColorBy',
'linkWidth',
'linkCurvature',
'linkDirectionalParticles',
'linkDirectionalParticleSpeed',
'linkDirectionalParticleWidth',
Expand Down

0 comments on commit c420197

Please sign in to comment.