详解如何在react中搭建d3力导向图
d3js力导向图搭建
d3js是一个可以基于数据来操作文档的javascript库。可以使用html,css,svg以及canvas来展示数据。力导向图能够用来表示节点间多对多的关系。
实现效果:连线有箭头,点击节点能改变该节点颜色和所连接的线的粗细,缩放、拖拽。
版本:4.x
安装和导入
npm安装:npm install d3
前端导入:import * as d3 from 'd3';
一、完整代码
import react, { component } from 'react'; import proptypes from 'prop-types'; import { connect } from 'react-redux'; import { push } from 'react-router-redux'; import * as d3 from 'd3'; import { row, form } from 'antd'; import { chartreq} from './actioncreator'; import './chart.less'; const width = 1900; const height = 580; const r = 30; let simulation; class chart extends component { constructor(props, context) { super(props, context); this.print = this.print.bind(this); this.forcechart = this.forcechart.bind(this); this.state = { }; } componentwillmount() { this.props.dispatch(push('/chart')); } componentdidmount() { this.print(); } print() { let callback = (res) => { // callback获取后台返回的数据,并存入state let nodedata = res.data.nodes; let relationdata = res.data.rels; this.setstate({ nodedata: res.data.nodes, relationdata: res.data.rels, }); let nodes = []; for (let i = 0; i < nodedata.length; i++) { nodes.push({ id: (nodedata[i] && nodedata[i].id) || '', name: (nodedata[i] && nodedata[i].name) || '', type: (nodedata[i] && nodedata[i].type) || '', definition: (nodedata[i] && nodedata[i].definition) || '', }); } let edges = []; for (let i = 0; i < relationdata.length; i++) { edges.push({ id: (relationdata[i] && (relationdata[i].id)) || '', source: (relationdata[i] && relationdata[i].start.id) || '', target: (relationdata[i] && relationdata[i].end.id) || '', tag: (relationdata[i] && relationdata[i].name) || '', }); } this.forcechart(nodes, edges); // d3力导向图内容 }; this.props.dispatch(chartreq({ param: param }, callback)); } // func forcechart(nodes, edges) { this.refs['thechart'].innerhtml = ''; // 函数内其余代码请看拆解代码 } render() { return ( <row style={{ minwidth: 900 }}> <div classname="outerdiv"> <div classname="thechart" id="thechart" ref="thechart"> </div> </div> </row> ); } } chart.proptypes = { dispatch: proptypes.func.isrequired, }; function mapstatetoprops(state) { return { }; } const wrappedchart = form.create({})(chart); export default connect(mapstatetoprops)(wrappedchart);
二、拆解代码
1.组件
<div classname="thechart" id="thechart" ref="thechart"> </div>
整个图都将在div里绘制。
2.构造节点和连线
let nodes = []; // 节点 for (let i = 0; i < nodedata.length; i++) { nodes.push({ id: (nodedata[i] && nodedata[i].id) || '', name: (nodedata[i] && nodedata[i].name) || '', // 节点名称 }); } let edges = []; // 连线 for (let i = 0; i < relationdata.length; i++) { edges.push({ id: (relationdata[i] && (relationdata[i].id)) || '', source: (relationdata[i] && relationdata[i].start.id) || '', // 开始节点 target: (relationdata[i] && relationdata[i].end.id) || '', // 结束节点 tag: (relationdata[i] && relationdata[i].name) || '', // 连线名称 }); }
具体怎么构造依据你们的项目数据。
3.定义力模型
const simulation = d3.forcesimulation(nodes) // 指定被引用的nodes数组 .force('link', d3.forcelink(edges).id(d => d.id).distance(150)) .force('collision', d3.forcecollide(1).strength(0.1)) .force('center', d3.forcecenter(width / 2, height / 2)) .force('charge', d3.forcemanybody().strength(-1000).distancemax(800));
通过simulation.force()设置力,可以设置这几种力:
- centering:中心力,设置图中心点位置。
- collision:节点碰撞作用力,.strength参数范围为[0,1]。
- links:连线的作用力;.distance设置连线两端节点的距离。
- many-body:.strength的参数为正时,模拟重力,为负时,模拟电荷力;.distancemax的参数设置最大距离。
positioning:给定向某个方向的力。
通过simulation.on监听力图元素位置变化。
4.绘制svg
const svg = d3.select('#thechart').append('svg') // 在id为‘thechart'的标签内创建svg .style('width', width) .style('height', height * 0.9) .on('click', () => { console.log('click', d3.event.target.tagname); }) .call(zoom); // 缩放 const g = svg.append('g'); // 则svg中创建g
创建svg,在svg里创建g,将节点连线等内容放在g内。
- select:选择第一个对应的元素
- selectall:选择所有对应的元素
- append:创建元素
5.绘制连线
const edgesline = svg.select('g') .selectall('line') .data(edges) // 绑定数据 .enter() // 添加数据到选择集edgepath .append('path') // 生成折线 .attr('d', (d) => { return d && 'm ' + d.source.x + ' ' + d.source.y + ' l ' + d.target.x + ' ' + d.target.y; }) // 遍历所有数据,d表示当前遍历到的数据,返回绘制的贝塞尔曲线 .attr('id', (d, i) => { return i && 'edgepath' + i; }) // 设置id,用于连线文字 .attr('marker-end', 'url(#arrow)') // 根据箭头标记的id号标记箭头 .style('stroke', '#000') // 颜色 .style('stroke-width', 1); // 粗细
连线用贝塞尔曲线绘制:(m 起点x 起点y l 终点x 终点y)
6.绘制连线上的箭头
const defs = g.append('defs'); // defs定义可重复使用的元素 const arrowheads = defs.append('marker') // 创建箭头 .attr('id', 'arrow') // .attr('markerunits', 'strokewidth') // 设置为strokewidth箭头会随着线的粗细进行缩放 .attr('markerunits', 'userspaceonuse') // 设置为userspaceonuse箭头不受连接元素的影响 .attr('class', 'arrowhead') .attr('markerwidth', 20) // viewport .attr('markerheight', 20) // viewport .attr('viewbox', '0 0 20 20') // viewbox .attr('refx', 9.3 + r) // 偏离圆心距离 .attr('refy', 5) // 偏离圆心距离 .attr('orient', 'auto'); // 绘制方向,可设定为:auto(自动确认方向)和 角度值 arrowheads.append('path') .attr('d', 'm0,0 l0,10 l10,5 z') // d: 路径描述,贝塞尔曲线 .attr('fill', '#000'); // 填充颜色
- viewport:可视区域
- viewbox:实际大小,会自动缩放填充viewport
7.绘制节点
const nodescircle = svg.select('g') .selectall('circle') .data(nodes) .enter() .append('circle') // 创建圆 .attr('r', 30) // 半径 .style('fill', '#9ff') // 填充颜色 .style('stroke', '#0cf') // 边框颜色 .style('stroke-width', 2) // 边框粗细 .on('click', (node) => { // 点击事件 console.log('click'); }) .call(drag); // 拖拽单个节点带动整个图
创建圆作为节点。
.call()调用拖拽函数。
8.节点名称
const nodestexts = svg.select('g') .selectall('text') .data(nodes) .enter() .append('text') .attr('dy', '.3em') // 偏移量 .attr('text-anchor', 'middle') // 节点名称放在圆圈中间位置 .style('fill', 'black') // 颜色 .style('pointer-events', 'none') // 禁止鼠标事件 .text((d) => { // 文字内容 return d && d.name; // 遍历nodes每一项,获取对应的name });
因为文字在节点上层,如果没有设置禁止鼠标事件,点击文字将无法响应点击节点的效果,也无法拖拽节点。
9.连线名称
const edgestext = svg.select('g').selectall('.edgelabel') .data(edges) .enter() .append('text') // 为每一条连线创建文字区域 .attr('class', 'edgelabel') .attr('dx', 80) .attr('dy', 0); edgestext.append('textpath')// 设置文字内容 .attr('xlink:href', (d, i) => { return i && '#edgepath' + i; }) // 文字布置在对应id的连线上 .style('pointer-events', 'none') .text((d) => { return d && d.tag; });
10.鼠标移到节点上有气泡提示
nodescircle.append('title') .text((node) => { // .text设置气泡提示内容 return node.definition; });
11.监听图元素的位置变化
simulation.on('tick', () => { // 更新节点坐标 nodescircle.attr('transform', (d) => { return d && 'translate(' + d.x + ',' + d.y + ')'; }); // 更新节点文字坐标 nodestexts.attr('transform', (d) => { return 'translate(' + (d.x) + ',' + d.y + ')'; }); // 更新连线位置 edgesline.attr('d', (d) => { const path = 'm ' + d.source.x + ' ' + d.source.y + ' l ' + d.target.x + ' ' + d.target.y; return path; }); // 更新连线文字位置 edgestext.attr('transform', (d, i) => { return 'rotate(0)'; }); });
12.拖拽
function ondragstart(d) { // console.log('start'); // console.log(d3.event.active); if (!d3.event.active) { simulation.alphatarget(1) // 设置衰减系数,对节点位置移动过程的模拟,数值越高移动越快,数值范围[0,1] .restart(); // 拖拽节点后,重新启动模拟 } d.fx = d.x; // d.x是当前位置,d.fx是静止时位置 d.fy = d.y; } function dragging(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function ondragend(d) { if (!d3.event.active) simulation.alphatarget(0); d.fx = null; // 解除dragged中固定的坐标 d.fy = null; } const drag = d3.drag() .on('start', ondragstart) .on('drag', dragging) // 拖拽过程 .on('end', ondragend);
13.缩放
function onzoomstart(d) { // console.log('start zoom'); } function zooming(d) { // 缩放和拖拽整个g // console.log('zoom ing', d3.event.transform, d3.zoomtransform(this)); g.attr('transform', d3.event.transform); // 获取g的缩放系数和平移的坐标值。 } function onzoomend() { // console.log('zoom end'); } const zoom = d3.zoom() // .translateextent([[0, 0], [width, height]]) // 设置或获取平移区间, 默认为[[-∞, -∞], [+∞, +∞]] .scaleextent([1 / 10, 10]) // 设置最大缩放比例 .on('start', onzoomstart) .on('zoom', zooming) .on('end', onzoomend);
三、其它效果
1.单击节点时让连接线加粗
nodescircle.on('click, (node) => { edges_line.style("stroke-width",function(line){ if(line.source.name==node.name || line.target.name==node.name){ return 4; }else{ return 0.5; } }); })
2.被点击的节点变色
nodescircle.on('click, (node) => { nodescircle.style('fill', (nodeofselected) => { // nodeofselected:所有节点, node: 选中的节点 if (nodeofselected.id === node.id) { // 被点击的节点变色 console.log('node') return '#36f'; } else { return '#9ff'; } }); })
四、在react中使用注意事项
componentdidmount() { this.print(); } print() { let callback = (res) => { // callback获取后台返回的数据,并存入state let nodedata = res.data.nodes; let relationdata = res.data.rels; this.setstate({ nodedata: res.data.nodes, relationdata: res.data.rels, }); let nodes = []; for (let i = 0; i < nodedata.length; i++) { nodes.push({ id: (nodedata[i] && nodedata[i].id) || '', name: (nodedata[i] && nodedata[i].name) || '', type: (nodedata[i] && nodedata[i].type) || '', definition: (nodedata[i] && nodedata[i].definition) || '', }); } let edges = []; for (let i = 0; i < relationdata.length; i++) { edges.push({ id: (relationdata[i] && (relationdata[i].id)) || '', source: (relationdata[i] && relationdata[i].start.id) || '', target: (relationdata[i] && relationdata[i].end.id) || '', tag: (relationdata[i] && relationdata[i].name) || '', }); } this.forcechart(nodes, edges); // d3力导向图内容 }; this.props.dispatch(getdatafromneo4j({ neo4jrun: 'match p=(()-[r]-()) return p limit 300', }, callback)); }
在哪里构造图 因为图是动态的,如果渲染多次(render执行多次,渲染多次),不会覆盖前面渲染的图,反而会造成渲染多次,出现多个图的现象。把构造图的函数print()放到componentdidmount()内执行,则只会渲染一次。
对节点和连线数据进行增删改操作后,需要再次调用print()函数,重新构造图。
从哪里获取数据 数据不从redux获取,发送请求后callback直接获取。
五、干货:d3项目查找网址
d3js所有项目检索.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。