本文作者:BP-Captain
D3js是什么?
- 是一个能够制作精美、复杂图表的数据可视化库。
- 是一个由数据驱动的数据可视化库,要把数据绑定到DOM上,然后才能展示。
- 是一个基于Html、CSS、svg/canvas的js数据可视化库。
本文代码实现效果:
- 连线有箭头
- 点击节点能改变该节点颜色及其连接线的粗细
- 能够缩放
- 能够拖拽。
版本: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.构造节点和连线
在【完整代码】中已经构造了节点和连线,但那是基于后台传来的数据,可能不够直观。现在我给出两组数据,然后构造节点和连线的数据。
const nodeData = [
{ id: 1, 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: '浙江' },
{ id: 10, name: '江苏' },
{ id: 11, name: '河北' },
{ id: 12, name: '山西' },
{ id: 13, name: '吉林' },
{ id: 14, name: '辽宁' },
{ id: 15, name: '黑龙江' },
{ id: 16, name: '安徽' },
{ id: 17, name: '江西' },
{ id: 18, name: '山东' },
{ id: 19, name: '河南' },
{ id: 20, name: '湖南' },
{ id: 21, name: '湖北' },
{ id: 22, name: '海南' },
{ id: 23, name: '贵州' },
{ id: 24, name: '云南' },
{ id: 25, name: '新疆' },
{ id: 26, name: '西藏' },
{ id: 27, name: '台湾' },
{ id: 28, name: '澳门' },
{ id: 29, name: '香港' },
{ id: 30, name: '陕西' },
{ id: 31, name: '甘肃' },
{ id: 32, name: '青海' },
{ id: 33, name: '内蒙古' },
{ id: 34, name: '宁夏' },
{ id: 35, name: '四川' },
{ id: 36, name: '福州' },
{ id: 37, name: '厦门' },
{ id: 38, name: '漳州' },
{ id: 39, name: '莆田' },
{ id: 40, name: '南平' },
{ id: 41, name: '龙岩' },
{ id: 42, name: '三明' },
{ id: 43, name: '宁德' },
{ id: 44, name: '泉州' },
];
let nodes = [];
for (let i = 0; i < nodeData.length; i++) {
nodes.push({
id: (nodeData[i] && nodeData[i].id) || '', // 节点id
name: (nodeData[i] && nodeData[i].name) || '', // 节点名称
});
}
const relData = [
{ id: 1, source: 1, target: 2, tag: '省份' },
{ id: 2, source: 1, target: 3, tag: '省份' },
{ id: 3, source: 1, target: 4, tag: '省份' },
{ id: 4, source: 1, target: 5, tag: '省份' },
{ id: 5, source: 1, target: 6, tag: '省份' },
{ id: 6, source: 6, target: 36, tag: '地级市' },
{ id: 7, source: 6, target: 37, tag: '地级市' },
{ id: 8, source: 6, target: 38, tag: '地级市' },
{ id: 9, source: 6, target: 39, tag: '地级市' },
{ id: 10, source: 6, target: 40, tag: '地级市' },
{ id: 11, source: 6, target: 41, tag: '地级市' },
{ id: 12, source: 6, target: 42, tag: '地级市' },
{ id: 13, source: 6, target: 43, tag: '地级市' },
{ id: 14, source: 6, target: 44, tag: '地级市' },
{ id: 15, source: 1, target: 7, tag: '省份' },
{ id: 16, source: 1, target: 8, tag: '省份' },
{ id: 17, source: 1, target: 9, tag: '省份' },
{ id: 18, source: 1, target: 44, tag: '省份' },
{ id: 19, source: 1, target: 10, tag: '省份' },
{ id: 20, source: 1, target: 11, tag: '省份' },
{ id: 21, source: 1, target: 12, tag: '省份' },
{ id: 22, source: 1, target: 13, tag: '省份' },
{ id: 23, source: 1, target: 14, tag: '省份' },
{ id: 24, source: 1, target: 15, tag: '省份' },
{ id: 25, source: 1, target: 16, tag: '省份' },
{ id: 26, source: 1, target: 17, tag: '省份' },
{ id: 27, source: 1, target: 18, tag: '省份' },
{ id: 28, source: 1, target: 19, tag: '省份' },
{ id: 29, source: 1, target: 20, tag: '省份' },
{ id: 23, source: 1, target: 21, tag: '省份' },
{ id: 31 source: 1, target: 22, tag: '省份' },
{ id: 32, source: 1, target: 23, tag: '省份' },
{ id: 33, source: 1, target: 24, tag: '省份' },
{ id: 34, source: 1, target: 25, tag: '省份' },
{ id: 35, source: 1, target: 26, tag: '省份' },
{ id: 36, source: 1, target: 27, tag: '省份' },
{ id: 37, source: 1, target: 28, tag: '省份' },
{ id: 38, source: 1, target: 29, tag: '省份' },
{ id: 39, source: 1, target: 30, tag: '省份' },
{ id: 40, source: 1, target: 31, tag: '省份' },
{ id: 41, source: 1, target: 32, tag: '省份' },
{ id: 42, source: 1, target: 33, tag: '省份' },
{ id: 43, source: 1, target: 34, tag: '省份' },
];
let edges = [];
for (let i = 0; i < relData.length; i++) {
edges.push({
id: (relData[i] && (relData[i].id)) || '', // 连线id
source: relData[i].source, // 开始节点
target: relData[i].target, // 结束节点
tag: (relData[i].tag) || '', // 连线名称
});
}
依据自己的项目,力导向图需要什么就给它什么。
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()设置力,可以设置这几种力:
-
Center:重力点,设置力导向图的力重心位置。设置之后无论怎么拖拽,力的重心都不会变;不设置的话力重心会改变,但力重心的初始位置会在原点,这意味着刚进入页面你只能看到1/4的图,很影响体验。
-
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:创建元素
-
style:设置样式
-
on('click', function()):click设置点击响应事件
-
call(zoom):缩放函数,详细请查阅下文【缩放】部分
5.绘制连线
const edgesLine = svg.select('g')
.selectAll('line')
.data(edges) // 绑定数据
.enter() // 为数据添加对应数量的占位符
.append('path') // 在占位符上面生成折线(用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); // 粗细
-
data(),enter(),append():这仨是一起的,绑定数据到创建图形
-
attr:设置属性
-
style:设置样式
-
连线用贝塞尔曲线绘制:(M 起点X 起点y L 终点x 终点y)
如果你想详细了解贝塞尔曲线,请移步:点我了解贝塞尔曲线更多知识.
6.连线名称
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; }); // 设置文字内容
- attr()放在.append()后面,表示为.append()创建的元素设置属性
- .style('pointer-events', 'none')禁止鼠标事件:无法选中,无法点击,鼠标在上面不会变成竖杆。
7.绘制连线上的箭头
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
对viewport和viewBox的关系还是不懂,请移步:理解SVG的viewport,viewBox,preserveAspectRatio.
8.绘制节点
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()调用拖拽函数。
9.节点名称
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
});
这里禁止鼠标事件的意义有两点:
-
鼠标移到文字上不会变成竖杆,还是保持箭头的样子,这样体验好。
-
我们给节点设置了点击事件,如果没有禁止鼠标事件,那么文字是占空间的,而且在节点上层,我们点击文字时就无法响应节点事件了。
10.鼠标移到节点上有气泡提示
nodesCircle.append('title')
.text((node) => { // .text设置气泡提示内容
return node.name; // 气泡提示为node的名称
});
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中,prevState是前一个state的值。有了state和prevState,就能进行更多判断,创造更多变化了。
四、在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实例,您可以狠狠点击这里:D3js项目检索.
各位大佬多指教!!!
转载请按格式注明出处:
本文作者:BP-Captain