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<List< String >>这种结构
[
[‘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<String, Object> getAllPathToListByDynamicConditions(SearchPathDTO searchPathDTO) {
        Map<String, Object> map = new HashMap<>();
        List<Map<String, Object>> path = new ArrayList<>();
        List<NodeModel> nodes = new ArrayList<>();
        List<RelationshipModel> relationships = new ArrayList<>();
        Result result = this.nodeService.searchPath(searchPathDTO);
        List<Map<String, Object>> resultList = this.copyIterator(result.iterator());
        if (!resultList.isEmpty()) {
            nodes = (List<NodeModel>) resultList.get(0).get("nodes");
            try {
                relationships = (List<RelationshipModel>) resultList.get(0).get("relationships");
            } catch (ClassCastException e) {
                relationships = new ArrayList<>();
            }
            map.put("nodes", toNodeList(nodes));
            if (!relationships.isEmpty()) {
                relationships.forEach(item -> {
                    Map<String, Object> 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<String, Object> itemMap = new HashMap<>();
                    itemMap.put("from", res.getId());
                    itemMap.put("to", res.getId());
                    path.add(itemMap);
                });
            }
            map.put("data", path);
        }
        return map;
    }

    private List<Map<String, Object>> toNodeList(List<NodeModel> nodes) {
        List<Map<String, Object>> list = new ArrayList<>();
        nodes.forEach(item -> {
            Map<String, Object> 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 <T> List<T> copyIterator(Iterator<T> iter) {
        List<T> copy = new ArrayList<T>();
        while (iter.hasNext())
            copy.add(iter.next());
        return copy;
    }

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

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

    private String getNameFromPropertyList(List<Property<String, Object>> 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 + '<br>' + this.point.fromNode.id + '\u2192' + this.point.toNode.id;
        },
        formatter: function () {
          // return this.point.id + ' . ' + this.point.custom.view.view_name + '<br> ' + 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

<div id="dialog">
    <p-dialog [header]="'Node ['+dialogTitle+'] Detail'" [(visible)]="dialogDisplay" [style]="{width: '50%'}"
        [modal]="true" [responsive]="true" [maximizable]="true" [baseZIndex]="10000">

        <br>
        <table *ngIf="!!nodeDetail" class="table table-striped text-center table-bordered" style="table-layout: fixed;">
            <tbody>
                <tr>
                    <th colspan="1" scope="row">ID</th>
                    <td colspan="3">{{nodeDetail.id}}</td>
                </tr>
                <tr>
                    <th colspan="1" scope="row">Labels</th>
                    <td colspan="3">
                        <span *ngFor="let item of nodeDetail.labels;index as i">
                            <span *ngIf="i!==0">&nbsp;,&nbsp;</span>
                            <span>{{item}}</span>
                        </span>
                    </td>
                </tr>
            </tbody>
        </table>
        <br><br>
        <table *ngIf="!!nodeDetail" class="table text-center table-bordered" style="table-layout: fixed;">
            <tbody>
                <tr>
                    <th colspan="1" scope="row">Properties</th>
                    <td colspan="3" style="padding: 30px 20px;">
                        <p-table [value]="nodeDetail.propertyList">
                            <ng-template pTemplate="body" let-item>
                                <tr>
                                    <th colspan="1">{{item.key}}</th>
                                    <td colspan="3">{{item.value}}</td>
                                </tr>
                            </ng-template>
                        </p-table>
                    </td>
                </tr>
            </tbody>
        </table>
    </p-dialog>
</div>

这一段是在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

<p-overlayPanel #op [style]="{width: '450px',marginTop:'-4rem',padding:'2rem'}" (onHide)="mofidyChart()">
    <div>
        <h3 class="first">Node Radius: <strong>{{radius}}</strong> </h3>
        <p-slider [(ngModel)]="radius" (onChange)="reloadDemo()" [style]="{'width':'14em'}"></p-slider>
        <br><br>
        <h3 class="first">Node Distance: <strong>{{distance}}</strong> </h3>
        <p-slider [(ngModel)]="distance" (onChange)="reloadDemo()" [style]="{'width':'14em'}"></p-slider>
    </div>

    <div [chart]="demoChart"></div>

    <div>
        <h3 class="first">Hide</h3>
        <p-multiSelect  [options]="showLabels" standlone="true" [(ngModel)]="hideLabels"
            [style]="{minWidth:'175px'}" [filter]="true" (onChange)="hideLabel()"></p-multiSelect>
    </div>
</p-overlayPanel>
  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));
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值