diff --git a/demo/d3-force-3d.html b/demo/d3-force-3d.html
new file mode 100644
index 0000000..6fbc207
--- /dev/null
+++ b/demo/d3-force-3d.html
@@ -0,0 +1,221 @@
+
+
+
+
+
+ D3Force
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/d3force.html b/demo/d3force.html
index 8f3465d..894e6a5 100644
--- a/demo/d3force.html
+++ b/demo/d3force.html
@@ -53,132 +53,132 @@
const { D3ForceLayout } = window.Layout;
fetch(
- "https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json"
+ 'https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json',
)
.then((res) => res.json())
.then((d) => {
const data = {
nodes: [
{
- id: "0",
+ id: '0',
data: {
- label: "0",
+ label: '0',
},
},
{
- id: "1",
+ id: '1',
data: {
- label: "1",
+ label: '1',
},
},
{
- id: "2",
+ id: '2',
data: {
- label: "2",
+ label: '2',
},
},
{
- id: "3",
+ id: '3',
data: {
- label: "3",
+ label: '3',
},
},
{
- id: "4",
+ id: '4',
data: {
- label: "4",
+ label: '4',
},
},
{
- id: "5",
+ id: '5',
data: {
- label: "5",
+ label: '5',
},
},
{
- id: "6",
+ id: '6',
data: {
- label: "6",
+ label: '6',
},
},
{
- id: "7",
+ id: '7',
data: {
- label: "7",
+ label: '7',
},
},
{
- id: "8",
+ id: '8',
data: {
- label: "8",
+ label: '8',
},
},
{
- id: "9",
+ id: '9',
data: {
- label: "9",
+ label: '9',
},
},
],
edges: [
{
- source: "0",
- target: "1",
+ source: '0',
+ target: '1',
data: {},
},
{
- source: "0",
- target: "2",
+ source: '0',
+ target: '2',
data: {},
},
{
- source: "0",
- target: "3",
+ source: '0',
+ target: '3',
data: {},
},
{
- source: "0",
- target: "4",
+ source: '0',
+ target: '4',
data: {},
},
{
- source: "0",
- target: "5",
+ source: '0',
+ target: '5',
data: {},
},
{
- source: "0",
- target: "7",
+ source: '0',
+ target: '7',
data: {},
},
{
- source: "0",
- target: "8",
+ source: '0',
+ target: '8',
data: {},
},
{
- source: "0",
- target: "9",
+ source: '0',
+ target: '9',
data: {},
},
{
- source: "2",
- target: "3",
+ source: '2',
+ target: '3',
data: {},
},
{
- source: "4",
- target: "5",
+ source: '4',
+ target: '5',
data: {},
},
{
- source: "4",
- target: "6",
+ source: '4',
+ target: '6',
data: {},
},
{
- source: "5",
- target: "6",
+ source: '5',
+ target: '6',
data: {},
},
],
@@ -192,7 +192,7 @@
// create a canvas
const canvas = new Canvas({
- container: "container",
+ container: 'container',
width: 500,
height: 500,
renderer: canvasRenderer,
@@ -212,10 +212,10 @@
const createNodesEdges = (positions) => {
positions.edges.forEach(({ source, target }, i) => {
const sourceNode = positions.nodes.find(
- ({ id }) => id === source
+ ({ id }) => id === source,
);
const targetNode = positions.nodes.find(
- ({ id }) => id === target
+ ({ id }) => id === target,
);
const line = new Line({
style: {
@@ -224,7 +224,7 @@
x2: targetNode.data.x,
y2: targetNode.data.y,
lineWidth: 1,
- stroke: "grey",
+ stroke: 'grey',
},
});
canvas.appendChild(line);
@@ -237,8 +237,8 @@
cx: node.data.x,
cy: node.data.y,
r: 6,
- fill: "#1890FF",
- stroke: "#F04864",
+ fill: '#1890FF',
+ stroke: '#F04864',
lineWidth: 2,
draggable: true,
},
@@ -263,14 +263,14 @@
});
}
- circle.addEventListener("dragstart", function (e) {
+ circle.addEventListener('dragstart', function (e) {
const [x, y] = e.target.getPosition();
shiftX = e.canvasX - x;
shiftY = e.canvasY - y;
moveAt(e.target, e.canvasX, e.canvasY);
});
- circle.addEventListener("drag", function (e) {
+ circle.addEventListener('drag', function (e) {
moveAt(e.target, e.canvasX, e.canvasY);
});
nodes.push(circle);
@@ -280,10 +280,10 @@
const updateNodesEdges = (positions) => {
positions.edges.forEach(({ source, target }, i) => {
const sourceNode = positions.nodes.find(
- ({ id }) => id === source
+ ({ id }) => id === source,
);
const targetNode = positions.nodes.find(
- ({ id }) => id === target
+ ({ id }) => id === target,
);
edges[i].attr({
@@ -304,8 +304,10 @@
(async () => {
await force.assign(graph, {
- center: [200, 200], // The center of the graph by default
- preventOverlap: true,
+ center: {
+ x: 250,
+ y: 250,
+ },
nodeSize: 20,
onTick: (positions) => {
if (!rendered) {
diff --git a/demo/d3force.stop.html b/demo/d3force.stop.html
index 7e5f959..a189f2f 100644
--- a/demo/d3force.stop.html
+++ b/demo/d3force.stop.html
@@ -53,132 +53,132 @@
const { D3ForceLayout } = window.Layout;
fetch(
- "https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json"
+ 'https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json',
)
.then((res) => res.json())
.then((d) => {
const data = {
nodes: [
{
- id: "0",
+ id: '0',
data: {
- label: "0",
+ label: '0',
},
},
{
- id: "1",
+ id: '1',
data: {
- label: "1",
+ label: '1',
},
},
{
- id: "2",
+ id: '2',
data: {
- label: "2",
+ label: '2',
},
},
{
- id: "3",
+ id: '3',
data: {
- label: "3",
+ label: '3',
},
},
{
- id: "4",
+ id: '4',
data: {
- label: "4",
+ label: '4',
},
},
{
- id: "5",
+ id: '5',
data: {
- label: "5",
+ label: '5',
},
},
{
- id: "6",
+ id: '6',
data: {
- label: "6",
+ label: '6',
},
},
{
- id: "7",
+ id: '7',
data: {
- label: "7",
+ label: '7',
},
},
{
- id: "8",
+ id: '8',
data: {
- label: "8",
+ label: '8',
},
},
{
- id: "9",
+ id: '9',
data: {
- label: "9",
+ label: '9',
},
},
],
edges: [
{
- source: "0",
- target: "1",
+ source: '0',
+ target: '1',
data: {},
},
{
- source: "0",
- target: "2",
+ source: '0',
+ target: '2',
data: {},
},
{
- source: "0",
- target: "3",
+ source: '0',
+ target: '3',
data: {},
},
{
- source: "0",
- target: "4",
+ source: '0',
+ target: '4',
data: {},
},
{
- source: "0",
- target: "5",
+ source: '0',
+ target: '5',
data: {},
},
{
- source: "0",
- target: "7",
+ source: '0',
+ target: '7',
data: {},
},
{
- source: "0",
- target: "8",
+ source: '0',
+ target: '8',
data: {},
},
{
- source: "0",
- target: "9",
+ source: '0',
+ target: '9',
data: {},
},
{
- source: "2",
- target: "3",
+ source: '2',
+ target: '3',
data: {},
},
{
- source: "4",
- target: "5",
+ source: '4',
+ target: '5',
data: {},
},
{
- source: "4",
- target: "6",
+ source: '4',
+ target: '6',
data: {},
},
{
- source: "5",
- target: "6",
+ source: '5',
+ target: '6',
data: {},
},
],
@@ -192,7 +192,7 @@
// create a canvas
const canvas = new Canvas({
- container: "container",
+ container: 'container',
width: 500,
height: 500,
renderer: canvasRenderer,
@@ -211,10 +211,10 @@
const createNodesEdges = (positions) => {
positions.edges.forEach(({ source, target }, i) => {
const sourceNode = positions.nodes.find(
- ({ id }) => id === source
+ ({ id }) => id === source,
);
const targetNode = positions.nodes.find(
- ({ id }) => id === target
+ ({ id }) => id === target,
);
const line = new Line({
style: {
@@ -223,7 +223,7 @@
x2: targetNode.data.x,
y2: targetNode.data.y,
lineWidth: 1,
- stroke: "grey",
+ stroke: 'grey',
},
});
canvas.appendChild(line);
@@ -236,8 +236,8 @@
cx: node.data.x,
cy: node.data.y,
r: 6,
- fill: "#1890FF",
- stroke: "#F04864",
+ fill: '#1890FF',
+ stroke: '#F04864',
lineWidth: 2,
draggable: true,
},
@@ -262,14 +262,14 @@
});
}
- circle.addEventListener("dragstart", function (e) {
+ circle.addEventListener('dragstart', function (e) {
const [x, y] = e.target.getPosition();
shiftX = e.canvasX - x;
shiftY = e.canvasY - y;
moveAt(e.target, e.canvasX, e.canvasY);
});
- circle.addEventListener("drag", function (e) {
+ circle.addEventListener('drag', function (e) {
moveAt(e.target, e.canvasX, e.canvasY);
});
nodes.push(circle);
@@ -279,10 +279,10 @@
const updateNodesEdges = (positions) => {
positions.edges.forEach(({ source, target }, i) => {
const sourceNode = positions.nodes.find(
- ({ id }) => id === source
+ ({ id }) => id === source,
);
const targetNode = positions.nodes.find(
- ({ id }) => id === target
+ ({ id }) => id === target,
);
edges[i].attr({
@@ -303,8 +303,10 @@
let rendered = false;
force.assign(graph, {
- center: [200, 200], // The center of the graph by default
- preventOverlap: true,
+ center: {
+ x: 250,
+ y: 250,
+ },
nodeSize: 20,
onTick: (positions) => {
if (!rendered) {
@@ -322,8 +324,10 @@
setTimeout(() => {
force.assign(graph, {
- center: [200, 200], // The center of the graph by default
- preventOverlap: true,
+ center: {
+ x: 250,
+ y: 250,
+ },
nodeSize: 20,
onTick: (positions) => {
updateNodesEdges(positions);
diff --git a/demo/d3force.tick.html b/demo/d3force.tick.html
index 050f3c1..fc8a53b 100644
--- a/demo/d3force.tick.html
+++ b/demo/d3force.tick.html
@@ -53,132 +53,132 @@
const { D3ForceLayout } = window.Layout;
fetch(
- "https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json"
+ 'https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json',
)
.then((res) => res.json())
.then((d) => {
const data = {
nodes: [
{
- id: "0",
+ id: '0',
data: {
- label: "0",
+ label: '0',
},
},
{
- id: "1",
+ id: '1',
data: {
- label: "1",
+ label: '1',
},
},
{
- id: "2",
+ id: '2',
data: {
- label: "2",
+ label: '2',
},
},
{
- id: "3",
+ id: '3',
data: {
- label: "3",
+ label: '3',
},
},
{
- id: "4",
+ id: '4',
data: {
- label: "4",
+ label: '4',
},
},
{
- id: "5",
+ id: '5',
data: {
- label: "5",
+ label: '5',
},
},
{
- id: "6",
+ id: '6',
data: {
- label: "6",
+ label: '6',
},
},
{
- id: "7",
+ id: '7',
data: {
- label: "7",
+ label: '7',
},
},
{
- id: "8",
+ id: '8',
data: {
- label: "8",
+ label: '8',
},
},
{
- id: "9",
+ id: '9',
data: {
- label: "9",
+ label: '9',
},
},
],
edges: [
{
- source: "0",
- target: "1",
+ source: '0',
+ target: '1',
data: {},
},
{
- source: "0",
- target: "2",
+ source: '0',
+ target: '2',
data: {},
},
{
- source: "0",
- target: "3",
+ source: '0',
+ target: '3',
data: {},
},
{
- source: "0",
- target: "4",
+ source: '0',
+ target: '4',
data: {},
},
{
- source: "0",
- target: "5",
+ source: '0',
+ target: '5',
data: {},
},
{
- source: "0",
- target: "7",
+ source: '0',
+ target: '7',
data: {},
},
{
- source: "0",
- target: "8",
+ source: '0',
+ target: '8',
data: {},
},
{
- source: "0",
- target: "9",
+ source: '0',
+ target: '9',
data: {},
},
{
- source: "2",
- target: "3",
+ source: '2',
+ target: '3',
data: {},
},
{
- source: "4",
- target: "5",
+ source: '4',
+ target: '5',
data: {},
},
{
- source: "4",
- target: "6",
+ source: '4',
+ target: '6',
data: {},
},
{
- source: "5",
- target: "6",
+ source: '5',
+ target: '6',
data: {},
},
],
@@ -192,7 +192,7 @@
// create a canvas
const canvas = new Canvas({
- container: "container",
+ container: 'container',
width: 500,
height: 500,
renderer: canvasRenderer,
@@ -211,10 +211,10 @@
const createNodesEdges = (positions) => {
positions.edges.forEach(({ source, target }, i) => {
const sourceNode = positions.nodes.find(
- ({ id }) => id === source
+ ({ id }) => id === source,
);
const targetNode = positions.nodes.find(
- ({ id }) => id === target
+ ({ id }) => id === target,
);
const line = new Line({
style: {
@@ -223,7 +223,7 @@
x2: targetNode.data.x,
y2: targetNode.data.y,
lineWidth: 1,
- stroke: "grey",
+ stroke: 'grey',
},
});
canvas.appendChild(line);
@@ -236,8 +236,8 @@
cx: node.data.x,
cy: node.data.y,
r: 6,
- fill: "#1890FF",
- stroke: "#F04864",
+ fill: '#1890FF',
+ stroke: '#F04864',
lineWidth: 2,
draggable: true,
},
@@ -262,14 +262,14 @@
});
}
- circle.addEventListener("dragstart", function (e) {
+ circle.addEventListener('dragstart', function (e) {
const [x, y] = e.target.getPosition();
shiftX = e.canvasX - x;
shiftY = e.canvasY - y;
moveAt(e.target, e.canvasX, e.canvasY);
});
- circle.addEventListener("drag", function (e) {
+ circle.addEventListener('drag', function (e) {
moveAt(e.target, e.canvasX, e.canvasY);
});
nodes.push(circle);
@@ -279,10 +279,10 @@
const updateNodesEdges = (positions) => {
positions.edges.forEach(({ source, target }, i) => {
const sourceNode = positions.nodes.find(
- ({ id }) => id === source
+ ({ id }) => id === source,
);
const targetNode = positions.nodes.find(
- ({ id }) => id === target
+ ({ id }) => id === target,
);
edges[i].attr({
@@ -302,12 +302,14 @@
};
force.assign(graph, {
- center: [200, 200], // The center of the graph by default
- preventOverlap: true,
+ center: {
+ x: 250,
+ y: 250,
+ },
nodeSize: 20,
});
force.stop();
- const positions = force.tick(1000);
+ const positions = force.tick(10);
createNodesEdges(positions);
});
diff --git a/jest.config.js b/jest.config.js
index b3a067a..a539226 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,14 +1,3 @@
-// module.exports = {
-
-// preset: "ts-jest",
-// collectCoverage: false,
-// collectCoverageFrom: [
-// "packages/layout/src/**/*.{ts,js}",
-// "!**/node_modules/**",
-// "!**/vendor/**",
-// ],
-
-/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
testEnvironment: 'jsdom',
testRegex: '__tests__/.*test\\.ts?$',
@@ -19,13 +8,6 @@ module.exports = {
'@antv/layout/(.*)': '/src/$1',
},
transform: {
- // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
- // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
- '^.+\\.tsx?$': [
- 'ts-jest',
- {
- diagnostics: false,
- },
- ],
+ '^.+\\.tsx?$': ['@swc/jest'],
},
};
diff --git a/package.json b/package.json
index 7d0531d..5aebb12 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,8 @@
"@babel/preset-env": "^7.24.0",
"@babel/preset-typescript": "^7.23.3",
"@changesets/cli": "^2.27.1",
+ "@swc/core": "^1.4.12",
+ "@swc/jest": "^0.2.36",
"@types/jest": "latest",
"babel-jest": "29.7.0",
"babel-loader": "^8.3.0",
@@ -74,7 +76,6 @@
"prettier-plugin-organize-imports": "^3.2.4",
"rimraf": "^3.0.2",
"seedrandom": "^3.0.5",
- "ts-jest": "^29.1.2",
"ts-loader": "^8.4.0",
"typescript": "^4.9.5",
"vite": "^4.5.2"
diff --git a/packages/layout/__tests__/dataset/force-3d.json b/packages/layout/__tests__/dataset/force-3d.json
new file mode 100644
index 0000000..3572b89
--- /dev/null
+++ b/packages/layout/__tests__/dataset/force-3d.json
@@ -0,0 +1,1838 @@
+{
+ "nodes": [
+ {
+ "id": "Myriel",
+ "data": { "group": 1 }
+ },
+ {
+ "id": "Napoleon",
+ "data": { "group": 1 }
+ },
+ {
+ "id": "Mlle.Baptistine",
+ "data": { "group": 1 }
+ },
+ {
+ "id": "Mme.Magloire",
+ "data": { "group": 1 }
+ },
+ {
+ "id": "CountessdeLo",
+ "data": { "group": 1 }
+ },
+ {
+ "id": "Geborand",
+ "data": { "group": 1 }
+ },
+ {
+ "id": "Champtercier",
+ "data": { "group": 1 }
+ },
+ {
+ "id": "Cravatte",
+ "data": { "group": 1 }
+ },
+ {
+ "id": "Count",
+ "data": { "group": 1 }
+ },
+ {
+ "id": "OldMan",
+ "data": { "group": 1 }
+ },
+ {
+ "id": "Labarre",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Valjean",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Marguerite",
+ "data": { "group": 3 }
+ },
+ {
+ "id": "Mme.deR",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Isabeau",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Gervais",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Tholomyes",
+ "data": { "group": 3 }
+ },
+ {
+ "id": "Listolier",
+ "data": { "group": 3 }
+ },
+ {
+ "id": "Fameuil",
+ "data": { "group": 3 }
+ },
+ {
+ "id": "Blacheville",
+ "data": { "group": 3 }
+ },
+ {
+ "id": "Favourite",
+ "data": { "group": 3 }
+ },
+ {
+ "id": "Dahlia",
+ "data": { "group": 3 }
+ },
+ {
+ "id": "Zephine",
+ "data": { "group": 3 }
+ },
+ {
+ "id": "Fantine",
+ "data": { "group": 3 }
+ },
+ {
+ "id": "Mme.Thenardier",
+ "data": { "group": 4 }
+ },
+ {
+ "id": "Thenardier",
+ "data": { "group": 4 }
+ },
+ {
+ "id": "Cosette",
+ "data": { "group": 5 }
+ },
+ {
+ "id": "Javert",
+ "data": { "group": 4 }
+ },
+ {
+ "id": "Fauchelevent",
+ "data": { "group": 0 }
+ },
+ {
+ "id": "Bamatabois",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Perpetue",
+ "data": { "group": 3 }
+ },
+ {
+ "id": "Simplice",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Scaufflaire",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Woman1",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Judge",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Champmathieu",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Brevet",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Chenildieu",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Cochepaille",
+ "data": { "group": 2 }
+ },
+ {
+ "id": "Pontmercy",
+ "data": { "group": 4 }
+ },
+ {
+ "id": "Boulatruelle",
+ "data": { "group": 6 }
+ },
+ {
+ "id": "Eponine",
+ "data": { "group": 4 }
+ },
+ {
+ "id": "Anzelma",
+ "data": { "group": 4 }
+ },
+ {
+ "id": "Woman2",
+ "data": { "group": 5 }
+ },
+ {
+ "id": "MotherInnocent",
+ "data": { "group": 0 }
+ },
+ {
+ "id": "Gribier",
+ "data": { "group": 0 }
+ },
+ {
+ "id": "Jondrette",
+ "data": { "group": 7 }
+ },
+ {
+ "id": "Mme.Burgon",
+ "data": { "group": 7 }
+ },
+ {
+ "id": "Gavroche",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "Gillenormand",
+ "data": { "group": 5 }
+ },
+ {
+ "id": "Magnon",
+ "data": { "group": 5 }
+ },
+ {
+ "id": "Mlle.Gillenormand",
+ "data": { "group": 5 }
+ },
+ {
+ "id": "Mme.Pontmercy",
+ "data": { "group": 5 }
+ },
+ {
+ "id": "Mlle.Vaubois",
+ "data": { "group": 5 }
+ },
+ {
+ "id": "Lt.Gillenormand",
+ "data": { "group": 5 }
+ },
+ {
+ "id": "Marius",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "BaronessT",
+ "data": { "group": 5 }
+ },
+ {
+ "id": "Mabeuf",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "Enjolras",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "Combeferre",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "Prouvaire",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "Feuilly",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "Courfeyrac",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "Bahorel",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "Bossuet",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "Joly",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "Grantaire",
+ "data": { "group": 8 }
+ },
+ {
+ "id": "MotherPlutarch",
+ "data": { "group": 9 }
+ },
+ {
+ "id": "Gueulemer",
+ "data": { "group": 4 }
+ },
+ {
+ "id": "Babet",
+ "data": { "group": 4 }
+ },
+ {
+ "id": "Claquesous",
+ "data": { "group": 4 }
+ },
+ {
+ "id": "Montparnasse",
+ "data": { "group": 4 }
+ },
+ {
+ "id": "Toussaint",
+ "data": { "group": 5 }
+ },
+ {
+ "id": "Child1",
+ "data": { "group": 10 }
+ },
+ {
+ "id": "Child2",
+ "data": { "group": 10 }
+ },
+ {
+ "id": "Brujon",
+ "data": { "group": 4 }
+ },
+ {
+ "id": "Mme.Hucheloup",
+ "data": { "group": 8 }
+ }
+ ],
+ "edges": [
+ {
+ "id": "Napoleon-Myriel",
+ "source": "Napoleon",
+ "target": "Myriel",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mlle.Baptistine-Myriel",
+ "source": "Mlle.Baptistine",
+ "target": "Myriel",
+ "data": { "value": 8 }
+ },
+ {
+ "id": "Mme.Magloire-Myriel",
+ "source": "Mme.Magloire",
+ "target": "Myriel",
+ "data": { "value": 10 }
+ },
+ {
+ "id": "Mme.Magloire-Mlle.Baptistine",
+ "source": "Mme.Magloire",
+ "target": "Mlle.Baptistine",
+ "data": { "value": 6 }
+ },
+ {
+ "id": "CountessdeLo-Myriel",
+ "source": "CountessdeLo",
+ "target": "Myriel",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Geborand-Myriel",
+ "source": "Geborand",
+ "target": "Myriel",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Champtercier-Myriel",
+ "source": "Champtercier",
+ "target": "Myriel",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Cravatte-Myriel",
+ "source": "Cravatte",
+ "target": "Myriel",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Count-Myriel",
+ "source": "Count",
+ "target": "Myriel",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "OldMan-Myriel",
+ "source": "OldMan",
+ "target": "Myriel",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Valjean-Labarre",
+ "source": "Valjean",
+ "target": "Labarre",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Valjean-Mme.Magloire",
+ "source": "Valjean",
+ "target": "Mme.Magloire",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Valjean-Mlle.Baptistine",
+ "source": "Valjean",
+ "target": "Mlle.Baptistine",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Valjean-Myriel",
+ "source": "Valjean",
+ "target": "Myriel",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Marguerite-Valjean",
+ "source": "Marguerite",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mme.deR-Valjean",
+ "source": "Mme.deR",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Isabeau-Valjean",
+ "source": "Isabeau",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Gervais-Valjean",
+ "source": "Gervais",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Listolier-Tholomyes",
+ "source": "Listolier",
+ "target": "Tholomyes",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Fameuil-Tholomyes",
+ "source": "Fameuil",
+ "target": "Tholomyes",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Fameuil-Listolier",
+ "source": "Fameuil",
+ "target": "Listolier",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Blacheville-Tholomyes",
+ "source": "Blacheville",
+ "target": "Tholomyes",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Blacheville-Listolier",
+ "source": "Blacheville",
+ "target": "Listolier",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Blacheville-Fameuil",
+ "source": "Blacheville",
+ "target": "Fameuil",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Favourite-Tholomyes",
+ "source": "Favourite",
+ "target": "Tholomyes",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Favourite-Listolier",
+ "source": "Favourite",
+ "target": "Listolier",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Favourite-Fameuil",
+ "source": "Favourite",
+ "target": "Fameuil",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Favourite-Blacheville",
+ "source": "Favourite",
+ "target": "Blacheville",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Dahlia-Tholomyes",
+ "source": "Dahlia",
+ "target": "Tholomyes",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Dahlia-Listolier",
+ "source": "Dahlia",
+ "target": "Listolier",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Dahlia-Fameuil",
+ "source": "Dahlia",
+ "target": "Fameuil",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Dahlia-Blacheville",
+ "source": "Dahlia",
+ "target": "Blacheville",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Dahlia-Favourite",
+ "source": "Dahlia",
+ "target": "Favourite",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Zephine-Tholomyes",
+ "source": "Zephine",
+ "target": "Tholomyes",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Zephine-Listolier",
+ "source": "Zephine",
+ "target": "Listolier",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Zephine-Fameuil",
+ "source": "Zephine",
+ "target": "Fameuil",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Zephine-Blacheville",
+ "source": "Zephine",
+ "target": "Blacheville",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Zephine-Favourite",
+ "source": "Zephine",
+ "target": "Favourite",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Zephine-Dahlia",
+ "source": "Zephine",
+ "target": "Dahlia",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Fantine-Tholomyes",
+ "source": "Fantine",
+ "target": "Tholomyes",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Fantine-Listolier",
+ "source": "Fantine",
+ "target": "Listolier",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Fantine-Fameuil",
+ "source": "Fantine",
+ "target": "Fameuil",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Fantine-Blacheville",
+ "source": "Fantine",
+ "target": "Blacheville",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Fantine-Favourite",
+ "source": "Fantine",
+ "target": "Favourite",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Fantine-Dahlia",
+ "source": "Fantine",
+ "target": "Dahlia",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Fantine-Zephine",
+ "source": "Fantine",
+ "target": "Zephine",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Fantine-Marguerite",
+ "source": "Fantine",
+ "target": "Marguerite",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Fantine-Valjean",
+ "source": "Fantine",
+ "target": "Valjean",
+ "data": { "value": 9 }
+ },
+ {
+ "id": "Mme.Thenardier-Fantine",
+ "source": "Mme.Thenardier",
+ "target": "Fantine",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Mme.Thenardier-Valjean",
+ "source": "Mme.Thenardier",
+ "target": "Valjean",
+ "data": { "value": 7 }
+ },
+ {
+ "id": "Thenardier-Mme.Thenardier",
+ "source": "Thenardier",
+ "target": "Mme.Thenardier",
+ "data": { "value": 13 }
+ },
+ {
+ "id": "Thenardier-Fantine",
+ "source": "Thenardier",
+ "target": "Fantine",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Thenardier-Valjean",
+ "source": "Thenardier",
+ "target": "Valjean",
+ "data": { "value": 12 }
+ },
+ {
+ "id": "Cosette-Mme.Thenardier",
+ "source": "Cosette",
+ "target": "Mme.Thenardier",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Cosette-Valjean",
+ "source": "Cosette",
+ "target": "Valjean",
+ "data": { "value": 31 }
+ },
+ {
+ "id": "Cosette-Tholomyes",
+ "source": "Cosette",
+ "target": "Tholomyes",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Cosette-Thenardier",
+ "source": "Cosette",
+ "target": "Thenardier",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Javert-Valjean",
+ "source": "Javert",
+ "target": "Valjean",
+ "data": { "value": 17 }
+ },
+ {
+ "id": "Javert-Fantine",
+ "source": "Javert",
+ "target": "Fantine",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Javert-Thenardier",
+ "source": "Javert",
+ "target": "Thenardier",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Javert-Mme.Thenardier",
+ "source": "Javert",
+ "target": "Mme.Thenardier",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Javert-Cosette",
+ "source": "Javert",
+ "target": "Cosette",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Fauchelevent-Valjean",
+ "source": "Fauchelevent",
+ "target": "Valjean",
+ "data": { "value": 8 }
+ },
+ {
+ "id": "Fauchelevent-Javert",
+ "source": "Fauchelevent",
+ "target": "Javert",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Bamatabois-Fantine",
+ "source": "Bamatabois",
+ "target": "Fantine",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Bamatabois-Javert",
+ "source": "Bamatabois",
+ "target": "Javert",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Bamatabois-Valjean",
+ "source": "Bamatabois",
+ "target": "Valjean",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Perpetue-Fantine",
+ "source": "Perpetue",
+ "target": "Fantine",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Simplice-Perpetue",
+ "source": "Simplice",
+ "target": "Perpetue",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Simplice-Valjean",
+ "source": "Simplice",
+ "target": "Valjean",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Simplice-Fantine",
+ "source": "Simplice",
+ "target": "Fantine",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Simplice-Javert",
+ "source": "Simplice",
+ "target": "Javert",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Scaufflaire-Valjean",
+ "source": "Scaufflaire",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Woman1-Valjean",
+ "source": "Woman1",
+ "target": "Valjean",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Woman1-Javert",
+ "source": "Woman1",
+ "target": "Javert",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Judge-Valjean",
+ "source": "Judge",
+ "target": "Valjean",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Judge-Bamatabois",
+ "source": "Judge",
+ "target": "Bamatabois",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Champmathieu-Valjean",
+ "source": "Champmathieu",
+ "target": "Valjean",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Champmathieu-Judge",
+ "source": "Champmathieu",
+ "target": "Judge",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Champmathieu-Bamatabois",
+ "source": "Champmathieu",
+ "target": "Bamatabois",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Brevet-Judge",
+ "source": "Brevet",
+ "target": "Judge",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Brevet-Champmathieu",
+ "source": "Brevet",
+ "target": "Champmathieu",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Brevet-Valjean",
+ "source": "Brevet",
+ "target": "Valjean",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Brevet-Bamatabois",
+ "source": "Brevet",
+ "target": "Bamatabois",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Chenildieu-Judge",
+ "source": "Chenildieu",
+ "target": "Judge",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Chenildieu-Champmathieu",
+ "source": "Chenildieu",
+ "target": "Champmathieu",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Chenildieu-Brevet",
+ "source": "Chenildieu",
+ "target": "Brevet",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Chenildieu-Valjean",
+ "source": "Chenildieu",
+ "target": "Valjean",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Chenildieu-Bamatabois",
+ "source": "Chenildieu",
+ "target": "Bamatabois",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Cochepaille-Judge",
+ "source": "Cochepaille",
+ "target": "Judge",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Cochepaille-Champmathieu",
+ "source": "Cochepaille",
+ "target": "Champmathieu",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Cochepaille-Brevet",
+ "source": "Cochepaille",
+ "target": "Brevet",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Cochepaille-Chenildieu",
+ "source": "Cochepaille",
+ "target": "Chenildieu",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Cochepaille-Valjean",
+ "source": "Cochepaille",
+ "target": "Valjean",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Cochepaille-Bamatabois",
+ "source": "Cochepaille",
+ "target": "Bamatabois",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Pontmercy-Thenardier",
+ "source": "Pontmercy",
+ "target": "Thenardier",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Boulatruelle-Thenardier",
+ "source": "Boulatruelle",
+ "target": "Thenardier",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Eponine-Mme.Thenardier",
+ "source": "Eponine",
+ "target": "Mme.Thenardier",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Eponine-Thenardier",
+ "source": "Eponine",
+ "target": "Thenardier",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Anzelma-Eponine",
+ "source": "Anzelma",
+ "target": "Eponine",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Anzelma-Thenardier",
+ "source": "Anzelma",
+ "target": "Thenardier",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Anzelma-Mme.Thenardier",
+ "source": "Anzelma",
+ "target": "Mme.Thenardier",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Woman2-Valjean",
+ "source": "Woman2",
+ "target": "Valjean",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Woman2-Cosette",
+ "source": "Woman2",
+ "target": "Cosette",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Woman2-Javert",
+ "source": "Woman2",
+ "target": "Javert",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "MotherInnocent-Fauchelevent",
+ "source": "MotherInnocent",
+ "target": "Fauchelevent",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "MotherInnocent-Valjean",
+ "source": "MotherInnocent",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Gribier-Fauchelevent",
+ "source": "Gribier",
+ "target": "Fauchelevent",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Mme.Burgon-Jondrette",
+ "source": "Mme.Burgon",
+ "target": "Jondrette",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Gavroche-Mme.Burgon",
+ "source": "Gavroche",
+ "target": "Mme.Burgon",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Gavroche-Thenardier",
+ "source": "Gavroche",
+ "target": "Thenardier",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Gavroche-Javert",
+ "source": "Gavroche",
+ "target": "Javert",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Gavroche-Valjean",
+ "source": "Gavroche",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Gillenormand-Cosette",
+ "source": "Gillenormand",
+ "target": "Cosette",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Gillenormand-Valjean",
+ "source": "Gillenormand",
+ "target": "Valjean",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Magnon-Gillenormand",
+ "source": "Magnon",
+ "target": "Gillenormand",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Magnon-Mme.Thenardier",
+ "source": "Magnon",
+ "target": "Mme.Thenardier",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mlle.Gillenormand-Gillenormand",
+ "source": "Mlle.Gillenormand",
+ "target": "Gillenormand",
+ "data": { "value": 9 }
+ },
+ {
+ "id": "Mlle.Gillenormand-Cosette",
+ "source": "Mlle.Gillenormand",
+ "target": "Cosette",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Mlle.Gillenormand-Valjean",
+ "source": "Mlle.Gillenormand",
+ "target": "Valjean",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Mme.Pontmercy-Mlle.Gillenormand",
+ "source": "Mme.Pontmercy",
+ "target": "Mlle.Gillenormand",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mme.Pontmercy-Pontmercy",
+ "source": "Mme.Pontmercy",
+ "target": "Pontmercy",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mlle.Vaubois-Mlle.Gillenormand",
+ "source": "Mlle.Vaubois",
+ "target": "Mlle.Gillenormand",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Lt.Gillenormand-Mlle.Gillenormand",
+ "source": "Lt.Gillenormand",
+ "target": "Mlle.Gillenormand",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Lt.Gillenormand-Gillenormand",
+ "source": "Lt.Gillenormand",
+ "target": "Gillenormand",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Lt.Gillenormand-Cosette",
+ "source": "Lt.Gillenormand",
+ "target": "Cosette",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Marius-Mlle.Gillenormand",
+ "source": "Marius",
+ "target": "Mlle.Gillenormand",
+ "data": { "value": 6 }
+ },
+ {
+ "id": "Marius-Gillenormand",
+ "source": "Marius",
+ "target": "Gillenormand",
+ "data": { "value": 12 }
+ },
+ {
+ "id": "Marius-Pontmercy",
+ "source": "Marius",
+ "target": "Pontmercy",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Marius-Lt.Gillenormand",
+ "source": "Marius",
+ "target": "Lt.Gillenormand",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Marius-Cosette",
+ "source": "Marius",
+ "target": "Cosette",
+ "data": { "value": 21 }
+ },
+ {
+ "id": "Marius-Valjean",
+ "source": "Marius",
+ "target": "Valjean",
+ "data": { "value": 19 }
+ },
+ {
+ "id": "Marius-Tholomyes",
+ "source": "Marius",
+ "target": "Tholomyes",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Marius-Thenardier",
+ "source": "Marius",
+ "target": "Thenardier",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Marius-Eponine",
+ "source": "Marius",
+ "target": "Eponine",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Marius-Gavroche",
+ "source": "Marius",
+ "target": "Gavroche",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "BaronessT-Gillenormand",
+ "source": "BaronessT",
+ "target": "Gillenormand",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "BaronessT-Marius",
+ "source": "BaronessT",
+ "target": "Marius",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mabeuf-Marius",
+ "source": "Mabeuf",
+ "target": "Marius",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mabeuf-Eponine",
+ "source": "Mabeuf",
+ "target": "Eponine",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mabeuf-Gavroche",
+ "source": "Mabeuf",
+ "target": "Gavroche",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Enjolras-Marius",
+ "source": "Enjolras",
+ "target": "Marius",
+ "data": { "value": 7 }
+ },
+ {
+ "id": "Enjolras-Gavroche",
+ "source": "Enjolras",
+ "target": "Gavroche",
+ "data": { "value": 7 }
+ },
+ {
+ "id": "Enjolras-Javert",
+ "source": "Enjolras",
+ "target": "Javert",
+ "data": { "value": 6 }
+ },
+ {
+ "id": "Enjolras-Mabeuf",
+ "source": "Enjolras",
+ "target": "Mabeuf",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Enjolras-Valjean",
+ "source": "Enjolras",
+ "target": "Valjean",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Combeferre-Enjolras",
+ "source": "Combeferre",
+ "target": "Enjolras",
+ "data": { "value": 15 }
+ },
+ {
+ "id": "Combeferre-Marius",
+ "source": "Combeferre",
+ "target": "Marius",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Combeferre-Gavroche",
+ "source": "Combeferre",
+ "target": "Gavroche",
+ "data": { "value": 6 }
+ },
+ {
+ "id": "Combeferre-Mabeuf",
+ "source": "Combeferre",
+ "target": "Mabeuf",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Prouvaire-Gavroche",
+ "source": "Prouvaire",
+ "target": "Gavroche",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Prouvaire-Enjolras",
+ "source": "Prouvaire",
+ "target": "Enjolras",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Prouvaire-Combeferre",
+ "source": "Prouvaire",
+ "target": "Combeferre",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Feuilly-Gavroche",
+ "source": "Feuilly",
+ "target": "Gavroche",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Feuilly-Enjolras",
+ "source": "Feuilly",
+ "target": "Enjolras",
+ "data": { "value": 6 }
+ },
+ {
+ "id": "Feuilly-Prouvaire",
+ "source": "Feuilly",
+ "target": "Prouvaire",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Feuilly-Combeferre",
+ "source": "Feuilly",
+ "target": "Combeferre",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Feuilly-Mabeuf",
+ "source": "Feuilly",
+ "target": "Mabeuf",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Feuilly-Marius",
+ "source": "Feuilly",
+ "target": "Marius",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Courfeyrac-Marius",
+ "source": "Courfeyrac",
+ "target": "Marius",
+ "data": { "value": 9 }
+ },
+ {
+ "id": "Courfeyrac-Enjolras",
+ "source": "Courfeyrac",
+ "target": "Enjolras",
+ "data": { "value": 17 }
+ },
+ {
+ "id": "Courfeyrac-Combeferre",
+ "source": "Courfeyrac",
+ "target": "Combeferre",
+ "data": { "value": 13 }
+ },
+ {
+ "id": "Courfeyrac-Gavroche",
+ "source": "Courfeyrac",
+ "target": "Gavroche",
+ "data": { "value": 7 }
+ },
+ {
+ "id": "Courfeyrac-Mabeuf",
+ "source": "Courfeyrac",
+ "target": "Mabeuf",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Courfeyrac-Eponine",
+ "source": "Courfeyrac",
+ "target": "Eponine",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Courfeyrac-Feuilly",
+ "source": "Courfeyrac",
+ "target": "Feuilly",
+ "data": { "value": 6 }
+ },
+ {
+ "id": "Courfeyrac-Prouvaire",
+ "source": "Courfeyrac",
+ "target": "Prouvaire",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Bahorel-Combeferre",
+ "source": "Bahorel",
+ "target": "Combeferre",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Bahorel-Gavroche",
+ "source": "Bahorel",
+ "target": "Gavroche",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Bahorel-Courfeyrac",
+ "source": "Bahorel",
+ "target": "Courfeyrac",
+ "data": { "value": 6 }
+ },
+ {
+ "id": "Bahorel-Mabeuf",
+ "source": "Bahorel",
+ "target": "Mabeuf",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Bahorel-Enjolras",
+ "source": "Bahorel",
+ "target": "Enjolras",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Bahorel-Feuilly",
+ "source": "Bahorel",
+ "target": "Feuilly",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Bahorel-Prouvaire",
+ "source": "Bahorel",
+ "target": "Prouvaire",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Bahorel-Marius",
+ "source": "Bahorel",
+ "target": "Marius",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Bossuet-Marius",
+ "source": "Bossuet",
+ "target": "Marius",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Bossuet-Courfeyrac",
+ "source": "Bossuet",
+ "target": "Courfeyrac",
+ "data": { "value": 12 }
+ },
+ {
+ "id": "Bossuet-Gavroche",
+ "source": "Bossuet",
+ "target": "Gavroche",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Bossuet-Bahorel",
+ "source": "Bossuet",
+ "target": "Bahorel",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Bossuet-Enjolras",
+ "source": "Bossuet",
+ "target": "Enjolras",
+ "data": { "value": 10 }
+ },
+ {
+ "id": "Bossuet-Feuilly",
+ "source": "Bossuet",
+ "target": "Feuilly",
+ "data": { "value": 6 }
+ },
+ {
+ "id": "Bossuet-Prouvaire",
+ "source": "Bossuet",
+ "target": "Prouvaire",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Bossuet-Combeferre",
+ "source": "Bossuet",
+ "target": "Combeferre",
+ "data": { "value": 9 }
+ },
+ {
+ "id": "Bossuet-Mabeuf",
+ "source": "Bossuet",
+ "target": "Mabeuf",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Bossuet-Valjean",
+ "source": "Bossuet",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Joly-Bahorel",
+ "source": "Joly",
+ "target": "Bahorel",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Joly-Bossuet",
+ "source": "Joly",
+ "target": "Bossuet",
+ "data": { "value": 7 }
+ },
+ {
+ "id": "Joly-Gavroche",
+ "source": "Joly",
+ "target": "Gavroche",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Joly-Courfeyrac",
+ "source": "Joly",
+ "target": "Courfeyrac",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Joly-Enjolras",
+ "source": "Joly",
+ "target": "Enjolras",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Joly-Feuilly",
+ "source": "Joly",
+ "target": "Feuilly",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Joly-Prouvaire",
+ "source": "Joly",
+ "target": "Prouvaire",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Joly-Combeferre",
+ "source": "Joly",
+ "target": "Combeferre",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Joly-Mabeuf",
+ "source": "Joly",
+ "target": "Mabeuf",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Joly-Marius",
+ "source": "Joly",
+ "target": "Marius",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Grantaire-Bossuet",
+ "source": "Grantaire",
+ "target": "Bossuet",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Grantaire-Enjolras",
+ "source": "Grantaire",
+ "target": "Enjolras",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Grantaire-Combeferre",
+ "source": "Grantaire",
+ "target": "Combeferre",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Grantaire-Courfeyrac",
+ "source": "Grantaire",
+ "target": "Courfeyrac",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Grantaire-Joly",
+ "source": "Grantaire",
+ "target": "Joly",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Grantaire-Gavroche",
+ "source": "Grantaire",
+ "target": "Gavroche",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Grantaire-Bahorel",
+ "source": "Grantaire",
+ "target": "Bahorel",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Grantaire-Feuilly",
+ "source": "Grantaire",
+ "target": "Feuilly",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Grantaire-Prouvaire",
+ "source": "Grantaire",
+ "target": "Prouvaire",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "MotherPlutarch-Mabeuf",
+ "source": "MotherPlutarch",
+ "target": "Mabeuf",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Gueulemer-Thenardier",
+ "source": "Gueulemer",
+ "target": "Thenardier",
+ "data": { "value": 5 }
+ },
+ {
+ "id": "Gueulemer-Valjean",
+ "source": "Gueulemer",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Gueulemer-Mme.Thenardier",
+ "source": "Gueulemer",
+ "target": "Mme.Thenardier",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Gueulemer-Javert",
+ "source": "Gueulemer",
+ "target": "Javert",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Gueulemer-Gavroche",
+ "source": "Gueulemer",
+ "target": "Gavroche",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Gueulemer-Eponine",
+ "source": "Gueulemer",
+ "target": "Eponine",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Babet-Thenardier",
+ "source": "Babet",
+ "target": "Thenardier",
+ "data": { "value": 6 }
+ },
+ {
+ "id": "Babet-Gueulemer",
+ "source": "Babet",
+ "target": "Gueulemer",
+ "data": { "value": 6 }
+ },
+ {
+ "id": "Babet-Valjean",
+ "source": "Babet",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Babet-Mme.Thenardier",
+ "source": "Babet",
+ "target": "Mme.Thenardier",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Babet-Javert",
+ "source": "Babet",
+ "target": "Javert",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Babet-Gavroche",
+ "source": "Babet",
+ "target": "Gavroche",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Babet-Eponine",
+ "source": "Babet",
+ "target": "Eponine",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Claquesous-Thenardier",
+ "source": "Claquesous",
+ "target": "Thenardier",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Claquesous-Babet",
+ "source": "Claquesous",
+ "target": "Babet",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Claquesous-Gueulemer",
+ "source": "Claquesous",
+ "target": "Gueulemer",
+ "data": { "value": 4 }
+ },
+ {
+ "id": "Claquesous-Valjean",
+ "source": "Claquesous",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Claquesous-Mme.Thenardier",
+ "source": "Claquesous",
+ "target": "Mme.Thenardier",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Claquesous-Javert",
+ "source": "Claquesous",
+ "target": "Javert",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Claquesous-Eponine",
+ "source": "Claquesous",
+ "target": "Eponine",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Claquesous-Enjolras",
+ "source": "Claquesous",
+ "target": "Enjolras",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Montparnasse-Javert",
+ "source": "Montparnasse",
+ "target": "Javert",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Montparnasse-Babet",
+ "source": "Montparnasse",
+ "target": "Babet",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Montparnasse-Gueulemer",
+ "source": "Montparnasse",
+ "target": "Gueulemer",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Montparnasse-Claquesous",
+ "source": "Montparnasse",
+ "target": "Claquesous",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Montparnasse-Valjean",
+ "source": "Montparnasse",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Montparnasse-Gavroche",
+ "source": "Montparnasse",
+ "target": "Gavroche",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Montparnasse-Eponine",
+ "source": "Montparnasse",
+ "target": "Eponine",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Montparnasse-Thenardier",
+ "source": "Montparnasse",
+ "target": "Thenardier",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Toussaint-Cosette",
+ "source": "Toussaint",
+ "target": "Cosette",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Toussaint-Javert",
+ "source": "Toussaint",
+ "target": "Javert",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Toussaint-Valjean",
+ "source": "Toussaint",
+ "target": "Valjean",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Child1-Gavroche",
+ "source": "Child1",
+ "target": "Gavroche",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Child2-Gavroche",
+ "source": "Child2",
+ "target": "Gavroche",
+ "data": { "value": 2 }
+ },
+ {
+ "id": "Child2-Child1",
+ "source": "Child2",
+ "target": "Child1",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Brujon-Babet",
+ "source": "Brujon",
+ "target": "Babet",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Brujon-Gueulemer",
+ "source": "Brujon",
+ "target": "Gueulemer",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Brujon-Thenardier",
+ "source": "Brujon",
+ "target": "Thenardier",
+ "data": { "value": 3 }
+ },
+ {
+ "id": "Brujon-Gavroche",
+ "source": "Brujon",
+ "target": "Gavroche",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Brujon-Eponine",
+ "source": "Brujon",
+ "target": "Eponine",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Brujon-Claquesous",
+ "source": "Brujon",
+ "target": "Claquesous",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Brujon-Montparnasse",
+ "source": "Brujon",
+ "target": "Montparnasse",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mme.Hucheloup-Bossuet",
+ "source": "Mme.Hucheloup",
+ "target": "Bossuet",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mme.Hucheloup-Joly",
+ "source": "Mme.Hucheloup",
+ "target": "Joly",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mme.Hucheloup-Grantaire",
+ "source": "Mme.Hucheloup",
+ "target": "Grantaire",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mme.Hucheloup-Bahorel",
+ "source": "Mme.Hucheloup",
+ "target": "Bahorel",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mme.Hucheloup-Courfeyrac",
+ "source": "Mme.Hucheloup",
+ "target": "Courfeyrac",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mme.Hucheloup-Gavroche",
+ "source": "Mme.Hucheloup",
+ "target": "Gavroche",
+ "data": { "value": 1 }
+ },
+ {
+ "id": "Mme.Hucheloup-Enjolras",
+ "source": "Mme.Hucheloup",
+ "target": "Enjolras",
+ "data": { "value": 1 }
+ }
+ ]
+}
diff --git a/packages/layout/__tests__/tsconfig.json b/packages/layout/__tests__/tsconfig.json
new file mode 100644
index 0000000..289c125
--- /dev/null
+++ b/packages/layout/__tests__/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../tsconfig.json",
+ "include": ["../src/**/*", "__tests__/**/*"],
+ "exclude": [],
+ "compilerOptions": {
+ "rootDir": "..",
+ "resolveJsonModule": true
+ }
+}
\ No newline at end of file
diff --git a/packages/layout/__tests__/unit/d3-force-3d.test.ts b/packages/layout/__tests__/unit/d3-force-3d.test.ts
new file mode 100644
index 0000000..51ee435
--- /dev/null
+++ b/packages/layout/__tests__/unit/d3-force-3d.test.ts
@@ -0,0 +1,52 @@
+import { Graph } from '@antv/graphlib';
+import { D3Force3DLayout } from '../../src/d3-force-3d';
+import type { EdgeData, NodeData } from '../../src/types';
+import data from '../dataset/force-3d.json';
+
+describe('d3 force 3d', () => {
+ test('default layout', async () => {
+ const graph = new Graph(data);
+
+ const d3Force3D = new D3Force3DLayout();
+
+ const positions = await d3Force3D.execute(graph);
+
+ expect(positions.nodes.length).toBe(data.nodes.length);
+ // @ts-ignore
+ expect(data.nodes[0].x).toBeUndefined();
+ // @ts-ignore
+ expect(data.nodes[0].y).toBeUndefined();
+ // @ts-ignore
+ expect(data.nodes[0].z).toBeUndefined();
+ // @ts-ignore
+ expect(data.nodes[0].vx).toBeUndefined();
+ // @ts-ignore
+ expect(data.nodes[0].vy).toBeUndefined();
+ // @ts-ignore
+ expect(data.nodes[0].vz).toBeUndefined();
+
+ expect(positions.nodes[0].data.x).toBeDefined();
+ expect(positions.nodes[0].data.y).toBeDefined();
+ expect(positions.nodes[0].data.z).toBeDefined();
+ expect(positions.nodes[0].data.vx).toBeDefined();
+ expect(positions.nodes[0].data.vy).toBeDefined();
+ expect(positions.nodes[0].data.vz).toBeDefined();
+
+ expect(positions.edges.length).toBe(data.edges.length);
+ expect(positions.edges[0].source).toBe(data.edges[0].source);
+ expect(positions.edges[0].target).toBe(data.edges[0].target);
+ });
+
+ test('tick layout', async () => {
+ const graph = new Graph(data);
+ const d3Force3D = new D3Force3DLayout();
+
+ const onTick = jest.fn();
+
+ await d3Force3D.execute(graph, {
+ onTick,
+ });
+
+ expect(onTick).toHaveBeenCalledTimes(300);
+ });
+});
diff --git a/packages/layout/__tests__/unit/d3-force.test.ts b/packages/layout/__tests__/unit/d3-force.test.ts
new file mode 100644
index 0000000..461c46b
--- /dev/null
+++ b/packages/layout/__tests__/unit/d3-force.test.ts
@@ -0,0 +1,46 @@
+import { Graph } from '@antv/graphlib';
+import { D3ForceLayout } from '../../src/d3-force';
+import type { EdgeData, NodeData } from '../../src/types';
+import data from '../dataset/force-3d.json';
+
+describe('d3 force', () => {
+ test('default layout', async () => {
+ const graph = new Graph(data);
+
+ const d3Force3D = new D3ForceLayout();
+
+ const positions = await d3Force3D.execute(graph);
+
+ expect(positions.nodes.length).toBe(data.nodes.length);
+ // @ts-ignore
+ expect(data.nodes[0].x).toBeUndefined();
+ // @ts-ignore
+ expect(data.nodes[0].y).toBeUndefined();
+ // @ts-ignore
+ expect(data.nodes[0].vx).toBeUndefined();
+ // @ts-ignore
+ expect(data.nodes[0].vy).toBeUndefined();
+
+ expect(positions.nodes[0].data.x).toBeDefined();
+ expect(positions.nodes[0].data.y).toBeDefined();
+ expect(positions.nodes[0].data.vx).toBeDefined();
+ expect(positions.nodes[0].data.vy).toBeDefined();
+
+ expect(positions.edges.length).toBe(data.edges.length);
+ expect(positions.edges[0].source).toBe(data.edges[0].source);
+ expect(positions.edges[0].target).toBe(data.edges[0].target);
+ });
+
+ test('tick layout', async () => {
+ const graph = new Graph(data);
+ const d3Force = new D3ForceLayout();
+
+ const onTick = jest.fn();
+
+ await d3Force.execute(graph, {
+ onTick,
+ });
+
+ expect(onTick).toHaveBeenCalledTimes(300);
+ });
+});
diff --git a/packages/layout/jest.config.js b/packages/layout/jest.config.js
index 013f281..4962eae 100644
--- a/packages/layout/jest.config.js
+++ b/packages/layout/jest.config.js
@@ -1,24 +1,12 @@
module.exports = {
- preset: 'ts-jest/presets/js-with-ts',
+ testTimeout: 10 * 1000,
transform: {
- '^.+\\.[tj]s$': [
- 'ts-jest',
- {
- diagnostics: {
- exclude: ['**'],
- },
- tsconfig: {
- allowJs: true,
- target: 'esnext',
- esModuleInterop: true,
- },
- },
- ],
+ '^.+\\.[tj]s$': ['@swc/jest'],
},
collectCoverageFrom: ['src/**/*.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverage: false,
testRegex: '(/__tests__/.*\\.(test|spec))\\.(ts|tsx|js)$',
- transformIgnorePatterns: ['/node_modules/(?!(d3.*)/)'],
+ transformIgnorePatterns: ['node_modules/(?!(?:.pnpm/)?(d3.*))'],
testPathIgnorePatterns: ['/(lib|esm)/__tests__/'],
};
diff --git a/packages/layout/package.json b/packages/layout/package.json
index ae7d432..ae74f40 100644
--- a/packages/layout/package.json
+++ b/packages/layout/package.json
@@ -1,6 +1,6 @@
{
"name": "@antv/layout",
- "version": "1.2.14-beta.1",
+ "version": "1.2.14-beta.2",
"description": "graph layout algorithm",
"license": "MIT",
"repository": {
@@ -33,6 +33,7 @@
"@naoak/workerize-transferable": "^0.1.0",
"comlink": "^4.4.1",
"d3-force": "^3.0.0",
+ "d3-force-3d": "^3.0.5",
"d3-octree": "^1.0.2",
"d3-quadtree": "^3.0.1",
"dagre": "^0.8.5",
diff --git a/packages/layout/src/d3-force-3d/index.ts b/packages/layout/src/d3-force-3d/index.ts
new file mode 100644
index 0000000..1473fc0
--- /dev/null
+++ b/packages/layout/src/d3-force-3d/index.ts
@@ -0,0 +1,63 @@
+import {
+ forceCenter,
+ forceCollide,
+ forceLink,
+ forceManyBody,
+ forceRadial,
+ forceSimulation,
+ forceX,
+ forceY,
+ forceZ,
+} from 'd3-force-3d';
+import { D3ForceLayout } from '../d3-force';
+import type { LayoutWithIterations } from '../types';
+import type { D3Force3DLayoutOptions } from './types';
+
+export class D3Force3DLayout
+ extends D3ForceLayout
+ implements LayoutWithIterations
+{
+ public id = 'd3-force-3d';
+
+ protected config = {
+ inputNodeAttrs: ['x', 'y', 'z', 'vx', 'vy', 'vz', 'fx', 'fy', 'fz'],
+ outputNodeAttrs: ['x', 'y', 'z', 'vx', 'vy', 'vz'],
+ simulationAttrs: [
+ 'alpha',
+ 'alphaMin',
+ 'alphaDecay',
+ 'alphaTarget',
+ 'velocityDecay',
+ 'randomSource',
+ 'numDimensions',
+ ],
+ };
+
+ protected forceMap = {
+ link: forceLink,
+ manyBody: forceManyBody,
+ center: forceCenter,
+ collide: forceCollide,
+ radial: forceRadial,
+ x: forceX,
+ y: forceY,
+ z: forceZ,
+ };
+
+ public options: Partial = {
+ numDimensions: 3,
+ link: {
+ id: (edge) => edge.id,
+ },
+ manyBody: {},
+ center: {
+ x: 0,
+ y: 0,
+ z: 0,
+ },
+ };
+
+ protected initSimulation() {
+ return forceSimulation();
+ }
+}
diff --git a/packages/layout/src/d3-force-3d/types.ts b/packages/layout/src/d3-force-3d/types.ts
new file mode 100644
index 0000000..c5dca34
--- /dev/null
+++ b/packages/layout/src/d3-force-3d/types.ts
@@ -0,0 +1,51 @@
+import type {
+ D3ForceLayoutOptions,
+ EdgeDatum,
+ NodeDatum as _NodeDatum,
+} from '../d3-force/types';
+import type { NodeData } from '../types';
+
+/**
+ * @see https://github.com/vasturiano/d3-force-3d
+ */
+export interface D3Force3DLayoutOptions extends D3ForceLayoutOptions {
+ numDimensions?: number;
+ /**
+ * 中心力
+ * Center force
+ */
+ center?: {
+ x?: number;
+ y?: number;
+ z?: number;
+ strength?: number;
+ };
+ /**
+ * 径向力
+ *
+ * Radial force
+ */
+ radial?: {
+ strength?: number | ((node: NodeData) => number);
+ radius?: number | ((node: NodeData) => number);
+ x?: number;
+ y?: number;
+ z?: number;
+ };
+ /**
+ * Z 轴力
+ *
+ * Z axis force
+ */
+ z?: {
+ strength?: number | ((node: NodeData) => number);
+ z?: number | ((node: NodeData) => number);
+ };
+}
+
+export interface NodeDatum extends _NodeDatum {
+ z: number;
+ vz: number;
+}
+
+export type { EdgeDatum };
diff --git a/packages/layout/src/d3-force-3d/typing.d.ts b/packages/layout/src/d3-force-3d/typing.d.ts
new file mode 100644
index 0000000..f355a70
--- /dev/null
+++ b/packages/layout/src/d3-force-3d/typing.d.ts
@@ -0,0 +1,301 @@
+// TODO wait for d3-force-3d to be published
+declare module 'd3-force-3d' {
+ export function forceCenter(x?: number, y?: number, z?: number): ForceCenter;
+
+ export function forceCollide(
+ radius?:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ ): ForceCollide;
+
+ export function forceLink(links?: LinkData[]): ForceLink;
+
+ export function forceManyBody(): ForceManyBody;
+
+ export function forceRadial(
+ radius?:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ x?: number,
+ y?: number,
+ z?: number,
+ ): ForceRadial;
+
+ export function forceSimulation(
+ nodes?: NodeData[],
+ numDimensions?: Dimensions,
+ ): ForceSimulation;
+
+ export function forceX(x?: number): ForceX;
+
+ export function forceY(y?: number): ForceY;
+
+ export function forceZ(z?: number): ForceZ;
+
+ interface ForceSimulation {
+ tick(iterations?: number): this;
+
+ restart(): this;
+
+ stop(): this;
+
+ numDimensions(): Dimensions;
+ numDimensions(value: Dimensions): this;
+
+ nodes(): NodeData[];
+ nodes(nodes: NodeData[]): this;
+
+ alpha(): number;
+ alpha(alpha: number): this;
+
+ alphaMin(): number;
+ alphaMin(min: number): this;
+
+ alphaDecay(): number;
+ alphaDecay(decay: number): this;
+
+ alphaTarget(): number;
+ alphaTarget(target: number): this;
+
+ velocityDecay(): number;
+ velocityDecay(decay: number): this;
+
+ randomSource(): () => number;
+ randomSource(source: () => number): this;
+
+ force(name: string): T;
+ force(name: string, force: Force | null): this;
+
+ find(x?: number, y?: number, z?: number, radius?: number): NodeData;
+
+ on(name: string): (...args: any[]) => void;
+ on(name: string, listener: (...args: any[]) => void): this;
+ }
+
+ type Force =
+ | ForceCenter
+ | ForceCollide
+ | ForceLink
+ | ForceManyBody
+ | ForceRadial
+ | ForceX
+ | ForceY
+ | ForceZ;
+
+ interface ForceCenter {
+ (): void;
+
+ initialize(nodes: NodeData[]): void;
+
+ x(): number;
+ x(x: number): this;
+
+ y(): number;
+ y(y: number): this;
+
+ z(): number;
+ z(z: number): this;
+
+ strength(): number;
+ strength(strength: number): this;
+ }
+
+ interface ForceCollide {
+ (): void;
+
+ initialize(
+ nodes: NodeData[],
+ random?: () => number,
+ nDim?: Dimensions,
+ ): void;
+
+ iterations(): number;
+ iterations(iterations: number): this;
+
+ strength(): number;
+ strength(strength: number): this;
+
+ radius(): number;
+ radius(
+ radius:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ ): this;
+ }
+
+ interface ForceLink {
+ (alpha: number): void;
+
+ initialize(nodes: NodeData[], random: () => number, dim: Dimensions): void;
+
+ links(): LinkData[];
+ links(links: LinkData[]): this;
+
+ id(): (node: NodeData, index: number, nodes: NodeData[]) => any;
+ id(id: (node: NodeData, index: number, nodes: NodeData[]) => any): this;
+
+ iterations(): number;
+ iterations(iterations: number): this;
+
+ strength(): (link: LinkData, index: number, links: LinkData[]) => number;
+ strength(
+ strength:
+ | number
+ | ((link: LinkData, index: number, links: LinkData[]) => number),
+ ): this;
+
+ distance(): (link: LinkData, index: number, links: LinkData[]) => number;
+ distance(
+ distance:
+ | number
+ | ((link: LinkData, index: number, links: LinkData[]) => number),
+ ): this;
+ }
+
+ interface ForceManyBody {
+ (alpha: number): void;
+
+ initialize(nodes: NodeData[], random: () => number, dim: Dimensions): void;
+
+ strength(): (node: NodeData, index: number, nodes: NodeData[]) => number;
+ strength(
+ strength:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ ): this;
+
+ distanceMin(): number;
+ distanceMin(min: number): this;
+
+ distanceMax(): number;
+ distanceMax(max: number): this;
+
+ theta(): number;
+ theta(theta: number): this;
+ }
+
+ interface ForceRadial {
+ (alpha: number): void;
+
+ initialize(nodes: NodeData[], dim: Dimensions): void;
+
+ strength(): (node: NodeData, index: number, nodes: NodeData[]) => number;
+ strength(
+ strength:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ ): this;
+
+ radius(): (node: NodeData, index: number, nodes: NodeData[]) => number;
+ radius(
+ radius:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ ): this;
+
+ x(): number;
+ x(x: number): this;
+
+ y(): number;
+ y(y: number): this;
+
+ z(): number;
+ z(z: number): this;
+ }
+
+ interface ForceX {
+ (alpha: number): void;
+
+ initialize(nodes: NodeData[]): void;
+
+ strength(): number;
+ strength(
+ strength:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ ): this;
+
+ x(): number;
+ x(
+ x:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ ): this;
+ }
+
+ interface ForceY {
+ (alpha: number): void;
+
+ initialize(nodes: NodeData[]): void;
+
+ strength(): number;
+ strength(
+ strength:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ ): this;
+
+ y(): number;
+ y(
+ y:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ ): this;
+ }
+
+ interface ForceZ {
+ (alpha: number): void;
+
+ initialize(nodes: NodeData[]): void;
+
+ strength(): number;
+ strength(
+ strength:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ ): this;
+
+ z(): number;
+ z(
+ z:
+ | number
+ | ((node: NodeData, index: number, nodes: NodeData[]) => number),
+ ): this;
+ }
+
+ interface NodeData {
+ /** the node’s zero-based index into nodes */
+ index?: number;
+ /** the node’s current x-position */
+ x?: number;
+ /** the node’s current y-position (if using 2 or more dimensions) */
+ y?: number;
+ /** the node’s current z-position (if using 3 dimensions) */
+ z?: number;
+ /** the node’s current x-velocity */
+ vx?: number;
+ /** the node’s current y-velocity (if using 2 or more dimensions) */
+ vy?: number;
+ /** the node’s current z-velocity (if using 3 dimensions) */
+ vz?: number;
+ /** the node’s fixed x-position */
+ fx?: number;
+ /** the node’s fixed y-position */
+ fy?: number;
+ /** the node’s fixed z-position */
+ fz?: number;
+ [key: string]: any;
+ }
+
+ interface LinkData {
+ /** the zero-based index into links, assigned by this method */
+ index?: number;
+ /** the link’s source node */
+ source: NodeData | any;
+ /** the link’s target node */
+ target: NodeData | any;
+ [key: string]: any;
+ }
+
+ type Dimensions = 1 | 2 | 3;
+}
diff --git a/packages/layout/src/d3-force/index.ts b/packages/layout/src/d3-force/index.ts
new file mode 100644
index 0000000..03a4401
--- /dev/null
+++ b/packages/layout/src/d3-force/index.ts
@@ -0,0 +1,221 @@
+import { deepMix, pick } from '@antv/util';
+import type { Simulation } from 'd3-force';
+import {
+ forceCenter,
+ forceCollide,
+ ForceLink,
+ forceLink,
+ forceManyBody,
+ forceRadial,
+ forceSimulation,
+ forceX,
+ forceY,
+} from 'd3-force';
+import type { Graph, LayoutMapping, LayoutWithIterations } from '../types';
+import type { D3ForceLayoutOptions, EdgeDatum, NodeDatum } from './types';
+
+export class D3ForceLayout<
+ T extends D3ForceLayoutOptions = D3ForceLayoutOptions,
+> implements LayoutWithIterations
+{
+ public id = 'd3-force';
+
+ protected simulation: Simulation;
+
+ protected resolver: (value: LayoutMapping) => void;
+
+ protected config = {
+ inputNodeAttrs: ['x', 'y', 'vx', 'vy', 'fx', 'fy'],
+ outputNodeAttrs: ['x', 'y', 'vx', 'vy'],
+ simulationAttrs: [
+ 'alpha',
+ 'alphaMin',
+ 'alphaDecay',
+ 'alphaTarget',
+ 'velocityDecay',
+ 'randomSource',
+ ],
+ };
+
+ protected forceMap: Record = {
+ link: forceLink,
+ manyBody: forceManyBody,
+ center: forceCenter,
+ collide: forceCollide,
+ radial: forceRadial,
+ x: forceX,
+ y: forceY,
+ };
+
+ // @ts-ignore
+ public options: Partial = {
+ link: {
+ id: (edge) => edge.id,
+ },
+ manyBody: {},
+ center: {
+ x: 0,
+ y: 0,
+ },
+ };
+
+ protected context: {
+ assign: boolean;
+ options: Partial;
+ nodes: NodeDatum[];
+ edges: EdgeDatum[];
+ graph?: Graph;
+ } = {
+ options: {},
+ assign: false,
+ nodes: [],
+ edges: [],
+ };
+
+ constructor(options: Partial = {}) {
+ const { forceSimulation, ..._ } = options;
+ deepMix(this.options, _);
+ if (forceSimulation) this.simulation = forceSimulation;
+ }
+
+ public async execute(graph: Graph, options?: T): Promise {
+ return this.genericLayout(false, graph, options);
+ }
+
+ public async assign(graph: Graph, options?: T): Promise {
+ await this.genericLayout(true, graph, options);
+ }
+
+ public stop() {
+ this.simulation.stop();
+ }
+
+ public tick(iterations?: number): LayoutMapping {
+ this.simulation.tick(iterations);
+ return this.getResult();
+ }
+
+ public restart() {
+ this.simulation.restart();
+ }
+
+ protected getOptions(options: Partial): T {
+ const _ = { ...this.options, ...options };
+ // process nodeSize
+ if (_.collide?.radius === undefined) {
+ _.collide = _.collide || {};
+ _.collide.radius = _.nodeSize ?? 10;
+ }
+ // process iterations
+ if (_.iterations === undefined) {
+ if (_.link && _.link.iterations === undefined) {
+ _.iterations = _.link.iterations;
+ }
+ if (_.collide && _.collide.iterations === undefined) {
+ _.iterations = _.collide.iterations;
+ }
+ }
+
+ // assign to context
+ this.context.options = _;
+ return _ as T;
+ }
+
+ protected async genericLayout(
+ assign: boolean,
+ graph: Graph,
+ options?: T,
+ ): Promise {
+ const _options = this.getOptions(options);
+
+ const nodes = graph.getAllNodes().map(({ id, data }) => ({
+ id,
+ data,
+ ...pick(data, this.config.inputNodeAttrs),
+ }));
+
+ const edges = graph.getAllEdges().map((edge) => ({ ...edge }));
+
+ Object.assign(this.context, { assign, nodes, edges, graph });
+
+ const promise = new Promise((resolver) => {
+ this.resolver = resolver;
+ });
+
+ const simulation = this.setSimulation(_options);
+
+ simulation.nodes(nodes);
+ simulation.force>('link')?.links(edges);
+
+ return promise;
+ }
+
+ protected getResult(): LayoutMapping {
+ const { assign, nodes, edges, graph } = this.context;
+
+ const nodesResult = nodes.map((node) => ({
+ id: node.id,
+ data: {
+ ...node.data,
+ ...(pick(node, this.config.outputNodeAttrs) as any),
+ },
+ }));
+
+ const edgeResult = edges.map(({ id, source, target, data }) => ({
+ id,
+ source: typeof source === 'object' ? source.id : source,
+ target: typeof target === 'object' ? target.id : target,
+ data,
+ }));
+
+ if (assign) {
+ nodesResult.forEach((node) => graph.mergeNodeData(node.id, node.data));
+ }
+
+ return { nodes: nodesResult, edges: edgeResult };
+ }
+
+ protected initSimulation() {
+ return forceSimulation();
+ }
+
+ protected setSimulation(options: T) {
+ const simulation =
+ this.simulation || this.options.forceSimulation || this.initSimulation();
+
+ if (!this.simulation) {
+ this.simulation = simulation
+ .on('tick', () => options.onTick?.(this.getResult()))
+ .on('end', () => this.resolver?.(this.getResult()));
+ }
+
+ apply(
+ simulation,
+ this.config.simulationAttrs.map((name) => [
+ name,
+ options[name as keyof T],
+ ]),
+ );
+
+ Object.entries(this.forceMap).forEach(([name, Ctor]) => {
+ const forceName = name;
+ if (name in options) {
+ let force = simulation.force(forceName);
+ if (!force) {
+ force = Ctor();
+ simulation.force(forceName, force);
+ }
+ apply(force, Object.entries(options[forceName as keyof T]));
+ } else simulation.force(forceName, null);
+ });
+
+ return simulation;
+ }
+}
+
+const apply = (target: any, params: [string, any][]) => {
+ return params.reduce((acc, [method, param]) => {
+ if (!acc[method] || param === undefined) return acc;
+ return acc[method].call(target, param);
+ }, target);
+};
diff --git a/packages/layout/src/d3-force/types.ts b/packages/layout/src/d3-force/types.ts
new file mode 100644
index 0000000..3320fc8
--- /dev/null
+++ b/packages/layout/src/d3-force/types.ts
@@ -0,0 +1,114 @@
+import type {
+ Simulation,
+ SimulationLinkDatum,
+ SimulationNodeDatum,
+} from 'd3-force';
+import type { EdgeData, LayoutMapping, NodeData } from '../types';
+
+export interface D3ForceLayoutOptions {
+ /**
+ * 节点尺寸,默认为 10
+ *
+ * Node size, default is 10
+ */
+ nodeSize?: number | ((node: NodeDatum) => number);
+ /**
+ * 每次迭代执行回调
+ *
+ * Callback executed on each tick
+ * @param data - 布局结果 | layout result
+ */
+ onTick?: (data: LayoutMapping) => void;
+ /**
+ * 迭代次数
+ *
+ * Number of iterations
+ * @description
+ * 设置的是力的迭代次数,而不是布局的迭代次数
+ *
+ * The number of iterations of the force, not the layout
+ */
+ iterations?: number;
+ forceSimulation?: Simulation;
+
+ alpha?: number;
+ alphaMin?: number;
+ alphaDecay?: number;
+ alphaTarget?: number;
+ velocityDecay?: number;
+ randomSource?: () => number;
+ /**
+ * 中心力
+ * Center force
+ */
+ center?: {
+ x?: number;
+ y?: number;
+ strength?: number;
+ };
+ /**
+ * 碰撞力
+ *
+ * Collision force
+ */
+ collide?: {
+ radius?: number | ((node: NodeDatum) => number);
+ strength?: number;
+ iterations?: number;
+ };
+ /**
+ * 多体力
+ *
+ * Many body force
+ */
+ manyBody?: {
+ strength?: number;
+ theta?: number;
+ distanceMin?: number;
+ distanceMax?: number;
+ };
+ /**
+ * 链接力
+ *
+ * Link force
+ */
+ link?: {
+ id?: (edge: EdgeDatum) => string;
+ distance?: number | ((edge: EdgeDatum) => number);
+ strength?: number | ((edge: EdgeDatum) => number);
+ iterations?: number;
+ };
+ /**
+ * 径向力
+ *
+ * Radial force
+ */
+ radial?: {
+ strength?: number | ((node: NodeDatum) => number);
+ radius?: number | ((node: NodeDatum) => number);
+ x?: number;
+ y?: number;
+ };
+ /**
+ * X 轴力
+ *
+ * X axis force
+ */
+ x?: {
+ strength?: number | ((node: NodeDatum) => number);
+ x?: number | ((node: NodeDatum) => number);
+ };
+ /**
+ * Y 轴力
+ *
+ * Y axis force
+ */
+ y?: {
+ strength?: number | ((node: NodeDatum) => number);
+ y?: number | ((node: NodeDatum) => number);
+ };
+}
+
+export interface NodeDatum extends NodeData, SimulationNodeDatum {}
+
+export interface EdgeDatum extends EdgeData, SimulationLinkDatum {}
diff --git a/packages/layout/src/d3Force/forceInBox.ts b/packages/layout/src/d3Force/forceInBox.ts
deleted file mode 100644
index a08e1ab..0000000
--- a/packages/layout/src/d3Force/forceInBox.ts
+++ /dev/null
@@ -1,399 +0,0 @@
-import * as d3Force from 'd3-force';
-
-interface INode {
- id: string;
- x: number;
- y: number;
- vx: number;
- vy: number;
- cluster: any;
-}
-
-// https://github.com/john-guerra/forceInABox/blob/master/src/forceInABox.js
-export default function forceInBox() {
- function constant(_: any): () => any {
- return () => _;
- }
-
- let groupBy = (d: INode) => {
- return d.cluster;
- };
- let forceNodeSize: (() => number) | ((d: any) => number) = constant(1);
- let forceCharge: (() => number) | ((d: any) => number) = constant(-1);
- let forceLinkDistance: (() => number) | ((d: any) => number) = constant(100);
- let forceLinkStrength: (() => number) | ((d: any) => number) = constant(0.1);
- let offset = [0, 0];
-
- let nodes: INode[] = [];
- let nodesMap: any = {};
- let links: any[] = [];
- let centerX = 100;
- let centerY = 100;
- let foci: any = {
- none: {
- x: 0,
- y: 0,
- },
- };
- let templateNodes: INode[] = [];
- let templateForce: any;
- let template = 'force';
- let enableGrouping = true;
- let strength = 0.1;
-
- function force(alpha: number) {
- if (!enableGrouping) {
- return force;
- }
- templateForce.tick();
- getFocisFromTemplate();
-
- for (let i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) {
- node = nodes[i];
- node.vx += (foci[groupBy(node)].x - node.x) * k;
- node.vy += (foci[groupBy(node)].y - node.y) * k;
- }
- }
-
- function initialize() {
- if (!nodes) return;
- initializeWithForce();
- }
-
- function initializeWithForce() {
- if (!nodes || !nodes.length) {
- return;
- }
-
- if (groupBy(nodes[0]) === undefined) {
- throw Error(
- "Couldnt find the grouping attribute for the nodes. Make sure to set it up with forceInBox.groupBy('clusterAttr') before calling .links()",
- );
- }
-
- // checkLinksAsObjects();
-
- const net = getGroupsGraph();
- templateForce = d3Force
- .forceSimulation(net.nodes)
- .force('x', d3Force.forceX(centerX).strength(0.1))
- .force('y', d3Force.forceY(centerY).strength(0.1))
- .force('collide', d3Force.forceCollide((d: any) => d.r).iterations(4))
- .force('charge', d3Force.forceManyBody().strength(forceCharge))
- .force(
- 'links',
- d3Force
- .forceLink(net.nodes.length ? net.links : [])
- .distance(forceLinkDistance)
- .strength(forceLinkStrength),
- );
-
- templateNodes = templateForce.nodes();
-
- getFocisFromTemplate();
- }
-
- function getGroupsGraph() {
- const gnodes: any = [];
- const glinks: any = [];
- const dNodes: any = {};
- let clustersList: string[] = [];
- let clustersCounts: any = {};
- let clustersLinks: any = [];
-
- clustersCounts = computeClustersNodeCounts(nodes);
- clustersLinks = computeClustersLinkCounts(links);
-
- clustersList = Object.keys(clustersCounts);
-
- clustersList.forEach((key, index) => {
- const val = clustersCounts[key];
- // Uses approx meta-node size
- gnodes.push({
- id: key,
- size: val.count,
- r: Math.sqrt(val.sumforceNodeSize / Math.PI),
- });
- dNodes[key] = index;
- });
-
- clustersLinks.forEach((link: any) => {
- const sourceTerminal = link.source;
- const targetTerminal = link.target;
- const source = dNodes[sourceTerminal];
- const target = dNodes[targetTerminal];
- if (source !== undefined && target !== undefined) {
- glinks.push({
- source,
- target,
- count: link.count,
- });
- }
- });
-
- return {
- nodes: gnodes,
- links: glinks,
- };
- }
-
- function computeClustersNodeCounts(nodes: any) {
- const clustersCounts: any = {};
-
- nodes.forEach((d: any) => {
- const key = groupBy(d);
- if (!clustersCounts[key]) {
- clustersCounts[key] = {
- count: 0,
- sumforceNodeSize: 0,
- };
- }
- });
- nodes.forEach((d: any) => {
- const key = groupBy(d);
- const nodeSize = forceNodeSize(d);
- const tmpCount = clustersCounts[key];
- tmpCount.count = tmpCount.count + 1;
- tmpCount.sumforceNodeSize =
- tmpCount.sumforceNodeSize + Math.PI * (nodeSize * nodeSize) * 1.3;
- clustersCounts[key] = tmpCount;
- });
-
- return clustersCounts;
- }
-
- function computeClustersLinkCounts(links: any) {
- const dClusterLinks: any = {};
- const clusterLinks: any = [];
- links.forEach((l: any) => {
- const key = getLinkKey(l);
- let count = 0;
- if (dClusterLinks[key] !== undefined) {
- count = dClusterLinks[key];
- }
- count += 1;
- dClusterLinks[key] = count;
- });
-
- // @ts-ignore
- const entries = Object.entries(dClusterLinks);
-
- entries.forEach(([key, count]: any) => {
- const source = key.split('~')[0];
- const target = key.split('~')[1];
- if (source !== undefined && target !== undefined) {
- clusterLinks.push({
- source,
- target,
- count,
- });
- }
- });
-
- return clusterLinks;
- }
-
- function getFocisFromTemplate() {
- foci = {
- none: {
- x: 0,
- y: 0,
- },
- };
- templateNodes.forEach((d) => {
- foci[d.id] = {
- x: d.x - offset[0],
- y: d.y - offset[1],
- };
- });
- return foci;
- }
-
- function getLinkKey(link: any) {
- const source = link.source;
- const target = link.target;
- const sourceID = groupBy(nodesMap[source]);
- const targetID = groupBy(nodesMap[target]);
-
- return sourceID <= targetID
- ? `${sourceID}~${targetID}`
- : `${targetID}~${sourceID}`;
- }
-
- function genNodesMap(nodes: any) {
- nodesMap = {};
- nodes.forEach((node: any) => {
- nodesMap[node.id] = node;
- });
- }
-
- function setTemplate(x: any) {
- if (!arguments.length) return template;
- template = x;
- initialize();
- return force;
- }
-
- function setGroupBy(x: any) {
- if (!arguments.length) return groupBy;
- if (typeof x === 'string') {
- groupBy = (d: any) => {
- return d[x];
- };
- return force;
- }
- groupBy = x;
- return force;
- }
-
- function setEnableGrouping(x: any) {
- if (!arguments.length) return enableGrouping;
- enableGrouping = x;
- return force;
- }
-
- function setStrength(x: any) {
- if (!arguments.length) return strength;
- strength = x;
- return force;
- }
-
- function setCenterX(_: any) {
- if (arguments.length) {
- centerX = _;
- return force;
- }
-
- return centerX;
- }
-
- function setCenterY(_: any) {
- if (arguments.length) {
- centerY = _;
- return force;
- }
-
- return centerY;
- }
-
- function setNodes(_: any) {
- if (arguments.length) {
- genNodesMap(_ || []);
- nodes = _ || [];
- return force;
- }
- return nodes;
- }
-
- function setLinks(_: any) {
- if (arguments.length) {
- links = _ || [];
- initialize();
- return force;
- }
- return links;
- }
-
- function setForceNodeSize(_: any) {
- if (arguments.length) {
- if (typeof _ === 'function') {
- forceNodeSize = _;
- } else {
- forceNodeSize = constant(+_);
- }
- initialize();
- return force;
- }
-
- return forceNodeSize;
- }
-
- function setForceCharge(_: any) {
- if (arguments.length) {
- if (typeof _ === 'function') {
- forceCharge = _;
- } else {
- forceCharge = constant(+_);
- }
- initialize();
- return force;
- }
-
- return forceCharge;
- }
-
- function setForceLinkDistance(_: any) {
- if (arguments.length) {
- if (typeof _ === 'function') {
- forceLinkDistance = _;
- } else {
- forceLinkDistance = constant(+_);
- }
- initialize();
- return force;
- }
-
- return forceLinkDistance;
- }
-
- function setForceLinkStrength(_: any) {
- if (arguments.length) {
- if (typeof _ === 'function') {
- forceLinkStrength = _;
- } else {
- forceLinkStrength = constant(+_);
- }
- initialize();
- return force;
- }
-
- return forceLinkStrength;
- }
-
- function setOffset(_: any) {
- if (arguments.length) {
- offset = _;
- return force;
- }
-
- return offset;
- }
-
- force.initialize = (_: any) => {
- nodes = _;
- initialize();
- };
-
- force.template = setTemplate;
-
- force.groupBy = setGroupBy;
-
- force.enableGrouping = setEnableGrouping;
-
- force.strength = setStrength;
-
- force.centerX = setCenterX;
-
- force.centerY = setCenterY;
-
- force.nodes = setNodes;
-
- force.links = setLinks;
-
- force.forceNodeSize = setForceNodeSize;
-
- // Legacy support
- force.nodeSize = force.forceNodeSize;
-
- force.forceCharge = setForceCharge;
-
- force.forceLinkDistance = setForceLinkDistance;
-
- force.forceLinkStrength = setForceLinkStrength;
-
- force.offset = setOffset;
-
- force.getFocis = getFocisFromTemplate;
-
- return force;
-}
diff --git a/packages/layout/src/d3Force/index.ts b/packages/layout/src/d3Force/index.ts
deleted file mode 100644
index e27a93a..0000000
--- a/packages/layout/src/d3Force/index.ts
+++ /dev/null
@@ -1,426 +0,0 @@
-import { isFunction, isNumber, isObject } from '@antv/util';
-import * as d3Force from 'd3-force';
-import type {
- D3ForceLayoutOptions,
- Edge,
- Graph,
- LayoutMapping,
- LayoutWithIterations,
- Node,
- OutEdge,
- OutNode,
-} from '../types';
-import { cloneFormatData, isArray } from '../util';
-import forceInBox from './forceInBox';
-
-/**
- * D3 writes x and y as the first level properties
- */
-interface CalcNode extends Node {
- x: number;
- y: number;
-}
-
-const DEFAULTS_LAYOUT_OPTIONS: Partial = {
- center: [0, 0],
- preventOverlap: false,
- nodeSize: undefined,
- nodeSpacing: undefined,
- linkDistance: 50,
- forceSimulation: null,
- alphaDecay: 0.028,
- alphaMin: 0.001,
- alpha: 0.3,
- collideStrength: 1,
- clustering: false,
- clusterNodeStrength: -1,
- clusterEdgeStrength: 0.1,
- clusterEdgeDistance: 100,
- clusterFociStrength: 0.8,
- clusterNodeSize: 10,
-};
-
-/**
- * Layout the nodes' positions with d3's basic classic force
- *
- * @example
- * // Assign layout options when initialization.
- * const layout = new D3ForceLayout({ center: [100, 100] });
- * const positions = await layout.execute(graph); // { nodes: [], edges: [] }
- *
- * // Or use different options later.
- * const layout = new D3ForceLayout({ center: [100, 100] });
- * const positions = await layout.execute(graph, { center: [100, 100] }); // { nodes: [], edges: [] }
- *
- * // If you want to assign the positions directly to the nodes, use assign method.
- * await layout.assign(graph, { center: [100, 100] });
- */
-export class D3ForceLayout
- implements LayoutWithIterations
-{
- id = 'd3force';
-
- private forceSimulation: d3Force.Simulation;
-
- private lastLayoutNodes: CalcNode[];
- private lastLayoutEdges: OutEdge[];
- private lastAssign: boolean;
- private lastGraph: Graph;
-
- constructor(
- public options: D3ForceLayoutOptions = {} as D3ForceLayoutOptions,
- ) {
- this.options = {
- ...DEFAULTS_LAYOUT_OPTIONS,
- ...options,
- };
- }
-
- /**
- * Return the positions of nodes and edges(if needed).
- */
- async execute(graph: Graph, options?: D3ForceLayoutOptions) {
- return this.genericForceLayout(false, graph, options);
- }
- /**
- * To directly assign the positions to the nodes.
- */
- async assign(graph: Graph, options?: D3ForceLayoutOptions) {
- this.genericForceLayout(true, graph, options);
- }
-
- /**
- * Stop simulation immediately.
- */
- stop() {
- this.forceSimulation?.stop();
- }
-
- /**
- * Manually steps the simulation by the specified number of iterations.
- * @see https://github.com/d3/d3-force#simulation_tick
- */
- tick(iterations = 1) {
- this.forceSimulation.tick(iterations);
-
- const result = {
- nodes: formatOutNodes(this.lastLayoutNodes),
- edges: formatOutEdges(this.lastLayoutEdges),
- };
-
- if (this.lastAssign) {
- result.nodes.forEach((node) =>
- this.lastGraph.mergeNodeData(node.id, {
- x: node.data.x,
- y: node.data.y,
- }),
- );
- }
-
- return result;
- }
-
- private async genericForceLayout(
- assign: false,
- graph: Graph,
- options?: D3ForceLayoutOptions,
- ): Promise;
- private async genericForceLayout(
- assign: true,
- graph: Graph,
- options?: D3ForceLayoutOptions,
- ): Promise;
- private async genericForceLayout(
- assign: boolean,
- graph: Graph,
- options?: D3ForceLayoutOptions,
- ): Promise {
- const mergedOptions = { ...this.options, ...options };
-
- const nodes = graph.getAllNodes();
- const edges = graph.getAllEdges();
- const layoutNodes: CalcNode[] = nodes.map(
- (node) =>
- ({
- ...cloneFormatData(node),
- x: node.data?.x,
- y: node.data?.y,
- } as CalcNode),
- );
- const layoutEdges: OutEdge[] = edges.map((edge) => cloneFormatData(edge));
-
- // Use them later in `tick`.
- this.lastLayoutNodes = layoutNodes;
- this.lastLayoutEdges = layoutEdges;
- this.lastAssign = assign;
- this.lastGraph = graph;
-
- const {
- alphaMin,
- alphaDecay,
- alpha,
- nodeStrength,
- edgeStrength,
- linkDistance,
- clustering,
- clusterFociStrength,
- clusterEdgeDistance,
- clusterEdgeStrength,
- clusterNodeStrength,
- clusterNodeSize,
- collideStrength = 1,
- center = [0, 0],
- preventOverlap,
- nodeSize,
- nodeSpacing,
- onTick,
- } = mergedOptions;
- let { forceSimulation } = mergedOptions;
-
- return new Promise((resolve) => {
- if (!forceSimulation) {
- try {
- // 定义节点的力
- const nodeForce = d3Force.forceManyBody();
- if (nodeStrength) {
- nodeForce.strength(nodeStrength as any);
- }
- forceSimulation = d3Force.forceSimulation().nodes(layoutNodes as any);
-
- if (clustering) {
- const clusterForce = forceInBox() as any;
- clusterForce
- .centerX(center[0])
- .centerY(center[1])
- .template('force')
- .strength(clusterFociStrength);
- if (layoutEdges) {
- clusterForce.links(layoutEdges);
- }
- if (layoutNodes) {
- clusterForce.nodes(layoutNodes);
- }
- clusterForce
- .forceLinkDistance(clusterEdgeDistance)
- .forceLinkStrength(clusterEdgeStrength)
- .forceCharge(clusterNodeStrength)
- .forceNodeSize(clusterNodeSize);
-
- forceSimulation.force('group', clusterForce);
- }
- forceSimulation
- .force('center', d3Force.forceCenter(center[0], center[1]))
- .force('charge', nodeForce)
- .alpha(alpha)
- .alphaDecay(alphaDecay)
- .alphaMin(alphaMin);
-
- if (preventOverlap) {
- this.overlapProcess(forceSimulation, {
- nodeSize,
- nodeSpacing,
- collideStrength,
- });
- }
- // 如果有边,定义边的力
- if (layoutEdges) {
- // d3 的 forceLayout 会重新生成边的数据模型,为了避免污染源数据
- const edgeForce = d3Force
- .forceLink()
- .id((d: any) => d.id)
- .links(layoutEdges);
- if (edgeStrength) {
- edgeForce.strength(edgeStrength as any);
- }
- if (linkDistance) {
- edgeForce.distance(linkDistance as any);
- }
- forceSimulation.force('link', edgeForce);
- }
-
- forceSimulation
- .on('tick', () => {
- const outNodes = formatOutNodes(layoutNodes);
- onTick?.({
- nodes: outNodes,
- edges: formatOutEdges(layoutEdges),
- });
-
- if (assign) {
- outNodes.forEach((node) =>
- graph.mergeNodeData(node.id, {
- x: node.data.x,
- y: node.data.y,
- }),
- );
- }
- })
- .on('end', () => {
- const outNodes = formatOutNodes(layoutNodes);
-
- if (assign) {
- outNodes.forEach((node) =>
- graph.mergeNodeData(node.id, {
- x: node.data.x,
- y: node.data.y,
- }),
- );
- }
-
- resolve({
- nodes: outNodes,
- edges: formatOutEdges(layoutEdges),
- });
- });
- } catch (e) {
- console.warn(e);
- }
- } else {
- // forceSimulation is defined
- if (clustering) {
- const clusterForce = forceInBox() as any;
- clusterForce.nodes(layoutNodes);
- clusterForce.links(layoutEdges);
- }
- forceSimulation.nodes(layoutNodes);
- if (layoutEdges) {
- // d3 的 forceLayout 会重新生成边的数据模型,为了避免污染源数据
- const edgeForce = d3Force
- .forceLink()
- .id((d: any) => d.id)
- .links(layoutEdges);
- if (edgeStrength) {
- edgeForce.strength(edgeStrength as any);
- }
- if (linkDistance) {
- edgeForce.distance(linkDistance as any);
- }
- forceSimulation.force('link', edgeForce);
- }
- if (preventOverlap) {
- this.overlapProcess(forceSimulation, {
- nodeSize,
- nodeSpacing,
- collideStrength,
- });
- }
- forceSimulation.alpha(alpha).restart();
-
- // since d3 writes x and y as node's first level properties, format them into data
- const outNodes = formatOutNodes(layoutNodes);
- const outEdges = formatOutEdges(layoutEdges);
-
- if (assign) {
- outNodes.forEach((node) =>
- graph.mergeNodeData(node.id, {
- x: node.data.x,
- y: node.data.y,
- }),
- );
- }
-
- resolve({
- nodes: outNodes,
- edges: outEdges,
- });
- }
-
- this.forceSimulation = forceSimulation;
- });
- }
-
- /**
- * Prevent overlappings.
- * @param {object} simulation force simulation of d3
- */
- public overlapProcess(
- simulation: d3Force.Simulation,
- options: {
- nodeSize: number | number[] | ((d?: Node) => number) | undefined;
- nodeSpacing: number | number[] | ((d?: Node) => number) | undefined;
- collideStrength: number;
- },
- ) {
- const { nodeSize, nodeSpacing, collideStrength } = options;
- let nodeSizeFunc: (d: any) => number;
- let nodeSpacingFunc: any;
-
- if (isNumber(nodeSpacing)) {
- nodeSpacingFunc = () => nodeSpacing;
- } else if (isFunction(nodeSpacing)) {
- nodeSpacingFunc = nodeSpacing;
- } else {
- nodeSpacingFunc = () => 0;
- }
-
- if (!nodeSize) {
- nodeSizeFunc = (d) => {
- if (d.size) {
- if (isArray(d.size)) {
- const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1];
- return res / 2 + nodeSpacingFunc(d);
- }
- if (isObject(d.size)) {
- const res =
- d.size.width > d.size.height ? d.size.width : d.size.height;
- return res / 2 + nodeSpacingFunc(d);
- }
- return d.size / 2 + nodeSpacingFunc(d);
- }
- return 10 + nodeSpacingFunc(d);
- };
- } else if (isFunction(nodeSize)) {
- nodeSizeFunc = (d) => {
- const size = nodeSize(d);
- return size + nodeSpacingFunc(d);
- };
- } else if (isArray(nodeSize)) {
- const larger = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
- const radius = larger / 2;
- nodeSizeFunc = (d) => radius + nodeSpacingFunc(d);
- } else if (isNumber(nodeSize)) {
- const radius = (nodeSize as number) / 2;
- nodeSizeFunc = (d) => radius + nodeSpacingFunc(d);
- } else {
- nodeSizeFunc = () => 10;
- }
-
- // forceCollide's parameter is a radius
- simulation.force(
- 'collisionForce',
- d3Force.forceCollide(nodeSizeFunc).strength(collideStrength),
- );
- }
-}
-
-/**
- * Format the calculation nodes into output nodes.
- * Since d3 reads properties in plain node data object which is not compact to the OutNode
- * @param layoutNodes
- * @returns
- */
-const formatOutNodes = (layoutNodes: CalcNode[]): OutNode[] =>
- layoutNodes.map((node) => {
- const { x, y, ...others } = node;
- return {
- ...others,
- data: {
- ...others.data,
- x,
- y,
- },
- };
- });
-
-/**
- * d3 will modify `source` and `target` on edge object.
- */
-const formatOutEdges = (edges: any[]): Edge[] =>
- edges.map((edge) => {
- const { source, target, ...rest } = edge;
- return {
- ...rest,
- source: source.id,
- target: target.id,
- };
- });
diff --git a/packages/layout/src/exports.ts b/packages/layout/src/exports.ts
index 2eb2393..78baf57 100644
--- a/packages/layout/src/exports.ts
+++ b/packages/layout/src/exports.ts
@@ -3,7 +3,10 @@ export type { DagreAlign, DagreRankdir } from './antv-dagre/types';
export * from './circular';
export * from './comboCombined';
export * from './concentric';
-export * from './d3Force';
+export { D3ForceLayout } from './d3-force';
+export { D3Force3DLayout } from './d3-force-3d';
+export type { D3Force3DLayoutOptions } from './d3-force-3d/types';
+export type { D3ForceLayoutOptions } from './d3-force/types';
export * from './dagre';
export * from './force';
export * from './forceAtlas2';
diff --git a/packages/layout/src/registry.ts b/packages/layout/src/registry.ts
index 5be2f07..5e4b090 100644
--- a/packages/layout/src/registry.ts
+++ b/packages/layout/src/registry.ts
@@ -2,7 +2,8 @@ import { AntVDagreLayout } from './antv-dagre';
import { CircularLayout } from './circular';
import { ComboCombinedLayout } from './comboCombined';
import { ConcentricLayout } from './concentric';
-import { D3ForceLayout } from './d3Force';
+import { D3ForceLayout } from './d3-force';
+import { D3Force3DLayout } from './d3-force-3d';
import { DagreLayout } from './dagre';
import { ForceLayout } from './force';
import { ForceAtlas2Layout } from './forceAtlas2';
@@ -22,6 +23,7 @@ export const registry: Record Layout> = {
radial: RadialLayout,
force: ForceLayout,
d3force: D3ForceLayout,
+ 'd3-force-3d': D3Force3DLayout,
fruchterman: FruchtermanLayout,
forceAtlas2: ForceAtlas2Layout,
dagre: DagreLayout,