栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > .Net

Neo4j apoc 整合Angular,HighChart.networkgraph,Primeng从任意节点拉出和该节点有关系的整个图 (Enhanced)

.Net 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Neo4j apoc 整合Angular,HighChart.networkgraph,Primeng从任意节点拉出和该节点有关系的整个图 (Enhanced)

最近有个需求,其中一个子需求就是从任意节点进入,拉出和他有关系的整个图,所以研究了下

APOC

关于介绍就去看这篇帖子吧https://blog.csdn.net/graphway/article/details/78957415

apoc.path.expand

我们要使用的API就是apoc.path.expand,具体介绍看https://neo4j.com/labs/apoc/4.1/overview/apoc.path/apoc.path.expand/
以及
https://neo4j.com/labs/apoc/4.1/graph-querying/expand-paths/

Cypher
apoc.path.expand(start :: ANY?, relationshipFilter :: STRING?, labelFilter :: STRING?, minLevel :: INTEGER?, maxLevel :: INTEGER?) :: (path :: PATH?)
MATCH (user:jhi_user {login:'admin'})
CALL apoc.path.expand(user,'<|>','*',0,-1)
YIELD path as paths return paths

关于最大关系数,我测试了一下,填写-1可以拉出所有关系

Result

第二张图就是所有的图,打红圈的地方就是进入节点,也就是cypher中

MATCH (user:jhi_user {login:'admin'})

查找的节点,第一张图就是这段cypher查找出来的和该节点有关系的所有节点的图了.

改进

经过几天的使用,发现这种方式其实有点问题,他是根据你指定的节点到任意关系节点,只要小于设置的最大level数都会返回一次,换句话说,该节点经过一个关系的节点,该节点经过两个关系的节点,这样的路径来返回,如果前端要做数据处理或者使用highchart来展示的话数据适配很麻烦.并且返回的是节点的ID,获取不到详细信息

所以后来我使用了另外一个,apoc.path.subgraphAll()
https://neo4j.com/labs/apoc/4.1/graph-querying/expand-subgraph/
查询cypher变成了

MATCH (user:jhi_user {login:'admin'})
CALL apoc.path.subgraphAll(user,{relationshipFilter: '<|>',minLevel: 0,maxLevel: -1})
YIELD nodes,relationships return nodes,relationships


他这样查出来的数据不仅返回了每个节点的详细信息,还返回了关系,有起始节点和目标节点(这个关系在java中使用的是org.neo4j.ogm.response.model.RelationshipModel封装的,即使使用的Result接收的封装数据,处理数据的时候盲目的去转Map就会报错)

格式化数据,使用HighChart展示

因为highchart的数据格式是List>这种结构
[
[‘Proto Indo-European’, ‘Balto-Slavic’],
[‘Proto Indo-European’, ‘Germanic’],
[‘Proto Indo-European’, ‘Celtic’],
[‘Proto Indo-European’, ‘Italic’],
[‘Proto Indo-European’, ‘Hellenic’],
[‘Proto Indo-European’, ‘Anatolian’],
[‘Proto Indo-European’, ‘Indo-Iranian’],
[‘Proto Indo-European’, ‘Tocharian’],
[‘Indo-Iranian’, ‘Dardic’],
[‘Indo-Iranian’, ‘Indic’],
[‘Indo-Iranian’, ‘Iranian’],
[‘Iranian’, ‘Old Persian’],
]
相同的名字被认为是相同节点

DTO

@Data
public class NodeDTO {

    private String label;
    private String properity;
    private String value;
}

@Data
public class RelationshipDTO {

    private NodeDTO fromNode;
    private NodeDTO toNode;
    private String relationship;
}

service

   public Map getAllPathToListByDynamicConditions(SearchPathDTO searchPathDTO) {
        Map map = new HashMap<>();
        List> path = new ArrayList<>();
        List nodes = new ArrayList<>();
        List relationships = new ArrayList<>();
        Result result = this.nodeService.searchPath(searchPathDTO);
        List> resultList = this.copyIterator(result.iterator());
        if (!resultList.isEmpty()) {
            nodes = (List) resultList.get(0).get("nodes");
            try {
                relationships = (List) resultList.get(0).get("relationships");
            } catch (ClassCastException e) {
                relationships = new ArrayList<>();
            }
            map.put("nodes", toNodeList(nodes));
            if (!relationships.isEmpty()) {
                relationships.forEach(item -> {
                    Map itemMap = new HashMap<>();
                    itemMap.put("from", ((RelationshipModel) item).getStartNode());
                    itemMap.put("to", ((RelationshipModel) item).getEndNode());
                    path.add(itemMap);
                });
                map.put("data", path);
            } else {
                nodes.forEach(res->{
                    Map itemMap = new HashMap<>();
                    itemMap.put("from", res.getId());
                    itemMap.put("to", res.getId());
                    path.add(itemMap);
                });
            }
            map.put("data", path);
        }
        return map;
    }

    private List> toNodeList(List nodes) {
        List> list = new ArrayList<>();
        nodes.forEach(item -> {
            Map map = new HashMap<>();
            map.put("id", item.getId());
            map.put("name", this.coverIterableToString(item.getLabels(), item.getPropertyList()));
            map.put("properties", item.getPropertyList());
            list.add(map);
        });
        return list;
    }

    private  List copyIterator(Iterator iter) {
        List copy = new ArrayList();
        while (iter.hasNext())
            copy.add(iter.next());
        return copy;
    }

    private Map coverListToMap(List list) {
        Map map = new HashMap<>();
        list.forEach(res->{
            map.put(res.getId().toString(), res);
        });
        return map;
    }

    private String coverIterableToString(String[] labels, List> propertyList) {
        String text = "";
        for (String src : labels) {
            text += src + ".";
        }
        text += this.getNameFromPropertyList(propertyList);
        return text;
    }

    private String getNameFromPropertyList(List> propertyList) {
        String[] text = { "" };
        propertyList.forEach(res -> {
            if ("name".equals((res.getKey()))) {
                text[0] = res.getValue().toString();
            }
        });
        return text[0];
    }

Cypher

//import org.neo4j.ogm.session.Session;
//import org.neo4j.ogm.session.SessionFactory;
  @Autowired
  private SessionFactory sessionFactory;
  
  public Result searchPath(SearchPathDTO searchPathDTO) {
        String cypher = "MATCH (n:" + searchPathDTO.getFromNode() + "{" + searchPathDTO.getProperity() + ":'"
                + searchPathDTO.getValue() + "'}) "
                + "CALL apoc.path.subgraphAll(n,{relationshipFilter: '<|>',minLevel: 0,maxLevel: -1}) "
                + "YIELD nodes,relationships RETURN nodes, relationships";
        Session session = sessionFactory.openSession();
        return session.query(cypher, new HashMap<>(), false);
    }
结果

改进2

首先现在可以在一个hover出来的OverlayPanel中实时更改graph图中节点大小以及节点之间的距离,来让用户更好的更全方位的控制这个图的显示,当然图不是实时渲染,不然一旦数据量过大的话这样即时动态更改消耗资源太大,即时更改的只有OverlayPanel中的demo演示,用户可以根据这个demo来查看当前选择的节点大小以及距离的具体样式.当点击OverlayPanel其他任意地方,OverlayPanel消失之后,这个设置就会应用,这时候才会重新渲染图.当然也会有个flag记录是否真的更改了数据,如果是没有更改任何数据就hide的话也是不会重新渲染graph图的.


还有一个功能就是隐藏某些label,最后一个multiSelect中选择的label,都会在图中被隐藏,下图就是隐藏了firewall这个label,导致整个图的链接被截断,变成了两个图.


还有一个功能就是点击某个node,会弹出一个dialog展示该node的所有detail

这里贴的是chart以及demochart和color的配置

export const CHART_SRC = {
  chart: {
    type: 'networkgraph',
    height: 800,
    scrollablePlotArea: {
      minWidth: 1800,
      minHeight: 800,
      opacity: 0
    }
  },
  exporting: {
    enabled: false
  },
  title: null,
  credits: {
    enabled: false
  },
  plotOptions: {
    networkgraph: {
      keys: ['from', 'to', 'custom', 'order'],
      layoutAlgorithm: {
        enableSimulation: false,
        friction: -0.96,
        linkLength: 45,
      },
      cursor: 'pointer',
      events: {}
    }
  },
  series: [
    {
      dataLabels: {
        enabled: true,
        linkTextPath: {
          attributes: {
            dy: 12
          }
        },
        linkFormatter: function () {
          return this.point.options.relationship + '
' + this.point.fromNode.id + 'u2192' + this.point.toNode.id; }, formatter: function () { // return this.point.id + ' . ' + this.point.custom.view.view_name + '
' + this.point.custom.view.view_description; return this.point.id + ' . ' + this.point.custom.view.view_description; }, }, marker: { radius: 15 }, color: 'rgb(124, 181, 236)', data: [] } ] } export const DEMO_SRC = { chart: { type: 'networkgraph', width: 360, height: 300, scrollablePlotArea: { minWidth: 360, minHeight: 300, opacity: 0 } }, exporting: { enabled: false, }, title: null, credits: { enabled: false }, plotOptions: { networkgraph: { keys: ['from', 'to'], layoutAlgorithm: { enableSimulation: false, friction: -0.96, linkLength: 45, }, cursor: 'pointer', events: {} } }, series: [ { dataLabels: { enabled: true, linkTextPath: { attributes: { dy: 12 } }, linkFormatter: function () { return this.point.fromNode.id + 'u2192' + this.point.toNode.id; } }, marker: { radius: 15 }, color: 'rgb(124, 181, 236)', data: [{ from: 'Node A', to: 'Node B' }] } ] } export const COLOR = { Monitor: '#B3DFEC', Router: '#E8A5CC', Web: '#B9C0EA', TrafficManager: '#F6AC5A', Server1: '#F0725C', Server2: '#F0725C', Server3: '#F0725C', Server4: '#F0725C', Database1: '#F282A7', Database2: '#F282A7', outside_user: '#FA8072', internal_l4: '#E6E6FA', firewall: '#B0C4DE', access_server: '#FFB6C1', san_switch: '#F0725C', router: '#F282A7', internet_backbone1: '#B3DFEC', vpn1: '#E8A5CC', virtualization_backbone1: '#B9C0EA', vpn2: '#F6AC5A', virtualization_backbone2: '#F0725C', ips: '#F4A460', virtualization_switch: '#B3DFEC', storage: '#F282A7', external_l4: '#B9C0EA', vdi_server: '#E8A5CC', internet_backbone2: '#AFEEEE', inside_user: '#DDA0DD' }

这是当前module中所有引用的module,这里把route comment是因为这个Component最终是以DynamicDialog的形式呈现的,所以不需要route了

// import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { GraphComponent } from './graph.component';
import { XXXSharedModule} from 'app/shared/shared.module';
// import { GRAPH_ROUTE } from './graph.route';
import { FormsModule } from '@angular/forms';
import { HIGHCHARTS_MODULES, ChartModule } from 'angular-highcharts';
import * as more from 'highcharts/highcharts-more.src';
import * as exporting from 'highcharts/modules/exporting.src';
import * as networkgraph from 'highcharts/modules/networkgraph.src';
import { CardModule } from 'primeng/card';
import { InputTextModule } from 'primeng/inputtext';
import { ButtonModule } from 'primeng/button';
import { TabViewModule } from 'primeng/tabview';
import { DropdownModule } from 'primeng/dropdown';
import { ToastModule } from 'primeng/toast';
import { DialogModule } from 'primeng/dialog';
import { TableModule } from 'primeng/table';
import { MultiSelectModule } from 'primeng/multiselect';
import { SliderModule } from 'primeng/slider';
import { OverlayPanelModule } from 'primeng/overlaypanel';

@NgModule({
  declarations: [GraphComponent],
  imports: [
    XXXSharedModule,//某些必须引用的Module已经被这ShareModule引用,这里是公司其他同事写的所以就不贴了,但是都是写基本的包,经常使用angular的应该都会引用.
    CardModule,
    ButtonModule,
    MultiSelectModule,
    SliderModule,
    OverlayPanelModule,
    DropdownModule,
    ToastModule,
    InputTextModule,
    FormsModule,
    TabViewModule,
    ChartModule,
    DialogModule,
    TableModule
    // RouterModule.forChild(GRAPH_ROUTE)
  ],
  providers: [
    { provide: HIGHCHARTS_MODULES, useFactory: () => [more, exporting, networkgraph] }
  ]
})
export class GraphModule { }

Dialog

ID {{nodeDetail.id}}
Labels *ngFor="let item of nodeDetail.labels;index as i"> *ngIf="i!==0"> ,  >{{item}}


Properties
{{item.key}} {{item.value}}

这一段是在compoment中添加的,因为需要使用component中this里面的变量.这个是给节点添加点击event

 this.chartStr['plotOptions']['networkgraph']['events'] = {
      click: (event: any) => {
        this.nodeDetail = event['point']['custom']['detail'];
        this.dialogTitle = event['point']['name'];
        this.dialogDisplay = true;
      }
    }
OverlayPanel

    

Node Radius: {{radius}}

Node Distance: {{distance}}

Hide

  mofidyChart(): void {
    if (this.isChange) {
      this.isChange = false;
      this.chartStr['plotOptions']['networkgraph']['layoutAlgorithm']['linkLength'] = this.distance;
      this.chartStr['series'][0]['marker']['radius'] = this.radius;
      this.chart = new Chart(this.chartStr as any);
    }
  }
  
  reloadDemo(): void {
    this.isChange = true;
    this.demoChartStr['plotOptions']['networkgraph']['layoutAlgorithm']['linkLength'] = this.distance;
    this.demoChartStr['series'][0]['marker']['radius'] = this.radius;
    this.demoChart = new Chart(this.demoChartStr as any);
  }

  hideLabel(): void {
    this.chartStr.series[0].data = JSON.parse(JSON.stringify(this.orginData));
    this.chartStr.series[0].nodes = JSON.parse(JSON.stringify(this.orginNode));
    // eslint-disable-next-line no-extra-boolean-cast
    if (!!this.hideLabels.length) {
      const blockIds = [];
      this.chartStr.series[0].nodes = this.chartStr.series[0].nodes.filter(item => {
        let isBlock = false;
        item.custom.detail.labels.forEach(label => {
          isBlock = this.hideLabels.includes(label) ? true : isBlock;
        });
        if (isBlock) {
          blockIds.push(item.custom.detail.id);
        }
        return !isBlock;
      });
      this.chartStr.series[0].data = this.chartStr.series[0].data.filter(item => {
        return !blockIds.includes(item.from) && !blockIds.includes(item.to);
      });
    }
    this.chart = new Chart(this.chartStr as any);
  }

这个搜索graph的代码,还新增了一个功能,就是可配置的blockList,配置的label将不会被显示在备选搜索label条件以及搜索出来的graph图中,代码中的 BLOCK_LIST 就是配置的需要block的一个Array.

getPathByNode(): void {
    const request = {};
    const properities = {};
    request['fromNodeLabels'] = this.searchPath['fromNodeLabel'];
    properities[this.searchPath['properity']] = this.searchPath['value'];
    request['properities'] = [properities];
    this.showLabels = [];
    this.service.getPathByProperies(request).subscribe(res => {
      const blockIds = [];
      this.chartStr.series[0].nodes = res.nodes.filter(item => {
        let isBlock = false;
        item.custom.detail.labels.forEach(label => {
          isBlock = BLOCK_LIST.includes(label) ? true : isBlock;
        });
        if (isBlock) {
          blockIds.push(item.custom.detail.id);
        }
        return !isBlock;
      }).map(item => {
        const label = item.custom.detail.labels[0];
        item['color'] = COLOR[label];
        this.showLabels = this.showLabels.concat(item.custom.detail.labels);
        return item;
      });
      this.chartStr.series[0].data = res.data.filter(item => {
        return !blockIds.includes(item.from) && !blockIds.includes(item.to);
      });
      this.showLabels = Array.from(new Set(this.showLabels)).map(labelStr => {
        return { label: labelStr, value: labelStr };
      })
      this.orginData = JSON.parse(JSON.stringify(this.chartStr.series[0].data));
      this.orginNode = JSON.parse(JSON.stringify(this.chartStr.series[0].nodes));
      this.chart = new Chart(this.chartStr as any);
      this.activeToast('success', 'Success', 'Load Success');
    }, error => {
      if (error.status === 404) {
        this.activeToast('warn', 'Warning', 'Can not found the node of this condition');
      } else {
        this.activeToast('error', 'Error', error.error.detail);
      }
    });
  }

下面的写法分别是为了,去重,深拷贝(关于关于这个写法,可以看我的另外的blog 去重 深拷贝)

Array.from(new Set(this.showLabels));

this.orginData = JSON.parse(JSON.stringify(this.chartStr.series[0].data));
this.orginNode = JSON.parse(JSON.stringify(this.chartStr.series[0].nodes));
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/955648.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号