最近有个需求,其中一个子需求就是从任意节点进入,拉出和他有关系的整个图,所以研究了下
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"> , </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));