05-Nebula Graph 图数据 可视化

2022-08-24 08:32:47 浏览数 (1)

图数据库的可视化

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的图可视化完成, 当然了, 样式不是很好看, 前端大佬自行美化吧~

0 人点赞