图数据库的可视化
Nebula本身自带的Studio
虽然很好用, 但是并不能直接嵌入到业务系统中, 也不能直接给客户用, 所以我找了好多也没有说直接能展示图关系的, 但是我看网上好多都说是基于D3.js就可以做, 但是我是一个后端呀, D3相对复杂, 但是需求刚在眼前还是要做的..
基于D3开发Nebula的关系可视化
前端
前端在网上找到了一个基于React antd做的一个Demo, 为此我还特意去学习了React Antd D3
这个就可以用于做Nebula的可视化
于是我把这个代码从Git上拿了下来
看了一下, 发现大佬写的非常好
前端需要的数据结构
代码语言:javascript复制<Route exact path="/simple-force-chart" component={SimpleForceChart} />
import React from 'react'
import {Row, Col, Card} from 'antd'
import D3SimpleForceChart from '../components/charts/D3SimpleForceChart'
class SimpleForceChart extends React.Component {
render() {
const data = {
nodes:[
{
"i": 0,
"name": "test3",
"description": "this is desc!",
"id": "186415162885763072"
},
{
"i": 1,
"name": "test4",
"description": "this is desc!",
"id": "186415329756147712"
},
{
"i": 2,
"name": "test7",
"description": "this is desc!",
"id": "186420276928757760"
},
{
"i": 3,
"name": "test6",
"description": "this is desc!",
"id": "186417155309998080"
}
],
edges:[
{
"source": 0,
"target": 1,
"relation": "类-类",
"id": "1",
"value": 2
},
{
"source": 1,
"target": 2,
"relation": "类-类",
"id": "1",
"value": 3
},
{
"source": 1,
"target": 3,
"relation": "类-类",
"id": "1",
"value": 3
}
]
}
return (
<div className="gutter-example simple-force-chart-demo">
<Row gutter={10}>
<Col className="gutter-row" md={24}>
<div className="gutter-box">
<Card title="D3 简单力导向图" bordered={false}>
<D3SimpleForceChart data={data}/>
</Card>
</div>
</Col>
</Row>
</div>
)
}
}
export default SimpleForceChart
D3渲染
代码语言:javascript复制import React from 'react'
import PropTypes from 'prop-types'
import * as d3 from 'd3'
class D3SimpleForceChart extends React.Component {
componentDidMount() {
// 容器宽度
const containerWidth = this.chartRef.parentElement.offsetWidth
// 数据
const data = this.props.data
// 外边距
const margin = { top: 60, right: 60, bottom: 60, left: 60 }
// 计算宽度
const width = containerWidth - margin.left - margin.right
// 固定高度
const height = 700 - margin.top - margin.bottom
// this.chartRef 是个啥 看着像SVG标签
console.log("this.chartRef",this.chartRef)
console.log("data",this.props.data)
let chart = d3
.select(this.chartRef)
.attr('width', width margin.left margin.right)
.attr('height', height margin.top margin.bottom)
let g = chart
.append('g')
.attr('transform', 'translate(' margin.left ',' margin.top ')') // 设最外包层在总图上的相对位置
let simulation = d3
.forceSimulation() // 构建力导向图
.force('link',
d3.forceLink()
.id((d,i) => i)
.distance(d => d.value * 50)
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
let z = d3.scaleOrdinal(d3.schemeCategory20) // 通用线条的颜色
let link = g
.append('g') // 画连接线
.attr('class', 'links')
.selectAll('line')
.data(data.edges)
.enter()
.append('line')
// .on('click',function (d,i) {
// console.log("click",d,i)
// // 连接线条点击事件
// 调用接口请求属性数据, 但是感觉, 线的话, 太细了, 不容易点击, 考虑点击标题, 或者悬浮到线上
// })
// .on('mouseover',function (d, i) {
// console.log("mouseover",d,i)
// // 线条悬浮事件
// // 被文字遮盖了一部份, 还是考虑点击文字
// })
// 画连接连上面的关系文字
let linkText = g
.append('g')
.attr('class', 'link-text')
.selectAll('text')
.data(data.edges)
.enter()
.append('text')
.text(d => d.relation)
.on('click',function (d,i) {
// 线上标题文本的点击事件
// 可以在这里做请求接口然后 获取属性展示
// 取d.id即可
console.log("clicktitle",d,i)
})
.style("fill-opacity",1)
let node = g
.append('g') // 画圆圈和文字
.attr('class', 'nodes')
.selectAll('g')
.data(data.nodes)
.enter()
.append('g')
// 这个是悬浮节点展示线路的标签 感觉听炫酷的
// .on('mouseover', function(d, i) {
// //显示连接线上的文字
// linkText.style('fill-opacity', function(edge) {
// if (edge.source === d || edge.target === d) {
// return 1
// }
// })
// //连接线加粗
// link
// .style('stroke-width', function(edge) {
// if (edge.source === d || edge.target === d) {
// return '2px'
// }
// })
// .style('stroke', function(edge) {
// if (edge.source === d || edge.target === d) {
// return '#000'
// }
// })
// })
// .on('mouseout', function(d, i) {
// //隐去连接线上的文字
// linkText.style('fill-opacity', function(edge) {
// if (edge.source === d || edge.target === d) {
// return 0
// }
// })
// //连接线减粗
// link
// .style('stroke-width', function(edge) {
// if (edge.source === d || edge.target === d) {
// return '1px'
// }
// })
// .style('stroke', function(edge) {
// if (edge.source === d || edge.target === d) {
// return '#ddd'
// }
// })
// })
.on('click', function (d,i){
console.log(d,i)
// d是数据 i 是索引
// 在这里可以做点击事件, 请求后端接口 返回属性数据, 然后渲染
})
.call(
d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended)
)
node.append('circle')
.attr('r', 5)
.attr('fill', (d,i) => z(i))
node.append('text')
.attr('fill', (d,i) => z(i))
.attr('y', -20)
.attr('dy', '.71em')
.text(d => d.name)
// 初始化力导向图
simulation.nodes(data.nodes)
.on('tick', ticked)
simulation.force('link')
.links(data.edges)
chart.append('g') // 输出标题
.attr('class', 'bar--title')
.append('text')
.attr('fill', '#000')
.attr('font-size', '16px')
.attr('font-weight', '700')
.attr('text-anchor', 'middle')
.attr('x', containerWidth / 2)
.attr('y', 20)
.text('人物关系图')
function ticked() {
// 力导向图变化函数,让力学图不断更新
link
.attr('x1', function(d) {
return d.source.x
})
.attr('y1', function(d) {
return d.source.y
})
.attr('x2', function(d) {
return d.target.x
})
.attr('y2', function(d) {
return d.target.y
})
linkText
.attr('x', function(d) {
return (d.source.x d.target.x) / 2
})
.attr('y', function(d) {
return (d.source.y d.target.y) / 2
})
node.attr('transform', function(d) {
return 'translate(' d.x ',' d.y ')'
})
}
function dragstarted(d) {
if (!d3.event.active) {
simulation.alphaTarget(0.3).restart()
}
d.fx = d.x
d.fy = d.y
}
function dragged(d) {
d.fx = d3.event.x
d.fy = d3.event.y
}
function dragended(d) {
if (!d3.event.active) {
simulation.alphaTarget(0)
}
d.fx = null
d.fy = null
}
}
render() {
return (
<div className="force-chart--simple">
<svg ref={r => (this.chartRef = r)} />
</div>
)
}
}
D3SimpleForceChart.propTypes = {
data: PropTypes.shape({
nodes: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired
// href:PropTypes.string.isRequired,
}).isRequired
).isRequired,
edges: PropTypes.arrayOf(
PropTypes.shape({
source: PropTypes.number.isRequired,
target: PropTypes.number.isRequired,
relation: PropTypes.string.isRequired
}).isRequired
).isRequired
}).isRequired
}
export default D3SimpleForceChart
虽然代码看不懂, 但是并不影响我完成功能, 我在样式上面对原有的做了一些改变
后端
做数据结构转化, 转为D3需要的数据结构
虽然我前端不咋地, 但是后端我行呀
代码语言:javascript复制MATCH p=(v:test3)-[*2]->() where id(v) == '186344099868655616' return [n in nodes(p) | properties(n)] as node,[x in relationships(p) | properties(x)] as rela
这个是查询test3 id=186344099868655616 近2跳的数据, 我在语法上做了一些处理
本来是直接返回路径变量p的, 但是居然直接报错了
Nebula自身提供的Jar包解析不了, 自己的返回结果, 当时差点绝望了, 还不底层的调用全部都封装了起来...
最重只能在语法上进行处理, 通过两个函数和管道符循环,来完成, 但是会吧节点和关系拆开, 拆成两个列.., 不过也算是能返回结果了
然后在程序里面处理, 转为D3需要的数据结构
导入需要的模型类
代码语言:javascript复制package com.jd.knowledgeextractionplatform.nebulagraph.model;
import lombok.Data;
import java.util.List;
@Data
public class PathPar {
private List<Node> node;
private List<Rela> rela;
}
代码语言:javascript复制package com.jd.knowledgeextractionplatform.nebulagraph.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
public class Node {
private Integer i;
private String name;
private String description;
private String id;
public boolean equals(Node node) {
return this.id.equals(node.id);
}
}
代码语言:javascript复制package com.jd.knowledgeextractionplatform.nebulagraph.d3model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Edges {
private Integer source;
private Integer target;
private String relation;
private String id;
private Integer value;
}
代码语言:javascript复制package com.jd.knowledgeextractionplatform.nebulagraph.d3model;
import com.jd.knowledgeextractionplatform.nebulagraph.model.Node;
import lombok.Data;
import java.util.List;
import java.util.Set;
@Data
public class D3Model {
private List<Node> nodes;
private List<Edges> edges;
}
代码语言:javascript复制package com.jd.knowledgeextractionplatform.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jd.knowledgeextractionplatform.common.CommonResult;
import com.jd.knowledgeextractionplatform.mapper.ClassAndAttrMapper;
import com.jd.knowledgeextractionplatform.nebulagraph.d3model.D3Model;
import com.jd.knowledgeextractionplatform.nebulagraph.d3model.Edges;
import com.jd.knowledgeextractionplatform.nebulagraph.d3model.SE;
import com.jd.knowledgeextractionplatform.nebulagraph.model.Node;
import com.jd.knowledgeextractionplatform.nebulagraph.model.PathPar;
import com.jd.knowledgeextractionplatform.nebulagraph.model.Rela;
import com.jd.knowledgeextractionplatform.nebulagraph.template.NebulaTemplate;
import com.jd.knowledgeextractionplatform.pojo.ClassAndAttr;
import com.jd.knowledgeextractionplatform.service.SearchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@Slf4j
public class SearchServiceImpl implements SearchService {
@Autowired
private NebulaTemplate nebulaTemplate;
@Autowired
private ClassAndAttrMapper classAndAttrMapper;
@Override
public CommonResult search(Long projectId, String name, Integer skip) {
LambdaQueryWrapper<ClassAndAttr> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(ClassAndAttr::getProjectId, projectId);
lambdaQueryWrapper.eq(ClassAndAttr::getName, name);
lambdaQueryWrapper.eq(ClassAndAttr::getType, 1);
lambdaQueryWrapper.eq(ClassAndAttr::getDeleted, 0);
ClassAndAttr classAndAttrs = classAndAttrMapper.selectOne(lambdaQueryWrapper);
String match = "MATCH p=(v:%s)-[*%s]->() where id(v) == '%s' return [n in nodes(p) | properties(n)] as node,[x in relationships(p) | properties(x)] as rela";
String matchSql = String.format(match, classAndAttrs.getCode(), skip, classAndAttrs.getId());
log.info("search sql : {}", matchSql);
JSONObject resultSet = nebulaTemplate.executeJson(matchSql);
String datas = resultSet.getString("data");
List<PathPar> pathPars = JSONArray.parseArray(datas, PathPar.class);
D3Model d3Model = pathParsConvertToD3Model(pathPars);
return CommonResult.success("查询成功", d3Model);
}
private D3Model pathParsConvertToD3Model(List<PathPar> pathPars) {
D3Model d3Model = new D3Model();
d3Model.setNodes(new ArrayList<>());
d3Model.setEdges(new ArrayList<>());
int i = -1;
for (PathPar pathPar : pathPars) {
List<Node> nodes = pathPar.getNode();
List<Rela> relas = pathPar.getRela();
int jul = 2;
for (int i1 = 0; i1 < nodes.size() - 1; i1 ) {
Node node = nodes.get(i1);
Node node2 = nodes.get(i1 1);
Node fir = null;
Node sed = null;
for (Node d3ModelNode : d3Model.getNodes()) {
boolean equals = d3ModelNode.getId().equals(node.getId());
if (equals) {
fir = d3ModelNode;
}
boolean equals2 = d3ModelNode.getId().equals(node2.getId());
if (equals2) {
sed = d3ModelNode;
break;
}
}
if (null == fir) {
i = i 1;
fir = new Node();
BeanUtils.copyProperties(node, fir);
fir.setI(i);
d3Model.getNodes().add(fir);
}
if (null == sed) {
i = i 1;
sed = new Node();
BeanUtils.copyProperties(node2, sed);
sed.setI(i);
d3Model.getNodes().add(sed);
}
Rela rela = relas.get(i1);
List<Edges> edges1 = d3Model.getEdges();
Edges edges = new Edges(fir.getI(), sed.getI(), rela.getName(), rela.getId(), jul);
boolean flag = true;
for (Edges edges2 : edges1) {
if (edges2.getSource().equals(edges.getSource()) && edges2.getTarget().equals(edges.getTarget())) {
flag = false;
break;
}
}
if (flag) {
d3Model.getEdges().add(edges);
}
jul ;
}
}
// List<Node> collect = d3Model.getNodes().stream().sorted((x, y) -> {
// if (x.getI() < y.getI()) {
// return 1;
// } else if (x.getI() > y.getI()) {
// return -1;
// }
// return 0;
// }).collect(Collectors.toList());
// d3Model.setNodes(collect);
// 获取到所有的自环边
List<Node> nodes = d3Model.getNodes();
List<Edges> edges = d3Model.getEdges();
List<SE> indexs = new ArrayList<>();
for (int i1 = 0; i1 < nodes.size(); i1 ) {
Node node = nodes.get(i1);
String id = node.getId();
for (int i2 = i1 1; i2 <= nodes.size() - 1; i2 ) {
Node node2 = nodes.get(i2);
String id2 = node2.getId();
if (id.equals(id2)) {
// 存在重复, 自环数据
SE se = new SE();
se.setS(node.getI());
se.setE(node2.getI());
indexs.add(se);
}
}
}
// 解决图数据库存在自环边的问题 必须倒序遍历, 不然会造成数据越界问题
for (int i1 = indexs.size()-1; i1 >= 0 ; i1--) {
SE index = indexs.get(i1);
Integer s = index.getS();
Integer e = index.getE();
// 删除重复的节点
nodes.remove(e.intValue());
for (Edges edge : edges) {
Integer source = edge.getSource();
Integer target = edge.getTarget();
if(source.equals(e)){
// 将e 设置为 s
edge.setSource(s);
}
if(target.equals(e)){
// 将e 设置为 s
edge.setTarget(s);
}
}
}
// 处理后面的数据全部前移
for (int i1 = 0; i1 < nodes.size(); i1 ) {
Node node = nodes.get(i1);
if(!node.getI().equals(i1)){
// 如果不一样
Integer i2 = node.getI();
// 设置为当前的I
node.setI(i1);
// 循环遍历边
for (Edges edge : edges) {
Integer source = edge.getSource();
Integer target = edge.getTarget();
if(source.equals(i2)){
// 将e 设置为 s
edge.setSource(i1);
}
if(target.equals(i2)){
// 将e 设置为 s
edge.setTarget(i1);
}
}
}
}
// 获取到所有的重复点位
return d3Model;
}
}
给大家看一个 我执行返回的结果
代码语言:javascript复制{
"code": 200,
"msg": "查询成功",
"data": {
"nodes": [
{
"i": 0,
"name": "test3",
"description": "this is desc!",
"id": "186415162885763072"
},
{
"i": 1,
"name": "test4",
"description": "this is desc!",
"id": "186415329756147712"
},
{
"i": 2,
"name": "test7",
"description": "this is desc!",
"id": "186420276928757760"
},
{
"i": 3,
"name": "test6",
"description": "this is desc!",
"id": "186417155309998080"
}
],
"edges": [
{
"source": 0,
"target": 1,
"relation": "类-类",
"id": "1",
"value": 2
},
{
"source": 1,
"target": 2,
"relation": "类-类",
"id": "1",
"value": 3
},
{
"source": 1,
"target": 3,
"relation": "类-类",
"id": "1",
"value": 3
}
]
}
}
解决了自环和双向的问题
这就是上面前端需要的数据结构
把这个数据直接放入前端的静态数据里面就能展示了
到此, 基于D3的图可视化完成, 当然了, 样式不是很好看, 前端大佬自行美化吧~