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,