首页 > 其他分享 >05-Nebula Graph 图数据 可视化

05-Nebula Graph 图数据 可视化

时间:2022-08-23 16:02:26浏览次数:58  
标签: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
From: https://www.cnblogs.com/flower-dance/p/16616558.html

相关文章

  • AcWing算法基础课---第一讲基础算法---05位运算
    ###整数n的二进制数的第k位数```n>>k&1```###lowbit运算```lowbit(x)x&(~x+1)=x&(-x)```###AcWing801.二进制中1的个数```#include<iostream>usingn......
  • JAVA---05
    第五天1.面向对象(OO)编程(P)1.面向过程思想&面向对象思想面向过程:步骤清晰简单,第一步做什么,第二步做什么适合于一些简单的问题面向对象分类的思维模......
  • AtCoder Grand Contest 058 部分题目不简要题解
    从这里开始比赛目录ProblemA MakeitZigzag考虑使$1,3,5,7,\cdots,2n-3$这些位置后三个中的最大值在中间,最后再处理一下最后两个位置就行了。Cod......
  • 题解 CF1712D Empty Graph
    CF1712D洛谷的CF的提交无了,所以可能没人来看了,但是在题解区是清一色的二分,而唯一一篇贪心题解的讨论还略显复杂的情况下,我还是希望提供一种比较简洁的贪心题解。在复杂......
  • 054_末晨曦Vue技术_处理边界情况之组件之间的循环引用
    组件之间的循环引用点击打开视频讲解更详细假设你需要构建一个文件目录树,像访达或资源管理器那样的。你可能有一个<tree-folder>组件,模板是这样的:<p><span>{{fold......
  • 1041 [SCOI2005]繁忙的都市 kruskal 最小生成树
     链接:https://ac.nowcoder.com/acm/contest/26077/1041来源:牛客网题目描述城市C是一个非常繁忙的大都市,城市中的道路十分的拥挤,于是市长决......
  • P3605 [USACO17JAN]Promotion Counting P 题解
    solution考虑权值线段树合并:首先离散化,然后对于一个节点,我们将它的所有子树合并上来,并统计所有能力指数的个数(权值线段树基本操作),查询时只需查询\(p_i+1\simn\)的和即......
  • 2022杭电多校 第9场 1005 Leapfrogger (期望)
    可以说官方题解除了恶心其他人和告诉你这题不难之外没有任何作用。考虑期望的线性性,可以将每一个跳蛙的每一个亡语单独考虑。令\(f_n\)代表剩余\(n\)个随从,其中有一个是......
  • re.findall()及for..in..迭代器_05
    re.findall(pattern,string,flags=0)返回pattern在string中的所有非重叠匹配,以字符串列表或字符串元组列表的形式。对string的扫描从左至右,匹配结果按照找到......
  • 向QtableWidget中添加自定义widget崩溃异常: 0xC0000005
    1.问题描述想给QTableWidget添加QCheckBox,代码如下,tableWidget->setCellWidget老是崩溃(0x0F954E63(qwindows.dll)处(位于QStockView.exe中)引发的异常:0xC0000005: ),......