Skip to content

Commit

Permalink
Merge pull request #14 from d3/two
Browse files Browse the repository at this point in the history
Add d3.chordDirected and d3.ribbonArrow.
  • Loading branch information
Fil committed Aug 23, 2020
2 parents eae664a + c4f11ac commit 4bc44ad
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 176 deletions.
90 changes: 68 additions & 22 deletions README.md

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"name": "d3-chord",
"version": "1.0.6",
"version": "2.0.0-rc.10",
"publishConfig": {
"tag": "next"
},
"description": "Visualize relationships or network flow with an aesthetically-pleasing circular layout.",
"keywords": [
"d3",
Expand Down Expand Up @@ -35,8 +38,7 @@
"postpublish": "git push && git push --tags && cd ../d3.github.com && git pull && cp ../${npm_package_name}/dist/${npm_package_name}.js ${npm_package_name}.v${npm_package_version%%.*}.js && cp ../${npm_package_name}/dist/${npm_package_name}.min.js ${npm_package_name}.v${npm_package_version%%.*}.min.js && git add ${npm_package_name}.v${npm_package_version%%.*}.js ${npm_package_name}.v${npm_package_version%%.*}.min.js && git commit -m \"${npm_package_name} ${npm_package_version}\" && git push && cd - && zip -j dist/${npm_package_name}.zip -- LICENSE README.md dist/${npm_package_name}.js dist/${npm_package_name}.min.js"
},
"dependencies": {
"d3-array": "1",
"d3-path": "1"
"d3-path": ">=2.0.0-rc.1"
},
"sideEffects": false,
"devDependencies": {
Expand Down
147 changes: 74 additions & 73 deletions src/chord.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {range} from "d3-array";
import {max, tau} from "./math.js";

function range(i, j) {
return Array.from({length: j - i}, (_, k) => i + k);
}

function compareValue(compare) {
return function(a, b) {
return compare(
Expand All @@ -11,93 +14,91 @@ function compareValue(compare) {
}

export default function() {
return chord(false, false);
}

export function chordTranspose() {
return chord(false, true);
}

export function chordDirected() {
return chord(true, false);
}

function chord(directed, transpose) {
var padAngle = 0,
sortGroups = null,
sortSubgroups = null,
sortChords = null;

function chord(matrix) {
var n = matrix.length,
groupSums = [],
groupIndex = range(n),
subgroupIndex = [],
chords = [],
groups = chords.groups = new Array(n),
subgroups = new Array(n * n),
k,
x,
x0,
dx,
i,
j;

// Compute the sum.
k = 0, i = -1; while (++i < n) {
x = 0, j = -1; while (++j < n) {
x += matrix[i][j];
}
groupSums.push(x);
subgroupIndex.push(range(n));
k += x;
}
groupSums = new Array(n),
groupIndex = range(0, n),
chords = new Array(n * n),
groups = new Array(n),
k = 0, dx;

// Sort groups…
if (sortGroups) groupIndex.sort(function(a, b) {
return sortGroups(groupSums[a], groupSums[b]);
});

// Sort subgroups…
if (sortSubgroups) subgroupIndex.forEach(function(d, i) {
d.sort(function(a, b) {
return sortSubgroups(matrix[i][a], matrix[i][b]);
});
});

// Convert the sum to scaling factor for [0, 2pi].
// TODO Allow start and end angle to be specified?
// TODO Allow padding to be specified as percentage?
k = max(0, tau - padAngle * n) / k;
dx = k ? padAngle : tau / n;
matrix = Float64Array.from({length: n * n}, transpose
? (_, i) => matrix[i % n][i / n | 0]
: (_, i) => matrix[i / n | 0][i % n]);

// Compute the start and end angle for each group and subgroup.
// Note: Opera has a bug reordering object literal properties!
x = 0, i = -1; while (++i < n) {
x0 = x, j = -1; while (++j < n) {
var di = groupIndex[i],
dj = subgroupIndex[di][j],
v = matrix[di][dj],
a0 = x,
a1 = x += v * k;
subgroups[dj * n + di] = {
index: di,
subindex: dj,
startAngle: a0,
endAngle: a1,
value: v
};
}
groups[di] = {
index: di,
startAngle: x0,
endAngle: x,
value: groupSums[di]
};
x += dx;
// Compute the scaling factor from value to angle in [0, 2pi].
for (let i = 0; i < n; ++i) {
let x = 0;
for (let j = 0; j < n; ++j) x += matrix[i * n + j] + directed * matrix[j * n + i];
k += groupSums[i] = x;
}
k = max(0, tau - padAngle * n) / k;
dx = k ? padAngle : tau / n;

// Generate chords for each (non-empty) subgroup-subgroup link.
i = -1; while (++i < n) {
j = i - 1; while (++j < n) {
var source = subgroups[j * n + i],
target = subgroups[i * n + j];
if (source.value || target.value) {
chords.push(source.value < target.value
? {source: target, target: source}
: {source: source, target: target});
// Compute the angles for each group and constituent chord.
{
let x = 0;
if (sortGroups) groupIndex.sort((a, b) => sortGroups(groupSums[a], groupSums[b]));
for (const i of groupIndex) {
const x0 = x;
if (directed) {
const subgroupIndex = range(~n + 1, n).filter(j => j < 0 ? matrix[~j * n + i] : matrix[i * n + j]);
if (sortSubgroups) subgroupIndex.sort((a, b) => sortSubgroups(a < 0 ? -matrix[~a * n + i] : matrix[i * n + a], b < 0 ? -matrix[~b * n + i] : matrix[i * n + b]));
for (const j of subgroupIndex) {
if (j < 0) {
const chord = chords[~j * n + i] || (chords[~j * n + i] = {source: null, target: null});
chord.target = {index: i, startAngle: x, endAngle: x += matrix[~j * n + i] * k, value: matrix[~j * n + i]};
} else {
const chord = chords[i * n + j] || (chords[i * n + j] = {source: null, target: null});
chord.source = {index: i, startAngle: x, endAngle: x += matrix[i * n + j] * k, value: matrix[i * n + j]};
}
}
groups[i] = {index: i, startAngle: x0, endAngle: x, value: groupSums[i]};
} else {
const subgroupIndex = range(0, n).filter(j => matrix[i * n + j] || matrix[j * n + i]);
if (sortSubgroups) subgroupIndex.sort((a, b) => sortSubgroups(matrix[i * n + a], matrix[i * n + b]));
for (const j of subgroupIndex) {
let chord;
if (i < j) {
chord = chords[i * n + j] || (chords[i * n + j] = {source: null, target: null});
chord.source = {index: i, startAngle: x, endAngle: x += matrix[i * n + j] * k, value: matrix[i * n + j]};
} else {
chord = chords[j * n + i] || (chords[j * n + i] = {source: null, target: null});
chord.target = {index: i, startAngle: x, endAngle: x += matrix[i * n + j] * k, value: matrix[i * n + j]};
if (i === j) chord.source = chord.target;
}
if (chord.source && chord.target && chord.source.value < chord.target.value) {
const source = chord.source;
chord.source = chord.target;
chord.target = source;
}
}
groups[i] = {index: i, startAngle: x0, endAngle: x, value: groupSums[i]};
}
x += dx;
}
}

// Remove empty chords.
chords = Object.values(chords);
chords.groups = groups;
return sortChords ? chords.sort(sortChords) : chords;
}

Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export {default as chord} from "./chord.js";
export {default as ribbon} from "./ribbon.js";
export {default as chord, chordTranspose, chordDirected} from "./chord.js";
export {default as ribbon, ribbonArrow} from "./ribbon.js";
2 changes: 2 additions & 0 deletions src/math.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export var abs = Math.abs;
export var cos = Math.cos;
export var sin = Math.sin;
export var pi = Math.PI;
export var halfPi = pi / 2;
export var tau = pi * 2;
export var max = Math.max;
export var epsilon = 1e-12;
79 changes: 63 additions & 16 deletions src/ribbon.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {path} from "d3-path";
import {slice} from "./array.js";
import constant from "./constant.js";
import {cos, halfPi, sin} from "./math.js";
import {path} from "d3-path";
import {abs, cos, epsilon, halfPi, sin} from "./math.js";

function defaultSource(d) {
return d.source;
Expand All @@ -23,44 +23,79 @@ function defaultEndAngle(d) {
return d.endAngle;
}

export default function() {
function defaultPadAngle() {
return 0;
}

function defaultArrowheadRadius() {
return 10;
}

function ribbon(headRadius) {
var source = defaultSource,
target = defaultTarget,
radius = defaultRadius,
sourceRadius = defaultRadius,
targetRadius = defaultRadius,
startAngle = defaultStartAngle,
endAngle = defaultEndAngle,
padAngle = defaultPadAngle,
context = null;

function ribbon() {
var buffer,
s = source.apply(this, arguments),
t = target.apply(this, arguments),
ap = padAngle.apply(this, arguments) / 2,
argv = slice.call(arguments),
s = source.apply(this, argv),
t = target.apply(this, argv),
sr = +radius.apply(this, (argv[0] = s, argv)),
sr = +sourceRadius.apply(this, (argv[0] = s, argv)),
sa0 = startAngle.apply(this, argv) - halfPi,
sa1 = endAngle.apply(this, argv) - halfPi,
sx0 = sr * cos(sa0),
sy0 = sr * sin(sa0),
tr = +radius.apply(this, (argv[0] = t, argv)),
tr = +targetRadius.apply(this, (argv[0] = t, argv)),
ta0 = startAngle.apply(this, argv) - halfPi,
ta1 = endAngle.apply(this, argv) - halfPi;

if (!context) context = buffer = path();

context.moveTo(sx0, sy0);
if (ap > epsilon) {
if (abs(sa1 - sa0) > ap * 2 + epsilon) sa1 > sa0 ? (sa0 += ap, sa1 -= ap) : (sa0 -= ap, sa1 += ap);
else sa0 = sa1 = (sa0 + sa1) / 2;
if (abs(ta1 - ta0) > ap * 2 + epsilon) ta1 > ta0 ? (ta0 += ap, ta1 -= ap) : (ta0 -= ap, ta1 += ap);
else ta0 = ta1 = (ta0 + ta1) / 2;
}

context.moveTo(sr * cos(sa0), sr * sin(sa0));
context.arc(0, 0, sr, sa0, sa1);
if (sa0 !== ta0 || sa1 !== ta1) { // TODO sr !== tr?
context.quadraticCurveTo(0, 0, tr * cos(ta0), tr * sin(ta0));
context.arc(0, 0, tr, ta0, ta1);
if (sa0 !== ta0 || sa1 !== ta1) {
if (headRadius) {
var hr = +headRadius.apply(this, arguments), tr2 = tr - hr, ta2 = (ta0 + ta1) / 2;
context.quadraticCurveTo(0, 0, tr2 * cos(ta0), tr2 * sin(ta0));
context.lineTo(tr * cos(ta2), tr * sin(ta2));
context.lineTo(tr2 * cos(ta1), tr2 * sin(ta1));
} else {
context.quadraticCurveTo(0, 0, tr * cos(ta0), tr * sin(ta0));
context.arc(0, 0, tr, ta0, ta1);
}
}
context.quadraticCurveTo(0, 0, sx0, sy0);
context.quadraticCurveTo(0, 0, sr * cos(sa0), sr * sin(sa0));
context.closePath();

if (buffer) return context = null, buffer + "" || null;
}

if (headRadius) ribbon.headRadius = function(_) {
return arguments.length ? (headRadius = typeof _ === "function" ? _ : constant(+_), ribbon) : headRadius;
};

ribbon.radius = function(_) {
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), ribbon) : radius;
return arguments.length ? (sourceRadius = targetRadius = typeof _ === "function" ? _ : constant(+_), ribbon) : sourceRadius;
};

ribbon.sourceRadius = function(_) {
return arguments.length ? (sourceRadius = typeof _ === "function" ? _ : constant(+_), ribbon) : sourceRadius;
};

ribbon.targetRadius = function(_) {
return arguments.length ? (targetRadius = typeof _ === "function" ? _ : constant(+_), ribbon) : targetRadius;
};

ribbon.startAngle = function(_) {
Expand All @@ -71,6 +106,10 @@ export default function() {
return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant(+_), ribbon) : endAngle;
};

ribbon.padAngle = function(_) {
return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant(+_), ribbon) : padAngle;
};

ribbon.source = function(_) {
return arguments.length ? (source = _, ribbon) : source;
};
Expand All @@ -85,3 +124,11 @@ export default function() {

return ribbon;
}

export default function() {
return ribbon();
}

export function ribbonArrow() {
return ribbon(defaultArrowheadRadius);
}

0 comments on commit 4bc44ad

Please sign in to comment.