• 05-Nebula Graph 图数据 可视化


    图数据库的可视化

    Nebula本身自带的Studio

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

    基于D3开发Nebula的关系可视化

    前端

    前端在网上找到了一个基于React+antd做的一个Demo, 为此我还特意去学习了React+Antd+D3

    这个就可以用于做Nebula的可视化

    于是我把这个代码从Git上拿了下来

    看了一下, 发现大佬写的非常好

    前端需要的数据结构

    1. <Route exact path="/simple-force-chart" component={SimpleForceChart} />
    2. import React from 'react'
    3. import {Row, Col, Card} from 'antd'
    4. import D3SimpleForceChart from '../components/charts/D3SimpleForceChart'
    5. class SimpleForceChart extends React.Component {
    6. render() {
    7. const data = {
    8. nodes:[
    9. {
    10. "i": 0,
    11. "name": "test3",
    12. "description": "this is desc!",
    13. "id": "186415162885763072"
    14. },
    15. {
    16. "i": 1,
    17. "name": "test4",
    18. "description": "this is desc!",
    19. "id": "186415329756147712"
    20. },
    21. {
    22. "i": 2,
    23. "name": "test7",
    24. "description": "this is desc!",
    25. "id": "186420276928757760"
    26. },
    27. {
    28. "i": 3,
    29. "name": "test6",
    30. "description": "this is desc!",
    31. "id": "186417155309998080"
    32. }
    33. ],
    34. edges:[
    35. {
    36. "source": 0,
    37. "target": 1,
    38. "relation": "类-类",
    39. "id": "1",
    40. "value": 2
    41. },
    42. {
    43. "source": 1,
    44. "target": 2,
    45. "relation": "类-类",
    46. "id": "1",
    47. "value": 3
    48. },
    49. {
    50. "source": 1,
    51. "target": 3,
    52. "relation": "类-类",
    53. "id": "1",
    54. "value": 3
    55. }
    56. ]
    57. }
    58. return (
    59. <div className="gutter-example simple-force-chart-demo">
    60. <Row gutter={10}>
    61. <Col className="gutter-row" md={24}>
    62. <div className="gutter-box">
    63. <Card title="D3 简单力导向图" bordered={false}>
    64. <D3SimpleForceChart data={data}/>
    65. </Card>
    66. </div>
    67. </Col>
    68. </Row>
    69. </div>
    70. )
    71. }
    72. }
    73. export default SimpleForceChart

    D3渲染

    1. import React from 'react'
    2. import PropTypes from 'prop-types'
    3. import * as d3 from 'd3'
    4. class D3SimpleForceChart extends React.Component {
    5. componentDidMount() {
    6. // 容器宽度
    7. const containerWidth = this.chartRef.parentElement.offsetWidth
    8. // 数据
    9. const data = this.props.data
    10. // 外边距
    11. const margin = { top: 60, right: 60, bottom: 60, left: 60 }
    12. // 计算宽度
    13. const width = containerWidth - margin.left - margin.right
    14. // 固定高度
    15. const height = 700 - margin.top - margin.bottom
    16. // this.chartRef 是个啥 看着像SVG标签
    17. console.log("this.chartRef",this.chartRef)
    18. console.log("data",this.props.data)
    19. let chart = d3
    20. .select(this.chartRef)
    21. .attr('width', width + margin.left + margin.right)
    22. .attr('height', height + margin.top + margin.bottom)
    23. let g = chart
    24. .append('g')
    25. .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') // 设最外包层在总图上的相对位置
    26. let simulation = d3
    27. .forceSimulation() // 构建力导向图
    28. .force('link',
    29. d3.forceLink()
    30. .id((d,i) => i)
    31. .distance(d => d.value * 50)
    32. )
    33. .force('charge', d3.forceManyBody())
    34. .force('center', d3.forceCenter(width / 2, height / 2))
    35. let z = d3.scaleOrdinal(d3.schemeCategory20) // 通用线条的颜色
    36. let link = g
    37. .append('g') // 画连接线
    38. .attr('class', 'links')
    39. .selectAll('line')
    40. .data(data.edges)
    41. .enter()
    42. .append('line')
    43. // .on('click',function (d,i) {
    44. // console.log("click",d,i)
    45. // // 连接线条点击事件
    46. // 调用接口请求属性数据, 但是感觉, 线的话, 太细了, 不容易点击, 考虑点击标题, 或者悬浮到线上
    47. // })
    48. // .on('mouseover',function (d, i) {
    49. // console.log("mouseover",d,i)
    50. // // 线条悬浮事件
    51. // // 被文字遮盖了一部份, 还是考虑点击文字
    52. // })
    53. // 画连接连上面的关系文字
    54. let linkText = g
    55. .append('g')
    56. .attr('class', 'link-text')
    57. .selectAll('text')
    58. .data(data.edges)
    59. .enter()
    60. .append('text')
    61. .text(d => d.relation)
    62. .on('click',function (d,i) {
    63. // 线上标题文本的点击事件
    64. // 可以在这里做请求接口然后 获取属性展示
    65. // 取d.id即可
    66. console.log("clicktitle",d,i)
    67. })
    68. .style("fill-opacity",1)
    69. let node = g
    70. .append('g') // 画圆圈和文字
    71. .attr('class', 'nodes')
    72. .selectAll('g')
    73. .data(data.nodes)
    74. .enter()
    75. .append('g')
    76. // 这个是悬浮节点展示线路的标签 感觉听炫酷的
    77. // .on('mouseover', function(d, i) {
    78. // //显示连接线上的文字
    79. // linkText.style('fill-opacity', function(edge) {
    80. // if (edge.source === d || edge.target === d) {
    81. // return 1
    82. // }
    83. // })
    84. // //连接线加粗
    85. // link
    86. // .style('stroke-width', function(edge) {
    87. // if (edge.source === d || edge.target === d) {
    88. // return '2px'
    89. // }
    90. // })
    91. // .style('stroke', function(edge) {
    92. // if (edge.source === d || edge.target === d) {
    93. // return '#000'
    94. // }
    95. // })
    96. // })
    97. // .on('mouseout', function(d, i) {
    98. // //隐去连接线上的文字
    99. // linkText.style('fill-opacity', function(edge) {
    100. // if (edge.source === d || edge.target === d) {
    101. // return 0
    102. // }
    103. // })
    104. // //连接线减粗
    105. // link
    106. // .style('stroke-width', function(edge) {
    107. // if (edge.source === d || edge.target === d) {
    108. // return '1px'
    109. // }
    110. // })
    111. // .style('stroke', function(edge) {
    112. // if (edge.source === d || edge.target === d) {
    113. // return '#ddd'
    114. // }
    115. // })
    116. // })
    117. .on('click', function (d,i){
    118. console.log(d,i)
    119. // d是数据 i 是索引
    120. // 在这里可以做点击事件, 请求后端接口 返回属性数据, 然后渲染
    121. })
    122. .call(
    123. d3.drag()
    124. .on('start', dragstarted)
    125. .on('drag', dragged)
    126. .on('end', dragended)
    127. )
    128. node.append('circle')
    129. .attr('r', 5)
    130. .attr('fill', (d,i) => z(i))
    131. node.append('text')
    132. .attr('fill', (d,i) => z(i))
    133. .attr('y', -20)
    134. .attr('dy', '.71em')
    135. .text(d => d.name)
    136. // 初始化力导向图
    137. simulation.nodes(data.nodes)
    138. .on('tick', ticked)
    139. simulation.force('link')
    140. .links(data.edges)
    141. chart.append('g') // 输出标题
    142. .attr('class', 'bar--title')
    143. .append('text')
    144. .attr('fill', '#000')
    145. .attr('font-size', '16px')
    146. .attr('font-weight', '700')
    147. .attr('text-anchor', 'middle')
    148. .attr('x', containerWidth / 2)
    149. .attr('y', 20)
    150. .text('人物关系图')
    151. function ticked() {
    152. // 力导向图变化函数,让力学图不断更新
    153. link
    154. .attr('x1', function(d) {
    155. return d.source.x
    156. })
    157. .attr('y1', function(d) {
    158. return d.source.y
    159. })
    160. .attr('x2', function(d) {
    161. return d.target.x
    162. })
    163. .attr('y2', function(d) {
    164. return d.target.y
    165. })
    166. linkText
    167. .attr('x', function(d) {
    168. return (d.source.x + d.target.x) / 2
    169. })
    170. .attr('y', function(d) {
    171. return (d.source.y + d.target.y) / 2
    172. })
    173. node.attr('transform', function(d) {
    174. return 'translate(' + d.x + ',' + d.y + ')'
    175. })
    176. }
    177. function dragstarted(d) {
    178. if (!d3.event.active) {
    179. simulation.alphaTarget(0.3).restart()
    180. }
    181. d.fx = d.x
    182. d.fy = d.y
    183. }
    184. function dragged(d) {
    185. d.fx = d3.event.x
    186. d.fy = d3.event.y
    187. }
    188. function dragended(d) {
    189. if (!d3.event.active) {
    190. simulation.alphaTarget(0)
    191. }
    192. d.fx = null
    193. d.fy = null
    194. }
    195. }
    196. render() {
    197. return (
    198. <div className="force-chart--simple">
    199. <svg ref={r => (this.chartRef = r)} />
    200. </div>
    201. )
    202. }
    203. }
    204. D3SimpleForceChart.propTypes = {
    205. data: PropTypes.shape({
    206. nodes: PropTypes.arrayOf(
    207. PropTypes.shape({
    208. name: PropTypes.string.isRequired
    209. // href:PropTypes.string.isRequired,
    210. }).isRequired
    211. ).isRequired,
    212. edges: PropTypes.arrayOf(
    213. PropTypes.shape({
    214. source: PropTypes.number.isRequired,
    215. target: PropTypes.number.isRequired,
    216. relation: PropTypes.string.isRequired
    217. }).isRequired
    218. ).isRequired
    219. }).isRequired
    220. }
    221. 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需要的数据结构

    导入需要的模型类

    1. package com.jd.knowledgeextractionplatform.nebulagraph.model;
    2. import lombok.Data;
    3. import java.util.List;
    4. @Data
    5. public class PathPar {
    6. private List node;
    7. private List rela;
    8. }
    1. package com.jd.knowledgeextractionplatform.nebulagraph.model;
    2. import lombok.Data;
    3. import lombok.EqualsAndHashCode;
    4. @Data
    5. public class Node {
    6. private Integer i;
    7. private String name;
    8. private String description;
    9. private String id;
    10. public boolean equals(Node node) {
    11. return this.id.equals(node.id);
    12. }
    13. }
    1. package com.jd.knowledgeextractionplatform.nebulagraph.d3model;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Data;
    4. import lombok.NoArgsConstructor;
    5. @Data
    6. @AllArgsConstructor
    7. @NoArgsConstructor
    8. public class Edges {
    9. private Integer source;
    10. private Integer target;
    11. private String relation;
    12. private String id;
    13. private Integer value;
    14. }
    1. package com.jd.knowledgeextractionplatform.nebulagraph.d3model;
    2. import com.jd.knowledgeextractionplatform.nebulagraph.model.Node;
    3. import lombok.Data;
    4. import java.util.List;
    5. import java.util.Set;
    6. @Data
    7. public class D3Model {
    8. private List nodes;
    9. private List edges;
    10. }
    1. package com.jd.knowledgeextractionplatform.service.impl;
    2. import com.alibaba.fastjson.JSONArray;
    3. import com.alibaba.fastjson.JSONObject;
    4. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    5. import com.jd.knowledgeextractionplatform.common.CommonResult;
    6. import com.jd.knowledgeextractionplatform.mapper.ClassAndAttrMapper;
    7. import com.jd.knowledgeextractionplatform.nebulagraph.d3model.D3Model;
    8. import com.jd.knowledgeextractionplatform.nebulagraph.d3model.Edges;
    9. import com.jd.knowledgeextractionplatform.nebulagraph.d3model.SE;
    10. import com.jd.knowledgeextractionplatform.nebulagraph.model.Node;
    11. import com.jd.knowledgeextractionplatform.nebulagraph.model.PathPar;
    12. import com.jd.knowledgeextractionplatform.nebulagraph.model.Rela;
    13. import com.jd.knowledgeextractionplatform.nebulagraph.template.NebulaTemplate;
    14. import com.jd.knowledgeextractionplatform.pojo.ClassAndAttr;
    15. import com.jd.knowledgeextractionplatform.service.SearchService;
    16. import lombok.extern.slf4j.Slf4j;
    17. import org.springframework.beans.BeanUtils;
    18. import org.springframework.beans.factory.annotation.Autowired;
    19. import org.springframework.stereotype.Service;
    20. import java.util.ArrayList;
    21. import java.util.List;
    22. @Service
    23. @Slf4j
    24. public class SearchServiceImpl implements SearchService {
    25. @Autowired
    26. private NebulaTemplate nebulaTemplate;
    27. @Autowired
    28. private ClassAndAttrMapper classAndAttrMapper;
    29. @Override
    30. public CommonResult search(Long projectId, String name, Integer skip) {
    31. LambdaQueryWrapper<ClassAndAttr> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    32. lambdaQueryWrapper.eq(ClassAndAttr::getProjectId, projectId);
    33. lambdaQueryWrapper.eq(ClassAndAttr::getName, name);
    34. lambdaQueryWrapper.eq(ClassAndAttr::getType, 1);
    35. lambdaQueryWrapper.eq(ClassAndAttr::getDeleted, 0);
    36. ClassAndAttr classAndAttrs = classAndAttrMapper.selectOne(lambdaQueryWrapper);
    37. 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";
    38. String matchSql = String.format(match, classAndAttrs.getCode(), skip, classAndAttrs.getId());
    39. log.info("search sql : {}", matchSql);
    40. JSONObject resultSet = nebulaTemplate.executeJson(matchSql);
    41. String datas = resultSet.getString("data");
    42. List<PathPar> pathPars = JSONArray.parseArray(datas, PathPar.class);
    43. D3Model d3Model = pathParsConvertToD3Model(pathPars);
    44. return CommonResult.success("查询成功", d3Model);
    45. }
    46. private D3Model pathParsConvertToD3Model(List<PathPar> pathPars) {
    47. D3Model d3Model = new D3Model();
    48. d3Model.setNodes(new ArrayList<>());
    49. d3Model.setEdges(new ArrayList<>());
    50. int i = -1;
    51. for (PathPar pathPar : pathPars) {
    52. List<Node> nodes = pathPar.getNode();
    53. List<Rela> relas = pathPar.getRela();
    54. int jul = 2;
    55. for (int i1 = 0; i1 < nodes.size() - 1; i1++) {
    56. Node node = nodes.get(i1);
    57. Node node2 = nodes.get(i1 + 1);
    58. Node fir = null;
    59. Node sed = null;
    60. for (Node d3ModelNode : d3Model.getNodes()) {
    61. boolean equals = d3ModelNode.getId().equals(node.getId());
    62. if (equals) {
    63. fir = d3ModelNode;
    64. }
    65. boolean equals2 = d3ModelNode.getId().equals(node2.getId());
    66. if (equals2) {
    67. sed = d3ModelNode;
    68. break;
    69. }
    70. }
    71. if (null == fir) {
    72. i = i + 1;
    73. fir = new Node();
    74. BeanUtils.copyProperties(node, fir);
    75. fir.setI(i);
    76. d3Model.getNodes().add(fir);
    77. }
    78. if (null == sed) {
    79. i = i + 1;
    80. sed = new Node();
    81. BeanUtils.copyProperties(node2, sed);
    82. sed.setI(i);
    83. d3Model.getNodes().add(sed);
    84. }
    85. Rela rela = relas.get(i1);
    86. List<Edges> edges1 = d3Model.getEdges();
    87. Edges edges = new Edges(fir.getI(), sed.getI(), rela.getName(), rela.getId(), jul);
    88. boolean flag = true;
    89. for (Edges edges2 : edges1) {
    90. if (edges2.getSource().equals(edges.getSource()) && edges2.getTarget().equals(edges.getTarget())) {
    91. flag = false;
    92. break;
    93. }
    94. }
    95. if (flag) {
    96. d3Model.getEdges().add(edges);
    97. }
    98. jul++;
    99. }
    100. }
    101. // List<Node> collect = d3Model.getNodes().stream().sorted((x, y) -> {
    102. // if (x.getI() < y.getI()) {
    103. // return 1;
    104. // } else if (x.getI() > y.getI()) {
    105. // return -1;
    106. // }
    107. // return 0;
    108. // }).collect(Collectors.toList());
    109. // d3Model.setNodes(collect);
    110. // 获取到所有的自环边
    111. List<Node> nodes = d3Model.getNodes();
    112. List<Edges> edges = d3Model.getEdges();
    113. List<SE> indexs = new ArrayList<>();
    114. for (int i1 = 0; i1 < nodes.size(); i1++) {
    115. Node node = nodes.get(i1);
    116. String id = node.getId();
    117. for (int i2 = i1+1; i2 <= nodes.size() - 1; i2++) {
    118. Node node2 = nodes.get(i2);
    119. String id2 = node2.getId();
    120. if (id.equals(id2)) {
    121. // 存在重复, 自环数据
    122. SE se = new SE();
    123. se.setS(node.getI());
    124. se.setE(node2.getI());
    125. indexs.add(se);
    126. }
    127. }
    128. }
    129. // 解决图数据库存在自环边的问题 必须倒序遍历, 不然会造成数据越界问题
    130. for (int i1 = indexs.size()-1; i1 >= 0 ; i1--) {
    131. SE index = indexs.get(i1);
    132. Integer s = index.getS();
    133. Integer e = index.getE();
    134. // 删除重复的节点
    135. nodes.remove(e.intValue());
    136. for (Edges edge : edges) {
    137. Integer source = edge.getSource();
    138. Integer target = edge.getTarget();
    139. if(source.equals(e)){
    140. // 将e 设置为 s
    141. edge.setSource(s);
    142. }
    143. if(target.equals(e)){
    144. // 将e 设置为 s
    145. edge.setTarget(s);
    146. }
    147. }
    148. }
    149. // 处理后面的数据全部前移
    150. for (int i1 = 0; i1 < nodes.size(); i1++) {
    151. Node node = nodes.get(i1);
    152. if(!node.getI().equals(i1)){
    153. // 如果不一样
    154. Integer i2 = node.getI();
    155. // 设置为当前的I
    156. node.setI(i1);
    157. // 循环遍历边
    158. for (Edges edge : edges) {
    159. Integer source = edge.getSource();
    160. Integer target = edge.getTarget();
    161. if(source.equals(i2)){
    162. // 将e 设置为 s
    163. edge.setSource(i1);
    164. }
    165. if(target.equals(i2)){
    166. // 将e 设置为 s
    167. edge.setTarget(i1);
    168. }
    169. }
    170. }
    171. }
    172. // 获取到所有的重复点位
    173. return d3Model;
    174. }
    175. }

    给大家看一个 我执行返回的结果

    1. {
    2. "code": 200,
    3. "msg": "查询成功",
    4. "data": {
    5. "nodes": [
    6. {
    7. "i": 0,
    8. "name": "test3",
    9. "description": "this is desc!",
    10. "id": "186415162885763072"
    11. },
    12. {
    13. "i": 1,
    14. "name": "test4",
    15. "description": "this is desc!",
    16. "id": "186415329756147712"
    17. },
    18. {
    19. "i": 2,
    20. "name": "test7",
    21. "description": "this is desc!",
    22. "id": "186420276928757760"
    23. },
    24. {
    25. "i": 3,
    26. "name": "test6",
    27. "description": "this is desc!",
    28. "id": "186417155309998080"
    29. }
    30. ],
    31. "edges": [
    32. {
    33. "source": 0,
    34. "target": 1,
    35. "relation": "类-类",
    36. "id": "1",
    37. "value": 2
    38. },
    39. {
    40. "source": 1,
    41. "target": 2,
    42. "relation": "类-类",
    43. "id": "1",
    44. "value": 3
    45. },
    46. {
    47. "source": 1,
    48. "target": 3,
    49. "relation": "类-类",
    50. "id": "1",
    51. "value": 3
    52. }
    53. ]
    54. }
    55. }

    解决了自环和双向的问题

    这就是上面前端需要的数据结构

    把这个数据直接放入前端的静态数据里面就能展示了

    到此, 基于D3的图可视化完成, 当然了, 样式不是很好看, 前端大佬自行美化吧~

  • 相关阅读:
    PhpSpreadsheet设置单元格常用操作汇总
    浅浅懂了一些transformer中的self-attation
    C\C++中数组指针和二维数组最强最简单粗暴深刻理解!!!一遍包过!
    文件包含漏洞学习小结
    重定向:基于神经网络优化的方法
    如何做一个无符号数识别程序
    多项目并行管理:优化协调策略提高效率
    【LeetCode高频100题-2】冲冲冲
    05-分布式计算框架
    rust字面量
  • 原文地址:https://blog.csdn.net/flowerStream/article/details/126487933