D3.js是一个用于数据可视化的JavaScript库,广泛应用于Web端的数据交互式图形展示
中文文档:入门 | D3 中文网
一、D3.js核心特点
1、核心思想
将数据绑定到DOM元素,通过数据动态生成/修改可视化图形。
2、应用场景
- 交互式图表:如动态条形图、散点图、桑基图等。
- 地理信息可视化:通过地理投影(如墨卡托投影)绘制地图。
- 实时数据展示:如股票行情、监控仪表盘。
- 复杂网络图:力导向布局可直观展示社交网络或拓扑关系。
二、首先要弄明白“力导向图布局”
力导向图布局(各节点之间有力,正值表示斥力、负值表示吸引力)
官网实例:力导向图 / D3 |观察 — Force-directed graph / D3 | Observable
官方代码+代码解释:
chart = {// 1、设置宽度和高度const width = 928;const height = 600;// color 是一个颜色映射函数,根据节点的 group 属性分配颜色(如不同类别的节点显示不同颜色)。const color = d3.scaleOrdinal(d3.schemeCategory10);// 2、数据准备// 连接数据和节点数据const links = data.links.map(d => ({...d}));const nodes = data.nodes.map(d => ({...d}));// 3、创建一个力模拟,作用于节点数据。const simulation = d3.forceSimulation(nodes).force("link", d3.forceLink(links).id(d => d.id)) //定义连接力,使连接的节点保持一定距离,指定如何从节点数据中获取唯一标识符(用于匹配 links 中的 source 和 target)。.force("charge", d3.forceManyBody()) //定义节点间的斥力(负值表示排斥,正值表示吸引).force("center", d3.forceCenter(width / 2, height / 2)) //将整个图居中显示。.on("tick", ticked); //每次模拟更新时,调用 ticked 函数更新节点和边的位置// 4、创建 SVG 容器const svg = d3.create("svg").attr("width", width).attr("height", height).attr("viewBox", [0, 0, width, height]).attr("style", "max-width: 100%; height: auto;");// 5、在每个节点之间绘制连接线(Links)const link = svg.append("g") .attr("stroke", "#999") // 线条颜色.attr("stroke-opacity", 0.6) //透明度.selectAll().data(links).join("line") // 为每条连接创建 `<line>` 元素.attr("stroke-width", d => Math.sqrt(d.value)); // 线条宽度(基于 `value` 属性)// 6、绘制节点const node = svg.append("g") .attr("stroke", "#fff") // 节点边框颜色 .attr("stroke-width", 1.5) // 边框宽度.selectAll().data(nodes).join("circle") // 为每个节点创建 `<circle>` 元素.attr("r", 5) // 节点半径.attr("fill", d => color(d.group)); // 节点颜色(按 `group` 分组)node.append("title").text(d => d.id); // 鼠标悬停时显示节点 ID// 7、拖拽交互行为(拖曳前中后调用的方法,在最下面)node.call(d3.drag().on("start", dragstarted) .on("drag", dragged) .on("end", dragended));// 8、更新节点和连接位置(ticked 函数)function ticked() {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);}// 重新激活模拟(alphaTarget 控制模拟的冷却速度)function dragstarted(event) {if (!event.active) simulation.alphaTarget(0.3).restart();event.subject.fx = event.subject.x;event.subject.fy = event.subject.y;}// 更新被拖拽节点的固定位置(fx, fy)。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;}// 当图表需要销毁时(如重新渲染),停止力模拟以释放资源。invalidation.then(() => simulation.stop());return svg.node();
}
总结:
- 输入数据:
nodes
(节点列表)和links
(连接列表)。 - 力模拟:通过物理模型计算节点位置(斥力、连接力、中心力)。
- 渲染:使用 SVG 绘制节点(圆形)和连接(线条)。
- 交互:支持拖拽节点,悬停显示信息。
- 动态更新:每次模拟迭代后更新视图。
交互过程:拖动Drag、缩放Zoom、点击Click
官网:Force-directed graph / D3 | Observable
三、实现一个网络拓扑图
工作流程图:
使用流程:
1、从npm安装d3
npm install d3
2、将 d3 加载到应用中
import * as d3 from "d3";
3、创建一个SVG容器
-
选中 #chart 容器,创建 SVG 元素。
const svg = d3.select('#chart').append('svg');
-
设置svg宽高
-
建一个
<g>
分组元素作为所有图表内容(节点和连接线)的统一容器(并设置为拖放/平移),添加到svg -
在统一容器下面创建两个
<g>
组,分别用于渲染节点和边,添加到container
注:<g>
是 SVG 的一个核心元素,表示 分组(Group)。它的作用类似于 HTML 中的 <div>
,主要用于结构化组织多个图形元素,并可以对这些元素统一应用属性或变换
比如我可以创建一个组,设置这个组里面所有边都是一个颜色。
使用组的优点:统一属性设置、代码可读性高、减少重复操作、对组绑定事件
类比解释
概念 | 类比 | 说明 |
---|---|---|
zoom | 遥控器 | 接收用户输入(按钮/滚轮),发出控制信号 |
container | 电视屏幕 | 接收遥控器信号,实际改变显示内容 |
e.transform | 遥控信号 | 包含"音量调高"或"频道切换"等具体指令 |
子元素 | 电视画面内容 | 自动跟随屏幕的变化,无需直接处理信号 |
4、定义数据类型
网络拓扑图由节点、边、环组成,前端需从后端获取数据
let nodesData = []
let edgesData = []
let hoopsData = []
定义数据类型存储节点、边、环的数据。
- 节点:图中的基本实体
- 边:连接两个节点之间的线
- 环:图中首尾相连的路径
5、创建一个力导向布局
const simulation = d3.forceSimulation(nodes) // 初始化模拟,传入节点数组.force('link', // 添加"连接力"(使连接的节点保持特定距离)d3.forceLink() // 创建连接力.id((d) => d.node_id) // 指定如何从节点数据中获取唯一标识符).force('collision', // 添加"碰撞力"(防止节点重叠)d3.forceCollide().radius(50) // 设置碰撞检测半径(节点间距至少为50) ).velocityDecay(0.5) // 设置速度衰减系数(类似摩擦力,0-1)// 0.5表示每帧速度衰减50%,值越小移动越"滑",越大越"顿"
6、节点和边的渲染
节点:
-
用 显示节点图标(通过 getImg 获取)。
-
用
显示节点名称,超长自动截断。
node = node.data(nodes).join((enter) => {const g = enter.append('g').attr('class', 'node').attr('id', (d) => `node-${d.node_id}`).attr('transform', `translate(200, 200)`);g.append("image").attr("xlink:href", (d) => {return getImg(d.node_type).img}).attr("x", -(NODE_HEIGHT / 2)) // 图片宽度的一半,用于居中.attr("y", -(NODE_HEIGHT / 2)) // 图片高度的一半,用于居中.attr("width", NODE_HEIGHT) // 图片宽度.attr("height", NODE_HEIGHT); // 图片高度// 添加名称g.append('text').style('fill', '#000').attr('class', 'node-text').text(d => {return d.node_name.length > 15 ? d.node_name.slice(0, 15) + '...' : d.node_name}).style('text-anchor', 'middle').attr('dx', 0).style('font-size', NODE_FONT_SIZE).attr('y', NODE_WIDTH - 5);if (atlasKey.value == '1' && checked.value == 1) {g.append('text').style('fill', "#000").attr('class', 'node-text').text(d => {return d.ext_prop['alarm.ne_name']}).style('text-anchor', 'middle').attr('dx', 0).style('font-size', NODE_FONT_SIZE).attr('y', NODE_WIDTH + 20);}return g;},(exit) => exit.remove()).on('click', clickNode).call(drag(simulation));
边:用
link = link.data(links).join(enter => {const g = enter.append('g').attr('id', d => getLinkIds(d, 'line')).attr('class', d => 'top line')if (atlasKey.value == '1') {// 根因告警 虚线g.append('path').attr('id', d => getLinkIds(d)).style('stroke', d => getLinkColor(d)).attr('fill', 'none').attr('stroke-width', '2px').attr('stroke-dasharray', '10 5 5 10')} else {// 网络拓扑 实线g.append('path').attr('id', d => getLinkIds(d)).style('stroke', d => getLinkColor(d)).attr('fill', 'none').attr('stroke-width', '2px').attr('transform', d => `translate(${calculateMidPoint(d)})`)}// 边上的图标g.append('image').attr('class', d => {if (!(d?.ext_prop?.alarmIdList?.length > 0)) {return 'hide'}}).attr('xlink:href', d => d.isAlarmRoot ? icon09 : icon08).attr('width', NODE_WIDTH).attr('height', NODE_HEIGHT).attr('x', -10).attr('y', -10);return g}).on('click', clickLink)
7、设置交互逻辑
const chartEl = document.querySelector('#chart');
const svg = d3.select('#chart').append('svg');
const width = chartEl.offsetWidth;
const height = chartEl.clientHeight
const zoom = d3.zoom().on('zoom', handleZoom);
const container = svg.append('g').attr('class', 'container');function handleZoom(e) {container.attr('transform', e.transform);
}svg.attr('width', width).attr('height', height).call(zoom);
async function clickNode(e) {tooltipNode.visible = falseconst node = e.target.__data__;getAlarmList(true, node, nodesData); // 这里会弹出详细信息e.stopPropagation();
}async function clickLink(e) {tooltipNode.visible = falseconst node = e.target.__data__;getAlarmList(false, node, nodesData); // 这里会弹出详细信息e.stopPropagation();
}
const drag = () => {function dragstarted(event, d) {if (!event.active) simulation.alphaTarget(0.3).restart();d.x = d.x;d.y = d.y;}function dragged(event, d) {d.fx = event.x;d.fy = event.y;// 更新线条updateLinks();// 更新图像位置updateImages();}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);
};
function updateLinks() {link.attr('d', d => {const dx = d.target.x - d.source.x,dy = d.target.y - d.source.y,dr = Math.sqrt(dx * dx + dy * dy);return `M ${d.source.x},${d.source.y} A ${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;});
}function updateImages() {link.select('image').attr('x', d => {const midX = (d.source.x + d.target.x) / 2;return midX - 20 / 2; // 调整水平位置,使图片居中}).attr('y', d => {const midY = (d.source.y + d.target.y) / 2;return midY - 20 / 2; // 调整垂直位置,使图片居中});
}
node = node.data(nodes).join(// ...enter逻辑).on('click', clickNode).call(drag(simulation));link = link.data(links).join(// ...enter逻辑).on('click', clickLink)
四、参考力导向图组件
力导向图形组件 / D3 |观察 — Force-directed graph component / D3 | Observable
官方代码+中文注释
function ForceGraph({nodes, // 节点对象数组,通常格式如 [{id}, …]links // 连接线对象数组,通常格式如 [{source, target}, …]
}, {nodeId = d => d.id, // 从节点数据中获取唯一标识符的函数nodeGroup, // 从节点数据中获取分组信息的函数nodeGroups, // 节点分组的可选值数组nodeTitle, // 节点标题文本nodeFill = "currentColor", // 节点填充颜色nodeStroke = "#fff", // 节点边框颜色nodeStrokeWidth = 1.5, // 节点边框宽度(像素)nodeStrokeOpacity = 1, // 节点边框透明度nodeRadius = 5, // 节点半径(像素)nodeStrength, // 节点间作用力强度linkSource = ({source}) => source, // 从连接线数据中获取源节点linkTarget = ({target}) => target, // 从连接线数据中获取目标节点linkStroke = "#999", // 连接线颜色linkStrokeOpacity = 0.6, // 连接线透明度linkStrokeWidth = 1.5, // 连接线宽度(像素)linkStrokeLinecap = "round", // 连接线端点样式linkStrength, // 连接线作用力强度colors = d3.schemeTableau10, // 颜色方案,用于节点分组width = 640, // 画布宽度(像素)height = 400, // 画布高度(像素)invalidation // 当此Promise完成时停止模拟
} = {}) {// 数据处理const N = d3.map(nodes, nodeId).map(intern); // 节点ID数组const R = typeof nodeRadius !== "function" ? null : d3.map(nodes, nodeRadius); // 节点半径数组const LS = d3.map(links, linkSource).map(intern); // 连接线源节点数组const LT = d3.map(links, linkTarget).map(intern); // 连接线目标节点数组if (nodeTitle === undefined) nodeTitle = (_, i) => N[i]; // 默认使用节点ID作为标题const T = nodeTitle == null ? null : d3.map(nodes, nodeTitle); // 节点标题数组const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern); // 节点分组数组const W = typeof linkStrokeWidth !== "function" ? null : d3.map(links, linkStrokeWidth); // 连接线宽度数组const L = typeof linkStroke !== "function" ? null : d3.map(links, linkStroke); // 连接线颜色数组// 将输入数据转换为可修改对象供模拟使用nodes = d3.map(nodes, (_, i) => ({id: N[i]}));links = d3.map(links, (_, i) => ({source: LS[i], target: LT[i]}));// 计算默认分组if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);// 创建比例尺const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);// 创建作用力模型const forceNode = d3.forceManyBody(); // 节点间作用力const forceLink = d3.forceLink(links).id(({index: i}) => N[i]); // 连接线作用力if (nodeStrength !== undefined) forceNode.strength(nodeStrength); // 设置节点作用力强度if (linkStrength !== undefined) forceLink.strength(linkStrength); // 设置连接线作用力强度// 创建力导向模拟const simulation = d3.forceSimulation(nodes).force("link", forceLink) // 添加连接线作用力.force("charge", forceNode) // 添加节点间作用力.force("center", d3.forceCenter()) // 添加向中心的作用力.on("tick", ticked); // 设置每帧更新回调// 创建SVG画布const svg = d3.create("svg").attr("width", width) // 设置宽度.attr("height", height) // 设置高度.attr("viewBox", [-width / 2, -height / 2, width, height]) // 设置视图框.attr("style", "max-width: 100%; height: auto; height: intrinsic;"); // 响应式样式// 创建连接线组const link = svg.append("g").attr("stroke", typeof linkStroke !== "function" ? linkStroke : null) // 连接线颜色.attr("stroke-opacity", linkStrokeOpacity) // 连接线透明度.attr("stroke-width", typeof linkStrokeWidth !== "function" ? linkStrokeWidth : null) // 连接线宽度.attr("stroke-linecap", linkStrokeLinecap) // 连接线端点样式.selectAll("line").data(links).join("line");// 创建节点组const node = svg.append("g").attr("fill", nodeFill) // 节点填充色.attr("stroke", nodeStroke) // 节点边框色.attr("stroke-opacity", nodeStrokeOpacity) // 节点边框透明度.attr("stroke-width", nodeStrokeWidth) // 节点边框宽度.selectAll("circle").data(nodes).join("circle").attr("r", nodeRadius) // 节点半径.call(drag(simulation)); // 添加拖拽交互// 设置动态样式if (W) link.attr("stroke-width", ({index: i}) => W[i]); // 动态连接线宽度if (L) link.attr("stroke", ({index: i}) => L[i]); // 动态连接线颜色if (G) node.attr("fill", ({index: i}) => color(G[i])); // 按分组设置节点颜色if (R) node.attr("r", ({index: i}) => R[i]); // 动态节点半径if (T) node.append("title").text(({index: i}) => T[i]); // 添加节点提示文本if (invalidation != null) invalidation.then(() => simulation.stop()); // 设置模拟停止条件// 辅助函数:确保值为可比较的原始值function intern(value) {return value !== null && typeof value === "object" ? value.valueOf() : value;}// 模拟更新时的回调函数function ticked() {// 更新连接线位置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);}// 拖拽交互函数function drag(simulation) { // 拖拽开始function dragstarted(event) {if (!event.active) simulation.alphaTarget(0.3).restart(); // 激活模拟event.subject.fx = event.subject.x; // 固定节点x坐标event.subject.fy = event.subject.y; // 固定节点y坐标}// 拖拽过程中function dragged(event) {event.subject.fx = event.x; // 更新固定x坐标event.subject.fy = event.y; // 更新固定y坐标}// 拖拽结束function dragended(event) {if (!event.active) simulation.alphaTarget(0); // 停止模拟event.subject.fx = null; // 释放x坐标event.subject.fy = null; // 释放y坐标}return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);}// 返回SVG元素和比例尺return Object.assign(svg.node(), {scales: {color}});
}
代码结构分析
1. 初始化与数据处理
const N = d3.map(nodes, nodeId).map(intern); // 节点ID
const R = typeof nodeRadius !== "function" ? null : d3.map(nodes, nodeRadius); // 节点半径
const LS = d3.map(links, linkSource).map(intern); // 连接源节点
const LT = d3.map(links, linkTarget).map(intern); // 连接目标节点
2. 力模拟设置
const simulation = d3.forceSimulation(nodes).force("link", forceLink) // 连接力.force("charge", forceNode) // 节点间斥力.force("center", d3.forceCenter()) // 向心力.on("tick", ticked); // 每帧更新
- 创建力模拟系统,包含三种力:
forceLink
: 保持连接长度的力forceNode
: 节点间斥力(避免重叠)forceCenter
: 将图形居中
3. SVG元素创建
const svg = d3.create("svg")...; // 创建SVG画布const link = svg.append("g")...; // 创建连接线组
const node = svg.append("g")...; // 创建节点组
- 创建SVG容器和分组
- 绑定数据到DOM元素
4. 交互功能
function drag(simulation) {// 拖拽事件处理function dragstarted(event) {...}function dragged(event) {...}function dragended(event) {...}return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);
}
- 实现节点拖拽功能
- 拖拽时临时固定节点位置
- 释放后恢复物理模拟
5. 更新函数
function ticked() {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);
}
- 每次模拟"tick"时更新节点和连接线位置
- 将模拟中的坐标同步到SVG元素
项目代码
import * as d3 from 'd3';
import {getLinkIds, linkArc, registerDefs, getLinkColor, getImg} from '@/pages/chart_model/chart/summary/utils';
import {NODE_WIDTH,NODE_HEIGHT,NODE_FONT_SIZE,} from '@/pages/chart_model/chart/summary/config';
import {tooltipNode, setToolTipNode} from '@/pages/chart_model/store/tool-node';
import {getSceneKey} from "@/pages/chart_model/chart/sceneCommon";import icon08 from '@/assets/chart_model/node2/icon08.png'
import icon09 from '@/assets/chart_model/node2/icon09da.png'import {setCoordinate,
} from "@/pages/chart_model/js/index_default2";
import {adjustCoordinates,calculateMidPoint,filterDataHandle, getAlarmList,setLinkisRoot
} from "@/pages/chart_model/js/index_default";
window.d3 = d3let graph = null;
let nodesData = []
let edgesData = []
let hoopsData = []export default (state) => {let nodes = [];let links = [];const chartEl = document.querySelector('#chart');const svg = d3.select('#chart').append('svg');const width = chartEl.offsetWidth;const height = chartEl.clientHeightconst zoom = d3.zoom().on('zoom', handleZoom);const container = svg.append('g').attr('class', 'container');function handleZoom(e) {container.attr('transform', e.transform);}svg.attr('width', width).attr('height', height).call(zoom);async function clickNode(e) {tooltipNode.visible = falseconst node = e.target.__data__;getAlarmList(true, node, nodesData);e.stopPropagation();}async function clickLink(e) {tooltipNode.visible = falseconst node = e.target.__data__;getAlarmList(false, node,nodesData);e.stopPropagation();}const drag = () => {function dragstarted(event, d) {if (!event.active) simulation.alphaTarget(0.3).restart();d.x = d.x;d.y = d.y;}function dragged(event, d) {d.fx = event.x;d.fy = event.y;// 更新线条updateLinks();// 更新图像位置updateImages();}function dragended(event, d) {if (!event.active) simulation.alphaTarget(0);}return d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended);};let link = container.append('g').attr('class', 'lines').selectAll('g');let node = container.append('g').attr('class', 'nodes').selectAll('g');const simulation = d3.forceSimulation(nodes).force('link', d3.forceLink().id((d) => d.node_id)).force('collision', d3.forceCollide().radius(50)).velocityDecay(0.5);function ticked(d) {link.selectAll('path').attr('d', linkArc);node.attr('transform', (d) => `translate(${d.x},${d.y})`);}// 更新线条的方法function updateLinks() {link.attr('d', d => {const dx = d.target.x - d.source.x,dy = d.target.y - d.source.y,dr = Math.sqrt(dx * dx + dy * dy);return `M ${d.source.x},${d.source.y} A ${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;});}// 更新图像位置的方法function updateImages() {link.select('image').attr('x', d => {const midX = (d.source.x + d.target.x) / 2;return midX - 20 / 2; // 调整水平位置,使图片居中}).attr('y', d => {const midY = (d.source.y + d.target.y) / 2;return midY - 20 / 2; // 调整垂直位置,使图片居中});}graph = {name: 'summary',nodes,links,graph,appendData(data, alarmOrReason, atlasKey, value1, nodeIdInhoop) {simulation.stop();let filterDataHandle4 = filterDataHandle(data, alarmOrReason,atlasKey, value1, nodeIdInhoop, state);setCoordinate(filterDataHandle4.nodes, filterDataHandle4.links, atlasKey, alarmOrReason, state, state.sceneKey)nodes = filterDataHandle4.nodes;links = filterDataHandle4.links;adjustCoordinates(nodes)setLinkisRoot(links, nodesData)this.restart(atlasKey, alarmOrReason);},restart(atlasKey = 1, checked) {node = node.data(nodes).join((enter) => {const g = enter.append('g').attr('class', 'node').attr('id', (d) => `node-${d.node_id}`).attr('transform', `translate(200, 200)`);g.append("image").attr("xlink:href", (d) => {return getImg(d.node_type).img}).attr("x", -(NODE_HEIGHT / 2)) // 图片宽度的一半,用于居中.attr("y", -(NODE_HEIGHT / 2)) // 图片高度的一半,用于居中.attr("width", NODE_HEIGHT) // 图片宽度.attr("height", NODE_HEIGHT); // 图片高度// 添加名称g.append('text')// .style('fill', '#fff').style('fill', '#000').attr('class', 'node-text').text(d => {return d.node_name.length > 15 ? d.node_name.slice(0, 15) + '...' : d.node_name}).style('text-anchor', 'middle').attr('dx', 0).style('font-size', NODE_FONT_SIZE).attr('y', NODE_WIDTH - 5);if (atlasKey.value == '1' && checked.value == 1) {g.append('text')// .style('fill', '#fff').style('fill', "#000").attr('class', 'node-text').text(d => {return d.ext_prop['alarm.ne_name']}).style('text-anchor', 'middle').attr('dx', 0).style('font-size', NODE_FONT_SIZE).attr('y', NODE_WIDTH + 20);}return g;},(exit) => exit.remove()).on('click', clickNode).call(drag(simulation));link = link.data(links).join(enter => {const g = enter.append('g').attr('id', d => getLinkIds(d, 'line')).attr('class', d => 'top line')if (atlasKey.value == '1') {// 根因告警 虚线g.append('path').attr('id', d => getLinkIds(d)).style('stroke', d => getLinkColor(d)).attr('fill', 'none').attr('stroke-width', '2px').attr('stroke-dasharray', '10 5 5 10')} else {// 网络拓扑 实现 后续带颜色g.append('path').attr('id', d => getLinkIds(d)).style('stroke', d => getLinkColor(d)).attr('fill', 'none').attr('stroke-width', '2px').attr('transform', d => `translate(${calculateMidPoint(d)})`) // 计算中点位置}g.append('image').attr('class', d => {if (!(d?.ext_prop?.alarmIdList?.length > 0)) {return 'hide'}}).attr('xlink:href', d => d.isAlarmRoot ? icon09 : icon08).attr('width', NODE_WIDTH) // 图片宽度.attr('height', NODE_HEIGHT) // 图片高度.attr('x', -10) // 调整水平位置,使图片居中.attr('y', -10); // 调整垂直位置,使图片居中return g}).on('click', clickLink)simulation.nodes(nodes);simulation.force('link').links(links);simulation.alpha(1).restart();simulation.on('tick', ticked)graph.resetGraphLink()},resetGraph() {simulation.stop()},setData(nodes, links, hoops) {nodesData = nodesedgesData = linkshoopsData = hoops},resetGraphLink() {updateLinks()updateImages()},resetSvg(width,height) {svg.attr("width", width).attr("height", height);}};return graph;
};