Public
Edited
Mar 30, 2023
Insert cell
Insert cell
chart = {
let links = linksData.filter(d => d.sourceSize + d.targetSize>800)
// links.forEach(link => {
// // 假设link.source和link.target分别表示连线两端的节点
// const source = link.source;
// const target = link.target;

// // 为source节点添加neighbors属性
// if (!source.neighbors) {
// source.neighbors = [];
// }
// source.neighbors.push(target);

// // 为target节点添加neighbors属性
// if (!target.neighbors) {
// target.neighbors = [];
// }
// target.neighbors.push(source);
// });
let nodes = nodesData;
let chargeScale=d3.scaleLinear().domain(d3.extent(nodes,d=>d.symbolSize)).range([-5,-125])
let sizeScale=d3.scaleSqrt().domain(d3.extent(nodes,d=>d.symbolSize)).range([1,15])
let strengthScale=d3.scaleLinear().domain(d3.extent(links,d=>d.targetSize+d.sourceSize)).range([0,1])
// 创建模拟系统
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(30).strength(.05))
.force("charge", d3.forceManyBody().strength(d=>chargeScale(d.symbolSize)))
.force("x", d3.forceX())
.force("y", d3.forceY())
// .alphaTarget(0.001)
// links = linksData.filter(d => d.sourceSize + d.targetSize>500)


// 添加svg
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height])
.call(d3.zoom().on("zoom", (event) => {
node.attr("transform", event.transform);
link.attr("transform", event.transform);
text.attr('transform',event.transform)
}));

// 添加边
let link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.7)
.selectAll("line")
.data(links)
.join("line")
// .attr('fill',d=>{
// return d.sourceSize+d.targetSize>1000?'#fff':'#000'
// })
let linkF=link.filter(d => d.sourceSize + d.targetSize >800)
// .style("visibility", "none")
// .attr("stroke-width", 0);
// 添加节点
const nodeColor=["#e21818","#00235b","#ffdd83","#98dfd6"];
const node = svg.append("g")
.attr("fill", "#fff")
.attr("stroke", "#000")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("fill", d => nodeColor[d.category])
.attr('stroke-width',.5)
.attr("r", d=>sizeScale(d.symbolSize))
.call(drag(simulation))

// 添加鼠标悬停事件
node.on("mouseover", (event, d) => {
node
.transition()
.duration(1000)
.style("opacity", n => {
// 判断n节点是否与d节点相连
// const isConnected = linksData.some(link => (link.source === d && link.target === n) || (link.source === n && link.target === d));
return isConnected(d,n) ? 1 : 0.1;
});
text
.transition()
.duration(1000)
.style("display", n => isConnected(d, n)&n.symbolSize > 400||n==d? "block" : "none");
// linkF=link.filter(l => l.source === d || l.target === d)
linkF
.transition()
.duration(1000)
.style("opacity", l => l.source === d || l.target === d ? 1 : 0.1);

clicked = [true, true, true, true];
legendEntry.selectAll("path")
.attr("fill", d => d.color)
})
.on("mouseout", () => {
node
.transition()
.duration(1000)
.style("opacity", 1);
text
.transition()
.duration(1000)
.style("display", d => d.symbolSize > 1000 ? "block" : "none");
linkF
.transition()
.duration(1000)
.style("opacity", 1);
});

function isConnected(a, b) {
return linkedByIndex[`${a.index},${b.index}`] || linkedByIndex[`${b.index},${a.index}`] || a.index === b.index;
}

const linkedByIndex = {};
linksData.forEach(d => {
linkedByIndex[`${d.source.index},${d.target.index}`] = true;
});


// 添加图例
const legend = svg.append("g")
.attr("transform", "translate(-250,-470)");

const legendData = nodeColor.map((color, i) => ({color: color, category: i}));

const legendEntry = legend.selectAll("g")
.data(legendData)
.enter()
.append("g")
.attr("transform", (d,i) => `translate(${i*120},0)`);

let clicked = [true, true, true, true];
legendEntry.on("click", function(event, d) {
// 获取点击的类别
let category = event.srcElement.__data__.category;
// 反转对应类别的状态
clicked[category] = !clicked[category];
// 筛选出对应类别的节点
const selectedNodes = d3.selectAll("circle").filter(n => n.category === category);
if(clicked[category]){
// 如果状态为true,显示节点和原来的图例颜色
selectedNodes.transition()
.duration(1000)
.style("opacity", 1);
legendEntry.selectAll('path').filter(d=>d.category===category).attr('fill',d=>d.color)
}else{
// 如果状态为false,隐藏节点和改变图例颜色为灰色
selectedNodes.transition()
.duration(1000)
.style("opacity", .1);
legendEntry.selectAll('path').filter(d=>d.category===category).attr('fill','gray')
}
// 给节点添加悬停事件监听器
selectedNodes.on("mouseover", function(event, d) {
// 判断当前节点是否被隐藏,如果是,就不执行悬停事件的逻辑
if (!clicked[category]) return;
// 否则,正常执行悬停事件的逻辑
node
.transition()
.duration(1000)
.style("opacity", n => {
// 判断n节点是否与d节点相连
// const isConnected = linksData.some(link => (link.source === d && link.target === n) || (link.source === n && link.target === d));
return isConnected(d,n) ? 1 : 0.1;
});
text
.transition()
.duration(1000)
.style("display", n => isConnected(d, n)&n.symbolSize > 400||n==d? "block" : "none");
// linkF=link.filter(l => l.source === d || l.target === d)
linkF
.transition()
.duration(1000)
.style("opacity", l => l.source === d || l.target === d ? 1 : 0.1);
})
.on("mouseout", function(event, d) {
// 判断当前节点是否被隐藏,如果是,就不执行悬停事件的逻辑
if (!clicked[category]) return;
// 否则,正常执行悬停事件的逻辑
node
.transition()
.duration(1000)
.style("opacity", 1);
text
.transition()
.duration(1000)
.style("display", d => d.symbolSize > 1000 ? "block" : "none");
linkF
.transition()
.duration(1000)
.style("opacity", 1);
})
});
let rwidth = 20;
let rheight = 20;
let rradius = 5;
const pathData = d3.path();
pathData.moveTo(rradius, 0);
pathData.lineTo(rwidth - rradius, 0);
pathData.quadraticCurveTo(rwidth, 0, rwidth, rradius);
pathData.lineTo(rwidth, rheight - rradius);
pathData.quadraticCurveTo(rwidth, rheight, rwidth - rradius, rheight);
pathData.lineTo(rradius,rheight);
pathData.quadraticCurveTo(0,rheight ,0 ,rheight - rradius);
pathData.lineTo(0,rradius);
pathData.quadraticCurveTo(0 ,0 ,rradius ,0);

legendEntry.append("path")
.attr("d", pathData.toString())
.attr("fill", d => d.color)
legendEntry.append("text")
.text(d => `Category ${d.category}`)
.attr("x", 25)
.attr("y", 15);

// 添加标题
// node.append("title")
// .text(d => d.name)
// 添加默认文本
const text = svg.append("g")
.attr("fill", "#000")
.selectAll("text")
.data(nodes)
.join("text")
.text(d => d.name)
.attr("x", d => d.x)
.attr("y", d => d.y)
.style("font-size", "12px")
.style("display", d => d.symbolSize > 1000 ? "block" : "none")
.style("text-anchor", "middle") // 水平居中对齐
.style("dominant-baseline", "central")
.style ("pointer-events" , "none")
// .transition()
// .duration(1000)
simulation.on("tick", () => {
linkF
.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);

// 更新text元素的位置
text.attr("x", d => d.x )
.attr("y", d => d.y );
});
function ticked() {
linkF
.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);

// 更新text元素的位置
text.attr("x", d => d.x )
.attr("y", d => d.y );
}
// simulation.stop()
// setInterval(()=>{
// simulation.tick(1)
// ticked()
// },1000)
invalidation.then(() => simulation.stop());

return svg.node();
}
Insert cell
nodesData = FileAttachment("nodes.json").json()
Insert cell
linksData = FileAttachment("links.json").json()
Insert cell
height = 1000
Insert cell
drag = simulation => {
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
d3 = require("d3@6")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more