Pre Merge pull request !6 from Ender_1024/master
|
@ -2,4 +2,4 @@
|
|||
ENV = 'development'
|
||||
|
||||
# base api
|
||||
VUE_APP_BASE_API = '/dev-api'
|
||||
VUE_APP_BASE_API = '/'
|
||||
|
|
|
@ -13,4 +13,4 @@ tests/**/coverage/
|
|||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sln
|
15
package.json
|
@ -16,13 +16,19 @@
|
|||
"dependencies": {
|
||||
"axios": "0.18.1",
|
||||
"core-js": "3.6.5",
|
||||
"element-ui": "2.13.2",
|
||||
"echarts": "^5.3.3",
|
||||
"echarts-wordcloud": "^2.0.0",
|
||||
"element-ui": "^2.13.2",
|
||||
"font-awesome": "^4.7.0",
|
||||
"js-cookie": "2.2.0",
|
||||
"jsmind": "^0.4.8",
|
||||
"neo4j-driver": "^4.2.2",
|
||||
"normalize.css": "7.0.0",
|
||||
"nprogress": "0.2.0",
|
||||
"path-to-regexp": "2.4.0",
|
||||
"vue": "2.6.10",
|
||||
"vue-router": "3.0.6",
|
||||
"vue-styled-components": "^1.6.0",
|
||||
"vuex": "3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -35,12 +41,13 @@
|
|||
"babel-eslint": "10.1.0",
|
||||
"babel-jest": "23.6.0",
|
||||
"babel-plugin-dynamic-import-node": "2.3.3",
|
||||
"coffee-loader": "^0.7.3",
|
||||
"coffee-script": "^1.12.7",
|
||||
"chalk": "2.4.2",
|
||||
"connect": "3.6.6",
|
||||
"eslint": "6.7.2",
|
||||
"eslint-plugin-vue": "6.2.2",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"mockjs": "1.0.1-beta3",
|
||||
"runjs": "4.3.2",
|
||||
"sass": "1.26.8",
|
||||
"sass-loader": "8.0.2",
|
||||
|
@ -48,7 +55,9 @@
|
|||
"serve-static": "1.13.2",
|
||||
"svg-sprite-loader": "4.1.3",
|
||||
"svgo": "1.2.2",
|
||||
"vue-template-compiler": "2.6.10"
|
||||
"vue-loader": "14.2.0",
|
||||
"vue-template-compiler": "2.6.10",
|
||||
"url-loader": "^0.5.8"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
|
Before Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 4.9 KiB |
|
@ -4,8 +4,10 @@
|
|||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.png">
|
||||
<title><%= webpackConfig.name %></title>
|
||||
<!-- <link rel="stylesheet" href="<%= BASE_URL %>common.css"> -->
|
||||
<script type="text/javascript" src="<%= BASE_URL %>d3.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
export function getFramework(params) {
|
||||
return request({
|
||||
url: '/job/framework',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function modifyFramework(data) {
|
||||
return request({
|
||||
url: '/job/framework',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getDepartmentDetail(params) {
|
||||
return request({
|
||||
url: '/job/department-detail',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function modifyDepartmentDetail(data) {
|
||||
return request({
|
||||
url: '/job/department-detail',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getJobDetail(params) {
|
||||
return request({
|
||||
url: '/job/job-detail',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function modifyJobDetail(data) {
|
||||
return request({
|
||||
url: '/job/job-detail',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function queryDepartmentJob() {
|
||||
return request({
|
||||
url: '/ahp/department-job/query',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function queryAhpFramework(params) {
|
||||
return request({
|
||||
url: '/ahp/framework/query',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function modifyAhpFramework(params) {
|
||||
return request({
|
||||
url: '/ahp/framework/save',
|
||||
method: 'post',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function queryScore(params) {
|
||||
return request({
|
||||
url: '/ahp/score/query',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function saveScore(params) {
|
||||
return request({
|
||||
url: '/ahp/score/save',
|
||||
method: 'post',
|
||||
params
|
||||
})
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
export function getPersonData(params) {
|
||||
return request({
|
||||
url: '/person/information2',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getPersonTag(params) {
|
||||
return request({
|
||||
url: '/person/tag',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getReviewPopulation(params) {
|
||||
return request({
|
||||
url: '/person/review/population',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getReviewEducation(params) {
|
||||
return request({
|
||||
url: '/person/review/education',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getReviewParty(params) {
|
||||
return request({
|
||||
url: '/person/review/party',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getReviewPerformance(params) {
|
||||
return request({
|
||||
url: '/person/review/performance',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getReviewLineup(params) {
|
||||
return request({
|
||||
url: '/person/review/lineup',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getReviewInnovation(params) {
|
||||
return request({
|
||||
url: '/person/review/innovation',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getReviewActivity(params) {
|
||||
return request({
|
||||
url: '/person/review/activity',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getReviewRoutine(params) {
|
||||
return request({
|
||||
url: '/person/review/routine',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getAppraise(params) {
|
||||
return request({
|
||||
url: '/person/appraise',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getHonor(params) {
|
||||
return request({
|
||||
url: '/person/honor',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getAdvance(params) {
|
||||
return request({
|
||||
url: '/person/advance',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getInnovation(params) {
|
||||
return request({
|
||||
url: '/person/innovation',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getArticle(params) {
|
||||
return request({
|
||||
url: '/person/article',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getActivity(params) {
|
||||
return request({
|
||||
url: '/person/activity',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getRoutine(params) {
|
||||
return request({
|
||||
url: '/person/routine',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getParty(params) {
|
||||
return request({
|
||||
url: '/person/party',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function selectBasicInfo(params) {
|
||||
return request({
|
||||
url: '/person/information',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function modifyBasicInfo(data) {
|
||||
return request({
|
||||
url: '/person/information/modify',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function addBasicInfo(data) {
|
||||
return request({
|
||||
url: '/person/information/add',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteBasicInfo(params) {
|
||||
return request({
|
||||
url: '/person/information/delete',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getBasicTips(params) {
|
||||
return request({
|
||||
url: '/person/information/tips',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
export function getPriority(params) {
|
||||
return request({
|
||||
url: '/priority/select',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function modifyPriority(data) {
|
||||
return request({
|
||||
url: '/priority/modify',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function addUser(data) {
|
||||
return request({
|
||||
url: '/priority/add-user',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteUser(data) {
|
||||
return request({
|
||||
url: '/priority/delete-user',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function verifyJobPriority(params) {
|
||||
return request({
|
||||
url: '/priority/verify',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params) {
|
||||
return request({
|
||||
url: '/vue-admin-template/table/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
|
@ -2,7 +2,7 @@ import request from '@/utils/request'
|
|||
|
||||
export function login(data) {
|
||||
return request({
|
||||
url: '/vue-admin-template/user/login',
|
||||
url: '/user/login2',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
|
@ -10,7 +10,7 @@ export function login(data) {
|
|||
|
||||
export function getInfo(token) {
|
||||
return request({
|
||||
url: '/vue-admin-template/user/info',
|
||||
url: '/user/info',
|
||||
method: 'get',
|
||||
params: { token }
|
||||
})
|
||||
|
@ -18,7 +18,15 @@ export function getInfo(token) {
|
|||
|
||||
export function logout() {
|
||||
return request({
|
||||
url: '/vue-admin-template/user/logout',
|
||||
url: '/user/logout',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function changePassword(data) {
|
||||
return request({
|
||||
url: '/user/change-password',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* fullPage 2.4.6
|
||||
* https://github.com/alvarotrigo/fullPage.js
|
||||
* MIT licensed
|
||||
*
|
||||
* Copyright (C) 2013 alvarotrigo.com - A project by Alvaro Trigo
|
||||
*/
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow:hidden;
|
||||
|
||||
/*Avoid flicker on slides transitions for mobile phones #336 */
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
}
|
||||
#superContainer {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
/* Touch detection for Windows 8 */
|
||||
-ms-touch-action: none;
|
||||
|
||||
/* IE 11 on Windows Phone 8.1*/
|
||||
touch-action: none;
|
||||
}
|
||||
.fp-section {
|
||||
position: relative;
|
||||
-webkit-box-sizing: border-box; /* Safari<=5 Android<=3 */
|
||||
-moz-box-sizing: border-box; /* <=28 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.fp-slide {
|
||||
float: left;
|
||||
}
|
||||
.fp-slide, .fp-slidesContainer {
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.fp-slides {
|
||||
z-index:1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
-webkit-transition: all 0.3s ease-out; /* Safari<=6 Android<=4.3 */
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
.fp-section.fp-table, .fp-slide.fp-table {
|
||||
display: table;
|
||||
table-layout:fixed;
|
||||
width: 100%;
|
||||
}
|
||||
.fp-tableCell {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.fp-slidesContainer {
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
.fp-controlArrow {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
top: 50%;
|
||||
cursor: pointer;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
margin-top: -38px;
|
||||
}
|
||||
.fp-controlArrow.fp-prev {
|
||||
left: 15px;
|
||||
width: 0;
|
||||
border-width: 38.5px 34px 38.5px 0;
|
||||
border-color: transparent #fff transparent transparent;
|
||||
}
|
||||
.fp-controlArrow.fp-next {
|
||||
right: 15px;
|
||||
border-width: 38.5px 0 38.5px 34px;
|
||||
border-color: transparent transparent transparent #fff;
|
||||
}
|
||||
.fp-scrollable {
|
||||
overflow: scroll;
|
||||
|
||||
}
|
||||
.fp-notransition {
|
||||
-webkit-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
#fp-nav {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
margin-top: -32px;
|
||||
top: 50%;
|
||||
opacity: 1;
|
||||
}
|
||||
#fp-nav.right {
|
||||
right: 17px;
|
||||
}
|
||||
#fp-nav.left {
|
||||
left: 17px;
|
||||
}
|
||||
.fp-slidesNav{
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
left: 50%;
|
||||
opacity: 1;
|
||||
}
|
||||
.fp-slidesNav.bottom {
|
||||
bottom: 17px;
|
||||
}
|
||||
.fp-slidesNav.top {
|
||||
top: 17px;
|
||||
}
|
||||
#fp-nav ul,
|
||||
.fp-slidesNav ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#fp-nav ul li,
|
||||
.fp-slidesNav ul li {
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 13px;
|
||||
margin: 7px;
|
||||
position:relative;
|
||||
}
|
||||
.fp-slidesNav ul li {
|
||||
display: inline-block;
|
||||
}
|
||||
#fp-nav ul li a,
|
||||
.fp-slidesNav ul li a {
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
#fp-nav ul li a.active span,
|
||||
.fp-slidesNav ul li a.active span {
|
||||
background: #333;
|
||||
}
|
||||
#fp-nav ul li a span,
|
||||
.fp-slidesNav ul li a span {
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border: 1px solid #000;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
#fp-nav ul li .fp-tooltip {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-family: arial, helvetica, sans-serif;
|
||||
white-space: nowrap;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
#fp-nav ul li:hover .fp-tooltip {
|
||||
-webkit-transition: opacity 0.2s ease-in;
|
||||
transition: opacity 0.2s ease-in;
|
||||
width: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
#fp-nav ul li .fp-tooltip.right {
|
||||
right: 20px;
|
||||
}
|
||||
#fp-nav ul li .fp-tooltip.left {
|
||||
left: 20px;
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Released under BSD License
|
||||
* Copyright (c) 2014-2021 hizzgdev@163.com
|
||||
*
|
||||
* Project Home:
|
||||
* https://github.com/hizzgdev/jsmind/
|
||||
*/
|
||||
|
||||
/* important section */
|
||||
.jsmind-inner{position:relative;overflow: hidden auto;width:100%;height:100%;outline:none;}/*box-shadow:0 0 2px #000;*/
|
||||
.jsmind-inner{
|
||||
moz-user-select:-moz-none;
|
||||
-moz-user-select:none;
|
||||
-o-user-select:none;
|
||||
-khtml-user-select:none;
|
||||
-webkit-user-select:none;
|
||||
-ms-user-select:none;
|
||||
user-select:none;
|
||||
}
|
||||
|
||||
/* z-index:1 */
|
||||
svg.jsmind{position:absolute;z-index:1;}
|
||||
canvas.jsmind{position:absolute;z-index:1;}
|
||||
|
||||
/* z-index:2 */
|
||||
jmnodes{position:absolute;z-index:2;background-color:rgba(0,0,0,0);}/*background color is necessary*/
|
||||
jmnode{position:absolute;cursor:default;max-width:400px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
jmexpander{position:absolute;width:11px;height:11px;display:block;overflow:hidden;line-height:12px;font-size:12px;text-align:center;border-radius:6px;border-width:1px;border-style:solid;cursor:pointer;}
|
||||
|
||||
/* default theme */
|
||||
jmnode{padding:10px;background-color:#fff;color:#333;border-radius:5px;box-shadow:1px 1px 3px #bfbfbf;font:16px/1.125 Verdana,Arial,Helvetica,sans-serif;}
|
||||
jmnode:hover{box-shadow:2px 2px 8px #999;background-color:#ebebeb;color:#333;}
|
||||
jmnode.selected{background-color:#ebebeb;color:#333;box-shadow:2px 2px 8px #999;}
|
||||
jmnode.root{font-size:24px;}
|
||||
jmexpander{border-color:gray;}
|
||||
jmexpander:hover{border-color:#000;}
|
||||
|
||||
@media screen and (max-device-width: 1024px) {
|
||||
jmnode{padding:5px;border-radius:3px;font-size:14px;}
|
||||
jmnode.root{font-size:21px;}
|
||||
}
|
||||
/* primary theme */
|
||||
jmnodes.theme-primary jmnode{background-color:#428bca;color:#fff;border-color:#357ebd;}
|
||||
jmnodes.theme-primary jmnode:hover{background-color:#3276b1;border-color:#285e8e;}
|
||||
jmnodes.theme-primary jmnode.selected{background-color:#f1c40f;color:#fff;}
|
||||
jmnodes.theme-primary jmnode.root{}
|
||||
jmnodes.theme-primary jmexpander{}
|
||||
jmnodes.theme-primary jmexpander:hover{}
|
||||
|
||||
/* warning theme */
|
||||
jmnodes.theme-warning jmnode{background-color:#f0ad4e;border-color:#eea236;color:#fff;}
|
||||
jmnodes.theme-warning jmnode:hover{background-color:#ed9c28;border-color:#d58512;}
|
||||
jmnodes.theme-warning jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-warning jmnode.root{}
|
||||
jmnodes.theme-warning jmexpander{}
|
||||
jmnodes.theme-warning jmexpander:hover{}
|
||||
|
||||
/* danger theme */
|
||||
jmnodes.theme-danger jmnode{background-color:#d9534f;border-color:#d43f3a;color:#fff;}
|
||||
jmnodes.theme-danger jmnode:hover{background-color:#d2322d;border-color:#ac2925;}
|
||||
jmnodes.theme-danger jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-danger jmnode.root{}
|
||||
jmnodes.theme-danger jmexpander{}
|
||||
jmnodes.theme-danger jmexpander:hover{}
|
||||
|
||||
/* success theme */
|
||||
jmnodes.theme-success jmnode{background-color:#5cb85c;border-color:#4cae4c;color:#fff;}
|
||||
jmnodes.theme-success jmnode:hover{background-color:#47a447;border-color:#398439;}
|
||||
jmnodes.theme-success jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-success jmnode.root{}
|
||||
jmnodes.theme-success jmexpander{}
|
||||
jmnodes.theme-success jmexpander:hover{}
|
||||
|
||||
/* info theme */
|
||||
jmnodes.theme-info jmnode{background-color:#5dc0de;border-color:#46b8da;;color:#fff;}
|
||||
jmnodes.theme-info jmnode:hover{background-color:#39b3d7;border-color:#269abc;}
|
||||
jmnodes.theme-info jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-info jmnode.root{}
|
||||
jmnodes.theme-info jmexpander{}
|
||||
jmnodes.theme-info jmexpander:hover{}
|
||||
|
||||
/* greensea theme */
|
||||
jmnodes.theme-greensea jmnode{background-color:#1abc9c;color:#fff;}
|
||||
jmnodes.theme-greensea jmnode:hover{background-color:#16a085;}
|
||||
jmnodes.theme-greensea jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-greensea jmnode.root{}
|
||||
jmnodes.theme-greensea jmexpander{}
|
||||
jmnodes.theme-greensea jmexpander:hover{}
|
||||
|
||||
/* nephrite theme */
|
||||
jmnodes.theme-nephrite jmnode{background-color:#2ecc71;color:#fff;}
|
||||
jmnodes.theme-nephrite jmnode:hover{background-color:#27ae60;}
|
||||
jmnodes.theme-nephrite jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-nephrite jmnode.root{}
|
||||
jmnodes.theme-nephrite jmexpander{}
|
||||
jmnodes.theme-nephrite jmexpander:hover{}
|
||||
|
||||
/* belizehole theme */
|
||||
jmnodes.theme-belizehole jmnode{background-color:#3498db;color:#fff;}
|
||||
jmnodes.theme-belizehole jmnode:hover{background-color:#2980b9;}
|
||||
jmnodes.theme-belizehole jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-belizehole jmnode.root{}
|
||||
jmnodes.theme-belizehole jmexpander{}
|
||||
jmnodes.theme-belizehole jmexpander:hover{}
|
||||
|
||||
/* wisteria theme */
|
||||
jmnodes.theme-wisteria jmnode{background-color:#9b59b6;color:#fff;}
|
||||
jmnodes.theme-wisteria jmnode:hover{background-color:#8e44ad;}
|
||||
jmnodes.theme-wisteria jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-wisteria jmnode.root{}
|
||||
jmnodes.theme-wisteria jmexpander{}
|
||||
jmnodes.theme-wisteria jmexpander:hover{}
|
||||
|
||||
/* asphalt theme */
|
||||
jmnodes.theme-asphalt jmnode{background-color:#34495e;color:#fff;}
|
||||
jmnodes.theme-asphalt jmnode:hover{background-color:#2c3e50;}
|
||||
jmnodes.theme-asphalt jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-asphalt jmnode.root{}
|
||||
jmnodes.theme-asphalt jmexpander{}
|
||||
jmnodes.theme-asphalt jmexpander:hover{}
|
||||
|
||||
/* orange theme */
|
||||
jmnodes.theme-orange jmnode{background-color:#f1c40f;color:#fff;}
|
||||
jmnodes.theme-orange jmnode:hover{background-color:#f39c12;}
|
||||
jmnodes.theme-orange jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-orange jmnode.root{}
|
||||
jmnodes.theme-orange jmexpander{}
|
||||
jmnodes.theme-orange jmexpander:hover{}
|
||||
|
||||
/* pumpkin theme */
|
||||
jmnodes.theme-pumpkin jmnode{background-color:#e67e22;color:#fff;}
|
||||
jmnodes.theme-pumpkin jmnode:hover{background-color:#d35400;}
|
||||
jmnodes.theme-pumpkin jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-pumpkin jmnode.root{}
|
||||
jmnodes.theme-pumpkin jmexpander{}
|
||||
jmnodes.theme-pumpkin jmexpander:hover{}
|
||||
|
||||
/* pomegranate theme */
|
||||
jmnodes.theme-pomegranate jmnode{background-color:#e74c3c;color:#fff;}
|
||||
jmnodes.theme-pomegranate jmnode:hover{background-color:#c0392b;}
|
||||
jmnodes.theme-pomegranate jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-pomegranate jmnode.root{}
|
||||
jmnodes.theme-pomegranate jmexpander{}
|
||||
jmnodes.theme-pomegranate jmexpander:hover{}
|
||||
|
||||
/* clouds theme */
|
||||
jmnodes.theme-clouds jmnode{background-color:#ecf0f1;color:#333;}
|
||||
jmnodes.theme-clouds jmnode:hover{background-color:#bdc3c7;}
|
||||
jmnodes.theme-clouds jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-clouds jmnode.root{}
|
||||
jmnodes.theme-clouds jmexpander{}
|
||||
jmnodes.theme-clouds jmexpander:hover{}
|
||||
|
||||
/* asbestos theme */
|
||||
jmnodes.theme-asbestos jmnode{background-color:#95a5a6;color:#fff;}
|
||||
jmnodes.theme-asbestos jmnode:hover{background-color:#7f8c8d;}
|
||||
jmnodes.theme-asbestos jmnode.selected{background-color:#11f;color:#fff;}
|
||||
jmnodes.theme-asbestos jmnode.root{}
|
||||
jmnodes.theme-asbestos jmexpander{}
|
||||
jmnodes.theme-asbestos jmexpander:hover{}
|
|
@ -0,0 +1,108 @@
|
|||
.right-main {
|
||||
/* padding-top: 10px; */
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
.jd-title {
|
||||
text-align: center;
|
||||
/* font-family: "Times New Roman","宋体","微软雅黑"; */
|
||||
font-size: 2em;
|
||||
/* line-height: 2em; */
|
||||
}
|
||||
|
||||
.jd-h1 {
|
||||
font-size: 1.5em;
|
||||
line-height: 1.8em;
|
||||
}
|
||||
|
||||
.jd-h2 {
|
||||
font-size: 1.3em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.jd-h3 {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
text-indent: 1em;
|
||||
}
|
||||
|
||||
.jd-text {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5em;
|
||||
text-indent: 2em;
|
||||
}
|
||||
|
||||
.jd-li {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5em;
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.jd-li2 {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.jd-block1 {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.jd-block2 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.jd-block3 {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.jd-table {
|
||||
border-collapse: collapse;
|
||||
width: 80%;
|
||||
margin: 10px auto 0px;
|
||||
}
|
||||
|
||||
.jd-table tr {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.jd-table table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ebebeb;
|
||||
}
|
||||
|
||||
.jd-table td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.jd-table2 td {
|
||||
padding-left: 1em;
|
||||
text-align: left;
|
||||
/* text-indent: 1em; */
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.vertical-one {
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.vertical-two {
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
.vertical-four {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.confirm-button {
|
||||
background: #fff;
|
||||
color:#000;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.confirm-button:hover {
|
||||
color: #409eff;
|
||||
background-color: #ecf5ff;
|
||||
}
|
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* fullPage 2.5.4
|
||||
* https://github.com/alvarotrigo/fullPage.js
|
||||
* MIT licensed
|
||||
*
|
||||
* Copyright (C) 2013 alvarotrigo.com - A project by Alvaro Trigo
|
||||
*/
|
||||
(function(b){b.fn.fullpage=function(c){function pa(a){a.find(".fp-slides").after('<div class="fp-controlArrow fp-prev"></div><div class="fp-controlArrow fp-next"></div>');"#fff"!=c.controlArrowColor&&(a.find(".fp-controlArrow.fp-next").css("border-color","transparent transparent transparent "+c.controlArrowColor),a.find(".fp-controlArrow.fp-prev").css("border-color","transparent "+c.controlArrowColor+" transparent transparent"));c.loopHorizontal||a.find(".fp-controlArrow.fp-prev").hide()}function qa(){b("body").append('<div id="fp-nav"><ul></ul></div>');
|
||||
k=b("#fp-nav");k.css("color",c.navigationColor);k.addClass(c.navigationPosition);for(var a=0;a<b(".fp-section").length;a++){var d="";c.anchors.length&&(d=c.anchors[a]);var d='<li><a href="#'+d+'"><span></span></a>',e=c.navigationTooltips[a];void 0!=e&&""!=e&&(d+='<div class="fp-tooltip '+c.navigationPosition+'">'+e+"</div>");d+="</li>";k.find("ul").append(d)}}function R(){b(".fp-section").each(function(){var a=b(this).find(".fp-slide");a.length?a.each(function(){y(b(this))}):y(b(this))});b.isFunction(c.afterRender)&&
|
||||
c.afterRender.call(this)}function S(){if(!c.autoScrolling||c.scrollBar){var a=b(window).scrollTop(),d=0,e=Math.abs(a-b(".fp-section").first().offset().top);b(".fp-section").each(function(c){var f=Math.abs(a-b(this).offset().top);f<e&&(d=c,e=f)});var f=b(".fp-section").eq(d)}if(!c.autoScrolling&&!f.hasClass("active")){F=!0;var ra=b(".fp-section.active").index(".fp-section")+1,g=G(f),h=f.data("anchor"),k=f.index(".fp-section")+1,l=f.find(".fp-slide.active");if(l.length)var n=l.data("anchor"),t=l.index();
|
||||
f.addClass("active").siblings().removeClass("active");m||(b.isFunction(c.onLeave)&&c.onLeave.call(this,ra,k,g),b.isFunction(c.afterLoad)&&c.afterLoad.call(this,h,k));H(h,0);c.anchors.length&&!m&&(p=h,I(t,n,h,k));clearTimeout(T);T=setTimeout(function(){F=!1},100)}c.scrollBar&&(clearTimeout(U),U=setTimeout(function(){m||q(f)},1E3))}function V(a){return scrollable=a.find(".fp-slides").length?a.find(".fp-slide.active").find(".fp-scrollable"):a.find(".fp-scrollable")}function z(a,d){if(l[a]){if("down"==
|
||||
a)var c="bottom",f=b.fn.fullpage.moveSectionDown;else c="top",f=b.fn.fullpage.moveSectionUp;if(0<d.length)if(c="top"===c?!d.scrollTop():"bottom"===c?d.scrollTop()+1+d.innerHeight()>=d[0].scrollHeight:void 0,c)f();else return!0;else f()}}function sa(a){var d=a.originalEvent;if(!W(a.target)){c.autoScrolling&&!c.scrollBar&&a.preventDefault();a=b(".fp-section.active");var e=V(a);m||t||(d=X(d),u=d.y,A=d.x,a.find(".fp-slides").length&&Math.abs(B-A)>Math.abs(v-u)?Math.abs(B-A)>b(window).width()/100*c.touchSensitivity&&
|
||||
(B>A?l.right&&b.fn.fullpage.moveSlideRight():l.left&&b.fn.fullpage.moveSlideLeft()):c.autoScrolling&&!c.scrollBar&&Math.abs(v-u)>b(window).height()/100*c.touchSensitivity&&(v>u?z("down",e):u>v&&z("up",e)))}}function W(a,d){d=d||0;var e=b(a).parent();return d<c.normalScrollElementTouchThreshold&&e.is(c.normalScrollElements)?!0:d==c.normalScrollElementTouchThreshold?!1:W(e,++d)}function ta(a){a=X(a.originalEvent);v=a.y;B=a.x}function r(a){if(c.autoScrolling){a=window.event||a;var d=Math.max(-1,Math.min(1,
|
||||
a.wheelDelta||-a.deltaY||-a.detail));c.scrollBar&&(a.preventDefault?a.preventDefault():a.returnValue=!1);a=b(".fp-section.active");a=V(a);m||(0>d?z("down",a):z("up",a));return!1}}function Y(a){var d=b(".fp-section.active").find(".fp-slides");if(d.length&&!t){var e=d.find(".fp-slide.active"),f=null,f="prev"===a?e.prev(".fp-slide"):e.next(".fp-slide");if(!f.length){if(!c.loopHorizontal)return;f="prev"===a?e.siblings(":last"):e.siblings(":first")}t=!0;w(d,f)}}function Z(){b(".fp-slide.active").each(function(){J(b(this))})}
|
||||
function q(a,d,e){var f=a.position();if("undefined"!==typeof f&&(d={element:a,callback:d,isMovementUp:e,dest:f,dtop:f.top,yMovement:G(a),anchorLink:a.data("anchor"),sectionIndex:a.index(".fp-section"),activeSlide:a.find(".fp-slide.active"),activeSection:b(".fp-section.active"),leavingSection:b(".fp-section.active").index(".fp-section")+1,localIsResizing:x},!(d.activeSection.is(a)&&!x||c.scrollBar&&b(window).scrollTop()===d.dtop))){if(d.activeSlide.length)var g=d.activeSlide.data("anchor"),h=d.activeSlide.index();
|
||||
c.autoScrolling&&c.continuousVertical&&"undefined"!==typeof d.isMovementUp&&(!d.isMovementUp&&"up"==d.yMovement||d.isMovementUp&&"down"==d.yMovement)&&(d.isMovementUp?b(".fp-section.active").before(d.activeSection.nextAll(".fp-section")):b(".fp-section.active").after(d.activeSection.prevAll(".fp-section").get().reverse()),n(b(".fp-section.active").position().top),Z(),d.wrapAroundElements=d.activeSection,d.dest=d.element.position(),d.dtop=d.dest.top,d.yMovement=G(d.element));a.addClass("active").siblings().removeClass("active");
|
||||
m=!0;I(h,g,d.anchorLink,d.sectionIndex);b.isFunction(c.onLeave)&&!d.localIsResizing&&c.onLeave.call(this,d.leavingSection,d.sectionIndex+1,d.yMovement);ua(d);p=d.anchorLink;c.autoScrolling&&H(d.anchorLink,d.sectionIndex)}}function ua(a){if(c.css3&&c.autoScrolling&&!c.scrollBar)aa("translate3d(0px, -"+a.dtop+"px, 0px)",!0),setTimeout(function(){ba(a)},c.scrollingSpeed);else{var d=va(a);b(d.element).animate(d.options,c.scrollingSpeed,c.easing).promise().done(function(){ba(a)})}}function va(a){var b=
|
||||
{};c.autoScrolling&&!c.scrollBar?(b.options={top:-a.dtop},b.element="."+ca):(b.options={scrollTop:a.dtop},b.element="html, body");return b}function wa(a){a.wrapAroundElements&&a.wrapAroundElements.length&&(a.isMovementUp?b(".fp-section:first").before(a.wrapAroundElements):b(".fp-section:last").after(a.wrapAroundElements),n(b(".fp-section.active").position().top),Z())}function ba(a){wa(a);b.isFunction(c.afterLoad)&&!a.localIsResizing&&c.afterLoad.call(this,a.anchorLink,a.sectionIndex+1);setTimeout(function(){m=
|
||||
!1;b.isFunction(a.callback)&&a.callback.call(this)},600)}function da(){if(!F){var a=window.location.hash.replace("#","").split("/"),b=a[0],a=a[1];if(b.length){var c="undefined"===typeof p,f="undefined"===typeof p&&"undefined"===typeof a&&!t;(b&&b!==p&&!c||f||!t&&K!=a)&&L(b,a)}}}function w(a,d){var e=d.position(),f=a.find(".fp-slidesContainer").parent(),g=d.index(),h=a.closest(".fp-section"),k=h.index(".fp-section"),l=h.data("anchor"),n=h.find(".fp-slidesNav"),m=d.data("anchor"),q=x;if(c.onSlideLeave){var p=
|
||||
h.find(".fp-slide.active").index(),r;r=p==g?"none":p>g?"left":"right";q||"none"===r||b.isFunction(c.onSlideLeave)&&c.onSlideLeave.call(this,l,k+1,p,r)}d.addClass("active").siblings().removeClass("active");"undefined"===typeof m&&(m=g);!c.loopHorizontal&&c.controlArrows&&(h.find(".fp-controlArrow.fp-prev").toggle(0!=g),h.find(".fp-controlArrow.fp-next").toggle(!d.is(":last-child")));h.hasClass("active")&&I(g,m,l,k);var u=function(){q||b.isFunction(c.afterSlideLoad)&&c.afterSlideLoad.call(this,l,k+
|
||||
1,m,g);t=!1};c.css3?(e="translate3d(-"+e.left+"px, 0px, 0px)",ea(a.find(".fp-slidesContainer"),0<c.scrollingSpeed).css(fa(e)),setTimeout(function(){u()},c.scrollingSpeed,c.easing)):f.animate({scrollLeft:e.left},c.scrollingSpeed,c.easing,function(){u()});n.find(".active").removeClass("active");n.find("li").eq(g).find("a").addClass("active")}function ga(){ha();if(C){if("text"!==b(document.activeElement).attr("type")){var a=b(window).height();Math.abs(a-M)>20*Math.max(M,a)/100&&(b.fn.fullpage.reBuild(!0),
|
||||
M=a)}}else clearTimeout(ia),ia=setTimeout(function(){b.fn.fullpage.reBuild(!0)},500)}function ha(){if(c.responsive){var a=g.hasClass("fp-responsive");b(window).width()<c.responsive?a||(b.fn.fullpage.setAutoScrolling(!1,"internal"),b("#fp-nav").hide(),g.addClass("fp-responsive")):a&&(b.fn.fullpage.setAutoScrolling(N.autoScrolling,"internal"),b("#fp-nav").show(),g.removeClass("fp-responsive"))}}function ea(a){var b="all "+c.scrollingSpeed+"ms "+c.easingcss3;a.removeClass("fp-notransition");return a.css({"-webkit-transition":b,
|
||||
transition:b})}function O(a){return a.addClass("fp-notransition")}function xa(a,d){if(825>a||900>d){var c=Math.min(100*a/825,100*d/900).toFixed(2);b("body").css("font-size",c+"%")}else b("body").css("font-size","100%")}function H(a,d){c.menu&&(b(c.menu).find(".active").removeClass("active"),b(c.menu).find('[data-menuanchor="'+a+'"]').addClass("active"));c.navigation&&(b("#fp-nav").find(".active").removeClass("active"),a?b("#fp-nav").find('a[href="#'+a+'"]').addClass("active"):b("#fp-nav").find("li").eq(d).find("a").addClass("active"))}
|
||||
function G(a){var d=b(".fp-section.active").index(".fp-section");a=a.index(".fp-section");return d==a?"none":d>a?"up":"down"}function y(a){a.css("overflow","hidden");var b=a.closest(".fp-section"),e=a.find(".fp-scrollable");if(e.length)var f=e.get(0).scrollHeight;else f=a.get(0).scrollHeight,c.verticalCentered&&(f=a.find(".fp-tableCell").get(0).scrollHeight);b=h-parseInt(b.css("padding-bottom"))-parseInt(b.css("padding-top"));f>b?e.length?e.css("height",b+"px").parent().css("height",b+"px"):(c.verticalCentered?
|
||||
a.find(".fp-tableCell").wrapInner('<div class="fp-scrollable" />'):a.wrapInner('<div class="fp-scrollable" />'),a.find(".fp-scrollable").slimScroll({allowPageScroll:!0,height:b+"px",size:"10px",alwaysVisible:!0})):ja(a);a.css("overflow","")}function ja(a){a.find(".fp-scrollable").children().first().unwrap().unwrap();a.find(".slimScrollBar").remove();a.find(".slimScrollRail").remove()}function ka(a){a.addClass("fp-table").wrapInner('<div class="fp-tableCell" style="height:'+la(a)+'px;" />')}function la(a){var b=
|
||||
h;if(c.paddingTop||c.paddingBottom)b=a,b.hasClass("fp-section")||(b=a.closest(".fp-section")),a=parseInt(b.css("padding-top"))+parseInt(b.css("padding-bottom")),b=h-a;return b}function aa(a,b){b?ea(g):O(g);g.css(fa(a));setTimeout(function(){g.removeClass("fp-notransition")},10)}function L(a,d){"undefined"===typeof d&&(d=0);var c=isNaN(a)?b('[data-anchor="'+a+'"]'):b(".fp-section").eq(a-1);a===p||c.hasClass("active")?ma(c,d):q(c,function(){ma(c,d)})}function ma(a,b){if("undefined"!=typeof b){var c=
|
||||
a.find(".fp-slides"),f=c.find('[data-anchor="'+b+'"]');f.length||(f=c.find(".fp-slide").eq(b));f.length&&w(c,f)}}function ya(a,b){a.append('<div class="fp-slidesNav"><ul></ul></div>');var e=a.find(".fp-slidesNav");e.addClass(c.slidesNavPosition);for(var f=0;f<b;f++)e.find("ul").append('<li><a href="#"><span></span></a></li>');e.css("margin-left","-"+e.width()/2+"px");e.find("li").first().find("a").addClass("active")}function I(a,b,e,f){var g="";c.anchors.length?(a?("undefined"!==typeof e&&(g=e),"undefined"===
|
||||
typeof b&&(b=a),K=b,na(g+"/"+b)):("undefined"!==typeof a&&(K=b),na(e)),D(location.hash)):"undefined"!==typeof a?D(f+"-"+a):D(String(f))}function na(a){if(c.recordHistory)location.hash=a;else if(C||P)history.replaceState(void 0,void 0,"#"+a);else{var b=window.location.href.split("#")[0];window.location.replace(b+"#"+a)}}function D(a){a=a.replace("/","-").replace("#","");b("body")[0].className=b("body")[0].className.replace(/\b\s?fp-viewing-[^\s]+\b/g,"");b("body").addClass("fp-viewing-"+a)}function za(){var a=
|
||||
document.createElement("p"),b,c={webkitTransform:"-webkit-transform",OTransform:"-o-transform",msTransform:"-ms-transform",MozTransform:"-moz-transform",transform:"transform"};document.body.insertBefore(a,null);for(var f in c)void 0!==a.style[f]&&(a.style[f]="translate3d(1px,1px,1px)",b=window.getComputedStyle(a).getPropertyValue(c[f]));document.body.removeChild(a);return void 0!==b&&0<b.length&&"none"!==b}function oa(){return window.PointerEvent?{down:"pointerdown",move:"pointermove"}:{down:"MSPointerDown",
|
||||
move:"MSPointerMove"}}function X(a){var b=[];b.y="undefined"!==typeof a.pageY&&(a.pageY||a.pageX)?a.pageY:a.touches[0].pageY;b.x="undefined"!==typeof a.pageX&&(a.pageY||a.pageX)?a.pageX:a.touches[0].pageX;return b}function J(a){b.fn.fullpage.setScrollingSpeed(0,"internal");w(a.closest(".fp-slides"),a);b.fn.fullpage.setScrollingSpeed(N.scrollingSpeed,"internal")}function n(a){c.scrollBar?g.scrollTop(a):c.css3?aa("translate3d(0px, -"+a+"px, 0px)",!1):g.css("top",-a)}function fa(a){return{"-webkit-transform":a,
|
||||
"-moz-transform":a,"-ms-transform":a,transform:a}}function Aa(){n(0);b("#fp-nav, .fp-slidesNav, .fp-controlArrow").remove();b(".fp-section").css({height:"","background-color":"",padding:""});b(".fp-slide").css({width:""});g.css({height:"",position:"","-ms-touch-action":"","touch-action":""});b(".fp-section, .fp-slide").each(function(){ja(b(this));b(this).removeClass("fp-table active")});O(g);O(g.find(".fp-easing"));g.find(".fp-tableCell, .fp-slidesContainer, .fp-slides").each(function(){b(this).replaceWith(this.childNodes)});
|
||||
b("html, body").scrollTop(0)}function Q(a,b,e){c[a]=b;"internal"!==e&&(N[a]=b)}function E(a,b){console&&console[a]&&console[a]("fullPage: "+b)}c=b.extend({menu:!1,anchors:[],navigation:!1,navigationPosition:"right",navigationColor:"#000",navigationTooltips:[],slidesNavigation:!1,slidesNavPosition:"bottom",scrollBar:!1,css3:!0,scrollingSpeed:700,autoScrolling:!0,easing:"easeInQuart",easingcss3:"ease",loopBottom:!1,loopTop:!1,loopHorizontal:!0,continuousVertical:!1,normalScrollElements:null,scrollOverflow:!1,
|
||||
touchSensitivity:5,normalScrollElementTouchThreshold:5,keyboardScrolling:!0,animateAnchor:!0,recordHistory:!0,controlArrows:!0,controlArrowColor:"#fff",verticalCentered:!0,resize:!0,sectionsColor:[],paddingTop:0,paddingBottom:0,fixedElements:null,responsive:0,sectionSelector:".section",slideSelector:".slide",afterLoad:null,onLeave:null,afterRender:null,afterResize:null,afterReBuild:null,afterSlideLoad:null,onSlideLeave:null},c);(function(){c.continuousVertical&&(c.loopTop||c.loopBottom)&&(c.continuousVertical=
|
||||
!1,E("warn","Option `loopTop/loopBottom` is mutually exclusive with `continuousVertical`; `continuousVertical` disabled"));c.continuousVertical&&c.scrollBar&&(c.continuousVertical=!1,E("warn","Option `scrollBar` is mutually exclusive with `continuousVertical`; `continuousVertical` disabled"));b.each(c.anchors,function(a,c){(b("#"+c).length||b('[name="'+c+'"]').length)&&E("error","data-anchor tags can not have the same value as any `id` element on the site (or `name` element for IE).")})})();b.extend(b.easing,
|
||||
{easeInQuart:function(a,b,c,f,g){return f*(b/=g)*b*b*b+c}});b.fn.fullpage.setAutoScrolling=function(a,d){Q("autoScrolling",a,d);var e=b(".fp-section.active");c.autoScrolling&&!c.scrollBar?(b("html, body").css({overflow:"hidden",height:"100%"}),b.fn.fullpage.setRecordHistory(c.recordHistory,"internal"),g.css({"-ms-touch-action":"none","touch-action":"none"}),e.length&&n(e.position().top)):(b("html, body").css({overflow:"visible",height:"initial"}),b.fn.fullpage.setRecordHistory(!1,"internal"),g.css({"-ms-touch-action":"",
|
||||
"touch-action":""}),n(0),b("html, body").scrollTop(e.position().top))};b.fn.fullpage.setRecordHistory=function(a,b){Q("recordHistory",a,b)};b.fn.fullpage.setScrollingSpeed=function(a,b){Q("scrollingSpeed",a,b)};b.fn.fullpage.setMouseWheelScrolling=function(a){a?document.addEventListener?(document.addEventListener("mousewheel",r,!1),document.addEventListener("wheel",r,!1)):document.attachEvent("onmousewheel",r):document.addEventListener?(document.removeEventListener("mousewheel",r,!1),document.removeEventListener("wheel",
|
||||
r,!1)):document.detachEvent("onmousewheel",r)};b.fn.fullpage.setAllowScrolling=function(a,c){if("undefined"!=typeof c)c=c.replace(" ","").split(","),b.each(c,function(c,d){switch(d){case "up":l.up=a;break;case "down":l.down=a;break;case "left":l.left=a;break;case "right":l.right=a;break;case "all":b.fn.fullpage.setAllowScrolling(a)}});else if(a){if(b.fn.fullpage.setMouseWheelScrolling(!0),C||P)MSPointer=oa(),b(document).off("touchstart "+MSPointer.down).on("touchstart "+MSPointer.down,ta),b(document).off("touchmove "+
|
||||
MSPointer.move).on("touchmove "+MSPointer.move,sa)}else if(b.fn.fullpage.setMouseWheelScrolling(!1),C||P)MSPointer=oa(),b(document).off("touchstart "+MSPointer.down),b(document).off("touchmove "+MSPointer.move)};b.fn.fullpage.setKeyboardScrolling=function(a){c.keyboardScrolling=a};b.fn.fullpage.moveSectionUp=function(){var a=b(".fp-section.active").prev(".fp-section");a.length||!c.loopTop&&!c.continuousVertical||(a=b(".fp-section").last());a.length&&q(a,null,!0)};b.fn.fullpage.moveSectionDown=function(){var a=
|
||||
b(".fp-section.active").next(".fp-section");a.length||!c.loopBottom&&!c.continuousVertical||(a=b(".fp-section").first());a.length&&q(a,null,!1)};b.fn.fullpage.moveTo=function(a,c){var e="",e=isNaN(a)?b('[data-anchor="'+a+'"]'):b(".fp-section").eq(a-1);"undefined"!==typeof c?L(a,c):0<e.length&&q(e)};b.fn.fullpage.moveSlideRight=function(){Y("next")};b.fn.fullpage.moveSlideLeft=function(){Y("prev")};b.fn.fullpage.reBuild=function(a){x=!0;var d=b(window).width();h=b(window).height();c.resize&&xa(h,d);
|
||||
b(".fp-section").each(function(){parseInt(b(this).css("padding-bottom"));parseInt(b(this).css("padding-top"));c.verticalCentered&&b(this).find(".fp-tableCell").css("height",la(b(this))+"px");b(this).css("height",h+"px");if(c.scrollOverflow){var a=b(this).find(".fp-slide");a.length?a.each(function(){y(b(this))}):y(b(this))}a=b(this).find(".fp-slides");a.length&&w(a,a.find(".fp-slide.active"))});b(".fp-section.active").position();d=b(".fp-section.active");d.index(".fp-section")&&q(d);x=!1;b.isFunction(c.afterResize)&&
|
||||
a&&c.afterResize.call(this);b.isFunction(c.afterReBuild)&&!a&&c.afterReBuild.call(this)};var t=!1,C=navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry|BB10|Windows Phone|Tizen|Bada)/),P="ontouchstart"in window||0<navigator.msMaxTouchPoints||navigator.maxTouchPoints,g=b(this),h=b(window).height(),m=!1,x=!1,p,K,k,ca="fullpage-wrapper",l={up:!0,down:!0,left:!0,right:!0},N=jQuery.extend(!0,{},c);b.fn.fullpage.setAllowScrolling(!0);c.css3&&(c.css3=za());b(this).length?(g.css({height:"100%",
|
||||
position:"relative"}),g.addClass(ca)):E("error","Error! Fullpage.js needs to be initialized with a selector. For example: $('#myContainer').fullpage();");b(c.sectionSelector).each(function(){b(this).addClass("fp-section")});b(c.slideSelector).each(function(){b(this).addClass("fp-slide")});c.navigation&&qa();b(".fp-section").each(function(a){var d=b(this),e=b(this).find(".fp-slide"),f=e.length;a||0!==b(".fp-section.active").length||b(this).addClass("active");b(this).css("height",h+"px");(c.paddingTop||
|
||||
c.paddingBottom)&&b(this).css("padding",c.paddingTop+" 0 "+c.paddingBottom+" 0");"undefined"!==typeof c.sectionsColor[a]&&b(this).css("background-color",c.sectionsColor[a]);"undefined"!==typeof c.anchors[a]&&b(this).attr("data-anchor",c.anchors[a]);if(1<f){a=100*f;var g=100/f;e.wrapAll('<div class="fp-slidesContainer" />');e.parent().wrap('<div class="fp-slides" />');b(this).find(".fp-slidesContainer").css("width",a+"%");c.controlArrows&&pa(b(this));c.slidesNavigation&&ya(b(this),f);e.each(function(a){b(this).css("width",
|
||||
g+"%");c.verticalCentered&&ka(b(this))});d=d.find(".fp-slide.active");0==d.length?e.eq(0).addClass("active"):J(d)}else c.verticalCentered&&ka(b(this))}).promise().done(function(){b.fn.fullpage.setAutoScrolling(c.autoScrolling,"internal");var a=b(".fp-section.active").find(".fp-slide.active");a.length&&(0!=b(".fp-section.active").index(".fp-section")||0==b(".fp-section.active").index(".fp-section")&&0!=a.index())&&J(a);c.fixedElements&&c.css3&&b(c.fixedElements).appendTo("body");c.navigation&&(k.css("margin-top",
|
||||
"-"+k.height()/2+"px"),k.find("li").eq(b(".fp-section.active").index(".fp-section")).find("a").addClass("active"));c.menu&&c.css3&&b(c.menu).closest(".fullpage-wrapper").length&&b(c.menu).appendTo("body");c.scrollOverflow?("complete"===document.readyState&&R(),b(window).on("load",R)):b.isFunction(c.afterRender)&&c.afterRender.call(this);ha();a=window.location.hash.replace("#","").split("/")[0];if(a.length){var d=b('[data-anchor="'+a+'"]');!c.animateAnchor&&d.length&&(c.autoScrolling?n(d.position().top):
|
||||
(n(0),D(a),b("html, body").scrollTop(d.position().top)),H(a,null),b.isFunction(c.afterLoad)&&c.afterLoad.call(this,a,d.index(".fp-section")+1),d.addClass("active").siblings().removeClass("active"))}b(window).on("load",function(){var a=window.location.hash.replace("#","").split("/"),b=a[0],a=a[1];b&&L(b,a)})});var T,U,F=!1;b(window).on("scroll",S);var v=0,B=0,u=0,A=0;b(window).on("hashchange",da);b(document).keydown(function(a){if(c.keyboardScrolling&&c.autoScrolling&&(40!=a.which&&38!=a.which||a.preventDefault(),
|
||||
!m))switch(a.which){case 38:case 33:b.fn.fullpage.moveSectionUp();break;case 40:case 34:b.fn.fullpage.moveSectionDown();break;case 36:b.fn.fullpage.moveTo(1);break;case 35:b.fn.fullpage.moveTo(b(".fp-section").length);break;case 37:b.fn.fullpage.moveSlideLeft();break;case 39:b.fn.fullpage.moveSlideRight()}});b(document).on("click touchstart","#fp-nav a",function(a){a.preventDefault();a=b(this).parent().index();q(b(".fp-section").eq(a))});b(document).on("click touchstart",".fp-slidesNav a",function(a){a.preventDefault();
|
||||
a=b(this).closest(".fp-section").find(".fp-slides");var c=a.find(".fp-slide").eq(b(this).closest("li").index());w(a,c)});c.normalScrollElements&&(b(document).on("mouseenter",c.normalScrollElements,function(){b.fn.fullpage.setMouseWheelScrolling(!1)}),b(document).on("mouseleave",c.normalScrollElements,function(){b.fn.fullpage.setMouseWheelScrolling(!0)}));b(".fp-section").on("click touchstart",".fp-controlArrow",function(){b(this).hasClass("fp-prev")?b.fn.fullpage.moveSlideLeft():b.fn.fullpage.moveSlideRight()});
|
||||
b(window).resize(ga);var M=h,ia;b.fn.fullpage.destroy=function(a){b.fn.fullpage.setAutoScrolling(!1,"internal");b.fn.fullpage.setAllowScrolling(!1);b.fn.fullpage.setKeyboardScrolling(!1);b(window).off("scroll",S).off("hashchange",da).off("resize",ga);b(document).off("click","#fp-nav a").off("mouseenter","#fp-nav li").off("mouseleave","#fp-nav li").off("click",".fp-slidesNav a").off("mouseover",c.normalScrollElements).off("mouseout",c.normalScrollElements);b(".fp-section").off("click",".fp-controlArrow");
|
||||
a&&Aa()}}})(jQuery);
|
|
@ -0,0 +1,439 @@
|
|||
/*
|
||||
* Released under BSD License
|
||||
* Copyright (c) 2019-2020 Allen_sun_js@hotmail.com
|
||||
*
|
||||
* Project Home:
|
||||
* https://github.com/allensunjian
|
||||
*/
|
||||
|
||||
(function ($w, temp) {
|
||||
var obj = null
|
||||
|
||||
|
||||
var Jm = $w[temp], name = 'menu', $d = $w['document'], menuEvent = 'oncontextmenu', clickEvent = 'onclick', $c = function (tag) { return $d.createElement(tag); }, _noop = function () { }, logger = (typeof console === 'undefined') ? { log: _noop, debug: _noop, error: _noop, warn: _noop, info: _noop } : console;
|
||||
|
||||
var $t = function (n, t) { if (n.hasChildNodes()) { n.firstChild.nodeValue = t; } else { n.appendChild($d.createTextNode(t)); } };
|
||||
|
||||
var $h = function (n, t) {
|
||||
if (t instanceof HTMLElement) {
|
||||
t.innerHTML = "";
|
||||
n.appendChild(t)
|
||||
} else {
|
||||
n.innerHTML = t;
|
||||
}
|
||||
};
|
||||
|
||||
if (!Jm || Jm[name]) return;
|
||||
|
||||
Jm.menu = function (_jm) {
|
||||
obj = this
|
||||
this._get_menu_options(_jm, function () {
|
||||
|
||||
this.init(_jm);
|
||||
|
||||
this._mount_events()
|
||||
})
|
||||
}
|
||||
Jm.menu.prototype = {
|
||||
|
||||
defaultDataMap: {
|
||||
funcMap: {
|
||||
edit: {
|
||||
isDepNode: true,
|
||||
// defaultFn不受到中台变量的控制,始终会先于fn去执行
|
||||
defaultFn: function (node) {
|
||||
var f = this._menu_default_mind_methods._menu_begin_edit.call(this.jm);
|
||||
f && this._menu_default_mind_methods._menu_edit_node_begin(this.jm.view, node);
|
||||
},
|
||||
fn: _noop,
|
||||
text: 'edit node'
|
||||
},
|
||||
addChild: {
|
||||
isDepNode: true,
|
||||
fn: function (nodeid,text) {
|
||||
var selected_node = this.get_selected_node();
|
||||
if (selected_node) {
|
||||
var node = this.add_node(selected_node, nodeid, text);
|
||||
if (node) {
|
||||
this.select_node(nodeid);
|
||||
this.begin_edit(nodeid);
|
||||
obj._mount_events();
|
||||
}
|
||||
}
|
||||
},
|
||||
text: 'append child'
|
||||
},
|
||||
addBrother: {
|
||||
isDepNode: true,
|
||||
fn: function (nodeid,text) {
|
||||
var selected_node = this.get_selected_node();
|
||||
if (selected_node && !selected_node.isroot) {
|
||||
var node = this.insert_node_after(selected_node, nodeid, text);
|
||||
if (node) {
|
||||
this.select_node(nodeid);
|
||||
this.begin_edit(nodeid);
|
||||
obj._mount_events();
|
||||
}
|
||||
}
|
||||
},
|
||||
text: 'append brother'
|
||||
},
|
||||
delete: {
|
||||
isDepNode: true,
|
||||
fn: function () {
|
||||
this.shortcut.handle_delnode.call(this.shortcut, this);
|
||||
},
|
||||
text: 'delete node'
|
||||
},
|
||||
showAll: {
|
||||
sDepNode: false,
|
||||
fn: function () {
|
||||
this.expand_all(this)
|
||||
},
|
||||
text: 'show all'
|
||||
},
|
||||
hideAll: {
|
||||
isDepNode: false,
|
||||
fn: function () {
|
||||
this.collapse_all(this)
|
||||
},
|
||||
text: 'hide all'
|
||||
},
|
||||
screenshot: {
|
||||
isDepNode: false,
|
||||
fn: function () {
|
||||
if (!this.screenshot) {
|
||||
logger.error('[jsmind] screenshot dependent on jsmind.screenshot.js !');
|
||||
return;
|
||||
}
|
||||
this.screenshot.shootDownload();
|
||||
},
|
||||
text: 'load mind picture'
|
||||
},
|
||||
showNode: {
|
||||
isDepNode: true,
|
||||
fn: function (node) {
|
||||
this.expand_node(node);
|
||||
},
|
||||
text: 'show target node'
|
||||
},
|
||||
hideNode: {
|
||||
isDepNode: true,
|
||||
fn: function (node) {
|
||||
this.collapse_node(node);
|
||||
},
|
||||
text: 'hide target node'
|
||||
},
|
||||
},
|
||||
menuStl: {
|
||||
'width': '150px',
|
||||
'padding': '12px 0',
|
||||
'position': 'fixed',
|
||||
'z-index': '10',
|
||||
'background': '#fff',
|
||||
'box-shadow': '0 2px 12px 0 rgba(0,0,0,0.1)',
|
||||
'border-radius': '5px',
|
||||
'font-size': '12px',
|
||||
'display': 'none'
|
||||
},
|
||||
menuItemStl:{
|
||||
padding: '5px 15px',
|
||||
cursor: 'pointer',
|
||||
display: 'block',
|
||||
'text-align': 'center',
|
||||
'transition':'all .2s'
|
||||
},
|
||||
injectionList:['edit','addChild','delete']
|
||||
},
|
||||
|
||||
init: function (_jm) {
|
||||
this._create_menu(_jm);
|
||||
this._get_injectionList(_jm);
|
||||
this.menuOpts.switchMidStage && Jm.util.dom.add_event(_jm.view.e_editor , 'blur', function () {
|
||||
this._menu_default_mind_methods._menu_edit_node_end.call(_jm.view);
|
||||
if(typeof this.menuOpts.editCaller == 'function') {
|
||||
this.menuOpts.editCaller($w.menu._update_node_info, this._menu_default_mind_methods._menu_update_edit_node)
|
||||
return
|
||||
}
|
||||
this._menu_default_mind_methods._menu_update_edit_node();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_event_contextMenu (e) {
|
||||
e.preventDefault();
|
||||
this.menu.style.left = e.clientX + 'px';
|
||||
this.menu.style.top = e.clientY + 'px';
|
||||
this.menu.style.display = 'block';
|
||||
this.selected_node = this.jm.get_selected_node();
|
||||
} ,
|
||||
|
||||
_event_hideMenu() {
|
||||
this.menu.style.display = 'none'
|
||||
},
|
||||
|
||||
_mount_events () {
|
||||
var items = document.getElementsByTagName('jmnode')
|
||||
for(let i = 0; i < items.length; i++) {
|
||||
items[i][menuEvent] = this._event_contextMenu.bind(this);
|
||||
}
|
||||
$w[clickEvent] = this._event_hideMenu.bind(this);
|
||||
},
|
||||
|
||||
_create_menu (_jm) {
|
||||
var d = $c('menu');
|
||||
this._set_menu_wrap_syl(d);
|
||||
this.menu = d;
|
||||
this.e_panel = _jm.view.e_panel;
|
||||
this.e_panel.appendChild(d);
|
||||
},
|
||||
|
||||
_create_menu_item (j, text, fn, isDepNode,cb, defaultFn) {
|
||||
var d = $c('menu-item'),_this = this;
|
||||
this._set_menu_item_syl(d);
|
||||
d.innerText = text;
|
||||
d.addEventListener('click', function () {
|
||||
if (this.selected_node || !isDepNode) {
|
||||
defaultFn.call(_this, this.selected_node);
|
||||
if (!_this._get_mid_opts()) {
|
||||
cb(this.selected_node, _noop)
|
||||
fn.call(j,Jm.util.uuid.newid(), this.menuOpts.newNodeText || 'New Node');
|
||||
return;
|
||||
}
|
||||
cb(this.selected_node,_this._mid_stage_next(function () {
|
||||
var retArgs = [this.selected_node],
|
||||
argus = Array.prototype.slice.call(arguments[0],0);
|
||||
argus[1] = this.menuOpts.newNodeText || 'New Node';
|
||||
if (argus[0]) {
|
||||
retArgs = argus
|
||||
}
|
||||
fn.apply(j,retArgs);
|
||||
}.bind(this)))
|
||||
return
|
||||
}
|
||||
alert(this.menuOpts.tipContent || 'Continue with node selected!')
|
||||
}.bind(this))
|
||||
d.addEventListener('mouseover', function () {
|
||||
d.style.background = 'rgb(179, 216, 255)'
|
||||
}.bind(this))
|
||||
d.addEventListener('mouseleave', function () {
|
||||
d.style.background = '#fff'
|
||||
}.bind(this))
|
||||
return d
|
||||
},
|
||||
|
||||
_set_menu_wrap_syl (d) {
|
||||
var os = this._get_option_sty('menu',this._get_mixin_sty);
|
||||
d.style.cssText = this._format_cssText(os);
|
||||
},
|
||||
|
||||
_set_menu_item_syl (d) {
|
||||
var os = this._get_option_sty('menuItem',this._get_mixin_sty);
|
||||
d.style.cssText = this._format_cssText(os)
|
||||
},
|
||||
|
||||
_format_cssText (o) {
|
||||
var text = '';
|
||||
Object.keys(o).forEach(function (k) {
|
||||
text += k +':'+o[k] +';'
|
||||
})
|
||||
return text;
|
||||
},
|
||||
|
||||
_empty_object (o) {
|
||||
return Object.keys(o).length == 0? true :false
|
||||
},
|
||||
|
||||
_get_option_sty (type, fn) {
|
||||
var sty = this.menuOpts.style,
|
||||
menu = this.defaultDataMap.menuStl,
|
||||
menuItem = this.defaultDataMap.menuItemStl,
|
||||
o = {menu,menuItem}
|
||||
|
||||
if (!sty) return o[type];
|
||||
if (!sty[type]) return o[type];
|
||||
if (!sty[type] || this._empty_object(sty[type])) return o[type];
|
||||
return fn( o[type],sty[type])
|
||||
},
|
||||
|
||||
_get_mixin_sty (dSty, oSty) {
|
||||
var o = {};
|
||||
Object.keys(oSty).forEach(function (k) {
|
||||
o[k] = oSty[k];
|
||||
})
|
||||
Object.keys(dSty).forEach(function (k) {
|
||||
if (!o[k]) o[k] = dSty[k];
|
||||
})
|
||||
return o
|
||||
},
|
||||
|
||||
_get_menu_options (j, fn) {
|
||||
var options = j.options;
|
||||
if (!options.menuOpts) return;
|
||||
if (!options.menuOpts.showMenu) return;
|
||||
this.menuOpts = j.options.menuOpts
|
||||
fn.call(this)
|
||||
},
|
||||
|
||||
_get_injectionDetail () {
|
||||
var iLs = this.menuOpts.injectionList,
|
||||
dLs = this.defaultDataMap.injectionList;
|
||||
if (!iLs) return dLs;
|
||||
if (!Array.isArray(iLs)) {
|
||||
logger.error('[jsmind] injectionList must be a Array');
|
||||
return;
|
||||
}
|
||||
if (iLs.length == 0) return dLs;
|
||||
return iLs
|
||||
},
|
||||
|
||||
_get_injectionList (j) {
|
||||
var list = this._get_injectionDetail(),
|
||||
_this = this;
|
||||
list.forEach(function (k) {
|
||||
var o = null,
|
||||
text = "",
|
||||
callback = _noop,
|
||||
defaultFn = _noop;
|
||||
|
||||
if (typeof k == 'object') {
|
||||
o = _this.defaultDataMap.funcMap[k.target];
|
||||
text = k.text;
|
||||
k.callback && (callback = k.callback);
|
||||
} else {
|
||||
o = _this.defaultDataMap.funcMap[k];
|
||||
text = o.text;
|
||||
}
|
||||
|
||||
if (o.defaultFn) defaultFn = o.defaultFn;
|
||||
_this.menu.appendChild(_this._create_menu_item(j ,text, o.fn, o.isDepNode,callback, defaultFn));
|
||||
})
|
||||
},
|
||||
|
||||
_get_mid_opts () {
|
||||
var b = this.menuOpts.switchMidStage;
|
||||
if (!b) return false;
|
||||
if (typeof b !== 'boolean') {
|
||||
logger.error('[jsmind] switchMidStage must be Boolean');
|
||||
return false;
|
||||
}
|
||||
return b
|
||||
},
|
||||
|
||||
_switch_view_db_event () {
|
||||
Jm.prototype.dblclick_handle = _noop;
|
||||
Jm.shortcut_provider.prototype.handler = _noop;
|
||||
Jm.view_provider.prototype.edit_node_end = _noop;
|
||||
},
|
||||
|
||||
_mid_stage_next (fn) {
|
||||
return function () {
|
||||
fn(arguments);
|
||||
}
|
||||
},
|
||||
|
||||
_reset_mind_event_edit () {},
|
||||
|
||||
_menu_default_mind_methods: {
|
||||
_menu_begin_edit: function () {
|
||||
var f = this.get_editable();
|
||||
if (!f) {
|
||||
logger.error('fail, this mind map is not editable.');
|
||||
}
|
||||
return f;
|
||||
},
|
||||
_menu_edit_node_begin (scope, node) {
|
||||
if (!node.topic) {
|
||||
logger.warn("don't edit image nodes");
|
||||
return;
|
||||
}
|
||||
if (scope.editing_node != null) {
|
||||
this._menu_default_mind_methods._menu_edit_node_end.call(scope);
|
||||
}
|
||||
scope.editing_node = node;
|
||||
var view_data = node._data.view;
|
||||
var element = view_data.element;
|
||||
var topic = node.topic;
|
||||
var ncs = getComputedStyle(element);
|
||||
scope.e_editor.value = topic;
|
||||
scope.e_editor.style.width = (element.clientWidth - parseInt(ncs.getPropertyValue('padding-left')) - parseInt(ncs.getPropertyValue('padding-right'))) + 'px';
|
||||
element.innerHTML = '';
|
||||
element.appendChild(scope.e_editor);
|
||||
element.style.zIndex = 5;
|
||||
scope.e_editor.focus();
|
||||
scope.e_editor.select();
|
||||
},
|
||||
_menu_edit_node_end: function () {
|
||||
if (this.editing_node != null) {
|
||||
var node = this.editing_node;
|
||||
this.editing_node = null;
|
||||
var view_data = node._data.view;
|
||||
var element = view_data.element;
|
||||
var topic = this.e_editor.value;
|
||||
element.style.zIndex = 'auto';
|
||||
element.removeChild(this.e_editor);
|
||||
$w.menu._update_node_info = {id: node.id, topic: topic};
|
||||
if (Jm.util.text.is_empty(topic) || node.topic === topic) {
|
||||
if (this.opts.support_html) {
|
||||
$h(element, node.topic);
|
||||
} else {
|
||||
$t(element, node.topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_menu_update_edit_node: function () {
|
||||
var info = $w.menu._update_node_info;
|
||||
$w.menu.jm.update_node(info.id, info.topic);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
var plugin = new Jm.plugin('menu',function (_jm) {
|
||||
|
||||
$w.menu = new Jm.menu(_jm);
|
||||
|
||||
$w.menu.jm = _jm;
|
||||
|
||||
if($w.menu.menuOpts) _jm.menu = $w.menu;
|
||||
|
||||
})
|
||||
|
||||
Jm.register_plugin(plugin)
|
||||
|
||||
function preventMindEventDefault() {
|
||||
|
||||
Jm.menu.prototype._switch_view_db_event();
|
||||
|
||||
}
|
||||
Jm.preventMindEventDefault = preventMindEventDefault
|
||||
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function reBuild () {
|
||||
if (obj !== null) {
|
||||
obj._mount_events()
|
||||
}
|
||||
}
|
||||
// export reBuild
|
||||
if (typeof module !== 'undefined' && typeof exports === 'object') {
|
||||
module.exports = {
|
||||
reBuild,
|
||||
init: function (opt) {
|
||||
Jm = opt
|
||||
}
|
||||
};
|
||||
}
|
||||
// exports = {
|
||||
// reBuild,
|
||||
// // init: function (opt) {
|
||||
// // console.log(opt)
|
||||
// // }
|
||||
// };
|
||||
|
||||
// $w['render'] = function () {
|
||||
// if (obj !== null) {
|
||||
// obj._mount_events()
|
||||
// }
|
||||
// }
|
||||
|
||||
})(window, 'jsMind')
|
|
@ -0,0 +1 @@
|
|||
[{"label": "汉族", "value": "汉族"}, {"label": "壮族", "value": "壮族"}, {"label": "蒙古族", "value": "蒙古族"}, {"label": "回族", "value": "回族"}, {"label": "藏族", "value": "藏族"}, {"label": "维吾尔族", "value": "维吾尔族"}, {"label": "苗族", "value": "苗族"}, {"label": "彝族", "value": "彝族"}, {"label": "布依族", "value": "布依族"}, {"label": "朝鲜族", "value": "朝鲜族"}, {"label": "满族", "value": "满族"}, {"label": "侗族", "value": "侗族"}, {"label": "瑶族", "value": "瑶族"}, {"label": "白族", "value": "白族"}, {"label": "东乡族", "value": "东乡族"}, {"label": "锡伯族", "value": "锡伯族"}, {"label": "土家族", "value": "土家族"}, {"label": "哈尼族", "value": "哈尼族"}, {"label": "哈萨克族", "value": "哈萨克族"}, {"label": "傣族", "value": "傣族"}, {"label": "黎族", "value": "黎族"}, {"label": "僳僳族", "value": "僳僳族"}, {"label": "佤族", "value": "佤族"}, {"label": "畲族", "value": "畲族"}, {"label": "拉祜族", "value": "拉祜族"}, {"label": "水族", "value": "水族"}, {"label": "纳西族", "value": "纳西族"}, {"label": "景颇族", "value": "景颇族"}, {"label": "柯尔克孜族", "value": "柯尔克孜族"}, {"label": "土族", "value": "土族"}, {"label": "高山族", "value": "高山族"}, {"label": "达斡尔族", "value": "达斡尔族"}, {"label": "仫佬族", "value": "仫佬族"}, {"label": "羌族", "value": "羌族"}, {"label": "撒拉族", "value": "撒拉族"}, {"label": "德昂族", "value": "德昂族"}, {"label": "仡佬族", "value": "仡佬族"}, {"label": "阿昌族", "value": "阿昌族"}, {"label": "普米族", "value": "普米族"}, {"label": "布朗族", "value": "布朗族"}, {"label": "塔吉克族", "value": "塔吉克族"}, {"label": "怒族", "value": "怒族"}, {"label": "乌孜别克族", "value": "乌孜别克族"}, {"label": "俄罗斯族", "value": "俄罗斯族"}, {"label": "鄂温克族", "value": "鄂温克族"}, {"label": "毛南族", "value": "毛南族"}, {"label": "保安族", "value": "保安族"}, {"label": "裕固族", "value": "裕固族"}, {"label": "京族", "value": "京族"}, {"label": "塔塔尔族", "value": "塔塔尔族"}, {"label": "独龙族", "value": "独龙族"}, {"label": "鄂伦春族", "value": "鄂伦春族"}, {"label": "赫哲族", "value": "赫哲族"}, {"label": "门巴族", "value": "门巴族"}, {"label": "珞巴族", "value": "珞巴族"}, {"label": "基诺族", "value": "基诺族"}]
|
|
@ -0,0 +1 @@
|
|||
{"上海": "上海市", "云南": "云南省", "内蒙古": "内蒙古自治区", "北京": "北京市", "台湾": "台湾省", "吉林": "吉林省", "四川": "四川省", "天津": "天津市", "宁夏": "宁夏回族自治区", "安徽": "安徽省", "山东": "山东省", "山西": "山西省", "广东": "广东省", "广西": "广西壮族自治区", "新疆": "新疆维吾尔自治区", "江苏": "江苏省", "江西": "江西省", "河北": "河北省", "河南": "河南省", "浙江": "浙江省", "海南": "海南省", "湖北": "湖北省", "湖南": "湖南省", "澳门": "澳门特别行政区", "甘肃": "甘肃省", "福建": "福建省", "西藏": "西藏自治区", "贵州": "贵州省", "辽宁": "辽宁省", "重庆": "重庆市", "陕西": "陕西省", "青海": "青海省", "香港": "香港特别行政区", "黑龙江": "黑龙江省"}
|
|
@ -33,7 +33,7 @@ export default {
|
|||
const first = matched[0]
|
||||
|
||||
if (!this.isDashboard(first)) {
|
||||
matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
|
||||
matched = [{ path: '/dashboard', meta: { title: '首页' }}].concat(matched)
|
||||
}
|
||||
|
||||
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {mapNodes, mapRelationships, getGraphStats} from './mapper'
|
||||
import store from '@/store'
|
||||
|
||||
export class GraphEventHandler {
|
||||
constructor (graph, graphView, getNodeNeighbours, onItemMouseOver, onItemSelected, onGraphModelChange) {
|
||||
this.graph = graph
|
||||
this.graphView = graphView
|
||||
this.getNodeNeighbours = getNodeNeighbours
|
||||
this.selectedItem = null
|
||||
this.onItemMouseOver = onItemMouseOver
|
||||
this.onItemSelected = onItemSelected
|
||||
this.onGraphModelChange = onGraphModelChange
|
||||
}
|
||||
|
||||
graphModelChanged () {
|
||||
this.onGraphModelChange(getGraphStats(this.graph))
|
||||
}
|
||||
|
||||
selectItem (item) {
|
||||
if (this.selectedItem) {
|
||||
this.selectedItem.selected = false
|
||||
}
|
||||
this.selectedItem = item
|
||||
item.selected = true
|
||||
this.graphView.update()
|
||||
}
|
||||
|
||||
deselectItem () {
|
||||
if (this.selectedItem) {
|
||||
this.selectedItem.selected = false
|
||||
this.selectedItem = null
|
||||
}
|
||||
this.onItemSelected({type: 'canvas', item: {nodeCount: this.graph.nodes().length, relationshipCount: this.graph.relationships().length}})
|
||||
this.graphView.update()
|
||||
}
|
||||
|
||||
nodeClose (d) {
|
||||
this.graph.removeConnectedRelationships(d)
|
||||
this.graph.removeNode(d)
|
||||
this.deselectItem()
|
||||
this.graphView.update()
|
||||
this.graphModelChanged()
|
||||
}
|
||||
|
||||
nodeClicked (d) {
|
||||
d.fixed = true
|
||||
if (!d.selected) {
|
||||
this.selectItem(d)
|
||||
this.onItemSelected({type: 'node', item: {id: d.id, labels: d.labels, properties: d.propertyList}})
|
||||
// 存储节点数据到vuex中,其他模块可使用,mutation函数似乎只能传一个参数
|
||||
store.commit('kg/SET_KG', {type: 'node', item: {id: d.id, labels: d.labels, properties: d.propertyList}})
|
||||
} else {
|
||||
this.deselectItem()
|
||||
}
|
||||
}
|
||||
|
||||
nodeUnlock (d) {
|
||||
d.fixed = false
|
||||
this.deselectItem()
|
||||
}
|
||||
|
||||
nodeDblClicked (d) {
|
||||
const graph = this.graph
|
||||
const graphView = this.graphView
|
||||
const graphModelChanged = this.graphModelChanged.bind(this)
|
||||
this.getNodeNeighbours(d, this.graph.findNodeNeighbourIds(d.id), function ({nodes, relationships}) {
|
||||
graph.addNodes(mapNodes(nodes))
|
||||
graph.addRelationships(mapRelationships(relationships, graph))
|
||||
graphView.update()
|
||||
graphModelChanged()
|
||||
})
|
||||
}
|
||||
|
||||
onNodeMouseOver (node) {
|
||||
if (!node.contextMenu) {
|
||||
this.onItemMouseOver({type: 'node', item: {id: node.id, labels: node.labels, properties: node.propertyList}})
|
||||
}
|
||||
}
|
||||
onMenuMouseOver (itemWithMenu) {
|
||||
this.onItemMouseOver({type: 'context-menu-item', item: {label: itemWithMenu.contextMenu.label, content: itemWithMenu.contextMenu.menuContent, selection: itemWithMenu.contextMenu.menuSelection}})
|
||||
}
|
||||
onRelationshipMouseOver (relationship) {
|
||||
this.onItemMouseOver({type: 'relationship', item: {id: relationship.id, type: relationship.type, properties: relationship.propertyList}})
|
||||
}
|
||||
|
||||
onRelationshipClicked (relationship) {
|
||||
if (!relationship.selected) {
|
||||
this.selectItem(relationship)
|
||||
this.onItemSelected({type: 'relationship', item: {id: relationship.id, type: relationship.type, properties: relationship.propertyList}})
|
||||
// 存储关系数据到vuex中,其他模块可使用
|
||||
store.commit('kg/SET_KG', {type: 'relation', item: {id: relationship.id, labels: [relationship.type], properties: relationship.propertyList}})
|
||||
} else {
|
||||
this.deselectItem()
|
||||
}
|
||||
}
|
||||
|
||||
onCanvasClicked () {
|
||||
this.deselectItem()
|
||||
}
|
||||
|
||||
onItemMouseOut (item) {
|
||||
this.onItemMouseOver({type: 'canvas', item: {nodeCount: this.graph.nodes().length, relationshipCount: this.graph.relationships().length}})
|
||||
}
|
||||
|
||||
bindEventHandlers () {
|
||||
this.graphView
|
||||
.on('nodeMouseOver', this.onNodeMouseOver.bind(this))
|
||||
.on('nodeMouseOut', this.onItemMouseOut.bind(this))
|
||||
.on('menuMouseOver', this.onMenuMouseOver.bind(this))
|
||||
.on('menuMouseOut', this.onItemMouseOut.bind(this))
|
||||
.on('relMouseOver', this.onRelationshipMouseOver.bind(this))
|
||||
.on('relMouseOut', this.onItemMouseOut.bind(this))
|
||||
.on('relationshipClicked', this.onRelationshipClicked.bind(this))
|
||||
.on('canvasClicked', this.onCanvasClicked.bind(this))
|
||||
.on('nodeClose', this.nodeClose.bind(this))
|
||||
.on('nodeClicked', this.nodeClicked.bind(this))
|
||||
.on('nodeDblClicked', this.nodeDblClicked.bind(this))
|
||||
.on('nodeUnlock', this.nodeUnlock.bind(this))
|
||||
this.onItemMouseOut()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
<template>
|
||||
<div
|
||||
:style="[{'width': '100%'},
|
||||
{'height': '75vh'},
|
||||
{'padding-top': '10px'},
|
||||
{'display': extendStyle.display}]">
|
||||
<ExplorerComponent
|
||||
ref="ExplorerComponent"
|
||||
:maxNeighbours="maxNeighbours"
|
||||
:initialNodeDisplay="initialNodeDisplay"
|
||||
:graphStyleData="graphStyleData"
|
||||
:updateStyle="updateStyle"
|
||||
:getNeighbours="getNeighbours"
|
||||
:nodes="state.nodesAndRelationships.nodes"
|
||||
:relationships="state.nodesAndRelationships.relationships"
|
||||
:fullscreen="fullscreen"
|
||||
:frameHeight="frameHeight"
|
||||
:assignVisElement="assignVisElement"
|
||||
:getAutoCompleteCallback="(callback) => { this.autoCompleteCallback = callback }"
|
||||
:setGraph="setGraph"
|
||||
@clickNode="handleClickNode"
|
||||
></ExplorerComponent>
|
||||
</div>
|
||||
</template>
|
||||
<script type="text/ecmascript-6">
|
||||
import bolt from './services/bolt/bolt'
|
||||
import ExplorerComponent from './components/Explorer'
|
||||
import { dim } from './constants'
|
||||
import Vue from 'vue'
|
||||
import { setting } from '@/config'
|
||||
|
||||
const neo4j = require('neo4j-driver');
|
||||
export default{
|
||||
|
||||
data(){
|
||||
return{
|
||||
dim: dim,
|
||||
state: {
|
||||
nodesAndRelationships: {
|
||||
nodes: [],
|
||||
relationships: []
|
||||
},
|
||||
justInitiated: true
|
||||
},
|
||||
graphStyleData: {},
|
||||
frameHeight: 500,
|
||||
graph: {}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
clearAll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
extendStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
maxNeighbours: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
initialNodeDisplay: '',
|
||||
assignVisElement: Function,
|
||||
records: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
components:{
|
||||
ExplorerComponent
|
||||
},
|
||||
created() {
|
||||
},
|
||||
mounted() {
|
||||
let me = this;
|
||||
this.frameHeight = $(this.$el).height() * 1.2;
|
||||
|
||||
this.driver = neo4j.driver(setting.neo4jUrl, neo4j.auth.basic(setting.neo4jUserName, setting.neo4jPassword));
|
||||
|
||||
me.componentWillMount();
|
||||
},
|
||||
methods: {
|
||||
handleClickNode(item) {
|
||||
this.$emit('clickNode', item);
|
||||
},
|
||||
updateStyle(graphStyleData) {
|
||||
if (JSON.stringify(graphStyleData) == JSON.stringify(this.graphStyleData)) return;
|
||||
|
||||
let inlineGraphStyleData = {};
|
||||
for (let nodeKey in this.graphStyleData) {
|
||||
inlineGraphStyleData[nodeKey] = this.graphStyleData[nodeKey];
|
||||
}
|
||||
|
||||
for (var nodeKey in graphStyleData) {
|
||||
let node = inlineGraphStyleData[nodeKey];
|
||||
if (!node) node = {};
|
||||
for (var propKey in graphStyleData[nodeKey]) {
|
||||
node[propKey] = graphStyleData[nodeKey][propKey];
|
||||
}
|
||||
|
||||
inlineGraphStyleData[nodeKey] = node;
|
||||
}
|
||||
|
||||
this.graphStyleData = inlineGraphStyleData;
|
||||
this.$refs.ExplorerComponent.reloadPanel();
|
||||
},
|
||||
componentWillMount () {
|
||||
if (this.records && this.records.length > 0) {
|
||||
this.populateDataToStateFromProps(this)
|
||||
}
|
||||
},
|
||||
|
||||
shouldComponentUpdate (nextProps) {
|
||||
return nextProps.extendStyle !== this.extendStyle ||
|
||||
nextProps.records !== this.records ||
|
||||
nextProps.graphStyleData !== this.graphStyleData
|
||||
},
|
||||
|
||||
populateDataToStateFromProps (props) {
|
||||
Vue.set(this.state, 'nodesAndRelationships', bolt.extractNodesAndRelationshipsFromRecordsForOldVis(props.records));
|
||||
},
|
||||
|
||||
mergeToList (list1, list2) {
|
||||
return list1.concat(list2.filter(itemInList2 => list1.findIndex(itemInList1 => itemInList1.id === itemInList2.id) < 0))
|
||||
},
|
||||
|
||||
autoCompleteRelationships (existingNodes, newNodes) {
|
||||
if (this.autoComplete) {
|
||||
const existingNodeIds = existingNodes.map(node => parseInt(node.id))
|
||||
const newNodeIds = newNodes.map(node => parseInt(node.id))
|
||||
|
||||
this.getInternalRelationships(existingNodeIds, newNodeIds)
|
||||
.then((graph) => {
|
||||
this.autoCompleteCallback && this.autoCompleteCallback(graph.relationships)
|
||||
})
|
||||
.catch((e) => {})
|
||||
}
|
||||
},
|
||||
|
||||
getNeighbours (id, currentNeighbourIds = []) {
|
||||
|
||||
const query = `MATCH path = (a)--(o)
|
||||
WHERE id(a) = ${id}
|
||||
AND NOT (id(o) IN[${currentNeighbourIds.join(',')}])
|
||||
RETURN path, size((a)--()) as c
|
||||
ORDER BY id(o)
|
||||
LIMIT ${this.maxNeighbours - currentNeighbourIds.length}`
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
var session = this.driver.session();
|
||||
|
||||
session.run(query, {})
|
||||
.then((result) => {
|
||||
let count = result.records.length > 0 ? parseInt(result.records[0].get('c').toString()) : 0
|
||||
const resultGraph = bolt.extractNodesAndRelationshipsFromRecordsForOldVis(result.records, false)
|
||||
this.autoCompleteRelationships(this.graph._nodes, resultGraph.nodes)
|
||||
resolve({...resultGraph, count: count})
|
||||
session.close();
|
||||
}).catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
getInternalRelationships (existingNodeIds, newNodeIds) {
|
||||
newNodeIds = newNodeIds.map(bolt.neo4j.int)
|
||||
existingNodeIds = existingNodeIds.map(bolt.neo4j.int)
|
||||
existingNodeIds = existingNodeIds.concat(newNodeIds)
|
||||
const query = 'MATCH (a)-[r]->(b) WHERE id(a) IN $existingNodeIds AND id(b) IN $newNodeIds RETURN r;'
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
var session = this.driver.session();
|
||||
|
||||
session.run(query, {})
|
||||
.then((result) => {
|
||||
resolve({...bolt.extractNodesAndRelationshipsFromRecordsForOldVis(result.records, false)})
|
||||
session.close();
|
||||
}).catch((error) => {
|
||||
reject({nodes: [], rels: []})
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
setGraph (graph) {
|
||||
this.graph = graph
|
||||
this.autoCompleteRelationships([], this.graph._nodes)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
records: {
|
||||
handler: function(val, oldVal) {
|
||||
this.populateDataToStateFromProps(this);
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
state: {
|
||||
handler: function(val, oldVal) {
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
'extendStyle.display': {
|
||||
handler: function(val, oldVal) {
|
||||
Vue.set(this.state, 'justInitiated', false);
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
clearAll: {
|
||||
handler: function(val, oldVal) {
|
||||
if (val) {
|
||||
this.$refs.ExplorerComponent.reloadPanel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,260 @@
|
|||
<template>
|
||||
<div
|
||||
id='svg-vis'
|
||||
:style="[{'height': '100%'},
|
||||
{'padding-top': (legendRowHeight * 2) + 1 + 'px'},
|
||||
{'padding-bottom': forcePaddingBottom ? forcePaddingBottom + 2 * pMarginTop + 'px' : '39px'}]">
|
||||
<LegendComponent
|
||||
v-if="state.freezeLegend"
|
||||
ref="LegendComponent"
|
||||
:stats="stats"
|
||||
:graphStyle="neoGraphStyle()"
|
||||
:graphStyleString="state.graphStyleString"
|
||||
:onSelectedLabel="onSelectedLabel"
|
||||
:onSelectedRelType="onSelectedRelType" />
|
||||
<LegendComponent
|
||||
v-else
|
||||
ref="LegendComponent"
|
||||
:stats="stats"
|
||||
:graphStyle="state.graphStyle"
|
||||
:graphStyleString="state.graphStyleString"
|
||||
:onSelectedLabel="onSelectedLabel"
|
||||
:onSelectedRelType="onSelectedRelType" />
|
||||
|
||||
<GraphComponent
|
||||
ref="GraphComponent"
|
||||
:fullscreen="fullscreen"
|
||||
:frameHeight="frameHeight"
|
||||
:relationships="state.relationships"
|
||||
:nodes="state.nodes"
|
||||
:getNodeNeighbours="getNodeNeighbours"
|
||||
:onItemMouseOver="onItemMouseOver"
|
||||
:onItemSelect="onItemSelect"
|
||||
:graphStyle="state.graphStyle"
|
||||
:onGraphModelChange="onGraphModelChange"
|
||||
:assignVisElement="assignVisElement"
|
||||
:getAutoCompleteCallback="getAutoCompleteCallback"
|
||||
:setGraph="setGraph" />
|
||||
<InspectorComponent
|
||||
ref="InspectorComponent"
|
||||
:fullscreen="fullscreen"
|
||||
:hoveredItem="hoveredItem"
|
||||
:selectedItem="selectedItem"
|
||||
:graphStyle="state.graphStyle"
|
||||
:graphStyleString="state.graphStyleString"
|
||||
:updateStyle="updateStyle"
|
||||
:onExpandToggled="onInspectorExpandToggled" />
|
||||
</div>
|
||||
</template>
|
||||
<script type="text/ecmascript-6">
|
||||
import GraphComponent from './Graph'
|
||||
import neoGraphStyle from '../graphStyle'
|
||||
import InspectorComponent from './Inspector'
|
||||
import LegendComponent from './Legend'
|
||||
import Vue from 'vue'
|
||||
|
||||
const deduplicateNodes = (nodes) => {
|
||||
return nodes.reduce((all, curr) => {
|
||||
if (all.taken.indexOf(curr.id) > -1) {
|
||||
return all
|
||||
} else {
|
||||
all.nodes.push(curr)
|
||||
all.taken.push(curr.id)
|
||||
return all
|
||||
}
|
||||
}, {nodes : [], taken : []}).nodes
|
||||
}
|
||||
|
||||
export default{
|
||||
components : {
|
||||
GraphComponent,
|
||||
InspectorComponent,
|
||||
LegendComponent
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
neoGraphStyle: neoGraphStyle,
|
||||
legendRowHeight : 32,
|
||||
forcePaddingBottom : 20,
|
||||
pMarginTop: 6,
|
||||
stats: {labels : {}, relTypes : {}},
|
||||
selectedItem: {},
|
||||
hoveredItem: {},
|
||||
graphStyle: {}
|
||||
}
|
||||
},
|
||||
props : {
|
||||
graphStyleData : Object,
|
||||
nodes : {
|
||||
type : Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
relationships : {
|
||||
type : Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
fullscreen : {
|
||||
type : Boolean,
|
||||
default : false
|
||||
},
|
||||
frameHeight : {
|
||||
type : Number,
|
||||
default : 0
|
||||
},
|
||||
assignVisElement : Function,
|
||||
getAutoCompleteCallback : {
|
||||
type : Function
|
||||
},
|
||||
setGraph : Function,
|
||||
updateStyle: Function,
|
||||
getNeighbours: Function
|
||||
},
|
||||
created() {
|
||||
let state = {}
|
||||
|
||||
state.stats = {labels : {}, relTypes : {}}
|
||||
state.graphStyle = neoGraphStyle();
|
||||
if (!$.isEmptyObject(this.graphStyleData)) {
|
||||
state.graphStyle.loadRules(this.graphStyleData)
|
||||
} else {
|
||||
state.graphStyle.resetToDefault()
|
||||
}
|
||||
state.graphStyleString = state.graphStyle.toString();
|
||||
let nodes = deduplicateNodes(this.nodes)
|
||||
state.relationships = this.relationships
|
||||
state.nodes = nodes
|
||||
state.relationships = this.relationships
|
||||
if (nodes.length > parseInt(this.initialNodeDisplay)) {
|
||||
state.nodes = nodes.slice(0, this.initialNodeDisplay)
|
||||
state.relationships = this.relationships.filter((item) =>
|
||||
state.nodes.filter((node) =>
|
||||
node.id === item.startNodeId).length > 0 && state.nodes.filter((node) =>
|
||||
node.id === item.endNodeId).length > 0)
|
||||
state.selectedItem = {
|
||||
type : 'status-item',
|
||||
item : `Not all return nodes are being displayed due to Initial Node Display setting. Only ${this.initialNodeDisplay} of ${nodes.length} nodes are being displayed`
|
||||
}
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods : {
|
||||
reloadPanel(forceReload) {
|
||||
this.$refs.GraphComponent.reloadPanel(forceReload);
|
||||
},
|
||||
getNodeNeighbours (node, currentNeighbours, callback) {
|
||||
if (currentNeighbours.length > this.maxNeighbours) {
|
||||
callback({nodes : [], relationships : []})
|
||||
}
|
||||
this.getNeighbours(node.id, currentNeighbours).then((result) => {
|
||||
let nodes = result.nodes
|
||||
if (result.count > (this.maxNeighbours - currentNeighbours.length)) {
|
||||
this.selectedItem = {
|
||||
type : 'status-item',
|
||||
item : `Rendering was limited to ${this.maxNeighbours} of the node's total ${result.count + currentNeighbours.length} neighbours due to browser config maxNeighbours.`
|
||||
}
|
||||
Vue.set(this.state, 'selectedItem', {
|
||||
type : 'status-item',
|
||||
item : `Rendering was limited to ${this.maxNeighbours} of the node's total ${result.count + currentNeighbours.length} neighbours due to browser config maxNeighbours.`
|
||||
})
|
||||
}
|
||||
callback({nodes : nodes, relationships : result.relationships})
|
||||
}, () => {
|
||||
callback({nodes : [], relationships : []})
|
||||
})
|
||||
},
|
||||
|
||||
onItemMouseOver (item) {
|
||||
this.hoveredItem = item;
|
||||
},
|
||||
|
||||
onItemSelect (item) {
|
||||
this.selectedItem = item;
|
||||
switch(item.type) {
|
||||
case 'node':
|
||||
this.$emit('clickNode', item.item)
|
||||
break;
|
||||
case 'relationship':
|
||||
this.$emit('clickNode', null)
|
||||
break;
|
||||
case 'canvas':
|
||||
this.$emit('clickNode', null)
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onGraphModelChange (stats) {
|
||||
this.stats = stats;
|
||||
Vue.set(this.state, 'graphStyleString', this.state.graphStyle.toString());
|
||||
this.updateStyle(this.state.graphStyle.toSheet())
|
||||
},
|
||||
|
||||
onSelectedLabel (label, propertyKeys) {
|
||||
this.selectedItem = {
|
||||
type : 'legend-item',
|
||||
item : {
|
||||
selectedLabel : {label : label, propertyKeys : propertyKeys},
|
||||
selectedRelType : null
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
onSelectedRelType (relType, propertyKeys) {
|
||||
this.selectedItem = {
|
||||
type : 'legend-item',
|
||||
item : {
|
||||
selectedLabel : null,
|
||||
selectedRelType : {relType : relType, propertyKeys : propertyKeys}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onInspectorExpandToggled (contracted, inspectorHeight) {
|
||||
this.inspectorContracted = contracted;
|
||||
this.forcePaddingBottom = inspectorHeight;
|
||||
}
|
||||
},
|
||||
watch : {
|
||||
nodes: {
|
||||
handler: function(val, oldVal) {
|
||||
Vue.set(this.state, 'nodes', deduplicateNodes(val));
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
relationships: {
|
||||
handler: function(val, oldVal) {
|
||||
Vue.set(this.state, 'relationships', val);
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
graphStyleData : {
|
||||
handler : function(val, oldVal) {
|
||||
if (val) {
|
||||
this.state.graphStyle.loadRules(val);
|
||||
Vue.set(this.state, 'graphStyleString', this.state.graphStyle.toString());
|
||||
} else {
|
||||
this.state.graphStyle.resetToDefault()
|
||||
Vue.set(this.state, 'graphStyleString', this.state.graphStyle.toString());
|
||||
}
|
||||
},
|
||||
deep : true
|
||||
},
|
||||
freezeLegend: {
|
||||
handler: function(val, oldVal) {
|
||||
if (val) {
|
||||
this.state.freezeLegend = false;
|
||||
Vue.set(this.state, 'graphStyleString', this.state.graphStyle.toString());
|
||||
this.updateStyle(this.state.graphStyle.toSheet())
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,216 @@
|
|||
<template>
|
||||
<StyledSvgWrapper>
|
||||
<svg class='neod3viz' ref="graphInit"/>
|
||||
<div v-if="true" style="position: absolute; right: 0px; bottom: 12px;">
|
||||
<div
|
||||
style="float:right;margin-right:5px;font-size:25px;cursor:pointer;"
|
||||
:class="[state.zoomInLimitReached ? 'faded zoom-in' : 'zoom-in']"
|
||||
@click="zoomInClicked">
|
||||
+
|
||||
</div>
|
||||
<div
|
||||
style="float:left;margin-right:10px;font-size:25px;cursor:pointer;"
|
||||
:class="[state.zoomOutLimitReached ? 'faded zoom-out' : 'zoom-out']"
|
||||
@click="zoomOutlicked">
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
</StyledSvgWrapper>
|
||||
</template>
|
||||
|
||||
<script type="text/ecmascript-6">
|
||||
import {StyledSvgWrapper} from './styled'
|
||||
import {createGraph, mapNodes, mapRelationships, getGraphStats} from '../mapper'
|
||||
import {GraphEventHandler} from '../GraphEventHandler'
|
||||
import { dim } from '../constants'
|
||||
import '../lib/visualization/index'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default{
|
||||
data(){
|
||||
return{
|
||||
state: {
|
||||
zoomInLimitReached: true,
|
||||
zoomOutLimitReached: false
|
||||
}
|
||||
}
|
||||
},
|
||||
components:{
|
||||
StyledSvgWrapper
|
||||
},
|
||||
props: {
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
frameHeight: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
nodes : {
|
||||
type : Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
relationships : {
|
||||
type : Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
graphStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
onItemMouseOver: Function,
|
||||
getNodeNeighbours: Function,
|
||||
onItemSelect: Function,
|
||||
onGraphModelChange: Function,
|
||||
getAutoCompleteCallback: Function,
|
||||
assignVisElement: Function
|
||||
},
|
||||
created() {
|
||||
},
|
||||
mounted() {
|
||||
this.graphInit();
|
||||
this.componentDidMount();
|
||||
},
|
||||
updated() {
|
||||
this.componentDidUpdate();
|
||||
},
|
||||
methods: {
|
||||
reloadPanel(forceReload) {
|
||||
if (forceReload && this.graph) {
|
||||
this.graph.resetGraph()
|
||||
}
|
||||
this.componentDidMount();
|
||||
},
|
||||
graphInit () {
|
||||
this.svgElement = this.$refs.graphInit;
|
||||
},
|
||||
|
||||
zoomInClicked (el) {
|
||||
let limits = this.graphView.zoomIn(el);
|
||||
Vue.set(this.state, 'zoomInLimitReached', limits.zoomInLimit);
|
||||
Vue.set(this.state, 'zoomOutLimitReached', limits.zoomOutLimit);
|
||||
},
|
||||
|
||||
zoomOutlicked (el) {
|
||||
let limits = this.graphView.zoomOut(el);
|
||||
Vue.set(this.state, 'zoomInLimitReached', limits.zoomInLimit);
|
||||
Vue.set(this.state, 'zoomOutLimitReached', limits.zoomOutLimit);
|
||||
},
|
||||
|
||||
getVisualAreaHeight () {
|
||||
let areaHeight = 0;
|
||||
if (this.frameHeight && this.fullscreen) {
|
||||
areaHeight = this.frameHeight - (dim.frameStatusbarHeight + dim.frameTitlebarHeight * 2)
|
||||
} else {
|
||||
areaHeight = this.frameHeight - (dim.frameStatusbarHeight + dim.frameTitlebarHeight * 2) || this.svgElement.parentNode.offsetHeight
|
||||
}
|
||||
|
||||
if (areaHeight < 0) {
|
||||
areaHeight = 0 - areaHeight;
|
||||
}
|
||||
return areaHeight;
|
||||
},
|
||||
|
||||
componentDidMount () {
|
||||
if (this.svgElement != null) {
|
||||
|
||||
if (!this.graphView) {
|
||||
let NeoConstructor = neo.graphView
|
||||
let measureSize = () => {
|
||||
return {width: this.svgElement.offsetWidth, height: this.getVisualAreaHeight()}
|
||||
}
|
||||
|
||||
this.graph = createGraph(this.nodes, this.relationships)
|
||||
this.graphView = new NeoConstructor(this.svgElement, measureSize, this.graph, this.graphStyle)
|
||||
|
||||
new GraphEventHandler(this.graph,
|
||||
this.graphView,
|
||||
this.getNodeNeighbours,
|
||||
this.onItemMouseOver,
|
||||
this.onItemSelect,
|
||||
this.onGraphModelChange
|
||||
).bindEventHandlers()
|
||||
this.graphView.resize()
|
||||
this.graphView.update()
|
||||
this.state.currentStyleRules = this.graphStyle.toString()
|
||||
this.onGraphModelChange(getGraphStats(this.graph))
|
||||
} else {
|
||||
this.graphView.update()
|
||||
this.state.currentStyleRules = this.graphStyle.toString()
|
||||
}
|
||||
|
||||
this.graph && this.setGraph && this.setGraph(this.graph)
|
||||
this.getAutoCompleteCallback && this.getAutoCompleteCallback(this.addInternalRelationships.bind(this))
|
||||
this.assignVisElement && this.assignVisElement(this.svgElement, this.graphView)
|
||||
}
|
||||
},
|
||||
|
||||
addInternalRelationships (internalRelationships) {
|
||||
if (this.graph) {
|
||||
this.graph.addInternalRelationships(mapRelationships(internalRelationships, this.graph))
|
||||
this.graphView.update()
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate () {
|
||||
if (this.state.shouldResize) {
|
||||
this.graphView.resize()
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
nodes: {
|
||||
handler: function(val, oldVal) {
|
||||
if (oldVal.toString() !== val.toString() && this.graphView) {
|
||||
this.graph.addNodes(mapNodes(val))
|
||||
this.graphView.update()
|
||||
this.onGraphModelChange(getGraphStats(this.graph))
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
// selectedNode: {
|
||||
// handler: function(val, oldVal) {
|
||||
// this.$store.state.selectedNode = val
|
||||
// console.log("change:", val)
|
||||
// }
|
||||
// },
|
||||
relationships: {
|
||||
handler: function(val, oldVal) {
|
||||
if (oldVal.toString() !== val.toString() && this.graphView) {
|
||||
this.graph.addRelationships(mapRelationships(val, this.graph))
|
||||
this.graphView.update()
|
||||
this.onGraphModelChange(getGraphStats(this.graph))
|
||||
}
|
||||
},
|
||||
deep: false
|
||||
},
|
||||
graphStyle: {
|
||||
handler: function(val, oldVal) {
|
||||
if (this.graphView) {
|
||||
this.graphView.update()
|
||||
Vue.set(this.state, 'currentStyleRules', val.toString());
|
||||
}
|
||||
},
|
||||
deep: false
|
||||
},
|
||||
fullscreen: {
|
||||
handler: function(val, oldVal) {
|
||||
Vue.set(this.state, 'shouldResize', true);
|
||||
}
|
||||
},
|
||||
frameHeight: {
|
||||
handler: function(val, oldVal) {
|
||||
Vue.set(this.state, 'shouldResize', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,194 @@
|
|||
<script type="text/ecmascript-6">
|
||||
import neoGraphStyle from '../graphStyle'
|
||||
import {StyledPickerSelector, StyledTokenRelationshipType, StyledInlineList, StyledInlineListItem, StyledLabelToken, StyledPickerListItem, StyledCircleSelector, StyledCaptionSelector} from './styled'
|
||||
|
||||
export default{
|
||||
data(){
|
||||
return{
|
||||
nodeDisplaySizes: [],
|
||||
widths: []
|
||||
}
|
||||
},
|
||||
props: {
|
||||
selectedLabel: Object,
|
||||
selectedRelType: Object,
|
||||
graphStyleData: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
graphStyle: {},
|
||||
outerUpdateStyle: Function
|
||||
},
|
||||
components:{
|
||||
|
||||
},
|
||||
created() {
|
||||
for (var index = 0; index < 10; index++) {
|
||||
this.nodeDisplaySizes.push(`${12 + 2 * index}px`)
|
||||
this.widths.push(`${5 + 3 * index}px`)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
sizeLessThan (size1, size2) {
|
||||
let size1Numerical = size1 ? size1.replace('px', '') + 0 : 0
|
||||
let size2Numerical = size1 ? size2.replace('px', '') + 0 : 0
|
||||
return size1Numerical <= size2Numerical
|
||||
},
|
||||
|
||||
updateStyle (selector, styleProp) {
|
||||
this.graphStyle.changeForSelector(selector, styleProp)
|
||||
if (this.outerUpdateStyle) {
|
||||
this.outerUpdateStyle(this.graphStyle.toSheet());
|
||||
}
|
||||
},
|
||||
|
||||
circleSelector (styleProps, styleProvider, activeProvider, className, selector, textProvider = () => { return '' }) {
|
||||
return styleProps.map((styleProp, i) => {
|
||||
const onClick = () => {
|
||||
this.updateStyle(selector, styleProp)
|
||||
}
|
||||
const style = styleProvider(styleProp, i)
|
||||
const text = textProvider(styleProp)
|
||||
const active = activeProvider(styleProp)
|
||||
return (
|
||||
<StyledPickerListItem class={className} key={i}>
|
||||
<StyledCircleSelector class={active ? 'active' : ''} style={style} nativeOnClick={onClick}>{text}</StyledCircleSelector>
|
||||
</StyledPickerListItem>
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
colorPicker (selector, styleForLabel) {
|
||||
return (
|
||||
<StyledInlineListItem key='color-picker'>
|
||||
<StyledInlineList class='color-picker picker'>
|
||||
<StyledInlineListItem>Color:</StyledInlineListItem>
|
||||
{this.circleSelector(this.graphStyle.defaultColors(), (color) => { return {'backgroundColor': color.color} }, (color) => { return color.color === styleForLabel.get('color') }, 'color-picker-item', selector)}
|
||||
</StyledInlineList>
|
||||
</StyledInlineListItem>
|
||||
)
|
||||
},
|
||||
|
||||
sizePicker (selector, styleForLabel) {
|
||||
return (
|
||||
<StyledInlineListItem key='size-picker'>
|
||||
<StyledInlineList class='size-picker picker'>
|
||||
<StyledInlineListItem>Size:</StyledInlineListItem>
|
||||
{this.circleSelector(this.graphStyle.defaultSizes(), (size, index) => { return { width: this.nodeDisplaySizes[index], height: this.nodeDisplaySizes[index] } }, (size) => { return this.sizeLessThan(size.diameter, styleForLabel.get('diameter')) }, 'size-picker-item', selector)}
|
||||
</StyledInlineList>
|
||||
</StyledInlineListItem>
|
||||
)
|
||||
},
|
||||
widthPicker (selector, styleForItem) {
|
||||
let widthSelectors = this.graphStyle.defaultArrayWidths().map((widthValue, i) => {
|
||||
const onClick = () => {
|
||||
this.updateStyle(selector, widthValue)
|
||||
}
|
||||
const style = { width: this.widths[i] }
|
||||
const active = styleForItem.get('shaft-width') === widthValue['shaft-width']
|
||||
return (
|
||||
<StyledPickerListItem class='width-picker-item' key={i}>
|
||||
<StyledPickerSelector class={active ? 'active' : ''} style={style} nativeOnClick={onClick} />
|
||||
</StyledPickerListItem>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<StyledInlineListItem key='width-picker'>
|
||||
<StyledInlineList class='width-picker picker'>
|
||||
<StyledInlineListItem>Line width:</StyledInlineListItem>
|
||||
{widthSelectors}
|
||||
</StyledInlineList>
|
||||
</StyledInlineListItem>
|
||||
)
|
||||
},
|
||||
|
||||
iconPicker (selector) {
|
||||
return (
|
||||
<li key='icon-picker'>
|
||||
Icon:
|
||||
<ul class='icon-picker picker'>
|
||||
{this.picker(this.graphStyle.defaultIconCodes(), (iconCode) => { return { 'fontFamily': 'streamline' } }, 'icon-picker-item', selector, (iconCode) => { return iconCode['icon-code'] })}
|
||||
</ul>
|
||||
</li>
|
||||
)
|
||||
},
|
||||
|
||||
captionPicker (selector, styleForItem, propertyKeys, showTypeSelector = false) {
|
||||
let captionSelector = (displayCaption, captionToSave, key) => {
|
||||
const onClick = () => {
|
||||
this.updateStyle(selector, { caption: captionToSave })
|
||||
}
|
||||
let active = styleForItem.props.caption === captionToSave
|
||||
return (
|
||||
<StyledPickerListItem key={key}>
|
||||
<StyledCaptionSelector class={active ? 'active' : ''} nativeOnClick={onClick}>{displayCaption}</StyledCaptionSelector>
|
||||
</StyledPickerListItem>
|
||||
)
|
||||
}
|
||||
let captionSelectors = propertyKeys.map((propKey, i) => {
|
||||
return captionSelector(propKey, `{${propKey}}`)
|
||||
})
|
||||
let typeCaptionSelector = null
|
||||
if (showTypeSelector) {
|
||||
typeCaptionSelector = captionSelector('<type>', '<type>', 'typecaption')
|
||||
}
|
||||
return (
|
||||
<StyledInlineListItem key='caption-picker'>
|
||||
<StyledInlineList class='caption-picker picker'>
|
||||
<StyledInlineListItem>Caption:</StyledInlineListItem>
|
||||
{captionSelector('<id>', '<id>', 'idcaption')}
|
||||
{typeCaptionSelector}
|
||||
{captionSelectors}
|
||||
</StyledInlineList>
|
||||
</StyledInlineListItem>
|
||||
)
|
||||
},
|
||||
|
||||
stylePicker () {
|
||||
let pickers;
|
||||
let title;
|
||||
|
||||
if (this.selectedLabel) {
|
||||
const labelList = this.selectedLabel.label !== '*' ? [this.selectedLabel.label] : [];
|
||||
|
||||
const styleForLabel = this.graphStyle.forNode({ labels: labelList })
|
||||
|
||||
const inlineStyle = {'backgroundColor': styleForLabel.get('color'), 'color': styleForLabel.get('text-color-internal')}
|
||||
pickers = [this.colorPicker(styleForLabel.selector, styleForLabel), this.sizePicker(styleForLabel.selector, styleForLabel), this.captionPicker(styleForLabel.selector, styleForLabel, this.selectedLabel.propertyKeys)]
|
||||
title = (<StyledLabelToken class='token token-label' style={inlineStyle}>{this.selectedLabel.label || '*'}</StyledLabelToken>)
|
||||
} else if (this.selectedRelType) {
|
||||
|
||||
const relTypeSelector = this.selectedRelType.relType !== '*' ? {type: this.selectedRelType.relType} : {}
|
||||
const styleForRelType = this.graphStyle.forRelationship(relTypeSelector)
|
||||
const inlineStyle = {'backgroundColor': styleForRelType.get('color'), 'color': styleForRelType.get('text-color-internal')}
|
||||
|
||||
pickers = [this.colorPicker(styleForRelType.selector, styleForRelType), this.widthPicker(styleForRelType.selector, styleForRelType), this.captionPicker(styleForRelType.selector, styleForRelType, this.selectedRelType.propertyKeys, true)]
|
||||
title = (<StyledTokenRelationshipType class='token token-relationship' style={inlineStyle}>{this.selectedRelType.relType || '*'}</StyledTokenRelationshipType>)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<StyledInlineList class='style-picker'>
|
||||
{title}
|
||||
{pickers}
|
||||
</StyledInlineList>
|
||||
)
|
||||
},
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.graphStyleData && nextProps.graphStyleData !== this.graphStyleData) {
|
||||
this.graphStyle.loadRules(nextProps.graphStyleData)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
render () {
|
||||
return this.stylePicker()
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,168 @@
|
|||
<script type="text/ecmascript-6">
|
||||
import {inspectorFooterContractedHeight, StyledInspectorFooterStatusMessage, StyledTokenContextMenuKey, StyledTokenRelationshipType, StyledLabelToken, StyledStatusBar, StyledStatus, StyledInspectorFooter, StyledInspectorFooterRow, StyledInspectorFooterRowListPair, StyledInspectorFooterRowListKey, StyledInspectorFooterRowListValue, StyledInlineList} from './styled'
|
||||
import GrassEditor from './GrassEditor'
|
||||
import RowExpandToggleComponent from './RowExpandToggle'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default{
|
||||
components:{
|
||||
GrassEditor,
|
||||
RowExpandToggleComponent
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
}
|
||||
},
|
||||
props: {
|
||||
graphStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
graphStyleString: '',
|
||||
onExpandToggled: Function,
|
||||
selectedItem: {},
|
||||
hoveredItem: {},
|
||||
updateStyle: Function,
|
||||
},
|
||||
created() {
|
||||
let state = {}
|
||||
state.contracted = true
|
||||
|
||||
this.state = state;
|
||||
},
|
||||
mounted() {
|
||||
this.setFooterRowELem();
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
methods: {
|
||||
reloadPanel() {
|
||||
|
||||
},
|
||||
setFooterRowELem () {
|
||||
if (this.$refs.setFooterRowELem) {
|
||||
this.footerRowElem = this.$refs.setFooterRowELem;
|
||||
}
|
||||
},
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.selectedItem !== nextProps.selectedItem) { // || this.hoveredItem !== nextProps.hoveredItem){
|
||||
this.setState({ contracted: true })
|
||||
this.onExpandToggled && this.onExpandToggled(true, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
render () {
|
||||
let item
|
||||
let type
|
||||
let inspectorContent
|
||||
|
||||
const mapItemProperties = (itemProperties) => {
|
||||
return itemProperties.map((prop, i) => {
|
||||
return (
|
||||
<StyledInspectorFooterRowListPair class='pair' key={'prop' + i}>
|
||||
<StyledInspectorFooterRowListKey class='key'>{prop.key + ': '}</StyledInspectorFooterRowListKey>
|
||||
<StyledInspectorFooterRowListValue class='value'>{prop.value.toString()}</StyledInspectorFooterRowListValue>
|
||||
</StyledInspectorFooterRowListPair>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const mapLabels = (itemLabels) => {
|
||||
return itemLabels.map((label, i) => {
|
||||
const graphStyleForLabel = this.graphStyle.forNode({labels: [label]})
|
||||
const style = {'backgroundColor': graphStyleForLabel.get('color'), 'color': graphStyleForLabel.get('text-color-internal')}
|
||||
return (
|
||||
<StyledLabelToken key={'label' + i} style={style} class={'token' + ' ' + 'token-label'}>{label}</StyledLabelToken>
|
||||
)
|
||||
})
|
||||
}
|
||||
if (this.hoveredItem && this.hoveredItem.type !== 'canvas') {
|
||||
item = this.hoveredItem.item
|
||||
type = this.hoveredItem.type
|
||||
} else if (this.selectedItem) {
|
||||
item = this.selectedItem.item
|
||||
type = this.selectedItem.type
|
||||
} else if (this.hoveredItem) {
|
||||
item = this.hoveredItem.item
|
||||
type = this.hoveredItem.type
|
||||
}
|
||||
|
||||
if (item && type) {
|
||||
if (type === 'legend-item') {
|
||||
inspectorContent = (
|
||||
<GrassEditor selectedLabel={item.selectedLabel} selectedRelType={item.selectedRelType} outerUpdateStyle={this.updateStyle} graphStyle={this.graphStyle} graphStyleString={this.graphStyleString}/>
|
||||
)
|
||||
}
|
||||
if (type === 'status-item') {
|
||||
inspectorContent = (
|
||||
<StyledInspectorFooterStatusMessage class='value'>{item}</StyledInspectorFooterStatusMessage>
|
||||
)
|
||||
}
|
||||
if (type === 'context-menu-item') {
|
||||
inspectorContent = (
|
||||
<StyledInlineList class='list-inline'>
|
||||
<StyledTokenContextMenuKey key='token' class={'token' + ' ' + 'token-context-menu-key' + ' ' + 'token-label'}>{item.label}</StyledTokenContextMenuKey>
|
||||
<StyledInspectorFooterRowListPair key='pair' class='pair'>
|
||||
<StyledInspectorFooterRowListValue class='value'>{item.content}</StyledInspectorFooterRowListValue>
|
||||
</StyledInspectorFooterRowListPair>
|
||||
</StyledInlineList>
|
||||
)
|
||||
} else if (type === 'canvas') {
|
||||
const description = `Displaying ${item.nodeCount} nodes, ${item.relationshipCount} relationships.`
|
||||
inspectorContent = (
|
||||
<StyledInlineList class='list-inline'>
|
||||
<StyledInspectorFooterRowListPair class='pair' key='pair'>
|
||||
<StyledInspectorFooterRowListValue class='value'>{description}</StyledInspectorFooterRowListValue>
|
||||
</StyledInspectorFooterRowListPair>
|
||||
</StyledInlineList>
|
||||
)
|
||||
} else if (type === 'node') {
|
||||
inspectorContent = (
|
||||
<StyledInlineList class='list-inline'>
|
||||
{mapLabels(item.labels)}
|
||||
<StyledInspectorFooterRowListPair key='pair' class='pair'>
|
||||
<StyledInspectorFooterRowListKey class='key'>{'<id>:'}</StyledInspectorFooterRowListKey>
|
||||
<StyledInspectorFooterRowListValue class='value'>{item.id}</StyledInspectorFooterRowListValue>
|
||||
</StyledInspectorFooterRowListPair>
|
||||
{mapItemProperties(item.properties)}
|
||||
</StyledInlineList>
|
||||
)
|
||||
} else if (type === 'relationship') {
|
||||
const style = {'backgroundColor': this.graphStyle.forRelationship(item).get('color'), 'color': this.graphStyle.forRelationship(item).get('text-color-internal')}
|
||||
inspectorContent = (
|
||||
<StyledInlineList class='list-inline'>
|
||||
<StyledTokenRelationshipType key='token' style={style} class={'token' + ' ' + 'token-relationship-type'}>{item.type}</StyledTokenRelationshipType>
|
||||
<StyledInspectorFooterRowListPair key='pair' class='pair'>
|
||||
<StyledInspectorFooterRowListKey class='key'>{'<id>:'}</StyledInspectorFooterRowListKey>
|
||||
<StyledInspectorFooterRowListValue class='value'>{item.id}</StyledInspectorFooterRowListValue>
|
||||
</StyledInspectorFooterRowListPair>
|
||||
{mapItemProperties(item.properties)}
|
||||
</StyledInlineList>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleExpand = () => {
|
||||
this.state.contracted = !this.state.contracted;
|
||||
|
||||
const inspectorHeight = this.footerRowElem.base.clientHeight
|
||||
this.onExpandToggled && this.onExpandToggled(this.state.contracted, this.state.contracted ? 0 : inspectorHeight)
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledStatusBar fullscreen={this.fullscreen} class='status-bar'>
|
||||
<StyledStatus class='status'>
|
||||
<StyledInspectorFooter class={this.state.contracted ? 'contracted inspector-footer' : 'inspector-footer'}>
|
||||
<StyledInspectorFooterRow class='inspector-footer-row' ref='setFooterRowELem'>
|
||||
{ type === 'canvas' ? null : <RowExpandToggleComponent contracted={this.state.contracted} rowElem={this.footerRowElem} containerHeight={inspectorFooterContractedHeight} nativeOnClick={toggleExpand} /> }
|
||||
{inspectorContent}
|
||||
</StyledInspectorFooterRow>
|
||||
</StyledInspectorFooter>
|
||||
</StyledStatus>
|
||||
</StyledStatusBar>
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,160 @@
|
|||
<script type="text/ecmascript-6">
|
||||
import {legendRowHeight, StyledLegendRow, StyledTokenRelationshipType, StyledLegendInlineListItem, StyledLegend, StyledLegendContents, StyledLabelToken, StyledTokenCount, StyledLegendInlineList} from './styled'
|
||||
import RowExpandToggleComponent from './RowExpandToggle'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default{
|
||||
components:{
|
||||
RowExpandToggleComponent,
|
||||
StyledLegendRow,
|
||||
StyledTokenRelationshipType,
|
||||
StyledLegendInlineListItem,
|
||||
StyledLegend,
|
||||
StyledLegendContents,
|
||||
StyledLabelToken,
|
||||
StyledTokenCount,
|
||||
StyledLegendInlineList
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
legendRowHeight: legendRowHeight,
|
||||
state: {
|
||||
typeRowContracted: true,
|
||||
labelRowContracted: true
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
stats: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
graphStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
graphStyleString: '',
|
||||
onSelectedLabel: Function,
|
||||
onSelectedRelType: Function
|
||||
},
|
||||
created() {
|
||||
},
|
||||
mounted() {
|
||||
this.setLabelRowELem();
|
||||
this.setTypeRowELem();
|
||||
},
|
||||
methods: {
|
||||
setTypeRowELem () {
|
||||
if (this.$refs.setTypeRowELem) {
|
||||
Vue.set(this.state, 'typeRowElem', this.$refs.setTypeRowELem);
|
||||
}
|
||||
},
|
||||
setLabelRowELem () {
|
||||
if (this.$refs.setLabelRowELem) {
|
||||
Vue.set(this.state, 'labelRowELem', this.$refs.setLabelRowELem);
|
||||
}
|
||||
},
|
||||
getLegendPanel() {
|
||||
const mapLabels = (labels) => {
|
||||
const labelList = Object.keys(labels).map((legendItemKey, i) => {
|
||||
const styleForItem = this.graphStyle.forNode({labels: [legendItemKey]})
|
||||
const onClick = () => { this.onSelectedLabel(legendItemKey, Object.keys(labels[legendItemKey].properties)) }
|
||||
const style = {'backgroundColor': styleForItem.get('color'), 'color': styleForItem.get('text-color-internal')}
|
||||
return (
|
||||
<StyledLegendInlineListItem key={i}>
|
||||
<StyledLegendContents class='contents' style="">
|
||||
<StyledLabelToken
|
||||
nativeOnClick={ onClick }
|
||||
style={style} class='token token-label'>
|
||||
{legendItemKey}
|
||||
<StyledTokenCount class='count'>({labels[legendItemKey].count})</StyledTokenCount>
|
||||
</StyledLabelToken>
|
||||
</StyledLegendContents>
|
||||
</StyledLegendInlineListItem>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<StyledLegendRow class={this.state.labelRowContracted ? 'contracted' : ''}>
|
||||
<StyledLegendInlineList class='list-inline' ref='setLabelRowELem'>
|
||||
<RowExpandToggleComponent
|
||||
contracted={ this.state.labelRowContracted }
|
||||
rowElem={ this.state.labelRowELem }
|
||||
containerHeight={ legendRowHeight }
|
||||
onClick={() => { this.state.labelRowContracted = !this.state.labelRowContracted; }} />
|
||||
{labelList}
|
||||
</StyledLegendInlineList>
|
||||
</StyledLegendRow>
|
||||
)
|
||||
}
|
||||
|
||||
const mapRelTypes = (legendItems) => {
|
||||
const relTypeList = Object.keys(legendItems).map((legendItemKey, i) => {
|
||||
const styleForItem = this.graphStyle.forRelationship({type: legendItemKey})
|
||||
const onClick = () => { this.onSelectedRelType(legendItemKey, Object.keys(legendItems[legendItemKey].properties)) }
|
||||
|
||||
const style = {'backgroundColor': styleForItem.get('color'), 'color': styleForItem.get('text-color-internal')}
|
||||
return (
|
||||
<StyledLegendInlineListItem key={i}>
|
||||
<StyledLegendContents class='contents'>
|
||||
<StyledTokenRelationshipType
|
||||
nativeOnClick={ onClick }
|
||||
style={style} class='token token-relationship-type'>
|
||||
{legendItemKey}
|
||||
<StyledTokenCount class='count'>({legendItems[legendItemKey].count})</StyledTokenCount>
|
||||
</StyledTokenRelationshipType>
|
||||
</StyledLegendContents>
|
||||
</StyledLegendInlineListItem>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<StyledLegendRow class={this.state.typeRowContracted ? 'contracted' : ''}>
|
||||
<StyledLegendInlineList class='list-inline' ref='setTypeRowELem'>
|
||||
<RowExpandToggleComponent
|
||||
contracted={this.state.typeRowContracted}
|
||||
rowElem={this.state.typeRowElem}
|
||||
containerHeight={legendRowHeight}
|
||||
onClick={() => { this.state.typeRowContracted = !this.state.typeRowContracted; }} />
|
||||
{relTypeList}
|
||||
</StyledLegendInlineList>
|
||||
</StyledLegendRow>
|
||||
)
|
||||
}
|
||||
|
||||
let relTypes = mapRelTypes(this.stats.relTypes);
|
||||
return (
|
||||
<StyledLegend class={relTypes ? '' : 'one-row'}>
|
||||
{mapLabels(this.stats.labels)}
|
||||
{relTypes}
|
||||
</StyledLegend>
|
||||
)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
stats: {
|
||||
handler: function(val, oldVal) {
|
||||
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
graphStyle: {
|
||||
handler: function(val, oldVal) {
|
||||
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
graphStyleString: {
|
||||
handler: function(val, oldVal) {
|
||||
this.$forceUpdate();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
render () {
|
||||
return this.getLegendPanel();
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,71 @@
|
|||
<script type="text/ecmascript-6">
|
||||
import { StyledRowToggle, StyledCaret } from './styled'
|
||||
import Vue from 'vue'
|
||||
|
||||
const getHeightFromElem = (rowElem) => (rowElem && rowElem.base) ? rowElem.base.clientHeight : 0
|
||||
|
||||
export default{
|
||||
components:{
|
||||
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
state: {}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
rowElem: null,
|
||||
onClick: Function,
|
||||
contracted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
containerHeight: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
created() {
|
||||
},
|
||||
mounted() {
|
||||
this.componentDidMount();
|
||||
},
|
||||
updated() {
|
||||
this.componentDidUpdate();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.componentWillUnmount();
|
||||
},
|
||||
methods: {
|
||||
updateDimensions () {
|
||||
Vue.set(this.state, 'rowHeight', getHeightFromElem(this.rowElem));
|
||||
},
|
||||
|
||||
componentDidMount () {
|
||||
this.updateDimensions()
|
||||
window.addEventListener('resize', this.updateDimensions.bind(this))
|
||||
},
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('resize', this.updateDimensions.bind(this))
|
||||
},
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
const rowHeight = getHeightFromElem(this.rowElem)
|
||||
if (this.state.rowHeight !== rowHeight) {
|
||||
this.updateDimensions()
|
||||
}
|
||||
}
|
||||
},
|
||||
render () {
|
||||
if (this.containerHeight * 1.1 < this.state.rowHeight) {
|
||||
return (
|
||||
<StyledRowToggle nativeOnClick={this.onClick}>
|
||||
<StyledCaret class={this.contracted ? 'fa fa-caret-left' : 'fa fa-caret-down'} />
|
||||
</StyledRowToggle>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,368 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import styled from 'vue-styled-components'
|
||||
|
||||
export const legendRowHeight = 32
|
||||
export const inspectorFooterContractedHeight = 22
|
||||
const pMarginTop = 6
|
||||
|
||||
export const StyledSvgWrapper = styled.div`
|
||||
line-height: 0;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
> svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #f9fbfd;
|
||||
.node {
|
||||
cursor: pointer;
|
||||
> .ring {
|
||||
fill: none;
|
||||
opacity: 0;
|
||||
stroke: #6ac6ff;
|
||||
}
|
||||
&.selected {
|
||||
> .ring {
|
||||
stroke: #fdcc59;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
> .ring {
|
||||
stroke: #6ac6ff;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
.relationship {
|
||||
> .overlay {
|
||||
opacity: 0;
|
||||
fill: #6ac6ff;
|
||||
}
|
||||
&.selected {
|
||||
> .overlay {
|
||||
fill: #fdcc59;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
> .overlay {
|
||||
fill: #6ac6ff;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
.remove_node {
|
||||
.expand_node {
|
||||
&:hover {
|
||||
border: 2px #000 solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
.outline {
|
||||
cursor: pointer
|
||||
}
|
||||
path {
|
||||
&.context-menu-item {
|
||||
stroke-width: 2px;
|
||||
fill: #d2d5da;
|
||||
}
|
||||
}
|
||||
text {
|
||||
line-height: normal;
|
||||
&.context-menu-item {
|
||||
fill: #fff;
|
||||
text-anchor: middle;
|
||||
pointer-events: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.context-menu-item {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
fill: #b9b9b9;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export const StyledStream = styled.div`
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 17px;
|
||||
`
|
||||
|
||||
export const p = styled.div`
|
||||
margin-top: ${pMarginTop}px;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
`
|
||||
|
||||
export const StyledRowToggle = styled.div`
|
||||
float: right;
|
||||
display: block;
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
line-height: 21px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
`
|
||||
export const StyledCaret = styled.div`
|
||||
font-size: 17px;
|
||||
vertical-align: middle;
|
||||
`
|
||||
|
||||
export const StyledInspectorFooter = styled.div`
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
overflow: scroll;
|
||||
&.contracted {
|
||||
max-height: ${inspectorFooterContractedHeight}px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledInspectorFooterRow = styled.ul`
|
||||
list-style: none;
|
||||
word-break: break-word;
|
||||
line-height: 21px;
|
||||
`
|
||||
|
||||
export const StyledInspectorFooterRowListKey = styled.div`
|
||||
float: left;
|
||||
font-weight: 800;
|
||||
`
|
||||
|
||||
export const StyledInspectorFooterRowListValue = styled.div`
|
||||
padding-left: 3px;
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
white-space: pre-wrap;
|
||||
`
|
||||
|
||||
export const StyledInlineList = styled.ul`
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
word-break: break-word
|
||||
`
|
||||
|
||||
export const StyledInlineListItem = styled.li`
|
||||
display: inline-block;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
`
|
||||
|
||||
export const StyledStatusBarWrapper = styled.div`
|
||||
height: 68px;
|
||||
display: none;
|
||||
`
|
||||
export const StyledStatusBar = styled.div`
|
||||
min-height: 39px;
|
||||
line-height: 39px;
|
||||
color: #788185;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid #e6e9ef;
|
||||
${props => props.fullscreen ? 'margin-top: -39px;' : 'margin-bottom: -39px;'}
|
||||
`
|
||||
|
||||
export const StyledStatus = styled.div`
|
||||
position: relative;
|
||||
float: left;
|
||||
padding-left: 16px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
margin-top: 3px;
|
||||
`
|
||||
|
||||
export const StyledInspectorFooterRowListPair = styled(StyledInlineListItem)`
|
||||
vertical-align: middle;
|
||||
font-size: 13px;
|
||||
`
|
||||
|
||||
export const StyledToken = styled(StyledInlineListItem)`
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
line-height: 1em;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
user-select: none;
|
||||
font-size: 12px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
`
|
||||
export const StyledLabelToken = styled(StyledToken)`
|
||||
padding: 4px 7px 4px 9px;
|
||||
border-radius: 20px;
|
||||
`
|
||||
export const StyledTokenRelationshipType = styled(StyledToken)`
|
||||
padding: 4px 7px 4px 5px;
|
||||
border-radius: 3px;
|
||||
`
|
||||
|
||||
export const tokenPropertyKey = styled(StyledToken)`
|
||||
padding: 3px 5px 3px 5px;
|
||||
`
|
||||
export const StyledTokenContextMenuKey = styled(StyledLabelToken)` {
|
||||
color: #f9fbfd;
|
||||
background-color: #d2d5da;
|
||||
font-family: FontAwesome;
|
||||
padding: 4px 9px;
|
||||
`
|
||||
|
||||
export const StyledTokenCount = styled.span`
|
||||
font-weight: normal;
|
||||
`
|
||||
|
||||
export const StyledLegendContents = styled.div`
|
||||
float: left;
|
||||
line-height: 1em;
|
||||
position: relative;
|
||||
top: 5px;
|
||||
`
|
||||
|
||||
export const StyledLegendRow = styled.div`
|
||||
border-bottom: 1px solid #e6e9ef;
|
||||
&.contracted {
|
||||
max-height: ${legendRowHeight}px
|
||||
overflow: hidden;
|
||||
}
|
||||
`
|
||||
export const StyledLegend = styled.div`
|
||||
background-color: #eef1f8;
|
||||
margin-top: -${(legendRowHeight * 2) + 1}px;
|
||||
&.one-row {
|
||||
margin-top: -${legendRowHeight}px;
|
||||
}
|
||||
`
|
||||
export const StyledLegendInlineList = styled(StyledInlineList) `
|
||||
padding: 7px 9px 0px 10px;
|
||||
`
|
||||
export const StyledLegendInlineListItem = styled(StyledInlineListItem) `
|
||||
display: inline-block;
|
||||
margin-bottom: 3px;
|
||||
`
|
||||
export const StyledPickerListItem = styled(StyledInlineListItem)`
|
||||
padding-right: 5px;
|
||||
padding-left: 0;
|
||||
vertical-align: middle;
|
||||
line-height: 0;
|
||||
`
|
||||
|
||||
export const StyledPickerSelector = styled.a`
|
||||
background-color: #aaa;
|
||||
display: inline-block;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
margin-top: 1px;
|
||||
line-height: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0.4;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
export const StyledCircleSelector = styled(StyledPickerSelector)`
|
||||
border-radius: 50%;
|
||||
`
|
||||
export const StyledCaptionSelector = styled.a`
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1em;
|
||||
color: #9195A0;
|
||||
border: 1px solid #9195A0;
|
||||
overflow: hidden;
|
||||
border-radius: .25em;
|
||||
margin-right: 0;
|
||||
font-weight: bold;
|
||||
&:hover {
|
||||
border-color: #aaa;
|
||||
color: #aaa;
|
||||
text-decoration: none;
|
||||
}
|
||||
&.active {
|
||||
color: white;
|
||||
background-color: #9195A0;
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledFullSizeContainer = styled.div`
|
||||
height: 100%;
|
||||
padding-top: ${(legendRowHeight * 2) + 1}px;
|
||||
padding-bottom: ${props => props.forcePaddingBottom ? (props.forcePaddingBottom + 2 * pMarginTop) + 'px' : '39px'};
|
||||
&.one-legend-row {
|
||||
padding-top: ${legendRowHeight}px;
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledInspectorFooterStatusMessage = styled.div`
|
||||
font-weight: bold;
|
||||
`
|
||||
|
||||
export const StyledZoomHolder = styled.div`
|
||||
position: absolute;
|
||||
bottom: 39px;
|
||||
right: 0;
|
||||
padding: 6px 6px 0 6px;
|
||||
border-left: #e6e9ef solid 1px;
|
||||
border-top: #e6e9ef solid 1px;
|
||||
background: #fff;
|
||||
`
|
||||
|
||||
export const StyledZoomButton = styled.button`
|
||||
display: list-item;
|
||||
list-style-type: none;
|
||||
font-size: 2em;
|
||||
margin-bottom: 10px;
|
||||
border: none;
|
||||
color: #9b9da2;
|
||||
background: transparent;
|
||||
border-color: black;
|
||||
padding: 2px 6px 3px;
|
||||
&:hover {
|
||||
color:black;
|
||||
}
|
||||
&:focus {
|
||||
outline: none
|
||||
}
|
||||
&.faded {
|
||||
opacity: .3;
|
||||
cursor: auto;
|
||||
&:hover {
|
||||
color: #9b9da2;
|
||||
}
|
||||
}
|
||||
`
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Created by chendaye666 on 17/7/31.
|
||||
*/
|
||||
|
||||
export const font = {
|
||||
|
||||
}
|
||||
|
||||
export const dim = {
|
||||
// Editor bar
|
||||
editorbarHeight: 70,
|
||||
// Frame
|
||||
frameBodyHeight: 550,
|
||||
frameTitlebarHeight: 39,
|
||||
frameStatusbarHeight: 39,
|
||||
frameBodyPadding: 20
|
||||
}
|
|
@ -0,0 +1,482 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export default function neoGraphStyle () {
|
||||
const defaultStyle = {
|
||||
'node': {
|
||||
'diameter': '50px',
|
||||
'color': '#A5ABB6',
|
||||
'border-color': '#9AA1AC',
|
||||
'border-width': '2px',
|
||||
'text-color-internal': '#FFFFFF',
|
||||
'font-size': '10px'
|
||||
},
|
||||
'relationship': {
|
||||
'color': '#A5ABB6',
|
||||
'shaft-width': '1px',
|
||||
'font-size': '8px',
|
||||
'padding': '3px',
|
||||
'text-color-external': '#000000',
|
||||
'text-color-internal': '#FFFFFF',
|
||||
'caption': '<type>'
|
||||
}
|
||||
}
|
||||
const defaultSizes = [
|
||||
{
|
||||
diameter: '10px'
|
||||
}, {
|
||||
diameter: '20px'
|
||||
}, {
|
||||
diameter: '50px'
|
||||
}, {
|
||||
diameter: '65px'
|
||||
}, {
|
||||
diameter: '80px'
|
||||
}
|
||||
]
|
||||
const defaultIconCodes = [
|
||||
{
|
||||
'icon-code': 'a'
|
||||
}, {
|
||||
'icon-code': '"'
|
||||
}, {
|
||||
'icon-code': 'z'
|
||||
}, {
|
||||
'icon-code': '_'
|
||||
}, {
|
||||
'icon-code': '/'
|
||||
}, {
|
||||
'icon-code': '>'
|
||||
}, {
|
||||
'icon-code': 'k'
|
||||
}
|
||||
]
|
||||
const defaultArrayWidths = [
|
||||
{
|
||||
'shaft-width': '1px'
|
||||
}, {
|
||||
'shaft-width': '2px'
|
||||
}, {
|
||||
'shaft-width': '3px'
|
||||
}, {
|
||||
'shaft-width': '5px'
|
||||
}, {
|
||||
'shaft-width': '8px'
|
||||
}, {
|
||||
'shaft-width': '13px'
|
||||
}, {
|
||||
'shaft-width': '25px'
|
||||
}, {
|
||||
'shaft-width': '38px'
|
||||
}
|
||||
]
|
||||
const defaultColors = [
|
||||
{
|
||||
color: '#A5ABB6',
|
||||
'border-color': '#9AA1AC',
|
||||
'text-color-internal': '#FFFFFF'
|
||||
}, {
|
||||
color: '#68BDF6',
|
||||
'border-color': '#5CA8DB',
|
||||
'text-color-internal': '#FFFFFF'
|
||||
}, {
|
||||
color: '#6DCE9E',
|
||||
'border-color': '#60B58B',
|
||||
'text-color-internal': '#FFFFFF'
|
||||
}, {
|
||||
color: '#FF756E',
|
||||
'border-color': '#E06760',
|
||||
'text-color-internal': '#FFFFFF'
|
||||
}, {
|
||||
color: '#DE9BF9',
|
||||
'border-color': '#BF85D6',
|
||||
'text-color-internal': '#FFFFFF'
|
||||
}, {
|
||||
color: '#FB95AF',
|
||||
'border-color': '#E0849B',
|
||||
'text-color-internal': '#FFFFFF'
|
||||
}, {
|
||||
color: '#FFD86E',
|
||||
'border-color': '#EDBA39',
|
||||
'text-color-internal': '#604A0E'
|
||||
}
|
||||
]
|
||||
const Selector = (function () {
|
||||
function Selector (tag1, classes1) {
|
||||
this.tag = tag1
|
||||
this.classes = classes1 != null ? classes1 : []
|
||||
}
|
||||
|
||||
Selector.prototype.toString = function () {
|
||||
let str = this.tag
|
||||
for (let i = 0; i < this.classes.length; i++) {
|
||||
const classs = this.classes[i]
|
||||
if (classs != null) {
|
||||
str += '.' + classs
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
return Selector
|
||||
})()
|
||||
|
||||
const StyleRule = (function () {
|
||||
function StyleRule (selector1, props1) {
|
||||
this.selector = selector1
|
||||
this.props = props1
|
||||
}
|
||||
|
||||
StyleRule.prototype.matches = function (selector) {
|
||||
if (this.selector.tag !== selector.tag) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < this.selector.classes.length; i++) {
|
||||
const classs = this.selector.classes[i]
|
||||
if ((classs != null) && selector.classes.indexOf(classs) === -1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
StyleRule.prototype.matchesExact = function (selector) {
|
||||
return this.matches(selector) && this.selector.classes.length === selector.classes.length
|
||||
}
|
||||
|
||||
return StyleRule
|
||||
})()
|
||||
|
||||
const StyleElement = (function () {
|
||||
function StyleElement (selector) {
|
||||
this.selector = selector
|
||||
this.props = {}
|
||||
}
|
||||
|
||||
StyleElement.prototype.applyRules = function (rules) {
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i]
|
||||
if (rule.matches(this.selector)) {
|
||||
this.props = {...this.props, ...rule.props}
|
||||
this.props.caption = this.props.caption || this.props.defaultCaption
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
StyleElement.prototype.get = function (attr) {
|
||||
return this.props[attr] || ''
|
||||
}
|
||||
|
||||
return StyleElement
|
||||
})()
|
||||
|
||||
const GraphStyle = (function () {
|
||||
function GraphStyle () {
|
||||
this.rules = []
|
||||
try {
|
||||
this.loadRules()
|
||||
} catch (_error) {
|
||||
// e = _error
|
||||
}
|
||||
}
|
||||
|
||||
const parseSelector = function (key) {
|
||||
let tokens = key.split('.')
|
||||
return new Selector(tokens[0], tokens.slice(1))
|
||||
}
|
||||
|
||||
const selector = function (item) {
|
||||
if (item.isNode) {
|
||||
return nodeSelector(item)
|
||||
} else if (item.isRelationship) {
|
||||
return relationshipSelector(item)
|
||||
}
|
||||
}
|
||||
|
||||
const nodeSelector = function (node) {
|
||||
node = node || {}
|
||||
const classes = node.labels != null ? node.labels : []
|
||||
return new Selector('node', classes)
|
||||
}
|
||||
|
||||
const relationshipSelector = function (rel) {
|
||||
rel = rel || {}
|
||||
const classes = rel.type != null ? [rel.type] : []
|
||||
return new Selector('relationship', classes)
|
||||
}
|
||||
|
||||
const findRule = function (selector, rules) {
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
let rule = rules[i]
|
||||
if (rule.matchesExact(selector)) {
|
||||
return rule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const findAvailableDefaultColor = function (rules) {
|
||||
const usedColors = rules.filter((rule) => { return rule.props.color != null }).map((rule) => {
|
||||
return rule.props.color
|
||||
})
|
||||
let index = usedColors.length - 1 > defaultColors ? 0 : usedColors.length - 1
|
||||
return defaultColors[index]
|
||||
}
|
||||
|
||||
const getDefaultNodeCaption = function (item) {
|
||||
if (!item || !(item.propertyList != null ? item.propertyList.length : 0) > 0) {
|
||||
return {
|
||||
defaultCaption: '<id>'
|
||||
}
|
||||
}
|
||||
const captionPrioOrder = [/^name$/i, /^title$/i, /^label$/i, /name$/i, /description$/i, /^.+/]
|
||||
let defaultCaption = captionPrioOrder.reduceRight(function (leading, current) {
|
||||
let hits = item.propertyList.filter(function (prop) {
|
||||
return current.test(prop.key)
|
||||
})
|
||||
if (hits.length) {
|
||||
return '{' + hits[0].key + '}'
|
||||
} else {
|
||||
return leading
|
||||
}
|
||||
}, '')
|
||||
defaultCaption || (defaultCaption = '<id>')
|
||||
return {
|
||||
caption: defaultCaption
|
||||
}
|
||||
}
|
||||
|
||||
GraphStyle.prototype.calculateStyle = function (selector) {
|
||||
return new StyleElement(selector).applyRules(this.rules)
|
||||
}
|
||||
|
||||
GraphStyle.prototype.forEntity = function (item) {
|
||||
return this.calculateStyle(selector(item))
|
||||
}
|
||||
|
||||
GraphStyle.prototype.setDefaultNodeStyling = function (selector, item) {
|
||||
let defaultColor = true
|
||||
let defaultCaption = true
|
||||
for (let i = 0; i < this.rules.length; i++) {
|
||||
let rule = this.rules[i]
|
||||
if (rule.selector.classes.length > 0 && rule.matches(selector)) {
|
||||
if (rule.props.hasOwnProperty('color')) {
|
||||
defaultColor = false
|
||||
}
|
||||
if (rule.props.hasOwnProperty('caption')) {
|
||||
defaultCaption = false
|
||||
}
|
||||
}
|
||||
}
|
||||
const minimalSelector = new Selector(selector.tag, selector.classes.sort().slice(0, 1))
|
||||
if (defaultColor) {
|
||||
this.changeForSelector(minimalSelector, findAvailableDefaultColor(this.rules))
|
||||
}
|
||||
if (defaultCaption) {
|
||||
return this.changeForSelector(minimalSelector, getDefaultNodeCaption(item))
|
||||
}
|
||||
}
|
||||
|
||||
GraphStyle.prototype.changeForSelector = function (selector, props) {
|
||||
let rule = findRule(selector, this.rules)
|
||||
if (rule == null) {
|
||||
rule = new StyleRule(selector, props)
|
||||
this.rules.push(rule)
|
||||
}
|
||||
rule.props = {...rule.props, ...props}
|
||||
return rule
|
||||
}
|
||||
|
||||
GraphStyle.prototype.destroyRule = function (rule) {
|
||||
const idx = this.rules.indexOf(rule)
|
||||
if (idx != null) {
|
||||
this.rules.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
GraphStyle.prototype.importGrass = function (string) {
|
||||
try {
|
||||
const rules = this.parse(string)
|
||||
return this.loadRules(rules)
|
||||
} catch (_error) {
|
||||
// e = _error
|
||||
}
|
||||
}
|
||||
|
||||
GraphStyle.prototype.parse = function (string) {
|
||||
const chars = string.split('')
|
||||
let insideString = false
|
||||
let insideProps = false
|
||||
let keyword = ''
|
||||
let props = ''
|
||||
let rules = {}
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const c = chars[i]
|
||||
let skipThis = true
|
||||
switch (c) {
|
||||
case '{':
|
||||
if (!insideString) {
|
||||
insideProps = true
|
||||
} else {
|
||||
skipThis = false
|
||||
}
|
||||
break
|
||||
case '}':
|
||||
if (!insideString) {
|
||||
insideProps = false
|
||||
rules[keyword] = props
|
||||
keyword = ''
|
||||
props = ''
|
||||
} else {
|
||||
skipThis = false
|
||||
}
|
||||
break
|
||||
case "'":
|
||||
case '\'':
|
||||
insideString ^= true
|
||||
break
|
||||
default:
|
||||
skipThis = false
|
||||
}
|
||||
if (skipThis) {
|
||||
continue
|
||||
}
|
||||
if (insideProps) {
|
||||
props += c
|
||||
} else {
|
||||
if (!c.match(/[\s\n]/)) {
|
||||
keyword += c
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let k in rules) {
|
||||
const v = rules[k]
|
||||
rules[k] = {}
|
||||
v.split(';').forEach((prop) => {
|
||||
const [key, val] = prop.split(':')
|
||||
if (key && val) {
|
||||
rules[k][key.trim()] = val.trim()
|
||||
}
|
||||
})
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
GraphStyle.prototype.resetToDefault = function () {
|
||||
this.loadRules()
|
||||
return true
|
||||
}
|
||||
|
||||
GraphStyle.prototype.toSheet = function () {
|
||||
let sheet = {}
|
||||
this.rules.forEach((rule) => {
|
||||
sheet[rule.selector.toString()] = rule.props
|
||||
})
|
||||
return sheet
|
||||
}
|
||||
|
||||
GraphStyle.prototype.toString = function () {
|
||||
let str = ''
|
||||
this.rules.forEach((r) => {
|
||||
str += r.selector.toString() + ' {\n'
|
||||
for (let k in r.props) {
|
||||
let v = r.props[k]
|
||||
if (k === 'caption') {
|
||||
v = "'" + v + "'"
|
||||
}
|
||||
str += ' ' + k + ': ' + v + ';\n'
|
||||
}
|
||||
str += '}\n\n'
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
GraphStyle.prototype.loadRules = function (data) {
|
||||
if (typeof data !== 'object') {
|
||||
data = defaultStyle
|
||||
}
|
||||
this.rules.length = 0
|
||||
for (let key in data) {
|
||||
const props = data[key]
|
||||
this.rules.push(new StyleRule(parseSelector(key), props))
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
GraphStyle.prototype.defaultSizes = function () {
|
||||
return defaultSizes
|
||||
}
|
||||
|
||||
GraphStyle.prototype.defaultIconCodes = function () {
|
||||
return defaultIconCodes
|
||||
}
|
||||
|
||||
GraphStyle.prototype.defaultArrayWidths = function () {
|
||||
return defaultArrayWidths
|
||||
}
|
||||
|
||||
GraphStyle.prototype.defaultColors = function () {
|
||||
return defaultColors
|
||||
}
|
||||
|
||||
GraphStyle.prototype.interpolate = function (str, item) {
|
||||
let ips = str.replace(/\{([^{}]*)\}/g, function (a, b) {
|
||||
let r = item.propertyMap[b]
|
||||
if (typeof r === 'object') {
|
||||
return r.join(', ')
|
||||
}
|
||||
if (typeof r === 'string' || typeof r === 'number') {
|
||||
return r
|
||||
}
|
||||
return ''
|
||||
})
|
||||
if (ips.length < 1 && str === '{type}' && item.isRelationship) {
|
||||
ips = '<type>'
|
||||
}
|
||||
if (ips.length < 1 && str === '{id}' && item.isNode) {
|
||||
ips = '<id>'
|
||||
}
|
||||
return ips.replace(/^<(id|type)>$/, function (a, b) {
|
||||
const r = item[b]
|
||||
if (typeof r === 'string' || typeof r === 'number') {
|
||||
return r
|
||||
}
|
||||
return ''
|
||||
})
|
||||
}
|
||||
|
||||
GraphStyle.prototype.forNode = function (node) {
|
||||
node = node || {}
|
||||
const selector = nodeSelector(node)
|
||||
if ((node.labels != null ? node.labels.length : 0) > 0) {
|
||||
this.setDefaultNodeStyling(selector, node)
|
||||
}
|
||||
return this.calculateStyle(selector)
|
||||
}
|
||||
|
||||
GraphStyle.prototype.forRelationship = function (rel) {
|
||||
const selector = relationshipSelector(rel)
|
||||
return this.calculateStyle(selector)
|
||||
}
|
||||
|
||||
return GraphStyle
|
||||
})()
|
||||
return new GraphStyle()
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Created by chendaye666 on 17/7/31.
|
||||
*/
|
||||
|
||||
|
||||
// export Visualization from './Visualization'
|
||||
export {default as Visualization} from './Visualization'
|
|
@ -0,0 +1,51 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
neo.collision = do ->
|
||||
collision = {}
|
||||
|
||||
collide = (node) ->
|
||||
r = node.radius + 10
|
||||
nx1 = node.x - r
|
||||
nx2 = node.x + r
|
||||
ny1 = node.y - r
|
||||
ny2 = node.y + r
|
||||
return (quad, x1, y1, x2, y2) ->
|
||||
if (quad.point && (quad.point != node))
|
||||
x = node.x - quad.point.x
|
||||
y = node.y - quad.point.y
|
||||
l = Math.sqrt(x * x + y * y)
|
||||
r = node.radius + 10 + quad.point.radius
|
||||
if (l < r)
|
||||
l = (l - r) / l * .5
|
||||
node.x -= x *= l
|
||||
node.y -= y *= l
|
||||
quad.point.x += x
|
||||
quad.point.y += y
|
||||
x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1
|
||||
|
||||
collision.avoidOverlap = (nodes) ->
|
||||
q = d3.geom.quadtree(nodes)
|
||||
for n in nodes
|
||||
q.visit collide(n)
|
||||
|
||||
collision
|
|
@ -0,0 +1,135 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.models.Graph
|
||||
constructor: () ->
|
||||
@nodeMap = {}
|
||||
@_nodes = []
|
||||
@relationshipMap = {}
|
||||
@_relationships = []
|
||||
|
||||
nodes: ->
|
||||
@_nodes
|
||||
|
||||
relationships: ->
|
||||
@_relationships
|
||||
|
||||
groupedRelationships: ->
|
||||
class NodePair
|
||||
constructor: (node1, node2) ->
|
||||
@relationships = []
|
||||
if node1.id < node2.id
|
||||
@nodeA = node1
|
||||
@nodeB = node2
|
||||
else
|
||||
@nodeA = node2
|
||||
@nodeB = node1
|
||||
|
||||
isLoop: ->
|
||||
@nodeA is @nodeB
|
||||
|
||||
toString: ->
|
||||
"#{@nodeA.id}:#{@nodeB.id}"
|
||||
groups = {}
|
||||
for relationship in @_relationships
|
||||
nodePair = new NodePair(relationship.source, relationship.target)
|
||||
nodePair = groups[nodePair] ? nodePair
|
||||
nodePair.relationships.push relationship
|
||||
groups[nodePair] = nodePair
|
||||
(pair for ignored, pair of groups)
|
||||
|
||||
addNodes: (nodes) =>
|
||||
for node in nodes
|
||||
if !@findNode(node.id)?
|
||||
@nodeMap[node.id] = node
|
||||
@_nodes.push(node)
|
||||
@
|
||||
|
||||
removeNode: (node) =>
|
||||
if @findNode(node.id)?
|
||||
delete @nodeMap[node.id]
|
||||
@_nodes.splice(@_nodes.indexOf(node), 1)
|
||||
@
|
||||
|
||||
updateNode: (node) =>
|
||||
if @findNode(node.id)?
|
||||
@removeNode node
|
||||
node.expanded = false
|
||||
node.minified = true
|
||||
@addNodes [node]
|
||||
@
|
||||
|
||||
removeConnectedRelationships: (node) =>
|
||||
for r in @findAllRelationshipToNode node
|
||||
@updateNode r.source
|
||||
@updateNode r.target
|
||||
@_relationships.splice(@_relationships.indexOf(r), 1)
|
||||
delete @relationshipMap[r.id]
|
||||
@
|
||||
|
||||
addRelationships: (relationships) =>
|
||||
for relationship in relationships
|
||||
existingRelationship = @findRelationship(relationship.id)
|
||||
if existingRelationship?
|
||||
existingRelationship.internal = false
|
||||
else
|
||||
relationship.internal = false
|
||||
@relationshipMap[relationship.id] = relationship
|
||||
@_relationships.push(relationship)
|
||||
@
|
||||
|
||||
addInternalRelationships: (relationships) =>
|
||||
for relationship in relationships
|
||||
relationship.internal = true
|
||||
if not @findRelationship(relationship.id)?
|
||||
@relationshipMap[relationship.id] = relationship
|
||||
@_relationships.push(relationship)
|
||||
@
|
||||
|
||||
pruneInternalRelationships: =>
|
||||
relationships = @_relationships.filter((relationship) -> not relationship.internal)
|
||||
@relationshipMap = {}
|
||||
@_relationships = []
|
||||
@addRelationships(relationships)
|
||||
|
||||
findNode: (id) => @nodeMap[id]
|
||||
|
||||
findNodeNeighbourIds: (id) =>
|
||||
@_relationships
|
||||
.filter((relationship) -> relationship.source.id is id or relationship.target.id is id)
|
||||
.map((relationship) ->
|
||||
if relationship.target.id is id
|
||||
return relationship.source.id
|
||||
return relationship.target.id
|
||||
)
|
||||
|
||||
findRelationship: (id) => @relationshipMap[id]
|
||||
|
||||
findAllRelationshipToNode: (node) =>
|
||||
@_relationships
|
||||
.filter((relationship) -> relationship.source.id is node.id or relationship.target.id is node.id)
|
||||
|
||||
resetGraph: ->
|
||||
@nodeMap = {}
|
||||
@_nodes = []
|
||||
@relationshipMap = {}
|
||||
@_relationships = []
|
|
@ -0,0 +1,111 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.NeoD3Geometry
|
||||
square = (distance) -> distance * distance
|
||||
|
||||
constructor: (@style) ->
|
||||
@relationshipRouting = new neo.utils.pairwiseArcsRelationshipRouting(@style)
|
||||
|
||||
addShortenedNextWord = (line, word, measure) ->
|
||||
until word.length <= 2
|
||||
word = word.substr(0, word.length - 2) + '\u2026'
|
||||
if measure(word) < line.remainingWidth
|
||||
line.text += " " + word
|
||||
break
|
||||
|
||||
noEmptyLines = (lines) ->
|
||||
for line in lines
|
||||
if line.text.length is 0 then return false
|
||||
true
|
||||
|
||||
fitCaptionIntoCircle = (node, style) ->
|
||||
template = style.forNode(node).get("caption")
|
||||
captionText = style.interpolate(template, node)
|
||||
fontFamily = 'sans-serif'
|
||||
fontSize = parseFloat(style.forNode(node).get('font-size'))
|
||||
lineHeight = fontSize
|
||||
measure = (text) ->
|
||||
neo.utils.measureText(text, fontFamily, fontSize)
|
||||
|
||||
words = captionText.split(" ")
|
||||
|
||||
emptyLine = (lineCount, iLine) ->
|
||||
baseline = (1 + iLine - lineCount / 2) * lineHeight
|
||||
if (style.forNode(node).get("icon-code"))
|
||||
baseline = baseline + node.radius/3
|
||||
constainingHeight = if iLine < lineCount / 2 then baseline - lineHeight else baseline
|
||||
lineWidth = Math.sqrt(square(node.radius) - square(constainingHeight)) * 2
|
||||
{
|
||||
node: node
|
||||
text: ''
|
||||
baseline: baseline
|
||||
remainingWidth: lineWidth
|
||||
}
|
||||
|
||||
fitOnFixedNumberOfLines = (lineCount) ->
|
||||
lines = []
|
||||
iWord = 0;
|
||||
for iLine in [0..lineCount - 1]
|
||||
line = emptyLine(lineCount, iLine)
|
||||
while iWord < words.length and measure(" " + words[iWord]) < line.remainingWidth
|
||||
line.text += " " + words[iWord]
|
||||
line.remainingWidth -= measure(" " + words[iWord])
|
||||
iWord++
|
||||
lines.push line
|
||||
if iWord < words.length
|
||||
addShortenedNextWord(lines[lineCount - 1], words[iWord], measure)
|
||||
[lines, iWord]
|
||||
|
||||
consumedWords = 0
|
||||
maxLines = node.radius * 2 / fontSize
|
||||
|
||||
lines = [emptyLine(1, 0)]
|
||||
for lineCount in [1..maxLines]
|
||||
[candidateLines, candidateWords] = fitOnFixedNumberOfLines(lineCount)
|
||||
if noEmptyLines(candidateLines)
|
||||
[lines, consumedWords] = [candidateLines, candidateWords]
|
||||
if consumedWords >= words.length
|
||||
return lines
|
||||
lines
|
||||
|
||||
formatNodeCaptions: (nodes) ->
|
||||
for node in nodes
|
||||
node.caption = fitCaptionIntoCircle(node, @style)
|
||||
|
||||
formatRelationshipCaptions: (relationships) ->
|
||||
for relationship in relationships
|
||||
template = @style.forRelationship(relationship).get("caption")
|
||||
relationship.caption = @style.interpolate(template, relationship)
|
||||
|
||||
setNodeRadii: (nodes) ->
|
||||
for node in nodes
|
||||
node.radius = parseFloat(@style.forNode(node).get("diameter")) / 2
|
||||
|
||||
onGraphChange: (graph) ->
|
||||
@setNodeRadii(graph.nodes())
|
||||
@formatNodeCaptions(graph.nodes())
|
||||
@formatRelationshipCaptions(graph.relationships())
|
||||
@relationshipRouting.measureRelationshipCaptions(graph.relationships())
|
||||
|
||||
onTick: (graph) ->
|
||||
@relationshipRouting.layoutRelationships(graph)
|
|
@ -0,0 +1,65 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.graphView
|
||||
constructor: (element, measureSize, @graph, @style) ->
|
||||
layout = neo.layout.force()
|
||||
@viz = neo.viz(element, measureSize, @graph, layout, @style)
|
||||
@callbacks = {}
|
||||
callbacks = @callbacks
|
||||
@viz.trigger = do ->
|
||||
(event, args...) ->
|
||||
callback.apply(null, args) for callback in (callbacks[event] or [])
|
||||
|
||||
on: (event, callback) ->
|
||||
(@callbacks[event] ?= []).push(callback)
|
||||
@
|
||||
|
||||
layout: (value) ->
|
||||
return layout unless arguments.length
|
||||
layout = value
|
||||
@
|
||||
|
||||
grass: (value) ->
|
||||
return @style.toSheet() unless arguments.length
|
||||
@style.importGrass(value)
|
||||
@
|
||||
|
||||
update: ->
|
||||
@viz.update()
|
||||
@
|
||||
|
||||
resize: ->
|
||||
@viz.resize()
|
||||
@
|
||||
|
||||
boundingBox: ->
|
||||
@viz.boundingBox()
|
||||
|
||||
collectStats: ->
|
||||
@viz.collectStats()
|
||||
|
||||
zoomIn: (elem) ->
|
||||
@viz.zoomInClick(elem)
|
||||
|
||||
zoomOut: (elem) ->
|
||||
@viz.zoomOutClick(elem)
|
|
@ -0,0 +1,106 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
neo.layout = do ->
|
||||
_layout = {}
|
||||
|
||||
_layout.force = ->
|
||||
_force = {}
|
||||
|
||||
_force.init = (render) ->
|
||||
forceLayout = {}
|
||||
|
||||
linkDistance = 45
|
||||
|
||||
d3force = d3.layout.force()
|
||||
.linkDistance((relationship) -> relationship.source.radius + relationship.target.radius + linkDistance)
|
||||
.charge(-1000)
|
||||
|
||||
newStatsBucket = ->
|
||||
bucket =
|
||||
layoutTime: 0
|
||||
layoutSteps: 0
|
||||
bucket
|
||||
|
||||
currentStats = newStatsBucket()
|
||||
|
||||
forceLayout.collectStats = ->
|
||||
latestStats = currentStats
|
||||
currentStats = newStatsBucket()
|
||||
latestStats
|
||||
|
||||
accelerateLayout = ->
|
||||
maxStepsPerTick = 100
|
||||
maxAnimationFramesPerSecond = 60
|
||||
maxComputeTime = 1000 / maxAnimationFramesPerSecond
|
||||
now = if window.performance and window.performance.now
|
||||
() ->
|
||||
window.performance.now()
|
||||
else
|
||||
() ->
|
||||
Date.now()
|
||||
|
||||
d3Tick = d3force.tick
|
||||
d3force.tick = ->
|
||||
startTick = now()
|
||||
step = maxStepsPerTick
|
||||
while step-- and now() - startTick < maxComputeTime
|
||||
startCalcs = now()
|
||||
currentStats.layoutSteps++
|
||||
|
||||
neo.collision.avoidOverlap d3force.nodes()
|
||||
|
||||
if d3Tick()
|
||||
maxStepsPerTick = 2
|
||||
return true
|
||||
currentStats.layoutTime += now() - startCalcs
|
||||
render()
|
||||
false
|
||||
|
||||
accelerateLayout()
|
||||
|
||||
oneRelationshipPerPairOfNodes = (graph) ->
|
||||
(pair.relationships[0] for pair in graph.groupedRelationships())
|
||||
|
||||
forceLayout.update = (graph, size) ->
|
||||
|
||||
nodes = neo.utils.cloneArray(graph.nodes())
|
||||
relationships = oneRelationshipPerPairOfNodes(graph)
|
||||
|
||||
radius = nodes.length * linkDistance / (Math.PI * 2)
|
||||
center =
|
||||
x: size[0] / 2
|
||||
y: size[1] / 2
|
||||
neo.utils.circularLayout(nodes, center, radius)
|
||||
|
||||
d3force
|
||||
.nodes(nodes)
|
||||
.links(relationships)
|
||||
.size(size)
|
||||
.start()
|
||||
|
||||
forceLayout.drag = d3force.drag
|
||||
forceLayout
|
||||
|
||||
_force
|
||||
|
||||
_layout
|
|
@ -0,0 +1,39 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.models.Node
|
||||
constructor: (@id, @labels, properties) ->
|
||||
@propertyMap = properties
|
||||
@propertyList = for own key,value of properties
|
||||
{ key: key, value: value }
|
||||
|
||||
toJSON: ->
|
||||
@propertyMap
|
||||
|
||||
isNode: true
|
||||
isRelationship: false
|
||||
|
||||
relationshipCount: (graph) ->
|
||||
node = @
|
||||
rels = []
|
||||
rels.push[relationship] for relationship in graph.relationships() when relationship.source is node or relationship.target is node
|
||||
rels.length
|
|
@ -0,0 +1,620 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
neo.queryPlan = (element)->
|
||||
|
||||
maxChildOperators = 2 # Fact we know about the cypher compiler
|
||||
maxComparableRows = 1000000 # link widths are comparable between plans if all operators are below this row count
|
||||
maxComparableDbHits = 1000000 # db hits are comparable between plans if all operators are below this db hit count
|
||||
|
||||
operatorWidth = 180
|
||||
operatorCornerRadius = 4
|
||||
operatorHeaderHeight = 18
|
||||
operatorHeaderFontSize = 11
|
||||
operatorDetailHeight = 14
|
||||
maxCostHeight = 50
|
||||
detailFontSize = 10
|
||||
operatorMargin = 50
|
||||
operatorPadding = 3
|
||||
rankMargin = 50
|
||||
margin = 10
|
||||
standardFont = "'Helvetica Neue',Helvetica,Arial,sans-serif"
|
||||
fixedWidthFont = "Monaco,'Courier New',Terminal,monospace"
|
||||
linkColor = '#DFE1E3'
|
||||
costColor = '#F25A29'
|
||||
dividerColor = '#DFE1E3'
|
||||
operatorColors = ["#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#08519c","#08306b"]
|
||||
|
||||
operatorCategories =
|
||||
result: ['result']
|
||||
seek: ['scan', 'seek', 'argument']
|
||||
rows: ['limit', 'top', 'skip', 'sort', 'union', 'projection']
|
||||
other: []
|
||||
filter: ['select', 'filter', 'apply', 'distinct']
|
||||
expand: ['expand', 'product', 'join', 'optional', 'path']
|
||||
eager: ['eager']
|
||||
|
||||
augment = (color) ->
|
||||
{
|
||||
color: color,
|
||||
'border-color': d3.rgb(color).darker(),
|
||||
'text-color-internal': if d3.hsl(color).l < 0.7 then '#FFFFFF' else '#000000'
|
||||
}
|
||||
|
||||
colors =
|
||||
d3.scale.ordinal()
|
||||
.domain(d3.keys(operatorCategories))
|
||||
.range(operatorColors);
|
||||
|
||||
color = (d) ->
|
||||
for name, keywords of operatorCategories
|
||||
for keyword in keywords
|
||||
if new RegExp(keyword, 'i').test(d)
|
||||
return augment(colors(name))
|
||||
augment(colors('other'))
|
||||
|
||||
rows = (operator) ->
|
||||
operator.Rows ? operator.EstimatedRows ? 0
|
||||
|
||||
plural = (noun, count) ->
|
||||
if count is 1 then noun else noun + 's'
|
||||
|
||||
formatNumber = d3.format(",.0f")
|
||||
|
||||
operatorDetails = (operator) ->
|
||||
return [] unless operator.expanded
|
||||
|
||||
details = []
|
||||
|
||||
wordWrap = (string, className) ->
|
||||
measure = (text) ->
|
||||
neo.utils.measureText(text, fixedWidthFont, 10)
|
||||
|
||||
words = string.split(/([^a-zA-Z\d])/)
|
||||
|
||||
firstWord = 0
|
||||
lastWord = 1
|
||||
while firstWord < words.length
|
||||
while lastWord < words.length and measure(words.slice(firstWord, lastWord + 1).join('')) < operatorWidth - operatorPadding * 2
|
||||
lastWord++
|
||||
details.push { className: className, value: words.slice(firstWord, lastWord).join('') }
|
||||
firstWord = lastWord
|
||||
lastWord = firstWord + 1
|
||||
|
||||
if identifiers = operator.identifiers ? operator.KeyNames?.split(', ')
|
||||
wordWrap(identifiers.filter((d) -> not (/^ /.test(d))).join(', '), 'identifiers')
|
||||
details.push { className: 'padding' }
|
||||
|
||||
if index = operator.Index
|
||||
wordWrap(index, 'index')
|
||||
details.push { className: 'padding' }
|
||||
|
||||
if expression = operator.LegacyExpression ? operator.ExpandExpression ? operator.LabelName ? operator.Signature
|
||||
wordWrap(expression, 'expression')
|
||||
details.push { className: 'padding' }
|
||||
|
||||
if operator.Rows? and operator.EstimatedRows?
|
||||
details.push { className: 'estimated-rows', key: 'estimated rows', value: formatNumber(operator.EstimatedRows)}
|
||||
if operator.DbHits? and not operator.alwaysShowCost
|
||||
details.push { className: 'db-hits', key: plural('db hit', operator.DbHits || 0), value: formatNumber(operator.DbHits || 0)}
|
||||
|
||||
if details.length and details[details.length - 1].className == 'padding'
|
||||
details.pop()
|
||||
|
||||
y = operatorDetailHeight
|
||||
for detail in details
|
||||
detail.y = y
|
||||
y += if detail.className == 'padding'
|
||||
operatorPadding * 2
|
||||
else
|
||||
operatorDetailHeight
|
||||
|
||||
details
|
||||
|
||||
transform = (queryPlan) ->
|
||||
operators = []
|
||||
links = []
|
||||
|
||||
result =
|
||||
operatorType: 'Result'
|
||||
children: [queryPlan.root]
|
||||
|
||||
collectLinks = (operator, rank) ->
|
||||
operators.push operator
|
||||
operator.rank = rank
|
||||
for child in operator.children
|
||||
child.parent = operator
|
||||
collectLinks child, rank + 1
|
||||
links.push
|
||||
source: child
|
||||
target: operator
|
||||
|
||||
collectLinks result, 0
|
||||
|
||||
[operators, links]
|
||||
|
||||
layout = (operators, links) ->
|
||||
costHeight = do ->
|
||||
scale = d3.scale.log()
|
||||
.domain([1, Math.max(d3.max(operators, (operator) -> operator.DbHits or 0), maxComparableDbHits)])
|
||||
.range([0, maxCostHeight])
|
||||
(operator) ->
|
||||
scale((operator.DbHits ? 0) + 1)
|
||||
|
||||
operatorHeight = (operator) ->
|
||||
height = operatorHeaderHeight
|
||||
if operator.expanded
|
||||
height += operatorDetails(operator).slice(-1)[0].y + operatorPadding * 2
|
||||
height += costHeight(operator)
|
||||
height
|
||||
|
||||
linkWidth = do ->
|
||||
scale = d3.scale.log()
|
||||
.domain([1, Math.max(d3.max(operators, (operator) -> rows(operator) + 1), maxComparableRows)])
|
||||
.range([2, (operatorWidth - operatorCornerRadius * 2) / maxChildOperators])
|
||||
(operator) ->
|
||||
scale(rows(operator) + 1)
|
||||
|
||||
for operator in operators
|
||||
operator.height = operatorHeight(operator)
|
||||
operator.costHeight = costHeight(operator)
|
||||
if operator.costHeight > operatorDetailHeight + operatorPadding
|
||||
operator.alwaysShowCost = true
|
||||
childrenWidth = d3.sum(operator.children, linkWidth)
|
||||
tx = (operatorWidth - childrenWidth) / 2
|
||||
for child in operator.children
|
||||
child.tx = tx
|
||||
tx += linkWidth(child)
|
||||
|
||||
for link in links
|
||||
link.width = linkWidth(link.source)
|
||||
|
||||
ranks = d3.nest()
|
||||
.key((operator) -> operator.rank)
|
||||
.entries(operators)
|
||||
|
||||
currentY = 0
|
||||
|
||||
for rank in ranks
|
||||
currentY -= (d3.max(rank.values, operatorHeight) + rankMargin)
|
||||
for operator in rank.values
|
||||
operator.x = 0
|
||||
operator.y = currentY
|
||||
|
||||
width = d3.max(ranks.map((rank) -> rank.values.length * (operatorWidth + operatorMargin)))
|
||||
height = -currentY
|
||||
|
||||
collide = ->
|
||||
for rank in ranks
|
||||
x0 = 0
|
||||
for operator in rank.values
|
||||
dx = x0 - operator.x
|
||||
if dx > 0
|
||||
operator.x += dx
|
||||
x0 = operator.x + operatorWidth + operatorMargin
|
||||
|
||||
dx = x0 - operatorMargin - width
|
||||
if dx > 0
|
||||
lastOperator = rank.values[rank.values.length - 1]
|
||||
x0 = lastOperator.x -= dx
|
||||
for i in [rank.values.length - 2..0] by -1
|
||||
operator = rank.values[i]
|
||||
dx = operator.x + operatorWidth + operatorMargin - x0
|
||||
if dx > 0
|
||||
operator.x -= operatorWidth
|
||||
x0 = operator.x
|
||||
|
||||
center = (operator) ->
|
||||
operator.x + operatorWidth / 2
|
||||
|
||||
relaxUpwards = (alpha) ->
|
||||
for rank in ranks
|
||||
for operator in rank.values
|
||||
if operator.children.length
|
||||
x = d3.sum(operator.children, (child) -> linkWidth(child) * center(child)) / d3.sum(operator.children, linkWidth)
|
||||
operator.x += (x - center(operator)) * alpha
|
||||
|
||||
relaxDownwards = (alpha) ->
|
||||
for rank in ranks.slice().reverse()
|
||||
for operator in rank.values
|
||||
if operator.parent
|
||||
operator.x += (center(operator.parent) - center(operator)) * alpha
|
||||
|
||||
collide()
|
||||
iterations = 300
|
||||
alpha = 1
|
||||
while iterations--
|
||||
relaxUpwards(alpha)
|
||||
collide()
|
||||
relaxDownwards(alpha)
|
||||
collide()
|
||||
alpha *= .98
|
||||
|
||||
width = d3.max(operators, (o) -> o.x) - d3.min(operators, (o) -> o.x) + operatorWidth
|
||||
|
||||
[width, height]
|
||||
|
||||
render = (operators, links, width, height, redisplay) ->
|
||||
svg = d3.select(element)
|
||||
|
||||
svg.transition()
|
||||
.attr('width', width + margin * 2)
|
||||
.attr('height', height + margin * 2)
|
||||
.attr('viewBox', [d3.min(operators, (o) -> o.x) - margin, -margin - height, width + margin * 2, height + margin * 2].join(' '))
|
||||
|
||||
join = (parent, children) ->
|
||||
for child in d3.entries(children)
|
||||
selection = parent.selectAll(child.key).data(child.value.data)
|
||||
child.value.selections(selection.enter(), selection, selection.exit())
|
||||
if child.value.children
|
||||
join(selection, child.value.children)
|
||||
|
||||
join(svg, {
|
||||
'g.layer.links':
|
||||
data: [links]
|
||||
selections: (enter) ->
|
||||
enter.append('g')
|
||||
.attr('class', 'layer links')
|
||||
children:
|
||||
|
||||
'.link':
|
||||
data: ((d) -> d),
|
||||
selections: (enter) ->
|
||||
enter.append('g')
|
||||
.attr('class', 'link')
|
||||
children:
|
||||
|
||||
'path':
|
||||
data: (d) -> [d]
|
||||
selections: (enter, update) ->
|
||||
enter
|
||||
.append('path')
|
||||
.attr('fill', linkColor)
|
||||
|
||||
update
|
||||
.transition()
|
||||
.attr('d', (d) ->
|
||||
width = Math.max(1, d.width)
|
||||
sourceX = d.source.x + operatorWidth / 2
|
||||
targetX = d.target.x + d.source.tx
|
||||
|
||||
sourceY = d.source.y + d.source.height
|
||||
targetY = d.target.y
|
||||
yi = d3.interpolateNumber(sourceY, targetY)
|
||||
|
||||
curvature = .5
|
||||
control1 = yi(curvature)
|
||||
control2 = yi(1 - curvature)
|
||||
controlWidth = Math.min(width / Math.PI, (targetY - sourceY) / Math.PI)
|
||||
if sourceX > targetX + width / 2
|
||||
controlWidth *= -1
|
||||
|
||||
[
|
||||
'M', (sourceX + width / 2), sourceY,
|
||||
'C', (sourceX + width / 2), control1 - controlWidth,
|
||||
(targetX + width), control2 - controlWidth,
|
||||
(targetX + width), targetY,
|
||||
'L', targetX, targetY,
|
||||
'C', targetX, control2 + controlWidth,
|
||||
(sourceX - width / 2), control1 + controlWidth,
|
||||
(sourceX - width / 2), sourceY,
|
||||
'Z'
|
||||
].join(' '))
|
||||
|
||||
'text':
|
||||
data: (d) ->
|
||||
x = d.source.x + operatorWidth / 2
|
||||
y = d.source.y + d.source.height + operatorDetailHeight
|
||||
source = d.source
|
||||
if source.Rows? or source.EstimatedRows?
|
||||
[key, caption] = if source.Rows?
|
||||
['Rows', 'row']
|
||||
else
|
||||
['EstimatedRows', 'estimated row']
|
||||
[
|
||||
{ x: x, y: y, text: formatNumber(source[key]) + '\u00A0', anchor: 'end' }
|
||||
{ x: x, y: y, text: plural(caption, source[key]), anchor: 'start' }
|
||||
]
|
||||
else
|
||||
[]
|
||||
selections: (enter, update) ->
|
||||
enter
|
||||
.append('text')
|
||||
.attr('font-size', detailFontSize)
|
||||
.attr('font-family', standardFont)
|
||||
|
||||
update
|
||||
.transition()
|
||||
.attr('x', (d) -> d.x)
|
||||
.attr('y', (d) -> d.y)
|
||||
.attr('text-anchor', (d) -> d.anchor)
|
||||
.text((d) -> d.text)
|
||||
|
||||
'g.layer.operators':
|
||||
data: [operators]
|
||||
selections: (enter) ->
|
||||
enter.append('g')
|
||||
.attr('class', 'layer operators')
|
||||
children:
|
||||
|
||||
'.operator':
|
||||
data: ((d) -> d)
|
||||
selections: (enter, update) ->
|
||||
enter
|
||||
.append('g')
|
||||
.attr('class', 'operator')
|
||||
|
||||
update
|
||||
.transition()
|
||||
.attr('transform', (d) -> "translate(#{d.x},#{d.y})")
|
||||
children:
|
||||
|
||||
'rect.background':
|
||||
data: (d) -> [d]
|
||||
selections: (enter, update) ->
|
||||
enter
|
||||
.append('rect')
|
||||
.attr('class', 'background')
|
||||
|
||||
update
|
||||
.transition()
|
||||
.attr('width', operatorWidth)
|
||||
.attr('height', (d) -> d.height)
|
||||
.attr('rx', operatorCornerRadius)
|
||||
.attr('ry', operatorCornerRadius)
|
||||
.attr('fill', 'white')
|
||||
.style('stroke', 'none')
|
||||
|
||||
'g.header':
|
||||
data: (d) -> [d]
|
||||
selections: (enter) ->
|
||||
enter
|
||||
.append('g')
|
||||
.attr('class', 'header')
|
||||
.attr('pointer-events', 'all')
|
||||
.on('click', (d) ->
|
||||
d.expanded = !d.expanded
|
||||
redisplay()
|
||||
)
|
||||
children:
|
||||
|
||||
'path.banner':
|
||||
data: (d) -> [d]
|
||||
selections: (enter, update) ->
|
||||
enter
|
||||
.append('path')
|
||||
.attr('class', 'banner')
|
||||
|
||||
update
|
||||
.attr('d', (d) ->
|
||||
shaving =
|
||||
if d.height <= operatorHeaderHeight
|
||||
operatorCornerRadius
|
||||
else if d.height < operatorHeaderHeight + operatorCornerRadius
|
||||
operatorCornerRadius - Math.sqrt(Math.pow(operatorCornerRadius, 2) -
|
||||
Math.pow(operatorCornerRadius - d.height + operatorHeaderHeight, 2))
|
||||
else 0
|
||||
[
|
||||
'M', operatorWidth - operatorCornerRadius, 0
|
||||
'A', operatorCornerRadius, operatorCornerRadius, 0, 0, 1, operatorWidth, operatorCornerRadius
|
||||
'L', operatorWidth, operatorHeaderHeight - operatorCornerRadius
|
||||
'A', operatorCornerRadius, operatorCornerRadius, 0, 0, 1, operatorWidth - shaving, operatorHeaderHeight
|
||||
'L', shaving, operatorHeaderHeight
|
||||
'A', operatorCornerRadius, operatorCornerRadius, 0, 0, 1, 0, operatorHeaderHeight - operatorCornerRadius
|
||||
'L', 0, operatorCornerRadius
|
||||
'A', operatorCornerRadius, operatorCornerRadius, 0, 0, 1, operatorCornerRadius, 0
|
||||
'Z'
|
||||
].join(' '))
|
||||
.style('fill', (d) -> color(d.operatorType).color)
|
||||
|
||||
'path.expand':
|
||||
data: (d) -> if d.operatorType is 'Result' then [] else [d]
|
||||
selections: (enter, update) ->
|
||||
rotateForExpand = (d) ->
|
||||
d3.transform()
|
||||
"translate(#{operatorHeaderHeight / 2}, #{operatorHeaderHeight / 2}) " +
|
||||
"rotate(#{if d.expanded then 90 else 0}) " +
|
||||
"scale(0.5)"
|
||||
|
||||
enter
|
||||
.append('path')
|
||||
.attr('class', 'expand')
|
||||
.attr('fill', (d) -> color(d.operatorType)['text-color-internal'])
|
||||
.attr('d', 'M -5 -10 L 8.66 0 L -5 10 Z')
|
||||
.attr('transform', rotateForExpand)
|
||||
|
||||
update
|
||||
.transition()
|
||||
.attrTween('transform', (d, i, a) ->
|
||||
d3.interpolateString(a, rotateForExpand(d))
|
||||
)
|
||||
|
||||
'text.title':
|
||||
data: (d) -> [d]
|
||||
selections: (enter) ->
|
||||
enter
|
||||
.append('text')
|
||||
.attr('class', 'title')
|
||||
.attr('font-size', operatorHeaderFontSize)
|
||||
.attr('font-family', standardFont)
|
||||
.attr('x', operatorHeaderHeight)
|
||||
.attr('y', 13)
|
||||
.attr('fill', (d) -> color(d.operatorType)['text-color-internal'])
|
||||
.text((d) -> d.operatorType)
|
||||
|
||||
'g.detail':
|
||||
data: operatorDetails
|
||||
selections: (enter, update, exit) ->
|
||||
enter
|
||||
.append('g')
|
||||
|
||||
update
|
||||
.attr('class', (d) -> 'detail ' + d.className)
|
||||
.attr('transform', (d) -> "translate(0, #{operatorHeaderHeight + d.y})")
|
||||
.attr('font-family', (d) ->
|
||||
if d.className is 'expression' or d.className is 'identifiers'
|
||||
fixedWidthFont
|
||||
else
|
||||
standardFont)
|
||||
|
||||
exit.remove()
|
||||
children:
|
||||
|
||||
'text':
|
||||
data: (d) ->
|
||||
if d.key
|
||||
[
|
||||
{ text: d.value + '\u00A0', anchor: 'end', x: operatorWidth / 2 }
|
||||
{ text: d.key, anchor: 'start', x: operatorWidth / 2 }
|
||||
]
|
||||
else
|
||||
[
|
||||
{ text: d.value, anchor: 'start', x: operatorPadding }
|
||||
]
|
||||
selections: (enter, update, exit) ->
|
||||
enter
|
||||
.append('text')
|
||||
.attr('font-size', detailFontSize)
|
||||
|
||||
update
|
||||
.attr('x', (d) -> d.x)
|
||||
.attr('text-anchor', (d) -> d.anchor)
|
||||
.attr('fill', 'black')
|
||||
.transition()
|
||||
.each('end', ->
|
||||
update
|
||||
.text((d) -> d.text)
|
||||
)
|
||||
|
||||
exit.remove()
|
||||
|
||||
'path.divider':
|
||||
data: (d) ->
|
||||
if (d.className == 'padding')
|
||||
[d]
|
||||
else
|
||||
[]
|
||||
selections: (enter, update) ->
|
||||
enter
|
||||
.append('path')
|
||||
.attr('class', 'divider')
|
||||
.attr('visibility', 'hidden')
|
||||
|
||||
update
|
||||
.attr('d', [
|
||||
'M', 0, -operatorPadding * 2
|
||||
'L', operatorWidth, -operatorPadding * 2
|
||||
].join(' '))
|
||||
.attr('stroke', dividerColor)
|
||||
.transition()
|
||||
.each('end', ->
|
||||
update
|
||||
.attr('visibility', 'visible')
|
||||
)
|
||||
|
||||
'path.cost':
|
||||
data: (d) -> [d]
|
||||
selections: (enter, update) ->
|
||||
enter
|
||||
.append('path')
|
||||
.attr('class', 'cost')
|
||||
.attr('fill', costColor)
|
||||
|
||||
update
|
||||
.transition()
|
||||
.attr('d', (d) ->
|
||||
if d.costHeight < operatorCornerRadius
|
||||
shaving = operatorCornerRadius -
|
||||
Math.sqrt(Math.pow(operatorCornerRadius, 2) - Math.pow(operatorCornerRadius - d.costHeight, 2))
|
||||
[
|
||||
'M', operatorWidth - shaving, d.height - d.costHeight
|
||||
'A', operatorCornerRadius, operatorCornerRadius, 0, 0, 1, operatorWidth - operatorCornerRadius, d.height
|
||||
'L', operatorCornerRadius, d.height
|
||||
'A', operatorCornerRadius, operatorCornerRadius, 0, 0, 1, shaving, d.height - d.costHeight
|
||||
'Z'
|
||||
].join(' ')
|
||||
else
|
||||
[
|
||||
'M', 0, d.height - d.costHeight
|
||||
'L', operatorWidth, d.height - d.costHeight
|
||||
'L', operatorWidth, d.height - operatorCornerRadius
|
||||
'A', operatorCornerRadius, operatorCornerRadius, 0, 0, 1, operatorWidth - operatorCornerRadius, d.height
|
||||
'L', operatorCornerRadius, d.height
|
||||
'A', operatorCornerRadius, operatorCornerRadius, 0, 0, 1, 0, d.height - operatorCornerRadius
|
||||
'Z'
|
||||
].join(' ')
|
||||
|
||||
)
|
||||
|
||||
'text.cost':
|
||||
data: (d) ->
|
||||
if d.alwaysShowCost
|
||||
y = d.height - d.costHeight + operatorDetailHeight
|
||||
[
|
||||
{ text: formatNumber(d.DbHits) + '\u00A0', anchor: 'end', y: y }
|
||||
{ text: 'db hits', anchor: 'start', y: y }
|
||||
]
|
||||
else
|
||||
[]
|
||||
selections: (enter, update) ->
|
||||
enter
|
||||
.append('text')
|
||||
.attr('class', 'cost')
|
||||
.attr('font-size', detailFontSize)
|
||||
.attr('font-family', standardFont)
|
||||
.attr('fill', 'white')
|
||||
|
||||
update
|
||||
.attr('x', operatorWidth / 2)
|
||||
.attr('text-anchor', (d) -> d.anchor)
|
||||
.transition()
|
||||
.attr('y', (d) -> d.y)
|
||||
.each('end', ->
|
||||
update
|
||||
.text((d) -> d.text)
|
||||
)
|
||||
|
||||
'rect.outline':
|
||||
data: (d) -> [d]
|
||||
selections: (enter, update) ->
|
||||
enter
|
||||
.append('rect')
|
||||
.attr('class', 'outline')
|
||||
|
||||
update
|
||||
.transition()
|
||||
.attr('width', operatorWidth)
|
||||
.attr('height', (d) -> d.height)
|
||||
.attr('rx', operatorCornerRadius)
|
||||
.attr('ry', operatorCornerRadius)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke-width', 1)
|
||||
.style('stroke', (d) -> color(d.operatorType)['border-color'])
|
||||
})
|
||||
|
||||
display = (queryPlan) ->
|
||||
|
||||
[operators, links] = transform(queryPlan)
|
||||
[width, height] = layout(operators, links)
|
||||
render(operators, links, width, height, -> display(queryPlan))
|
||||
@display = display
|
||||
@
|
|
@ -0,0 +1,35 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.models.Relationship
|
||||
constructor: (@id, @source, @target, @type, properties) ->
|
||||
@propertyMap = properties
|
||||
@propertyList = for own key,value of @propertyMap
|
||||
{ key: key, value: value }
|
||||
|
||||
toJSON: ->
|
||||
@propertyMap
|
||||
|
||||
isNode: false
|
||||
isRelationship: true
|
||||
isLoop: ->
|
||||
@source is @target
|
|
@ -0,0 +1,27 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.Renderer
|
||||
constructor: (opts = {})->
|
||||
neo.utils.extend(@, opts)
|
||||
@onGraphChange ?= ->
|
||||
@onTick ?= ->
|
|
@ -0,0 +1,317 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
neo.style = do ->
|
||||
_style = (storage) ->
|
||||
new GraphStyle(storage)
|
||||
|
||||
_style.defaults =
|
||||
autoColor: yes
|
||||
colors: [
|
||||
{ color: '#DFE1E3', 'border-color': '#D4D6D7', 'text-color-internal': '#000000' }
|
||||
{ color: '#F25A29', 'border-color': '#DC4717', 'text-color-internal': '#FFFFFF' }
|
||||
{ color: '#AD62CE', 'border-color': '#9453B1', 'text-color-internal': '#FFFFFF' }
|
||||
{ color: '#30B6AF', 'border-color': '#46A39E', 'text-color-internal': '#FFFFFF' }
|
||||
{ color: '#FF6C7C', 'border-color': '#EB5D6C', 'text-color-internal': '#FFFFFF' }
|
||||
{ color: '#FCC940', 'border-color': '#F3BA25', 'text-color-internal': '#000000' }
|
||||
{ color: '#4356C0', 'border-color': '#3445A2', 'text-color-internal': '#FFFFFF' }
|
||||
]
|
||||
style:
|
||||
'node':
|
||||
'diameter': '40px'
|
||||
'color': '#DFE1E3'
|
||||
'border-color': '#5CA8DB'
|
||||
'border-width': '2px'
|
||||
'text-color-internal': '#000000'
|
||||
'caption': '{id}'
|
||||
'font-size': '10px'
|
||||
'relationship':
|
||||
'color': '#D4D6D7'
|
||||
'shaft-width': '1px'
|
||||
'font-size': '8px'
|
||||
'padding': '3px'
|
||||
'text-color-external': '#5CA8DB'
|
||||
'text-color-internal': '#68BDF6'
|
||||
# 'node':
|
||||
# 'diameter': '40px'
|
||||
# 'color': '#DFE1E3'
|
||||
# 'border-color': '#D4D6D7'
|
||||
# 'border-width': '2px'
|
||||
# 'text-color-internal': '#000000'
|
||||
# 'caption': '{id}'
|
||||
# 'font-size': '10px'
|
||||
# 'relationship':
|
||||
# 'color': '#D4D6D7'
|
||||
# 'shaft-width': '1px'
|
||||
# 'font-size': '8px'
|
||||
# 'padding': '3px'
|
||||
# 'text-color-external': '#000000'
|
||||
# 'text-color-internal': '#FFFFFF'
|
||||
sizes: [
|
||||
{ diameter: '10px' }
|
||||
{ diameter: '20px' }
|
||||
{ diameter: '30px' }
|
||||
{ diameter: '50px' }
|
||||
{ diameter: '80px' }
|
||||
]
|
||||
arrayWidths: [
|
||||
{ 'shaft-width': '1px' }
|
||||
{ 'shaft-width': '2px' }
|
||||
{ 'shaft-width': '3px' }
|
||||
{ 'shaft-width': '5px' }
|
||||
{ 'shaft-width': '8px' }
|
||||
{ 'shaft-width': '13px' }
|
||||
{ 'shaft-width': '25px' }
|
||||
{ 'shaft-width': '38px' }
|
||||
]
|
||||
|
||||
class Selector
|
||||
constructor: (selector) ->
|
||||
[@tag, @klass] = if selector.indexOf('.') > 0
|
||||
selector.split('.')
|
||||
else
|
||||
[selector, undefined]
|
||||
|
||||
toString: ->
|
||||
str = @tag
|
||||
str += ".#{@klass}" if @klass?
|
||||
str
|
||||
|
||||
class StyleRule
|
||||
constructor: (@selector, @props) ->
|
||||
|
||||
matches: (selector) ->
|
||||
if @selector.tag is selector.tag
|
||||
if @selector.klass is selector.klass or not @selector.klass
|
||||
return yes
|
||||
return no
|
||||
|
||||
matchesExact: (selector) ->
|
||||
@selector.tag is selector.tag and @selector.klass is selector.klass
|
||||
|
||||
class StyleElement
|
||||
constructor: (selector, @data) ->
|
||||
@selector = selector
|
||||
@props = {}
|
||||
|
||||
applyRules: (rules) ->
|
||||
# Two passes
|
||||
for rule in rules when rule.matches(@selector)
|
||||
neo.utils.extend(@props, rule.props)
|
||||
break
|
||||
for rule in rules when rule.matchesExact(@selector)
|
||||
neo.utils.extend(@props, rule.props)
|
||||
break
|
||||
@
|
||||
|
||||
get: (attr) ->
|
||||
@props[attr] or ''
|
||||
|
||||
|
||||
class GraphStyle
|
||||
constructor: (@storage) ->
|
||||
@rules = []
|
||||
@loadRules()
|
||||
|
||||
# Generate a selector string from an object (node or rel)
|
||||
selector: (item) ->
|
||||
if item.isNode
|
||||
@nodeSelector(item)
|
||||
else if item.isRelationship
|
||||
@relationshipSelector(item)
|
||||
|
||||
#
|
||||
# Methods for calculating applied style for elements
|
||||
#
|
||||
calculateStyle: (selector, data) ->
|
||||
new StyleElement(selector, data).applyRules(@rules)
|
||||
|
||||
forEntity: (item) ->
|
||||
@calculateStyle(@selector(item), item)
|
||||
|
||||
forNode: (node = {}) ->
|
||||
selector = @nodeSelector(node)
|
||||
if node.labels?.length > 0
|
||||
@setDefaultStyling(selector)
|
||||
@calculateStyle(selector, node)
|
||||
|
||||
forRelationship: (rel) ->
|
||||
@calculateStyle(@relationshipSelector(rel), rel)
|
||||
|
||||
findAvailableDefaultColor: () ->
|
||||
usedColors = {}
|
||||
for rule in @rules
|
||||
if rule.props.color?
|
||||
usedColors[rule.props.color] = yes
|
||||
|
||||
for defaultColor in _style.defaults.colors
|
||||
if !usedColors[defaultColor.color]?
|
||||
return neo.utils.copy(defaultColor)
|
||||
|
||||
return neo.utils.copy(_style.defaults.colors[0])
|
||||
|
||||
setDefaultStyling: (selector) ->
|
||||
rule = @findRule(selector)
|
||||
|
||||
if _style.defaults.autoColor and not rule?
|
||||
rule = new StyleRule(selector, @findAvailableDefaultColor())
|
||||
@rules.push(rule)
|
||||
@persist()
|
||||
|
||||
#
|
||||
# Methods for getting and modifying rules
|
||||
#
|
||||
change: (item, props) ->
|
||||
selector = @selector(item)
|
||||
rule = @findRule(selector)
|
||||
|
||||
if not rule?
|
||||
rule = new StyleRule(selector, {})
|
||||
@rules.push(rule)
|
||||
neo.utils.extend(rule.props, props)
|
||||
@persist()
|
||||
rule
|
||||
|
||||
destroyRule: (rule) ->
|
||||
idx = @rules.indexOf(rule)
|
||||
@rules.splice(idx, 1) if idx?
|
||||
@persist()
|
||||
|
||||
findRule: (selector) ->
|
||||
rule = r for r in @rules when r.matchesExact(selector)
|
||||
rule
|
||||
|
||||
#
|
||||
# Selector helpers
|
||||
#
|
||||
nodeSelector: (node = {}) ->
|
||||
selector = 'node'
|
||||
if node.labels?.length > 0
|
||||
selector += ".#{node.labels[0]}"
|
||||
new Selector(selector)
|
||||
|
||||
relationshipSelector: (rel = {}) ->
|
||||
selector = 'relationship'
|
||||
selector += ".#{rel.type}" if rel.type?
|
||||
new Selector(selector)
|
||||
|
||||
#
|
||||
# Import/export
|
||||
#
|
||||
|
||||
importGrass: (string) ->
|
||||
try
|
||||
rules = @parse(string)
|
||||
@loadRules(rules)
|
||||
@persist()
|
||||
catch e
|
||||
return
|
||||
|
||||
loadRules: (data) ->
|
||||
data = _style.defaults.style unless neo.utils.isObject(data)
|
||||
@rules.length = 0
|
||||
for rule, props of data
|
||||
@rules.push(new StyleRule(new Selector(rule), neo.utils.copy(props)))
|
||||
@
|
||||
|
||||
parse: (string)->
|
||||
chars = string.split('')
|
||||
insideString = no
|
||||
insideProps = no
|
||||
keyword = ""
|
||||
props = ""
|
||||
|
||||
rules = {}
|
||||
|
||||
for c in chars
|
||||
skipThis = yes
|
||||
switch c
|
||||
when "{"
|
||||
if not insideString
|
||||
insideProps = yes
|
||||
else
|
||||
skipThis = no
|
||||
when "}"
|
||||
if not insideString
|
||||
insideProps = no
|
||||
rules[keyword] = props
|
||||
keyword = ""
|
||||
props = ""
|
||||
else
|
||||
skipThis = no
|
||||
when "'", "\"" then insideString ^= true
|
||||
else skipThis = no
|
||||
|
||||
continue if skipThis
|
||||
|
||||
if insideProps
|
||||
props += c
|
||||
else
|
||||
keyword += c unless c.match(/[\s\n]/)
|
||||
|
||||
for k, v of rules
|
||||
rules[k] = {}
|
||||
for prop in v.split(';')
|
||||
[key, val] = prop.split(':')
|
||||
continue unless key and val
|
||||
rules[k][key?.trim()] = val?.trim()
|
||||
|
||||
rules
|
||||
|
||||
persist: ->
|
||||
@storage?.add('grass', JSON.stringify(@toSheet()))
|
||||
|
||||
resetToDefault: ->
|
||||
@loadRules()
|
||||
@persist()
|
||||
|
||||
toSheet: ->
|
||||
sheet = {}
|
||||
sheet[rule.selector.toString()] = rule.props for rule in @rules
|
||||
sheet
|
||||
|
||||
toString: ->
|
||||
str = ""
|
||||
for r in @rules
|
||||
str += r.selector.toString() + " {\n"
|
||||
for k, v of r.props
|
||||
v = "'#{v}'" if k == "caption"
|
||||
str += " #{k}: #{v};\n"
|
||||
str += "}\n\n"
|
||||
str
|
||||
|
||||
#
|
||||
# Misc.
|
||||
#
|
||||
nextDefaultColor: 0
|
||||
defaultColors: -> neo.utils.copy(_style.defaults.colors)
|
||||
interpolate: (str, id, properties) ->
|
||||
# Supplant
|
||||
# http://javascript.crockford.com/remedial.html
|
||||
str.replace(
|
||||
/\{([^{}]*)\}/g,
|
||||
(a, b) ->
|
||||
r = properties[b] or id
|
||||
return if (typeof r is 'string' or typeof r is 'number') then r else a
|
||||
)
|
||||
|
||||
_style
|
|
@ -0,0 +1,285 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
neo.viz = (el, measureSize, graph, layout, style) ->
|
||||
viz =
|
||||
style: style
|
||||
|
||||
root = d3.select(el)
|
||||
base_group = root.append('g').attr("transform", "translate(0,0)")
|
||||
rect = base_group.append("rect")
|
||||
.style("fill", "none")
|
||||
.style("pointer-events", "all")
|
||||
#Make the rect cover the whole surface
|
||||
.attr('x', '-2500')
|
||||
.attr('y', '-2500')
|
||||
.attr('width', '5000')
|
||||
.attr('height', '5000')
|
||||
.attr('transform', 'scale(1)')
|
||||
|
||||
container = base_group.append('g')
|
||||
geometry = new neo.NeoD3Geometry(style)
|
||||
|
||||
# This flags that a panning is ongoing and won't trigger
|
||||
# 'canvasClick' event when panning ends.
|
||||
draw = no
|
||||
|
||||
# Arbitrary dimension used to keep force layout aligned with
|
||||
# the centre of the svg view-port.
|
||||
layoutDimension = 200
|
||||
|
||||
updateViz = yes
|
||||
|
||||
# To be overridden
|
||||
viz.trigger = (event, args...) ->
|
||||
|
||||
onNodeClick = (node) =>
|
||||
updateViz = no
|
||||
viz.trigger('nodeClicked', node)
|
||||
|
||||
onNodeDblClick = (node) => viz.trigger('nodeDblClicked', node)
|
||||
|
||||
onNodeDragToggle = (node) -> viz.trigger('nodeDragToggle', node)
|
||||
|
||||
onRelationshipClick = (relationship) =>
|
||||
d3.event.stopPropagation()
|
||||
updateViz = no
|
||||
viz.trigger('relationshipClicked', relationship)
|
||||
|
||||
onNodeMouseOver = (node) -> viz.trigger('nodeMouseOver', node)
|
||||
onNodeMouseOut = (node) -> viz.trigger('nodeMouseOut', node)
|
||||
|
||||
onRelMouseOver = (rel) -> viz.trigger('relMouseOver', rel)
|
||||
onRelMouseOut = (rel) -> viz.trigger('relMouseOut', rel)
|
||||
|
||||
zoomLevel = null
|
||||
|
||||
zoomed = ->
|
||||
draw = yes
|
||||
container.attr("transform", "translate(" + zoomBehavior.translate() + ")" + "scale(" + zoomBehavior.scale() + ")")
|
||||
|
||||
zoomBehavior = d3.behavior.zoom().scaleExtent([0.2, 1]).on("zoom", zoomed)
|
||||
|
||||
interpolateZoom = (translate, scale) ->
|
||||
d3.transition().duration(500).tween("zoom", ->
|
||||
t = d3.interpolate(zoomBehavior.translate(), translate)
|
||||
s = d3.interpolate(zoomBehavior.scale(), scale)
|
||||
(a)->
|
||||
zoomBehavior.scale(s(a)).translate(t(a))
|
||||
zoomed())
|
||||
|
||||
isZoomingIn = true
|
||||
|
||||
viz.zoomInClick = ->
|
||||
isZoomingIn = true
|
||||
zoomClick this
|
||||
|
||||
viz.zoomOutClick = ->
|
||||
isZoomingIn = false
|
||||
zoomClick this
|
||||
|
||||
zoomClick = (element) ->
|
||||
draw = yes
|
||||
limitsReached = {zoomInLimit: false, zoomOutLimit: false}
|
||||
|
||||
if isZoomingIn
|
||||
zoomLevel = Number (zoomBehavior.scale() * (1 + 0.2 * 1)).toFixed(2)
|
||||
if zoomLevel >= zoomBehavior.scaleExtent()[1]
|
||||
limitsReached.zoomInLimit = true
|
||||
interpolateZoom(zoomBehavior.translate(), zoomBehavior.scaleExtent()[1])
|
||||
else
|
||||
interpolateZoom(zoomBehavior.translate(), zoomLevel)
|
||||
|
||||
else
|
||||
zoomLevel = Number (zoomBehavior.scale() * (1 + 0.2 * -1)).toFixed(2)
|
||||
if zoomLevel <= zoomBehavior.scaleExtent()[0]
|
||||
limitsReached.zoomOutLimit = true
|
||||
interpolateZoom(zoomBehavior.translate(), zoomBehavior.scaleExtent()[0])
|
||||
else
|
||||
interpolateZoom(zoomBehavior.translate(), zoomLevel)
|
||||
limitsReached
|
||||
# Background click event
|
||||
# Check if panning is ongoing
|
||||
rect.on('click', ->
|
||||
if not draw then viz.trigger('canvasClicked', el)
|
||||
)
|
||||
|
||||
base_group.call(zoomBehavior)
|
||||
.on("dblclick.zoom", null)
|
||||
#Single click is not panning
|
||||
.on("click.zoom", -> draw = no)
|
||||
.on("DOMMouseScroll.zoom", null)
|
||||
.on("wheel.zoom", null)
|
||||
.on("mousewheel.zoom", null)
|
||||
|
||||
newStatsBucket = ->
|
||||
bucket =
|
||||
frameCount: 0
|
||||
geometry: 0
|
||||
relationshipRenderers: do ->
|
||||
timings = {}
|
||||
neo.renderers.relationship.forEach((r) ->
|
||||
timings[r.name] = 0
|
||||
)
|
||||
timings
|
||||
bucket.duration = ->
|
||||
bucket.lastFrame - bucket.firstFrame
|
||||
bucket.fps = ->
|
||||
(1000 * bucket.frameCount / bucket.duration()).toFixed(1)
|
||||
bucket.lps = ->
|
||||
(1000 * bucket.layout.layoutSteps / bucket.duration()).toFixed(1)
|
||||
bucket.top = ->
|
||||
renderers = []
|
||||
for name, time of bucket.relationshipRenderers
|
||||
renderers.push {
|
||||
name: name
|
||||
time: time
|
||||
}
|
||||
renderers.push
|
||||
name: 'forceLayout'
|
||||
time: bucket.layout.layoutTime
|
||||
renderers.sort (a, b) -> b.time - a.time
|
||||
totalRenderTime = renderers.reduce ((prev, current) -> prev + current.time), 0
|
||||
renderers.map((d) -> "#{d.name}: #{(100 * d.time / totalRenderTime).toFixed(1)}%").join(', ')
|
||||
bucket
|
||||
|
||||
currentStats = newStatsBucket()
|
||||
|
||||
now = if window.performance and window.performance.now
|
||||
() ->
|
||||
window.performance.now()
|
||||
else
|
||||
() ->
|
||||
Date.now()
|
||||
|
||||
render = ->
|
||||
currentStats.firstFrame = now() unless currentStats.firstFrame
|
||||
currentStats.frameCount++
|
||||
startRender = now()
|
||||
geometry.onTick(graph)
|
||||
currentStats.geometry += (now() - startRender)
|
||||
|
||||
nodeGroups = container.selectAll('g.node')
|
||||
.attr('transform', (d) ->
|
||||
"translate(#{ d.x },#{ d.y })")
|
||||
|
||||
for renderer in neo.renderers.node
|
||||
nodeGroups.call(renderer.onTick, viz)
|
||||
|
||||
relationshipGroups = container.selectAll('g.relationship')
|
||||
.attr('transform', (d) ->
|
||||
"translate(#{ d.source.x } #{ d.source.y }) rotate(#{ d.naturalAngle + 180 })")
|
||||
|
||||
for renderer in neo.renderers.relationship
|
||||
startRenderer = now()
|
||||
relationshipGroups.call(renderer.onTick, viz)
|
||||
currentStats.relationshipRenderers[renderer.name] += (now() - startRenderer)
|
||||
|
||||
currentStats.lastFrame = now()
|
||||
|
||||
force = layout.init(render)
|
||||
|
||||
#Add custom drag event listeners
|
||||
force.drag().on('dragstart.node', (d) ->
|
||||
onNodeDragToggle(d)
|
||||
).on('dragend.node', () ->
|
||||
onNodeDragToggle()
|
||||
)
|
||||
|
||||
viz.collectStats = ->
|
||||
latestStats = currentStats
|
||||
latestStats.layout = force.collectStats()
|
||||
currentStats = newStatsBucket()
|
||||
latestStats
|
||||
|
||||
viz.update = ->
|
||||
return unless graph
|
||||
|
||||
layers = container.selectAll("g.layer").data(["relationships", "nodes"])
|
||||
layers.enter().append("g")
|
||||
.attr("class", (d) -> "layer " + d )
|
||||
|
||||
nodes = graph.nodes()
|
||||
relationships = graph.relationships()
|
||||
|
||||
relationshipGroups = container.select("g.layer.relationships")
|
||||
.selectAll("g.relationship").data(relationships, (d) -> d.id)
|
||||
|
||||
relationshipGroups.enter().append("g")
|
||||
.attr("class", "relationship")
|
||||
.on("mousedown", onRelationshipClick)
|
||||
.on('mouseover', onRelMouseOver)
|
||||
.on('mouseout', onRelMouseOut)
|
||||
|
||||
relationshipGroups
|
||||
.classed("selected", (relationship) -> relationship.selected)
|
||||
|
||||
geometry.onGraphChange(graph)
|
||||
|
||||
for renderer in neo.renderers.relationship
|
||||
relationshipGroups.call(renderer.onGraphChange, viz)
|
||||
|
||||
relationshipGroups.exit().remove();
|
||||
|
||||
nodeGroups = container.select("g.layer.nodes")
|
||||
.selectAll("g.node").data(nodes, (d) -> d.id)
|
||||
|
||||
nodeGroups.enter().append("g")
|
||||
.attr("class", "node")
|
||||
.call(force.drag)
|
||||
.call(clickHandler)
|
||||
.on('mouseover', onNodeMouseOver)
|
||||
.on('mouseout', onNodeMouseOut)
|
||||
|
||||
nodeGroups
|
||||
.classed("selected", (node) -> node.selected)
|
||||
|
||||
for renderer in neo.renderers.node
|
||||
nodeGroups.call(renderer.onGraphChange, viz)
|
||||
|
||||
for renderer in neo.renderers.menu
|
||||
nodeGroups.call(renderer.onGraphChange, viz)
|
||||
|
||||
nodeGroups.exit().remove();
|
||||
|
||||
if updateViz
|
||||
force.update(graph, [layoutDimension, layoutDimension])
|
||||
|
||||
viz.resize()
|
||||
viz.trigger('updated')
|
||||
|
||||
updateViz = yes
|
||||
|
||||
viz.resize = ->
|
||||
size = measureSize()
|
||||
root.attr('viewBox', [
|
||||
0, (layoutDimension - size.height) / 2, layoutDimension, size.height
|
||||
].join(' '))
|
||||
|
||||
viz.boundingBox = ->
|
||||
container.node().getBBox()
|
||||
|
||||
clickHandler = neo.utils.clickHandler()
|
||||
clickHandler.on 'click', onNodeClick
|
||||
clickHandler.on 'dblclick', onNodeDblClick
|
||||
|
||||
viz
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import './neod3.coffee'
|
||||
import './components/collision.coffee'
|
||||
import './components/graph.coffee'
|
||||
import './components/graphGeometry.coffee'
|
||||
import './components/graphView.coffee'
|
||||
import './components/layout.coffee'
|
||||
import './components/node.coffee'
|
||||
import './components/relationship.coffee'
|
||||
import './components/renderer.coffee'
|
||||
import './components/style.coffee'
|
||||
import './components/visualization.coffee'
|
||||
import './renders/init.coffee'
|
||||
import './renders/menu.coffee'
|
||||
import './utils/adjacentAngles.coffee'
|
||||
import './utils/angleList.coffee'
|
||||
import './utils/arcArrow.coffee'
|
||||
import './utils/arrays.coffee'
|
||||
import './utils/circularLayout.coffee'
|
||||
import './utils/circumferentialDistribution.coffee'
|
||||
import './utils/circumferentialRelationshipRouting.coffee'
|
||||
import './utils/clickHandler.coffee'
|
||||
import './utils/loopArrow.coffee'
|
||||
import './utils/pairwiseArcsRelationshipRouting.coffee'
|
||||
import './utils/straightArrow.coffee'
|
||||
import './utils/textMeasurement.coffee'
|
|
@ -0,0 +1,45 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
window.neo = window.neo || {}
|
||||
|
||||
neo.models = {}
|
||||
|
||||
neo.renderers =
|
||||
menu: []
|
||||
node: []
|
||||
relationship: []
|
||||
|
||||
neo.utils =
|
||||
# Note: quick n' dirty. Only works for serializable objects
|
||||
copy: (src) ->
|
||||
JSON.parse(JSON.stringify(src))
|
||||
|
||||
extend: (dest, src) ->
|
||||
return if not neo.utils.isObject(dest) and neo.utils.isObject(src)
|
||||
dest[k] = v for own k, v of src
|
||||
return dest
|
||||
|
||||
isArray: Array.isArray or (obj) ->
|
||||
Object::toString.call(obj) == '[object Array]';
|
||||
|
||||
isObject: (obj) -> Object(obj) is obj
|
|
@ -0,0 +1,181 @@
|
|||
###!
|
||||
Copyright (c) 2002-2015 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
do ->
|
||||
noop = ->
|
||||
|
||||
nodeRingStrokeSize = 8
|
||||
|
||||
nodeOutline = new neo.Renderer(
|
||||
onGraphChange: (selection, viz) ->
|
||||
circles = selection.selectAll('circle.outline').data((node) -> [node])
|
||||
|
||||
circles.enter()
|
||||
.append('circle')
|
||||
.classed('outline', true)
|
||||
.attr
|
||||
cx: 0
|
||||
cy: 0
|
||||
|
||||
circles
|
||||
.attr
|
||||
r: (node) -> node.radius
|
||||
fill: (node) -> viz.style.forNode(node).get('color')
|
||||
stroke: (node) -> viz.style.forNode(node).get('border-color')
|
||||
'stroke-width': (node) -> viz.style.forNode(node).get('border-width')
|
||||
|
||||
circles.exit().remove()
|
||||
onTick: noop
|
||||
)
|
||||
|
||||
nodeCaption = new neo.Renderer(
|
||||
onGraphChange: (selection, viz) ->
|
||||
text = selection.selectAll('text.caption').data((node) -> node.caption)
|
||||
|
||||
text.enter().append('text')
|
||||
# .classed('caption', true)
|
||||
.attr('text-anchor': 'middle')
|
||||
.attr('pointer-events': 'none')
|
||||
|
||||
text
|
||||
.text((line) -> line.text)
|
||||
.attr('y', (line) -> line.baseline )
|
||||
.attr('font-size', (line) -> viz.style.forNode(line.node).get('font-size'))
|
||||
.attr('fill': (line) -> viz.style.forNode(line.node).get('text-color-internal'))
|
||||
|
||||
text.exit().remove()
|
||||
|
||||
onTick: noop
|
||||
)
|
||||
|
||||
nodeIcon = new neo.Renderer(
|
||||
onGraphChange: (selection, viz) ->
|
||||
text = selection.selectAll('text').data((node) -> node.caption)
|
||||
|
||||
text.enter().append('text')
|
||||
.attr('text-anchor': 'middle')
|
||||
.attr('pointer-events': 'none')
|
||||
.attr('font-family': 'streamline')
|
||||
|
||||
text
|
||||
.text((line) -> viz.style.forNode(line.node).get('icon-code'))
|
||||
.attr('dy', (line) -> line.node.radius/16)
|
||||
.attr('font-size', (line) -> line.node.radius)
|
||||
.attr('fill': (line) -> viz.style.forNode(line.node).get('text-color-internal'))
|
||||
|
||||
text.exit().remove()
|
||||
|
||||
onTick: noop
|
||||
)
|
||||
|
||||
nodeRing = new neo.Renderer(
|
||||
onGraphChange: (selection) ->
|
||||
circles = selection.selectAll('circle.ring').data((node) -> [node])
|
||||
circles.enter()
|
||||
.insert('circle', '.outline')
|
||||
.classed('ring', true)
|
||||
.attr
|
||||
cx: 0
|
||||
cy: 0
|
||||
'stroke-width': nodeRingStrokeSize + 'px'
|
||||
|
||||
circles
|
||||
.attr
|
||||
r: (node) -> node.radius + 4
|
||||
|
||||
circles.exit().remove()
|
||||
|
||||
onTick: noop
|
||||
)
|
||||
|
||||
arrowPath = new neo.Renderer(
|
||||
name: 'arrowPath'
|
||||
onGraphChange: (selection, viz) ->
|
||||
paths = selection.selectAll('path.outline').data((rel) -> [rel])
|
||||
|
||||
paths.enter()
|
||||
.append('path')
|
||||
.classed('outline', true)
|
||||
|
||||
paths
|
||||
.attr('fill', (rel) -> viz.style.forRelationship(rel).get('color'))
|
||||
.attr('stroke', 'none')
|
||||
|
||||
paths.exit().remove()
|
||||
|
||||
onTick: (selection) ->
|
||||
selection.selectAll('path')
|
||||
.attr('d', (d) -> d.arrow.outline(d.shortCaptionLength))
|
||||
)
|
||||
|
||||
relationshipType = new neo.Renderer(
|
||||
name: 'relationshipType'
|
||||
onGraphChange: (selection, viz) ->
|
||||
texts = selection.selectAll("text").data((rel) -> [rel])
|
||||
|
||||
texts.enter().append("text")
|
||||
.attr("text-anchor": "middle")
|
||||
.attr('pointer-events': 'none')
|
||||
|
||||
texts
|
||||
.attr('font-size', (rel) -> viz.style.forRelationship(rel).get('font-size'))
|
||||
.attr('fill', (rel) -> viz.style.forRelationship(rel).get('text-color-' + rel.captionLayout))
|
||||
|
||||
texts.exit().remove()
|
||||
|
||||
onTick: (selection, viz) ->
|
||||
selection.selectAll('text')
|
||||
.attr('x', (rel) -> rel.arrow.midShaftPoint.x)
|
||||
.attr('y', (rel) -> rel.arrow.midShaftPoint.y + parseFloat(viz.style.forRelationship(rel).get('font-size')) / 2 - 1)
|
||||
.attr('transform', (rel) ->
|
||||
if rel.naturalAngle < 90 or rel.naturalAngle > 270
|
||||
"rotate(180 #{ rel.arrow.midShaftPoint.x } #{ rel.arrow.midShaftPoint.y })"
|
||||
else
|
||||
null)
|
||||
.text((rel) -> rel.shortCaption)
|
||||
)
|
||||
|
||||
relationshipOverlay = new neo.Renderer(
|
||||
name: 'relationshipOverlay'
|
||||
onGraphChange: (selection) ->
|
||||
rects = selection.selectAll('path.overlay').data((rel) -> [rel])
|
||||
|
||||
rects.enter()
|
||||
.append('path')
|
||||
.classed('overlay', true)
|
||||
|
||||
rects.exit().remove()
|
||||
|
||||
onTick: (selection) ->
|
||||
band = 16
|
||||
|
||||
selection.selectAll('path.overlay')
|
||||
.attr('d', (d) -> d.arrow.overlay(band))
|
||||
)
|
||||
|
||||
neo.renderers.node.push(nodeOutline)
|
||||
neo.renderers.node.push(nodeIcon)
|
||||
neo.renderers.node.push(nodeCaption)
|
||||
neo.renderers.node.push(nodeRing)
|
||||
neo.renderers.relationship.push(arrowPath)
|
||||
neo.renderers.relationship.push(relationshipType)
|
||||
neo.renderers.relationship.push(relationshipOverlay)
|
|
@ -0,0 +1,120 @@
|
|||
###!
|
||||
Copyright (c) 2002-2015 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
do ->
|
||||
noop = ->
|
||||
|
||||
numberOfItemsInContextMenu = 3
|
||||
|
||||
arc = (radius, itemNumber, width = 30) ->
|
||||
itemNumber = itemNumber - 1
|
||||
startAngle = ((2*Math.PI)/numberOfItemsInContextMenu) * itemNumber
|
||||
endAngle = startAngle + ((2*Math.PI)/numberOfItemsInContextMenu)
|
||||
innerRadius = Math.max(radius + 8, 20)
|
||||
d3.svg.arc().innerRadius(innerRadius).outerRadius(innerRadius + width).startAngle(startAngle).endAngle(endAngle).padAngle(.03)
|
||||
|
||||
getSelectedNode = (node) -> if node.selected then [node] else []
|
||||
|
||||
attachContextEvent = (event, elems, viz, content, label) ->
|
||||
for elem in elems
|
||||
elem.on('mousedown.drag', ->
|
||||
d3.event.stopPropagation()
|
||||
null)
|
||||
elem.on('mouseup', (node) ->
|
||||
viz.trigger(event, node))
|
||||
elem.on('mouseover', (node) ->
|
||||
node.contextMenu =
|
||||
menuSelection: event
|
||||
menuContent: content
|
||||
label:label
|
||||
viz.trigger('menuMouseOver', node))
|
||||
elem.on('mouseout', (node) ->
|
||||
delete node.contextMenu
|
||||
viz.trigger('menuMouseOut', node))
|
||||
|
||||
createMenuItem = (selection, viz, eventName, itemNumber, className, position, textValue, helpValue) ->
|
||||
path = selection.selectAll('path.' + className).data(getSelectedNode)
|
||||
textpath = selection.selectAll('text.' + className).data(getSelectedNode)
|
||||
|
||||
tab = path.enter()
|
||||
.append('path')
|
||||
.classed(className, true)
|
||||
.classed('context-menu-item', true)
|
||||
.attr
|
||||
d: (node) -> arc(node.radius, itemNumber, 1)()
|
||||
|
||||
text = textpath.enter()
|
||||
.append('text')
|
||||
.classed('context-menu-item', true)
|
||||
.text(textValue)
|
||||
.attr("transform", "scale(0.1)")
|
||||
.attr
|
||||
'font-family': 'FontAwesome'
|
||||
fill: (node) -> viz.style.forNode(node).get('text-color-internal')
|
||||
x: (node) -> arc(node.radius, itemNumber).centroid()[0] + position[0]
|
||||
y: (node) -> arc(node.radius, itemNumber).centroid()[1] + position[1]
|
||||
|
||||
attachContextEvent(eventName, [tab, text], viz, helpValue, textValue)
|
||||
|
||||
tab
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr
|
||||
d: (node) -> arc(node.radius, itemNumber)()
|
||||
|
||||
text
|
||||
.attr("transform", "scale(1)")
|
||||
|
||||
path
|
||||
.exit()
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr
|
||||
d: (node) -> arc(node.radius, itemNumber, 1)()
|
||||
.remove()
|
||||
|
||||
textpath
|
||||
.exit()
|
||||
.attr("transform", "scale(0)")
|
||||
.remove()
|
||||
|
||||
donutRemoveNode = new neo.Renderer(
|
||||
onGraphChange: (selection, viz) -> createMenuItem(selection, viz, 'nodeClose', 1, 'remove_node', [-4, 0], '\uf00d', 'Remove node from the visualization')
|
||||
|
||||
onTick: noop
|
||||
)
|
||||
|
||||
donutExpandNode = new neo.Renderer(
|
||||
onGraphChange: (selection, viz) -> createMenuItem(selection, viz, 'nodeDblClicked', 2, 'expand_node', [0, 4], '\uf0b2', 'Expand child relationships')
|
||||
|
||||
onTick: noop
|
||||
)
|
||||
|
||||
donutUnlockNode = new neo.Renderer(
|
||||
onGraphChange: (selection, viz) -> createMenuItem(selection, viz, 'nodeUnlock', 3, 'unlock_node', [4, 0], '\uf09c', 'Unlock the node to re-layout the graph')
|
||||
|
||||
onTick: noop
|
||||
)
|
||||
|
||||
neo.renderers.menu.push(donutExpandNode)
|
||||
neo.renderers.menu.push(donutRemoveNode)
|
||||
neo.renderers.menu.push(donutUnlockNode)
|
|
@ -0,0 +1,91 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.utils.adjacentAngles
|
||||
|
||||
findRuns: (angleList, minSeparation) ->
|
||||
|
||||
p = 0
|
||||
start = 0
|
||||
end = 0
|
||||
runs = []
|
||||
minStart = () ->
|
||||
if runs.length == 0
|
||||
0
|
||||
else
|
||||
runs[0].start
|
||||
|
||||
scanForDensePair = ->
|
||||
start = p
|
||||
end = angleList.wrapIndex(p + 1)
|
||||
if end == minStart()
|
||||
'done'
|
||||
else
|
||||
p = end
|
||||
if tooDense(start, end)
|
||||
extendEnd
|
||||
|
||||
else
|
||||
scanForDensePair
|
||||
|
||||
extendEnd = ->
|
||||
if p == minStart()
|
||||
'done'
|
||||
|
||||
else if tooDense(start, angleList.wrapIndex(p + 1))
|
||||
end = angleList.wrapIndex(p + 1)
|
||||
p = end
|
||||
extendEnd
|
||||
|
||||
else
|
||||
p = start
|
||||
extendStart
|
||||
|
||||
extendStart = ->
|
||||
candidateStart = angleList.wrapIndex(p - 1)
|
||||
if tooDense(candidateStart, end) and candidateStart != end
|
||||
start = candidateStart
|
||||
p = start
|
||||
extendStart
|
||||
|
||||
else
|
||||
runs.push
|
||||
start: start
|
||||
end: end
|
||||
p = end
|
||||
scanForDensePair
|
||||
|
||||
tooDense = (start, end) ->
|
||||
run =
|
||||
start: start
|
||||
end: end
|
||||
angleList.angle(run) < angleList.length(run) * minSeparation
|
||||
|
||||
stepCount = 0
|
||||
step = scanForDensePair
|
||||
while step != 'done'
|
||||
if stepCount++ > angleList.totalLength() * 10
|
||||
console.log 'Warning: failed to layout arrows', ("#{ key }: #{ value.angle }" for own key, value of angleList.list).join('\n'), minSeparation
|
||||
break
|
||||
step = step()
|
||||
|
||||
runs
|
|
@ -0,0 +1,54 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.utils.angleList
|
||||
|
||||
constructor: (@list) ->
|
||||
|
||||
getAngle: (index) ->
|
||||
@list[index].angle
|
||||
|
||||
fixed: (index) ->
|
||||
@list[index].fixed
|
||||
|
||||
totalLength: ->
|
||||
@list.length
|
||||
|
||||
length: (run) ->
|
||||
if run.start < run.end
|
||||
run.end - run.start
|
||||
else
|
||||
run.end + @list.length - run.start
|
||||
|
||||
angle: (run) ->
|
||||
if run.start < run.end
|
||||
@list[run.end].angle - @list[run.start].angle
|
||||
else
|
||||
360 - (@list[run.start].angle - @list[run.end].angle)
|
||||
|
||||
wrapIndex: (index) ->
|
||||
if index == -1
|
||||
@list.length - 1
|
||||
else if index >= @list.length
|
||||
index - @list.length
|
||||
else
|
||||
index
|
|
@ -0,0 +1,180 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.utils.arcArrow
|
||||
|
||||
constructor: (startRadius, endRadius, endCentre, @deflection, arrowWidth, headWidth, headLength, captionLayout) ->
|
||||
square = (l) ->
|
||||
l * l
|
||||
|
||||
deflectionRadians = @deflection * Math.PI / 180
|
||||
startAttach =
|
||||
x: Math.cos( deflectionRadians ) * (startRadius),
|
||||
y: Math.sin( deflectionRadians ) * (startRadius)
|
||||
|
||||
radiusRatio = startRadius / (endRadius + headLength)
|
||||
homotheticCenter = -endCentre * radiusRatio / (1 - radiusRatio)
|
||||
|
||||
intersectWithOtherCircle = (fixedPoint, radius, xCenter, polarity) ->
|
||||
gradient = fixedPoint.y / (fixedPoint.x - homotheticCenter)
|
||||
hc = fixedPoint.y - gradient * fixedPoint.x
|
||||
|
||||
A = 1 + square(gradient)
|
||||
B = 2 * (gradient * hc - xCenter)
|
||||
C = square(hc) + square(xCenter) - square(radius)
|
||||
|
||||
intersection = { x: (-B + polarity * Math.sqrt(square(B) - 4 * A * C)) / (2 * A) }
|
||||
intersection.y = (intersection.x - homotheticCenter) * gradient
|
||||
|
||||
intersection
|
||||
|
||||
endAttach = intersectWithOtherCircle(startAttach, endRadius + headLength, endCentre, -1)
|
||||
|
||||
g1 = -startAttach.x / startAttach.y
|
||||
c1 = startAttach.y + (square(startAttach.x) / startAttach.y)
|
||||
g2 = -(endAttach.x - endCentre) / endAttach.y
|
||||
c2 = endAttach.y + (endAttach.x - endCentre) * endAttach.x / endAttach.y
|
||||
|
||||
cx = ( c1 - c2 ) / (g2 - g1)
|
||||
cy = g1 * cx + c1
|
||||
|
||||
arcRadius = Math.sqrt(square(cx - startAttach.x) + square(cy - startAttach.y))
|
||||
startAngle = Math.atan2(startAttach.x - cx, cy - startAttach.y)
|
||||
endAngle = Math.atan2(endAttach.x - cx, cy - endAttach.y)
|
||||
sweepAngle = endAngle - startAngle
|
||||
if @deflection > 0
|
||||
sweepAngle = 2 * Math.PI - sweepAngle
|
||||
|
||||
@shaftLength = sweepAngle * arcRadius
|
||||
if startAngle > endAngle
|
||||
@shaftLength = 0
|
||||
|
||||
midShaftAngle = (startAngle + endAngle) / 2
|
||||
if @deflection > 0
|
||||
midShaftAngle += Math.PI
|
||||
@midShaftPoint =
|
||||
x: cx + arcRadius * Math.sin(midShaftAngle)
|
||||
y: cy - arcRadius * Math.cos(midShaftAngle)
|
||||
|
||||
startTangent = (dr) ->
|
||||
dx = (if dr < 0 then 1 else -1) * Math.sqrt(square(dr) / (1 + square(g1)))
|
||||
dy = g1 * dx
|
||||
{
|
||||
x: startAttach.x + dx,
|
||||
y: startAttach.y + dy
|
||||
}
|
||||
|
||||
endTangent = (dr) ->
|
||||
dx = (if dr < 0 then -1 else 1) * Math.sqrt(square(dr) / (1 + square(g2)))
|
||||
dy = g2 * dx
|
||||
{
|
||||
x: endAttach.x + dx,
|
||||
y: endAttach.y + dy
|
||||
}
|
||||
|
||||
angleTangent = (angle, dr) ->
|
||||
{
|
||||
x: cx + (arcRadius + dr) * Math.sin(angle),
|
||||
y: cy - (arcRadius + dr) * Math.cos(angle)
|
||||
}
|
||||
|
||||
endNormal = (dc) ->
|
||||
dx = (if dc < 0 then -1 else 1) * Math.sqrt(square(dc) / (1 + square(1 / g2)))
|
||||
dy = dx / g2
|
||||
{
|
||||
x: endAttach.x + dx
|
||||
y: endAttach.y - dy
|
||||
}
|
||||
|
||||
endOverlayCorner = (dr, dc) ->
|
||||
shoulder = endTangent(dr)
|
||||
arrowTip = endNormal(dc)
|
||||
{
|
||||
x: shoulder.x + arrowTip.x - endAttach.x,
|
||||
y: shoulder.y + arrowTip.y - endAttach.y
|
||||
}
|
||||
|
||||
coord = (point) ->
|
||||
"#{point.x},#{point.y}"
|
||||
|
||||
shaftRadius = arrowWidth / 2
|
||||
headRadius = headWidth / 2
|
||||
positiveSweep = if startAttach.y > 0 then 0 else 1
|
||||
negativeSweep = if startAttach.y < 0 then 0 else 1
|
||||
|
||||
@outline = (shortCaptionLength) ->
|
||||
if startAngle > endAngle
|
||||
return [
|
||||
'M', coord(endTangent(-headRadius)),
|
||||
'L', coord(endNormal(headLength)),
|
||||
'L', coord(endTangent(headRadius)),
|
||||
'Z'
|
||||
].join(' ')
|
||||
|
||||
if captionLayout is 'external'
|
||||
captionSweep = shortCaptionLength / arcRadius
|
||||
if @deflection > 0
|
||||
captionSweep *= -1
|
||||
|
||||
startBreak = midShaftAngle - captionSweep / 2
|
||||
endBreak = midShaftAngle + captionSweep / 2
|
||||
|
||||
[
|
||||
'M', coord(startTangent(shaftRadius)),
|
||||
'L', coord(startTangent(-shaftRadius)),
|
||||
'A', arcRadius - shaftRadius, arcRadius - shaftRadius, 0, 0, positiveSweep, coord(angleTangent(startBreak, -shaftRadius)),
|
||||
'L', coord(angleTangent(startBreak, shaftRadius)),
|
||||
'A', arcRadius + shaftRadius, arcRadius + shaftRadius, 0, 0, negativeSweep, coord(startTangent(shaftRadius))
|
||||
'Z',
|
||||
'M', coord(angleTangent(endBreak, shaftRadius)),
|
||||
'L', coord(angleTangent(endBreak, -shaftRadius)),
|
||||
'A', arcRadius - shaftRadius, arcRadius - shaftRadius, 0, 0, positiveSweep, coord(endTangent(-shaftRadius)),
|
||||
'L', coord(endTangent(-headRadius)),
|
||||
'L', coord(endNormal(headLength)),
|
||||
'L', coord(endTangent(headRadius)),
|
||||
'L', coord(endTangent(shaftRadius)),
|
||||
'A', arcRadius + shaftRadius, arcRadius + shaftRadius, 0, 0, negativeSweep, coord(angleTangent(endBreak, shaftRadius))
|
||||
].join(' ')
|
||||
else
|
||||
[
|
||||
'M', coord(startTangent(shaftRadius)),
|
||||
'L', coord(startTangent(-shaftRadius)),
|
||||
'A', arcRadius - shaftRadius, arcRadius - shaftRadius, 0, 0, positiveSweep, coord(endTangent(-shaftRadius)),
|
||||
'L', coord(endTangent(-headRadius)),
|
||||
'L', coord(endNormal(headLength)),
|
||||
'L', coord(endTangent(headRadius)),
|
||||
'L', coord(endTangent(shaftRadius)),
|
||||
'A', arcRadius + shaftRadius, arcRadius + shaftRadius, 0, 0, negativeSweep, coord(startTangent(shaftRadius))
|
||||
].join(' ')
|
||||
|
||||
@overlay = (minWidth) ->
|
||||
radius = Math.max(minWidth / 2, shaftRadius)
|
||||
|
||||
[
|
||||
'M', coord(startTangent(radius)),
|
||||
'L', coord(startTangent(-radius)),
|
||||
'A', arcRadius - radius, arcRadius - radius, 0, 0, positiveSweep, coord(endTangent(-radius)),
|
||||
'L', coord(endOverlayCorner(-radius, headLength)),
|
||||
'L', coord(endOverlayCorner(radius, headLength)),
|
||||
'L', coord(endTangent(radius)),
|
||||
'A', arcRadius + radius, arcRadius + radius, 0, 0, negativeSweep, coord(startTangent(radius))
|
||||
].join(' ')
|
|
@ -0,0 +1,26 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
neo.utils.cloneArray = (original) ->
|
||||
clone = new Array(original.length)
|
||||
clone[idx] = node for node, idx in original
|
||||
clone
|
|
@ -0,0 +1,28 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
neo.utils.circularLayout = (nodes, center, radius) ->
|
||||
unlocatedNodes = []
|
||||
unlocatedNodes.push(node) for node in nodes when !(node.x? and node.y?)
|
||||
for n, i in unlocatedNodes
|
||||
n.x = center.x + radius * Math.sin(2 * Math.PI * i / unlocatedNodes.length)
|
||||
n.y = center.y + radius * Math.cos(2 * Math.PI * i / unlocatedNodes.length)
|
|
@ -0,0 +1,99 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
neo.utils.distributeCircular = (arrowAngles, minSeparation) ->
|
||||
list = []
|
||||
for key, angle of arrowAngles.floating
|
||||
list.push
|
||||
key: key
|
||||
angle: angle
|
||||
fixed: false
|
||||
for key, angle of arrowAngles.fixed
|
||||
list.push
|
||||
key: key
|
||||
angle: angle
|
||||
fixed: true
|
||||
|
||||
list.sort((a, b) -> a.angle - b.angle)
|
||||
|
||||
angleList = new neo.utils.angleList(list)
|
||||
runsOfTooDenseArrows = new neo.utils.adjacentAngles().findRuns(angleList, minSeparation)
|
||||
|
||||
wrapAngle = (angle) ->
|
||||
if angle >= 360
|
||||
angle - 360
|
||||
else if angle < 0
|
||||
angle + 360
|
||||
else
|
||||
angle
|
||||
|
||||
result = {}
|
||||
|
||||
splitByFixedArrows = (run) ->
|
||||
runs = []
|
||||
currentStart = run.start
|
||||
for i in [1..angleList.length(run)]
|
||||
wrapped = angleList.wrapIndex(run.start + i)
|
||||
if angleList.fixed(wrapped)
|
||||
runs.push
|
||||
start: currentStart
|
||||
end: wrapped
|
||||
currentStart = wrapped
|
||||
if not angleList.fixed(run.end)
|
||||
runs.push
|
||||
start: currentStart
|
||||
end: run.end
|
||||
runs
|
||||
|
||||
for tooDenseRun in runsOfTooDenseArrows
|
||||
moveableRuns = splitByFixedArrows(tooDenseRun)
|
||||
for run in moveableRuns
|
||||
runLength = angleList.length(run)
|
||||
if angleList.fixed(run.start) and angleList.fixed(run.end)
|
||||
separation = angleList.angle(run) / runLength
|
||||
for i in [0..runLength]
|
||||
rawAngle = list[run.start].angle + i * separation
|
||||
result[list[angleList.wrapIndex(run.start + i)].key] = wrapAngle(rawAngle)
|
||||
else if angleList.fixed(run.start) and not angleList.fixed(run.end)
|
||||
for i in [0..runLength]
|
||||
rawAngle = list[run.start].angle + i * minSeparation
|
||||
result[list[angleList.wrapIndex(run.start + i)].key] = wrapAngle(rawAngle)
|
||||
else if not angleList.fixed(run.start) and angleList.fixed(run.end)
|
||||
for i in [0..runLength]
|
||||
rawAngle = list[run.end].angle - (runLength - i) * minSeparation
|
||||
result[list[angleList.wrapIndex(run.start + i)].key] = wrapAngle(rawAngle)
|
||||
else
|
||||
center = list[run.start].angle + angleList.angle(run) / 2
|
||||
for i in [0..runLength]
|
||||
rawAngle = center + (i - runLength / 2) * minSeparation
|
||||
result[list[angleList.wrapIndex(run.start + i)].key] = wrapAngle(rawAngle)
|
||||
|
||||
|
||||
for key of arrowAngles.floating
|
||||
if !result.hasOwnProperty(key)
|
||||
result[key] = arrowAngles.floating[key]
|
||||
|
||||
for key of arrowAngles.fixed
|
||||
if !result.hasOwnProperty(key)
|
||||
result[key] = arrowAngles.fixed[key]
|
||||
|
||||
result
|
|
@ -0,0 +1,136 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.utils.circumferentialRelationshipRouting
|
||||
constructor: (@style) ->
|
||||
|
||||
measureRelationshipCaption: (relationship, caption) ->
|
||||
fontFamily = 'sans-serif'
|
||||
fontSize = parseFloat(@style.forRelationship(relationship).get('font-size'))
|
||||
padding = parseFloat(@style.forRelationship(relationship).get('padding'))
|
||||
neo.utils.measureText(caption, fontFamily, fontSize) + padding * 2
|
||||
|
||||
captionFitsInsideArrowShaftWidth: (relationship) ->
|
||||
parseFloat(@style.forRelationship(relationship).get('shaft-width')) >
|
||||
parseFloat(@style.forRelationship(relationship).get('font-size'))
|
||||
|
||||
measureRelationshipCaptions: (relationships) ->
|
||||
for relationship in relationships
|
||||
relationship.captionLength = @measureRelationshipCaption(relationship, relationship.type)
|
||||
relationship.captionLayout =
|
||||
if @captionFitsInsideArrowShaftWidth(relationship)
|
||||
"internal"
|
||||
else
|
||||
"external"
|
||||
|
||||
shortenCaption: (relationship, caption, targetWidth) ->
|
||||
shortCaption = caption
|
||||
while true
|
||||
if shortCaption.length <= 2
|
||||
return ['', 0]
|
||||
shortCaption = shortCaption.substr(0, shortCaption.length - 2) + '\u2026'
|
||||
width = @measureRelationshipCaption(relationship, shortCaption)
|
||||
if width < targetWidth
|
||||
return [shortCaption, width]
|
||||
layoutRelationships: (graph) ->
|
||||
for relationship in graph.relationships()
|
||||
dx = relationship.target.x - relationship.source.x
|
||||
dy = relationship.target.y - relationship.source.y
|
||||
relationship.naturalAngle = ((Math.atan2(dy, dx) / Math.PI * 180) + 180) % 360
|
||||
delete relationship.arrow
|
||||
|
||||
sortedNodes = graph.nodes().sort((a, b) ->
|
||||
b.relationshipCount(graph) - a.relationshipCount(graph))
|
||||
|
||||
for node in sortedNodes
|
||||
relationships = []
|
||||
relationships.push(relationship) for relationship in graph.relationships() when relationship.source is node or relationship.target is node
|
||||
|
||||
arrowAngles = { floating: {}, fixed: {} }
|
||||
relationshipMap = {}
|
||||
for relationship in relationships
|
||||
relationshipMap[relationship.id] = relationship
|
||||
|
||||
if node == relationship.source
|
||||
if relationship.hasOwnProperty('arrow')
|
||||
arrowAngles.fixed[relationship.id] = relationship.naturalAngle + relationship.arrow.deflection
|
||||
else
|
||||
arrowAngles.floating[relationship.id] = relationship.naturalAngle
|
||||
if node == relationship.target
|
||||
if relationship.hasOwnProperty('arrow')
|
||||
arrowAngles.fixed[relationship.id] = (relationship.naturalAngle - relationship.arrow.deflection + 180) % 360
|
||||
else
|
||||
arrowAngles.floating[relationship.id] = (relationship.naturalAngle + 180) % 360
|
||||
|
||||
distributedAngles = {}
|
||||
for id, angle of arrowAngles.floating
|
||||
distributedAngles[id] = angle
|
||||
for id, angle of arrowAngles.fixed
|
||||
distributedAngles[id] = angle
|
||||
|
||||
if (relationships.length > 1)
|
||||
distributedAngles = neo.utils.distributeCircular(arrowAngles, 30)
|
||||
|
||||
for id, angle of distributedAngles
|
||||
relationship = relationshipMap[id]
|
||||
if not relationship.hasOwnProperty('arrow')
|
||||
deflection = if node == relationship.source
|
||||
angle - relationship.naturalAngle
|
||||
else
|
||||
(relationship.naturalAngle - angle + 180) % 360
|
||||
|
||||
shaftRadius = (parseFloat(@style.forRelationship(relationship).get('shaft-width')) / 2) or 2
|
||||
headRadius = shaftRadius + 3
|
||||
headHeight = headRadius * 2
|
||||
|
||||
dx = relationship.target.x - relationship.source.x
|
||||
dy = relationship.target.y - relationship.source.y
|
||||
|
||||
square = (distance) -> distance * distance
|
||||
centreDistance = Math.sqrt(square(dx) + square(dy))
|
||||
|
||||
if Math.abs(deflection) < Math.PI / 180
|
||||
relationship.arrow = new neo.utils.straightArrow(
|
||||
relationship.source.radius,
|
||||
relationship.target.radius,
|
||||
centreDistance,
|
||||
shaftRadius,
|
||||
headRadius,
|
||||
headHeight,
|
||||
relationship.captionLayout
|
||||
)
|
||||
else
|
||||
relationship.arrow = new neo.utils.arcArrow(
|
||||
relationship.source.radius,
|
||||
relationship.target.radius,
|
||||
centreDistance,
|
||||
deflection,
|
||||
shaftRadius * 2,
|
||||
headRadius * 2,
|
||||
headHeight,
|
||||
relationship.captionLayout
|
||||
)
|
||||
|
||||
[relationship.shortCaption, relationship.shortCaptionLength] = if relationship.arrow.shaftLength > relationship.captionLength
|
||||
[relationship.caption, relationship.captionLength]
|
||||
else
|
||||
@shortenCaption(relationship, relationship.caption, relationship.arrow.shaftLength)
|
|
@ -0,0 +1,56 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
neo.utils.clickHandler = ->
|
||||
cc = (selection) ->
|
||||
|
||||
# euclidean distance
|
||||
dist = (a, b) ->
|
||||
Math.sqrt Math.pow(a[0] - b[0], 2), Math.pow(a[1] - b[1], 2)
|
||||
down = undefined
|
||||
tolerance = 5
|
||||
last = undefined
|
||||
wait = null
|
||||
selection.on "mousedown", ->
|
||||
d3.event.target.__data__.fixed = yes
|
||||
down = d3.mouse(document.body)
|
||||
last = +new Date()
|
||||
d3.event.stopPropagation()
|
||||
|
||||
selection.on "mouseup", ->
|
||||
if dist(down, d3.mouse(document.body)) > tolerance
|
||||
return
|
||||
else
|
||||
if wait
|
||||
window.clearTimeout wait
|
||||
wait = null
|
||||
event.dblclick d3.event.target.__data__
|
||||
else
|
||||
event.click d3.event.target.__data__
|
||||
wait = window.setTimeout(((e) ->
|
||||
->
|
||||
wait = null
|
||||
)(d3.event), 250)
|
||||
|
||||
event = d3.dispatch("click", "dblclick")
|
||||
d3.rebind cc, event, "on"
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.utils.loopArrow
|
||||
|
||||
constructor: (nodeRadius, straightLength, spreadDegrees, shaftWidth, headWidth, headLength, captionHeight) ->
|
||||
|
||||
spread = spreadDegrees * Math.PI / 180
|
||||
r1 = nodeRadius
|
||||
r2 = nodeRadius + headLength
|
||||
r3 = nodeRadius + straightLength
|
||||
loopRadius = r3 * Math.tan(spread / 2)
|
||||
shaftRadius = shaftWidth / 2
|
||||
@shaftLength = loopRadius * 3 + shaftWidth
|
||||
|
||||
class Point
|
||||
constructor: (@x, @y) ->
|
||||
toString: ->
|
||||
"#{@x} #{@y}"
|
||||
|
||||
normalPoint = (sweep, radius, displacement) ->
|
||||
localLoopRadius = radius * Math.tan(spread / 2)
|
||||
cy = radius / Math.cos(spread / 2)
|
||||
new Point(
|
||||
(localLoopRadius + displacement) * Math.sin(sweep),
|
||||
cy + (localLoopRadius + displacement) * Math.cos(sweep)
|
||||
)
|
||||
@midShaftPoint = normalPoint(0, r3, shaftRadius + captionHeight / 2 + 2)
|
||||
startPoint = (radius, displacement) ->
|
||||
normalPoint((Math.PI + spread) / 2, radius, displacement)
|
||||
endPoint = (radius, displacement) ->
|
||||
normalPoint(-(Math.PI + spread) / 2, radius, displacement)
|
||||
|
||||
@outline = ->
|
||||
inner = loopRadius - shaftRadius
|
||||
outer = loopRadius + shaftRadius
|
||||
[
|
||||
'M', startPoint(r1, shaftRadius)
|
||||
'L', startPoint(r3, shaftRadius)
|
||||
'A', outer, outer, 0, 1, 1, endPoint(r3, shaftRadius)
|
||||
'L', endPoint(r2, shaftRadius)
|
||||
'L', endPoint(r2, -headWidth / 2)
|
||||
'L', endPoint(r1, 0)
|
||||
'L', endPoint(r2, headWidth / 2)
|
||||
'L', endPoint(r2, -shaftRadius)
|
||||
'L', endPoint(r3, -shaftRadius)
|
||||
'A', inner, inner, 0, 1, 0, startPoint(r3, -shaftRadius)
|
||||
'L', startPoint(r1, -shaftRadius)
|
||||
'Z'
|
||||
].join(' ')
|
||||
|
||||
@overlay = (minWidth) ->
|
||||
displacement = Math.max(minWidth / 2, shaftRadius)
|
||||
inner = loopRadius - displacement
|
||||
outer = loopRadius + displacement
|
||||
[
|
||||
'M', startPoint(r1, displacement)
|
||||
'L', startPoint(r3, displacement)
|
||||
'A', outer, outer, 0, 1, 1, endPoint(r3, displacement)
|
||||
'L', endPoint(r2, displacement)
|
||||
'L', endPoint(r2, -displacement)
|
||||
'L', endPoint(r3, -displacement)
|
||||
'A', inner, inner, 0, 1, 0, startPoint(r3, -displacement)
|
||||
'L', startPoint(r1, -displacement)
|
||||
'Z'
|
||||
].join(' ')
|
|
@ -0,0 +1,166 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.utils.pairwiseArcsRelationshipRouting
|
||||
constructor: (@style) ->
|
||||
|
||||
measureRelationshipCaption: (relationship, caption) ->
|
||||
fontFamily = 'sans-serif'
|
||||
padding = parseFloat(@style.forRelationship(relationship).get('padding'))
|
||||
neo.utils.measureText(caption, fontFamily, relationship.captionHeight) + padding * 2
|
||||
|
||||
captionFitsInsideArrowShaftWidth: (relationship) ->
|
||||
parseFloat(@style.forRelationship(relationship).get('shaft-width')) > relationship.captionHeight
|
||||
|
||||
measureRelationshipCaptions: (relationships) ->
|
||||
for relationship in relationships
|
||||
relationship.captionHeight = parseFloat(@style.forRelationship(relationship).get('font-size'))
|
||||
relationship.captionLength = @measureRelationshipCaption(relationship, relationship.caption)
|
||||
relationship.captionLayout =
|
||||
if @captionFitsInsideArrowShaftWidth(relationship) and not relationship.isLoop()
|
||||
"internal"
|
||||
else
|
||||
"external"
|
||||
|
||||
shortenCaption: (relationship, caption, targetWidth) ->
|
||||
shortCaption = caption || 'caption'
|
||||
while true
|
||||
if shortCaption.length <= 2
|
||||
return ['', 0]
|
||||
shortCaption = shortCaption.substr(0, shortCaption.length - 2) + '\u2026'
|
||||
width = @measureRelationshipCaption(relationship, shortCaption)
|
||||
if width < targetWidth
|
||||
return [shortCaption, width]
|
||||
|
||||
computeGeometryForNonLoopArrows: (nodePairs) ->
|
||||
square = (distance) -> distance * distance
|
||||
for nodePair in nodePairs
|
||||
if not nodePair.isLoop()
|
||||
dx = nodePair.nodeA.x - nodePair.nodeB.x
|
||||
dy = nodePair.nodeA.y - nodePair.nodeB.y
|
||||
angle = ((Math.atan2(dy, dx) / Math.PI * 180) + 360) % 360
|
||||
centreDistance = Math.sqrt(square(dx) + square(dy))
|
||||
for relationship in nodePair.relationships
|
||||
relationship.naturalAngle = if relationship.target is nodePair.nodeA
|
||||
(angle + 180) % 360
|
||||
else
|
||||
angle
|
||||
relationship.centreDistance = centreDistance
|
||||
|
||||
distributeAnglesForLoopArrows: (nodePairs, relationships) ->
|
||||
for nodePair in nodePairs
|
||||
if nodePair.isLoop()
|
||||
angles = []
|
||||
node = nodePair.nodeA
|
||||
for relationship in relationships
|
||||
if not relationship.isLoop()
|
||||
if relationship.source is node
|
||||
angles.push relationship.naturalAngle
|
||||
if relationship.target is node
|
||||
angles.push relationship.naturalAngle + 180
|
||||
angles = angles.map((a) -> (a + 360) % 360).sort((a, b) -> a - b)
|
||||
if angles.length > 0
|
||||
biggestGap =
|
||||
start: 0
|
||||
end: 0
|
||||
for angle, i in angles
|
||||
start = angle
|
||||
end = if i == angles.length - 1
|
||||
angles[0] + 360
|
||||
else
|
||||
angles[i + 1]
|
||||
if end - start > biggestGap.end - biggestGap.start
|
||||
biggestGap.start = start
|
||||
biggestGap.end = end
|
||||
separation = (biggestGap.end - biggestGap.start) / (nodePair.relationships.length + 1)
|
||||
for relationship, i in nodePair.relationships
|
||||
relationship.naturalAngle = (biggestGap.start + (i + 1) * separation - 90) % 360
|
||||
else
|
||||
separation = 360 / nodePair.relationships.length
|
||||
for relationship, i in nodePair.relationships
|
||||
relationship.naturalAngle = i * separation
|
||||
|
||||
layoutRelationships: (graph) ->
|
||||
nodePairs = graph.groupedRelationships()
|
||||
@computeGeometryForNonLoopArrows(nodePairs)
|
||||
@distributeAnglesForLoopArrows(nodePairs, graph.relationships())
|
||||
|
||||
for nodePair in nodePairs
|
||||
for relationship in nodePair.relationships
|
||||
delete relationship.arrow
|
||||
|
||||
middleRelationshipIndex = (nodePair.relationships.length - 1) / 2
|
||||
defaultDeflectionStep = 30
|
||||
maximumTotalDeflection = 150
|
||||
numberOfSteps = nodePair.relationships.length - 1
|
||||
totalDeflection = defaultDeflectionStep * numberOfSteps
|
||||
|
||||
deflectionStep = if totalDeflection > maximumTotalDeflection then maximumTotalDeflection/numberOfSteps else defaultDeflectionStep
|
||||
|
||||
for relationship, i in nodePair.relationships
|
||||
|
||||
shaftWidth = parseFloat(@style.forRelationship(relationship).get('shaft-width')) or 2
|
||||
headWidth = shaftWidth + 6
|
||||
headHeight = headWidth
|
||||
|
||||
if nodePair.isLoop()
|
||||
relationship.arrow = new neo.utils.loopArrow(
|
||||
relationship.source.radius,
|
||||
40,
|
||||
defaultDeflectionStep,
|
||||
shaftWidth,
|
||||
headWidth,
|
||||
headHeight,
|
||||
relationship.captionHeight
|
||||
)
|
||||
else
|
||||
if i == middleRelationshipIndex
|
||||
relationship.arrow = new neo.utils.straightArrow(
|
||||
relationship.source.radius,
|
||||
relationship.target.radius,
|
||||
relationship.centreDistance,
|
||||
shaftWidth,
|
||||
headWidth,
|
||||
headHeight,
|
||||
relationship.captionLayout
|
||||
)
|
||||
else
|
||||
deflection = deflectionStep * (i - middleRelationshipIndex)
|
||||
|
||||
if nodePair.nodeA isnt relationship.source
|
||||
deflection *= -1
|
||||
|
||||
relationship.arrow = new neo.utils.arcArrow(
|
||||
relationship.source.radius,
|
||||
relationship.target.radius,
|
||||
relationship.centreDistance,
|
||||
deflection,
|
||||
shaftWidth,
|
||||
headWidth,
|
||||
headHeight,
|
||||
relationship.captionLayout
|
||||
)
|
||||
|
||||
[relationship.shortCaption, relationship.shortCaptionLength] = if relationship.arrow.shaftLength > relationship.captionLength
|
||||
[relationship.caption, relationship.captionLength]
|
||||
else
|
||||
@shortenCaption(relationship, relationship.caption, relationship.arrow.shaftLength)
|
|
@ -0,0 +1,82 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
class neo.utils.straightArrow
|
||||
|
||||
constructor: (startRadius, endRadius, centreDistance, shaftWidth, headWidth, headHeight, captionLayout) ->
|
||||
|
||||
@length = centreDistance - (startRadius + endRadius)
|
||||
|
||||
@shaftLength = @length - headHeight
|
||||
startArrow = startRadius
|
||||
endShaft = startArrow + @shaftLength
|
||||
endArrow = startArrow + @length
|
||||
shaftRadius = shaftWidth / 2
|
||||
headRadius = headWidth / 2
|
||||
|
||||
@midShaftPoint =
|
||||
x: startArrow + @shaftLength / 2
|
||||
y: 0
|
||||
|
||||
@outline = (shortCaptionLength) ->
|
||||
if captionLayout is "external"
|
||||
startBreak = startArrow + (@shaftLength - shortCaptionLength) / 2
|
||||
endBreak = endShaft - (@shaftLength - shortCaptionLength) / 2
|
||||
|
||||
[
|
||||
'M', startArrow, shaftRadius,
|
||||
'L', startBreak, shaftRadius,
|
||||
'L', startBreak, -shaftRadius,
|
||||
'L', startArrow, -shaftRadius,
|
||||
'Z'
|
||||
'M', endBreak, shaftRadius,
|
||||
'L', endShaft, shaftRadius,
|
||||
'L', endShaft, headRadius,
|
||||
'L', endArrow, 0,
|
||||
'L', endShaft, -headRadius,
|
||||
'L', endShaft, -shaftRadius,
|
||||
'L', endBreak, -shaftRadius,
|
||||
'Z'
|
||||
].join(' ')
|
||||
else
|
||||
[
|
||||
'M', startArrow, shaftRadius,
|
||||
'L', endShaft, shaftRadius,
|
||||
'L', endShaft, headRadius,
|
||||
'L', endArrow, 0,
|
||||
'L', endShaft, -headRadius,
|
||||
'L', endShaft, -shaftRadius,
|
||||
'L', startArrow, -shaftRadius,
|
||||
'Z'
|
||||
].join(' ')
|
||||
|
||||
@overlay = (minWidth) ->
|
||||
radius = Math.max(minWidth / 2, shaftRadius)
|
||||
[
|
||||
'M', startArrow, radius,
|
||||
'L', endArrow, radius,
|
||||
'L', endArrow, -radius,
|
||||
'L', startArrow, -radius,
|
||||
'Z'
|
||||
].join(' ')
|
||||
|
||||
deflection: 0
|
|
@ -0,0 +1,54 @@
|
|||
###!
|
||||
Copyright (c) 2002-2017 "Neo Technology,"
|
||||
Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
|
||||
This file is part of Neo4j.
|
||||
|
||||
Neo4j is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
###
|
||||
|
||||
'use strict'
|
||||
|
||||
neo.utils.measureText = do ->
|
||||
measureUsingCanvas = (text, font) ->
|
||||
canvasSelection = d3.select('canvas#textMeasurementCanvas').data([this])
|
||||
canvasSelection.enter().append('canvas')
|
||||
.attr('id', 'textMeasurementCanvas')
|
||||
.style('display', 'none')
|
||||
|
||||
canvas = canvasSelection.node()
|
||||
context = canvas.getContext('2d')
|
||||
context.font = font
|
||||
context.measureText(text).width
|
||||
|
||||
cache = do () ->
|
||||
cacheSize = 10000
|
||||
map = {}
|
||||
list = []
|
||||
(key, calc) ->
|
||||
cached = map[key]
|
||||
if cached
|
||||
cached
|
||||
else
|
||||
result = calc()
|
||||
if (list.length > cacheSize)
|
||||
delete map[list.splice(0, 1)]
|
||||
list.push(key)
|
||||
map[key] = result
|
||||
|
||||
return (text, fontFamily, fontSize) ->
|
||||
font = 'normal normal normal ' + fontSize + 'px/normal ' + fontFamily;
|
||||
cache(text + font, () ->
|
||||
measureUsingCanvas(text, font)
|
||||
)
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function createGraph (nodes, relationships) {
|
||||
let graph = new neo.models.Graph()
|
||||
graph.addNodes(mapNodes(nodes))
|
||||
graph.addRelationships(mapRelationships(relationships, graph))
|
||||
graph.display = { initialNodeDisplay: 300, nodeCount: 1 }
|
||||
return graph
|
||||
}
|
||||
|
||||
export function mapNodes (nodes) {
|
||||
return nodes.map((node) => new neo.models.Node(node.id, node.labels, node.properties))
|
||||
}
|
||||
|
||||
export function mapRelationships (relationships, graph) {
|
||||
return relationships.map((rel) => {
|
||||
const source = graph.findNode(rel.startNodeId)
|
||||
const target = graph.findNode(rel.endNodeId)
|
||||
return new neo.models.Relationship(rel.id, source, target, rel.type, rel.properties)
|
||||
})
|
||||
}
|
||||
|
||||
export function getGraphStats (graph) {
|
||||
let labelStats = {}
|
||||
let relTypeStats = {}
|
||||
|
||||
graph.nodes().forEach((node) => {
|
||||
node.labels.forEach((label) => {
|
||||
if (labelStats['*']) {
|
||||
labelStats['*'].count = labelStats['*'].count + 1
|
||||
} else {
|
||||
labelStats['*'] = {
|
||||
count: 1,
|
||||
properties: []
|
||||
}
|
||||
}
|
||||
if (labelStats[label]) {
|
||||
labelStats[label].count = labelStats[label].count + 1
|
||||
labelStats[label].properties = Object.assign({}, labelStats[label].properties, node.propertyMap)
|
||||
} else {
|
||||
labelStats[label] = {
|
||||
count: 1,
|
||||
properties: node.propertyMap
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
graph.relationships().forEach((rel) => {
|
||||
if (relTypeStats['*']) {
|
||||
relTypeStats['*'].count = relTypeStats['*'].count + 1
|
||||
} else {
|
||||
relTypeStats['*'] = {
|
||||
count: 1,
|
||||
properties: []
|
||||
}
|
||||
}
|
||||
if (relTypeStats[rel.type]) {
|
||||
relTypeStats[rel.type].count = relTypeStats[rel.type].count + 1
|
||||
relTypeStats[rel.type].properties = Object.assign({}, relTypeStats[rel.type].properties, rel.propertyMap)
|
||||
} else {
|
||||
relTypeStats[rel.type] = {
|
||||
count: 1,
|
||||
properties: rel.propertyMap
|
||||
}
|
||||
}
|
||||
})
|
||||
return {labels: labelStats, relTypes: relTypeStats}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
import { stringifyMod } from '../utils'
|
||||
import * as mappings from './boltMappings'
|
||||
import { BoltConnectionError, createErrorObject } from '../exceptions'
|
||||
var neo4j = require('neo4j-driver')
|
||||
|
||||
let _drivers = null
|
||||
let _useRoutingConfig = false
|
||||
let _routingAvailable = false
|
||||
const runningQueryRegister = {}
|
||||
|
||||
const _useRouting = () => _useRoutingConfig && _routingAvailable
|
||||
|
||||
const _getDriver = (host, auth, opts, protocol) => {
|
||||
const boltHost = protocol + (host || '').split('bolt://').join('')
|
||||
return neo4j.driver(boltHost, auth, opts)
|
||||
}
|
||||
|
||||
const _validateConnection = (driver, res, rej) => {
|
||||
if (!driver || !driver.session) return rej('No connection')
|
||||
const tmp = driver.session()
|
||||
tmp.run('CALL db.indexes()').then(() => {
|
||||
tmp.close()
|
||||
res(driver)
|
||||
}).catch((e) => {
|
||||
rej([e, driver])
|
||||
})
|
||||
}
|
||||
|
||||
const _routingAvailability = () => {
|
||||
return directTransaction('CALL dbms.procedures()').then((res) => {
|
||||
const names = res.records.map((r) => r.get('name'))
|
||||
return names.indexOf('dbms.cluster.overview') > -1
|
||||
})
|
||||
}
|
||||
|
||||
const _getDriversObj = (props, opts = {}) => {
|
||||
const driversObj = {}
|
||||
const auth = opts.withoutCredentials || !props.username
|
||||
? undefined
|
||||
: neo4j.auth.basic(props.username, props.password)
|
||||
const getDirectDriver = () => {
|
||||
if (driversObj.direct) return driversObj.direct
|
||||
driversObj.direct = _getDriver(props.host, auth, opts, 'bolt://')
|
||||
return driversObj.direct
|
||||
}
|
||||
const getRoutedDriver = () => {
|
||||
if (!_useRouting()) return getDirectDriver()
|
||||
if (driversObj.routed) return driversObj.routed
|
||||
driversObj.routed = _getDriver(props.host, auth, opts, 'bolt+routing://')
|
||||
return driversObj.routed
|
||||
}
|
||||
return {
|
||||
getDirectDriver,
|
||||
getRoutedDriver,
|
||||
close: () => {
|
||||
if (driversObj.direct) driversObj.direct.close()
|
||||
if (driversObj.routed) driversObj.routed.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function directConnect (props, opts = {}, onLostConnection = () => {}) {
|
||||
const p = new Promise((resolve, reject) => {
|
||||
const creds = opts.withoutCredentials || !props.username
|
||||
? undefined
|
||||
: neo4j.auth.basic(props.username, props.password)
|
||||
const driver = _getDriver(props.host, creds, opts, 'bolt://')
|
||||
driver.onError = (e) => {
|
||||
onLostConnection(e)
|
||||
reject([e, driver])
|
||||
}
|
||||
_validateConnection(driver, resolve, reject)
|
||||
})
|
||||
return p
|
||||
}
|
||||
|
||||
function openConnection (props, opts = {}, onLostConnection) {
|
||||
const p = new Promise((resolve, reject) => {
|
||||
const driversObj = _getDriversObj(props, opts)
|
||||
const driver = driversObj.getDirectDriver()
|
||||
driver.onError = (e) => {
|
||||
onLostConnection(e)
|
||||
_drivers = null
|
||||
driversObj.close()
|
||||
reject([e, driver])
|
||||
}
|
||||
const myResolve = (driver) => {
|
||||
_drivers = driversObj
|
||||
_routingAvailability()
|
||||
.then((r) => {
|
||||
if (r) _routingAvailable = true
|
||||
if (!r) _routingAvailable = false
|
||||
})
|
||||
.catch((e) => (_routingAvailable = false))
|
||||
resolve(driver)
|
||||
}
|
||||
const myReject = (err) => {
|
||||
_drivers = null
|
||||
driversObj.close()
|
||||
reject(err)
|
||||
}
|
||||
_validateConnection(driver, myResolve, myReject)
|
||||
})
|
||||
return p
|
||||
}
|
||||
|
||||
function _trackedTransaction (input, parameters = {}, session, requestId = null) {
|
||||
const id = requestId || v4()
|
||||
if (!session) {
|
||||
return [id, Promise.reject(createErrorObject(BoltConnectionError))]
|
||||
}
|
||||
const closeFn = (cb = () => {}) => {
|
||||
session.close(cb)
|
||||
if (runningQueryRegister[id]) delete runningQueryRegister[id]
|
||||
}
|
||||
runningQueryRegister[id] = closeFn
|
||||
const queryPromise = session.run(input, parameters)
|
||||
.then((r) => {
|
||||
closeFn()
|
||||
return r
|
||||
})
|
||||
.catch((e) => {
|
||||
closeFn()
|
||||
throw e
|
||||
})
|
||||
return [id, queryPromise]
|
||||
}
|
||||
|
||||
function cancelTransaction (id, cb) {
|
||||
if (runningQueryRegister[id]) runningQueryRegister[id](cb)
|
||||
}
|
||||
|
||||
function _transaction (input, parameters, session) {
|
||||
if (!session) return Promise.reject(createErrorObject(BoltConnectionError))
|
||||
return session.run(input, parameters)
|
||||
.then((r) => {
|
||||
session.close()
|
||||
return r
|
||||
})
|
||||
.catch((e) => {
|
||||
session.close()
|
||||
throw e
|
||||
})
|
||||
}
|
||||
|
||||
function directTransaction (input, parameters, requestId = null, cancelable = false) {
|
||||
const session = _drivers ? _drivers.getDirectDriver().session() : false
|
||||
if (!cancelable) return _transaction(input, parameters, session)
|
||||
return _trackedTransaction(input, parameters, session, requestId)
|
||||
}
|
||||
|
||||
function routedReadTransaction (input, parameters, requestId = null, cancelable = false) {
|
||||
const session = _drivers ? _drivers.getRoutedDriver().session(neo4j.session.READ) : false
|
||||
if (!cancelable) return _transaction(input, parameters, session)
|
||||
return _trackedTransaction(input, parameters, session, requestId)
|
||||
}
|
||||
|
||||
function routedWriteTransaction (input, parameters, requestId = null, cancelable = false) {
|
||||
const session = _drivers ? _drivers.getRoutedDriver().session(neo4j.session.WRITE) : false
|
||||
if (!cancelable) return _transaction(input, parameters, session)
|
||||
return _trackedTransaction(input, parameters, session, requestId)
|
||||
}
|
||||
|
||||
export default {
|
||||
directConnect,
|
||||
openConnection,
|
||||
closeConnection: () => {
|
||||
if (_drivers) {
|
||||
_drivers.close()
|
||||
_drivers = null
|
||||
}
|
||||
},
|
||||
directTransaction,
|
||||
routedReadTransaction,
|
||||
routedWriteTransaction,
|
||||
cancelTransaction,
|
||||
useRoutingConfig: (shouldWe) => (_useRoutingConfig = shouldWe),
|
||||
recordsToTableArray: (records, convertInts = true) => {
|
||||
const intChecker = convertInts ? neo4j.isInt : () => true
|
||||
const intConverter = convertInts ? (val) => val.toString() : (val) => val
|
||||
return mappings.recordsToTableArray(records, intChecker, intConverter)
|
||||
},
|
||||
stringifyRows: (rows) => {
|
||||
if (!Array.isArray(rows)) return rows
|
||||
const flat = mappings.flattenProperties(rows)
|
||||
if (!Array.isArray(flat)) return rows
|
||||
return flat.map((col) => {
|
||||
if (!col) return col
|
||||
return col.map((fVal) => {
|
||||
return stringifyMod()(fVal, (val) => {
|
||||
if (neo4j.isInt(val)) return val.toString()
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
extractNodesAndRelationshipsFromRecords: (records) => {
|
||||
return mappings.extractNodesAndRelationshipsFromRecords(records, neo4j.types)
|
||||
},
|
||||
extractNodesAndRelationshipsFromRecordsForOldVis: (records, filterRels = true) => {
|
||||
const intChecker = neo4j.isInt
|
||||
const intConverter = (val) => val.toString()
|
||||
return mappings.extractNodesAndRelationshipsFromRecordsForOldVis(records, neo4j.types, filterRels, intChecker, intConverter)
|
||||
},
|
||||
extractPlan: (result) => {
|
||||
return mappings.extractPlan(result)
|
||||
},
|
||||
retrieveFormattedUpdateStatistics: mappings.retrieveFormattedUpdateStatistics,
|
||||
itemIntToNumber: (item) => mappings.itemIntToString(item, neo4j.isInt, (val) => val.toNumber()),
|
||||
neo4j: neo4j
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* global location */
|
||||
import bolt from './bolt'
|
||||
import { getUrlInfo } from '../utils'
|
||||
|
||||
export const getEncryptionMode = () => {
|
||||
return location.protocol === 'https:'
|
||||
}
|
||||
|
||||
export const getDiscoveryEndpoint = () => {
|
||||
const url = location.host ? location.href : 'http://localhost:7474/'
|
||||
const info = getUrlInfo(url)
|
||||
return `${info.protocol}//${info.host}/`
|
||||
}
|
||||
|
||||
export const getServerConfig = (includePrefixes = []) => {
|
||||
return getJmxValues([['Configuration']])
|
||||
.then((confs) => {
|
||||
const conf = confs[0]
|
||||
let filtered
|
||||
if (conf) {
|
||||
Object.keys(conf)
|
||||
.filter((key) => includePrefixes.length < 1 || includePrefixes.some((pfx) => key.startsWith(pfx)))
|
||||
.forEach((key) => (filtered = {...filtered, [key]: bolt.itemIntToNumber(conf[key].value)}))
|
||||
}
|
||||
return filtered || conf
|
||||
})
|
||||
}
|
||||
|
||||
export const getJmxValues = (nameAttributePairs = []) => {
|
||||
if (!nameAttributePairs.length) return Promise.reject(null)
|
||||
return bolt.directTransaction('CALL dbms.queryJmx("org.neo4j:*")')
|
||||
.then((res) => {
|
||||
let out = []
|
||||
nameAttributePairs.forEach((pair) => {
|
||||
const [name, attribute = null] = pair
|
||||
if (!name) return out.push(null)
|
||||
const part = res.records.filter((record) => record.get('name').match(new RegExp(name + '$')))
|
||||
if (!part.length) return out.push(null)
|
||||
const attributes = part[0].get('attributes')
|
||||
if (!attribute) return out.push(attributes)
|
||||
const key = attribute
|
||||
if (typeof attributes[key] === 'undefined') return out.push(null)
|
||||
const val = bolt.itemIntToNumber(attributes[key].value)
|
||||
out.push({ [key]: val })
|
||||
})
|
||||
return out
|
||||
}).catch((e) => {
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
export const isConfigValTruthy = (val) => [true, 'true', 'yes', 1, '1'].indexOf(val) > -1
|
||||
export const isConfigValFalsy = (val) => [false, 'false', 'no', 0, '0'].indexOf(val) > -1
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import updateStatsFields from './updateStatisticsFields'
|
||||
|
||||
export function toObjects (records, intChecker, intConverter) {
|
||||
const recordValues = records.map((record) => {
|
||||
let out = []
|
||||
record.forEach((val, key) => out.push(itemIntToString(val, intChecker, intConverter)))
|
||||
return out
|
||||
})
|
||||
return recordValues
|
||||
}
|
||||
|
||||
export function recordsToTableArray (records, intChecker, intConverter) {
|
||||
const recordValues = toObjects(records, intChecker, intConverter)
|
||||
const keys = records[0].keys
|
||||
return [[...keys], ...recordValues]
|
||||
}
|
||||
|
||||
export function itemIntToString (item, intChecker, intConverter) {
|
||||
if (intChecker(item)) return intConverter(item)
|
||||
if (Array.isArray(item)) return arrayIntToString(item, intChecker, intConverter)
|
||||
if (['number', 'string', 'boolean'].indexOf(typeof item) !== -1) return item
|
||||
if (item === null) return item
|
||||
if (typeof item === 'object') return objIntToString(item, intChecker, intConverter)
|
||||
}
|
||||
|
||||
export function arrayIntToString (arr, intChecker, intConverter) {
|
||||
return arr.map((item) => itemIntToString(item, intChecker, intConverter))
|
||||
}
|
||||
|
||||
export function objIntToString (obj, intChecker, intConverter) {
|
||||
let newObj = {}
|
||||
Object.keys(obj).forEach((key) => {
|
||||
newObj[key] = itemIntToString(obj[key], intChecker, intConverter)
|
||||
})
|
||||
return newObj
|
||||
}
|
||||
|
||||
export function extractPlan (result) {
|
||||
if (result.summary && (result.summary.plan || result.summary.profile)) {
|
||||
const rawPlan = result.summary.profile || result.summary.plan
|
||||
const boltPlanToRESTPlanShared = (plan) => {
|
||||
return {
|
||||
operatorType: plan.operatorType,
|
||||
LegacyExpression: plan.arguments.LegacyExpression,
|
||||
ExpandExpression: plan.arguments.ExpandExpression,
|
||||
DbHits: plan.dbHits,
|
||||
Rows: plan.rows,
|
||||
EstimatedRows: plan.arguments.EstimatedRows,
|
||||
identifiers: plan.identifiers,
|
||||
Index: plan.arguments.Index,
|
||||
children: plan.children.map(boltPlanToRESTPlanShared)
|
||||
}
|
||||
}
|
||||
let obj = boltPlanToRESTPlanShared(rawPlan)
|
||||
obj['runtime-impl'] = rawPlan.arguments['runtime-impl']
|
||||
obj['planner-impl'] = rawPlan.arguments['planner-impl']
|
||||
obj['version'] = rawPlan.arguments['version']
|
||||
obj['KeyNames'] = rawPlan.arguments['KeyNames']
|
||||
obj['planner'] = rawPlan.arguments['planner']
|
||||
obj['runtime'] = rawPlan.arguments['runtime']
|
||||
return {root: obj}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function extractNodesAndRelationshipsFromRecords (records, types) {
|
||||
if (records.length === 0) {
|
||||
return { nodes: [], relationships: [] }
|
||||
}
|
||||
|
||||
let keys = records[0].keys
|
||||
let rawNodes = []
|
||||
let rawRels = []
|
||||
records.forEach((record) => {
|
||||
let graphItems = keys.map((key) => record.get(key))
|
||||
rawNodes = [...rawNodes, ...graphItems.filter((item) => item instanceof types.Node)]
|
||||
rawRels = [...rawRels, ...graphItems.filter((item) => item instanceof types.Relationship)]
|
||||
let paths = graphItems.filter((item) => item instanceof types.Path)
|
||||
paths.forEach((item) => extractNodesAndRelationshipsFromPath(item, rawNodes, rawRels, types))
|
||||
})
|
||||
return { nodes: rawNodes, relationships: rawRels }
|
||||
}
|
||||
|
||||
const resultContainsGraphKeys = (keys) => {
|
||||
return (keys.includes('nodes') && keys.includes('relationships'))
|
||||
}
|
||||
|
||||
export function extractNodesAndRelationshipsFromRecordsForOldVis (records, types, filterRels, intChecker, intConverter) {
|
||||
if (records.length === 0) {
|
||||
return { nodes: [], relationships: [] }
|
||||
}
|
||||
let keys = records[0].keys
|
||||
let rawNodes = []
|
||||
let rawRels = []
|
||||
if (resultContainsGraphKeys(keys)) {
|
||||
rawNodes = [...rawNodes, ...records[0].get(keys[0])]
|
||||
rawRels = [...rawRels, ...records[0].get(keys[1])]
|
||||
} else {
|
||||
records.forEach((record) => {
|
||||
let graphItems = keys.map((key) => record.get(key))
|
||||
rawNodes = [...rawNodes, ...graphItems.filter((item) => item instanceof types.Node)]
|
||||
rawRels = [...rawRels, ...graphItems.filter((item) => item instanceof types.Relationship)]
|
||||
let paths = graphItems.filter((item) => item instanceof types.Path)
|
||||
paths.forEach((item) => extractNodesAndRelationshipsFromPath(item, rawNodes, rawRels, types))
|
||||
})
|
||||
}
|
||||
const nodes = rawNodes.map((item) => {
|
||||
return {id: item.identity.toString(), labels: item.labels, properties: itemIntToString(item.properties, intChecker, intConverter)}
|
||||
})
|
||||
let relationships = rawRels
|
||||
if (filterRels) {
|
||||
relationships = rawRels.filter((item) => nodes.filter((node) => node.id === item.start.toString()).length > 0 && nodes.filter((node) => node.id === item.end.toString()).length > 0)
|
||||
}
|
||||
relationships = relationships.map((item) => {
|
||||
return {id: item.identity.toString(), startNodeId: item.start.toString(), endNodeId: item.end.toString(), type: item.type, properties: itemIntToString(item.properties, intChecker, intConverter)}
|
||||
})
|
||||
return { nodes: nodes, relationships: relationships }
|
||||
}
|
||||
|
||||
const extractNodesAndRelationshipsFromPath = (item, rawNodes, rawRels) => {
|
||||
let paths = Array.isArray(item) ? item : [item]
|
||||
paths.forEach((path) => {
|
||||
path.segments.forEach((segment) => {
|
||||
rawNodes.push(segment.start)
|
||||
rawNodes.push(segment.end)
|
||||
rawRels.push(segment.relationship)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const retrieveFormattedUpdateStatistics = (result) => {
|
||||
if (result.summary.counters) {
|
||||
const stats = result.summary.counters._stats
|
||||
const statsMessages = updateStatsFields.filter(field => stats[field.field] > 0).map(field => `${field.verb} ${stats[field.field]} ${stats[field.field] === 1 ? field.singular : field.plural}`)
|
||||
return statsMessages.join(', ')
|
||||
} else return null
|
||||
}
|
||||
|
||||
export const flattenProperties = (rows) => {
|
||||
return rows.map((row) => row.map((entry) => (entry && entry.properties) ? entry.properties : entry))
|
||||
}
|
|
@ -0,0 +1,332 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* global test, expect */
|
||||
import { v1 as neo4j } from 'neo4j-driver-alias'
|
||||
import {
|
||||
itemIntToString,
|
||||
arrayIntToString,
|
||||
objIntToString,
|
||||
extractNodesAndRelationshipsFromRecords,
|
||||
extractPlan,
|
||||
flattenProperties
|
||||
} from './boltMappings'
|
||||
|
||||
describe('boltMappings', () => {
|
||||
describe('itemIntToString', () => {
|
||||
test('should convert matching values with provided function', () => {
|
||||
// Given
|
||||
const tests = [
|
||||
{val: 'hello', checker: (_) => false, converter: (_) => false, expected: 'hello'},
|
||||
{val: ['hello'], checker: (_) => false, converter: (val) => false, expected: ['hello']},
|
||||
{val: null, checker: (_) => false, converter: (_) => false, expected: null},
|
||||
{
|
||||
val: {str: 'hello'},
|
||||
checker: (_) => true,
|
||||
converter: (val) => {
|
||||
val.str = val.str.toUpperCase()
|
||||
return val
|
||||
},
|
||||
expected: {str: 'HELLO'}
|
||||
}
|
||||
]
|
||||
|
||||
// When and Then
|
||||
tests.forEach((test) => {
|
||||
expect(itemIntToString(test.val, test.checker, test.converter)).toEqual(test.expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('arrayIntToString', () => {
|
||||
test('should convert matching values with provided function', () => {
|
||||
// Given
|
||||
const tests = [
|
||||
{val: ['hello', 1], checker: (_) => false, converter: (val) => false, expected: ['hello', 1]},
|
||||
{val: ['hello', ['ola', 'hi']], checker: (val) => typeof val === 'string', converter: (val) => val.toUpperCase(), expected: ['HELLO', ['OLA', 'HI']]},
|
||||
{val: ['hello', 1], checker: (val) => typeof val === 'string', converter: (val) => val.toUpperCase(), expected: ['HELLO', 1]}
|
||||
]
|
||||
|
||||
// When and Then
|
||||
tests.forEach((test) => {
|
||||
expect(arrayIntToString(test.val, test.checker, test.converter)).toEqual(test.expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('objIntToString', () => {
|
||||
test('should convert matching values with provided function', () => {
|
||||
// Given
|
||||
const tests = [
|
||||
{val: {arr: ['hello']}, checker: (_) => false, converter: (val) => false, expected: {arr: ['hello']}},
|
||||
{
|
||||
val: {
|
||||
arr: [
|
||||
'hello',
|
||||
[
|
||||
'ola',
|
||||
'hi'
|
||||
]
|
||||
],
|
||||
str: 'hello',
|
||||
num: 2,
|
||||
obj: {
|
||||
num: 3,
|
||||
str: 'inner hello'
|
||||
}
|
||||
},
|
||||
checker: (val) => typeof val === 'string',
|
||||
converter: (val) => val.toUpperCase(),
|
||||
expected: {
|
||||
arr: [
|
||||
'HELLO',
|
||||
[
|
||||
'OLA',
|
||||
'HI'
|
||||
]
|
||||
],
|
||||
str: 'HELLO',
|
||||
num: 2,
|
||||
obj: {
|
||||
num: 3,
|
||||
str: 'INNER HELLO'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// When and Then
|
||||
tests.forEach((test) => {
|
||||
expect(objIntToString(test.val, test.checker, test.converter)).toEqual(test.expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractNodesAndRelationshipsFromRecords', () => {
|
||||
test.skip('should map bolt records with a path to nodes and relationships', () => {
|
||||
let startNode = new neo4j.v1.types.Node('1', ['Person'], {prop1: 'prop1'})
|
||||
let endNode = new neo4j.v1.types.Node('2', ['Movie'], {prop2: 'prop2'})
|
||||
let relationship = new neo4j.v1.types.Relationship('3', startNode.identity, endNode.identity, 'ACTED_IN', {})
|
||||
let pathSegment = new neo4j.v1.types.PathSegment(startNode, relationship, endNode)
|
||||
let path = new neo4j.v1.types.Path(startNode, endNode, [pathSegment])
|
||||
let boltRecord = {
|
||||
keys: ['p'],
|
||||
get: (key) => path
|
||||
}
|
||||
|
||||
let {nodes, relationships} = extractNodesAndRelationshipsFromRecords([boltRecord], neo4j.v1.types)
|
||||
expect(nodes).to.have.lengthOf(2)
|
||||
let graphNodeStart = nodes.filter((node) => node.id === '1')[0]
|
||||
expect(graphNodeStart).to.exist
|
||||
expect(graphNodeStart.labels).toEqual(['Person'])
|
||||
expect(graphNodeStart.properties).toEqual({prop1: 'prop1'})
|
||||
let graphNodeEnd = nodes.filter((node) => node.id === '2')[0]
|
||||
expect(graphNodeEnd).to.exist
|
||||
expect(graphNodeEnd.labels).toEqual(['Movie'])
|
||||
expect(graphNodeEnd.properties).toEqual({prop2: 'prop2'})
|
||||
expect(relationships).to.have.lengthOf(1)
|
||||
expect(relationships[0].id).toEqual('3')
|
||||
expect(relationships[0].startNodeId).toEqual('1')
|
||||
expect(relationships[0].endNodeId).toEqual('2')
|
||||
expect(relationships[0].type).toEqual('ACTED_IN')
|
||||
expect(relationships[0].properties).toEqual({})
|
||||
})
|
||||
|
||||
test.skip('should map bolt nodes and relationships to graph nodes and relationships', () => {
|
||||
let startNode = new neo4j.v1.types.Node('1', ['Person'], {prop1: 'prop1'})
|
||||
let endNode = new neo4j.v1.types.Node('2', ['Movie'], {prop2: 'prop2'})
|
||||
let relationship = new neo4j.v1.types.Relationship('3', startNode.identity, endNode.identity, 'ACTED_IN', {})
|
||||
let boltRecord = {
|
||||
keys: ['r', 'n1', 'n2'],
|
||||
get: (key) => {
|
||||
if (key === 'r') {
|
||||
return relationship
|
||||
}
|
||||
if (key === 'n1') {
|
||||
return startNode
|
||||
}
|
||||
if (key === 'n2') {
|
||||
return endNode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let {nodes, relationships} = extractNodesAndRelationshipsFromRecords([boltRecord], neo4j.v1.types)
|
||||
expect(nodes).to.have.lengthOf(2)
|
||||
let graphNodeStart = nodes.filter((node) => node.id === '1')[0]
|
||||
expect(graphNodeStart).to.exist
|
||||
expect(graphNodeStart.labels).toEqual(['Person'])
|
||||
expect(graphNodeStart.properties).toEqual({prop1: 'prop1'})
|
||||
let graphNodeEnd = nodes.filter((node) => node.id === '2')[0]
|
||||
expect(graphNodeEnd).to.exist
|
||||
expect(graphNodeEnd.labels).toEqual(['Movie'])
|
||||
expect(graphNodeEnd.properties).toEqual({prop2: 'prop2'})
|
||||
expect(relationships).to.have.lengthOf(1)
|
||||
expect(relationships[0].id).toEqual('3')
|
||||
expect(relationships[0].startNodeId).toEqual('1')
|
||||
expect(relationships[0].endNodeId).toEqual('2')
|
||||
expect(relationships[0].type).toEqual('ACTED_IN')
|
||||
expect(relationships[0].properties).toEqual({})
|
||||
})
|
||||
|
||||
test.skip('should not include relationships where neither start or end node is not in nodes list', () => {
|
||||
let relationship = new neo4j.v1.types.Relationship('3', 1, 2, 'ACTED_IN', {})
|
||||
let boltRecord = {
|
||||
keys: ['r'],
|
||||
get: (key) => relationship
|
||||
}
|
||||
let relationships = extractNodesAndRelationshipsFromRecords([boltRecord], neo4j.v1.types).relationships
|
||||
expect(relationships.length).toBe(0)
|
||||
})
|
||||
test.skip('should not include relationships where end node is not in nodes list', () => {
|
||||
let startNode = new neo4j.v1.types.Node('1', ['Person'], {prop1: 'prop1'})
|
||||
let relationship = new neo4j.v1.types.Relationship('3', startNode.identity, 2, 'ACTED_IN', {})
|
||||
let boltRecord = {
|
||||
keys: ['r', 'n1'],
|
||||
get: (key) => {
|
||||
if (key === 'r') {
|
||||
return relationship
|
||||
}
|
||||
if (key === 'n1') {
|
||||
return startNode
|
||||
}
|
||||
}
|
||||
}
|
||||
let relationships = extractNodesAndRelationshipsFromRecords([boltRecord], neo4j.v1.types).relationships
|
||||
expect(relationships.length).toBe(0)
|
||||
})
|
||||
test.skip('should not include relationships where start node is not in nodes list', () => {
|
||||
let endNode = new neo4j.v1.types.Node('2', ['Movie'], {prop2: 'prop2'})
|
||||
let relationship = new neo4j.v1.types.Relationship('3', '1', endNode.identity, 'ACTED_IN', {})
|
||||
let boltRecord = {
|
||||
keys: ['r', 'n1'],
|
||||
get: (key) => {
|
||||
if (key === 'r') {
|
||||
return relationship
|
||||
}
|
||||
if (key === 'n1') {
|
||||
return endNode
|
||||
}
|
||||
}
|
||||
}
|
||||
let relationships = extractNodesAndRelationshipsFromRecords([boltRecord], neo4j.v1.types).relationships
|
||||
expect(relationships.length).toBe(0)
|
||||
})
|
||||
})
|
||||
describe('extractPlan', () => {
|
||||
const createPlan = () => {
|
||||
return {
|
||||
operatorType: 'operatorType',
|
||||
arguments: {
|
||||
LegacyExpression: 'legacy',
|
||||
ExpandExpression: 'expand',
|
||||
EstimatedRows: 10,
|
||||
Index: 1,
|
||||
version: 'version',
|
||||
KeyNames: ['keyname'],
|
||||
planner: 'planner',
|
||||
runtime: 'runtime',
|
||||
'planner-impl': 'planner-impl',
|
||||
'runtime-impl': 'runtime-impl'
|
||||
},
|
||||
identifiers: [],
|
||||
children: []
|
||||
}
|
||||
}
|
||||
|
||||
const checkExtractedPlan = (extractedPlan) => {
|
||||
expect(extractedPlan).to.not.be.null
|
||||
expect(extractedPlan.operatorType).toEqual('operatorType')
|
||||
expect(extractedPlan.identifiers).toEqual([])
|
||||
expect(extractedPlan.operatorType).toEqual('operatorType')
|
||||
expect(extractedPlan.LegacyExpression).toEqual('legacy')
|
||||
expect(extractedPlan.ExpandExpression).toEqual('expand')
|
||||
expect(extractedPlan.EstimatedRows).toEqual(10)
|
||||
expect(extractedPlan.Index).toEqual(1)
|
||||
expect(extractedPlan.version).toEqual('version')
|
||||
expect(extractedPlan.KeyNames).toEqual(['keyname'])
|
||||
expect(extractedPlan.planner).toEqual('planner')
|
||||
expect(extractedPlan.runtime).toEqual('runtime')
|
||||
expect(extractedPlan['planner-impl']).toEqual('planner-impl')
|
||||
expect(extractedPlan['runtime-impl']).toEqual('runtime-impl')
|
||||
}
|
||||
|
||||
test.skip('should extract plan from result summary', () => {
|
||||
// Given
|
||||
const result = {
|
||||
summary: {
|
||||
plan: createPlan()
|
||||
}
|
||||
}
|
||||
const extractedPlan = extractPlan(result).root
|
||||
checkExtractedPlan(extractedPlan)
|
||||
})
|
||||
|
||||
test.skip('should extract profile from result summary', () => {
|
||||
// Given
|
||||
const profile = createPlan()
|
||||
profile.dbHits = 20
|
||||
profile.rows = 14
|
||||
const result = {
|
||||
summary: {
|
||||
profile: profile
|
||||
}
|
||||
}
|
||||
const extractedPlan = extractPlan(result).root
|
||||
checkExtractedPlan(extractedPlan)
|
||||
expect(extractedPlan.DbHits).toEqual(20)
|
||||
expect(extractedPlan.Rows).toEqual(14)
|
||||
})
|
||||
|
||||
test.skip('should return null if no plan or profile is available', () => {
|
||||
const result = {
|
||||
summary: {}
|
||||
}
|
||||
expect(extractPlan(result)).to.be.null
|
||||
})
|
||||
})
|
||||
describe('flattenProperties', () => {
|
||||
test('should map properties to object when properties exist', () => {
|
||||
// Given
|
||||
const result = [
|
||||
[{properties: {foo: 'bar'}}]
|
||||
]
|
||||
const expectedResult = [
|
||||
[{foo: 'bar'}]
|
||||
]
|
||||
|
||||
// When
|
||||
const flattenedProperties = flattenProperties(result)
|
||||
|
||||
// Then
|
||||
expect(flattenedProperties).toEqual(expectedResult)
|
||||
})
|
||||
test('should not map properties to object when properties do not exist', () => {
|
||||
// Given
|
||||
const result = [
|
||||
[{x: {foo: 'bar'}}]
|
||||
]
|
||||
|
||||
// When
|
||||
const flattenedProperties = flattenProperties(result)
|
||||
|
||||
// Then
|
||||
expect(flattenedProperties).toEqual(result)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,14 @@
|
|||
export default [
|
||||
{ plural: 'constraints', singular: 'constraint', verb: 'added', field: 'constraintsAdded' },
|
||||
{ plural: 'constraints', singular: 'constraint', verb: 'removed', field: 'constraintsRemoved' },
|
||||
{ plural: 'indexes', singular: 'index', verb: 'added', field: 'indexesAdded' },
|
||||
{ plural: 'indexes', singular: 'index', verb: 'removed', field: 'indexesRemoved' },
|
||||
{ plural: 'labels', singular: 'label', verb: 'added', field: 'labelsAdded' },
|
||||
{ plural: 'labels', singular: 'label', verb: 'removed', field: 'labelsRemoved' },
|
||||
{ plural: 'nodes', singular: 'node', verb: 'created', field: 'nodesCreated' },
|
||||
{ plural: 'nodes', singular: 'node', verb: 'deleted', field: 'nodesDeleted' },
|
||||
{ plural: 'properties', singular: 'property', verb: 'set', field: 'propertiesSet' },
|
||||
{ plural: 'relationships', singular: 'relationship', verb: 'deleted', field: 'relationshipDeleted' },
|
||||
{ plural: 'relationships', singular: 'relationship', verb: 'deleted', field: 'relationshipsDeleted' },
|
||||
{ plural: 'relationships', singular: 'relationship', verb: 'created', field: 'relationshipsCreated' }
|
||||
]
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const hydrate = (initialState, state) => {
|
||||
if (!state) return state
|
||||
return (state.hydrated) ? state : { ...initialState, ...state, hydrated: true }
|
||||
}
|
||||
|
||||
export const dehydrate = (state) => {
|
||||
if (state) {
|
||||
delete state.hydrated
|
||||
}
|
||||
return state
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const AddServerValidationError = 'Wrong format. It should be ":server add name username:password@host:port"'
|
||||
export const CreateDataSourceValidationError = 'Wrong format. It should be ":datasource create {"name": "myName", "command": "RETURN rand()", "bookmarkId":"uuid-of-existing-bookmark", "refreshInterval": 10, "parameters": {}}"'
|
||||
export const RemoveDataSourceValidationError = 'Wrong format. It should be ":datasource remove uuid-of-existing-datasource"'
|
||||
export const BoltConnectionError = 'No connection found, did you connect to Neo4j?'
|
||||
export const BoltError = '#code# - #message#'
|
||||
export const Neo4jError = '#message#'
|
||||
export const UnknownCommandError = 'Unknown command #cmd#'
|
||||
export const BookmarkNotFoundError = 'No connection with the name #name# found. Add a bookmark before trying to connect.'
|
||||
export const OpenConnectionNotFoundError = 'No open connection with the name #name# found. You have to connect to a bookmark before you can use it.'
|
||||
export const CouldNotFetchRemoteGuideError = 'Can not fetch remote guide: #error#'
|
||||
export const FetchURLError = 'Could not fetch URL: "#error#". This could be due to the remote server policy. See your web browsers error console for more information.'
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as messages from './exceptionMessages'
|
||||
|
||||
export function getErrorMessage (errorObject) {
|
||||
let str = messages[errorObject.type]
|
||||
if (!str) return
|
||||
const keys = Object.keys(errorObject)
|
||||
keys.forEach((prop) => {
|
||||
const re = new RegExp('(#' + prop + '#)', 'g')
|
||||
str = str.replace(re, errorObject[prop])
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
export function createErrorObject (ErrorType, ...rest) {
|
||||
const obj = new ErrorType(...rest)
|
||||
if (!obj.code) obj.code = obj.type
|
||||
obj.message = getErrorMessage(obj)
|
||||
return obj
|
||||
}
|
||||
|
||||
export function UserException (message) {
|
||||
return {
|
||||
type: 'UserException',
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
export function ConnectionException (message, code = 'Connection Error') {
|
||||
return {fields: [{
|
||||
code,
|
||||
message
|
||||
}]}
|
||||
}
|
||||
|
||||
export function AddServerValidationError () {
|
||||
return {
|
||||
type: 'AddServerValidationError'
|
||||
}
|
||||
}
|
||||
|
||||
export function CreateDataSourceValidationError () {
|
||||
return {
|
||||
type: 'CreateDataSourceValidationError'
|
||||
}
|
||||
}
|
||||
|
||||
export function RemoveDataSourceValidationError () {
|
||||
return {
|
||||
type: 'RemoveDataSourceValidationError'
|
||||
}
|
||||
}
|
||||
|
||||
export function BoltConnectionError () {
|
||||
return {
|
||||
type: 'BoltConnectionError'
|
||||
}
|
||||
}
|
||||
|
||||
export function BoltError (obj) {
|
||||
return {
|
||||
type: 'BoltError',
|
||||
code: obj.fields[0].code,
|
||||
message: obj.fields[0].message
|
||||
}
|
||||
}
|
||||
|
||||
export function Neo4jError (obj) {
|
||||
return {
|
||||
type: 'Neo4jError',
|
||||
message: obj.message
|
||||
}
|
||||
}
|
||||
|
||||
export function ConnectionNotFoundError (name) {
|
||||
return {
|
||||
type: 'ConnectionNotFoundError',
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
export function OpenConnectionNotFoundError (name) {
|
||||
return {
|
||||
type: 'OpenConnectionNotFoundError',
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
export function UnknownCommandError (cmd) {
|
||||
return {
|
||||
type: 'UnknownCommandError',
|
||||
cmd
|
||||
}
|
||||
}
|
||||
|
||||
export function CouldNotFetchRemoteGuideError (error) {
|
||||
return {
|
||||
type: 'CouldNotFetchRemoteGuideError',
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
export function FetchURLError (error) {
|
||||
return {
|
||||
type: 'FetchURLError',
|
||||
error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* Copyright (c) 2002-2017 "Neo Technology,"
|
||||
* Network Engine for Objects in Lund AB [http://neotechnology.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* Neo4j is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const deepEquals = (x, y) => {
|
||||
if (x && y && typeof x === 'object' && typeof y === 'object') {
|
||||
if (Object.keys(x).length !== Object.keys(y).length) return false
|
||||
return Object.keys(x).every((key) => deepEquals(x[key], y[key]))
|
||||
}
|
||||
return (x === y)
|
||||
}
|
||||
|
||||
export const flatten = arr => arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [])
|
||||
|
||||
export const moveInArray = (fromIndex, toIndex, arr) => {
|
||||
if (!Array.isArray(arr)) return false
|
||||
if (fromIndex < 0 || fromIndex >= arr.length) return false
|
||||
if (toIndex < 0 || toIndex >= arr.length) return false
|
||||
const newArr = [].concat(arr)
|
||||
const el = arr[fromIndex]
|
||||
newArr.splice(fromIndex, 1)
|
||||
newArr.splice(toIndex, 0, el)
|
||||
return newArr
|
||||
}
|
||||
|
||||
export const debounce = (fn, time, context = null) => {
|
||||
let pending
|
||||
return (...args) => {
|
||||
if (pending) clearTimeout(pending)
|
||||
pending = setTimeout(() => typeof fn === 'function' && fn.apply(context, args), parseInt(time))
|
||||
}
|
||||
}
|
||||
|
||||
export const throttle = (fn, time, context = null) => {
|
||||
let blocking
|
||||
return (...args) => {
|
||||
if (blocking) return
|
||||
blocking = true
|
||||
typeof fn === 'function' && fn.apply(context, args)
|
||||
setTimeout(() => (blocking = false), parseInt(time))
|
||||
}
|
||||
}
|
||||
|
||||
export const isRoutingHost = (host) => {
|
||||
return /^bolt\+routing:\/\//.test(host)
|
||||
}
|
||||
|
||||
export const toBoltHost = (host) => {
|
||||
return 'bolt://' + (host || '') // prepend with bolt://
|
||||
.split('bolt://').join('') // remove bolt://
|
||||
.split('bolt+routing://').join('') // remove bolt+routing://
|
||||
}
|
||||
|
||||
export const hostIsAllowed = (uri, whitelist = null) => {
|
||||
if (whitelist === '*') return true
|
||||
const urlInfo = getUrlInfo(uri)
|
||||
const hostname = urlInfo.hostname
|
||||
const hostnamePlusProtocol = urlInfo.protocol + '//' + hostname
|
||||
|
||||
let whitelistedHosts = ['guides.neo4j.com', 'localhost']
|
||||
if (whitelist && whitelist !== '') {
|
||||
whitelistedHosts = whitelist.split(',')
|
||||
}
|
||||
return whitelistedHosts.indexOf(hostname) > -1 ||
|
||||
whitelistedHosts.indexOf(hostnamePlusProtocol) > -1
|
||||
}
|
||||
|
||||
export const getUrlInfo = (url) => {
|
||||
const reURLInformation = new RegExp([
|
||||
'^(?:(https?:)//)?', // protocol
|
||||
'(([^:/?#]*)(?::([0-9]+))?)', // host (hostname and port)
|
||||
'(/{0,1}[^?#]*)', // pathname
|
||||
'(\\?[^#]*|)', // search
|
||||
'(#.*|)$' // hash
|
||||
].join(''))
|
||||
const match = url.match(reURLInformation)
|
||||
return match && {
|
||||
protocol: match[1],
|
||||
host: match[2],
|
||||
hostname: match[3],
|
||||
port: match[4],
|
||||
pathname: match[5],
|
||||
search: match[6],
|
||||
hash: match[7]
|
||||
}
|
||||
}
|
||||
|
||||
export const getUrlParamValue = (name, url) => {
|
||||
if (!url) return false
|
||||
let out = []
|
||||
const re = new RegExp('[\\?&]' + name + '=([^&#]*)', 'g')
|
||||
let results
|
||||
while ((results = re.exec(url)) !== null) {
|
||||
if (results && results[1]) out.push(results[1])
|
||||
}
|
||||
if (!out.length) return undefined
|
||||
return out
|
||||
}
|
||||
|
||||
export const toHumanReadableBytes = (input) => {
|
||||
let number = +input
|
||||
if (!isFinite(number)) { return '-' }
|
||||
|
||||
if (number < 1024) {
|
||||
return `${number} B`
|
||||
}
|
||||
|
||||
number /= 1024
|
||||
let units = ['KiB', 'MiB', 'GiB', 'TiB']
|
||||
|
||||
for (let unit of Array.from(units)) {
|
||||
if (number < 1024) { return `${number.toFixed(2)} ${unit}` }
|
||||
number /= 1024
|
||||
}
|
||||
|
||||
return `${number.toFixed(2)} PiB`
|
||||
}
|
||||
|
||||
export const getBrowserName = function () {
|
||||
if (!!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0) {
|
||||
return 'Opera'
|
||||
}
|
||||
if (typeof InstallTrigger !== 'undefined') {
|
||||
return 'Firefox'
|
||||
}
|
||||
if (navigator.userAgent.match(/Version\/[\d.]+.*Safari/)) {
|
||||
return 'Safari'
|
||||
}
|
||||
if (window.chrome) {
|
||||
return 'Chrome'
|
||||
}
|
||||
if (document.documentMode) {
|
||||
return 'Internet Explorer'
|
||||
}
|
||||
if (window.StyleMedia) {
|
||||
return 'Edge'
|
||||
}
|
||||
return 'Unknown'
|
||||
}
|
||||
|
||||
export const removeComments = (string) => {
|
||||
return string.split(/\r?\n/).filter((line) => !line.startsWith('//')).join('\r\n')
|
||||
}
|
||||
|
||||
export const canUseDOM = () => !!(
|
||||
(typeof window !== 'undefined' &&
|
||||
window.document && window.document.createElement)
|
||||
)
|
||||
|
||||
export const stringifyMod = () => {
|
||||
const toString = Object.prototype.toString
|
||||
const isArray = Array.isArray || function (a) { return toString.call(a) === '[object Array]' }
|
||||
const escMap = {'"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t'}
|
||||
const escFunc = function (m) { return escMap[m] || '\\u' + (m.charCodeAt(0) + 0x10000).toString(16).substr(1) }
|
||||
const escRE = /[\\"\u0000-\u001F\u2028\u2029]/g
|
||||
return function stringify (value, modFn = null) {
|
||||
if (modFn) {
|
||||
const modVal = modFn && modFn(value)
|
||||
if (typeof modVal !== 'undefined') return modVal
|
||||
}
|
||||
if (value == null) return 'null'
|
||||
if (typeof value === 'number') return isFinite(value) ? value.toString() : 'null'
|
||||
if (typeof value === 'boolean') return value.toString()
|
||||
if (typeof value === 'object') {
|
||||
if (typeof value.toJSON === 'function') {
|
||||
return stringify(value.toJSON(), modFn)
|
||||
} else if (isArray(value)) {
|
||||
let res = '['
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
res += (i ? ',' : '') + stringify(value[i], modFn)
|
||||
}
|
||||
return res + ']'
|
||||
} else if (toString.call(value) === '[object Object]') {
|
||||
let tmp = []
|
||||
for (const k in value) {
|
||||
if (value.hasOwnProperty(k)) tmp.push(stringify(k, modFn) + ':' + stringify(value[k], modFn))
|
||||
}
|
||||
return '{' + tmp.join(',') + '}'
|
||||
}
|
||||
}
|
||||
return '"' + value.toString().replace(escRE, escFunc) + '"'
|
||||
}
|
||||
}
|
||||
|
||||
// Epic helpers
|
||||
export const put = (dispatch) => (action) => dispatch(action)
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<div
|
||||
ref="editor"
|
||||
:contenteditable="editAble"
|
||||
:class="{ editing: editAble }"
|
||||
@blur="inputBlur"
|
||||
@keyup.27="$event.target.blur"
|
||||
@dblclick="allowInput"
|
||||
v-text="inputText"
|
||||
></div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "editDiv",
|
||||
props: ["value"],
|
||||
data() {
|
||||
return {
|
||||
editAble: false,
|
||||
inputText: this.value
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// 失去焦点
|
||||
inputBlur() {
|
||||
this.$emit("input", this.$refs.editor.innerText);
|
||||
this.$emit("changeVal")
|
||||
this.editAble = false;
|
||||
},
|
||||
allowInput() {
|
||||
this.editAble = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.editor.focus();
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.input-box {
|
||||
width: 400px;
|
||||
height: 250px;
|
||||
border: 1px solid #6e6e6e;
|
||||
outline: none; /* 隐藏聚焦时外边框 */
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.editing {
|
||||
border: 1px solid #6e6e6e;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<ul :key="listKey">
|
||||
<li v-for="(item, index) in dataList" :key="index">
|
||||
<edit-div
|
||||
v-model="dataList[index]"
|
||||
@changeVal="checkList"
|
||||
:class="{ add: item == '双击添加' }"
|
||||
></edit-div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditDiv from "@/components/EditDiv/index.vue";
|
||||
export default {
|
||||
name: "EditList",
|
||||
props: ["data"],
|
||||
data() {
|
||||
return {
|
||||
dataList: this.data,
|
||||
listKey: 0
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.dataList[this.dataList.length - 1] != "双击添加") {
|
||||
this.dataList.push("双击添加");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkList() {
|
||||
// 删除""
|
||||
for (let i=0; i<this.dataList.length; i++) {
|
||||
if (!this.dataList[i].trim()) {
|
||||
this.dataList.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.dataList[this.dataList.length - 1] != "双击添加") {
|
||||
console.log("add");
|
||||
this.dataList.push("双击添加");
|
||||
}
|
||||
|
||||
this.reRenderList();
|
||||
},
|
||||
reRenderList() {
|
||||
this.listKey = (this.listKey + 1) % 100;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
EditDiv,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.add {
|
||||
color: #c0c4cc;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
ul {
|
||||
list-style-type:circle;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<div id="test" class="test" :class="{ editing: isChecked }">
|
||||
<div class="view">
|
||||
<label @dblclick="dbTest()">{{ item }}</label>
|
||||
</div>
|
||||
<input
|
||||
v-myfoucs="isChecked"
|
||||
class="edit"
|
||||
type="text"
|
||||
v-model="inputStr"
|
||||
@keyup.enter="$event.target.blur"
|
||||
@blur="inputStred"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "EditText",
|
||||
props: ["val", "itemName","bigDomain"],
|
||||
data() {
|
||||
return {
|
||||
item: this.val,
|
||||
isChecked: false,
|
||||
inputStr: "",
|
||||
domain: this.itemName,
|
||||
bigDomain:this.bigDomain
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
dbTest() {
|
||||
this.isChecked = true;
|
||||
this.inputStr = this.item;
|
||||
},
|
||||
inputStred() {
|
||||
this.item = this.inputStr;
|
||||
this.isChecked = false;
|
||||
this.$emit("changeDomainVal",this.bigDomain, this.domain, this.item)
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
myfoucs: {
|
||||
update(el, binding) {
|
||||
if (binding.value) {
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
label {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.test.editing .edit {
|
||||
display: block;
|
||||
// width: 150px;
|
||||
}
|
||||
|
||||
.test.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.test .edit {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* 文本双击可编辑 组件
|
||||
*/
|
||||
import EditText from "./EditText.vue";
|
||||
EditText.install = function (vue) {
|
||||
vue.component(EditText.name, EditText);
|
||||
};
|
||||
export default EditText;
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<div class="year-range-picker">
|
||||
<el-date-picker
|
||||
v-model="startYear"
|
||||
type="year"
|
||||
placeholder="选择开始年"
|
||||
size="mini"
|
||||
class="year-picker"
|
||||
@change="changeStartYear"
|
||||
format="yyyy 年"
|
||||
value-format="yyyy"
|
||||
>
|
||||
</el-date-picker>
|
||||
<span class="range-word"> 至 </span>
|
||||
<el-date-picker
|
||||
v-model="endYear"
|
||||
type="year"
|
||||
placeholder="选择结束年"
|
||||
size="mini"
|
||||
class="year-picker"
|
||||
@change="changeEndYear"
|
||||
format="yyyy 年"
|
||||
value-format="yyyy"
|
||||
>
|
||||
</el-date-picker>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "YearRangePicker",
|
||||
// 接收父组件传入的数据
|
||||
props: {
|
||||
yearRange: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
startYear: "",
|
||||
endYear: "",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// 初始化自身变量
|
||||
this.startYear = String(this.yearRange[0]);
|
||||
this.endYear = String(this.yearRange[1]);
|
||||
},
|
||||
methods: {
|
||||
changeStartYear(val) {
|
||||
// 开始年份大于结束年份,调换
|
||||
if (Number(this.startYear) > Number(this.endYear)) {
|
||||
this.startYear = this.endYear;
|
||||
this.endYear = val;
|
||||
}
|
||||
// 将改动传回父组件
|
||||
this.$emit("yearChanged", [Number(this.startYear), Number(this.endYear)]);
|
||||
},
|
||||
changeEndYear(val) {
|
||||
if (Number(this.startYear) > Number(this.endYear)) {
|
||||
this.endYear = this.startYear;
|
||||
this.startYear = val;
|
||||
}
|
||||
this.$emit("yearChanged", [Number(this.startYear), Number(this.endYear)])
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.year-range-picker {
|
||||
color: black;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.range-word {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.year-range-picker .year-picker {
|
||||
max-width: 160px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
setting: {
|
||||
neo4jUrl: 'bolt://222.20.95.239:7687',
|
||||
neo4jUserName: 'neo4j',
|
||||
neo4jPassword: 'sunrui00'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656899019644" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2273" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
|
||||
</style></defs><path d="M704 864c-6.4 0-12.8 0-19.2-6.4l-153.6-102.4L454.4 832c-6.4 6.4-25.6 12.8-32 6.4-12.8-6.4-19.2-19.2-19.2-32l0-140.8c0-6.4 0-12.8 6.4-19.2L652.8 384c12.8-12.8 32-12.8 44.8 0 12.8 12.8 12.8 32 0 44.8l-236.8 256 0 51.2 44.8-38.4c12.8-12.8 25.6-12.8 38.4-6.4l140.8 89.6 108.8-531.2L256 512l121.6 76.8C390.4 595.2 396.8 620.8 384 633.6c-6.4 12.8-32 19.2-44.8 12.8L172.8 537.6C166.4 531.2 160 524.8 160 512c0-12.8 6.4-19.2 19.2-25.6l640-320c12.8-6.4 25.6-6.4 32 0 12.8 6.4 12.8 19.2 12.8 32l-128 640c0 12.8-6.4 19.2-19.2 25.6C710.4 864 710.4 864 704 864z" p-id="2274"></path></svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1670222642954" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6022" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M320 64h64v160h-64z" p-id="6023"></path><path d="M192 192h640v64H192z" p-id="6024"></path><path d="M640 64h64v160h-64z" p-id="6025"></path><path d="M482.26 832H192.11l-0.11-0.11V192.11l0.11-0.11h639.78l0.11 0.11v290.15a257.16 257.16 0 0 1 64 52.44V192a64.19 64.19 0 0 0-64-64H192a64.19 64.19 0 0 0-64 64v640a64.19 64.19 0 0 0 64 64h342.69a257.16 257.16 0 0 1-52.43-64z" p-id="6026"></path><path d="M896 534.69v89.71a48 48 0 0 1 0 75.7V832a64.19 64.19 0 0 1-64 64H534.69A255 255 0 0 0 704 960c141.38 0 256-114.62 256-256a255 255 0 0 0-64-169.31z" p-id="6027"></path><path d="M808 775.25a48 48 0 1 1-89.22-24.6l-71.1-71.1a47.84 47.84 0 0 1-21.32 0l-43.21 43.21a48 48 0 1 1-24.34-20.91l40-40a48 48 0 1 1 76.38 0l68.33 68.33a48.2 48.2 0 0 1 32.93 0l46.88-46.88a48 48 0 0 1 8.67-54.4V482.26A254.81 254.81 0 0 0 704 448c-141.38 0-256 114.62-256 256a254.82 254.82 0 0 0 34.26 128h349.63l0.11-0.11v-112l-30.79 30.79a47.77 47.77 0 0 1 6.79 24.57zM832 482.26V628.9a48 48 0 0 1 64-4.5v-89.71a257.16 257.16 0 0 0-64-52.43z" p-id="6028"></path><path d="M896 832V700.1a48 48 0 0 1-49.86 5.62L832 719.86v112l-0.11 0.11H482.26a257.16 257.16 0 0 0 52.44 64H832A64.19 64.19 0 0 0 896 832zM256 352h512v64H256zM256 480h224v64H256z" p-id="6029"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1672282048164" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4415" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M540.7 133.4l103.5 209.8c4.6 9.4 13.6 15.9 24 17.4l231.5 33.6c26.1 3.8 36.6 35.9 17.7 54.4L749.9 612c-7.5 7.3-10.9 17.9-9.2 28.2l39.5 230.6c4.5 26-22.9 45.9-46.2 33.6L526.9 795.5c-9.3-4.9-20.4-4.9-29.7 0l-207 108.9c-23.4 12.3-50.7-7.6-46.2-33.6l39.5-230.6c1.8-10.3-1.7-20.9-9.2-28.2L106.8 448.7c-18.9-18.4-8.5-50.6 17.7-54.4L356 360.7c10.4-1.5 19.4-8 24-17.4l103.5-209.8c11.7-23.7 45.5-23.7 57.2-0.1z" fill="#409eff" p-id="4416"></path></svg>
|
After Width: | Height: | Size: 775 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1672129106572" class="icon" viewBox="0 0 1034 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13034" xmlns:xlink="http://www.w3.org/1999/xlink" width="201.953125" height="200"><path d="M743.17312 681.10336c59.00288-59.07968 91.8784-137.12896 93.056-220.2368 0.256-1.63328 0.43008-3.29728 0.4352-5.00224V55.46496H199.1168v398.2336c-0.04608 0.72192-0.10752 1.43872-0.10752 2.17088 0 164.30592 124.95872 299.94496 284.8256 316.94848v129.7664H252.44672c-18.18624 0-32.97792 14.7968-32.97792 32.9728s14.79168 32.96768 32.97792 32.96768h297.33888v-195.47648c73.02144-7.2448 140.75904-39.24992 193.38752-91.94496zM264.99584 457.35424c0.02048-0.4608 0.07168-0.91136 0.07168-1.37728V121.5232h505.53856v330.72128a33.9456 33.9456 0 0 0-0.20992 3.62496c0 139.35104-113.37216 252.71808-252.71808 252.71808-138.8544 0-251.8784-112.56832-252.68224-251.23328z" p-id="13035" fill="#409eff"></path><path d="M654.83776 334.83264a38.22592 38.22592 0 0 0-30.75072-26.09152l-50.55488-7.36256-22.62528-45.9008a38.13376 38.13376 0 0 0-68.1984-0.10752l-22.62528 45.79328-50.33984 7.36256a38.016 38.016 0 0 0-21.3248 64.95232l36.59264 35.61472-8.66304 50.45248A38.12352 38.12352 0 0 0 471.56224 499.712l45.25056-23.71072 45.35808 23.71072a36.79232 36.79232 0 0 0 17.64352 4.33152 37.97504 37.97504 0 0 0 37.3504-44.4928l-8.66304-50.33984 36.37248-35.4048a37.66784 37.66784 0 0 0 9.96352-38.97344z m-95.0528 58.46016l10.17856 59.1104-53.04832-27.8272L463.872 452.4032l10.17856-59.1104-42.86976-41.68192 59.2128-8.65792 26.52672-53.69344 26.5216 53.69344 59.2128 8.65792-42.86976 41.68192zM613.48352 341.00736l-0.10752 0.10752 0.10752-0.10752zM803.52256 509.30176v-65.9456h10.24c54.94272 0 99.64544-44.6976 99.64544-99.64032s-44.6976-99.64032-99.64544-99.64032h-10.24V178.1248h10.24c91.3664 0 165.69856 74.33216 165.69856 165.69344-0.11264 91.25376-74.44992 165.48352-165.69856 165.48352h-10.24zM221.91616 509.30176c-91.3664 0-165.69856-74.33216-165.69856-165.69856s74.33216-165.69856 165.69856-165.69856h10.24v65.95072h-10.24c-54.94272 0-99.64032 44.6976-99.64032 99.64032s44.6976 99.64032 99.64032 99.64032h10.24v66.16576h-10.24zM817.01888 934.83008a33.69984 33.69984 0 0 1-33.69984 33.69984h-190.90944a33.69984 33.69984 0 1 1 0-67.4048h190.90944a33.69984 33.69984 0 0 1 33.69984 33.70496z" p-id="13036" fill="#409eff"></path></svg>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656923670749" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2742" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
|
||||
</style></defs><path d="M426.666667 170.666667l85.333333 85.333333h341.333333a85.333333 85.333333 0 0 1 85.333334 85.333333v426.666667a85.333333 85.333333 0 0 1-85.333334 85.333333H170.666667a85.333333 85.333333 0 0 1-85.333334-85.333333V256a85.333333 85.333333 0 0 1 85.333334-85.333333h256z m426.666666 170.666666H170.666667v426.666667h682.666666V341.333333z m-392.533333 247.466667a102.826667 102.826667 0 0 1 102.4 102.4v34.133333H256v-34.133333a102.826667 102.826667 0 0 1 102.4-102.4h102.4z m273.066667-68.266667v68.266667H597.333333v-68.266667h136.533334zM409.6 384a85.333333 85.333333 0 1 1 0 170.666667 85.333333 85.333333 0 0 1 0-170.666667z m392.533333 34.133333v68.266667H597.333333V418.133333h204.8z" p-id="2743"></path></svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656923467424" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2293" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
|
||||
</style></defs><path d="M141.730909 369.989818a34.909091 34.909091 0 0 1 34.909091 34.909091v220.020364a34.909091 34.909091 0 0 1-69.818182 0V404.898909a34.909091 34.909091 0 0 1 34.909091-34.909091zM847.825455 46.545455H233.565091a126.929455 126.929455 0 0 0-126.743273 126.743272v66.978909a34.909091 34.909091 0 0 0 69.818182 0V173.288727c0-31.371636 25.553455-56.925091 56.925091-56.925091h614.260364c31.371636 0 56.925091 25.553455 56.92509 56.925091v677.422546c0 31.371636-25.553455 56.925091-56.92509 56.925091H233.565091a57.018182 57.018182 0 0 1-56.925091-56.925091v-72.750546a34.443636 34.443636 0 0 0-30.394182-33.978182h83.502546a34.909091 34.909091 0 0 0 0-69.818181H84.340364a34.909091 34.909091 0 0 0 0 69.818181h52.922181a34.536727 34.536727 0 0 0-30.394181 33.978182v72.750546A126.836364 126.836364 0 0 0 233.565091 977.454545h614.260364a126.929455 126.929455 0 0 0 126.743272-126.743272V173.288727A126.929455 126.929455 0 0 0 847.825455 46.545455zM382.324364 655.685818l87.831272-256.139636-41.611636-112.733091a34.909091 34.909091 0 0 1 32.721455-46.964364h177.943272a34.862545 34.862545 0 0 1 32.721455 46.964364l-41.658182 112.733091 87.831273 256.139636a34.909091 34.909091 0 0 1-13.824 40.494546l-139.636364 91.927272a35.002182 35.002182 0 0 1-39.330909-0.698181L395.170909 695.482182a34.909091 34.909091 0 0 1-12.846545-39.796364z m129.070545-346.065454l20.154182 54.551272h37.422545l20.200728-54.551272h-77.777455z m-54.551273 343.970909l89.320728 63.069091 97.140363-63.953455-74.984727-218.670545h-36.165818l-75.310546 219.554909zM84.340364 363.240727h145.454545a34.909091 34.909091 0 0 0 0-69.818182H84.340364a34.909091 34.909091 0 0 0 0 69.818182z" p-id="2294"></path></svg>
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1658111017393" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2746" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
|
||||
</style></defs><path d="M898.34 188.81L538.35 74.51l-359.87 114.3c-13.53 4.27-22.67 16.86-22.55 31.1v454.94c0.72 3.09 17.69 77.98 91.27 150.5 56.61 55.78 192.4 127.48 251.63 158.57 9.25 4.87 13.65 6.65 17.56 8.78l17.57 8.78c4.63 2.49 12.94 2.38 17.57 0l17.57-8.78c34.65-16.97 195-102.9 260.4-167.35 73.71-72.52 90.68-147.41 91.39-150.5V219.91c0.23-14.24-9.02-26.83-22.55-31.1zM542.86 930.98l-0.24-0.12h0.59c-0.23 0.12-0.35 0.12-0.35 0.12z m85.81-264.32h-58.16v81.9c0 18.99-15.43 34.42-34.42 34.42-18.99 0-34.42-15.43-34.42-34.42V537.29c0-0.71 0.12-1.3 0.12-1.9-6.29-1.9-12.58-4.16-18.87-7-21.24-9.73-39.4-27.18-51.51-47-12.58-20.65-19.23-46.53-17.21-70.74 2.37-27.65 12.34-51.75 30.03-73.23 30.62-37.15 86.88-52.22 131.98-35.73 25.52 9.38 46.29 25.05 61.96 47.12 13.77 19.35 20.77 43.44 21.37 67.06 0 0.71 0.12 1.3 0 2.02 0 0.83 0 1.66-0.12 2.49-1.66 52.46-37.74 101.95-89.02 115.61v60.41h58.16c18.99 0 34.42 15.43 34.42 34.42v1.42h0.12c-0.01 18.99-15.44 34.42-34.43 34.42z" p-id="2747"></path><path d="M577.99 449.1c0.35-0.59 0.71-1.07 0.83-1.19a97.38 97.38 0 0 0 5.58-9.74c1.54-4.39 2.73-8.78 3.68-13.41 0.12-2.25 0.24-4.63 0.36-6.89 0-2.26-0.12-4.63-0.36-6.88-0.83-4.51-2.14-9.02-3.68-13.41a108.73 108.73 0 0 0-4.51-8.19c-0.36-0.36-1.19-1.54-2.61-3.8-1.3-1.43-2.61-2.97-4.03-4.27-1.66-1.66-3.44-3.32-5.34-4.87-0.59-0.35-1.06-0.71-1.19-0.83a97.226 97.226 0 0 0-9.73-5.58c-4.39-1.54-8.78-2.73-13.29-3.68-4.63-0.35-9.26-0.35-13.89 0-4.51 0.83-9.02 2.14-13.29 3.68a108.73 108.73 0 0 0-8.19 4.51c-0.36 0.36-1.54 1.19-3.8 2.61-1.43 1.3-2.97 2.61-4.27 4.03-1.66 1.66-3.32 3.44-4.87 5.34-0.35 0.59-0.71 1.07-0.83 1.19a97.226 97.226 0 0 0-5.58 9.73c-1.54 4.39-2.73 8.78-3.68 13.29-0.35 4.63-0.35 9.26 0 13.89 0.83 4.51 2.14 9.02 3.68 13.29 1.42 2.85 2.85 5.46 4.51 8.19 0.36 0.36 1.19 1.54 2.61 3.8 1.3 1.43 2.61 2.97 4.03 4.27 1.66 1.66 3.44 3.32 5.34 4.87 0.59 0.35 1.07 0.71 1.19 0.83 3.08 2.02 6.41 3.92 9.73 5.58 4.39 1.54 8.78 2.73 13.29 3.68 4.63 0.36 9.26 0.36 13.89 0 4.51-0.83 9.02-2.14 13.29-3.68 2.85-1.42 5.46-2.85 8.19-4.51 0.36-0.36 1.54-1.19 3.8-2.61 1.54-1.19 2.97-2.49 4.27-3.92 1.67-1.64 3.33-3.42 4.87-5.32z" p-id="2748"></path></svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1672111313123" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7058" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M784.8 64.1H242.6c-30.7 0-55.7 24.9-55.7 55.7v180.5c0 18.6 9.3 36 24.8 46.3l177.1 117.9C306.6 509 250.6 596.1 250.6 696c0 145.1 118 263.1 263.1 263.1 145 0 263-118 263-263.1 0-99.9-56-186.9-138.2-231.5l177.1-117.9c15.5-10.3 24.8-27.7 24.8-46.3V119.8c0-30.8-24.9-55.7-55.6-55.7z m-63.8 632c0 114.6-92.8 207.4-207.4 207.4-114.6 0-207.4-92.8-207.4-207.4 0-114.5 92.8-207.4 207.4-207.4 114.6 0 207.4 92.9 207.4 207.4z m63.8-395.8L678.7 371V265c0-10.2-8.3-18.5-18.6-18.5-10.2 0-18.6 8.3-18.6 18.5v130.7l-66.9 44.6c-19.6-4.7-40-7.2-61-7.2s-41.4 2.6-61 7.2L384.5 395V265c0-10.2-8.3-18.5-18.6-18.5s-18.6 8.3-18.6 18.5v105.2l-104.9-69.9V119.8h104.9v49.7c0 10.2 8.3 18.6 18.6 18.6 10.2 0 18.6-8.3 18.6-18.6v-49.7h257v49.7c0 10.2 8.3 18.6 18.6 18.6s18.6-8.3 18.6-18.6v-49.7h106.1v180.5z" fill="#e6a23c" p-id="7059"></path><path d="M480.4 550.4L449.5 613l-69.1 10c-14 2-25.6 11.8-30 25.3-4.4 13.4-0.7 28.2 9.4 38.1l50 48.7-11.8 68.8c-2.4 13.9 3.3 28 14.8 36.3 6.5 4.7 14.1 7.1 21.8 7.1 5.9 0 11.8-1.4 17.3-4.3l61.8-32.5 61.8 32.5c5.4 2.8 11.4 4.3 17.3 4.3 7.7 0 15.3-2.4 21.8-7.1 11.4-8.3 17.2-22.4 14.8-36.3L617.5 735l50-48.8c10.1-9.9 13.8-24.6 9.4-38-4.4-13.4-16-23.2-30-25.3l-69.1-10-30.8-62.5c-6.3-12.7-19.2-20.7-33.3-20.7-14.1 0-27 8-33.3 20.7z m72.8 96.5l88.4 12.8-64 62.3 15.1 88-79-41.5-79 41.5 15.1-88-63.9-62.3 88.4-12.8 39.5-80.1 39.4 80.1z" fill="#e6a23c" p-id="7060"></path></svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1670223140218" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9793" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M820.736 807.488l96.288 96.288a32 32 0 1 1-45.248 45.248l-105.696-105.696a224 224 0 1 1 54.656-35.84z m-143.04-455.424a288.352 288.352 0 0 0-81.184 9.92c-25.28-19.84-53.888-33.44-84.512-39.04v77.568a289.6 289.6 0 0 0-64 58.464V322.88c-108.544 19.968-192 140-192 285.088 0 145.088 83.456 265.12 192 285.088v-72.064a289.6 289.6 0 0 0 64 58.464v13.6c5.344-0.96 10.656-2.24 15.872-3.68a286.144 286.144 0 0 0 83.168 32.128C571.744 946.112 527.2 960 480 960c-100.416 0-188.864-62.816-240.384-158.08A64 64 0 0 0 160 864a32 32 0 0 1-64 0 128 128 0 0 1 115.84-127.424A415.904 415.904 0 0 1 193.152 640H96a32 32 0 0 1 0-64h97.184c2.496-33.792 8.896-66.24 18.624-96.576A128 128 0 0 1 96 352a32 32 0 0 1 64 0 64 64 0 0 0 79.616 62.08c17.28-31.936 38.688-60.224 63.328-83.712a192 192 0 1 1 354.144 0c7.136 6.848 14.048 14.08 20.64 21.696z m116.32 26.976c3.84-8.192 5.984-17.376 5.984-27.04a32 32 0 0 1 64 0c0 21.92-5.504 42.56-15.232 60.608a288 288 0 0 0-54.72-33.568zM356.544 289.92A244.448 244.448 0 0 1 480 256c44.16 0 86.048 12.16 123.456 33.92A128.128 128.128 0 0 0 480 128a128 128 0 0 0-123.456 161.92zM672 800a160 160 0 1 0 0-320 160 160 0 0 0 0 320z" fill="#000000" p-id="9794"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1658195300643" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2408" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
|
||||
</style></defs><path d="M512 563.2c-127.9744 0-230.4-115.2512-230.4-256s102.4256-256 230.4-256 230.4 115.2512 230.4 256-102.4256 256-230.4 256z m0-51.2c98.2528 0 179.2-91.0592 179.2-204.8s-80.9472-204.8-179.2-204.8-179.2 91.0592-179.2 204.8 80.9472 204.8 179.2 204.8zM128 793.6a179.2 179.2 0 0 1 179.3792-179.2h409.2416C815.6416 614.4 896 694.7328 896 793.6a179.2 179.2 0 0 1-179.3792 179.2H307.3792C208.3584 972.8 128 892.4672 128 793.6z m51.2 0c0 70.5792 57.4464 128 128.1792 128h409.2416A128 128 0 0 0 844.8 793.6c0-70.5792-57.4464-128-128.1792-128H307.3792A128 128 0 0 0 179.2 793.6z" p-id="2409"></path></svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1670222779936" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7905" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M544.033302 516.000776V96.000564c0-17.672051-14.478655-31.998984-32.150706-31.998984s-32.150706 14.326933-32.150707 31.998984v420.061624c-54.909072 14.251072-95.809105 64.333925-95.809105 123.935554 0 59.691941 40.82056 109.836206 96.090876 124.000579a32.298816 32.298816 0 0 0-0.281771 4.006195v159.99492c0 17.672051 14.478655 31.998984 32.150707 31.998984s32.150706-14.326933 32.150706-31.998984V768.004516a32.280754 32.280754 0 0 0-0.317894-3.941171c55.389526-14.088512 96.397933-64.301413 96.397932-124.058378 0.010837-59.691941-40.809723-109.832593-96.080038-124.004191z m13.221527 169.249989a64.00158 64.00158 0 1 1 18.744945-45.256635 63.578925 63.578925 0 0 1-18.744945 45.260247zM223.678606 196.003711a32.287979 32.287979 0 0 0 0.292607-4.002582V96.000564a31.970084 31.970084 0 1 0-63.940169 0V192.001129a32.371065 32.371065 0 0 0 0.180622 4.060381c-55.270316 14.160761-96.173962 64.301413-96.173961 124.000579s40.910871 109.818143 96.181186 123.996966a32.360228 32.360228 0 0 0-0.184234 3.937558V927.999436a31.970084 31.970084 0 1 0 63.940169 0V448.000226a32.287979 32.287979 0 0 0-0.278158-3.883372c55.385914-14.088512 96.350971-64.301413 96.350971-124.054765S279.071744 210.084998 223.678606 196.003711z m13.575545 169.315013A64.00158 64.00158 0 1 1 255.999097 320.062089a63.578925 63.578925 0 0 1-18.744946 45.256635zM959.99842 320.062089c0-59.775027-41.048144-109.977091-96.45212-124.058378a32.360228 32.360228 0 0 0 0.173397-4.002582V96.000564a31.970084 31.970084 0 1 0-63.940169 0V192.001129a32.291592 32.291592 0 0 0 0.299833 4.060381c-55.270316 14.160761-96.116162 64.301413-96.116163 124.000579s40.82056 109.836206 96.090876 124.000578a32.298816 32.298816 0 0 0-0.281771 3.99897V927.999436a31.970084 31.970084 0 1 0 63.940169 0V448.061637a32.381903 32.381903 0 0 0-0.166172-3.941171c55.407588-14.084899 96.452119-64.286963 96.45212-124.058377z m-82.746526 45.253022a63.997968 63.997968 0 1 1 18.748558-45.253022 63.578925 63.578925 0 0 1-18.748558 45.253022z" fill="" p-id="7906"></path></svg>
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1672111280527" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6806" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M784.8 64.1H242.6c-30.7 0-55.7 24.9-55.7 55.7v180.5c0 18.6 9.3 36 24.8 46.3l177.1 117.9C306.6 509 250.6 596.1 250.6 696c0 145.1 118 263.1 263.1 263.1 145 0 263-118 263-263.1 0-99.9-56-186.9-138.2-231.5l177.1-117.9c15.5-10.3 24.8-27.7 24.8-46.3V119.8c0-30.8-24.9-55.7-55.6-55.7z m-63.8 632c0 114.6-92.8 207.4-207.4 207.4-114.6 0-207.4-92.8-207.4-207.4 0-114.5 92.8-207.4 207.4-207.4 114.6 0 207.4 92.9 207.4 207.4z m63.8-395.8L678.7 371V265c0-10.2-8.3-18.5-18.6-18.5-10.2 0-18.6 8.3-18.6 18.5v130.7l-66.9 44.6c-19.6-4.7-40-7.2-61-7.2s-41.4 2.6-61 7.2L384.5 395V265c0-10.2-8.3-18.5-18.6-18.5s-18.6 8.3-18.6 18.5v105.2l-104.9-69.9V119.8h104.9v49.7c0 10.2 8.3 18.6 18.6 18.6 10.2 0 18.6-8.3 18.6-18.6v-49.7h257v49.7c0 10.2 8.3 18.6 18.6 18.6s18.6-8.3 18.6-18.6v-49.7h106.1v180.5z" fill="#f56c6c" p-id="6807"></path><path d="M480.4 550.4L449.5 613l-69.1 10c-14 2-25.6 11.8-30 25.3-4.4 13.4-0.7 28.2 9.4 38.1l50 48.7-11.8 68.8c-2.4 13.9 3.3 28 14.8 36.3 6.5 4.7 14.1 7.1 21.8 7.1 5.9 0 11.8-1.4 17.3-4.3l61.8-32.5 61.8 32.5c5.4 2.8 11.4 4.3 17.3 4.3 7.7 0 15.3-2.4 21.8-7.1 11.4-8.3 17.2-22.4 14.8-36.3L617.5 735l50-48.8c10.1-9.9 13.8-24.6 9.4-38-4.4-13.4-16-23.2-30-25.3l-69.1-10-30.8-62.5c-6.3-12.7-19.2-20.7-33.3-20.7-14.1 0-27 8-33.3 20.7z m72.8 96.5l88.4 12.8-64 62.3 15.1 88-79-41.5-79 41.5 15.1-88-63.9-62.3 88.4-12.8 39.5-80.1 39.4 80.1z" fill="#f56c6c" p-id="6808"></path></svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1670222723946" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6974" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1024.25175 0l-209.92 23.04L883.45175 92.16 655.61175 370.688 419.06775 152.064c-15.872-14.848-40.96-14.848-57.344-0.512L14.07575 465.408C-3.33225 481.28-4.86825 508.416 11.00375 525.824c8.192 9.216 19.968 13.824 31.744 13.824 10.24 0 20.48-3.584 28.672-10.752l318.464-287.744 241.152 222.72c8.704 8.192 20.48 11.776 31.744 11.264 11.776-1.024 22.528-6.656 30.208-15.36l250.88-306.688 57.344 57.344L1024.25175 0z m0 0M133.37175 1024H30.97175c-16.896 0-30.72-13.824-30.72-30.72v-348.16c0-16.896 13.824-30.72 30.72-30.72h102.4c16.896 0 30.72 13.824 30.72 30.72v348.16c0 16.896-13.824 30.72-30.72 30.72z" p-id="6975"></path><path d="M420.09175 1024H317.69175c-16.896 0-30.72-13.824-30.72-30.72V440.32c0-16.896 13.824-30.72 30.72-30.72h102.4c16.896 0 30.72 13.824 30.72 30.72v552.96c0 16.896-13.824 30.72-30.72 30.72zM706.81175 1024h-102.4c-16.896 0-30.72-13.824-30.72-30.72v-399.36c0-16.896 13.824-30.72 30.72-30.72h102.4c16.896 0 30.72 13.824 30.72 30.72v399.36c0 16.896-13.824 30.72-30.72 30.72zM993.53175 1024h-102.4c-16.896 0-30.72-13.824-30.72-30.72V337.92c0-16.896 13.824-30.72 30.72-30.72h102.4c16.896 0 30.72 13.824 30.72 30.72v655.36c0 16.896-13.824 30.72-30.72 30.72z" p-id="6976"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1672111395736" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9490" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M784.8 64.1H242.6c-30.7 0-55.7 24.9-55.7 55.7v180.5c0 18.6 9.3 36 24.8 46.3l177.1 117.9C306.6 509 250.6 596.1 250.6 696c0 145.1 118 263.1 263.1 263.1 145 0 263-118 263-263.1 0-99.9-56-186.9-138.2-231.5l177.1-117.9c15.5-10.3 24.8-27.7 24.8-46.3V119.8c0-30.8-24.9-55.7-55.6-55.7z m-63.8 632c0 114.6-92.8 207.4-207.4 207.4-114.6 0-207.4-92.8-207.4-207.4 0-114.5 92.8-207.4 207.4-207.4 114.6 0 207.4 92.9 207.4 207.4z m63.8-395.8L678.7 371V265c0-10.2-8.3-18.5-18.6-18.5-10.2 0-18.6 8.3-18.6 18.5v130.7l-66.9 44.6c-19.6-4.7-40-7.2-61-7.2s-41.4 2.6-61 7.2L384.5 395V265c0-10.2-8.3-18.5-18.6-18.5s-18.6 8.3-18.6 18.5v105.2l-104.9-69.9V119.8h104.9v49.7c0 10.2 8.3 18.6 18.6 18.6 10.2 0 18.6-8.3 18.6-18.6v-49.7h257v49.7c0 10.2 8.3 18.6 18.6 18.6s18.6-8.3 18.6-18.6v-49.7h106.1v180.5z" fill="#67c23a" p-id="9491"></path><path d="M480.4 550.4L449.5 613l-69.1 10c-14 2-25.6 11.8-30 25.3-4.4 13.4-0.7 28.2 9.4 38.1l50 48.7-11.8 68.8c-2.4 13.9 3.3 28 14.8 36.3 6.5 4.7 14.1 7.1 21.8 7.1 5.9 0 11.8-1.4 17.3-4.3l61.8-32.5 61.8 32.5c5.4 2.8 11.4 4.3 17.3 4.3 7.7 0 15.3-2.4 21.8-7.1 11.4-8.3 17.2-22.4 14.8-36.3L617.5 735l50-48.8c10.1-9.9 13.8-24.6 9.4-38-4.4-13.4-16-23.2-30-25.3l-69.1-10-30.8-62.5c-6.3-12.7-19.2-20.7-33.3-20.7-14.1 0-27 8-33.3 20.7z m72.8 96.5l88.4 12.8-64 62.3 15.1 88-79-41.5-79 41.5 15.1-88-63.9-62.3 88.4-12.8 39.5-80.1 39.4 80.1z" fill="#67c23a" p-id="9492"></path></svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1672111436452" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9742" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M784.8 64.1H242.6c-30.7 0-55.7 24.9-55.7 55.7v180.5c0 18.6 9.3 36 24.8 46.3l177.1 117.9C306.6 509 250.6 596.1 250.6 696c0 145.1 118 263.1 263.1 263.1 145 0 263-118 263-263.1 0-99.9-56-186.9-138.2-231.5l177.1-117.9c15.5-10.3 24.8-27.7 24.8-46.3V119.8c0-30.8-24.9-55.7-55.6-55.7z m-63.8 632c0 114.6-92.8 207.4-207.4 207.4-114.6 0-207.4-92.8-207.4-207.4 0-114.5 92.8-207.4 207.4-207.4 114.6 0 207.4 92.9 207.4 207.4z m63.8-395.8L678.7 371V265c0-10.2-8.3-18.5-18.6-18.5-10.2 0-18.6 8.3-18.6 18.5v130.7l-66.9 44.6c-19.6-4.7-40-7.2-61-7.2s-41.4 2.6-61 7.2L384.5 395V265c0-10.2-8.3-18.5-18.6-18.5s-18.6 8.3-18.6 18.5v105.2l-104.9-69.9V119.8h104.9v49.7c0 10.2 8.3 18.6 18.6 18.6 10.2 0 18.6-8.3 18.6-18.6v-49.7h257v49.7c0 10.2 8.3 18.6 18.6 18.6s18.6-8.3 18.6-18.6v-49.7h106.1v180.5z" fill="#409eff" p-id="9743"></path><path d="M480.4 550.4L449.5 613l-69.1 10c-14 2-25.6 11.8-30 25.3-4.4 13.4-0.7 28.2 9.4 38.1l50 48.7-11.8 68.8c-2.4 13.9 3.3 28 14.8 36.3 6.5 4.7 14.1 7.1 21.8 7.1 5.9 0 11.8-1.4 17.3-4.3l61.8-32.5 61.8 32.5c5.4 2.8 11.4 4.3 17.3 4.3 7.7 0 15.3-2.4 21.8-7.1 11.4-8.3 17.2-22.4 14.8-36.3L617.5 735l50-48.8c10.1-9.9 13.8-24.6 9.4-38-4.4-13.4-16-23.2-30-25.3l-69.1-10-30.8-62.5c-6.3-12.7-19.2-20.7-33.3-20.7-14.1 0-27 8-33.3 20.7z m72.8 96.5l88.4 12.8-64 62.3 15.1 88-79-41.5-79 41.5 15.1-88-63.9-62.3 88.4-12.8 39.5-80.1 39.4 80.1z" fill="#409eff" p-id="9744"></path></svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1670222805024" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8841" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M892.224 117.024c-83.936-83.936-231.648-81.12-387.616-3.84-155.936-77.28-303.68-80.096-387.616 3.84-69.76 69.76-79.488 183.552-36.64 309.344a95.776 95.776 0 0 0 12.992 122.528c-55.936 136.896-53.472 266.176 23.68 343.328 83.936 83.936 231.712 81.152 387.776 3.776 152.8 75.68 302.016 81.632 387.424-3.776 85.44-85.44 79.424-234.72 3.68-387.584 75.744-152.896 81.76-302.176-3.68-387.616z m-45.248 729.952c-96.416 96.416-332.64 29.408-523.36-161.344a31.968 31.968 0 1 0-45.248 45.248 913.056 913.056 0 0 0 157.376 126.304c-117.248 47.904-219.104 44.128-273.472-10.24-55.776-55.776-56.544-158.368-10.656-271.808 2.816 0.256 5.504 0.864 8.384 0.864a96 96 0 0 0 96-96c0-21.024-6.944-40.288-18.4-56.096a870.592 870.592 0 0 1 86.016-100.288 31.968 31.968 0 1 0-45.248-45.248 933.6 933.6 0 0 0-93.472 109.344A92.928 92.928 0 0 0 160 384a95.68 95.68 0 0 0-24.736 3.616c-28.704-96.448-19.776-178.56 27.008-225.344 62.368-62.368 187.264-58.176 326.112 14.24a31.008 31.008 0 0 0 16.544 3.04 31.104 31.104 0 0 0 16.032-3.104c138.816-72.352 263.648-76.544 326.016-14.176 56 56 56.544 159.2 10.048 273.216a879.36 879.36 0 0 0-67.584-92.48c6.528-11.584 10.56-24.768 10.56-39.008a80 80 0 1 0-80 80c6.368 0 12.48-0.928 18.4-2.336a795.968 795.968 0 0 1 85.408 122.944c-31.52 56.672-73.984 114.08-126.176 168.352-3.2-0.384-6.336-0.96-9.632-0.96a80 80 0 1 0 65.792 34.624 901.056 901.056 0 0 0 103.232-132.864c46.464 113.984 45.952 217.216-10.048 273.216z" p-id="8842"></path><path d="M512 384a128 128 0 1 0 0 256 128 128 0 0 0 0-256z m0 192a64 64 0 1 1 0.032-128.032A64 64 0 0 1 512 576z" p-id="8843"></path></svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1670222569896" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3504" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M969.6 502.4l-118.4-112-323.2-300.8c-9.6-9.6-22.4-9.6-32 0l-313.6 297.6c-3.2 3.2-6.4 6.4-9.6 9.6l-118.4 112c-9.6 9.6-9.6 22.4 0 32s22.4 9.6 32 0l83.2-80 0 393.6c0 48 41.6 89.6 92.8 89.6l83.2 0c38.4 0 70.4-28.8 70.4-67.2l0-217.6 99.2 0 99.2 0 0 217.6c0 35.2 32 67.2 70.4 67.2l83.2 0c51.2 0 92.8-38.4 92.8-89.6l0-396.8 80 73.6c9.6 9.6 22.4 9.6 32 0C979.2 524.8 979.2 512 969.6 502.4zM809.6 857.6c0 25.6-19.2 44.8-44.8 44.8l-83.2 0c-12.8 0-22.4-9.6-22.4-22.4L659.2 640c0-12.8-9.6-22.4-22.4-22.4l-121.6 0-121.6 0c-12.8 0-22.4 9.6-22.4 22.4l0 240c0 12.8-9.6 22.4-22.4 22.4l-83.2 0c-25.6 0-44.8-19.2-44.8-44.8l0-438.4 294.4-281.6 294.4 281.6L809.6 857.6z" p-id="3505"></path></svg>
|
After Width: | Height: | Size: 1008 B |
|
@ -7,23 +7,17 @@
|
|||
<div class="right-menu">
|
||||
<el-dropdown class="avatar-container" trigger="click">
|
||||
<div class="avatar-wrapper">
|
||||
<img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
|
||||
<img src="@/assets/images/default.png" class="user-avatar">
|
||||
<i class="el-icon-caret-bottom" />
|
||||
</div>
|
||||
<el-dropdown-menu slot="dropdown" class="user-dropdown">
|
||||
<router-link to="/">
|
||||
<el-dropdown-item>
|
||||
Home
|
||||
首页
|
||||
</el-dropdown-item>
|
||||
</router-link>
|
||||
<a target="_blank" href="https://github.com/PanJiaChen/vue-admin-template/">
|
||||
<el-dropdown-item>Github</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
|
||||
<el-dropdown-item>Docs</el-dropdown-item>
|
||||
</a>
|
||||
<el-dropdown-item divided @click.native="logout">
|
||||
<span style="display:block;">Log Out</span>
|
||||
<span style="display:block;">退出登录</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
@ -43,8 +37,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar',
|
||||
'avatar'
|
||||
'sidebar'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
|
|