赞
踩
最近有个需求,其中一个子需求就是从任意节点进入,拉出和他有关系的整个图,所以研究了下
关于介绍就去看这篇帖子吧https://blog.csdn.net/graphway/article/details/78957415
我们要使用的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/
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可以拉出所有关系
第二张图就是所有的图,打红圈的地方就是进入节点,也就是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的数据格式是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);
}
首先现在可以在一个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 { }
<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"> , </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;
}
}
<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));
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。