<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://d3js.org/d3.v7.min.js"></script> <style> .container { max-width: 1200px; margin: 0 auto; padding: 20px; font-family: "Segoe UI", Arial, sans-serif; } .node { cursor: grab; transition: fill 0.3s; } .node-text { font-size: 12px; user-select: none; } .link { stroke: #999; stroke-opacity: 0.6; } .time-axis { opacity: 0.8; } .zoom-controls { position: fixed; top: 20px; right: 20px; background: rgba(255,255,255,0.9); padding: 10px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } </style> </head> <body> <div class="container"> <div class="zoom-controls"> <button onclick="zoomIn()">+</button> <button onclick="zoomOut()">-</button> </div> <svg id="topo-svg"></svg> </div> <script> // 模拟数据 const nodes = [ { id: "Node1", date: "2020-01-01", group: 1 }, { id: "Node2", date: "2020-06-15", group: 2 }, { id: "Node3", date: "2021-03-22", group: 1 } ]; const links = [ { source: "Node1", target: "Node2", value: 5 }, { source: "Node2", target: "Node3", value: 3 } ]; // 创建基础SVG画布 const svg = d3.select("#topo-svg") .attr("width", 1200) .attr("height", 600); // 创建力导向布局 const simulation = d3.forceSimulation() .force("link", d3.forceLink().id(d => d.id)) .force("charge", d3.forceManyBody().strength(-100)) .force("center", d3.forceCenter(600, 300)); // 创建时间比例尺 const timeScale = d3.scaleTime() .domain([new Date("2020-01-01"), new Date("2021-12-31")]) .range([50, 1150]); // 绘制时间轴 const timeAxis = d3.axisBottom(timeScale) .ticks(d3.timeMonth.every(3)); svg.append("g") .attr("class", "time-axis") .attr("transform", "translate(0,550)") .call(timeAxis); // 绘制拓扑图 function renderTopo() { // 更新连线 const link = svg.selectAll(".link") .data(links) .join("line") .attr("class", "link") .attr("stroke-width", d => Math.sqrt(d.value)); // 更新节点 const node = svg.selectAll(".node") .data(nodes) .join("circle") .attr("class", "node") .attr("r", 10) .attr("fill", d => d3.schemeCategory10[d.group]) .call(drag(simulation)); // 添加文字标签 const text = svg.selectAll(".node-text") .data(nodes) .join("text") .attr("class", "node-text") .text(d => d.id) .attr("dx", 12) .attr("dy", 4); // 更新力导向布局 simulation.nodes(nodes).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); text.attr("x", d => d.x) .attr("y", d => d.y); }); simulation.force("link").links(links); } // 实现拖拽交互 function drag(simulation) { return d3.drag() .on("start", event => { if (!event.active) simulation.alphaTarget(0.3).restart(); event.subject.fx = event.subject.x; event.subject.fy = event.subject.y; }) .on("drag", event => { event.subject.fx = event.x; event.subject.fy = event.y; }) .on("end", event => { if (!event.active) simulation.alphaTarget(0); event.subject.fx = null; event.subject.fy = null; }); } // 实现缩放控制 let currentZoom = 1; function zoomIn() { currentZoom *= 1.2; svg.selectAll('*').attr('transform', `scale(${currentZoom})`); } function zoomOut() { currentZoom *= 0.8; svg.selectAll('*').attr('transform', `scale(${currentZoom})`); } // 初始化渲染 renderTopo(); </script> </body> </html>
核心功能解析:
最佳实践建议:
数据更新策略
// 使用以下方法实现动态数据更新 function updateData(newNodes, newLinks) { nodes = newNodes; links = newLinks; simulation.nodes(nodes); simulation.force("link").links(links); simulation.alpha(1).restart(); renderTopo(); }
性能优化技巧
引用说明:
本文代码实现参考以下权威资源:
(本示例在Chrome 89+、Firefox 86+浏览器测试通过,建议使用现代浏览器访问)