├── .babelrc ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── build ├── build.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── index.html ├── neo4j-vue.iml ├── package.json ├── screenshots ├── 项目截图1.png └── 项目截图2.png ├── src ├── App.vue ├── app.js ├── components │ └── D3Visualization │ │ ├── GraphEventHandler.js │ │ ├── Visualization.vue │ │ ├── components │ │ ├── Explorer.vue │ │ ├── Graph.vue │ │ ├── GrassEditor.vue │ │ ├── Inspector.vue │ │ ├── Legend.vue │ │ ├── RowExpandToggle.vue │ │ └── styled.js │ │ ├── constants.js │ │ ├── graphStyle.js │ │ ├── index.js │ │ ├── lib │ │ └── visualization │ │ │ ├── components │ │ │ ├── collision.coffee │ │ │ ├── graph.coffee │ │ │ ├── graphGeometry.coffee │ │ │ ├── graphView.coffee │ │ │ ├── layout.coffee │ │ │ ├── node.coffee │ │ │ ├── queryPlan.coffee │ │ │ ├── relationship.coffee │ │ │ ├── renderer.coffee │ │ │ ├── style.coffee │ │ │ └── visualization.coffee │ │ │ ├── index.js │ │ │ ├── neod3.coffee │ │ │ ├── renders │ │ │ ├── init.coffee │ │ │ └── menu.coffee │ │ │ └── utils │ │ │ ├── adjacentAngles.coffee │ │ │ ├── angleList.coffee │ │ │ ├── arcArrow.coffee │ │ │ ├── arrays.coffee │ │ │ ├── circularLayout.coffee │ │ │ ├── circumferentialDistribution.coffee │ │ │ ├── circumferentialRelationshipRouting.coffee │ │ │ ├── clickHandler.coffee │ │ │ ├── loopArrow.coffee │ │ │ ├── pairwiseArcsRelationshipRouting.coffee │ │ │ ├── straightArrow.coffee │ │ │ └── textMeasurement.coffee │ │ ├── mapper.js │ │ └── services │ │ ├── bolt │ │ ├── bolt.js │ │ ├── boltHelpers.js │ │ ├── boltMappings.js │ │ ├── boltMappings.test.js │ │ └── updateStatisticsFields.js │ │ ├── duckUtils.js │ │ ├── exceptionMessages.js │ │ ├── exceptions.js │ │ └── utils.js ├── index.js ├── router │ └── index.js └── views │ └── Main.vue └── static ├── css └── common.css └── js └── external └── d3.min.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": [ 7 | "transform-export-extensions", 8 | "transform-vue-jsx" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /.settings 3 | /.classpath 4 | /.project 5 | /node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 郑辉强 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neo4j Vue 2 | 3 | ## 安装步骤 4 | 1. 克隆或下载该库到本地 5 | 2. 下载neo4j并安装到本地, 启动neo4j服务 6 | 3. 进入到该工程的根目录, 输入命令: npm run update 进行npm包的导入 7 | 4. 使用命令: npm run dev 启动该工程 8 | 5. 默认端口是:8081 9 | - neo4j账号信息: 10 | - url: bolt://localhost 11 | - userName: neo4j 12 | - passsword: password 13 | 14 | ### 启动服务 15 | `npm run dev` and point your web browser to `http://localhost:8081`. 16 | 17 | ### 项目截图 18 | ![image](https://github.com/zhq734/neo4j-vue/blob/master/screenshots/项目截图1.png) 19 | ![image](https://github.com/zhq734/neo4j-vue/blob/master/screenshots/项目截图2.png) 20 | 21 | ### 目录结构 22 | ├── index.html 入口页面
23 | ├── build 构建脚本目录
24 | │ ├── build-server.js 运行本地构建服务器,可以访问构建后的页面
25 | │ ├── build.js 生产环境构建脚本
26 | │ ├── dev-client.js 开发服务器热重载脚本,主要用来实现开发阶段的页面自动刷新
27 | │ ├── dev-server.js 运行本地开发服务器
28 | │ ├── utils.js 构建相关工具方法
29 | │ ├── webpack.base.conf.js wabpack基础配置
30 | │ ├── webpack.dev.conf.js wabpack开发环境配置
31 | │ └── webpack.prod.conf.js wabpack生产环境配置
32 | ├── config 项目配置
33 | │ ├── dev.env.js 开发环境变量
34 | │ ├── index.js 项目配置文件
35 | │ ├── prod.env.js 生产环境变量
36 | │ └── test.env.js 测试环境变量
37 | ├── package.json npm包配置文件,里面定义了项目的npm脚本,依赖包等信息
38 | ├── src 项目源码目录
39 | │ ├── index.js 入口js文件
40 | │ ├── app.vue 根组件
41 | │ ├── app.js
42 | │ ├── components 公共组件目录
43 | │ │ └── title.vue
44 | │ ├── routes 前端路由
45 | │ │ └── index.js
46 | │ └── views 页面目录
47 | │ └── Main.vue
48 | └── static 纯静态资源,不会被wabpack构建。
49 | 50 | 51 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | // https://github.com/shelljs/shelljs 2 | 'use strict' 3 | 4 | require('shelljs/global') 5 | env.NODE_ENV = 'production' 6 | 7 | const path = require('path') 8 | const config = require('../config') 9 | const ora = require('ora') 10 | const webpack = require('webpack') 11 | const webpackConfig = require('./webpack.prod.conf') 12 | 13 | const spinner = ora('building for production...') 14 | spinner.start() 15 | 16 | const assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) 17 | rm('-rf', assetsPath) 18 | mkdir('-p', assetsPath) 19 | cp('-R', 'static/*', assetsPath) 20 | 21 | const compiler = webpack(webpackConfig) 22 | const ProgressPlugin = require('webpack/lib/ProgressPlugin') 23 | compiler.apply(new ProgressPlugin()) 24 | 25 | compiler.run((err, stats) => { 26 | spinner.stop() 27 | if (err) throw err 28 | process.stdout.write(stats.toString({ 29 | colors: true, 30 | modules: false, 31 | children: false, 32 | chunks: false, 33 | chunkModules: false 34 | }) + '\n') 35 | }) 36 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // require('eventsource-polyfill') 3 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe((event) => { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var express = require('express') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var proxyMiddleware = require('http-proxy-middleware') 6 | var webpackConfig = process.env.NODE_ENV === 'testing' 7 | ? require('./webpack.prod.conf') 8 | : require('./webpack.dev.conf') 9 | 10 | // default port where dev server listens for incoming traffic 11 | var port = process.env.PORT || config.dev.port 12 | // Define HTTP proxies to your custom API backend 13 | // https://github.com/chimurai/http-proxy-middleware 14 | var proxyTable = config.dev.proxyTable 15 | 16 | var app = express() 17 | var compiler = webpack(webpackConfig) 18 | 19 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 20 | publicPath: webpackConfig.output.publicPath, 21 | stats: { 22 | colors: true, 23 | chunks: false 24 | } 25 | }) 26 | 27 | var hotMiddleware = require('webpack-hot-middleware')(compiler) 28 | // force page reload when html-webpack-plugin template changes 29 | compiler.plugin('compilation', function (compilation) { 30 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 31 | hotMiddleware.publish({ action: 'reload' }) 32 | cb() 33 | }) 34 | }) 35 | 36 | // proxy api requests 37 | Object.keys(proxyTable).forEach(function (context) { 38 | var options = proxyTable[context] 39 | if (typeof options === 'string') { 40 | options = { target: options } 41 | } 42 | app.use(proxyMiddleware(context, options)) 43 | }) 44 | 45 | // handle fallback for HTML5 history API 46 | app.use(require('connect-history-api-fallback')()) 47 | 48 | // serve webpack bundle output 49 | app.use(devMiddleware) 50 | 51 | // enable hot-reload and state-preserving 52 | // compilation error display 53 | app.use(hotMiddleware) 54 | 55 | // serve pure static assets 56 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 57 | app.use(staticPath, express.static('./static')) 58 | 59 | module.exports = app.listen(port, function (err) { 60 | if (err) { 61 | console.log(err) 62 | return 63 | } 64 | var uri = 'http://localhost:' + port 65 | console.log('Listening at ' + uri + '\n') 66 | }) 67 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | // generate loader string to be used with extract text plugin 15 | function generateLoaders (loaders) { 16 | var sourceLoader = loaders.map(function (loader) { 17 | var extraParamChar 18 | if (/\?/.test(loader)) { 19 | loader = loader.replace(/\?/, '-loader?') 20 | extraParamChar = '&' 21 | } else { 22 | loader = loader + '-loader' 23 | extraParamChar = '?' 24 | } 25 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 26 | }).join('!') 27 | 28 | if (options.extract) { 29 | return ExtractTextPlugin.extract({ 30 | fallbackLoader: 'style-loader', 31 | loader: sourceLoader 32 | }) 33 | } else { 34 | return ['vue-style-loader', sourceLoader].join('!') 35 | } 36 | } 37 | 38 | // http://vuejs.github.io/vue-loader/configurations/extract-css.html 39 | return { 40 | css: generateLoaders(['css']), 41 | postcss: generateLoaders(['css']), 42 | less: generateLoaders(['css', 'less']), 43 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 44 | scss: generateLoaders(['css', 'sass']), 45 | stylus: generateLoaders(['css', 'stylus']), 46 | styl: generateLoaders(['css', 'stylus']) 47 | } 48 | } 49 | 50 | // Generate loaders for standalone style files (outside of .vue) 51 | exports.styleLoaders = function (options) { 52 | var output = [] 53 | var loaders = exports.cssLoaders(options) 54 | for (var extension in loaders) { 55 | var loader = loaders[extension] 56 | output.push({ 57 | test: new RegExp('\\.' + extension + '$'), 58 | loader: loader 59 | }) 60 | } 61 | return output 62 | } 63 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const utils = require('./utils') 7 | const projectRoot = path.resolve(__dirname, '../') 8 | 9 | const isProduction = process.env.NODE_ENV === 'production' 10 | 11 | module.exports = { 12 | entry: { 13 | app: ['babel-polyfill', './src/index.js'], 14 | vendor: ['vue', 'vue-router', 'vuex', 'jquery'] 15 | }, 16 | output: { 17 | path: config.build.assetsRoot, 18 | publicPath: isProduction ? config.build.assetsPublicPath : config.dev.assetsPublicPath, 19 | filename: '[name].js' 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.vue', '.css', '.json', '.coffee'], 23 | alias: { 24 | // https://github.com/vuejs/vue/wiki/Vue-2.0-RC-Starter-Resources 25 | vue: 'vue/dist/vue', 26 | package: path.resolve(__dirname, '../package.json'), 27 | config: path.resolve(__dirname, '../config'), 28 | src: path.resolve(__dirname, '../src'), 29 | assets: path.resolve(__dirname, '../src/static'), 30 | components: path.resolve(__dirname, '../src/components'), 31 | views: path.resolve(__dirname, '../src/views'), 32 | // third-party 33 | // vue-addon 34 | // 'vuex-store': path.resolve(__dirname, '../src/store'), 35 | 'neo4j-driver-alias': 'neo4j-driver/lib/browser/neo4j-web.min.js', 36 | } 37 | }, 38 | module: { 39 | loaders: [ 40 | { 41 | test: /\.coffee$/, 42 | exclude: /node_modules/, 43 | loader: 'coffee-loader' 44 | }, 45 | { 46 | test: /\.vue$/, 47 | loader: 'vue-loader' 48 | }, 49 | { 50 | test: /\.json$/, 51 | loader: 'json-loader' 52 | }, 53 | { 54 | test: /\.js$/, 55 | loader: 'babel-loader', 56 | include: projectRoot, 57 | exclude: /node_modules/ 58 | }, 59 | { 60 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 61 | loader: 'url-loader', 62 | query: { 63 | limit: 10000, 64 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 65 | } 66 | }, 67 | { 68 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 69 | loader: 'url-loader', 70 | query: { 71 | limit: 10000, 72 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 73 | } 74 | } 75 | ] 76 | }, 77 | plugins: [ 78 | new webpack.LoaderOptionsPlugin({ 79 | vue: { 80 | loaders: utils.cssLoaders({ 81 | sourceMap: isProduction, 82 | extract: isProduction 83 | }), 84 | postcss: [ 85 | require('autoprefixer')({ 86 | browsers: ['last 3 versions'] 87 | }) 88 | ] 89 | } 90 | }) 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const baseWebpackConfig = require('./webpack.base.conf') 5 | const config = require('../config') 6 | const utils = require('./utils') 7 | 8 | // add hot-reload related code to entry chunks 9 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 10 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 11 | }) 12 | 13 | module.exports = merge(baseWebpackConfig, { 14 | module: { 15 | loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 16 | }, 17 | // eval-source-map is faster for development 18 | devtool: '#eval-source-map', 19 | performance: { 20 | hints: false 21 | }, 22 | plugins: [ 23 | new webpack.DefinePlugin({ 24 | 'process.env': config.dev.env 25 | }), 26 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 27 | new webpack.optimize.OccurrenceOrderPlugin(), 28 | new webpack.HotModuleReplacementPlugin(), 29 | new webpack.NoEmitOnErrorsPlugin(), 30 | // extract vendor chunks for better caching 31 | new webpack.optimize.CommonsChunkPlugin({ 32 | name: 'vendor', 33 | filename: 'vendor.js' 34 | }), 35 | // https://github.com/ampedandwired/html-webpack-plugin 36 | new HtmlWebpackPlugin({ 37 | title: '测试环境', 38 | filename: 'index.html', 39 | template: 'index.html', 40 | inject: true 41 | }) 42 | ] 43 | }) 44 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | const merge = require('webpack-merge') 6 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 7 | const HtmlWebpackPlugin = require('html-webpack-plugin') 8 | const baseWebpackConfig = require('./webpack.base.conf') 9 | const config = require('../config') 10 | const utils = require('./utils') 11 | const env = process.env.NODE_ENV === 'testing' 12 | ? require('../config/test.env') 13 | : config.build.env 14 | const isELECTRON = process.env.NODE_ELECTRON === 'true' 15 | 16 | const webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | loaders: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true 21 | }) 22 | }, 23 | devtool: config.build.productionSourceMap ? '#source-map' : false, 24 | output: { 25 | path: config.build.assetsRoot, 26 | publicPath: isELECTRON ? path.join(__dirname, '../dist/') : '/', 27 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new webpack.LoaderOptionsPlugin({ 36 | minimize: true 37 | }), 38 | new webpack.optimize.UglifyJsPlugin({ 39 | sourceMap: true, 40 | compress: { 41 | warnings: false 42 | }, 43 | output: { 44 | comments: false 45 | } 46 | }), 47 | new webpack.optimize.OccurrenceOrderPlugin(), 48 | // extract css into its own file 49 | new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')), 50 | // generate dist index.html with correct asset hash for caching. 51 | // you can customize output by editing /index.html 52 | // see https://github.com/ampedandwired/html-webpack-plugin 53 | new HtmlWebpackPlugin({ 54 | title: '协同平台', 55 | filename: process.env.NODE_ENV === 'testing' 56 | ? 'index.html' 57 | : config.build.index, 58 | template: 'index.html', 59 | inject: true, 60 | // favicon: 'static/img/favicon.png', 61 | minify: { 62 | removeComments: true, 63 | collapseWhitespace: true, 64 | removeAttributeQuotes: true 65 | // more options: 66 | // https://github.com/kangax/html-minifier#options-quick-reference 67 | }, 68 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 69 | chunksSortMode: 'dependency' 70 | }), 71 | // split vendor js into its own file 72 | new webpack.optimize.CommonsChunkPlugin({ 73 | name: 'vendor', 74 | minChunks (module, count) { 75 | // any required modules inside node_modules are extracted to vendor 76 | return ( 77 | module.resource && 78 | /\.js$/.test(module.resource) && 79 | module.resource.indexOf( 80 | path.join(__dirname, '../node_modules') 81 | ) === 0 82 | ) 83 | } 84 | }), 85 | // extract webpack runtime and module manifest to its own file in order to 86 | // prevent vendor hash from being updated whenever app bundle is updated 87 | new webpack.optimize.CommonsChunkPlugin({ 88 | name: 'manifest', 89 | chunks: ['vendor'] 90 | }) 91 | ] 92 | }) 93 | 94 | if (config.build.productionGzip) { 95 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 96 | 97 | webpackConfig.plugins.push( 98 | new CompressionWebpackPlugin({ 99 | asset: '[path].gz[query]', 100 | algorithm: 'gzip', 101 | test: new RegExp( 102 | '\\.(' + 103 | config.build.productionGzipExtensions.join('|') + 104 | ')$' 105 | ), 106 | threshold: 10240, 107 | minRatio: 0.8 108 | }) 109 | ) 110 | } 111 | 112 | module.exports = webpackConfig 113 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: false, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'] 18 | }, 19 | dev: { 20 | env: require('./dev.env'), 21 | port: 8081, 22 | assetsSubDirectory: 'static', 23 | assetsPublicPath: '/', 24 | proxyTable: {}, 25 | // CSS Sourcemaps off by default because relative paths are "buggy" 26 | // with this option, according to the CSS-Loader README 27 | // (https://github.com/webpack/css-loader#sourcemaps) 28 | // In our experience, they generally work as expected, 29 | // just be aware of this issue when enabling this option. 30 | cssSourceMap: false 31 | }, 32 | setting: { 33 | neo4jUrl: 'bolt://localhost', 34 | neo4jUserName: 'neo4j', 35 | neo4jPassword: 'password' 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Neo4j 7 | 8 | 9 | 10 | 11 |
12 | 15 | 16 | -------------------------------------------------------------------------------- /neo4j-vue.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neo4j-vue", 3 | "version": "0.1.0", 4 | "description": "neo4j", 5 | "license": "MIT", 6 | "author": { 7 | "name": "zhenghuiqiang", 8 | "email": "zhq734@sina.com" 9 | }, 10 | "keywords": [ 11 | "neo4j", 12 | "d3", 13 | "vue" 14 | ], 15 | "engines": { 16 | "node": ">=4", 17 | "npm": ">=3" 18 | }, 19 | "scripts": { 20 | "update": "npm update --registry=https://registry.npm.taobao.org", 21 | "build": "cross-env NODE_ENV=production node build/build.js", 22 | "clean": "rm -rf dist", 23 | "dev": "cross-env NODE_ENV=development node build/dev-server.js" 24 | }, 25 | "dependencies": { 26 | "jquery": "^3.2.1", 27 | "vue": "^2.4.2", 28 | "vue-router": "^2.7.0", 29 | "vuex": "^2.3.1", 30 | "vuex-router-sync": "^4.2.0", 31 | "neo4j-driver": "~1.2.0", 32 | "vue-styled-components": "^1.1.2", 33 | "uuid": "^3.0.1" 34 | }, 35 | "devDependencies": { 36 | "autoprefixer": "^7.1.1", 37 | "babel-core": "^6.25.0", 38 | "babel-eslint": "^7.2.3", 39 | "babel-helper-vue-jsx-merge-props": "^2.0.2", 40 | "babel-loader": "^7.0.0", 41 | "babel-plugin-syntax-jsx": "^6.18.0", 42 | "babel-plugin-transform-export-extensions": "^6.22.0", 43 | "babel-plugin-transform-vue-jsx": "^3.4.3", 44 | "babel-preset-es2015": "^6.24.1", 45 | "babel-preset-stage-2": "^6.24.1", 46 | "babel-polyfill": "^6.23.0", 47 | "coffee-loader": "^0.7.3", 48 | "coffee-script": "^1.12.7", 49 | "connect-history-api-fallback": "^1.3.0", 50 | "cross-env": "^5.0.1", 51 | "css-loader": "^0.28.4", 52 | "express": "^4.15.3", 53 | "extract-text-webpack-plugin": "^2.1.2", 54 | "file-loader": "^0.11.2", 55 | "html-webpack-plugin": "^2.28.0", 56 | "http-proxy-middleware": "^0.17.4", 57 | "imports-loader": "^0.7.1", 58 | "json-loader": "^0.5.4", 59 | "ora": "^1.3.0", 60 | "serve-favicon": "^2.4.3", 61 | "shelljs": "^0.7.8", 62 | "style-loader": "^0.18.2", 63 | "url-loader": "^0.5.8", 64 | "vue-html-loader": "^1.2.4", 65 | "vue-loader": "^12.2.2", 66 | "vue-style-loader": "^3.0.1", 67 | "vue-template-compiler": "^2.4.2", 68 | "webpack": "^2.5.0", 69 | "webpack-dev-middleware": "^1.10.2", 70 | "webpack-hot-middleware": "^2.18.0", 71 | "webpack-merge": "^4.1.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /screenshots/项目截图1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhq734/neo4j-vue/e344d2c932d4fd56db0ba8e39716c30b5f431696/screenshots/项目截图1.png -------------------------------------------------------------------------------- /screenshots/项目截图2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhq734/neo4j-vue/e344d2c932d4fd56db0ba8e39716c30b5f431696/screenshots/项目截图2.png -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | 20 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhenghuiqiang on 17/8/5. 3 | */ 4 | import Vue from 'vue' 5 | import { sync } from 'vuex-router-sync' 6 | import App from './App.vue' 7 | import router from './router' 8 | 9 | window.jQuery = window.$ = require('jquery/dist/jquery') 10 | 11 | const app = new Vue({ 12 | router, 13 | ...App 14 | }) 15 | 16 | export { app, router } 17 | -------------------------------------------------------------------------------- /src/components/D3Visualization/GraphEventHandler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2002-2017 "Neo Technology," 3 | * Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Neo4j is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | import {mapNodes, mapRelationships, getGraphStats} from './mapper' 22 | 23 | export class GraphEventHandler { 24 | constructor (graph, graphView, getNodeNeighbours, onItemMouseOver, onItemSelected, onGraphModelChange) { 25 | this.graph = graph 26 | this.graphView = graphView 27 | this.getNodeNeighbours = getNodeNeighbours 28 | this.selectedItem = null 29 | this.onItemMouseOver = onItemMouseOver 30 | this.onItemSelected = onItemSelected 31 | this.onGraphModelChange = onGraphModelChange 32 | } 33 | 34 | graphModelChanged () { 35 | this.onGraphModelChange(getGraphStats(this.graph)) 36 | } 37 | 38 | selectItem (item) { 39 | if (this.selectedItem) { 40 | this.selectedItem.selected = false 41 | } 42 | this.selectedItem = item 43 | item.selected = true 44 | this.graphView.update() 45 | } 46 | 47 | deselectItem () { 48 | if (this.selectedItem) { 49 | this.selectedItem.selected = false 50 | this.selectedItem = null 51 | } 52 | this.onItemSelected({type: 'canvas', item: {nodeCount: this.graph.nodes().length, relationshipCount: this.graph.relationships().length}}) 53 | this.graphView.update() 54 | } 55 | 56 | nodeClose (d) { 57 | this.graph.removeConnectedRelationships(d) 58 | this.graph.removeNode(d) 59 | this.deselectItem() 60 | this.graphView.update() 61 | this.graphModelChanged() 62 | } 63 | 64 | nodeClicked (d) { 65 | d.fixed = true 66 | if (!d.selected) { 67 | this.selectItem(d) 68 | this.onItemSelected({type: 'node', item: {id: d.id, labels: d.labels, properties: d.propertyList}}) 69 | } else { 70 | this.deselectItem() 71 | } 72 | } 73 | 74 | nodeUnlock (d) { 75 | d.fixed = false 76 | this.deselectItem() 77 | } 78 | 79 | nodeDblClicked (d) { 80 | const graph = this.graph 81 | const graphView = this.graphView 82 | const graphModelChanged = this.graphModelChanged.bind(this) 83 | this.getNodeNeighbours(d, this.graph.findNodeNeighbourIds(d.id), function ({nodes, relationships}) { 84 | graph.addNodes(mapNodes(nodes)) 85 | graph.addRelationships(mapRelationships(relationships, graph)) 86 | graphView.update() 87 | graphModelChanged() 88 | }) 89 | } 90 | 91 | onNodeMouseOver (node) { 92 | if (!node.contextMenu) { 93 | this.onItemMouseOver({type: 'node', item: {id: node.id, labels: node.labels, properties: node.propertyList}}) 94 | } 95 | } 96 | onMenuMouseOver (itemWithMenu) { 97 | this.onItemMouseOver({type: 'context-menu-item', item: {label: itemWithMenu.contextMenu.label, content: itemWithMenu.contextMenu.menuContent, selection: itemWithMenu.contextMenu.menuSelection}}) 98 | } 99 | onRelationshipMouseOver (relationship) { 100 | this.onItemMouseOver({type: 'relationship', item: {id: relationship.id, type: relationship.type, properties: relationship.propertyList}}) 101 | } 102 | 103 | onRelationshipClicked (relationship) { 104 | if (!relationship.selected) { 105 | this.selectItem(relationship) 106 | this.onItemSelected({type: 'relationship', item: {id: relationship.id, type: relationship.type, properties: relationship.propertyList}}) 107 | } else { 108 | this.deselectItem() 109 | } 110 | } 111 | 112 | onCanvasClicked () { 113 | this.deselectItem() 114 | } 115 | 116 | onItemMouseOut (item) { 117 | this.onItemMouseOver({type: 'canvas', item: {nodeCount: this.graph.nodes().length, relationshipCount: this.graph.relationships().length}}) 118 | } 119 | 120 | bindEventHandlers () { 121 | this.graphView 122 | .on('nodeMouseOver', this.onNodeMouseOver.bind(this)) 123 | .on('nodeMouseOut', this.onItemMouseOut.bind(this)) 124 | .on('menuMouseOver', this.onMenuMouseOver.bind(this)) 125 | .on('menuMouseOut', this.onItemMouseOut.bind(this)) 126 | .on('relMouseOver', this.onRelationshipMouseOver.bind(this)) 127 | .on('relMouseOut', this.onItemMouseOut.bind(this)) 128 | .on('relationshipClicked', this.onRelationshipClicked.bind(this)) 129 | .on('canvasClicked', this.onCanvasClicked.bind(this)) 130 | .on('nodeClose', this.nodeClose.bind(this)) 131 | .on('nodeClicked', this.nodeClicked.bind(this)) 132 | .on('nodeDblClicked', this.nodeDblClicked.bind(this)) 133 | .on('nodeUnlock', this.nodeUnlock.bind(this)) 134 | this.onItemMouseOut() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/components/D3Visualization/Visualization.vue: -------------------------------------------------------------------------------- 1 | 24 | 225 | 228 | -------------------------------------------------------------------------------- /src/components/D3Visualization/components/Explorer.vue: -------------------------------------------------------------------------------- 1 | 49 | 261 | -------------------------------------------------------------------------------- /src/components/D3Visualization/components/Graph.vue: -------------------------------------------------------------------------------- 1 | 20 | 211 | -------------------------------------------------------------------------------- /src/components/D3Visualization/components/GrassEditor.vue: -------------------------------------------------------------------------------- 1 | 195 | -------------------------------------------------------------------------------- /src/components/D3Visualization/components/Inspector.vue: -------------------------------------------------------------------------------- 1 | 169 | -------------------------------------------------------------------------------- /src/components/D3Visualization/components/Legend.vue: -------------------------------------------------------------------------------- 1 | 161 | -------------------------------------------------------------------------------- /src/components/D3Visualization/components/RowExpandToggle.vue: -------------------------------------------------------------------------------- 1 | 72 | -------------------------------------------------------------------------------- /src/components/D3Visualization/components/styled.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2002-2017 "Neo Technology," 3 | * Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Neo4j is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | import styled from 'vue-styled-components' 22 | 23 | export const legendRowHeight = 32 24 | export const inspectorFooterContractedHeight = 22 25 | const pMarginTop = 6 26 | 27 | export const StyledSvgWrapper = styled.div` 28 | line-height: 0; 29 | height: 100%; 30 | position: relative; 31 | > svg { 32 | height: 100%; 33 | width: 100%; 34 | background-color: #f9fbfd; 35 | .node { 36 | cursor: pointer; 37 | > .ring { 38 | fill: none; 39 | opacity: 0; 40 | stroke: #6ac6ff; 41 | } 42 | &.selected { 43 | > .ring { 44 | stroke: #fdcc59; 45 | opacity: 0.3; 46 | } 47 | } 48 | &:hover { 49 | > .ring { 50 | stroke: #6ac6ff; 51 | opacity: 0.3; 52 | } 53 | } 54 | } 55 | .relationship { 56 | > .overlay { 57 | opacity: 0; 58 | fill: #6ac6ff; 59 | } 60 | &.selected { 61 | > .overlay { 62 | fill: #fdcc59; 63 | opacity: 0.3; 64 | } 65 | } 66 | &:hover { 67 | > .overlay { 68 | fill: #6ac6ff; 69 | opacity: 0.3; 70 | } 71 | } 72 | } 73 | .remove_node { 74 | .expand_node { 75 | &:hover { 76 | border: 2px #000 solid; 77 | } 78 | } 79 | } 80 | .outline { 81 | cursor: pointer 82 | } 83 | path { 84 | &.context-menu-item { 85 | stroke-width: 2px; 86 | fill: #d2d5da; 87 | } 88 | } 89 | text { 90 | line-height: normal; 91 | &.context-menu-item { 92 | fill: #fff; 93 | text-anchor: middle; 94 | pointer-events: none; 95 | font-size: 14px; 96 | } 97 | } 98 | .context-menu-item { 99 | cursor: pointer; 100 | &:hover { 101 | fill: #b9b9b9; 102 | font-size: 14px; 103 | } 104 | } 105 | } 106 | ` 107 | export const StyledStream = styled.div` 108 | padding: 0; 109 | display: flex; 110 | flex-direction: column; 111 | margin-top: 17px; 112 | ` 113 | 114 | export const p = styled.div` 115 | margin-top: ${pMarginTop}px; 116 | font-size: 12px; 117 | width: 100%; 118 | white-space: normal; 119 | ` 120 | 121 | export const StyledRowToggle = styled.div` 122 | float: right; 123 | display: block; 124 | width: 21px; 125 | height: 21px; 126 | line-height: 21px; 127 | text-align: center; 128 | cursor: pointer; 129 | ` 130 | export const StyledCaret = styled.div` 131 | font-size: 17px; 132 | vertical-align: middle; 133 | ` 134 | 135 | export const StyledInspectorFooter = styled.div` 136 | margin-top: 6px; 137 | font-size: 12px; 138 | width: 100%; 139 | white-space: normal; 140 | overflow: scroll; 141 | &.contracted { 142 | max-height: ${inspectorFooterContractedHeight}px; 143 | overflow: hidden; 144 | } 145 | ` 146 | 147 | export const StyledInspectorFooterRow = styled.ul` 148 | list-style: none; 149 | word-break: break-word; 150 | line-height: 21px; 151 | ` 152 | 153 | export const StyledInspectorFooterRowListKey = styled.div` 154 | float: left; 155 | font-weight: 800; 156 | ` 157 | 158 | export const StyledInspectorFooterRowListValue = styled.div` 159 | padding-left: 3px; 160 | overflow: hidden; 161 | float: left; 162 | white-space: pre-wrap; 163 | ` 164 | 165 | export const StyledInlineList = styled.ul` 166 | padding-left: 0; 167 | list-style: none; 168 | word-break: break-word 169 | ` 170 | 171 | export const StyledInlineListItem = styled.li` 172 | display: inline-block; 173 | padding-right: 5px; 174 | padding-left: 5px; 175 | ` 176 | 177 | export const StyledStatusBarWrapper = styled.div` 178 | height: 68px; 179 | display: none; 180 | ` 181 | export const StyledStatusBar = styled.div` 182 | min-height: 39px; 183 | line-height: 39px; 184 | color: #788185; 185 | font-size: 13px; 186 | position: relative; 187 | background-color: #fff; 188 | white-space: nowrap; 189 | overflow: hidden; 190 | border-top: 1px solid #e6e9ef; 191 | ${props => props.fullscreen ? 'margin-top: -39px;' : 'margin-bottom: -39px;'} 192 | ` 193 | 194 | export const StyledStatus = styled.div` 195 | position: relative; 196 | float: left; 197 | padding-left: 16px; 198 | margin-top: 0; 199 | margin-bottom: 0; 200 | width: 100%; 201 | margin-top: 3px; 202 | ` 203 | 204 | export const StyledInspectorFooterRowListPair = styled(StyledInlineListItem)` 205 | vertical-align: middle; 206 | font-size: 13px; 207 | ` 208 | 209 | export const StyledToken = styled(StyledInlineListItem)` 210 | display: inline-block; 211 | font-weight: bold; 212 | line-height: 1em; 213 | text-align: center; 214 | white-space: nowrap; 215 | vertical-align: baseline; 216 | user-select: none; 217 | font-size: 12px; 218 | margin-right: 5px; 219 | cursor: pointer; 220 | ` 221 | export const StyledLabelToken = styled(StyledToken)` 222 | padding: 4px 7px 4px 9px; 223 | border-radius: 20px; 224 | ` 225 | export const StyledTokenRelationshipType = styled(StyledToken)` 226 | padding: 4px 7px 4px 5px; 227 | border-radius: 3px; 228 | ` 229 | 230 | export const tokenPropertyKey = styled(StyledToken)` 231 | padding: 3px 5px 3px 5px; 232 | ` 233 | export const StyledTokenContextMenuKey = styled(StyledLabelToken)` { 234 | color: #f9fbfd; 235 | background-color: #d2d5da; 236 | font-family: FontAwesome; 237 | padding: 4px 9px; 238 | ` 239 | 240 | export const StyledTokenCount = styled.span` 241 | font-weight: normal; 242 | ` 243 | 244 | export const StyledLegendContents = styled.div` 245 | float: left; 246 | line-height: 1em; 247 | position: relative; 248 | top: 5px; 249 | ` 250 | 251 | export const StyledLegendRow = styled.div` 252 | border-bottom: 1px solid #e6e9ef; 253 | &.contracted { 254 | max-height: ${legendRowHeight}px 255 | overflow: hidden; 256 | } 257 | ` 258 | export const StyledLegend = styled.div` 259 | background-color: #eef1f8; 260 | margin-top: -${(legendRowHeight * 2) + 1}px; 261 | &.one-row { 262 | margin-top: -${legendRowHeight}px; 263 | } 264 | ` 265 | export const StyledLegendInlineList = styled(StyledInlineList) ` 266 | padding: 7px 9px 0px 10px; 267 | ` 268 | export const StyledLegendInlineListItem = styled(StyledInlineListItem) ` 269 | display: inline-block; 270 | margin-bottom: 3px; 271 | ` 272 | export const StyledPickerListItem = styled(StyledInlineListItem)` 273 | padding-right: 5px; 274 | padding-left: 0; 275 | vertical-align: middle; 276 | line-height: 0; 277 | ` 278 | 279 | export const StyledPickerSelector = styled.a` 280 | background-color: #aaa; 281 | display: inline-block; 282 | height: 12px; 283 | width: 12px; 284 | margin-top: 1px; 285 | line-height: 0; 286 | cursor: pointer; 287 | opacity: 0.4; 288 | &:hover { 289 | opacity: 1; 290 | } 291 | &.active { 292 | opacity: 1; 293 | } 294 | ` 295 | export const StyledCircleSelector = styled(StyledPickerSelector)` 296 | border-radius: 50%; 297 | ` 298 | export const StyledCaptionSelector = styled.a` 299 | cursor: pointer; 300 | user-select: none; 301 | display: inline-block; 302 | padding: 1px 6px; 303 | font-size: 12px; 304 | line-height: 1em; 305 | color: #9195A0; 306 | border: 1px solid #9195A0; 307 | overflow: hidden; 308 | border-radius: .25em; 309 | margin-right: 0; 310 | font-weight: bold; 311 | &:hover { 312 | border-color: #aaa; 313 | color: #aaa; 314 | text-decoration: none; 315 | } 316 | &.active { 317 | color: white; 318 | background-color: #9195A0; 319 | } 320 | ` 321 | 322 | export const StyledFullSizeContainer = styled.div` 323 | height: 100%; 324 | padding-top: ${(legendRowHeight * 2) + 1}px; 325 | padding-bottom: ${props => props.forcePaddingBottom ? (props.forcePaddingBottom + 2 * pMarginTop) + 'px' : '39px'}; 326 | &.one-legend-row { 327 | padding-top: ${legendRowHeight}px; 328 | } 329 | ` 330 | 331 | export const StyledInspectorFooterStatusMessage = styled.div` 332 | font-weight: bold; 333 | ` 334 | 335 | export const StyledZoomHolder = styled.div` 336 | position: absolute; 337 | bottom: 39px; 338 | right: 0; 339 | padding: 6px 6px 0 6px; 340 | border-left: #e6e9ef solid 1px; 341 | border-top: #e6e9ef solid 1px; 342 | background: #fff; 343 | ` 344 | 345 | export const StyledZoomButton = styled.button` 346 | display: list-item; 347 | list-style-type: none; 348 | font-size: 2em; 349 | margin-bottom: 10px; 350 | border: none; 351 | color: #9b9da2; 352 | background: transparent; 353 | border-color: black; 354 | padding: 2px 6px 3px; 355 | &:hover { 356 | color:black; 357 | } 358 | &:focus { 359 | outline: none 360 | } 361 | &.faded { 362 | opacity: .3; 363 | cursor: auto; 364 | &:hover { 365 | color: #9b9da2; 366 | } 367 | } 368 | ` 369 | -------------------------------------------------------------------------------- /src/components/D3Visualization/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhenghuiqiang on 17/7/31. 3 | */ 4 | 5 | export const font = { 6 | 7 | } 8 | 9 | export const dim = { 10 | // Editor bar 11 | editorbarHeight: 70, 12 | // Frame 13 | frameBodyHeight: 550, 14 | frameTitlebarHeight: 39, 15 | frameStatusbarHeight: 39, 16 | frameBodyPadding: 20 17 | } 18 | -------------------------------------------------------------------------------- /src/components/D3Visualization/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhenghuiqiang on 17/7/31. 3 | */ 4 | 5 | export Visualization from './Visualization' 6 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/components/collision.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | neo.collision = do -> 24 | collision = {} 25 | 26 | collide = (node) -> 27 | r = node.radius + 10 28 | nx1 = node.x - r 29 | nx2 = node.x + r 30 | ny1 = node.y - r 31 | ny2 = node.y + r 32 | return (quad, x1, y1, x2, y2) -> 33 | if (quad.point && (quad.point != node)) 34 | x = node.x - quad.point.x 35 | y = node.y - quad.point.y 36 | l = Math.sqrt(x * x + y * y) 37 | r = node.radius + 10 + quad.point.radius 38 | if (l < r) 39 | l = (l - r) / l * .5 40 | node.x -= x *= l 41 | node.y -= y *= l 42 | quad.point.x += x 43 | quad.point.y += y 44 | x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1 45 | 46 | collision.avoidOverlap = (nodes) -> 47 | q = d3.geom.quadtree(nodes) 48 | for n in nodes 49 | q.visit collide(n) 50 | 51 | collision -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/components/graph.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.models.Graph 24 | constructor: () -> 25 | @nodeMap = {} 26 | @_nodes = [] 27 | @relationshipMap = {} 28 | @_relationships = [] 29 | 30 | nodes: -> 31 | @_nodes 32 | 33 | relationships: -> 34 | @_relationships 35 | 36 | groupedRelationships: -> 37 | class NodePair 38 | constructor: (node1, node2) -> 39 | @relationships = [] 40 | if node1.id < node2.id 41 | @nodeA = node1 42 | @nodeB = node2 43 | else 44 | @nodeA = node2 45 | @nodeB = node1 46 | 47 | isLoop: -> 48 | @nodeA is @nodeB 49 | 50 | toString: -> 51 | "#{@nodeA.id}:#{@nodeB.id}" 52 | groups = {} 53 | for relationship in @_relationships 54 | nodePair = new NodePair(relationship.source, relationship.target) 55 | nodePair = groups[nodePair] ? nodePair 56 | nodePair.relationships.push relationship 57 | groups[nodePair] = nodePair 58 | (pair for ignored, pair of groups) 59 | 60 | addNodes: (nodes) => 61 | for node in nodes 62 | if !@findNode(node.id)? 63 | @nodeMap[node.id] = node 64 | @_nodes.push(node) 65 | @ 66 | 67 | removeNode: (node) => 68 | if @findNode(node.id)? 69 | delete @nodeMap[node.id] 70 | @_nodes.splice(@_nodes.indexOf(node), 1) 71 | @ 72 | 73 | updateNode: (node) => 74 | if @findNode(node.id)? 75 | @removeNode node 76 | node.expanded = false 77 | node.minified = true 78 | @addNodes [node] 79 | @ 80 | 81 | removeConnectedRelationships: (node) => 82 | for r in @findAllRelationshipToNode node 83 | @updateNode r.source 84 | @updateNode r.target 85 | @_relationships.splice(@_relationships.indexOf(r), 1) 86 | delete @relationshipMap[r.id] 87 | @ 88 | 89 | addRelationships: (relationships) => 90 | for relationship in relationships 91 | existingRelationship = @findRelationship(relationship.id) 92 | if existingRelationship? 93 | existingRelationship.internal = false 94 | else 95 | relationship.internal = false 96 | @relationshipMap[relationship.id] = relationship 97 | @_relationships.push(relationship) 98 | @ 99 | 100 | addInternalRelationships: (relationships) => 101 | for relationship in relationships 102 | relationship.internal = true 103 | if not @findRelationship(relationship.id)? 104 | @relationshipMap[relationship.id] = relationship 105 | @_relationships.push(relationship) 106 | @ 107 | 108 | pruneInternalRelationships: => 109 | relationships = @_relationships.filter((relationship) -> not relationship.internal) 110 | @relationshipMap = {} 111 | @_relationships = [] 112 | @addRelationships(relationships) 113 | 114 | findNode: (id) => @nodeMap[id] 115 | 116 | findNodeNeighbourIds: (id) => 117 | @_relationships 118 | .filter((relationship) -> relationship.source.id is id or relationship.target.id is id) 119 | .map((relationship) -> 120 | if relationship.target.id is id 121 | return relationship.source.id 122 | return relationship.target.id 123 | ) 124 | 125 | findRelationship: (id) => @relationshipMap[id] 126 | 127 | findAllRelationshipToNode: (node) => 128 | @_relationships 129 | .filter((relationship) -> relationship.source.id is node.id or relationship.target.id is node.id) 130 | 131 | resetGraph: -> 132 | @nodeMap = {} 133 | @_nodes = [] 134 | @relationshipMap = {} 135 | @_relationships = [] 136 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/components/graphGeometry.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.NeoD3Geometry 24 | square = (distance) -> distance * distance 25 | 26 | constructor: (@style) -> 27 | @relationshipRouting = new neo.utils.pairwiseArcsRelationshipRouting(@style) 28 | 29 | addShortenedNextWord = (line, word, measure) -> 30 | until word.length <= 2 31 | word = word.substr(0, word.length - 2) + '\u2026' 32 | if measure(word) < line.remainingWidth 33 | line.text += " " + word 34 | break 35 | 36 | noEmptyLines = (lines) -> 37 | for line in lines 38 | if line.text.length is 0 then return false 39 | true 40 | 41 | fitCaptionIntoCircle = (node, style) -> 42 | template = style.forNode(node).get("caption") 43 | captionText = style.interpolate(template, node) 44 | fontFamily = 'sans-serif' 45 | fontSize = parseFloat(style.forNode(node).get('font-size')) 46 | lineHeight = fontSize 47 | measure = (text) -> 48 | neo.utils.measureText(text, fontFamily, fontSize) 49 | 50 | words = captionText.split(" ") 51 | 52 | emptyLine = (lineCount, iLine) -> 53 | baseline = (1 + iLine - lineCount / 2) * lineHeight 54 | if (style.forNode(node).get("icon-code")) 55 | baseline = baseline + node.radius/3 56 | constainingHeight = if iLine < lineCount / 2 then baseline - lineHeight else baseline 57 | lineWidth = Math.sqrt(square(node.radius) - square(constainingHeight)) * 2 58 | { 59 | node: node 60 | text: '' 61 | baseline: baseline 62 | remainingWidth: lineWidth 63 | } 64 | 65 | fitOnFixedNumberOfLines = (lineCount) -> 66 | lines = [] 67 | iWord = 0; 68 | for iLine in [0..lineCount - 1] 69 | line = emptyLine(lineCount, iLine) 70 | while iWord < words.length and measure(" " + words[iWord]) < line.remainingWidth 71 | line.text += " " + words[iWord] 72 | line.remainingWidth -= measure(" " + words[iWord]) 73 | iWord++ 74 | lines.push line 75 | if iWord < words.length 76 | addShortenedNextWord(lines[lineCount - 1], words[iWord], measure) 77 | [lines, iWord] 78 | 79 | consumedWords = 0 80 | maxLines = node.radius * 2 / fontSize 81 | 82 | lines = [emptyLine(1, 0)] 83 | for lineCount in [1..maxLines] 84 | [candidateLines, candidateWords] = fitOnFixedNumberOfLines(lineCount) 85 | if noEmptyLines(candidateLines) 86 | [lines, consumedWords] = [candidateLines, candidateWords] 87 | if consumedWords >= words.length 88 | return lines 89 | lines 90 | 91 | formatNodeCaptions: (nodes) -> 92 | for node in nodes 93 | node.caption = fitCaptionIntoCircle(node, @style) 94 | 95 | formatRelationshipCaptions: (relationships) -> 96 | for relationship in relationships 97 | template = @style.forRelationship(relationship).get("caption") 98 | relationship.caption = @style.interpolate(template, relationship) 99 | 100 | setNodeRadii: (nodes) -> 101 | for node in nodes 102 | node.radius = parseFloat(@style.forNode(node).get("diameter")) / 2 103 | 104 | onGraphChange: (graph) -> 105 | @setNodeRadii(graph.nodes()) 106 | @formatNodeCaptions(graph.nodes()) 107 | @formatRelationshipCaptions(graph.relationships()) 108 | @relationshipRouting.measureRelationshipCaptions(graph.relationships()) 109 | 110 | onTick: (graph) -> 111 | @relationshipRouting.layoutRelationships(graph) 112 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/components/graphView.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.graphView 24 | constructor: (element, measureSize, @graph, @style) -> 25 | layout = neo.layout.force() 26 | @viz = neo.viz(element, measureSize, @graph, layout, @style) 27 | @callbacks = {} 28 | callbacks = @callbacks 29 | @viz.trigger = do -> 30 | (event, args...) -> 31 | callback.apply(null, args) for callback in (callbacks[event] or []) 32 | 33 | on: (event, callback) -> 34 | (@callbacks[event] ?= []).push(callback) 35 | @ 36 | 37 | layout: (value) -> 38 | return layout unless arguments.length 39 | layout = value 40 | @ 41 | 42 | grass: (value) -> 43 | return @style.toSheet() unless arguments.length 44 | @style.importGrass(value) 45 | @ 46 | 47 | update: -> 48 | @viz.update() 49 | @ 50 | 51 | resize: -> 52 | @viz.resize() 53 | @ 54 | 55 | boundingBox: -> 56 | @viz.boundingBox() 57 | 58 | collectStats: -> 59 | @viz.collectStats() 60 | 61 | zoomIn: (elem) -> 62 | @viz.zoomInClick(elem) 63 | 64 | zoomOut: (elem) -> 65 | @viz.zoomOutClick(elem) 66 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/components/layout.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | neo.layout = do -> 24 | _layout = {} 25 | 26 | _layout.force = -> 27 | _force = {} 28 | 29 | _force.init = (render) -> 30 | forceLayout = {} 31 | 32 | linkDistance = 45 33 | 34 | d3force = d3.layout.force() 35 | .linkDistance((relationship) -> relationship.source.radius + relationship.target.radius + linkDistance) 36 | .charge(-1000) 37 | 38 | newStatsBucket = -> 39 | bucket = 40 | layoutTime: 0 41 | layoutSteps: 0 42 | bucket 43 | 44 | currentStats = newStatsBucket() 45 | 46 | forceLayout.collectStats = -> 47 | latestStats = currentStats 48 | currentStats = newStatsBucket() 49 | latestStats 50 | 51 | accelerateLayout = -> 52 | maxStepsPerTick = 100 53 | maxAnimationFramesPerSecond = 60 54 | maxComputeTime = 1000 / maxAnimationFramesPerSecond 55 | now = if window.performance and window.performance.now 56 | () -> 57 | window.performance.now() 58 | else 59 | () -> 60 | Date.now() 61 | 62 | d3Tick = d3force.tick 63 | d3force.tick = -> 64 | startTick = now() 65 | step = maxStepsPerTick 66 | while step-- and now() - startTick < maxComputeTime 67 | startCalcs = now() 68 | currentStats.layoutSteps++ 69 | 70 | neo.collision.avoidOverlap d3force.nodes() 71 | 72 | if d3Tick() 73 | maxStepsPerTick = 2 74 | return true 75 | currentStats.layoutTime += now() - startCalcs 76 | render() 77 | false 78 | 79 | accelerateLayout() 80 | 81 | oneRelationshipPerPairOfNodes = (graph) -> 82 | (pair.relationships[0] for pair in graph.groupedRelationships()) 83 | 84 | forceLayout.update = (graph, size) -> 85 | 86 | nodes = neo.utils.cloneArray(graph.nodes()) 87 | relationships = oneRelationshipPerPairOfNodes(graph) 88 | 89 | radius = nodes.length * linkDistance / (Math.PI * 2) 90 | center = 91 | x: size[0] / 2 92 | y: size[1] / 2 93 | neo.utils.circularLayout(nodes, center, radius) 94 | 95 | d3force 96 | .nodes(nodes) 97 | .links(relationships) 98 | .size(size) 99 | .start() 100 | 101 | forceLayout.drag = d3force.drag 102 | forceLayout 103 | 104 | _force 105 | 106 | _layout -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/components/node.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.models.Node 24 | constructor: (@id, @labels, properties) -> 25 | @propertyMap = properties 26 | @propertyList = for own key,value of properties 27 | { key: key, value: value } 28 | 29 | toJSON: -> 30 | @propertyMap 31 | 32 | isNode: true 33 | isRelationship: false 34 | 35 | relationshipCount: (graph) -> 36 | node = @ 37 | rels = [] 38 | rels.push[relationship] for relationship in graph.relationships() when relationship.source is node or relationship.target is node 39 | rels.length 40 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/components/relationship.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.models.Relationship 24 | constructor: (@id, @source, @target, @type, properties) -> 25 | @propertyMap = properties 26 | @propertyList = for own key,value of @propertyMap 27 | { key: key, value: value } 28 | 29 | toJSON: -> 30 | @propertyMap 31 | 32 | isNode: false 33 | isRelationship: true 34 | isLoop: -> 35 | @source is @target -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/components/renderer.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.Renderer 24 | constructor: (opts = {})-> 25 | neo.utils.extend(@, opts) 26 | @onGraphChange ?= -> 27 | @onTick ?= -> -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/components/style.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | neo.style = do -> 24 | _style = (storage) -> 25 | new GraphStyle(storage) 26 | 27 | _style.defaults = 28 | autoColor: yes 29 | colors: [ 30 | { color: '#DFE1E3', 'border-color': '#D4D6D7', 'text-color-internal': '#000000' } 31 | { color: '#F25A29', 'border-color': '#DC4717', 'text-color-internal': '#FFFFFF' } 32 | { color: '#AD62CE', 'border-color': '#9453B1', 'text-color-internal': '#FFFFFF' } 33 | { color: '#30B6AF', 'border-color': '#46A39E', 'text-color-internal': '#FFFFFF' } 34 | { color: '#FF6C7C', 'border-color': '#EB5D6C', 'text-color-internal': '#FFFFFF' } 35 | { color: '#FCC940', 'border-color': '#F3BA25', 'text-color-internal': '#000000' } 36 | { color: '#4356C0', 'border-color': '#3445A2', 'text-color-internal': '#FFFFFF' } 37 | ] 38 | style: 39 | 'node': 40 | 'diameter': '40px' 41 | 'color': '#DFE1E3' 42 | 'border-color': '#5CA8DB' 43 | 'border-width': '2px' 44 | 'text-color-internal': '#000000' 45 | 'caption': '{id}' 46 | 'font-size': '10px' 47 | 'relationship': 48 | 'color': '#D4D6D7' 49 | 'shaft-width': '1px' 50 | 'font-size': '8px' 51 | 'padding': '3px' 52 | 'text-color-external': '#5CA8DB' 53 | 'text-color-internal': '#68BDF6' 54 | # 'node': 55 | # 'diameter': '40px' 56 | # 'color': '#DFE1E3' 57 | # 'border-color': '#D4D6D7' 58 | # 'border-width': '2px' 59 | # 'text-color-internal': '#000000' 60 | # 'caption': '{id}' 61 | # 'font-size': '10px' 62 | # 'relationship': 63 | # 'color': '#D4D6D7' 64 | # 'shaft-width': '1px' 65 | # 'font-size': '8px' 66 | # 'padding': '3px' 67 | # 'text-color-external': '#000000' 68 | # 'text-color-internal': '#FFFFFF' 69 | sizes: [ 70 | { diameter: '10px' } 71 | { diameter: '20px' } 72 | { diameter: '30px' } 73 | { diameter: '50px' } 74 | { diameter: '80px' } 75 | ] 76 | arrayWidths: [ 77 | { 'shaft-width': '1px' } 78 | { 'shaft-width': '2px' } 79 | { 'shaft-width': '3px' } 80 | { 'shaft-width': '5px' } 81 | { 'shaft-width': '8px' } 82 | { 'shaft-width': '13px' } 83 | { 'shaft-width': '25px' } 84 | { 'shaft-width': '38px' } 85 | ] 86 | 87 | class Selector 88 | constructor: (selector) -> 89 | [@tag, @klass] = if selector.indexOf('.') > 0 90 | selector.split('.') 91 | else 92 | [selector, undefined] 93 | 94 | toString: -> 95 | str = @tag 96 | str += ".#{@klass}" if @klass? 97 | str 98 | 99 | class StyleRule 100 | constructor: (@selector, @props) -> 101 | 102 | matches: (selector) -> 103 | if @selector.tag is selector.tag 104 | if @selector.klass is selector.klass or not @selector.klass 105 | return yes 106 | return no 107 | 108 | matchesExact: (selector) -> 109 | @selector.tag is selector.tag and @selector.klass is selector.klass 110 | 111 | class StyleElement 112 | constructor: (selector, @data) -> 113 | @selector = selector 114 | @props = {} 115 | 116 | applyRules: (rules) -> 117 | # Two passes 118 | for rule in rules when rule.matches(@selector) 119 | neo.utils.extend(@props, rule.props) 120 | break 121 | for rule in rules when rule.matchesExact(@selector) 122 | neo.utils.extend(@props, rule.props) 123 | break 124 | @ 125 | 126 | get: (attr) -> 127 | @props[attr] or '' 128 | 129 | 130 | class GraphStyle 131 | constructor: (@storage) -> 132 | @rules = [] 133 | @loadRules() 134 | 135 | # Generate a selector string from an object (node or rel) 136 | selector: (item) -> 137 | if item.isNode 138 | @nodeSelector(item) 139 | else if item.isRelationship 140 | @relationshipSelector(item) 141 | 142 | # 143 | # Methods for calculating applied style for elements 144 | # 145 | calculateStyle: (selector, data) -> 146 | new StyleElement(selector, data).applyRules(@rules) 147 | 148 | forEntity: (item) -> 149 | @calculateStyle(@selector(item), item) 150 | 151 | forNode: (node = {}) -> 152 | selector = @nodeSelector(node) 153 | if node.labels?.length > 0 154 | @setDefaultStyling(selector) 155 | @calculateStyle(selector, node) 156 | 157 | forRelationship: (rel) -> 158 | @calculateStyle(@relationshipSelector(rel), rel) 159 | 160 | findAvailableDefaultColor: () -> 161 | usedColors = {} 162 | for rule in @rules 163 | if rule.props.color? 164 | usedColors[rule.props.color] = yes 165 | 166 | for defaultColor in _style.defaults.colors 167 | if !usedColors[defaultColor.color]? 168 | return neo.utils.copy(defaultColor) 169 | 170 | return neo.utils.copy(_style.defaults.colors[0]) 171 | 172 | setDefaultStyling: (selector) -> 173 | rule = @findRule(selector) 174 | 175 | if _style.defaults.autoColor and not rule? 176 | rule = new StyleRule(selector, @findAvailableDefaultColor()) 177 | @rules.push(rule) 178 | @persist() 179 | 180 | # 181 | # Methods for getting and modifying rules 182 | # 183 | change: (item, props) -> 184 | selector = @selector(item) 185 | rule = @findRule(selector) 186 | 187 | if not rule? 188 | rule = new StyleRule(selector, {}) 189 | @rules.push(rule) 190 | neo.utils.extend(rule.props, props) 191 | @persist() 192 | rule 193 | 194 | destroyRule: (rule) -> 195 | idx = @rules.indexOf(rule) 196 | @rules.splice(idx, 1) if idx? 197 | @persist() 198 | 199 | findRule: (selector) -> 200 | rule = r for r in @rules when r.matchesExact(selector) 201 | rule 202 | 203 | # 204 | # Selector helpers 205 | # 206 | nodeSelector: (node = {}) -> 207 | selector = 'node' 208 | if node.labels?.length > 0 209 | selector += ".#{node.labels[0]}" 210 | new Selector(selector) 211 | 212 | relationshipSelector: (rel = {}) -> 213 | selector = 'relationship' 214 | selector += ".#{rel.type}" if rel.type? 215 | new Selector(selector) 216 | 217 | # 218 | # Import/export 219 | # 220 | 221 | importGrass: (string) -> 222 | try 223 | rules = @parse(string) 224 | @loadRules(rules) 225 | @persist() 226 | catch e 227 | return 228 | 229 | loadRules: (data) -> 230 | data = _style.defaults.style unless neo.utils.isObject(data) 231 | @rules.length = 0 232 | for rule, props of data 233 | @rules.push(new StyleRule(new Selector(rule), neo.utils.copy(props))) 234 | @ 235 | 236 | parse: (string)-> 237 | chars = string.split('') 238 | insideString = no 239 | insideProps = no 240 | keyword = "" 241 | props = "" 242 | 243 | rules = {} 244 | 245 | for c in chars 246 | skipThis = yes 247 | switch c 248 | when "{" 249 | if not insideString 250 | insideProps = yes 251 | else 252 | skipThis = no 253 | when "}" 254 | if not insideString 255 | insideProps = no 256 | rules[keyword] = props 257 | keyword = "" 258 | props = "" 259 | else 260 | skipThis = no 261 | when "'", "\"" then insideString ^= true 262 | else skipThis = no 263 | 264 | continue if skipThis 265 | 266 | if insideProps 267 | props += c 268 | else 269 | keyword += c unless c.match(/[\s\n]/) 270 | 271 | for k, v of rules 272 | rules[k] = {} 273 | for prop in v.split(';') 274 | [key, val] = prop.split(':') 275 | continue unless key and val 276 | rules[k][key?.trim()] = val?.trim() 277 | 278 | rules 279 | 280 | persist: -> 281 | @storage?.add('grass', JSON.stringify(@toSheet())) 282 | 283 | resetToDefault: -> 284 | @loadRules() 285 | @persist() 286 | 287 | toSheet: -> 288 | sheet = {} 289 | sheet[rule.selector.toString()] = rule.props for rule in @rules 290 | sheet 291 | 292 | toString: -> 293 | str = "" 294 | for r in @rules 295 | str += r.selector.toString() + " {\n" 296 | for k, v of r.props 297 | v = "'#{v}'" if k == "caption" 298 | str += " #{k}: #{v};\n" 299 | str += "}\n\n" 300 | str 301 | 302 | # 303 | # Misc. 304 | # 305 | nextDefaultColor: 0 306 | defaultColors: -> neo.utils.copy(_style.defaults.colors) 307 | interpolate: (str, id, properties) -> 308 | # Supplant 309 | # http://javascript.crockford.com/remedial.html 310 | str.replace( 311 | /\{([^{}]*)\}/g, 312 | (a, b) -> 313 | r = properties[b] or id 314 | return if (typeof r is 'string' or typeof r is 'number') then r else a 315 | ) 316 | 317 | _style 318 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/components/visualization.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | neo.viz = (el, measureSize, graph, layout, style) -> 24 | viz = 25 | style: style 26 | 27 | root = d3.select(el) 28 | base_group = root.append('g').attr("transform", "translate(0,0)") 29 | rect = base_group.append("rect") 30 | .style("fill", "none") 31 | .style("pointer-events", "all") 32 | #Make the rect cover the whole surface 33 | .attr('x', '-2500') 34 | .attr('y', '-2500') 35 | .attr('width', '5000') 36 | .attr('height', '5000') 37 | .attr('transform', 'scale(1)') 38 | 39 | container = base_group.append('g') 40 | geometry = new neo.NeoD3Geometry(style) 41 | 42 | # This flags that a panning is ongoing and won't trigger 43 | # 'canvasClick' event when panning ends. 44 | draw = no 45 | 46 | # Arbitrary dimension used to keep force layout aligned with 47 | # the centre of the svg view-port. 48 | layoutDimension = 200 49 | 50 | updateViz = yes 51 | 52 | # To be overridden 53 | viz.trigger = (event, args...) -> 54 | 55 | onNodeClick = (node) => 56 | updateViz = no 57 | viz.trigger('nodeClicked', node) 58 | 59 | onNodeDblClick = (node) => viz.trigger('nodeDblClicked', node) 60 | 61 | onNodeDragToggle = (node) -> viz.trigger('nodeDragToggle', node) 62 | 63 | onRelationshipClick = (relationship) => 64 | d3.event.stopPropagation() 65 | updateViz = no 66 | viz.trigger('relationshipClicked', relationship) 67 | 68 | onNodeMouseOver = (node) -> viz.trigger('nodeMouseOver', node) 69 | onNodeMouseOut = (node) -> viz.trigger('nodeMouseOut', node) 70 | 71 | onRelMouseOver = (rel) -> viz.trigger('relMouseOver', rel) 72 | onRelMouseOut = (rel) -> viz.trigger('relMouseOut', rel) 73 | 74 | zoomLevel = null 75 | 76 | zoomed = -> 77 | draw = yes 78 | container.attr("transform", "translate(" + zoomBehavior.translate() + ")" + "scale(" + zoomBehavior.scale() + ")") 79 | 80 | zoomBehavior = d3.behavior.zoom().scaleExtent([0.2, 1]).on("zoom", zoomed) 81 | 82 | interpolateZoom = (translate, scale) -> 83 | d3.transition().duration(500).tween("zoom", -> 84 | t = d3.interpolate(zoomBehavior.translate(), translate) 85 | s = d3.interpolate(zoomBehavior.scale(), scale) 86 | (a)-> 87 | zoomBehavior.scale(s(a)).translate(t(a)) 88 | zoomed()) 89 | 90 | isZoomingIn = true 91 | 92 | viz.zoomInClick = -> 93 | isZoomingIn = true 94 | zoomClick this 95 | 96 | viz.zoomOutClick = -> 97 | isZoomingIn = false 98 | zoomClick this 99 | 100 | zoomClick = (element) -> 101 | draw = yes 102 | limitsReached = {zoomInLimit: false, zoomOutLimit: false} 103 | 104 | if isZoomingIn 105 | zoomLevel = Number (zoomBehavior.scale() * (1 + 0.2 * 1)).toFixed(2) 106 | if zoomLevel >= zoomBehavior.scaleExtent()[1] 107 | limitsReached.zoomInLimit = true 108 | interpolateZoom(zoomBehavior.translate(), zoomBehavior.scaleExtent()[1]) 109 | else 110 | interpolateZoom(zoomBehavior.translate(), zoomLevel) 111 | 112 | else 113 | zoomLevel = Number (zoomBehavior.scale() * (1 + 0.2 * -1)).toFixed(2) 114 | if zoomLevel <= zoomBehavior.scaleExtent()[0] 115 | limitsReached.zoomOutLimit = true 116 | interpolateZoom(zoomBehavior.translate(), zoomBehavior.scaleExtent()[0]) 117 | else 118 | interpolateZoom(zoomBehavior.translate(), zoomLevel) 119 | limitsReached 120 | # Background click event 121 | # Check if panning is ongoing 122 | rect.on('click', -> 123 | if not draw then viz.trigger('canvasClicked', el) 124 | ) 125 | 126 | base_group.call(zoomBehavior) 127 | .on("dblclick.zoom", null) 128 | #Single click is not panning 129 | .on("click.zoom", -> draw = no) 130 | .on("DOMMouseScroll.zoom", null) 131 | .on("wheel.zoom", null) 132 | .on("mousewheel.zoom", null) 133 | 134 | newStatsBucket = -> 135 | bucket = 136 | frameCount: 0 137 | geometry: 0 138 | relationshipRenderers: do -> 139 | timings = {} 140 | neo.renderers.relationship.forEach((r) -> 141 | timings[r.name] = 0 142 | ) 143 | timings 144 | bucket.duration = -> 145 | bucket.lastFrame - bucket.firstFrame 146 | bucket.fps = -> 147 | (1000 * bucket.frameCount / bucket.duration()).toFixed(1) 148 | bucket.lps = -> 149 | (1000 * bucket.layout.layoutSteps / bucket.duration()).toFixed(1) 150 | bucket.top = -> 151 | renderers = [] 152 | for name, time of bucket.relationshipRenderers 153 | renderers.push { 154 | name: name 155 | time: time 156 | } 157 | renderers.push 158 | name: 'forceLayout' 159 | time: bucket.layout.layoutTime 160 | renderers.sort (a, b) -> b.time - a.time 161 | totalRenderTime = renderers.reduce ((prev, current) -> prev + current.time), 0 162 | renderers.map((d) -> "#{d.name}: #{(100 * d.time / totalRenderTime).toFixed(1)}%").join(', ') 163 | bucket 164 | 165 | currentStats = newStatsBucket() 166 | 167 | now = if window.performance and window.performance.now 168 | () -> 169 | window.performance.now() 170 | else 171 | () -> 172 | Date.now() 173 | 174 | render = -> 175 | currentStats.firstFrame = now() unless currentStats.firstFrame 176 | currentStats.frameCount++ 177 | startRender = now() 178 | geometry.onTick(graph) 179 | currentStats.geometry += (now() - startRender) 180 | 181 | nodeGroups = container.selectAll('g.node') 182 | .attr('transform', (d) -> 183 | "translate(#{ d.x },#{ d.y })") 184 | 185 | for renderer in neo.renderers.node 186 | nodeGroups.call(renderer.onTick, viz) 187 | 188 | relationshipGroups = container.selectAll('g.relationship') 189 | .attr('transform', (d) -> 190 | "translate(#{ d.source.x } #{ d.source.y }) rotate(#{ d.naturalAngle + 180 })") 191 | 192 | for renderer in neo.renderers.relationship 193 | startRenderer = now() 194 | relationshipGroups.call(renderer.onTick, viz) 195 | currentStats.relationshipRenderers[renderer.name] += (now() - startRenderer) 196 | 197 | currentStats.lastFrame = now() 198 | 199 | force = layout.init(render) 200 | 201 | #Add custom drag event listeners 202 | force.drag().on('dragstart.node', (d) -> 203 | onNodeDragToggle(d) 204 | ).on('dragend.node', () -> 205 | onNodeDragToggle() 206 | ) 207 | 208 | viz.collectStats = -> 209 | latestStats = currentStats 210 | latestStats.layout = force.collectStats() 211 | currentStats = newStatsBucket() 212 | latestStats 213 | 214 | viz.update = -> 215 | return unless graph 216 | 217 | layers = container.selectAll("g.layer").data(["relationships", "nodes"]) 218 | layers.enter().append("g") 219 | .attr("class", (d) -> "layer " + d ) 220 | 221 | nodes = graph.nodes() 222 | relationships = graph.relationships() 223 | 224 | relationshipGroups = container.select("g.layer.relationships") 225 | .selectAll("g.relationship").data(relationships, (d) -> d.id) 226 | 227 | relationshipGroups.enter().append("g") 228 | .attr("class", "relationship") 229 | .on("mousedown", onRelationshipClick) 230 | .on('mouseover', onRelMouseOver) 231 | .on('mouseout', onRelMouseOut) 232 | 233 | relationshipGroups 234 | .classed("selected", (relationship) -> relationship.selected) 235 | 236 | geometry.onGraphChange(graph) 237 | 238 | for renderer in neo.renderers.relationship 239 | relationshipGroups.call(renderer.onGraphChange, viz) 240 | 241 | relationshipGroups.exit().remove(); 242 | 243 | nodeGroups = container.select("g.layer.nodes") 244 | .selectAll("g.node").data(nodes, (d) -> d.id) 245 | 246 | nodeGroups.enter().append("g") 247 | .attr("class", "node") 248 | .call(force.drag) 249 | .call(clickHandler) 250 | .on('mouseover', onNodeMouseOver) 251 | .on('mouseout', onNodeMouseOut) 252 | 253 | nodeGroups 254 | .classed("selected", (node) -> node.selected) 255 | 256 | for renderer in neo.renderers.node 257 | nodeGroups.call(renderer.onGraphChange, viz) 258 | 259 | for renderer in neo.renderers.menu 260 | nodeGroups.call(renderer.onGraphChange, viz) 261 | 262 | nodeGroups.exit().remove(); 263 | 264 | if updateViz 265 | force.update(graph, [layoutDimension, layoutDimension]) 266 | 267 | viz.resize() 268 | viz.trigger('updated') 269 | 270 | updateViz = yes 271 | 272 | viz.resize = -> 273 | size = measureSize() 274 | root.attr('viewBox', [ 275 | 0, (layoutDimension - size.height) / 2, layoutDimension, size.height 276 | ].join(' ')) 277 | 278 | viz.boundingBox = -> 279 | container.node().getBBox() 280 | 281 | clickHandler = neo.utils.clickHandler() 282 | clickHandler.on 'click', onNodeClick 283 | clickHandler.on 'dblclick', onNodeDblClick 284 | 285 | viz 286 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2002-2017 "Neo Technology," 3 | * Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Neo4j is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | import '../../lib/visualization/neod3' 21 | import '../../lib/visualization/components/collision' 22 | import '../../lib/visualization/components/graph' 23 | import '../../lib/visualization/components/graphGeometry' 24 | import '../../lib/visualization/components/graphView' 25 | import '../../lib/visualization/components/layout' 26 | import '../../lib/visualization/components/node' 27 | import '../../lib/visualization/components/relationship' 28 | import '../../lib/visualization/components/renderer' 29 | import '../../lib/visualization/components/style' 30 | import '../../lib/visualization/components/visualization' 31 | import '../../lib/visualization/renders/init' 32 | import '../../lib/visualization/renders/menu' 33 | import '../../lib/visualization/utils/adjacentAngles' 34 | import '../../lib/visualization/utils/angleList' 35 | import '../../lib/visualization/utils/arcArrow' 36 | import '../../lib/visualization/utils/arrays' 37 | import '../../lib/visualization/utils/circularLayout' 38 | import '../../lib/visualization/utils/circumferentialDistribution' 39 | import '../../lib/visualization/utils/circumferentialRelationshipRouting' 40 | import '../../lib/visualization/utils/clickHandler' 41 | import '../../lib/visualization/utils/loopArrow' 42 | import '../../lib/visualization/utils/pairwiseArcsRelationshipRouting' 43 | import '../../lib/visualization/utils/straightArrow' 44 | import '../../lib/visualization/utils/textMeasurement' 45 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/neod3.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | window.neo = window.neo || {} 24 | 25 | neo.models = {} 26 | 27 | neo.renderers = 28 | menu: [] 29 | node: [] 30 | relationship: [] 31 | 32 | neo.utils = 33 | # Note: quick n' dirty. Only works for serializable objects 34 | copy: (src) -> 35 | JSON.parse(JSON.stringify(src)) 36 | 37 | extend: (dest, src) -> 38 | return if not neo.utils.isObject(dest) and neo.utils.isObject(src) 39 | dest[k] = v for own k, v of src 40 | return dest 41 | 42 | isArray: Array.isArray or (obj) -> 43 | Object::toString.call(obj) == '[object Array]'; 44 | 45 | isObject: (obj) -> Object(obj) is obj 46 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/renders/init.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2015 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | do -> 24 | noop = -> 25 | 26 | nodeRingStrokeSize = 8 27 | 28 | nodeOutline = new neo.Renderer( 29 | onGraphChange: (selection, viz) -> 30 | circles = selection.selectAll('circle.outline').data((node) -> [node]) 31 | 32 | circles.enter() 33 | .append('circle') 34 | .classed('outline', true) 35 | .attr 36 | cx: 0 37 | cy: 0 38 | 39 | circles 40 | .attr 41 | r: (node) -> node.radius 42 | fill: (node) -> viz.style.forNode(node).get('color') 43 | stroke: (node) -> viz.style.forNode(node).get('border-color') 44 | 'stroke-width': (node) -> viz.style.forNode(node).get('border-width') 45 | 46 | circles.exit().remove() 47 | onTick: noop 48 | ) 49 | 50 | nodeCaption = new neo.Renderer( 51 | onGraphChange: (selection, viz) -> 52 | text = selection.selectAll('text.caption').data((node) -> node.caption) 53 | 54 | text.enter().append('text') 55 | # .classed('caption', true) 56 | .attr('text-anchor': 'middle') 57 | .attr('pointer-events': 'none') 58 | 59 | text 60 | .text((line) -> line.text) 61 | .attr('y', (line) -> line.baseline ) 62 | .attr('font-size', (line) -> viz.style.forNode(line.node).get('font-size')) 63 | .attr('fill': (line) -> viz.style.forNode(line.node).get('text-color-internal')) 64 | 65 | text.exit().remove() 66 | 67 | onTick: noop 68 | ) 69 | 70 | nodeIcon = new neo.Renderer( 71 | onGraphChange: (selection, viz) -> 72 | text = selection.selectAll('text').data((node) -> node.caption) 73 | 74 | text.enter().append('text') 75 | .attr('text-anchor': 'middle') 76 | .attr('pointer-events': 'none') 77 | .attr('font-family': 'streamline') 78 | 79 | text 80 | .text((line) -> viz.style.forNode(line.node).get('icon-code')) 81 | .attr('dy', (line) -> line.node.radius/16) 82 | .attr('font-size', (line) -> line.node.radius) 83 | .attr('fill': (line) -> viz.style.forNode(line.node).get('text-color-internal')) 84 | 85 | text.exit().remove() 86 | 87 | onTick: noop 88 | ) 89 | 90 | nodeRing = new neo.Renderer( 91 | onGraphChange: (selection) -> 92 | circles = selection.selectAll('circle.ring').data((node) -> [node]) 93 | circles.enter() 94 | .insert('circle', '.outline') 95 | .classed('ring', true) 96 | .attr 97 | cx: 0 98 | cy: 0 99 | 'stroke-width': nodeRingStrokeSize + 'px' 100 | 101 | circles 102 | .attr 103 | r: (node) -> node.radius + 4 104 | 105 | circles.exit().remove() 106 | 107 | onTick: noop 108 | ) 109 | 110 | arrowPath = new neo.Renderer( 111 | name: 'arrowPath' 112 | onGraphChange: (selection, viz) -> 113 | paths = selection.selectAll('path.outline').data((rel) -> [rel]) 114 | 115 | paths.enter() 116 | .append('path') 117 | .classed('outline', true) 118 | 119 | paths 120 | .attr('fill', (rel) -> viz.style.forRelationship(rel).get('color')) 121 | .attr('stroke', 'none') 122 | 123 | paths.exit().remove() 124 | 125 | onTick: (selection) -> 126 | selection.selectAll('path') 127 | .attr('d', (d) -> d.arrow.outline(d.shortCaptionLength)) 128 | ) 129 | 130 | relationshipType = new neo.Renderer( 131 | name: 'relationshipType' 132 | onGraphChange: (selection, viz) -> 133 | texts = selection.selectAll("text").data((rel) -> [rel]) 134 | 135 | texts.enter().append("text") 136 | .attr("text-anchor": "middle") 137 | .attr('pointer-events': 'none') 138 | 139 | texts 140 | .attr('font-size', (rel) -> viz.style.forRelationship(rel).get('font-size')) 141 | .attr('fill', (rel) -> viz.style.forRelationship(rel).get('text-color-' + rel.captionLayout)) 142 | 143 | texts.exit().remove() 144 | 145 | onTick: (selection, viz) -> 146 | selection.selectAll('text') 147 | .attr('x', (rel) -> rel.arrow.midShaftPoint.x) 148 | .attr('y', (rel) -> rel.arrow.midShaftPoint.y + parseFloat(viz.style.forRelationship(rel).get('font-size')) / 2 - 1) 149 | .attr('transform', (rel) -> 150 | if rel.naturalAngle < 90 or rel.naturalAngle > 270 151 | "rotate(180 #{ rel.arrow.midShaftPoint.x } #{ rel.arrow.midShaftPoint.y })" 152 | else 153 | null) 154 | .text((rel) -> rel.shortCaption) 155 | ) 156 | 157 | relationshipOverlay = new neo.Renderer( 158 | name: 'relationshipOverlay' 159 | onGraphChange: (selection) -> 160 | rects = selection.selectAll('path.overlay').data((rel) -> [rel]) 161 | 162 | rects.enter() 163 | .append('path') 164 | .classed('overlay', true) 165 | 166 | rects.exit().remove() 167 | 168 | onTick: (selection) -> 169 | band = 16 170 | 171 | selection.selectAll('path.overlay') 172 | .attr('d', (d) -> d.arrow.overlay(band)) 173 | ) 174 | 175 | neo.renderers.node.push(nodeOutline) 176 | neo.renderers.node.push(nodeIcon) 177 | neo.renderers.node.push(nodeCaption) 178 | neo.renderers.node.push(nodeRing) 179 | neo.renderers.relationship.push(arrowPath) 180 | neo.renderers.relationship.push(relationshipType) 181 | neo.renderers.relationship.push(relationshipOverlay) 182 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/renders/menu.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2015 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | do -> 24 | noop = -> 25 | 26 | numberOfItemsInContextMenu = 3 27 | 28 | arc = (radius, itemNumber, width = 30) -> 29 | itemNumber = itemNumber - 1 30 | startAngle = ((2*Math.PI)/numberOfItemsInContextMenu) * itemNumber 31 | endAngle = startAngle + ((2*Math.PI)/numberOfItemsInContextMenu) 32 | innerRadius = Math.max(radius + 8, 20) 33 | d3.svg.arc().innerRadius(innerRadius).outerRadius(innerRadius + width).startAngle(startAngle).endAngle(endAngle).padAngle(.03) 34 | 35 | getSelectedNode = (node) -> if node.selected then [node] else [] 36 | 37 | attachContextEvent = (event, elems, viz, content, label) -> 38 | for elem in elems 39 | elem.on('mousedown.drag', -> 40 | d3.event.stopPropagation() 41 | null) 42 | elem.on('mouseup', (node) -> 43 | viz.trigger(event, node)) 44 | elem.on('mouseover', (node) -> 45 | node.contextMenu = 46 | menuSelection: event 47 | menuContent: content 48 | label:label 49 | viz.trigger('menuMouseOver', node)) 50 | elem.on('mouseout', (node) -> 51 | delete node.contextMenu 52 | viz.trigger('menuMouseOut', node)) 53 | 54 | createMenuItem = (selection, viz, eventName, itemNumber, className, position, textValue, helpValue) -> 55 | path = selection.selectAll('path.' + className).data(getSelectedNode) 56 | textpath = selection.selectAll('text.' + className).data(getSelectedNode) 57 | 58 | tab = path.enter() 59 | .append('path') 60 | .classed(className, true) 61 | .classed('context-menu-item', true) 62 | .attr 63 | d: (node) -> arc(node.radius, itemNumber, 1)() 64 | 65 | text = textpath.enter() 66 | .append('text') 67 | .classed('context-menu-item', true) 68 | .text(textValue) 69 | .attr("transform", "scale(0.1)") 70 | .attr 71 | 'font-family': 'FontAwesome' 72 | fill: (node) -> viz.style.forNode(node).get('text-color-internal') 73 | x: (node) -> arc(node.radius, itemNumber).centroid()[0] + position[0] 74 | y: (node) -> arc(node.radius, itemNumber).centroid()[1] + position[1] 75 | 76 | attachContextEvent(eventName, [tab, text], viz, helpValue, textValue) 77 | 78 | tab 79 | .transition() 80 | .duration(200) 81 | .attr 82 | d: (node) -> arc(node.radius, itemNumber)() 83 | 84 | text 85 | .attr("transform", "scale(1)") 86 | 87 | path 88 | .exit() 89 | .transition() 90 | .duration(200) 91 | .attr 92 | d: (node) -> arc(node.radius, itemNumber, 1)() 93 | .remove() 94 | 95 | textpath 96 | .exit() 97 | .attr("transform", "scale(0)") 98 | .remove() 99 | 100 | donutRemoveNode = new neo.Renderer( 101 | onGraphChange: (selection, viz) -> createMenuItem(selection, viz, 'nodeClose', 1, 'remove_node', [-4, 0], '\uf00d', 'Remove node from the visualization') 102 | 103 | onTick: noop 104 | ) 105 | 106 | donutExpandNode = new neo.Renderer( 107 | onGraphChange: (selection, viz) -> createMenuItem(selection, viz, 'nodeDblClicked', 2, 'expand_node', [0, 4], '\uf0b2', 'Expand child relationships') 108 | 109 | onTick: noop 110 | ) 111 | 112 | donutUnlockNode = new neo.Renderer( 113 | onGraphChange: (selection, viz) -> createMenuItem(selection, viz, 'nodeUnlock', 3, 'unlock_node', [4, 0], '\uf09c', 'Unlock the node to re-layout the graph') 114 | 115 | onTick: noop 116 | ) 117 | 118 | neo.renderers.menu.push(donutExpandNode) 119 | neo.renderers.menu.push(donutRemoveNode) 120 | neo.renderers.menu.push(donutUnlockNode) 121 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/adjacentAngles.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.utils.adjacentAngles 24 | 25 | findRuns: (angleList, minSeparation) -> 26 | 27 | p = 0 28 | start = 0 29 | end = 0 30 | runs = [] 31 | minStart = () -> 32 | if runs.length == 0 33 | 0 34 | else 35 | runs[0].start 36 | 37 | scanForDensePair = -> 38 | start = p 39 | end = angleList.wrapIndex(p + 1) 40 | if end == minStart() 41 | 'done' 42 | else 43 | p = end 44 | if tooDense(start, end) 45 | extendEnd 46 | 47 | else 48 | scanForDensePair 49 | 50 | extendEnd = -> 51 | if p == minStart() 52 | 'done' 53 | 54 | else if tooDense(start, angleList.wrapIndex(p + 1)) 55 | end = angleList.wrapIndex(p + 1) 56 | p = end 57 | extendEnd 58 | 59 | else 60 | p = start 61 | extendStart 62 | 63 | extendStart = -> 64 | candidateStart = angleList.wrapIndex(p - 1) 65 | if tooDense(candidateStart, end) and candidateStart != end 66 | start = candidateStart 67 | p = start 68 | extendStart 69 | 70 | else 71 | runs.push 72 | start: start 73 | end: end 74 | p = end 75 | scanForDensePair 76 | 77 | tooDense = (start, end) -> 78 | run = 79 | start: start 80 | end: end 81 | angleList.angle(run) < angleList.length(run) * minSeparation 82 | 83 | stepCount = 0 84 | step = scanForDensePair 85 | while step != 'done' 86 | if stepCount++ > angleList.totalLength() * 10 87 | console.log 'Warning: failed to layout arrows', ("#{ key }: #{ value.angle }" for own key, value of angleList.list).join('\n'), minSeparation 88 | break 89 | step = step() 90 | 91 | runs -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/angleList.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.utils.angleList 24 | 25 | constructor: (@list) -> 26 | 27 | getAngle: (index) -> 28 | @list[index].angle 29 | 30 | fixed: (index) -> 31 | @list[index].fixed 32 | 33 | totalLength: -> 34 | @list.length 35 | 36 | length: (run) -> 37 | if run.start < run.end 38 | run.end - run.start 39 | else 40 | run.end + @list.length - run.start 41 | 42 | angle: (run) -> 43 | if run.start < run.end 44 | @list[run.end].angle - @list[run.start].angle 45 | else 46 | 360 - (@list[run.start].angle - @list[run.end].angle) 47 | 48 | wrapIndex: (index) -> 49 | if index == -1 50 | @list.length - 1 51 | else if index >= @list.length 52 | index - @list.length 53 | else 54 | index -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/arcArrow.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.utils.arcArrow 24 | 25 | constructor: (startRadius, endRadius, endCentre, @deflection, arrowWidth, headWidth, headLength, captionLayout) -> 26 | square = (l) -> 27 | l * l 28 | 29 | deflectionRadians = @deflection * Math.PI / 180 30 | startAttach = 31 | x: Math.cos( deflectionRadians ) * (startRadius), 32 | y: Math.sin( deflectionRadians ) * (startRadius) 33 | 34 | radiusRatio = startRadius / (endRadius + headLength) 35 | homotheticCenter = -endCentre * radiusRatio / (1 - radiusRatio) 36 | 37 | intersectWithOtherCircle = (fixedPoint, radius, xCenter, polarity) -> 38 | gradient = fixedPoint.y / (fixedPoint.x - homotheticCenter) 39 | hc = fixedPoint.y - gradient * fixedPoint.x 40 | 41 | A = 1 + square(gradient) 42 | B = 2 * (gradient * hc - xCenter) 43 | C = square(hc) + square(xCenter) - square(radius) 44 | 45 | intersection = { x: (-B + polarity * Math.sqrt(square(B) - 4 * A * C)) / (2 * A) } 46 | intersection.y = (intersection.x - homotheticCenter) * gradient 47 | 48 | intersection 49 | 50 | endAttach = intersectWithOtherCircle(startAttach, endRadius + headLength, endCentre, -1) 51 | 52 | g1 = -startAttach.x / startAttach.y 53 | c1 = startAttach.y + (square(startAttach.x) / startAttach.y) 54 | g2 = -(endAttach.x - endCentre) / endAttach.y 55 | c2 = endAttach.y + (endAttach.x - endCentre) * endAttach.x / endAttach.y 56 | 57 | cx = ( c1 - c2 ) / (g2 - g1) 58 | cy = g1 * cx + c1 59 | 60 | arcRadius = Math.sqrt(square(cx - startAttach.x) + square(cy - startAttach.y)) 61 | startAngle = Math.atan2(startAttach.x - cx, cy - startAttach.y) 62 | endAngle = Math.atan2(endAttach.x - cx, cy - endAttach.y) 63 | sweepAngle = endAngle - startAngle 64 | if @deflection > 0 65 | sweepAngle = 2 * Math.PI - sweepAngle 66 | 67 | @shaftLength = sweepAngle * arcRadius 68 | if startAngle > endAngle 69 | @shaftLength = 0 70 | 71 | midShaftAngle = (startAngle + endAngle) / 2 72 | if @deflection > 0 73 | midShaftAngle += Math.PI 74 | @midShaftPoint = 75 | x: cx + arcRadius * Math.sin(midShaftAngle) 76 | y: cy - arcRadius * Math.cos(midShaftAngle) 77 | 78 | startTangent = (dr) -> 79 | dx = (if dr < 0 then 1 else -1) * Math.sqrt(square(dr) / (1 + square(g1))) 80 | dy = g1 * dx 81 | { 82 | x: startAttach.x + dx, 83 | y: startAttach.y + dy 84 | } 85 | 86 | endTangent = (dr) -> 87 | dx = (if dr < 0 then -1 else 1) * Math.sqrt(square(dr) / (1 + square(g2))) 88 | dy = g2 * dx 89 | { 90 | x: endAttach.x + dx, 91 | y: endAttach.y + dy 92 | } 93 | 94 | angleTangent = (angle, dr) -> 95 | { 96 | x: cx + (arcRadius + dr) * Math.sin(angle), 97 | y: cy - (arcRadius + dr) * Math.cos(angle) 98 | } 99 | 100 | endNormal = (dc) -> 101 | dx = (if dc < 0 then -1 else 1) * Math.sqrt(square(dc) / (1 + square(1 / g2))) 102 | dy = dx / g2 103 | { 104 | x: endAttach.x + dx 105 | y: endAttach.y - dy 106 | } 107 | 108 | endOverlayCorner = (dr, dc) -> 109 | shoulder = endTangent(dr) 110 | arrowTip = endNormal(dc) 111 | { 112 | x: shoulder.x + arrowTip.x - endAttach.x, 113 | y: shoulder.y + arrowTip.y - endAttach.y 114 | } 115 | 116 | coord = (point) -> 117 | "#{point.x},#{point.y}" 118 | 119 | shaftRadius = arrowWidth / 2 120 | headRadius = headWidth / 2 121 | positiveSweep = if startAttach.y > 0 then 0 else 1 122 | negativeSweep = if startAttach.y < 0 then 0 else 1 123 | 124 | @outline = (shortCaptionLength) -> 125 | if startAngle > endAngle 126 | return [ 127 | 'M', coord(endTangent(-headRadius)), 128 | 'L', coord(endNormal(headLength)), 129 | 'L', coord(endTangent(headRadius)), 130 | 'Z' 131 | ].join(' ') 132 | 133 | if captionLayout is 'external' 134 | captionSweep = shortCaptionLength / arcRadius 135 | if @deflection > 0 136 | captionSweep *= -1 137 | 138 | startBreak = midShaftAngle - captionSweep / 2 139 | endBreak = midShaftAngle + captionSweep / 2 140 | 141 | [ 142 | 'M', coord(startTangent(shaftRadius)), 143 | 'L', coord(startTangent(-shaftRadius)), 144 | 'A', arcRadius - shaftRadius, arcRadius - shaftRadius, 0, 0, positiveSweep, coord(angleTangent(startBreak, -shaftRadius)), 145 | 'L', coord(angleTangent(startBreak, shaftRadius)), 146 | 'A', arcRadius + shaftRadius, arcRadius + shaftRadius, 0, 0, negativeSweep, coord(startTangent(shaftRadius)) 147 | 'Z', 148 | 'M', coord(angleTangent(endBreak, shaftRadius)), 149 | 'L', coord(angleTangent(endBreak, -shaftRadius)), 150 | 'A', arcRadius - shaftRadius, arcRadius - shaftRadius, 0, 0, positiveSweep, coord(endTangent(-shaftRadius)), 151 | 'L', coord(endTangent(-headRadius)), 152 | 'L', coord(endNormal(headLength)), 153 | 'L', coord(endTangent(headRadius)), 154 | 'L', coord(endTangent(shaftRadius)), 155 | 'A', arcRadius + shaftRadius, arcRadius + shaftRadius, 0, 0, negativeSweep, coord(angleTangent(endBreak, shaftRadius)) 156 | ].join(' ') 157 | else 158 | [ 159 | 'M', coord(startTangent(shaftRadius)), 160 | 'L', coord(startTangent(-shaftRadius)), 161 | 'A', arcRadius - shaftRadius, arcRadius - shaftRadius, 0, 0, positiveSweep, coord(endTangent(-shaftRadius)), 162 | 'L', coord(endTangent(-headRadius)), 163 | 'L', coord(endNormal(headLength)), 164 | 'L', coord(endTangent(headRadius)), 165 | 'L', coord(endTangent(shaftRadius)), 166 | 'A', arcRadius + shaftRadius, arcRadius + shaftRadius, 0, 0, negativeSweep, coord(startTangent(shaftRadius)) 167 | ].join(' ') 168 | 169 | @overlay = (minWidth) -> 170 | radius = Math.max(minWidth / 2, shaftRadius) 171 | 172 | [ 173 | 'M', coord(startTangent(radius)), 174 | 'L', coord(startTangent(-radius)), 175 | 'A', arcRadius - radius, arcRadius - radius, 0, 0, positiveSweep, coord(endTangent(-radius)), 176 | 'L', coord(endOverlayCorner(-radius, headLength)), 177 | 'L', coord(endOverlayCorner(radius, headLength)), 178 | 'L', coord(endTangent(radius)), 179 | 'A', arcRadius + radius, arcRadius + radius, 0, 0, negativeSweep, coord(startTangent(radius)) 180 | ].join(' ') 181 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/arrays.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | neo.utils.cloneArray = (original) -> 24 | clone = new Array(original.length) 25 | clone[idx] = node for node, idx in original 26 | clone -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/circularLayout.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | neo.utils.circularLayout = (nodes, center, radius) -> 24 | unlocatedNodes = [] 25 | unlocatedNodes.push(node) for node in nodes when !(node.x? and node.y?) 26 | for n, i in unlocatedNodes 27 | n.x = center.x + radius * Math.sin(2 * Math.PI * i / unlocatedNodes.length) 28 | n.y = center.y + radius * Math.cos(2 * Math.PI * i / unlocatedNodes.length) 29 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/circumferentialDistribution.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | neo.utils.distributeCircular = (arrowAngles, minSeparation) -> 24 | list = [] 25 | for key, angle of arrowAngles.floating 26 | list.push 27 | key: key 28 | angle: angle 29 | fixed: false 30 | for key, angle of arrowAngles.fixed 31 | list.push 32 | key: key 33 | angle: angle 34 | fixed: true 35 | 36 | list.sort((a, b) -> a.angle - b.angle) 37 | 38 | angleList = new neo.utils.angleList(list) 39 | runsOfTooDenseArrows = new neo.utils.adjacentAngles().findRuns(angleList, minSeparation) 40 | 41 | wrapAngle = (angle) -> 42 | if angle >= 360 43 | angle - 360 44 | else if angle < 0 45 | angle + 360 46 | else 47 | angle 48 | 49 | result = {} 50 | 51 | splitByFixedArrows = (run) -> 52 | runs = [] 53 | currentStart = run.start 54 | for i in [1..angleList.length(run)] 55 | wrapped = angleList.wrapIndex(run.start + i) 56 | if angleList.fixed(wrapped) 57 | runs.push 58 | start: currentStart 59 | end: wrapped 60 | currentStart = wrapped 61 | if not angleList.fixed(run.end) 62 | runs.push 63 | start: currentStart 64 | end: run.end 65 | runs 66 | 67 | for tooDenseRun in runsOfTooDenseArrows 68 | moveableRuns = splitByFixedArrows(tooDenseRun) 69 | for run in moveableRuns 70 | runLength = angleList.length(run) 71 | if angleList.fixed(run.start) and angleList.fixed(run.end) 72 | separation = angleList.angle(run) / runLength 73 | for i in [0..runLength] 74 | rawAngle = list[run.start].angle + i * separation 75 | result[list[angleList.wrapIndex(run.start + i)].key] = wrapAngle(rawAngle) 76 | else if angleList.fixed(run.start) and not angleList.fixed(run.end) 77 | for i in [0..runLength] 78 | rawAngle = list[run.start].angle + i * minSeparation 79 | result[list[angleList.wrapIndex(run.start + i)].key] = wrapAngle(rawAngle) 80 | else if not angleList.fixed(run.start) and angleList.fixed(run.end) 81 | for i in [0..runLength] 82 | rawAngle = list[run.end].angle - (runLength - i) * minSeparation 83 | result[list[angleList.wrapIndex(run.start + i)].key] = wrapAngle(rawAngle) 84 | else 85 | center = list[run.start].angle + angleList.angle(run) / 2 86 | for i in [0..runLength] 87 | rawAngle = center + (i - runLength / 2) * minSeparation 88 | result[list[angleList.wrapIndex(run.start + i)].key] = wrapAngle(rawAngle) 89 | 90 | 91 | for key of arrowAngles.floating 92 | if !result.hasOwnProperty(key) 93 | result[key] = arrowAngles.floating[key] 94 | 95 | for key of arrowAngles.fixed 96 | if !result.hasOwnProperty(key) 97 | result[key] = arrowAngles.fixed[key] 98 | 99 | result -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/circumferentialRelationshipRouting.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.utils.circumferentialRelationshipRouting 24 | constructor: (@style) -> 25 | 26 | measureRelationshipCaption: (relationship, caption) -> 27 | fontFamily = 'sans-serif' 28 | fontSize = parseFloat(@style.forRelationship(relationship).get('font-size')) 29 | padding = parseFloat(@style.forRelationship(relationship).get('padding')) 30 | neo.utils.measureText(caption, fontFamily, fontSize) + padding * 2 31 | 32 | captionFitsInsideArrowShaftWidth: (relationship) -> 33 | parseFloat(@style.forRelationship(relationship).get('shaft-width')) > 34 | parseFloat(@style.forRelationship(relationship).get('font-size')) 35 | 36 | measureRelationshipCaptions: (relationships) -> 37 | for relationship in relationships 38 | relationship.captionLength = @measureRelationshipCaption(relationship, relationship.type) 39 | relationship.captionLayout = 40 | if @captionFitsInsideArrowShaftWidth(relationship) 41 | "internal" 42 | else 43 | "external" 44 | 45 | shortenCaption: (relationship, caption, targetWidth) -> 46 | shortCaption = caption 47 | while true 48 | if shortCaption.length <= 2 49 | return ['', 0] 50 | shortCaption = shortCaption.substr(0, shortCaption.length - 2) + '\u2026' 51 | width = @measureRelationshipCaption(relationship, shortCaption) 52 | if width < targetWidth 53 | return [shortCaption, width] 54 | layoutRelationships: (graph) -> 55 | for relationship in graph.relationships() 56 | dx = relationship.target.x - relationship.source.x 57 | dy = relationship.target.y - relationship.source.y 58 | relationship.naturalAngle = ((Math.atan2(dy, dx) / Math.PI * 180) + 180) % 360 59 | delete relationship.arrow 60 | 61 | sortedNodes = graph.nodes().sort((a, b) -> 62 | b.relationshipCount(graph) - a.relationshipCount(graph)) 63 | 64 | for node in sortedNodes 65 | relationships = [] 66 | relationships.push(relationship) for relationship in graph.relationships() when relationship.source is node or relationship.target is node 67 | 68 | arrowAngles = { floating: {}, fixed: {} } 69 | relationshipMap = {} 70 | for relationship in relationships 71 | relationshipMap[relationship.id] = relationship 72 | 73 | if node == relationship.source 74 | if relationship.hasOwnProperty('arrow') 75 | arrowAngles.fixed[relationship.id] = relationship.naturalAngle + relationship.arrow.deflection 76 | else 77 | arrowAngles.floating[relationship.id] = relationship.naturalAngle 78 | if node == relationship.target 79 | if relationship.hasOwnProperty('arrow') 80 | arrowAngles.fixed[relationship.id] = (relationship.naturalAngle - relationship.arrow.deflection + 180) % 360 81 | else 82 | arrowAngles.floating[relationship.id] = (relationship.naturalAngle + 180) % 360 83 | 84 | distributedAngles = {} 85 | for id, angle of arrowAngles.floating 86 | distributedAngles[id] = angle 87 | for id, angle of arrowAngles.fixed 88 | distributedAngles[id] = angle 89 | 90 | if (relationships.length > 1) 91 | distributedAngles = neo.utils.distributeCircular(arrowAngles, 30) 92 | 93 | for id, angle of distributedAngles 94 | relationship = relationshipMap[id] 95 | if not relationship.hasOwnProperty('arrow') 96 | deflection = if node == relationship.source 97 | angle - relationship.naturalAngle 98 | else 99 | (relationship.naturalAngle - angle + 180) % 360 100 | 101 | shaftRadius = (parseFloat(@style.forRelationship(relationship).get('shaft-width')) / 2) or 2 102 | headRadius = shaftRadius + 3 103 | headHeight = headRadius * 2 104 | 105 | dx = relationship.target.x - relationship.source.x 106 | dy = relationship.target.y - relationship.source.y 107 | 108 | square = (distance) -> distance * distance 109 | centreDistance = Math.sqrt(square(dx) + square(dy)) 110 | 111 | if Math.abs(deflection) < Math.PI / 180 112 | relationship.arrow = new neo.utils.straightArrow( 113 | relationship.source.radius, 114 | relationship.target.radius, 115 | centreDistance, 116 | shaftRadius, 117 | headRadius, 118 | headHeight, 119 | relationship.captionLayout 120 | ) 121 | else 122 | relationship.arrow = new neo.utils.arcArrow( 123 | relationship.source.radius, 124 | relationship.target.radius, 125 | centreDistance, 126 | deflection, 127 | shaftRadius * 2, 128 | headRadius * 2, 129 | headHeight, 130 | relationship.captionLayout 131 | ) 132 | 133 | [relationship.shortCaption, relationship.shortCaptionLength] = if relationship.arrow.shaftLength > relationship.captionLength 134 | [relationship.caption, relationship.captionLength] 135 | else 136 | @shortenCaption(relationship, relationship.caption, relationship.arrow.shaftLength) 137 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/clickHandler.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | neo.utils.clickHandler = -> 24 | cc = (selection) -> 25 | 26 | # euclidean distance 27 | dist = (a, b) -> 28 | Math.sqrt Math.pow(a[0] - b[0], 2), Math.pow(a[1] - b[1], 2) 29 | down = undefined 30 | tolerance = 5 31 | last = undefined 32 | wait = null 33 | selection.on "mousedown", -> 34 | d3.event.target.__data__.fixed = yes 35 | down = d3.mouse(document.body) 36 | last = +new Date() 37 | d3.event.stopPropagation() 38 | 39 | selection.on "mouseup", -> 40 | if dist(down, d3.mouse(document.body)) > tolerance 41 | return 42 | else 43 | if wait 44 | window.clearTimeout wait 45 | wait = null 46 | event.dblclick d3.event.target.__data__ 47 | else 48 | event.click d3.event.target.__data__ 49 | wait = window.setTimeout(((e) -> 50 | -> 51 | wait = null 52 | )(d3.event), 250) 53 | 54 | event = d3.dispatch("click", "dblclick") 55 | d3.rebind cc, event, "on" 56 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/loopArrow.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.utils.loopArrow 24 | 25 | constructor: (nodeRadius, straightLength, spreadDegrees, shaftWidth, headWidth, headLength, captionHeight) -> 26 | 27 | spread = spreadDegrees * Math.PI / 180 28 | r1 = nodeRadius 29 | r2 = nodeRadius + headLength 30 | r3 = nodeRadius + straightLength 31 | loopRadius = r3 * Math.tan(spread / 2) 32 | shaftRadius = shaftWidth / 2 33 | @shaftLength = loopRadius * 3 + shaftWidth 34 | 35 | class Point 36 | constructor: (@x, @y) -> 37 | toString: -> 38 | "#{@x} #{@y}" 39 | 40 | normalPoint = (sweep, radius, displacement) -> 41 | localLoopRadius = radius * Math.tan(spread / 2) 42 | cy = radius / Math.cos(spread / 2) 43 | new Point( 44 | (localLoopRadius + displacement) * Math.sin(sweep), 45 | cy + (localLoopRadius + displacement) * Math.cos(sweep) 46 | ) 47 | @midShaftPoint = normalPoint(0, r3, shaftRadius + captionHeight / 2 + 2) 48 | startPoint = (radius, displacement) -> 49 | normalPoint((Math.PI + spread) / 2, radius, displacement) 50 | endPoint = (radius, displacement) -> 51 | normalPoint(-(Math.PI + spread) / 2, radius, displacement) 52 | 53 | @outline = -> 54 | inner = loopRadius - shaftRadius 55 | outer = loopRadius + shaftRadius 56 | [ 57 | 'M', startPoint(r1, shaftRadius) 58 | 'L', startPoint(r3, shaftRadius) 59 | 'A', outer, outer, 0, 1, 1, endPoint(r3, shaftRadius) 60 | 'L', endPoint(r2, shaftRadius) 61 | 'L', endPoint(r2, -headWidth / 2) 62 | 'L', endPoint(r1, 0) 63 | 'L', endPoint(r2, headWidth / 2) 64 | 'L', endPoint(r2, -shaftRadius) 65 | 'L', endPoint(r3, -shaftRadius) 66 | 'A', inner, inner, 0, 1, 0, startPoint(r3, -shaftRadius) 67 | 'L', startPoint(r1, -shaftRadius) 68 | 'Z' 69 | ].join(' ') 70 | 71 | @overlay = (minWidth) -> 72 | displacement = Math.max(minWidth / 2, shaftRadius) 73 | inner = loopRadius - displacement 74 | outer = loopRadius + displacement 75 | [ 76 | 'M', startPoint(r1, displacement) 77 | 'L', startPoint(r3, displacement) 78 | 'A', outer, outer, 0, 1, 1, endPoint(r3, displacement) 79 | 'L', endPoint(r2, displacement) 80 | 'L', endPoint(r2, -displacement) 81 | 'L', endPoint(r3, -displacement) 82 | 'A', inner, inner, 0, 1, 0, startPoint(r3, -displacement) 83 | 'L', startPoint(r1, -displacement) 84 | 'Z' 85 | ].join(' ') -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/pairwiseArcsRelationshipRouting.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.utils.pairwiseArcsRelationshipRouting 24 | constructor: (@style) -> 25 | 26 | measureRelationshipCaption: (relationship, caption) -> 27 | fontFamily = 'sans-serif' 28 | padding = parseFloat(@style.forRelationship(relationship).get('padding')) 29 | neo.utils.measureText(caption, fontFamily, relationship.captionHeight) + padding * 2 30 | 31 | captionFitsInsideArrowShaftWidth: (relationship) -> 32 | parseFloat(@style.forRelationship(relationship).get('shaft-width')) > relationship.captionHeight 33 | 34 | measureRelationshipCaptions: (relationships) -> 35 | for relationship in relationships 36 | relationship.captionHeight = parseFloat(@style.forRelationship(relationship).get('font-size')) 37 | relationship.captionLength = @measureRelationshipCaption(relationship, relationship.caption) 38 | relationship.captionLayout = 39 | if @captionFitsInsideArrowShaftWidth(relationship) and not relationship.isLoop() 40 | "internal" 41 | else 42 | "external" 43 | 44 | shortenCaption: (relationship, caption, targetWidth) -> 45 | shortCaption = caption || 'caption' 46 | while true 47 | if shortCaption.length <= 2 48 | return ['', 0] 49 | shortCaption = shortCaption.substr(0, shortCaption.length - 2) + '\u2026' 50 | width = @measureRelationshipCaption(relationship, shortCaption) 51 | if width < targetWidth 52 | return [shortCaption, width] 53 | 54 | computeGeometryForNonLoopArrows: (nodePairs) -> 55 | square = (distance) -> distance * distance 56 | for nodePair in nodePairs 57 | if not nodePair.isLoop() 58 | dx = nodePair.nodeA.x - nodePair.nodeB.x 59 | dy = nodePair.nodeA.y - nodePair.nodeB.y 60 | angle = ((Math.atan2(dy, dx) / Math.PI * 180) + 360) % 360 61 | centreDistance = Math.sqrt(square(dx) + square(dy)) 62 | for relationship in nodePair.relationships 63 | relationship.naturalAngle = if relationship.target is nodePair.nodeA 64 | (angle + 180) % 360 65 | else 66 | angle 67 | relationship.centreDistance = centreDistance 68 | 69 | distributeAnglesForLoopArrows: (nodePairs, relationships) -> 70 | for nodePair in nodePairs 71 | if nodePair.isLoop() 72 | angles = [] 73 | node = nodePair.nodeA 74 | for relationship in relationships 75 | if not relationship.isLoop() 76 | if relationship.source is node 77 | angles.push relationship.naturalAngle 78 | if relationship.target is node 79 | angles.push relationship.naturalAngle + 180 80 | angles = angles.map((a) -> (a + 360) % 360).sort((a, b) -> a - b) 81 | if angles.length > 0 82 | biggestGap = 83 | start: 0 84 | end: 0 85 | for angle, i in angles 86 | start = angle 87 | end = if i == angles.length - 1 88 | angles[0] + 360 89 | else 90 | angles[i + 1] 91 | if end - start > biggestGap.end - biggestGap.start 92 | biggestGap.start = start 93 | biggestGap.end = end 94 | separation = (biggestGap.end - biggestGap.start) / (nodePair.relationships.length + 1) 95 | for relationship, i in nodePair.relationships 96 | relationship.naturalAngle = (biggestGap.start + (i + 1) * separation - 90) % 360 97 | else 98 | separation = 360 / nodePair.relationships.length 99 | for relationship, i in nodePair.relationships 100 | relationship.naturalAngle = i * separation 101 | 102 | layoutRelationships: (graph) -> 103 | nodePairs = graph.groupedRelationships() 104 | @computeGeometryForNonLoopArrows(nodePairs) 105 | @distributeAnglesForLoopArrows(nodePairs, graph.relationships()) 106 | 107 | for nodePair in nodePairs 108 | for relationship in nodePair.relationships 109 | delete relationship.arrow 110 | 111 | middleRelationshipIndex = (nodePair.relationships.length - 1) / 2 112 | defaultDeflectionStep = 30 113 | maximumTotalDeflection = 150 114 | numberOfSteps = nodePair.relationships.length - 1 115 | totalDeflection = defaultDeflectionStep * numberOfSteps 116 | 117 | deflectionStep = if totalDeflection > maximumTotalDeflection then maximumTotalDeflection/numberOfSteps else defaultDeflectionStep 118 | 119 | for relationship, i in nodePair.relationships 120 | 121 | shaftWidth = parseFloat(@style.forRelationship(relationship).get('shaft-width')) or 2 122 | headWidth = shaftWidth + 6 123 | headHeight = headWidth 124 | 125 | if nodePair.isLoop() 126 | relationship.arrow = new neo.utils.loopArrow( 127 | relationship.source.radius, 128 | 40, 129 | defaultDeflectionStep, 130 | shaftWidth, 131 | headWidth, 132 | headHeight, 133 | relationship.captionHeight 134 | ) 135 | else 136 | if i == middleRelationshipIndex 137 | relationship.arrow = new neo.utils.straightArrow( 138 | relationship.source.radius, 139 | relationship.target.radius, 140 | relationship.centreDistance, 141 | shaftWidth, 142 | headWidth, 143 | headHeight, 144 | relationship.captionLayout 145 | ) 146 | else 147 | deflection = deflectionStep * (i - middleRelationshipIndex) 148 | 149 | if nodePair.nodeA isnt relationship.source 150 | deflection *= -1 151 | 152 | relationship.arrow = new neo.utils.arcArrow( 153 | relationship.source.radius, 154 | relationship.target.radius, 155 | relationship.centreDistance, 156 | deflection, 157 | shaftWidth, 158 | headWidth, 159 | headHeight, 160 | relationship.captionLayout 161 | ) 162 | 163 | [relationship.shortCaption, relationship.shortCaptionLength] = if relationship.arrow.shaftLength > relationship.captionLength 164 | [relationship.caption, relationship.captionLength] 165 | else 166 | @shortenCaption(relationship, relationship.caption, relationship.arrow.shaftLength) 167 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/straightArrow.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | class neo.utils.straightArrow 24 | 25 | constructor: (startRadius, endRadius, centreDistance, shaftWidth, headWidth, headHeight, captionLayout) -> 26 | 27 | @length = centreDistance - (startRadius + endRadius) 28 | 29 | @shaftLength = @length - headHeight 30 | startArrow = startRadius 31 | endShaft = startArrow + @shaftLength 32 | endArrow = startArrow + @length 33 | shaftRadius = shaftWidth / 2 34 | headRadius = headWidth / 2 35 | 36 | @midShaftPoint = 37 | x: startArrow + @shaftLength / 2 38 | y: 0 39 | 40 | @outline = (shortCaptionLength) -> 41 | if captionLayout is "external" 42 | startBreak = startArrow + (@shaftLength - shortCaptionLength) / 2 43 | endBreak = endShaft - (@shaftLength - shortCaptionLength) / 2 44 | 45 | [ 46 | 'M', startArrow, shaftRadius, 47 | 'L', startBreak, shaftRadius, 48 | 'L', startBreak, -shaftRadius, 49 | 'L', startArrow, -shaftRadius, 50 | 'Z' 51 | 'M', endBreak, shaftRadius, 52 | 'L', endShaft, shaftRadius, 53 | 'L', endShaft, headRadius, 54 | 'L', endArrow, 0, 55 | 'L', endShaft, -headRadius, 56 | 'L', endShaft, -shaftRadius, 57 | 'L', endBreak, -shaftRadius, 58 | 'Z' 59 | ].join(' ') 60 | else 61 | [ 62 | 'M', startArrow, shaftRadius, 63 | 'L', endShaft, shaftRadius, 64 | 'L', endShaft, headRadius, 65 | 'L', endArrow, 0, 66 | 'L', endShaft, -headRadius, 67 | 'L', endShaft, -shaftRadius, 68 | 'L', startArrow, -shaftRadius, 69 | 'Z' 70 | ].join(' ') 71 | 72 | @overlay = (minWidth) -> 73 | radius = Math.max(minWidth / 2, shaftRadius) 74 | [ 75 | 'M', startArrow, radius, 76 | 'L', endArrow, radius, 77 | 'L', endArrow, -radius, 78 | 'L', startArrow, -radius, 79 | 'Z' 80 | ].join(' ') 81 | 82 | deflection: 0 83 | -------------------------------------------------------------------------------- /src/components/D3Visualization/lib/visualization/utils/textMeasurement.coffee: -------------------------------------------------------------------------------- 1 | ###! 2 | Copyright (c) 2002-2017 "Neo Technology," 3 | Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | 5 | This file is part of Neo4j. 6 | 7 | Neo4j is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ### 20 | 21 | 'use strict' 22 | 23 | neo.utils.measureText = do -> 24 | measureUsingCanvas = (text, font) -> 25 | canvasSelection = d3.select('canvas#textMeasurementCanvas').data([this]) 26 | canvasSelection.enter().append('canvas') 27 | .attr('id', 'textMeasurementCanvas') 28 | .style('display', 'none') 29 | 30 | canvas = canvasSelection.node() 31 | context = canvas.getContext('2d') 32 | context.font = font 33 | context.measureText(text).width 34 | 35 | cache = do () -> 36 | cacheSize = 10000 37 | map = {} 38 | list = [] 39 | (key, calc) -> 40 | cached = map[key] 41 | if cached 42 | cached 43 | else 44 | result = calc() 45 | if (list.length > cacheSize) 46 | delete map[list.splice(0, 1)] 47 | list.push(key) 48 | map[key] = result 49 | 50 | return (text, fontFamily, fontSize) -> 51 | font = 'normal normal normal ' + fontSize + 'px/normal ' + fontFamily; 52 | cache(text + font, () -> 53 | measureUsingCanvas(text, font) 54 | ) 55 | -------------------------------------------------------------------------------- /src/components/D3Visualization/mapper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2002-2017 "Neo Technology," 3 | * Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Neo4j is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | export function createGraph (nodes, relationships) { 22 | let graph = new neo.models.Graph() 23 | graph.addNodes(mapNodes(nodes)) 24 | graph.addRelationships(mapRelationships(relationships, graph)) 25 | graph.display = { initialNodeDisplay: 300, nodeCount: 1 } 26 | return graph 27 | } 28 | 29 | export function mapNodes (nodes) { 30 | return nodes.map((node) => new neo.models.Node(node.id, node.labels, node.properties)) 31 | } 32 | 33 | export function mapRelationships (relationships, graph) { 34 | return relationships.map((rel) => { 35 | const source = graph.findNode(rel.startNodeId) 36 | const target = graph.findNode(rel.endNodeId) 37 | return new neo.models.Relationship(rel.id, source, target, rel.type, rel.properties) 38 | }) 39 | } 40 | 41 | export function getGraphStats (graph) { 42 | let labelStats = {} 43 | let relTypeStats = {} 44 | 45 | graph.nodes().forEach((node) => { 46 | node.labels.forEach((label) => { 47 | if (labelStats['*']) { 48 | labelStats['*'].count = labelStats['*'].count + 1 49 | } else { 50 | labelStats['*'] = { 51 | count: 1, 52 | properties: [] 53 | } 54 | } 55 | if (labelStats[label]) { 56 | labelStats[label].count = labelStats[label].count + 1 57 | labelStats[label].properties = Object.assign({}, labelStats[label].properties, node.propertyMap) 58 | } else { 59 | labelStats[label] = { 60 | count: 1, 61 | properties: node.propertyMap 62 | } 63 | } 64 | }) 65 | }) 66 | graph.relationships().forEach((rel) => { 67 | if (relTypeStats['*']) { 68 | relTypeStats['*'].count = relTypeStats['*'].count + 1 69 | } else { 70 | relTypeStats['*'] = { 71 | count: 1, 72 | properties: [] 73 | } 74 | } 75 | if (relTypeStats[rel.type]) { 76 | relTypeStats[rel.type].count = relTypeStats[rel.type].count + 1 77 | relTypeStats[rel.type].properties = Object.assign({}, relTypeStats[rel.type].properties, rel.propertyMap) 78 | } else { 79 | relTypeStats[rel.type] = { 80 | count: 1, 81 | properties: rel.propertyMap 82 | } 83 | } 84 | }) 85 | return {labels: labelStats, relTypes: relTypeStats} 86 | } 87 | -------------------------------------------------------------------------------- /src/components/D3Visualization/services/bolt/bolt.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2002-2017 "Neo Technology," 3 | * Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Neo4j is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | import { v4 } from 'uuid' 22 | import { v1 as neo4j } from 'neo4j-driver/lib/browser/neo4j-web.min.js' 23 | import { stringifyMod } from '../utils' 24 | import * as mappings from './boltMappings' 25 | import { BoltConnectionError, createErrorObject } from '../exceptions' 26 | 27 | let _drivers = null 28 | let _useRoutingConfig = false 29 | let _routingAvailable = false 30 | const runningQueryRegister = {} 31 | 32 | const _useRouting = () => _useRoutingConfig && _routingAvailable 33 | 34 | const _getDriver = (host, auth, opts, protocol) => { 35 | const boltHost = protocol + (host || '').split('bolt://').join('') 36 | return neo4j.driver(boltHost, auth, opts) 37 | } 38 | 39 | const _validateConnection = (driver, res, rej) => { 40 | if (!driver || !driver.session) return rej('No connection') 41 | const tmp = driver.session() 42 | tmp.run('CALL db.indexes()').then(() => { 43 | tmp.close() 44 | res(driver) 45 | }).catch((e) => { 46 | rej([e, driver]) 47 | }) 48 | } 49 | 50 | const _routingAvailability = () => { 51 | return directTransaction('CALL dbms.procedures()').then((res) => { 52 | const names = res.records.map((r) => r.get('name')) 53 | return names.indexOf('dbms.cluster.overview') > -1 54 | }) 55 | } 56 | 57 | const _getDriversObj = (props, opts = {}) => { 58 | const driversObj = {} 59 | const auth = opts.withoutCredentials || !props.username 60 | ? undefined 61 | : neo4j.auth.basic(props.username, props.password) 62 | const getDirectDriver = () => { 63 | if (driversObj.direct) return driversObj.direct 64 | driversObj.direct = _getDriver(props.host, auth, opts, 'bolt://') 65 | return driversObj.direct 66 | } 67 | const getRoutedDriver = () => { 68 | if (!_useRouting()) return getDirectDriver() 69 | if (driversObj.routed) return driversObj.routed 70 | driversObj.routed = _getDriver(props.host, auth, opts, 'bolt+routing://') 71 | return driversObj.routed 72 | } 73 | return { 74 | getDirectDriver, 75 | getRoutedDriver, 76 | close: () => { 77 | if (driversObj.direct) driversObj.direct.close() 78 | if (driversObj.routed) driversObj.routed.close() 79 | } 80 | } 81 | } 82 | 83 | function directConnect (props, opts = {}, onLostConnection = () => {}) { 84 | const p = new Promise((resolve, reject) => { 85 | const creds = opts.withoutCredentials || !props.username 86 | ? undefined 87 | : neo4j.auth.basic(props.username, props.password) 88 | const driver = _getDriver(props.host, creds, opts, 'bolt://') 89 | driver.onError = (e) => { 90 | onLostConnection(e) 91 | reject([e, driver]) 92 | } 93 | _validateConnection(driver, resolve, reject) 94 | }) 95 | return p 96 | } 97 | 98 | function openConnection (props, opts = {}, onLostConnection) { 99 | const p = new Promise((resolve, reject) => { 100 | const driversObj = _getDriversObj(props, opts) 101 | const driver = driversObj.getDirectDriver() 102 | driver.onError = (e) => { 103 | onLostConnection(e) 104 | _drivers = null 105 | driversObj.close() 106 | reject([e, driver]) 107 | } 108 | const myResolve = (driver) => { 109 | _drivers = driversObj 110 | _routingAvailability() 111 | .then((r) => { 112 | if (r) _routingAvailable = true 113 | if (!r) _routingAvailable = false 114 | }) 115 | .catch((e) => (_routingAvailable = false)) 116 | resolve(driver) 117 | } 118 | const myReject = (err) => { 119 | _drivers = null 120 | driversObj.close() 121 | reject(err) 122 | } 123 | _validateConnection(driver, myResolve, myReject) 124 | }) 125 | return p 126 | } 127 | 128 | function _trackedTransaction (input, parameters = {}, session, requestId = null) { 129 | const id = requestId || v4() 130 | if (!session) { 131 | return [id, Promise.reject(createErrorObject(BoltConnectionError))] 132 | } 133 | const closeFn = (cb = () => {}) => { 134 | session.close(cb) 135 | if (runningQueryRegister[id]) delete runningQueryRegister[id] 136 | } 137 | runningQueryRegister[id] = closeFn 138 | const queryPromise = session.run(input, parameters) 139 | .then((r) => { 140 | closeFn() 141 | return r 142 | }) 143 | .catch((e) => { 144 | closeFn() 145 | throw e 146 | }) 147 | return [id, queryPromise] 148 | } 149 | 150 | function cancelTransaction (id, cb) { 151 | if (runningQueryRegister[id]) runningQueryRegister[id](cb) 152 | } 153 | 154 | function _transaction (input, parameters, session) { 155 | if (!session) return Promise.reject(createErrorObject(BoltConnectionError)) 156 | return session.run(input, parameters) 157 | .then((r) => { 158 | session.close() 159 | return r 160 | }) 161 | .catch((e) => { 162 | session.close() 163 | throw e 164 | }) 165 | } 166 | 167 | function directTransaction (input, parameters, requestId = null, cancelable = false) { 168 | const session = _drivers ? _drivers.getDirectDriver().session() : false 169 | if (!cancelable) return _transaction(input, parameters, session) 170 | return _trackedTransaction(input, parameters, session, requestId) 171 | } 172 | 173 | function routedReadTransaction (input, parameters, requestId = null, cancelable = false) { 174 | const session = _drivers ? _drivers.getRoutedDriver().session(neo4j.session.READ) : false 175 | if (!cancelable) return _transaction(input, parameters, session) 176 | return _trackedTransaction(input, parameters, session, requestId) 177 | } 178 | 179 | function routedWriteTransaction (input, parameters, requestId = null, cancelable = false) { 180 | const session = _drivers ? _drivers.getRoutedDriver().session(neo4j.session.WRITE) : false 181 | if (!cancelable) return _transaction(input, parameters, session) 182 | return _trackedTransaction(input, parameters, session, requestId) 183 | } 184 | 185 | export default { 186 | directConnect, 187 | openConnection, 188 | closeConnection: () => { 189 | if (_drivers) { 190 | _drivers.close() 191 | _drivers = null 192 | } 193 | }, 194 | directTransaction, 195 | routedReadTransaction, 196 | routedWriteTransaction, 197 | cancelTransaction, 198 | useRoutingConfig: (shouldWe) => (_useRoutingConfig = shouldWe), 199 | recordsToTableArray: (records, convertInts = true) => { 200 | const intChecker = convertInts ? neo4j.isInt : () => true 201 | const intConverter = convertInts ? (val) => val.toString() : (val) => val 202 | return mappings.recordsToTableArray(records, intChecker, intConverter) 203 | }, 204 | stringifyRows: (rows) => { 205 | if (!Array.isArray(rows)) return rows 206 | const flat = mappings.flattenProperties(rows) 207 | if (!Array.isArray(flat)) return rows 208 | return flat.map((col) => { 209 | if (!col) return col 210 | return col.map((fVal) => { 211 | return stringifyMod()(fVal, (val) => { 212 | if (neo4j.isInt(val)) return val.toString() 213 | }) 214 | }) 215 | }) 216 | }, 217 | extractNodesAndRelationshipsFromRecords: (records) => { 218 | return mappings.extractNodesAndRelationshipsFromRecords(records, neo4j.types) 219 | }, 220 | extractNodesAndRelationshipsFromRecordsForOldVis: (records, filterRels = true) => { 221 | const intChecker = neo4j.isInt 222 | const intConverter = (val) => val.toString() 223 | return mappings.extractNodesAndRelationshipsFromRecordsForOldVis(records, neo4j.types, filterRels, intChecker, intConverter) 224 | }, 225 | extractPlan: (result) => { 226 | return mappings.extractPlan(result) 227 | }, 228 | retrieveFormattedUpdateStatistics: mappings.retrieveFormattedUpdateStatistics, 229 | itemIntToNumber: (item) => mappings.itemIntToString(item, neo4j.isInt, (val) => val.toNumber()), 230 | neo4j: neo4j 231 | } 232 | -------------------------------------------------------------------------------- /src/components/D3Visualization/services/bolt/boltHelpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2002-2017 "Neo Technology," 3 | * Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Neo4j is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | /* global location */ 22 | import bolt from './bolt' 23 | import { getUrlInfo } from '../utils' 24 | 25 | export const getEncryptionMode = () => { 26 | return location.protocol === 'https:' 27 | } 28 | 29 | export const getDiscoveryEndpoint = () => { 30 | const url = location.host ? location.href : 'http://localhost:7474/' 31 | const info = getUrlInfo(url) 32 | return `${info.protocol}//${info.host}/` 33 | } 34 | 35 | export const getServerConfig = (includePrefixes = []) => { 36 | return getJmxValues([['Configuration']]) 37 | .then((confs) => { 38 | const conf = confs[0] 39 | let filtered 40 | if (conf) { 41 | Object.keys(conf) 42 | .filter((key) => includePrefixes.length < 1 || includePrefixes.some((pfx) => key.startsWith(pfx))) 43 | .forEach((key) => (filtered = {...filtered, [key]: bolt.itemIntToNumber(conf[key].value)})) 44 | } 45 | return filtered || conf 46 | }) 47 | } 48 | 49 | export const getJmxValues = (nameAttributePairs = []) => { 50 | if (!nameAttributePairs.length) return Promise.reject(null) 51 | return bolt.directTransaction('CALL dbms.queryJmx("org.neo4j:*")') 52 | .then((res) => { 53 | let out = [] 54 | nameAttributePairs.forEach((pair) => { 55 | const [name, attribute = null] = pair 56 | if (!name) return out.push(null) 57 | const part = res.records.filter((record) => record.get('name').match(new RegExp(name + '$'))) 58 | if (!part.length) return out.push(null) 59 | const attributes = part[0].get('attributes') 60 | if (!attribute) return out.push(attributes) 61 | const key = attribute 62 | if (typeof attributes[key] === 'undefined') return out.push(null) 63 | const val = bolt.itemIntToNumber(attributes[key].value) 64 | out.push({ [key]: val }) 65 | }) 66 | return out 67 | }).catch((e) => { 68 | return null 69 | }) 70 | } 71 | 72 | export const isConfigValTruthy = (val) => [true, 'true', 'yes', 1, '1'].indexOf(val) > -1 73 | export const isConfigValFalsy = (val) => [false, 'false', 'no', 0, '0'].indexOf(val) > -1 74 | -------------------------------------------------------------------------------- /src/components/D3Visualization/services/bolt/boltMappings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2002-2017 "Neo Technology," 3 | * Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Neo4j is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | import updateStatsFields from './updateStatisticsFields' 22 | 23 | export function toObjects (records, intChecker, intConverter) { 24 | const recordValues = records.map((record) => { 25 | let out = [] 26 | record.forEach((val, key) => out.push(itemIntToString(val, intChecker, intConverter))) 27 | return out 28 | }) 29 | return recordValues 30 | } 31 | 32 | export function recordsToTableArray (records, intChecker, intConverter) { 33 | const recordValues = toObjects(records, intChecker, intConverter) 34 | const keys = records[0].keys 35 | return [[...keys], ...recordValues] 36 | } 37 | 38 | export function itemIntToString (item, intChecker, intConverter) { 39 | if (intChecker(item)) return intConverter(item) 40 | if (Array.isArray(item)) return arrayIntToString(item, intChecker, intConverter) 41 | if (['number', 'string', 'boolean'].indexOf(typeof item) !== -1) return item 42 | if (item === null) return item 43 | if (typeof item === 'object') return objIntToString(item, intChecker, intConverter) 44 | } 45 | 46 | export function arrayIntToString (arr, intChecker, intConverter) { 47 | return arr.map((item) => itemIntToString(item, intChecker, intConverter)) 48 | } 49 | 50 | export function objIntToString (obj, intChecker, intConverter) { 51 | let newObj = {} 52 | Object.keys(obj).forEach((key) => { 53 | newObj[key] = itemIntToString(obj[key], intChecker, intConverter) 54 | }) 55 | return newObj 56 | } 57 | 58 | export function extractPlan (result) { 59 | if (result.summary && (result.summary.plan || result.summary.profile)) { 60 | const rawPlan = result.summary.profile || result.summary.plan 61 | const boltPlanToRESTPlanShared = (plan) => { 62 | return { 63 | operatorType: plan.operatorType, 64 | LegacyExpression: plan.arguments.LegacyExpression, 65 | ExpandExpression: plan.arguments.ExpandExpression, 66 | DbHits: plan.dbHits, 67 | Rows: plan.rows, 68 | EstimatedRows: plan.arguments.EstimatedRows, 69 | identifiers: plan.identifiers, 70 | Index: plan.arguments.Index, 71 | children: plan.children.map(boltPlanToRESTPlanShared) 72 | } 73 | } 74 | let obj = boltPlanToRESTPlanShared(rawPlan) 75 | obj['runtime-impl'] = rawPlan.arguments['runtime-impl'] 76 | obj['planner-impl'] = rawPlan.arguments['planner-impl'] 77 | obj['version'] = rawPlan.arguments['version'] 78 | obj['KeyNames'] = rawPlan.arguments['KeyNames'] 79 | obj['planner'] = rawPlan.arguments['planner'] 80 | obj['runtime'] = rawPlan.arguments['runtime'] 81 | return {root: obj} 82 | } 83 | return null 84 | } 85 | 86 | export function extractNodesAndRelationshipsFromRecords (records, types) { 87 | if (records.length === 0) { 88 | return { nodes: [], relationships: [] } 89 | } 90 | 91 | let keys = records[0].keys 92 | let rawNodes = [] 93 | let rawRels = [] 94 | records.forEach((record) => { 95 | let graphItems = keys.map((key) => record.get(key)) 96 | rawNodes = [...rawNodes, ...graphItems.filter((item) => item instanceof types.Node)] 97 | rawRels = [...rawRels, ...graphItems.filter((item) => item instanceof types.Relationship)] 98 | let paths = graphItems.filter((item) => item instanceof types.Path) 99 | paths.forEach((item) => extractNodesAndRelationshipsFromPath(item, rawNodes, rawRels, types)) 100 | }) 101 | return { nodes: rawNodes, relationships: rawRels } 102 | } 103 | 104 | const resultContainsGraphKeys = (keys) => { 105 | return (keys.includes('nodes') && keys.includes('relationships')) 106 | } 107 | 108 | export function extractNodesAndRelationshipsFromRecordsForOldVis (records, types, filterRels, intChecker, intConverter) { 109 | if (records.length === 0) { 110 | return { nodes: [], relationships: [] } 111 | } 112 | let keys = records[0].keys 113 | let rawNodes = [] 114 | let rawRels = [] 115 | if (resultContainsGraphKeys(keys)) { 116 | rawNodes = [...rawNodes, ...records[0].get(keys[0])] 117 | rawRels = [...rawRels, ...records[0].get(keys[1])] 118 | } else { 119 | records.forEach((record) => { 120 | let graphItems = keys.map((key) => record.get(key)) 121 | rawNodes = [...rawNodes, ...graphItems.filter((item) => item instanceof types.Node)] 122 | rawRels = [...rawRels, ...graphItems.filter((item) => item instanceof types.Relationship)] 123 | let paths = graphItems.filter((item) => item instanceof types.Path) 124 | paths.forEach((item) => extractNodesAndRelationshipsFromPath(item, rawNodes, rawRels, types)) 125 | }) 126 | } 127 | const nodes = rawNodes.map((item) => { 128 | return {id: item.identity.toString(), labels: item.labels, properties: itemIntToString(item.properties, intChecker, intConverter)} 129 | }) 130 | let relationships = rawRels 131 | if (filterRels) { 132 | relationships = rawRels.filter((item) => nodes.filter((node) => node.id === item.start.toString()).length > 0 && nodes.filter((node) => node.id === item.end.toString()).length > 0) 133 | } 134 | relationships = relationships.map((item) => { 135 | return {id: item.identity.toString(), startNodeId: item.start.toString(), endNodeId: item.end.toString(), type: item.type, properties: itemIntToString(item.properties, intChecker, intConverter)} 136 | }) 137 | return { nodes: nodes, relationships: relationships } 138 | } 139 | 140 | const extractNodesAndRelationshipsFromPath = (item, rawNodes, rawRels) => { 141 | let paths = Array.isArray(item) ? item : [item] 142 | paths.forEach((path) => { 143 | path.segments.forEach((segment) => { 144 | rawNodes.push(segment.start) 145 | rawNodes.push(segment.end) 146 | rawRels.push(segment.relationship) 147 | }) 148 | }) 149 | } 150 | 151 | export const retrieveFormattedUpdateStatistics = (result) => { 152 | if (result.summary.counters) { 153 | const stats = result.summary.counters._stats 154 | const statsMessages = updateStatsFields.filter(field => stats[field.field] > 0).map(field => `${field.verb} ${stats[field.field]} ${stats[field.field] === 1 ? field.singular : field.plural}`) 155 | return statsMessages.join(', ') 156 | } else return null 157 | } 158 | 159 | export const flattenProperties = (rows) => { 160 | return rows.map((row) => row.map((entry) => (entry && entry.properties) ? entry.properties : entry)) 161 | } 162 | -------------------------------------------------------------------------------- /src/components/D3Visualization/services/bolt/updateStatisticsFields.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { plural: 'constraints', singular: 'constraint', verb: 'added', field: 'constraintsAdded' }, 3 | { plural: 'constraints', singular: 'constraint', verb: 'removed', field: 'constraintsRemoved' }, 4 | { plural: 'indexes', singular: 'index', verb: 'added', field: 'indexesAdded' }, 5 | { plural: 'indexes', singular: 'index', verb: 'removed', field: 'indexesRemoved' }, 6 | { plural: 'labels', singular: 'label', verb: 'added', field: 'labelsAdded' }, 7 | { plural: 'labels', singular: 'label', verb: 'removed', field: 'labelsRemoved' }, 8 | { plural: 'nodes', singular: 'node', verb: 'created', field: 'nodesCreated' }, 9 | { plural: 'nodes', singular: 'node', verb: 'deleted', field: 'nodesDeleted' }, 10 | { plural: 'properties', singular: 'property', verb: 'set', field: 'propertiesSet' }, 11 | { plural: 'relationships', singular: 'relationship', verb: 'deleted', field: 'relationshipDeleted' }, 12 | { plural: 'relationships', singular: 'relationship', verb: 'deleted', field: 'relationshipsDeleted' }, 13 | { plural: 'relationships', singular: 'relationship', verb: 'created', field: 'relationshipsCreated' } 14 | ] 15 | -------------------------------------------------------------------------------- /src/components/D3Visualization/services/duckUtils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2002-2017 "Neo Technology," 3 | * Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Neo4j is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | export const hydrate = (initialState, state) => { 22 | if (!state) return state 23 | return (state.hydrated) ? state : { ...initialState, ...state, hydrated: true } 24 | } 25 | 26 | export const dehydrate = (state) => { 27 | if (state) { 28 | delete state.hydrated 29 | } 30 | return state 31 | } 32 | -------------------------------------------------------------------------------- /src/components/D3Visualization/services/exceptionMessages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2002-2017 "Neo Technology," 3 | * Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Neo4j is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | export const AddServerValidationError = 'Wrong format. It should be ":server add name username:password@host:port"' 22 | export const CreateDataSourceValidationError = 'Wrong format. It should be ":datasource create {"name": "myName", "command": "RETURN rand()", "bookmarkId":"uuid-of-existing-bookmark", "refreshInterval": 10, "parameters": {}}"' 23 | export const RemoveDataSourceValidationError = 'Wrong format. It should be ":datasource remove uuid-of-existing-datasource"' 24 | export const BoltConnectionError = 'No connection found, did you connect to Neo4j?' 25 | export const BoltError = '#code# - #message#' 26 | export const Neo4jError = '#message#' 27 | export const UnknownCommandError = 'Unknown command #cmd#' 28 | export const BookmarkNotFoundError = 'No connection with the name #name# found. Add a bookmark before trying to connect.' 29 | export const OpenConnectionNotFoundError = 'No open connection with the name #name# found. You have to connect to a bookmark before you can use it.' 30 | export const CouldNotFetchRemoteGuideError = 'Can not fetch remote guide: #error#' 31 | 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.' 32 | -------------------------------------------------------------------------------- /src/components/D3Visualization/services/exceptions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2002-2017 "Neo Technology," 3 | * Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Neo4j is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | import * as messages from './exceptionMessages' 22 | 23 | export function getErrorMessage (errorObject) { 24 | let str = messages[errorObject.type] 25 | if (!str) return 26 | const keys = Object.keys(errorObject) 27 | keys.forEach((prop) => { 28 | const re = new RegExp('(#' + prop + '#)', 'g') 29 | str = str.replace(re, errorObject[prop]) 30 | }) 31 | return str 32 | } 33 | 34 | export function createErrorObject (ErrorType, ...rest) { 35 | const obj = new ErrorType(...rest) 36 | if (!obj.code) obj.code = obj.type 37 | obj.message = getErrorMessage(obj) 38 | return obj 39 | } 40 | 41 | export function UserException (message) { 42 | return { 43 | type: 'UserException', 44 | message 45 | } 46 | } 47 | 48 | export function ConnectionException (message, code = 'Connection Error') { 49 | return {fields: [{ 50 | code, 51 | message 52 | }]} 53 | } 54 | 55 | export function AddServerValidationError () { 56 | return { 57 | type: 'AddServerValidationError' 58 | } 59 | } 60 | 61 | export function CreateDataSourceValidationError () { 62 | return { 63 | type: 'CreateDataSourceValidationError' 64 | } 65 | } 66 | 67 | export function RemoveDataSourceValidationError () { 68 | return { 69 | type: 'RemoveDataSourceValidationError' 70 | } 71 | } 72 | 73 | export function BoltConnectionError () { 74 | return { 75 | type: 'BoltConnectionError' 76 | } 77 | } 78 | 79 | export function BoltError (obj) { 80 | return { 81 | type: 'BoltError', 82 | code: obj.fields[0].code, 83 | message: obj.fields[0].message 84 | } 85 | } 86 | 87 | export function Neo4jError (obj) { 88 | return { 89 | type: 'Neo4jError', 90 | message: obj.message 91 | } 92 | } 93 | 94 | export function ConnectionNotFoundError (name) { 95 | return { 96 | type: 'ConnectionNotFoundError', 97 | name 98 | } 99 | } 100 | 101 | export function OpenConnectionNotFoundError (name) { 102 | return { 103 | type: 'OpenConnectionNotFoundError', 104 | name 105 | } 106 | } 107 | 108 | export function UnknownCommandError (cmd) { 109 | return { 110 | type: 'UnknownCommandError', 111 | cmd 112 | } 113 | } 114 | 115 | export function CouldNotFetchRemoteGuideError (error) { 116 | return { 117 | type: 'CouldNotFetchRemoteGuideError', 118 | error 119 | } 120 | } 121 | 122 | export function FetchURLError (error) { 123 | return { 124 | type: 'FetchURLError', 125 | error 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/components/D3Visualization/services/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2002-2017 "Neo Technology," 3 | * Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Neo4j is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | export const deepEquals = (x, y) => { 22 | if (x && y && typeof x === 'object' && typeof y === 'object') { 23 | if (Object.keys(x).length !== Object.keys(y).length) return false 24 | return Object.keys(x).every((key) => deepEquals(x[key], y[key])) 25 | } 26 | return (x === y) 27 | } 28 | 29 | export const flatten = arr => arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []) 30 | 31 | export const moveInArray = (fromIndex, toIndex, arr) => { 32 | if (!Array.isArray(arr)) return false 33 | if (fromIndex < 0 || fromIndex >= arr.length) return false 34 | if (toIndex < 0 || toIndex >= arr.length) return false 35 | const newArr = [].concat(arr) 36 | const el = arr[fromIndex] 37 | newArr.splice(fromIndex, 1) 38 | newArr.splice(toIndex, 0, el) 39 | return newArr 40 | } 41 | 42 | export const debounce = (fn, time, context = null) => { 43 | let pending 44 | return (...args) => { 45 | if (pending) clearTimeout(pending) 46 | pending = setTimeout(() => typeof fn === 'function' && fn.apply(context, args), parseInt(time)) 47 | } 48 | } 49 | 50 | export const throttle = (fn, time, context = null) => { 51 | let blocking 52 | return (...args) => { 53 | if (blocking) return 54 | blocking = true 55 | typeof fn === 'function' && fn.apply(context, args) 56 | setTimeout(() => (blocking = false), parseInt(time)) 57 | } 58 | } 59 | 60 | export const isRoutingHost = (host) => { 61 | return /^bolt\+routing:\/\//.test(host) 62 | } 63 | 64 | export const toBoltHost = (host) => { 65 | return 'bolt://' + (host || '') // prepend with bolt:// 66 | .split('bolt://').join('') // remove bolt:// 67 | .split('bolt+routing://').join('') // remove bolt+routing:// 68 | } 69 | 70 | export const hostIsAllowed = (uri, whitelist = null) => { 71 | if (whitelist === '*') return true 72 | const urlInfo = getUrlInfo(uri) 73 | const hostname = urlInfo.hostname 74 | const hostnamePlusProtocol = urlInfo.protocol + '//' + hostname 75 | 76 | let whitelistedHosts = ['guides.neo4j.com', 'localhost'] 77 | if (whitelist && whitelist !== '') { 78 | whitelistedHosts = whitelist.split(',') 79 | } 80 | return whitelistedHosts.indexOf(hostname) > -1 || 81 | whitelistedHosts.indexOf(hostnamePlusProtocol) > -1 82 | } 83 | 84 | export const getUrlInfo = (url) => { 85 | const reURLInformation = new RegExp([ 86 | '^(?:(https?:)//)?', // protocol 87 | '(([^:/?#]*)(?::([0-9]+))?)', // host (hostname and port) 88 | '(/{0,1}[^?#]*)', // pathname 89 | '(\\?[^#]*|)', // search 90 | '(#.*|)$' // hash 91 | ].join('')) 92 | const match = url.match(reURLInformation) 93 | return match && { 94 | protocol: match[1], 95 | host: match[2], 96 | hostname: match[3], 97 | port: match[4], 98 | pathname: match[5], 99 | search: match[6], 100 | hash: match[7] 101 | } 102 | } 103 | 104 | export const getUrlParamValue = (name, url) => { 105 | if (!url) return false 106 | let out = [] 107 | const re = new RegExp('[\\?&]' + name + '=([^&#]*)', 'g') 108 | let results 109 | while ((results = re.exec(url)) !== null) { 110 | if (results && results[1]) out.push(results[1]) 111 | } 112 | if (!out.length) return undefined 113 | return out 114 | } 115 | 116 | export const toHumanReadableBytes = (input) => { 117 | let number = +input 118 | if (!isFinite(number)) { return '-' } 119 | 120 | if (number < 1024) { 121 | return `${number} B` 122 | } 123 | 124 | number /= 1024 125 | let units = ['KiB', 'MiB', 'GiB', 'TiB'] 126 | 127 | for (let unit of Array.from(units)) { 128 | if (number < 1024) { return `${number.toFixed(2)} ${unit}` } 129 | number /= 1024 130 | } 131 | 132 | return `${number.toFixed(2)} PiB` 133 | } 134 | 135 | export const getBrowserName = function () { 136 | if (!!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0) { 137 | return 'Opera' 138 | } 139 | if (typeof InstallTrigger !== 'undefined') { 140 | return 'Firefox' 141 | } 142 | if (navigator.userAgent.match(/Version\/[\d.]+.*Safari/)) { 143 | return 'Safari' 144 | } 145 | if (window.chrome) { 146 | return 'Chrome' 147 | } 148 | if (document.documentMode) { 149 | return 'Internet Explorer' 150 | } 151 | if (window.StyleMedia) { 152 | return 'Edge' 153 | } 154 | return 'Unknown' 155 | } 156 | 157 | export const removeComments = (string) => { 158 | return string.split(/\r?\n/).filter((line) => !line.startsWith('//')).join('\r\n') 159 | } 160 | 161 | export const canUseDOM = () => !!( 162 | (typeof window !== 'undefined' && 163 | window.document && window.document.createElement) 164 | ) 165 | 166 | export const stringifyMod = () => { 167 | const toString = Object.prototype.toString 168 | const isArray = Array.isArray || function (a) { return toString.call(a) === '[object Array]' } 169 | const escMap = {'"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t'} 170 | const escFunc = function (m) { return escMap[m] || '\\u' + (m.charCodeAt(0) + 0x10000).toString(16).substr(1) } 171 | const escRE = /[\\"\u0000-\u001F\u2028\u2029]/g 172 | return function stringify (value, modFn = null) { 173 | if (modFn) { 174 | const modVal = modFn && modFn(value) 175 | if (typeof modVal !== 'undefined') return modVal 176 | } 177 | if (value == null) return 'null' 178 | if (typeof value === 'number') return isFinite(value) ? value.toString() : 'null' 179 | if (typeof value === 'boolean') return value.toString() 180 | if (typeof value === 'object') { 181 | if (typeof value.toJSON === 'function') { 182 | return stringify(value.toJSON(), modFn) 183 | } else if (isArray(value)) { 184 | let res = '[' 185 | for (let i = 0; i < value.length; i++) { 186 | res += (i ? ',' : '') + stringify(value[i], modFn) 187 | } 188 | return res + ']' 189 | } else if (toString.call(value) === '[object Object]') { 190 | let tmp = [] 191 | for (const k in value) { 192 | if (value.hasOwnProperty(k)) tmp.push(stringify(k, modFn) + ':' + stringify(value[k], modFn)) 193 | } 194 | return '{' + tmp.join(',') + '}' 195 | } 196 | } 197 | return '"' + value.toString().replace(escRE, escFunc) + '"' 198 | } 199 | } 200 | 201 | // Epic helpers 202 | export const put = (dispatch) => (action) => dispatch(action) 203 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhenghuiqiang on 17/8/5. 3 | */ 4 | import { app } from './app' 5 | 6 | app.$mount('#app') -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhenghuiqiang on 17/8/5. 3 | */ 4 | import Vue from 'vue' 5 | import VueRouter from 'vue-router' 6 | Vue.use(VueRouter) 7 | 8 | export default new VueRouter({ 9 | mode: 'history', 10 | linkActiveClass: 'is-active', 11 | scrollBehavior: () => ({ y: 0 }), 12 | routes: [{ 13 | path: '/', 14 | redirect: '/neo4j' 15 | }, { 16 | name : 'neo4j', 17 | path: '/neo4j', 18 | component: require('../views/Main') 19 | }] 20 | }) 21 | --------------------------------------------------------------------------------