05-Nebula Graph 图数据 可视化
作者:互联网
图数据库的可视化
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> node; private List<Rela> 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<Node> nodes; private List<Edges> 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的图可视化完成, 当然了, 样式不是很好看, 前端大佬自行美化吧~
标签:function,return,attr,05,Graph,Nebula,edge,import,id 来源: https://www.cnblogs.com/flower-dance/p/16616558.html