From 655e2af42b6070c26e5b7dc665160b0bf432a6c0 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 9 Apr 2024 17:12:40 +0800 Subject: [PATCH] feat: adapt d3-force-3d --- demo/d3-force-3d.html | 221 ++ .../layout/__tests__/dataset/force-3d.json | 1838 +++++++++++++++++ packages/layout/__tests__/tsconfig.json | 9 + .../layout/__tests__/unit/d3-force-3d.test.ts | 52 + packages/layout/jest.config.js | 1 + packages/layout/package.json | 3 +- packages/layout/src/d3-force-3d/index.ts | 212 ++ packages/layout/src/d3-force-3d/types.ts | 139 ++ packages/layout/src/d3-force-3d/typing.d.ts | 301 +++ packages/layout/src/exports.ts | 2 + 10 files changed, 2777 insertions(+), 1 deletion(-) create mode 100644 demo/d3-force-3d.html create mode 100644 packages/layout/__tests__/dataset/force-3d.json create mode 100644 packages/layout/__tests__/tsconfig.json create mode 100644 packages/layout/__tests__/unit/d3-force-3d.test.ts create mode 100644 packages/layout/src/d3-force-3d/index.ts create mode 100644 packages/layout/src/d3-force-3d/types.ts create mode 100644 packages/layout/src/d3-force-3d/typing.d.ts 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/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/jest.config.js b/packages/layout/jest.config.js index cfad8cb..4962eae 100644 --- a/packages/layout/jest.config.js +++ b/packages/layout/jest.config.js @@ -1,4 +1,5 @@ module.exports = { + testTimeout: 10 * 1000, transform: { '^.+\\.[tj]s$': ['@swc/jest'], }, 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..91966f9 --- /dev/null +++ b/packages/layout/src/d3-force-3d/index.ts @@ -0,0 +1,212 @@ +import { deepMix, pick } from '@antv/util'; +import { + forceCenter, + forceCollide, + forceLink, + forceManyBody, + forceRadial, + forceSimulation, + forceX, + forceY, + forceZ, +} from 'd3-force-3d'; +import type { Graph, LayoutMapping, LayoutWithIterations } from '../types'; +import type { + D3Force3DLayoutOptions, + SimulationEdgeDatum, + SimulationNodeDatum, +} from './types'; + +export class D3Force3DLayout + implements LayoutWithIterations +{ + public id = 'd3-force-3d'; + + private simulation: ReturnType; + + public options: Partial = { + numDimensions: 3, + link: { + id: (edge) => edge.id, + }, + manyBody: {}, + center: { + x: 0, + y: 0, + z: 0, + }, + }; + + private context: { + assign: boolean; + options: Partial; + nodes: SimulationNodeDatum[]; + edges: SimulationEdgeDatum[]; + graph?: Graph; + } = { + options: {}, + assign: false, + nodes: [], + edges: [], + }; + + constructor(options?: Partial) { + this.options = deepMix({}, this.options, options); + } + + public async execute( + graph: Graph, + options?: D3Force3DLayoutOptions, + ): Promise { + return this.genericLayout(false, graph, options); + } + + public async assign( + graph: Graph, + options?: D3Force3DLayoutOptions, + ): 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(); + } + + private getOptions(options: Partial) { + 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 _; + } + + private resolver: (value: LayoutMapping) => void; + + private async genericLayout( + assign: boolean, + graph: Graph, + options?: D3Force3DLayoutOptions, + ): Promise { + const _options = this.getOptions(options); + + const nodes = graph.getAllNodes().map(({ id, data }) => ({ + id, + data, + ...pick(data, ['x', 'y', 'z', 'vx', 'vy', 'vz', 'fx', 'fy', 'fz']), + })); + + 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.initSimulation(_options); + + simulation.nodes(nodes); + simulation.force('link')?.links(edges); + + return promise; + } + + private getResult(): LayoutMapping { + const { assign, nodes, edges, graph } = this.context; + + const nodesResult = nodes.map((node) => ({ + id: node.id, + data: { + ...node.data, + ...(pick(node, ['x', 'y', 'z', 'vx', 'vy', 'vz']) as any), + }, + })); + + const edgeResult = edges.map(({ id, source, target, data }) => ({ + id, + source: source.id, + target: target.id, + data, + })); + + if (assign) { + nodesResult.forEach((node) => graph.mergeNodeData(node.id, node.data)); + } + + return { nodes: nodesResult, edges: edgeResult }; + } + + private initSimulation(options: D3Force3DLayoutOptions) { + if (!this.simulation) { + this.simulation = forceSimulation(); + this.simulation + .on('tick', () => options.onTick?.(this.getResult())) + .on('end', () => this.resolver?.(this.getResult())); + } + + apply(this.simulation, [ + ['alpha', options.alpha], + ['alphaMin', options.alphaMin], + ['alphaDecay', options.alphaDecay], + ['alphaTarget', options.alphaTarget], + ['velocityDecay', options.velocityDecay], + ['randomSource', options.randomSource], + ['numDimensions', options.numDimensions], + ]); + + const forceMap = { + link: forceLink, + manyBody: forceManyBody, + center: forceCenter, + collide: forceCollide, + radial: forceRadial, + x: forceX, + y: forceY, + z: forceZ, + }; + + Object.entries(forceMap).forEach(([name, Ctor]) => { + const forceName = name as keyof typeof forceMap; + if (name in options) { + let force = this.simulation.force(forceName); + if (!force) { + force = Ctor(); + this.simulation.force(forceName, force); + } + apply(force, Object.entries(options[forceName])); + } else this.simulation.force(forceName, null); + }); + + return this.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-3d/types.ts b/packages/layout/src/d3-force-3d/types.ts new file mode 100644 index 0000000..753093b --- /dev/null +++ b/packages/layout/src/d3-force-3d/types.ts @@ -0,0 +1,139 @@ +import type { ID } from '@antv/graphlib'; +import type { EdgeData, LayoutMapping, NodeData } from '../types'; + +/** + * @see https://github.com/vasturiano/d3-force-3d + */ +export interface D3Force3DLayoutOptions { + /** + * 节点尺寸,默认为 10 + * + * Node size, default is 10 + */ + nodeSize?: number | ((node: NodeData) => 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; + + alpha?: number; + alphaMin?: number; + alphaDecay?: number; + alphaTarget?: number; + velocityDecay?: number; + randomSource?: () => number; + numDimensions?: number; + /** + * 中心力 + * Center force + */ + center?: { + x?: number; + y?: number; + z?: number; + strength?: number; + }; + /** + * 多体力 + * + * Many body force + */ + manyBody?: { + strength?: number; + distanceMin?: number; + distanceMax?: number; + theta?: number; + }; + /** + * 碰撞力 + * + * Collision force + */ + collide?: { + radius?: number | ((node: NodeData) => number); + strength?: number; + iterations?: number; + }; + /** + * 链接力 + * + * Link force + */ + link?: { + distance?: number | ((edge: EdgeData) => number); + strength?: number | ((edge: EdgeData) => number); + iterations?: number; + id?: (edge: EdgeData) => string; + }; + /** + * 辐射力 + * + * Radial force + */ + radial?: { + strength?: number | ((node: NodeData) => number); + radius?: number | ((node: NodeData) => number); + x?: number; + y?: number; + z?: number; + }; + /** + * X 轴力 + * + * X axis force + */ + x?: { + strength?: number | ((node: NodeData) => number); + x?: number | ((node: NodeData) => number); + }; + /** + * Y 轴力 + * + * Y axis force + */ + y?: { + strength?: number | ((node: NodeData) => number); + y?: number | ((node: NodeData) => number); + }; + /** + * Z 轴力 + * + * Z axis force + */ + z?: { + strength?: number | ((node: NodeData) => number); + z?: number | ((node: NodeData) => number); + }; +} + +// TODO wait for d3-force-3d to be published +export interface SimulationNodeDatum { + id: ID; + x: number; + y: number; + z: number; + vx: number; + vy: number; + vz: number; + [key: string]: any; +} + +export interface SimulationEdgeDatum { + id: ID; + source: SimulationNodeDatum; + target: SimulationNodeDatum; + [key: string]: any; +} 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/exports.ts b/packages/layout/src/exports.ts index 2eb2393..df589c1 100644 --- a/packages/layout/src/exports.ts +++ b/packages/layout/src/exports.ts @@ -3,6 +3,8 @@ export type { DagreAlign, DagreRankdir } from './antv-dagre/types'; export * from './circular'; export * from './comboCombined'; export * from './concentric'; +export { D3Force3DLayout } from './d3-force-3d'; +export type { D3Force3DLayoutOptions } from './d3-force-3d/types'; export * from './d3Force'; export * from './dagre'; export * from './force';