图数据库的可视化
Nebula本身自带的Studio

虽然很好用, 但是并不能直接嵌入到业务系统中, 也不能直接给客户用, 所以我找了好多也没有说直接能展示图关系的, 但是我看网上好多都说是基于D3.js就可以做, 但是我是一个后端呀, D3相对复杂, 但是需求刚在眼前还是要做的..
基于D3开发Nebula的关系可视化
前端
前端在网上找到了一个基于React+antd做的一个Demo, 为此我还特意去学习了React+Antd+D3

这个就可以用于做Nebula的可视化
于是我把这个代码从Git上拿了下来
看了一下, 发现大佬写的非常好
前端需要的数据结构
- <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渲染
- 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需要的数据结构
虽然我前端不咋地, 但是后端我行呀
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需要的数据结构
导入需要的模型类
- package com.jd.knowledgeextractionplatform.nebulagraph.model;
-
- import lombok.Data;
-
- import java.util.List;
-
- @Data
- public class PathPar {
- private List
node; - private List
rela; - }
- 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);
- }
- }
- 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;
- }
- 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
nodes; - private List
edges; - }
- 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;
- }
-
-
- }
给大家看一个 我执行返回的结果
- {
- "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的图可视化完成, 当然了, 样式不是很好看, 前端大佬自行美化吧~
