1 https://www.cnblogs.com/fastmover/p/7779660.html
<template>
<div>
<div class="container"></div>
</div>
</template>
<script>
import * as d3 from "d3";
export default {
data() {
return {
svgArea: null,
links: [],
nodes: [],
nodesName: [],
linksName: [],
simulation: null,
scale: 1,
width: 1200,
height: 600,
colorList: [
"#FD7623",
"#3388B1",
"#D82952",
"#F3D737",
"#409071",
"#D64E52",
],
testGraph: {
nodes: [
{ id: "Myriel", group: 1 },
{ id: "Napoleon", group: 1 },
{ id: "Mlle.Baptistine", group: 1 },
{ id: "Mme.Magloire", group: 1 },
{ id: "CountessdeLo", group: 1 },
{ id: "Geborand", group: 1 },
{ id: "Champtercier", group: 1 },
{ id: "Cravatte", group: 1 },
{ id: "Count", group: 1 },
{ id: "OldMan", group: 1 },
{ id: "Labarre", group: 2 },
{ id: "Valjean", group: 2 },
{ id: "Marguerite", group: 3 },
{ id: "Mme.deR", group: 2 },
{ id: "Isabeau", group: 2 },
{ id: "Gervais", group: 2 },
{ id: "Tholomyes", group: 3 },
{ id: "Listolier", group: 3 },
{ id: "Fameuil", group: 3 },
{ id: "Blacheville", group: 3 },
{ id: "Favourite", group: 3 },
{ id: "Dahlia", group: 3 },
{ id: "Zephine", group: 3 },
{ id: "Fantine", group: 3 },
{ id: "Mme.Thenardier", group: 4 },
{ id: "Thenardier", group: 4 },
{ id: "Cosette", group: 5 },
{ id: "Javert", group: 4 },
{ id: "Fauchelevent", group: 0 },
{ id: "Bamatabois", group: 2 },
{ id: "Perpetue", group: 3 },
{ id: "Simplice", group: 2 },
{ id: "Scaufflaire", group: 2 },
{ id: "Woman1", group: 2 },
{ id: "Judge", group: 2 },
{ id: "Champmathieu", group: 2 },
{ id: "Brevet", group: 2 },
{ id: "Chenildieu", group: 2 },
{ id: "Cochepaille", group: 2 },
{ id: "Pontmercy", group: 4 },
{ id: "Boulatruelle", group: 6 },
{ id: "Eponine", group: 4 },
{ id: "Anzelma", group: 4 },
{ id: "Woman2", group: 5 },
{ id: "MotherInnocent", group: 0 },
{ id: "Gribier", group: 0 },
{ id: "Jondrette", group: 7 },
{ id: "Mme.Burgon", group: 7 },
{ id: "Gavroche", group: 8 },
{ id: "Gillenormand", group: 5 },
{ id: "Magnon", group: 5 },
{ id: "Mlle.Gillenormand", group: 5 },
{ id: "Mme.Pontmercy", group: 5 },
{ id: "Mlle.Vaubois", group: 5 },
{ id: "Lt.Gillenormand", group: 5 },
{ id: "Marius", group: 8 },
{ id: "BaronessT", group: 5 },
{ id: "Mabeuf", group: 8 },
{ id: "Enjolras", group: 8 },
{ id: "Combeferre", group: 8 },
{ id: "Prouvaire", group: 8 },
{ id: "Feuilly", group: 8 },
{ id: "Courfeyrac", group: 8 },
{ id: "Bahorel", group: 8 },
{ id: "Bossuet", group: 8 },
{ id: "Joly", group: 8 },
{ id: "Grantaire", group: 8 },
{ id: "MotherPlutarch", group: 9 },
{ id: "Gueulemer", group: 4 },
{ id: "Babet", group: 4 },
{ id: "Claquesous", group: 4 },
{ id: "Montparnasse", group: 4 },
{ id: "Toussaint", group: 5 },
{ id: "Child1", group: 10 },
{ id: "Child2", group: 10 },
{ id: "Brujon", group: 4 },
{ id: "Mme.Hucheloup", group: 8 },
],
links: [
{ source: "Napoleon", target: "Myriel", value: 1 },
{ source: "Mlle.Baptistine", target: "Myriel", value: 8 },
{ source: "Mme.Magloire", target: "Myriel", value: 10 },
{ source: "Mme.Magloire", target: "Mlle.Baptistine", value: 6 },
{ source: "CountessdeLo", target: "Myriel", value: 1 },
{ source: "Geborand", target: "Myriel", value: 1 },
{ source: "Champtercier", target: "Myriel", value: 1 },
{ source: "Cravatte", target: "Myriel", value: 1 },
{ source: "Count", target: "Myriel", value: 2 },
{ source: "OldMan", target: "Myriel", value: 1 },
{ source: "Valjean", target: "Labarre", value: 1 },
{ source: "Valjean", target: "Mme.Magloire", value: 3 },
{ source: "Valjean", target: "Mlle.Baptistine", value: 3 },
{ source: "Valjean", target: "Myriel", value: 5 },
{ source: "Marguerite", target: "Valjean", value: 1 },
{ source: "Mme.deR", target: "Valjean", value: 1 },
{ source: "Isabeau", target: "Valjean", value: 1 },
{ source: "Gervais", target: "Valjean", value: 1 },
{ source: "Listolier", target: "Tholomyes", value: 4 },
{ source: "Fameuil", target: "Tholomyes", value: 4 },
{ source: "Fameuil", target: "Listolier", value: 4 },
{ source: "Blacheville", target: "Tholomyes", value: 4 },
{ source: "Blacheville", target: "Listolier", value: 4 },
{ source: "Blacheville", target: "Fameuil", value: 4 },
{ source: "Favourite", target: "Tholomyes", value: 3 },
{ source: "Favourite", target: "Listolier", value: 3 },
{ source: "Favourite", target: "Fameuil", value: 3 },
{ source: "Favourite", target: "Blacheville", value: 4 },
{ source: "Dahlia", target: "Tholomyes", value: 3 },
{ source: "Dahlia", target: "Listolier", value: 3 },
{ source: "Dahlia", target: "Fameuil", value: 3 },
{ source: "Dahlia", target: "Blacheville", value: 3 },
{ source: "Dahlia", target: "Favourite", value: 5 },
{ source: "Zephine", target: "Tholomyes", value: 3 },
{ source: "Zephine", target: "Listolier", value: 3 },
{ source: "Zephine", target: "Fameuil", value: 3 },
{ source: "Zephine", target: "Blacheville", value: 3 },
{ source: "Zephine", target: "Favourite", value: 4 },
{ source: "Zephine", target: "Dahlia", value: 4 },
{ source: "Fantine", target: "Tholomyes", value: 3 },
{ source: "Fantine", target: "Listolier", value: 3 },
{ source: "Fantine", target: "Fameuil", value: 3 },
{ source: "Fantine", target: "Blacheville", value: 3 },
{ source: "Fantine", target: "Favourite", value: 4 },
{ source: "Fantine", target: "Dahlia", value: 4 },
{ source: "Fantine", target: "Zephine", value: 4 },
{ source: "Fantine", target: "Marguerite", value: 2 },
{ source: "Fantine", target: "Valjean", value: 9 },
{ source: "Mme.Thenardier", target: "Fantine", value: 2 },
{ source: "Mme.Thenardier", target: "Valjean", value: 7 },
{ source: "Thenardier", target: "Mme.Thenardier", value: 13 },
{ source: "Thenardier", target: "Fantine", value: 1 },
{ source: "Thenardier", target: "Valjean", value: 12 },
{ source: "Cosette", target: "Mme.Thenardier", value: 4 },
{ source: "Cosette", target: "Valjean", value: 31 },
{ source: "Cosette", target: "Tholomyes", value: 1 },
{ source: "Cosette", target: "Thenardier", value: 1 },
{ source: "Javert", target: "Valjean", value: 17 },
{ source: "Javert", target: "Fantine", value: 5 },
{ source: "Javert", target: "Thenardier", value: 5 },
{ source: "Javert", target: "Mme.Thenardier", value: 1 },
{ source: "Javert", target: "Cosette", value: 1 },
{ source: "Fauchelevent", target: "Valjean", value: 8 },
{ source: "Fauchelevent", target: "Javert", value: 1 },
{ source: "Bamatabois", target: "Fantine", value: 1 },
{ source: "Bamatabois", target: "Javert", value: 1 },
{ source: "Bamatabois", target: "Valjean", value: 2 },
{ source: "Perpetue", target: "Fantine", value: 1 },
{ source: "Simplice", target: "Perpetue", value: 2 },
{ source: "Simplice", target: "Valjean", value: 3 },
{ source: "Simplice", target: "Fantine", value: 2 },
{ source: "Simplice", target: "Javert", value: 1 },
{ source: "Scaufflaire", target: "Valjean", value: 1 },
{ source: "Woman1", target: "Valjean", value: 2 },
{ source: "Woman1", target: "Javert", value: 1 },
{ source: "Judge", target: "Valjean", value: 3 },
{ source: "Judge", target: "Bamatabois", value: 2 },
{ source: "Champmathieu", target: "Valjean", value: 3 },
{ source: "Champmathieu", target: "Judge", value: 3 },
{ source: "Champmathieu", target: "Bamatabois", value: 2 },
{ source: "Brevet", target: "Judge", value: 2 },
{ source: "Brevet", target: "Champmathieu", value: 2 },
{ source: "Brevet", target: "Valjean", value: 2 },
{ source: "Brevet", target: "Bamatabois", value: 1 },
{ source: "Chenildieu", target: "Judge", value: 2 },
{ source: "Chenildieu", target: "Champmathieu", value: 2 },
{ source: "Chenildieu", target: "Brevet", value: 2 },
{ source: "Chenildieu", target: "Valjean", value: 2 },
{ source: "Chenildieu", target: "Bamatabois", value: 1 },
{ source: "Cochepaille", target: "Judge", value: 2 },
{ source: "Cochepaille", target: "Champmathieu", value: 2 },
{ source: "Cochepaille", target: "Brevet", value: 2 },
{ source: "Cochepaille", target: "Chenildieu", value: 2 },
{ source: "Cochepaille", target: "Valjean", value: 2 },
{ source: "Cochepaille", target: "Bamatabois", value: 1 },
{ source: "Pontmercy", target: "Thenardier", value: 1 },
{ source: "Boulatruelle", target: "Thenardier", value: 1 },
{ source: "Eponine", target: "Mme.Thenardier", value: 2 },
{ source: "Eponine", target: "Thenardier", value: 3 },
{ source: "Anzelma", target: "Eponine", value: 2 },
{ source: "Anzelma", target: "Thenardier", value: 2 },
{ source: "Anzelma", target: "Mme.Thenardier", value: 1 },
{ source: "Woman2", target: "Valjean", value: 3 },
{ source: "Woman2", target: "Cosette", value: 1 },
{ source: "Woman2", target: "Javert", value: 1 },
{ source: "MotherInnocent", target: "Fauchelevent", value: 3 },
{ source: "MotherInnocent", target: "Valjean", value: 1 },
{ source: "Gribier", target: "Fauchelevent", value: 2 },
{ source: "Mme.Burgon", target: "Jondrette", value: 1 },
{ source: "Gavroche", target: "Mme.Burgon", value: 2 },
{ source: "Gavroche", target: "Thenardier", value: 1 },
{ source: "Gavroche", target: "Javert", value: 1 },
{ source: "Gavroche", target: "Valjean", value: 1 },
{ source: "Gillenormand", target: "Cosette", value: 3 },
{ source: "Gillenormand", target: "Valjean", value: 2 },
{ source: "Magnon", target: "Gillenormand", value: 1 },
{ source: "Magnon", target: "Mme.Thenardier", value: 1 },
{ source: "Mlle.Gillenormand", target: "Gillenormand", value: 9 },
{ source: "Mlle.Gillenormand", target: "Cosette", value: 2 },
{ source: "Mlle.Gillenormand", target: "Valjean", value: 2 },
{ source: "Mme.Pontmercy", target: "Mlle.Gillenormand", value: 1 },
{ source: "Mme.Pontmercy", target: "Pontmercy", value: 1 },
{ source: "Mlle.Vaubois", target: "Mlle.Gillenormand", value: 1 },
{ source: "Lt.Gillenormand", target: "Mlle.Gillenormand", value: 2 },
{ source: "Lt.Gillenormand", target: "Gillenormand", value: 1 },
{ source: "Lt.Gillenormand", target: "Cosette", value: 1 },
{ source: "Marius", target: "Mlle.Gillenormand", value: 6 },
{ source: "Marius", target: "Gillenormand", value: 12 },
{ source: "Marius", target: "Pontmercy", value: 1 },
{ source: "Marius", target: "Lt.Gillenormand", value: 1 },
{ source: "Marius", target: "Cosette", value: 21 },
{ source: "Marius", target: "Valjean", value: 19 },
{ source: "Marius", target: "Tholomyes", value: 1 },
{ source: "Marius", target: "Thenardier", value: 2 },
{ source: "Marius", target: "Eponine", value: 5 },
{ source: "Marius", target: "Gavroche", value: 4 },
{ source: "BaronessT", target: "Gillenormand", value: 1 },
{ source: "BaronessT", target: "Marius", value: 1 },
{ source: "Mabeuf", target: "Marius", value: 1 },
{ source: "Mabeuf", target: "Eponine", value: 1 },
{ source: "Mabeuf", target: "Gavroche", value: 1 },
{ source: "Enjolras", target: "Marius", value: 7 },
{ source: "Enjolras", target: "Gavroche", value: 7 },
{ source: "Enjolras", target: "Javert", value: 6 },
{ source: "Enjolras", target: "Mabeuf", value: 1 },
{ source: "Enjolras", target: "Valjean", value: 4 },
{ source: "Combeferre", target: "Enjolras", value: 15 },
{ source: "Combeferre", target: "Marius", value: 5 },
{ source: "Combeferre", target: "Gavroche", value: 6 },
{ source: "Combeferre", target: "Mabeuf", value: 2 },
{ source: "Prouvaire", target: "Gavroche", value: 1 },
{ source: "Prouvaire", target: "Enjolras", value: 4 },
{ source: "Prouvaire", target: "Combeferre", value: 2 },
{ source: "Feuilly", target: "Gavroche", value: 2 },
{ source: "Feuilly", target: "Enjolras", value: 6 },
{ source: "Feuilly", target: "Prouvaire", value: 2 },
{ source: "Feuilly", target: "Combeferre", value: 5 },
{ source: "Feuilly", target: "Mabeuf", value: 1 },
{ source: "Feuilly", target: "Marius", value: 1 },
{ source: "Courfeyrac", target: "Marius", value: 9 },
{ source: "Courfeyrac", target: "Enjolras", value: 17 },
{ source: "Courfeyrac", target: "Combeferre", value: 13 },
{ source: "Courfeyrac", target: "Gavroche", value: 7 },
{ source: "Courfeyrac", target: "Mabeuf", value: 2 },
{ source: "Courfeyrac", target: "Eponine", value: 1 },
{ source: "Courfeyrac", target: "Feuilly", value: 6 },
{ source: "Courfeyrac", target: "Prouvaire", value: 3 },
{ source: "Bahorel", target: "Combeferre", value: 5 },
{ source: "Bahorel", target: "Gavroche", value: 5 },
{ source: "Bahorel", target: "Courfeyrac", value: 6 },
{ source: "Bahorel", target: "Mabeuf", value: 2 },
{ source: "Bahorel", target: "Enjolras", value: 4 },
{ source: "Bahorel", target: "Feuilly", value: 3 },
{ source: "Bahorel", target: "Prouvaire", value: 2 },
{ source: "Bahorel", target: "Marius", value: 1 },
{ source: "Bossuet", target: "Marius", value: 5 },
{ source: "Bossuet", target: "Courfeyrac", value: 12 },
{ source: "Bossuet", target: "Gavroche", value: 5 },
{ source: "Bossuet", target: "Bahorel", value: 4 },
{ source: "Bossuet", target: "Enjolras", value: 10 },
{ source: "Bossuet", target: "Feuilly", value: 6 },
{ source: "Bossuet", target: "Prouvaire", value: 2 },
{ source: "Bossuet", target: "Combeferre", value: 9 },
{ source: "Bossuet", target: "Mabeuf", value: 1 },
{ source: "Bossuet", target: "Valjean", value: 1 },
{ source: "Joly", target: "Bahorel", value: 5 },
{ source: "Joly", target: "Bossuet", value: 7 },
{ source: "Joly", target: "Gavroche", value: 3 },
{ source: "Joly", target: "Courfeyrac", value: 5 },
{ source: "Joly", target: "Enjolras", value: 5 },
{ source: "Joly", target: "Feuilly", value: 5 },
{ source: "Joly", target: "Prouvaire", value: 2 },
{ source: "Joly", target: "Combeferre", value: 5 },
{ source: "Joly", target: "Mabeuf", value: 1 },
{ source: "Joly", target: "Marius", value: 2 },
{ source: "Grantaire", target: "Bossuet", value: 3 },
{ source: "Grantaire", target: "Enjolras", value: 3 },
{ source: "Grantaire", target: "Combeferre", value: 1 },
{ source: "Grantaire", target: "Courfeyrac", value: 2 },
{ source: "Grantaire", target: "Joly", value: 2 },
{ source: "Grantaire", target: "Gavroche", value: 1 },
{ source: "Grantaire", target: "Bahorel", value: 1 },
{ source: "Grantaire", target: "Feuilly", value: 1 },
{ source: "Grantaire", target: "Prouvaire", value: 1 },
{ source: "MotherPlutarch", target: "Mabeuf", value: 3 },
{ source: "Gueulemer", target: "Thenardier", value: 5 },
{ source: "Gueulemer", target: "Valjean", value: 1 },
{ source: "Gueulemer", target: "Mme.Thenardier", value: 1 },
{ source: "Gueulemer", target: "Javert", value: 1 },
{ source: "Gueulemer", target: "Gavroche", value: 1 },
{ source: "Gueulemer", target: "Eponine", value: 1 },
{ source: "Babet", target: "Thenardier", value: 6 },
{ source: "Babet", target: "Gueulemer", value: 6 },
{ source: "Babet", target: "Valjean", value: 1 },
{ source: "Babet", target: "Mme.Thenardier", value: 1 },
{ source: "Babet", target: "Javert", value: 2 },
{ source: "Babet", target: "Gavroche", value: 1 },
{ source: "Babet", target: "Eponine", value: 1 },
{ source: "Claquesous", target: "Thenardier", value: 4 },
{ source: "Claquesous", target: "Babet", value: 4 },
{ source: "Claquesous", target: "Gueulemer", value: 4 },
{ source: "Claquesous", target: "Valjean", value: 1 },
{ source: "Claquesous", target: "Mme.Thenardier", value: 1 },
{ source: "Claquesous", target: "Javert", value: 1 },
{ source: "Claquesous", target: "Eponine", value: 1 },
{ source: "Claquesous", target: "Enjolras", value: 1 },
{ source: "Montparnasse", target: "Javert", value: 1 },
{ source: "Montparnasse", target: "Babet", value: 2 },
{ source: "Montparnasse", target: "Gueulemer", value: 2 },
{ source: "Montparnasse", target: "Claquesous", value: 2 },
{ source: "Montparnasse", target: "Valjean", value: 1 },
{ source: "Montparnasse", target: "Gavroche", value: 1 },
{ source: "Montparnasse", target: "Eponine", value: 1 },
{ source: "Montparnasse", target: "Thenardier", value: 1 },
{ source: "Toussaint", target: "Cosette", value: 2 },
{ source: "Toussaint", target: "Javert", value: 1 },
{ source: "Toussaint", target: "Valjean", value: 1 },
{ source: "Child1", target: "Gavroche", value: 2 },
{ source: "Child2", target: "Gavroche", value: 2 },
{ source: "Child2", target: "Child1", value: 3 },
{ source: "Brujon", target: "Babet", value: 3 },
{ source: "Brujon", target: "Gueulemer", value: 3 },
{ source: "Brujon", target: "Thenardier", value: 3 },
{ source: "Brujon", target: "Gavroche", value: 1 },
{ source: "Brujon", target: "Eponine", value: 1 },
{ source: "Brujon", target: "Claquesous", value: 1 },
{ source: "Brujon", target: "Montparnasse", value: 1 },
{ source: "Mme.Hucheloup", target: "Bossuet", value: 1 },
{ source: "Mme.Hucheloup", target: "Joly", value: 1 },
{ source: "Mme.Hucheloup", target: "Grantaire", value: 1 },
{ source: "Mme.Hucheloup", target: "Bahorel", value: 1 },
{ source: "Mme.Hucheloup", target: "Courfeyrac", value: 1 },
{ source: "Mme.Hucheloup", target: "Gavroche", value: 1 },
{ source: "Mme.Hucheloup", target: "Enjolras", value: 1 },
],
},
};
},
mounted() {
this.initGraph(this.testGraph);
},
methods: {
initGraph(data) {
const links = data.links.map((d) => Object.create(d));
const nodes = data.nodes.map((d) => Object.create(d));
const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3.forceLink(links).id((d) => d.id)
)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(this.width / 2, this.height / 2));
const svg = d3
.select(".container")
.append("svg")
.attr("viewBox", [0, 0, this.width, this.height]);
svg.call(
d3.zoom().on("zoom", function (d) {
g.attr("transform", d.transform);
})
);
const g =svg.append("g");
const link = g
.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", (d) => Math.sqrt(d.value));
const node = g
.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 25)
.attr("fill", this.color)
.call(this.drag(simulation));
node.append("title").text((d) => d.id);
simulation.on("tick", () => {
link
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
});
},
color(d) {
return this.colorList[d.group];
},
drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
},
},
};
</script>
<style scoped>
.container {
width: 1200px;
height: 600px;
border: 1px solid #2c3e50;
border-radius: 8px;
margin-top: 40px;
margin-left: auto;
margin-right: auto;
background: #154360
repeating-linear-gradient(
30deg,
hsla(0, 0%, 100%, 0.1),
hsla(0, 0%, 100%, 0.1) 15px,
transparent 0,
transparent 30px
);
}
.node {
stroke: #fff;
stroke-width: 1;
cursor: pointer;
}
.node:hover {
stroke-width: 5;
}
.nodeName {
fill: white;
}
</style>
更复杂的老版本,有报错问题
<template>
<div>
<div class="container"></div>
</div>
</template>
<script>
import * as d3 from "d3";
export default {
data() {
return {
svgArea: null,
links: [],
nodes: [],
nodesName: [],
linksName: [],
simulation: null,
scale: 1,
width: 1200,
height: 600,
colorList: [
"#FD7623",
"#3388B1",
"#D82952",
"#F3D737",
"#409071",
"#D64E52",
],
testGraph: {
nodes: [
{ id: "Myriel", group: 1 },
{ id: "Napoleon", group: 1 },
{ id: "Mlle.Baptistine", group: 1 },
{ id: "Mme.Magloire", group: 1 },
{ id: "CountessdeLo", group: 1 },
{ id: "Geborand", group: 1 },
{ id: "Champtercier", group: 1 },
{ id: "Cravatte", group: 1 },
{ id: "Count", group: 1 },
{ id: "OldMan", group: 1 },
{ id: "Labarre", group: 2 },
{ id: "Valjean", group: 2 },
{ id: "Marguerite", group: 3 },
{ id: "Mme.deR", group: 2 },
{ id: "Isabeau", group: 2 },
{ id: "Gervais", group: 2 },
{ id: "Tholomyes", group: 3 },
{ id: "Listolier", group: 3 },
{ id: "Fameuil", group: 3 },
{ id: "Blacheville", group: 3 },
{ id: "Favourite", group: 3 },
{ id: "Dahlia", group: 3 },
{ id: "Zephine", group: 3 },
{ id: "Fantine", group: 3 },
{ id: "Mme.Thenardier", group: 4 },
{ id: "Thenardier", group: 4 },
{ id: "Cosette", group: 5 },
{ id: "Javert", group: 4 },
{ id: "Fauchelevent", group: 0 },
{ id: "Bamatabois", group: 2 },
{ id: "Perpetue", group: 3 },
{ id: "Simplice", group: 2 },
{ id: "Scaufflaire", group: 2 },
{ id: "Woman1", group: 2 },
{ id: "Judge", group: 2 },
{ id: "Champmathieu", group: 2 },
{ id: "Brevet", group: 2 },
{ id: "Chenildieu", group: 2 },
{ id: "Cochepaille", group: 2 },
{ id: "Pontmercy", group: 4 },
{ id: "Boulatruelle", group: 6 },
{ id: "Eponine", group: 4 },
{ id: "Anzelma", group: 4 },
{ id: "Woman2", group: 5 },
{ id: "MotherInnocent", group: 0 },
{ id: "Gribier", group: 0 },
{ id: "Jondrette", group: 7 },
{ id: "Mme.Burgon", group: 7 },
{ id: "Gavroche", group: 8 },
{ id: "Gillenormand", group: 5 },
{ id: "Magnon", group: 5 },
{ id: "Mlle.Gillenormand", group: 5 },
{ id: "Mme.Pontmercy", group: 5 },
{ id: "Mlle.Vaubois", group: 5 },
{ id: "Lt.Gillenormand", group: 5 },
{ id: "Marius", group: 8 },
{ id: "BaronessT", group: 5 },
{ id: "Mabeuf", group: 8 },
{ id: "Enjolras", group: 8 },
{ id: "Combeferre", group: 8 },
{ id: "Prouvaire", group: 8 },
{ id: "Feuilly", group: 8 },
{ id: "Courfeyrac", group: 8 },
{ id: "Bahorel", group: 8 },
{ id: "Bossuet", group: 8 },
{ id: "Joly", group: 8 },
{ id: "Grantaire", group: 8 },
{ id: "MotherPlutarch", group: 9 },
{ id: "Gueulemer", group: 4 },
{ id: "Babet", group: 4 },
{ id: "Claquesous", group: 4 },
{ id: "Montparnasse", group: 4 },
{ id: "Toussaint", group: 5 },
{ id: "Child1", group: 10 },
{ id: "Child2", group: 10 },
{ id: "Brujon", group: 4 },
{ id: "Mme.Hucheloup", group: 8 },
],
links: [
{ source: "Napoleon", target: "Myriel", value: 1 },
{ source: "Mlle.Baptistine", target: "Myriel", value: 8 },
{ source: "Mme.Magloire", target: "Myriel", value: 10 },
{ source: "Mme.Magloire", target: "Mlle.Baptistine", value: 6 },
{ source: "CountessdeLo", target: "Myriel", value: 1 },
{ source: "Geborand", target: "Myriel", value: 1 },
{ source: "Champtercier", target: "Myriel", value: 1 },
{ source: "Cravatte", target: "Myriel", value: 1 },
{ source: "Count", target: "Myriel", value: 2 },
{ source: "OldMan", target: "Myriel", value: 1 },
{ source: "Valjean", target: "Labarre", value: 1 },
{ source: "Valjean", target: "Mme.Magloire", value: 3 },
{ source: "Valjean", target: "Mlle.Baptistine", value: 3 },
{ source: "Valjean", target: "Myriel", value: 5 },
{ source: "Marguerite", target: "Valjean", value: 1 },
{ source: "Mme.deR", target: "Valjean", value: 1 },
{ source: "Isabeau", target: "Valjean", value: 1 },
{ source: "Gervais", target: "Valjean", value: 1 },
{ source: "Listolier", target: "Tholomyes", value: 4 },
{ source: "Fameuil", target: "Tholomyes", value: 4 },
{ source: "Fameuil", target: "Listolier", value: 4 },
{ source: "Blacheville", target: "Tholomyes", value: 4 },
{ source: "Blacheville", target: "Listolier", value: 4 },
{ source: "Blacheville", target: "Fameuil", value: 4 },
{ source: "Favourite", target: "Tholomyes", value: 3 },
{ source: "Favourite", target: "Listolier", value: 3 },
{ source: "Favourite", target: "Fameuil", value: 3 },
{ source: "Favourite", target: "Blacheville", value: 4 },
{ source: "Dahlia", target: "Tholomyes", value: 3 },
{ source: "Dahlia", target: "Listolier", value: 3 },
{ source: "Dahlia", target: "Fameuil", value: 3 },
{ source: "Dahlia", target: "Blacheville", value: 3 },
{ source: "Dahlia", target: "Favourite", value: 5 },
{ source: "Zephine", target: "Tholomyes", value: 3 },
{ source: "Zephine", target: "Listolier", value: 3 },
{ source: "Zephine", target: "Fameuil", value: 3 },
{ source: "Zephine", target: "Blacheville", value: 3 },
{ source: "Zephine", target: "Favourite", value: 4 },
{ source: "Zephine", target: "Dahlia", value: 4 },
{ source: "Fantine", target: "Tholomyes", value: 3 },
{ source: "Fantine", target: "Listolier", value: 3 },
{ source: "Fantine", target: "Fameuil", value: 3 },
{ source: "Fantine", target: "Blacheville", value: 3 },
{ source: "Fantine", target: "Favourite", value: 4 },
{ source: "Fantine", target: "Dahlia", value: 4 },
{ source: "Fantine", target: "Zephine", value: 4 },
{ source: "Fantine", target: "Marguerite", value: 2 },
{ source: "Fantine", target: "Valjean", value: 9 },
{ source: "Mme.Thenardier", target: "Fantine", value: 2 },
{ source: "Mme.Thenardier", target: "Valjean", value: 7 },
{ source: "Thenardier", target: "Mme.Thenardier", value: 13 },
{ source: "Thenardier", target: "Fantine", value: 1 },
{ source: "Thenardier", target: "Valjean", value: 12 },
{ source: "Cosette", target: "Mme.Thenardier", value: 4 },
{ source: "Cosette", target: "Valjean", value: 31 },
{ source: "Cosette", target: "Tholomyes", value: 1 },
{ source: "Cosette", target: "Thenardier", value: 1 },
{ source: "Javert", target: "Valjean", value: 17 },
{ source: "Javert", target: "Fantine", value: 5 },
{ source: "Javert", target: "Thenardier", value: 5 },
{ source: "Javert", target: "Mme.Thenardier", value: 1 },
{ source: "Javert", target: "Cosette", value: 1 },
{ source: "Fauchelevent", target: "Valjean", value: 8 },
{ source: "Fauchelevent", target: "Javert", value: 1 },
{ source: "Bamatabois", target: "Fantine", value: 1 },
{ source: "Bamatabois", target: "Javert", value: 1 },
{ source: "Bamatabois", target: "Valjean", value: 2 },
{ source: "Perpetue", target: "Fantine", value: 1 },
{ source: "Simplice", target: "Perpetue", value: 2 },
{ source: "Simplice", target: "Valjean", value: 3 },
{ source: "Simplice", target: "Fantine", value: 2 },
{ source: "Simplice", target: "Javert", value: 1 },
{ source: "Scaufflaire", target: "Valjean", value: 1 },
{ source: "Woman1", target: "Valjean", value: 2 },
{ source: "Woman1", target: "Javert", value: 1 },
{ source: "Judge", target: "Valjean", value: 3 },
{ source: "Judge", target: "Bamatabois", value: 2 },
{ source: "Champmathieu", target: "Valjean", value: 3 },
{ source: "Champmathieu", target: "Judge", value: 3 },
{ source: "Champmathieu", target: "Bamatabois", value: 2 },
{ source: "Brevet", target: "Judge", value: 2 },
{ source: "Brevet", target: "Champmathieu", value: 2 },
{ source: "Brevet", target: "Valjean", value: 2 },
{ source: "Brevet", target: "Bamatabois", value: 1 },
{ source: "Chenildieu", target: "Judge", value: 2 },
{ source: "Chenildieu", target: "Champmathieu", value: 2 },
{ source: "Chenildieu", target: "Brevet", value: 2 },
{ source: "Chenildieu", target: "Valjean", value: 2 },
{ source: "Chenildieu", target: "Bamatabois", value: 1 },
{ source: "Cochepaille", target: "Judge", value: 2 },
{ source: "Cochepaille", target: "Champmathieu", value: 2 },
{ source: "Cochepaille", target: "Brevet", value: 2 },
{ source: "Cochepaille", target: "Chenildieu", value: 2 },
{ source: "Cochepaille", target: "Valjean", value: 2 },
{ source: "Cochepaille", target: "Bamatabois", value: 1 },
{ source: "Pontmercy", target: "Thenardier", value: 1 },
{ source: "Boulatruelle", target: "Thenardier", value: 1 },
{ source: "Eponine", target: "Mme.Thenardier", value: 2 },
{ source: "Eponine", target: "Thenardier", value: 3 },
{ source: "Anzelma", target: "Eponine", value: 2 },
{ source: "Anzelma", target: "Thenardier", value: 2 },
{ source: "Anzelma", target: "Mme.Thenardier", value: 1 },
{ source: "Woman2", target: "Valjean", value: 3 },
{ source: "Woman2", target: "Cosette", value: 1 },
{ source: "Woman2", target: "Javert", value: 1 },
{ source: "MotherInnocent", target: "Fauchelevent", value: 3 },
{ source: "MotherInnocent", target: "Valjean", value: 1 },
{ source: "Gribier", target: "Fauchelevent", value: 2 },
{ source: "Mme.Burgon", target: "Jondrette", value: 1 },
{ source: "Gavroche", target: "Mme.Burgon", value: 2 },
{ source: "Gavroche", target: "Thenardier", value: 1 },
{ source: "Gavroche", target: "Javert", value: 1 },
{ source: "Gavroche", target: "Valjean", value: 1 },
{ source: "Gillenormand", target: "Cosette", value: 3 },
{ source: "Gillenormand", target: "Valjean", value: 2 },
{ source: "Magnon", target: "Gillenormand", value: 1 },
{ source: "Magnon", target: "Mme.Thenardier", value: 1 },
{ source: "Mlle.Gillenormand", target: "Gillenormand", value: 9 },
{ source: "Mlle.Gillenormand", target: "Cosette", value: 2 },
{ source: "Mlle.Gillenormand", target: "Valjean", value: 2 },
{ source: "Mme.Pontmercy", target: "Mlle.Gillenormand", value: 1 },
{ source: "Mme.Pontmercy", target: "Pontmercy", value: 1 },
{ source: "Mlle.Vaubois", target: "Mlle.Gillenormand", value: 1 },
{ source: "Lt.Gillenormand", target: "Mlle.Gillenormand", value: 2 },
{ source: "Lt.Gillenormand", target: "Gillenormand", value: 1 },
{ source: "Lt.Gillenormand", target: "Cosette", value: 1 },
{ source: "Marius", target: "Mlle.Gillenormand", value: 6 },
{ source: "Marius", target: "Gillenormand", value: 12 },
{ source: "Marius", target: "Pontmercy", value: 1 },
{ source: "Marius", target: "Lt.Gillenormand", value: 1 },
{ source: "Marius", target: "Cosette", value: 21 },
{ source: "Marius", target: "Valjean", value: 19 },
{ source: "Marius", target: "Tholomyes", value: 1 },
{ source: "Marius", target: "Thenardier", value: 2 },
{ source: "Marius", target: "Eponine", value: 5 },
{ source: "Marius", target: "Gavroche", value: 4 },
{ source: "BaronessT", target: "Gillenormand", value: 1 },
{ source: "BaronessT", target: "Marius", value: 1 },
{ source: "Mabeuf", target: "Marius", value: 1 },
{ source: "Mabeuf", target: "Eponine", value: 1 },
{ source: "Mabeuf", target: "Gavroche", value: 1 },
{ source: "Enjolras", target: "Marius", value: 7 },
{ source: "Enjolras", target: "Gavroche", value: 7 },
{ source: "Enjolras", target: "Javert", value: 6 },
{ source: "Enjolras", target: "Mabeuf", value: 1 },
{ source: "Enjolras", target: "Valjean", value: 4 },
{ source: "Combeferre", target: "Enjolras", value: 15 },
{ source: "Combeferre", target: "Marius", value: 5 },
{ source: "Combeferre", target: "Gavroche", value: 6 },
{ source: "Combeferre", target: "Mabeuf", value: 2 },
{ source: "Prouvaire", target: "Gavroche", value: 1 },
{ source: "Prouvaire", target: "Enjolras", value: 4 },
{ source: "Prouvaire", target: "Combeferre", value: 2 },
{ source: "Feuilly", target: "Gavroche", value: 2 },
{ source: "Feuilly", target: "Enjolras", value: 6 },
{ source: "Feuilly", target: "Prouvaire", value: 2 },
{ source: "Feuilly", target: "Combeferre", value: 5 },
{ source: "Feuilly", target: "Mabeuf", value: 1 },
{ source: "Feuilly", target: "Marius", value: 1 },
{ source: "Courfeyrac", target: "Marius", value: 9 },
{ source: "Courfeyrac", target: "Enjolras", value: 17 },
{ source: "Courfeyrac", target: "Combeferre", value: 13 },
{ source: "Courfeyrac", target: "Gavroche", value: 7 },
{ source: "Courfeyrac", target: "Mabeuf", value: 2 },
{ source: "Courfeyrac", target: "Eponine", value: 1 },
{ source: "Courfeyrac", target: "Feuilly", value: 6 },
{ source: "Courfeyrac", target: "Prouvaire", value: 3 },
{ source: "Bahorel", target: "Combeferre", value: 5 },
{ source: "Bahorel", target: "Gavroche", value: 5 },
{ source: "Bahorel", target: "Courfeyrac", value: 6 },
{ source: "Bahorel", target: "Mabeuf", value: 2 },
{ source: "Bahorel", target: "Enjolras", value: 4 },
{ source: "Bahorel", target: "Feuilly", value: 3 },
{ source: "Bahorel", target: "Prouvaire", value: 2 },
{ source: "Bahorel", target: "Marius", value: 1 },
{ source: "Bossuet", target: "Marius", value: 5 },
{ source: "Bossuet", target: "Courfeyrac", value: 12 },
{ source: "Bossuet", target: "Gavroche", value: 5 },
{ source: "Bossuet", target: "Bahorel", value: 4 },
{ source: "Bossuet", target: "Enjolras", value: 10 },
{ source: "Bossuet", target: "Feuilly", value: 6 },
{ source: "Bossuet", target: "Prouvaire", value: 2 },
{ source: "Bossuet", target: "Combeferre", value: 9 },
{ source: "Bossuet", target: "Mabeuf", value: 1 },
{ source: "Bossuet", target: "Valjean", value: 1 },
{ source: "Joly", target: "Bahorel", value: 5 },
{ source: "Joly", target: "Bossuet", value: 7 },
{ source: "Joly", target: "Gavroche", value: 3 },
{ source: "Joly", target: "Courfeyrac", value: 5 },
{ source: "Joly", target: "Enjolras", value: 5 },
{ source: "Joly", target: "Feuilly", value: 5 },
{ source: "Joly", target: "Prouvaire", value: 2 },
{ source: "Joly", target: "Combeferre", value: 5 },
{ source: "Joly", target: "Mabeuf", value: 1 },
{ source: "Joly", target: "Marius", value: 2 },
{ source: "Grantaire", target: "Bossuet", value: 3 },
{ source: "Grantaire", target: "Enjolras", value: 3 },
{ source: "Grantaire", target: "Combeferre", value: 1 },
{ source: "Grantaire", target: "Courfeyrac", value: 2 },
{ source: "Grantaire", target: "Joly", value: 2 },
{ source: "Grantaire", target: "Gavroche", value: 1 },
{ source: "Grantaire", target: "Bahorel", value: 1 },
{ source: "Grantaire", target: "Feuilly", value: 1 },
{ source: "Grantaire", target: "Prouvaire", value: 1 },
{ source: "MotherPlutarch", target: "Mabeuf", value: 3 },
{ source: "Gueulemer", target: "Thenardier", value: 5 },
{ source: "Gueulemer", target: "Valjean", value: 1 },
{ source: "Gueulemer", target: "Mme.Thenardier", value: 1 },
{ source: "Gueulemer", target: "Javert", value: 1 },
{ source: "Gueulemer", target: "Gavroche", value: 1 },
{ source: "Gueulemer", target: "Eponine", value: 1 },
{ source: "Babet", target: "Thenardier", value: 6 },
{ source: "Babet", target: "Gueulemer", value: 6 },
{ source: "Babet", target: "Valjean", value: 1 },
{ source: "Babet", target: "Mme.Thenardier", value: 1 },
{ source: "Babet", target: "Javert", value: 2 },
{ source: "Babet", target: "Gavroche", value: 1 },
{ source: "Babet", target: "Eponine", value: 1 },
{ source: "Claquesous", target: "Thenardier", value: 4 },
{ source: "Claquesous", target: "Babet", value: 4 },
{ source: "Claquesous", target: "Gueulemer", value: 4 },
{ source: "Claquesous", target: "Valjean", value: 1 },
{ source: "Claquesous", target: "Mme.Thenardier", value: 1 },
{ source: "Claquesous", target: "Javert", value: 1 },
{ source: "Claquesous", target: "Eponine", value: 1 },
{ source: "Claquesous", target: "Enjolras", value: 1 },
{ source: "Montparnasse", target: "Javert", value: 1 },
{ source: "Montparnasse", target: "Babet", value: 2 },
{ source: "Montparnasse", target: "Gueulemer", value: 2 },
{ source: "Montparnasse", target: "Claquesous", value: 2 },
{ source: "Montparnasse", target: "Valjean", value: 1 },
{ source: "Montparnasse", target: "Gavroche", value: 1 },
{ source: "Montparnasse", target: "Eponine", value: 1 },
{ source: "Montparnasse", target: "Thenardier", value: 1 },
{ source: "Toussaint", target: "Cosette", value: 2 },
{ source: "Toussaint", target: "Javert", value: 1 },
{ source: "Toussaint", target: "Valjean", value: 1 },
{ source: "Child1", target: "Gavroche", value: 2 },
{ source: "Child2", target: "Gavroche", value: 2 },
{ source: "Child2", target: "Child1", value: 3 },
{ source: "Brujon", target: "Babet", value: 3 },
{ source: "Brujon", target: "Gueulemer", value: 3 },
{ source: "Brujon", target: "Thenardier", value: 3 },
{ source: "Brujon", target: "Gavroche", value: 1 },
{ source: "Brujon", target: "Eponine", value: 1 },
{ source: "Brujon", target: "Claquesous", value: 1 },
{ source: "Brujon", target: "Montparnasse", value: 1 },
{ source: "Mme.Hucheloup", target: "Bossuet", value: 1 },
{ source: "Mme.Hucheloup", target: "Joly", value: 1 },
{ source: "Mme.Hucheloup", target: "Grantaire", value: 1 },
{ source: "Mme.Hucheloup", target: "Bahorel", value: 1 },
{ source: "Mme.Hucheloup", target: "Courfeyrac", value: 1 },
{ source: "Mme.Hucheloup", target: "Gavroche", value: 1 },
{ source: "Mme.Hucheloup", target: "Enjolras", value: 1 },
],
},
};
},
mounted() {
this.initGraph(this.testGraph);
},
methods: {
getGraphData() {
var _this = this;
this.axios
.get("person/" + "Rob Reiner")
// this.axios.get("person/all")
.then(function (response) {
console.log(response);
_this.testGraph["nodes"] = [response.data];
_this.initGraph(_this.testGraph);
})
.catch(function (error) {
console.log(error);
});
},
initGraph(data) {
const links = data.links;
const nodes = data.nodes;
this.simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(links)
.id((d) => d.id)
.distance(150)
)
.force(
"collide",
d3.forceCollide().radius(() => 30)
)
.force("charge", d3.forceManyBody().strength(-10))
.force("center", d3.forceCenter(this.width / 2, this.height / 2));
this.svgArea = d3
.select(".container")
.append("svg")
.attr("viewBox", [0, 0, this.width, this.height])
.call(
d3.zoom().on("zoom", function (d) {
g.attr("transform", d.transform);
})
);
this.addMarkers();
const g = this.svgArea.append("g").attr("class", "content");
this.links = g
.append("g")
.selectAll("path")
.data(links, function (d) {
if (typeof d.source === "object") {
return d.source.id + "_" + d.relationship + "_" + d.target.id;
} else {
return d.source + "_" + d.relationship + "_" + d.target;
}
})
.join("path")
.attr("marker-end", "url(#positiveMarker)")
.attr("stroke-width", (d) => Math.sqrt(d.value))
.attr("class", "link")
.attr("id", function (d) {
if (typeof d.source === "object") {
return d.source.id + "_" + d.relationship + "_" + d.target.id;
} else {
return d.source + "_" + d.relationship + "_" + d.target;
}
});
this.linksName = g
.append("g")
.selectAll("text")
.data(links, function (d) {
if (typeof d.source === "object") {
return d.source.id + "_" + d.relationship + "_" + d.target.id;
} else {
return d.source + "_" + d.relationship + "_" + d.target;
}
})
.join("text")
.style("text-anchor", "middle")
.style("fill", "white")
.style("font-size", "10px")
.style("font-weight", "bold");
this.linksName
.append("textPath")
.attr(
"xlink:href",
(d) => "#" + d.source + "_" + d.relationship + "_" + d.target
)
.attr("startOffset", "50%")
.text((d) => d.relationship);
this.nodes = g
.append("g")
.selectAll("circle")
.data(nodes, (d) => d.id)
.join("circle")
.attr("r", 30)
.attr("class", "node")
.attr("fill", this.color)
.on("click", this.select)
.call(this.drag(this.simulation));
this.nodes.append("title").text((d) => d.id);
this.nodesName = g
.append("g")
.selectAll("text")
.data(nodes)
.join("text")
.text((d) => d.id)
.attr("dx", function () {
return (this.getBoundingClientRect().width / 2) * -1;
})
.attr("dy", 50)
.attr("class", "nodeName");
this.simulation.on("tick", () => {
this.links
.attr("d", function (d) {
if (d.source.x < d.target.x) {
return (
"M " +
d.source.x +
" " +
d.source.y +
" L " +
d.target.x +
" " +
d.target.y
);
} else {
return (
"M " +
d.target.x +
" " +
d.target.y +
" L " +
d.source.x +
" " +
d.source.y
);
}
})
.attr("marker-end", function (d) {
if (d.source.x < d.target.x) {
return "url(#positiveMarker)";
} else {
return null;
}
})
.attr("marker-start", function (d) {
if (d.source.x < d.target.x) {
return null;
} else {
return "url(#negativeMarker)";
}
});
this.nodes.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
this.nodesName.attr("x", (d) => d.x).attr("y", (d) => d.y);
});
},
addMarkers() {
const positiveMarker = this.svgArea
.append("marker")
.attr("id", "positiveMarker")
.attr("orient", "auto")
.attr("stroke-width", 2)
.attr("markerUnits", "strokeWidth")
.attr("markerUnits", "userSpaceOnUse")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 35)
.attr("refY", 0)
.attr("markerWidth", 12)
.attr("markerHeight", 12)
.append("path")
.attr("d", "M 0 -5 L 10 0 L 0 5")
.attr("fill", "#999")
.attr("stroke-opacity", 0.6);
const negativeMarker = this.svgArea
.append("marker")
.attr("id", "negativeMarker")
.attr("orient", "auto")
.attr("stroke-width", 2)
.attr("markerUnits", "strokeWidth")
.attr("markerUnits", "userSpaceOnUse")
.attr("viewBox", "0 -5 10 10")
.attr("refX", -25)
.attr("refY", 0)
.attr("markerWidth", 12)
.attr("markerHeight", 12)
.append("path")
.attr("d", "M 10 -5 L 0 0 L 10 5")
.attr("fill", "#999")
.attr("stroke-opacity", 0.6);
},
updateGraph(data) {
const links = data.links;
const nodes = data.nodes;
this.links = this.links
.data(links, function (d) {
if (typeof d.source === "object") {
return d.source.id + "_" + d.relationship + "_" + d.target.id;
} else {
return d.source + "_" + d.relationship + "_" + d.target;
}
})
.join("path")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", (d) => Math.sqrt(d.value))
.attr("marker-end", "url(#positiveMarker)")
.merge(this.links)
.attr("id", function (d) {
if (typeof d.source === "object") {
return d.source.id + "_" + d.relationship + "_" + d.target.id;
} else {
return d.source + "_" + d.relationship + "_" + d.target;
}
})
.attr("class", "link");
this.linksName = this.linksName
.data(links, function (d) {
if (typeof d.source === "object") {
return d.source.id + "_" + d.relationship + "_" + d.target.id;
} else {
return d.source + "_" + d.relationship + "_" + d.target;
}
})
.join("text")
.style("text-anchor", "middle")
.style("fill", "white")
.style("font-size", "10px")
.style("font-weight", "bold");
this.linksName
.append("textPath")
.attr("xlink:href", function (d) {
if (typeof d.source === "object") {
return "#" + d.source.id + "_" + d.relationship + "_" + d.target.id;
} else {
return "#" + d.source + "_" + d.relationship + "_" + d.target;
}
})
.attr("startOffset", "50%")
.merge(this.linksName)
.text((d) => d.relationship);
this.nodes = this.nodes
.data(nodes, (d) => d.id)
.join("circle")
.attr("r", 30)
.attr("class", "node")
.attr("fill", this.color)
.merge(this.nodes)
.on("click", this.select)
.call(this.drag(this.simulation));
this.nodes.append("title").text((d) => d.id);
this.nodesName = this.nodesName
.data(nodes)
.join("text")
.merge(this.nodesName)
.text(function (d) {
return d.id;
})
.attr("dx", function () {
return (this.getBoundingClientRect().width / 2) * -1;
})
.attr("dy", 50)
.attr("class", "nodeName");
this.simulation.nodes(nodes);
this.simulation.force("link").links(links);
this.simulation.alpha(0.2).restart();
},
color(d) {
return this.colorList[d.group];
},
drag(simulation) {
function dragstarted(d) {
if (!d.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d.x;
d.fy = d.y;
}
function dragended(d) {
if (!d.active) simulation.alphaTarget(0.5);
d.fx = null;
d.fy = null;
}
return d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
},
select(d) {
var _this = this;
let data = {};
for (var i in d.obj) {
let ifArray = d.obj[i] instanceof Array;
if (!ifArray) {
data[i] = d.obj[i];
}
}
_this.$refs.detailPanel.currentNode = data;
_this.$refs.detailPanel.ifShow = true;
},
getQueryResult(result, currentNode, currentType) {
for (var i = 0; i < result.length; i++) {
//result:查询得到的节点组
let flag = true;
for (var j = 0; j < this.testGraph.nodes.length; j++) {
if (this.testGraph.nodes[j].id === result[i].id) {
flag = false;
}
}
if (flag) {
this.testGraph.nodes.push(result[i]);
} else {
console.log("已存在的节点");
console.log(result[i]);
}
this.testGraph.links.push({
source: currentNode.name,
target: result[i].id,
value: 5,
relationship: currentType,
});
}
for (var i = this.testGraph.links.length - 1; i >= 0; i--) {
if (
this.testGraph.links[i].source.id === currentNode.name &&
this.testGraph.links[i].relationship !== currentType
) {
let ifRemove = true;
for (var k = 0; k < result.length; k++) {
if (result[k].id === this.testGraph.links[i].target.id) {
ifRemove = false;
console.log("不移除此节点" + result[k].id);
break;
}
}
if (ifRemove) {
console.log(this.testGraph.nodes);
for (var j = this.testGraph.nodes.length - 1; j >= 0; j--) {
console.log("移除此节点" + this.testGraph.links[i].target.id);
if (
this.testGraph.nodes[j].id === this.testGraph.links[i].target.id
) {
this.testGraph.nodes.splice(j, 1);
}
}
console.log(this.testGraph.nodes);
}
this.testGraph.links.splice(i, 1);
}
}
this.updateGraph(this.testGraph);
},
},
};
</script>
<style scoped>
.container{
width: 800px;
height: 500px;
border: 1px solid #2c3e50;
border-radius:8px;
margin-top: 40px;
margin-left: auto;
margin-right: auto;
background: #154360 repeating-linear-gradient(30deg,
hsla(0, 0%, 100%, .1), hsla(0, 0%, 100%, .1) 15px,
transparent 0, transparent 30px);
}
.node{
stroke:#fff;
stroke-width:1;
cursor: pointer;
}
.node:hover{
stroke-width:5;
}
.nodeName{
fill:white;
}
</style>
react例子
import React from 'react'
import PropTypes from 'prop-types'
import * as d3 from 'd3'
class D3SimpleForceChart extends React.Component {
componentDidMount() {
const containerWidth = this.chartRef.parentElement.offsetWidth
const data = this.props.data
const margin = { top: 60, right: 60, bottom: 60, left: 60 }
const width = containerWidth - margin.left - margin.right
const height = 700 - margin.top - margin.bottom
let chart = d3
.select(this.chartRef)
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
let g = chart
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') // 设最外包层在总图上的相对位置
let simulation = d3
.forceSimulation() // 构建力导向图
.force(
'link',
d3
.forceLink()
.id(function(d, i) {
return i
})
.distance(function(d) {
return d.value * 50
})
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
let z = d3.scaleOrdinal(d3.schemeCategory20) // 通用线条的颜色
let link = g
.append('g') // 画连接线
.attr('class', 'links')
.selectAll('line')
.data(data.edges)
.enter()
.append('line')
let linkText = g
.append('g') // 画连接连上面的关系文字
.attr('class', 'link-text')
.selectAll('text')
.data(data.edges)
.enter()
.append('text')
.text(function(d) {
return d.relation
})
let node = g
.append('g') // 画圆圈和文字
.attr('class', 'nodes')
.selectAll('g')
.data(data.nodes)
.enter()
.append('g')
.on('mouseover', function(d, i) {
//显示连接线上的文字
linkText.style('fill-opacity', function(edge) {
if (edge.source === d || edge.target === d) {
return 1
}
})
//连接线加粗
link
.style('stroke-width', function(edge) {
if (edge.source === d || edge.target === d) {
return '2px'
}
})
.style('stroke', function(edge) {
if (edge.source === d || edge.target === d) {
return '#000'
}
})
})
.on('mouseout', function(d, i) {
//隐去连接线上的文字
linkText.style('fill-opacity', function(edge) {
if (edge.source === d || edge.target === d) {
return 0
}
})
//连接线减粗
link
.style('stroke-width', function(edge) {
if (edge.source === d || edge.target === d) {
return '1px'
}
})
.style('stroke', function(edge) {
if (edge.source === d || edge.target === d) {
return '#ddd'
}
})
})
.call(
d3
.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended)
)
node
.append('circle')
.attr('r', 5)
.attr('fill', function(d, i) {
return z(i)
})
node
.append('text')
.attr('fill', function(d, i) {
return z(i)
})
.attr('y', -20)
.attr('dy', '.71em')
.text(function(d) {
return d.name
})
simulation // 初始化力导向图
.nodes(data.nodes)
.on('tick', ticked)
simulation.force('link').links(data.edges)
chart
.append('g') // 输出标题
.attr('class', 'bar--title')
.append('text')
.attr('fill', '#000')
.attr('font-size', '16px')
.attr('font-weight', '700')
.attr('text-anchor', 'middle')
.attr('x', containerWidth / 2)
.attr('y', 20)
.text('人物关系图')
function ticked() {
// 力导向图变化函数,让力学图不断更新
link
.attr('x1', function(d) {
return d.source.x
})
.attr('y1', function(d) {
return d.source.y
})
.attr('x2', function(d) {
return d.target.x
})
.attr('y2', function(d) {
return d.target.y
})
linkText
.attr('x', function(d) {
return (d.source.x + d.target.x) / 2
})
.attr('y', function(d) {
return (d.source.y + d.target.y) / 2
})
node.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')'
})
}
function dragstarted(d) {
if (!d3.event.active) {
simulation.alphaTarget(0.3).restart()
}
d.fx = d.x
d.fy = d.y
}
function dragged(d) {
d.fx = d3.event.x
d.fy = d3.event.y
}
function dragended(d) {
if (!d3.event.active) {
simulation.alphaTarget(0)
}
d.fx = null
d.fy = null
}
}
render() {
return (
<div className="force-chart--simple">
<svg ref={r => (this.chartRef = r)} />
</div>
)
}
}
D3SimpleForceChart.propTypes = {
data: PropTypes.shape({
nodes: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired
// href:PropTypes.string.isRequired,
}).isRequired
).isRequired,
edges: PropTypes.arrayOf(
PropTypes.shape({
source: PropTypes.number.isRequired,
target: PropTypes.number.isRequired,
relation: PropTypes.string.isRequired
}).isRequired
).isRequired
}).isRequired
}
export default D3SimpleForceChart
html d3v5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.link-active {
stroke-opacity: 1;
stroke-width: 3;
}
</style>
<script src="http://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<div style="border:1px solid #000;position: relative;">
<svg width="960" height="500"></svg>
<div
style="width:100px;height:100px;border:1px solid red;position:absolute;top:2px;right:2px;background-color: azure;">
<span id="xxx"></span>
</div>
</div>
</body>
<script type="text/javascript">
let marge = { top: 60, bottom: 60, left: 60, right: 60 }
let svg = d3.select('svg')
let width = svg.attr('width')
let height = svg.attr('height')
svg.call(
d3.zoom().on('zoom', function () {
g.attr('transform', d3.event.transform)
})
)
.on('dblclick.zoom', null)
let g = svg.append('g')
.attr('transform', 'translate(' + marge.top + ',' + marge.left + ')')
.attr('class', 'container')
// 准备数据
// 节点集
let nodes = [
{ id: 12, name: '湖南邵阳' },
{ id: 2, name: '山东泰安' },
{ id: 3, name: '广东阳江不知道怎么回' },
{ id: 4, name: '山西太原' },
{ id: 5, name: '亮' },
{ id: 6, name: '丽' },
{ id: 7, name: '雪' },
{ id: 8, name: '小明' },
{ id: 9, name: '组长' }
]
// 边集
let tempEdges = [
{ id: 1, source: 12, target: 5, relation: '籍贯', value: 1.3 },
{ id: 2, source: 5, target: 6, relation: '舍友', value: 1 },
{ id: 3, source: 5, target: 7, relation: '舍友', value: 1 },
{ id: 4, source: 5, target: 8, relation: '舍友', value: 1 },
{ id: 5, source: 2, target: 7, relation: '籍贯', value: 2 },
{ id: 6, source: 3, target: 6, relation: '籍贯', value: 0.9 },
{ id: 7, source: 4, target: 8, relation: '籍贯', value: 1 },
{ id: 8, source: 6, target: 7, relation: '同学', value: 1.6 },
{ id: 9, source: 7, target: 8, relation: '朋友', value: 0.7 },
{ id: 10, source: 7, target: 9, relation: '职责', value: 2 },
{ id: 11, source: 9, target: 7, relation: '人物', value: 2 },
{ id: 12, source: 9, target: 7, relation: '哈哈哈', value: 2 }
]
nodes.forEach(item => {
})
// 生成 nodes map
let nodesMap = genNodesMap(nodes);
console.log('3333',nodesMap)
nodesData = d3.values(nodesMap)
let linkMap = genLinkMap(tempEdges)
// 构建 links(source 属性必须从 0 开始)
edges = genLinks(tempEdges);
console.log('123123',edges,nodesData)
// 设置一个颜色比例尺
let colorScale = d3.scaleOrdinal()
.domain(d3.range(nodesData.length))
.range(d3.schemeCategory10)
// 新建一个力导向图
let forceSimulation = d3.forceSimulation()
.force('link', d3.forceLink())
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter())
// 生成节点数据
forceSimulation.nodes(nodesData)
// 生成边数据
forceSimulation.force('link')
.links(edges)
.distance(function (d) { // 每一边的长度
return d.value * 200
})
// 设置图形中心位置
forceSimulation.force('center')
.x(width / 2)
.y(height / 2)
// 箭头
var marker = g.append('g').attr('class', 'showLine').append('marker')
.attr('id', 'resolved')
// .attr("markerUnits","strokeWidth")// 设置为strokeWidth箭头会随着线的粗细发生变化
.attr('markerUnits', 'userSpaceOnUse')
.attr('viewBox', '0 -5 10 10')// 坐标系的区域
.attr('refX', 44)// 箭头坐标
.attr('refY', 0)
.attr('markerWidth', 10)// 标识的大小
.attr('markerHeight', 10)
.attr('orient', 'auto')// 绘制方向,可设定为:auto(自动确认方向)和 角度值
.attr('stroke-width', 2)// 箭头宽度
.append('path')
.attr('d', 'M0,-5L10,0L0,5')// 箭头的路径
.attr('fill', '#000000')// 箭头颜色
// 绘制边
let links = g.append('g').selectAll('path')
.data(edges)
.enter()
.append('path')
.attr('d', link => genLinkPath(link)) // 遍历所有数据。d表示当前遍历到的数据,返回绘制的贝塞尔曲线
.attr('id', (d, i) => { return 'edgepath' + d.id }) // 设置id,用于连线文字
.style('stroke', '#000') // 颜色
.style('stroke-width', 2) // 粗细
.attr('class', 'lines')
.attr('marker-end', 'url(#resolved)') // 根据箭头标记的id号标记箭头
// 边上的文字
let linksText = g.append('g')
.selectAll('text')
.data(edges)
.enter()
.append('text')
.attr('class', 'linksText')
.text(function (d) {
return d.relations
})
.style('font-size', 14)
.attr('fill-opacity', 0)
// 创建分组
let gs = g.append('g')
.selectAll('.circleText')
.data(nodesData)
.enter()
.append('g')
.attr('class', 'singleNode')
.attr('id', function (d) {
return 'singleNode' + d.id
})
.style('cursor', 'pointer')
.attr('transform', function (d) {
let cirX = d.x
let cirY = d.y
return 'translate(' + cirX + ',' + cirY + ')'
})
// 鼠标交互
gs.on('mouseover', function (d, i) {
// 显示连接线上的文字
toggleLineText(d, true)
toggleLine(links, d, true)
toggleNode(gs, d, true)
})
.on('mouseout', function (d, i) {
// 隐去连接线上的文字
toggleLineText(d, false)
toggleLine(links, d, false)
toggleNode(gs, d, false)
})
.on('click', function (d, i) {
linksText.style('fill-opacity', function (edge) {
if (edge.source === d) {
return 1
}
})
toggleCircle(d3.select(this), d)
}, true)
.call(d3.drag()
.on('start', started)
.on('drag', dragged)
.on('end', ended)
)
svg.on('click', function () {
nodes.forEach(d => d.clickFlag = false)
var event = d3.event
var target = event.srcElement, // 获取事件发生源
data = d3.select(target).datum(); // 获取事件发生源的数据
removeSingle()
if (!data) {
document.getElementById('xxx').innerText = ''
}
}, true)
forceSimulation.on('tick', ticked)
function toggleLineText(currNode, isHover) {
if (isHover) {
linksText.style('fill-opacity', function (edge) {
if (edge.source === currNode) {
return 1
}
})
} else {
linksText.style('fill-opacity', function (edge) {
if (edge.source === currNode || edge.target === currNode) {
return 0
}
})
}
}
function toggleLine(linkLine, currNode, isHover) {
if (isHover) {
// 加重连线样式
links
.style('opacity', 0.1)
.filter(link => isLinkLine(currNode, link))
.style('opacity', 1)
.classed('link-active', true)
} else {
links
.style('opacity', 1)
.classed('link-active', false)
}
}
function showMyList() {
var e = { id: 10, name: "河北" };
var h = { id: 11, name: "河南" };
var f = { id: 13, source: 9, target: 10, relation: '222', value: 2 };
nodes.push(e);
nodes.push(h);
tempEdges.push(f);
nodesMap = genNodesMap(nodes);
nodesData = d3.values(nodesMap)
linkMap = genLinkMap(tempEdges)
edges = genLinks(tempEdges)
updateData()
}
function updateData() {
links = links
.data(edges, function (d) {
})
.join("path")
.attr('id', (d, i) => { return 'edgepath' + d.id }) // 设置id,用于连线文字
.style('stroke', '#000') // 颜色
.style('stroke-width', 2) // 粗细
.attr('class', 'lines')
.attr('marker-end', 'url(#resolved)') // 根据箭头标记的id号标记箭头
.merge(links);
linksText = linksText
.data(edges)
.join('text')
.attr('class', 'linksText')
.text(function (d) {
return d.relations
})
.style('font-size', 14)
.attr('fill-opacity', 0)
gs = gs
.data(nodesData, function (d) {
})
.join("g")
.attr('class', 'singleNode')
.attr('id', function (d) {
return 'singleNode' + d.id
})
.style('cursor', 'pointer')
.merge(gs)
.call(d3.drag()
.on("start", started)
.on("drag", dragged)
.on("end", ended));
gs.append('circle')
.attr('fill', function (d) {
return 'orange'
})
.attr('r', 35)
.attr('stroke', 'grey')
.attr('stroke-width', 3)
gs.on('mouseover', function (d, i) {
// 显示连接线上的文字
toggleLineText(d, true)
toggleLine(links, d, true)
toggleNode(gs, d, true)
})
.on('mouseout', function (d, i) {
// 隐去连接线上的文字
toggleLineText(d, false)
toggleLine(links, d, false)
toggleNode(gs, d, false)
})
.on('click', function (d, i) {
linksText.style('fill-opacity', function (edge) {
if (edge.source === d) {
return 1
}
})
toggleCircle(d3.select(this), d)
}, true)
gs.append('text')
.attr('y', -20)
.attr('dy', 10)
.attr('text-anchor', 'middle')
.style('font-size', 12)
.attr('x', function ({ name }) {
return textBreaking(d3.select(this), name)
})
gs.append('title')
.text((node) => {
return node.name
})
forceSimulation.nodes(nodesData);
forceSimulation.force("link").links(edges);
forceSimulation.alpha(0.8).restart();
}
function getLineTextAngle(d, bbox) {
if (d.target.x < d.source.x) {
const {
x,
y,
width,
height
} = bbox;
const rx = x + width / 2;
const ry = y + height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
} else {
return 'rotate(0)';
}
}
function toggleNode(nodeCircle, currNode, isHover) {
if (isHover) {
// 提升节点层级
// nodeCircle.sort((a, b) => a.id === currNode.id ? 1 : -1);
// this.parentNode.appendChild(this);
nodeCircle
.style('opacity', .1)
.filter(node => isLinkNode(currNode, node))
.style('opacity', 1);
} else {
nodeCircle.style('opacity', 1);
}
}
function removeSingle() {
d3.select('.singleCircle').remove()
}
function toggleCircle(current, d) {
var currentD = d
if (d.clickFlag) {
removeSingle()
document.getElementById('xxx').innerText = ''
}
d.clickFlag = true
document.getElementById('xxx').innerText = d.name
var data = [{
population: 30,
value: 'X',
type: 'delete'
}, {
population: 30,
value: '收起',
type: 'showOn'
}, {
population: 30,
value: '展开',
type: 'showOff'
}]
var sum = d3.sum(data.map(function (d) {
return d.population
}))
for (i in data) {
data[i].Percentage = (data[i].population / sum * 100).toFixed(0) + "%";
}
var width = 300,
height = 300,
margin = { "left": 30, "top": 30, "right": 30, "bottom": 30 },
svg_width = width + margin.left + margin.right,
svg_height = height + margin.top + margin.bottom,
font_size = 15;
var g = current
.append("g")
.attr('class', 'singleCircle')
.attr("width", width)
.attr("height", height)
var Pie = g.append("g")
var arc_generator = d3.arc()
.innerRadius(width / 6.5)
.outerRadius(width / 4)
var angle_data = d3.pie()
.value(function (d) {
return d.population;
})
var pieData = angle_data(data)
var pieAngle = pieData.map(function (p) {
return (p.startAngle + p.endAngle) / 2 / Math.PI * 180;
});
// var color=d3.schemeCategory10;
//生成内部圆环
Pie.selectAll("path")
.data(angle_data(data))
.enter()
.append("path")
.attr("d", arc_generator)
.style("fill", function (d, i) {
return 'grey';
})
.style('stroke', 'black')
.attr("class", "path")
.attr('type', function (d) {
return d.data.type
})
.on('click', function (d) {
if (d.data.type === 'delete') {
deleteNode(currentD)
} else if (d.data.type === 'showOn') {
deleteNextNodes(currentD)
} else {
// showMyList()
}
d3.event.stopPropagation()
})
var arc_label = d3.arc()
.innerRadius(width / 4)
.outerRadius(width / 2)
Pie.selectAll(".arc_label")
.data(angle_data(data))
.enter()
.append("path")
.attr("d", arc_label)
.attr("class", "arc_label")
.style("fill", "none")
const labelFontSize = 12;
const labelValRadius = (170 * 0.35 - labelFontSize * 0.35); // 计算正确半径 文字位置
const labelValRadius1 = (170 * 0.35 + labelFontSize * 0.35);
const labelsVals = current.select('.singleCircle').append('g')
.classed('labelsvals', true);
// 定义两条路径以使标签的方向正确
labelsVals.append('def')
.append('path')
.attr('id', 'label-path-1')
.attr('d', `m0 ${-labelValRadius} a${labelValRadius} ${labelValRadius} 0 1,1 -0.01 0`);
labelsVals.append('def')
.append('path')
.attr('id', 'label-path-2')
.attr('d', `m0 ${-labelValRadius1} a${labelValRadius1} ${labelValRadius1} 0 1,0 0.01 0`);
labelsVals.selectAll('text')
.data(data)
.enter()
.append('text')
.style('font-size', labelFontSize)
.style('fill', 'black')
.style('font-weight', "bold")
.style('text-anchor', 'middle')
.append('textPath')
.attr('href', function (d, i) {
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 90 && angle <= 270) { // 根据角度选择路径
return '#label-path-2';
} else {
return '#label-path-1';
}
})
.attr('startOffset', function (d, i) {
const p = pieData[i];
const angle = pieAngle[i];
let percent = (p.startAngle + p.endAngle) / 2 / 2 / Math.PI * 100;
if (angle > 90 && angle <= 270) { // 分别计算每条路径的正确百分比
return 100 - percent + "%";
}
return percent + "%";
})
.text(function (d) {
return d.value;
})
.on('click', function (d) {
if (d.type === 'delete') {
deleteNode(currentD)
} else if (d.type === 'showOn') {
deleteNextNodes(currentD)
} else {
// showMyList()
}
d3.event.stopPropagation()
}, true)
}
//删除当前节点下一级没有其他关系的节点
function deleteNextNodes(curr) {
document.getElementById('xxx').innerText = '';
// var removeIndex = nodesData.findIndex(node=>node.id == curr.id)
// nodesData.splice(removeIndex,1)
// nodes.splice(removeIndex,1)
d3.select(this).remove();
let relationNode = [],
relationList = [],
hasRelationList = []
var clickNode = curr.id;//点击节点的名字
d3.selectAll(".lines").each(function (e) {
if (e.source.id == curr.id || e.target.id == curr.id) {
hasRelationList.push(e)
} else {
relationList.push(e)//出去跟删除节点有关系的其他关系
}
//需要删除的节点相关的节点
if (e.source.id == curr.id) {
relationNode.push(e.target)
}
//需要删除的节点相关的节点
if (e.target.id == curr.id) {
relationNode.push(e.source)
}
})
var tempNodeList = JSON.parse(JSON.stringify(relationNode))
tempNodeList = uniqObjInArray(tempNodeList)
//区分下级节点是否是孤节点
tempNodeList.forEach(function (item, index) {
var hasLine = relationList.findIndex(jtem => jtem.target.id == item.id || jtem.source.id == item.id)
if (hasLine >= 0) {
item.notSingle = true
}
})
tempNodeList.forEach(function (item, index) {
if (!item.notSingle) {
d3.select('#singleNode' + item.id).remove()
}
})
var otherTempNode = [];
tempNodeList = tempNodeList.map(item => {
if (!item.notSingle) {
otherTempNode.push(item)
}
})
hasRelationList.forEach(item => {
otherTempNode.forEach(jtem => {
if (jtem.id == item.source.id || jtem.id == item.target.id) {
d3.select('#edgepath' + item.id).remove()
}
})
})
d3.selectAll(".singleNode").each(function (d, i) {
var temp = d.id;
//删除当前需要隐藏的节点
if (temp == clickNode) {
removeSingle()
}
});
d3.selectAll(".linksText").each(function (e) {
if (e.source === curr || e.target === curr) {
d3.select(this).remove();
}
})
gs.style('opacity', 1);
links.style('opacity', 1)
.classed('link-active', false);
}
//删除当前及下一级没有其他关系的节点
function deleteNode(curr) {
document.getElementById('xxx').innerText = '';
var removeIndex = nodesData.findIndex(node => node.id == curr.id)
nodesData.splice(removeIndex, 1)
nodes.splice(removeIndex, 1)
d3.select(this).remove();
let relationNode = [],
relationList = []
var clickNode = curr.id;//点击节点的名字
d3.selectAll(".lines").each(function (e) {
if (e.source.id == curr.id || e.target.id == curr.id) {
d3.select(this).remove();
} else {
relationList.push(e)//出去跟删除节点有关系的其他关系
}
//需要删除的节点相关的节点
if (e.source.id == curr.id) {
relationNode.push(e.target)
}
//需要删除的节点相关的节点
if (e.target.id == curr.id) {
relationNode.push(e.source)
}
})
var tempNodeList = JSON.parse(JSON.stringify(relationNode))
tempNodeList = uniqObjInArray(tempNodeList)
//区分下级节点是否是孤节点
tempNodeList.forEach(function (item, index) {
var hasLine = relationList.findIndex(jtem => jtem.target.id == item.id || jtem.source.id == item.id)
if (hasLine >= 0) {
item.notSingle = true
}
})
tempNodeList.forEach(function (item, index) {
if (!item.notSingle) {
d3.select('#singleNode' + item.id).remove()
}
})
d3.selectAll(".singleNode").each(function (d, i) {
var temp = d.id;
//删除当前需要隐藏的节点
if (temp == clickNode) {
removeSingle()
d3.select(this).remove();
}
});
d3.selectAll(".linksText").each(function (e) {
if (e.source === curr || e.target === curr) {
d3.select(this).remove();
}
})
gs.style('opacity', 1);
links.style('opacity', 1)
.classed('link-active', false);
}
// 关联节点去重重组
function uniqObjInArray(objarray) {
let len = objarray.length;
let tempJson = {
};
let res = [];
for (let i = 0; i < len; i++) {
//取出每一个对象
tempJson[JSON.stringify(objarray[i])] = true;
}
let keyItems = Object.keys(tempJson);
for (let j = 0; j < keyItems.length; j++) {
res.push(JSON.parse(keyItems[j]));
}
return res;
}
function isLinkLine(node, link) {
return link.source.id === node.id
}
function isLinkNode(currNode, node) {
if (currNode.id === node.id) {
return true;
}
return linkMap[currNode.id + '-' + node.id] || linkMap[node.id + '-' + currNode.id];
}
function largerNode(nodes, currNode, isHover) {
if (isHover) {
gs
.style('stroke-width', 1)
.filter(node => isNode(currNode, node))
.style('stroke-width', 10)
} else {
gs
.style('stroke-width', 1)
}
}
function isNode(node, cNode) {
return true
}
// 绘制节点
gs.append('circle')
.attr('r', 35)
.attr('id', function (d) {
return 'circle' + d.id
})
.attr('fill', function (d, i) {
return 'orange'
})
.attr('stroke', 'grey')
.attr('stroke-width', 3)
// 文字
var nodeText = gs.append('text')
// .attr('x', -10)
.attr('y', -20)
.attr('dy', 10)
.attr('text-anchor', 'middle')
.style('font-size', 12)
.attr('x', function ({ name }) {
return textBreaking(d3.select(this), name)
})
gs.append('title')
.text((node) => {
return node.name
})
function genLinkMap(relations) {
const hash = {};
relations.map(function ({
source,
target,
relation
}) {
const key = source + '-' + target;
if (hash[key]) {
hash[key] += 1;
hash[key + '-relation'] += '、' + relation;
} else {
hash[key] = 1;
hash[key + '-relation'] = relation;
}
});
return hash;
}
function genLinks(relations) {
const indexHash = {};
return relations.map(function ({
id,
source,
target,
relation,
value
}, i) {
const linkKey = source + '-' + target;
const count = linkMap[linkKey];
if (indexHash[linkKey]) {
indexHash[linkKey] -= 1;
} else {
indexHash[linkKey] = count - 1;
}
return {
id,
source: nodesMap[source],
target: nodesMap[target],
relation,
value,
relations: linkMap[linkKey + '-relation'],
count: linkMap[linkKey],
index: indexHash[linkKey]
}
})
}
// 生成关系连线路径
function genLinkPath(link) {
const count = link.count;
const index = link.index;
let sx = link.source.x;
let tx = link.target.x;
let sy = link.source.y;
let ty = link.target.y;
return 'M' + sx + ',' + sy + ' L' + tx + ',' + ty;
}
function genNodesMap(nodes) {
const hash = {};
nodes.map(function ({
id,
name
}) {
hash[id] = {
id,
name
};
});
return hash;
}
// 处理节点文字换行
function textBreaking(d3text, text) {
const len = text.length
if (len <= 3) {
d3text.append('tspan')
.attr('x', 0)
.attr('y', -3)
.text(text)
} else {
const topText = text.substring(0, 3)
const midText = text.substring(3, 7)
let botText = text.substring(7, len)
let topY = -22
let midY = 8
let botY = 34
if (len <= 9) {
topY += 10
midY += 10
} else {
botText = text.substring(7, 9) + '...'
}
d3text.text('')
d3text.append('tspan')
.attr('x', 0)
.attr('y', topY)
.text(function () {
return topText
})
d3text.append('tspan')
.attr('x', 0)
.attr('y', midY)
.text(function () {
return midText
})
d3text.append('tspan')
.attr('x', 0)
.attr('y', botY - 7)
.text(function () {
return botText
})
}
}
// ticked
function ticked() {
// 连线路径
links
.attr('d', link => genLinkPath(link))
// 连线文字位置
linksText
.attr('x', function (d) { return (d.source.x + d.target.x) / 2 })
.attr('y', function (d) { return (d.source.y + d.target.y) / 2 })
// 节点位置
gs
.attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')' })
}
// drag
function started(d) {
if (!d3.event.active) {
forceSimulation.alphaTarget(0.8).restart() // 设置衰减系数,对节点位置移动过程的模拟,数值越高移动越快,数值范围[0, 1]
}
d.fx = d.x
d.fy = d.y
}
function dragged(d) {
d.fx = d3.event.x
d.fy = d3.event.y
}
function ended(d) {
if (!d3.event.active) {
forceSimulation.alphaTarget(0)
}
d.fx = null
d.fy = null
}
</script>
</html>
d3关系图网址Force-Directed Graph https://observablehq.com/@d3/force-directed-graph