ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

05-Nebula Graph 图数据 可视化

2022-08-23 16:02:28  阅读:220  来源: 互联网

标签:function return attr 05 Graph Nebula edge import id


图数据库的可视化

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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有