基于ts+react+d3实现数据可视化关系力导图
由于第一次做相关功能,并且是在ts中使用,也是遇到了许多坑,下面记录一下实现的过程
目的很明确直接找网站的例子,锁定:
到这里,像我这种喜欢直来直去的,不得第一时间立马搬到项目里面,殊不知接下来几天的折磨也就由此而来,我先贴一下官网这个例子的原代码(网站可以直接看到的):
chart = {
const links = data.links.map(d => Object.create(d));
const nodes = data.nodes.map(d => Object.create(d));
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
const link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", d => Math.sqrt(d.value));
const node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 5)
.attr("fill", color)
.call(drag(simulation));
node.append("title")
.text(d => d.id);
simulation.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);
});
invalidation.then(() => simulation.stop());
return svg.node();
}
// 这里的data就是渲染的数据,类似{nodes:[id: "Myriel", group: 1}], links[{source: "Napoleon", target: "Myriel", value: 1}]}这样的数据
data = FileAttachment("miserables.json").json()
height = 600
color = {
const scale = d3.scaleOrdinal(d3.schemeCategory10);
return d => scale(d.group);
}
drag = simulation => {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
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;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
d3 = require("d3@6")
上面就是网站上这个例子给的代码
这一块过程我没有记录下来,所以只能文字描述一下我碰到的问题,
\
npm/yarn....
等等都行
import * as d3 from "d3" ;
这引入写法不论是官方文档还是其他文章资料都是这样用得
此时由于这里引入d3.js库,ts项目会检查有没有类型声明文件,然后鼠标移到红线上会显示一堆英文。大致意思就是找不到这个库的类型声明文件(.d.ts)
结尾的,然后如果这个库有的话你可以使用安装@types/d3
这样的方式安装,或者自己写一个。。。。
那我肯定直接安装一个
d3.XXXX
这样使用的,有一些还是会报错,大致意思就是node_modules里面的@types/d3里的 .d.ts 文件没有导出你的引用(找不见)
,然后我找到好多方式都没找到解决办法,就想到了直接下载d3.js库的代码,删除里面其他文件,只保留d3.js2文件,直接本地离线引入d3.event......
这种在新版本的代码里面并没有导出,所以在实现实现某些功能的时候如果是v3版本的,这里也会报错,然后我又用的很笨的办法,直接上github上找到d3V3的版本,把里面的d3.js的代码拿到本地新建了一个js放了进去(大佬别骂我,我就是奔着解决问题的)造孽啊
)上面这一大堆废话就是自己在ts中引用d3js碰到的兼容和版本问题,虽然使用笨的办法,但好在问题解决了,有其他好的方法希望各位不吝赐教
不得不说一句,d3库的语法以及方式和jQuery确实很像,都是通过标签、id、class等直接选中dom然后做各种操作
但是这次的力导图本质上整体是一个svg,所以我中途也是去熟悉了下svg的属性和语法,不然我完全搞不懂我选这个,设置那个属性到底是在画猫还是画狗
直接上代码,注释也直接写在代码里面了
import { useEffect } from 'react';
// ts项目里面通过插件的方式引入d3时,会默认寻找插件的类型申明文件@types/d3,但是好多写法用的在声明文件里面并没有导出,会导致好多报错
// 网上直接下载d3库的代码,通过离线文件的方式引入使用,不过版本是v6+,学习d3库百度好多功能的时候能找到的基本上都是v3版本和js方式的写法
// 所以这里第一次使用v6+版本和ts也是踩了好多坑
// 最后实在没有好的解决办法只能去d3的github上找见v3版本的d3.js文件copy一份写在本地引用
// 不过到最后大部分功能实现的时候,某些事件、参数的取值和网上的又不太一样,到最后也是摸索避免使用d3版本的这种写法
import {
forceSimulation,
forceLink,
forceManyBody,
select,
drag,
forceCollide,
zoom,
selectAll,
} from './d3/d3.js';
// 定义四种节点颜色,线条渐变也会使用
const a = `#38CCB5`;
const b = `#FFA02D`;
const c = `#A096EA`;
const d = `#FF8988`;
export default (props: any) => {
const width = 1600;
const height = 800;
useEffect(() => {
if (props?.data?.edges?.length) {
// 调用渲染函数,渲染函数之前闲清除dom,用于条件查询之后重新渲染
select('.mySvg').remove();
chart();
}
}, [props.data]);
// 节点拖拽的方法
const drag1 = (simulation: any) => {
function dragstarted(event: any) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event: any) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event: any) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return drag().on('start', dragstarted).on('drag', dragged).on('end', dragended);
};
// 初始化
const chart = () => {
// 初始化数据
const links = props.data.edges.map((d: any) => ({
...d,
type: d.source.type,
source: d.source.label,
target: d.target.label,
}));
const nodes = props.data.nodes.map((d: any) => Object.create(d));
const simulation = forceSimulation(nodes)
// @ts-ignore
.force(
'link',
forceLink(links)
.id((d: any) => d.label)
// @ts-ignore
.distance(250),
) // 线的长度
.force('charge', forceManyBody().strength(-200))
.force('collide', forceCollide().radius(40).iterations(2)) // 节点碰撞力,不重叠
.on('tick', ticked); // 拖拽事件,更新坐标
// 获取svg
const svg = select('#myMap')
.append('svg')
.attr('class', 'mySvg')
.attr('width', 1600)
.attr('height', 650)
.attr('viewBox', `-300 ${-height / 2} ${width} ${height}`);
const defs = svg.append('defs'); // 渐变色linearGradient必须放在defs内
const g = svg.append('g');
// 缩放及平移事件
svg.call(
zoom().on('zoom', function (d: any) {
// 防止拖拽抖动和跳屏事件,将属性不要直接绑定到svg上面,所以在svg下面创建一个元素绑定
g.attr('transform', d.transform);
}),
);
// 节点线条颜色
function lineColor(y: any, i: any) {
// let color;
// 处理渐变色
const linerGradient = defs
.append('linearGradient')
.attr('id', 'linearColor')
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '0%');
linerGradient.append('stop').attr('offset', '0%').style('stop-color', a);
linerGradient.append('stop').attr('offset', '100%').style('stop-color', b);
const linerGradient1 = defs
.append('linearGradient')
.attr('id', 'linearColor1')
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '0%');
linerGradient1.append('stop').attr('offset', '0%').style('stop-color', a);
linerGradient1.append('stop').attr('offset', '100%').style('stop-color', c);
const linerGradient2 = defs
.append('linearGradient')
.attr('id', 'linearColor2')
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '0%');
linerGradient2.append('stop').attr('offset', '0%').style('stop-color', a);
linerGradient2.append('stop').attr('offset', '100%').style('stop-color', d);
// 这里是由于节点含义不同颜色也不同,所以根据类型渲染两个节点之间颜色渐变
if (props.data.edges[i].source.type === 'pair') {
return 'url(#' + linerGradient2.attr('id') + ')';
} else if (props.data.edges[i].source.type === 'tag') {
return 'url(#' + linerGradient.attr('id') + ')';
} else {
return 'url(#' + linerGradient1.attr('id') + ')';
}
}
// 画线
const link = g
.append('g')
.selectAll('path')
.data(links) // d3独有语法,用于给dom绑定数据
.enter()
.call((selection: any) => {
// 为连线绑定文字描述
selection
.append('svg:text')
.attr('text-anchor', 'middle')
.style('font-size', '14px')
.style('fill', '#B4B9C7')
.attr('y', 25)
.append('svg:textPath')
.attr('startOffset', '50%')
.attr('xlink:href', (d: any, i: any) => `#edgepath${i}`) // 需要和线的id绑定起来
.text((e: any) => {
if (e.properties) {
return `${e.properties.frequency} 次`;
}
return '0 次';
});
})
.append('path')
.attr('stroke', lineColor) // 颜色渐变
.style('opacity', '0.6')
.attr('fill', 'none')
.attr('stroke-width', 1)
.attr('id', function (d: any, i: any) {
return 'edgepath' + i;
});
// 画圆形节点
const node = g
.append('g')
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('r', 11) // 半径
.style('fill', function (d: any) {
let color; //圆圈背景色
if (d.type === 'table') {
color = '#38CCB5';
} else if (d.type === 'tag') {
color = '#FFA02D';
} else if (d.type === 'subjectDomain') {
color = '#A096EA';
} else if (d.type === 'businessDomain') {
color = '#A096EA';
} else {
color = '#FF8988';
}
return color;
})
.call(drag1(simulation)) // 绑定事件支持拖动更新坐标
.on('click', function (d: any, i: any) {
// 节点点击切换样式,锚定右侧详细信息
props.nodeClick(i.index);
selectAll('circle').attr('r', 11).attr('stroke', 'none');
if (nodes[i.index].type === 'table') {
// @ts-ignore
select(this)
.attr('r', 16)
.attr('stroke', `rgba(56, 204, 181, 0.3)`)
.attr('stroke-width', '15px');
} else if (
nodes[i.index].type === 'businessDomain' ||
nodes[i.index].type === 'subjectDomain'
) {
// @ts-ignore
select(this)
.attr('r', 16)
.attr('stroke', `rgba(160, 150, 234, 0.3)`)
.attr('stroke-width', '15px');
} else if (nodes[i.index].type === 'tag') {
// @ts-ignore
select(this)
.attr('r', 16)
.attr('stroke', `rgba(255, 160, 45, 0.3)`)
.attr('stroke-width', '15px');
} else {
// @ts-ignore
select(this)
.attr('r', 16)
.attr('stroke', `rgba(255, 137, 136, 0.3)`)
.attr('stroke-width', '15px');
}
});
// 圆形节点的描述信息
const svg_texts = g.append('g').selectAll('text').data(nodes).enter().append('g');
svg_texts
.append('svg:text')
.attr('text-anchor', 'middle')
.attr('y', 25)
.attr('fill', '#B4B9C7')
.attr('font-size', 14)
.text(function (d: any) {
return d.label;
});
//力导图节点拖拽时的事件监听器 以实时更新坐标
function ticked() {
// 弧线
// link.attr("d", function(d) {
// var 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;
// });
link.attr('d', (d: any) => {
return d.source.x < d.target.x
? 'M' + d.source.x + ',' + d.source.y + 'L' + d.target.x + ',' + d.target.y
: 'M' + d.target.x + ',' + d.target.y + 'L' + d.source.x + ',' + d.source.y;
});
node
.attr('cx', function (d: any) {
return d.x;
})
.attr('cy', function (d: any) {
return d.y;
});
svg_texts.attr('transform', function (d: any) {
return 'translate(' + d.x + ',' + d.y + ')';
});
}
};
return <div id="myMap" style={{ position: 'absolute', overflow: 'hidden' }}></div>;
};
至此,文章开头的图,已经实现完成,本来想实现节点之间弧线连接并且保持颜色渐变的功能,弧线是实现了,但是渐变缺兼容不了,也没找到合适的方法
*
*
*
*
*
*
###############################################
下面直接贴出改动部分的代码,并附上备注说明
// 节点图片
// 通过id绑定
defs
.selectAll('pattern')
.data(nodes)
.enter()
.append('pattern')
.attr('id', function (d: any, i: any) {
return 'insect' + i;
})
.attr('width', 1)
.attr('height', 1)
.append('svg:image')
.attr('xlink:href', function (d: any, i: any) {
// 不同节点不同图片
if (nodes[i].type === 'table') {
return '图片地址';
} else if (nodes[i].type === 'businessDomain' || nodes[i].type === 'subjectDomain') {
return '图片地址';
} else if (nodes[i].type === 'tag') {
return '图片地址';
} else {
return '图片地址';
}
})
.attr('width', 22)
.attr('class', 'img');
// 节点线条颜色
// 去掉之前的渐变版本
function lineColor(y: any, i: any) {
if (y.properties) {
if (y.properties.frequency <= 5) {
return '#ADE2D8';
} else if (y.properties.frequency <= 20) {
return '#029A8F';
} else {
// @ts-ignore
select(this).attr('stroke-width', 2.5);
return '#02292B';
}
}
// @ts-ignore
select(this).attr('stroke-width', 2.5);
return '#02292B';
}
// 圆形节点的描述信息
const svg_texts = g.append('g').selectAll('text').data(nodes).enter().append('g');
svg_texts
.append('svg:text')
.attr('text-anchor', 'middle')
.attr('y', 25)
.attr('fill', '#B4B9C7')
.attr('font-size', 14)
.attr('pointer-events', 'none') // 添加事件穿透
.text(function (d: any) {
return d.label;
});