├── LICENSE ├── README.md ├── app ├── index.html └── scripts │ └── app.js ├── package.json └── webpack.config.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 yWorks GmbH 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yfiles-neo4j-basic-demo 2 | Shows how to use yFiles for HTML and Neo4j in a web based single page app to visualize database contents. 3 | 4 | This repository serves as a reference for a demo that can be used as a guideline for creating single page web application that render a visualization of [Neo4j](https://www.neo4j.com) database contents using the [yFiles for HTML JavaScript graph drawing library](https://www.yworks.com/yfileshtml). __You cannot run or test the demo without a yFiles for HTML library or without a Neo4j database just by cloning this repository. This repository was originally meant as a reference for the sources in [this outdated posting](https://medium.com/neo4j/neo4j-graph-visualization-like-a-pro-18651963ebd4) and [the corresponding (also slightly outdated) YouTube screencast](https://youtu.be/ABixtyDjcKc), only.__ If you are evaluating yFiles for HTML today, be sure to stick to the sources in this repository which uses the now (at the time of writing) current version of yFiles, which is 2.4. 5 | 6 | For a much easier way to get started with yFiles for HTML and Neo4j, be sure to check out the [App-Generator](https://www.yworks.com/products/app-generator) which has several Neo4j related examples that you can adjust to your needs with low-code techniques. It also allows you to scaffold yFiles/Neo4j powered applications with your choice of UI framework and programming language, supporting VueJS, Angular, React, Plain Old HTML, with both JavaScript and TypeScript and a set of additional options like tooltips, search fields, overview, image and PDF export, etc. 7 | 8 | The current demo was scaffolded using the [yeoman generator for yFiles apps](https://www.npmjs.com/package/generator-yfiles-app) for yFiles for HTML 2.3 using the following settings: 9 | 10 | ``` 11 | ? Which framework do you want to use? No framework 12 | ? Application name Yfiles_neo4j_basic_demo_2 13 | ? Path of yFiles for HTML package C:\Path\to\your\yFilesForHTMLPackage 14 | ? Path of license file (e.g. 'path/to/license.json') C:\Path\to\your\yFilesForHTMLPackage\lib\license.json 15 | ? Which kind of yFiles modules do you want to use? Local NPM dependency (recommended) 16 | ? Which language variant do you want to use? ES6 17 | ? What else do you want? Use development library 18 | ? Which package manager would you like to use? npm 19 | ``` 20 | See [this updated YouTube screencast](https://youtu.be/fgY4ezIfVjI) on how to get started with yFiles and scaffold a yFiles for HTML powered application with yeoman. 21 | 22 | In order to run this demo, you should first obtain a current version of yFiles for HTML from [here](https://www.yworks.com/products/yfiles-for-html/evaluate). 23 | Then run the yeoman generator and replace the contents of the scaffolded `app.js` file with the ones from this repository and install neo4j using npm. 24 | For a *generic* introduction to yFiles for HTML see [this YouTube screencast](https://youtu.be/QITNGXNGM3w) 25 | 26 | If you are new to yFiles for HTML, you might also consider the approach shown in [this webinar recording](https://youtu.be/_Gz773u-TBQ) to get your first app up and running, quickly. 27 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Neo_yFiles 5 | 6 | 7 | 8 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | import 'yfiles/yfiles.css' 2 | 3 | import { 4 | License, 5 | GraphComponent, 6 | Class, 7 | LayoutExecutor, 8 | GraphViewerInputMode, 9 | RadialLayout, 10 | RadialLayoutData, 11 | Size, 12 | GraphBuilder, 13 | ShapeNodeStyle, 14 | EdgePathLabelModel, 15 | PolylineEdgeStyle, 16 | INode, 17 | DefaultLabelStyle, 18 | GraphItemTypes, 19 | Reachability, 20 | } from 'yfiles' 21 | 22 | // Tell the library about the license contents 23 | License.value = { 24 | /* put the license contents here - the yeoman generator does this automatically for you */ 25 | } 26 | 27 | import neo4j, { Node as NeoNode } from 'neo4j-driver' 28 | 29 | // setup the driver 30 | const neo4jDriver = neo4j.driver('bolt://1.2.3.4/', neo4j.auth.basic('username', 'TheS3cr3t')) 31 | 32 | // We need to load the yfiles/view-layout-bridge module explicitly to prevent the webpack 33 | // tree shaker from removing this dependency which is needed for 'morphLayout' in this demo. 34 | Class.ensure(LayoutExecutor) 35 | 36 | // hook up the graph control to the div in the page 37 | const graphComponent = new GraphComponent('#graphComponent') 38 | 39 | // make it interactive - we don't allow editing (creating new elements) 40 | // but are generally interested in viewing, only 41 | const inputMode = new GraphViewerInputMode() 42 | graphComponent.inputMode = inputMode 43 | 44 | // display a tooltip when the mouse hovers over an item 45 | inputMode.addQueryItemToolTipListener((sender, args) => { 46 | // the neo4j data is stored in the "tag" property of the item 47 | // if it contains "properties" - show them in a simple HTML list 48 | if (args.item?.tag?.properties) { 49 | // we can use a string, or set an HTML Element (e.g. when we do cannot trust the data) 50 | args.toolTip = `` 55 | } 56 | }) 57 | 58 | // when the user double-clicks on a node, we want to focus that node in the layout 59 | inputMode.addItemDoubleClickedListener(async (sender, args) => { 60 | // clicks could also be on a label, edge, port, etc. 61 | if (args.item instanceof INode) { 62 | // tell the engine that we don't want the default action for double-clicks to happen 63 | args.handled = true 64 | // we configure the layout data and tell it to put the item into the center 65 | const layoutData = new RadialLayoutData({ centerNodes: [args.item] }) 66 | // we build the layout algorithm 67 | const layout = new RadialLayout({ 68 | centerNodesPolicy: 'custom', 69 | }) 70 | // now we calculate the layout and morph the results 71 | await graphComponent.morphLayout({ 72 | layout, 73 | layoutData, 74 | easedAnimation: true, 75 | morphDuration: '1s', 76 | }) 77 | } 78 | }) 79 | 80 | // when the user hovers over a node, we want to highlight all nodes that are reachable from this node 81 | inputMode.itemHoverInputMode.addHoveredItemChangedListener((sender, args) => { 82 | // first we remove the old highlight 83 | graphComponent.highlightIndicatorManager.clearHighlights() 84 | if (args.item) { 85 | // and if we are hovering over a node 86 | // configure an instance of a new reachability algorithm 87 | new Reachability({ directed: true, startNodes: [args.item] }) 88 | // run the algorithm on the graph in the view 89 | .run(graphComponent.graph) 90 | // and use the results to iterate over all reachable nodes 91 | .reachableNodes.forEach(node => 92 | // and highlight them in the view 93 | graphComponent.highlightIndicatorManager.addHighlight(node) 94 | ) 95 | } 96 | }) 97 | 98 | // tell it that we are only interested in hovered-over nodes 99 | inputMode.itemHoverInputMode.hoverItems = GraphItemTypes.NODE 100 | // and other elements should not discard the hover 101 | inputMode.itemHoverInputMode.discardInvalidItems = false 102 | // by default the mode is disabled, so enable the functionality, here 103 | inputMode.itemHoverInputMode.enabled = true 104 | 105 | // this function will be executed at startup - it performs the main setup 106 | async function loadGraph() { 107 | // first we query a limited number of arbitrary nodeData 108 | // modify the query to suit your requirement! 109 | const nodeResult = await runCypherQuery('MATCH (node) RETURN node LIMIT 25') 110 | // we put the resulting records in a separate array 111 | /** @type {NeoNode[]} */ 112 | const nodeData = nodeResult.records.map(record => record.get('node')) 113 | 114 | // and we store all node identities in a separate array 115 | const nodeIds = nodeData.map(node => node.identity) 116 | 117 | // with the node ids we can query the edges between the nodeData 118 | const edgeResult = await runCypherQuery( 119 | `MATCH (n)-[edge]-(m) 120 | WHERE id(n) IN $nodeIds 121 | AND id(m) IN $nodeIds 122 | RETURN DISTINCT edge LIMIT 100`, 123 | { nodeIds } 124 | ) 125 | // and store the edges in an array 126 | const edgeData = edgeResult.records.map(record => record.get('edge')) 127 | 128 | // now we create the helper class that will help us build the graph from the data in a declarative way 129 | const graphBuilder = new GraphBuilder(graphComponent.graph) 130 | 131 | // we set the default style to use on the graph 132 | graphBuilder.graph.nodeDefaults.style = new ShapeNodeStyle({ 133 | shape: 'ellipse', 134 | fill: 'lightblue', 135 | }) 136 | // and the default size 137 | graphBuilder.graph.nodeDefaults.size = new Size(120, 40) 138 | // and we also configure the labels to be truncated if they exceed a certain size 139 | graphBuilder.graph.nodeDefaults.labels.style = new DefaultLabelStyle({ 140 | maximumSize: [116, 36], 141 | wrapping: 'word-ellipsis', 142 | }) 143 | // add semi-transparent background for the edge labels 144 | graphBuilder.graph.edgeDefaults.labels.style = new DefaultLabelStyle({ 145 | backgroundFill: 'rgba(255,255,255,0.83)', 146 | }) 147 | // last not least we specify the default placements for the labels. 148 | graphBuilder.graph.edgeDefaults.labels.layoutParameter = new EdgePathLabelModel({ 149 | distance: 3, 150 | autoRotation: true, 151 | sideOfEdge: 'above-edge', 152 | }).createDefaultParameter() 153 | 154 | const isMovie = dataItem => dataItem.labels && dataItem.labels.includes('Movie') 155 | 156 | // we split the data in movie nodes and non-movie nodes. 157 | const movieData = nodeData.filter(dataItem => isMovie(dataItem)) 158 | const otherData = nodeData.filter(dataItem => !isMovie(dataItem)) 159 | 160 | // now we configure the movie nodes 161 | const movieNodesSource = graphBuilder.createNodesSource(movieData, node => 162 | node.identity.toString(10) 163 | ) 164 | 165 | // we specify a distinct node style to use for the movie nodes 166 | movieNodesSource.nodeCreator.defaults.style = new ShapeNodeStyle({ 167 | shape: 'round-rectangle', 168 | fill: 'yellow', 169 | }) 170 | // and a different default size, too 171 | movieNodesSource.nodeCreator.defaults.size = new Size(120, 50) 172 | 173 | // as well as what text to use as the first label for the movie nodes 174 | const simpleLabelBinding = movieNodesSource.nodeCreator.createLabelBinding( 175 | node => node.properties?.['title'] ?? node.properties?.['name'] 176 | ) 177 | 178 | // then we add a second source for all the non-movie nodes, too 179 | graphBuilder 180 | .createNodesSource(otherData, dataItem => dataItem.identity.toString(10)) 181 | .nodeCreator.createLabelBinding(simpleLabelBinding.textProvider) 182 | 183 | // and we also specify the source for the edges, along with how 184 | // the source and target nodes get identified 185 | const edgesSource = graphBuilder.createEdgesSource( 186 | edgeData, 187 | dataItem => dataItem.start.toString(10), 188 | dataItem => dataItem.end.toString(10) 189 | ) 190 | // and we display the label, too, using the type of the relationship 191 | edgesSource.edgeCreator.createLabelBinding(dataItem => dataItem.type) 192 | 193 | // similar to the above code, we also change the appearance of the "ACTED_IN" relationship 194 | // to a customized visualization 195 | const actedInEdgeStyle = new PolylineEdgeStyle({ 196 | stroke: 'medium blue', 197 | smoothingLength: 30, 198 | targetArrow: 'blue default', 199 | }) 200 | 201 | // instead of providing two edge sources, we can also use one source 202 | // and conditionally change the style based on its type 203 | edgesSource.edgeCreator.styleProvider = dataItem => { 204 | // .. of type "ACTED_IN" 205 | return dataItem.type === 'ACTED_IN' ? actedInEdgeStyle : null 206 | } 207 | 208 | // all is configured; now trigger the initial construction of the graph 209 | graphBuilder.buildGraph() 210 | 211 | // the graph does not have a layout at this point, so we run a simple radial layout 212 | await graphComponent.morphLayout(new RadialLayout()) 213 | } 214 | 215 | /** 216 | * Asynchronous helper function that executes a query with parameters 217 | * *and* closes the session again. 218 | * @param {String} query The cypher query 219 | * @param {any} parameters The parameters to use for the query 220 | * @return {Promise} 221 | */ 222 | async function runCypherQuery(query, parameters = {}) { 223 | const session = neo4jDriver.session({ defaultAccessMode: 'READ' }) 224 | try { 225 | return await session.run(query, parameters) 226 | } finally { 227 | await session.close() 228 | } 229 | } 230 | 231 | // trigger the loading - show exceptions in an alert 232 | loadGraph().catch(reason => alert(reason)) 233 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "My first yFiles for HTML WebApp with neo4j.", 3 | "license": "unlicensed", 4 | "private": true, 5 | "name": "yfiles-neo4j-basic-demo", 6 | "version": "0.3.0", 7 | "scripts": { 8 | "production": "webpack --mode production", 9 | "dev": "webpack --mode development", 10 | "start": "webpack-dev-server --mode development --open" 11 | }, 12 | "devDependencies": { 13 | "@yworks/optimizer": "^1.6.0", 14 | "webpack": "^4.46.0", 15 | "webpack-cli": "^3.3.11", 16 | "webpack-dev-server": "^3.11.0", 17 | "css-loader": "^3.5.3", 18 | "mini-css-extract-plugin": "^0.9.0", 19 | "babel-loader": "^8.1.0", 20 | "@babel/core": "^7.10.2", 21 | "@babel/preset-env": "^7.10.2", 22 | "core-js": "^3.6.5", 23 | "regenerator-runtime": "^0.13.5" 24 | }, 25 | "dependencies": { 26 | "neo4j-driver": "^4.1.0", 27 | "yfiles": "../yFiles-for-HTML-Complete-2.4.0.4-Evaluation/lib-dev/es-modules/yfiles-24.0.4-eval-dev.tgz" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require("path"); 3 | const webpack = require('webpack'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 5 | const YFilesOptimizerPlugin = require('@yworks/optimizer/webpack-plugin'); 6 | 7 | const config = { 8 | 9 | entry: { 10 | app: ['regenerator-runtime/runtime', path.resolve('app/scripts/app.js')] 11 | }, 12 | 13 | output: { 14 | path: path.resolve(__dirname, 'app/dist/'), 15 | publicPath: 'dist', 16 | filename: '[name].js' 17 | }, 18 | resolve: { 19 | extensions: [".js", ".jsx"] 20 | }, 21 | 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.js$/, 26 | exclude: /(node_modules|bower_components|lib)/, 27 | loader: 'babel-loader', 28 | options: { 29 | compact: true, 30 | presets: ['@babel/preset-env'] 31 | } 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 36 | } 37 | ] 38 | }, 39 | optimization: { 40 | splitChunks: { 41 | cacheGroups: { 42 | lib: { 43 | test: /([\\/]lib)|([\\/]node_modules[\\/])/, 44 | name: 'lib', 45 | chunks: 'all' 46 | } 47 | } 48 | } 49 | }, 50 | plugins: [ 51 | new MiniCssExtractPlugin({ 52 | filename: '[name].css', 53 | chunkFilename: '[id].css' 54 | }) 55 | ], 56 | performance: { 57 | // don't complain about large chunks/assets 58 | hints: false 59 | } 60 | }; 61 | 62 | module.exports = function (env, options) { 63 | 64 | console.log("Running webpack..."); 65 | 66 | if (options.mode === 'development') { 67 | config.devServer = { 68 | contentBase: [path.join(__dirname, './app')], 69 | compress: true, 70 | port: 9003 71 | } 72 | // don't add the default SourceMapDevToolPlugin config 73 | config.devtool = false 74 | config.plugins.push( 75 | new webpack.SourceMapDevToolPlugin({ 76 | filename: '[file].map', 77 | // add source maps for non-library code to enable convenient debugging 78 | exclude: ['lib.js'] 79 | }) 80 | ) 81 | } 82 | 83 | if(options.mode === 'production') { 84 | // Add the core-js polyfill for production 85 | config.entry.app.unshift('core-js/stable') 86 | 87 | // Run the yWorks Optimizer 88 | config.plugins.unshift( 89 | new YFilesOptimizerPlugin({ 90 | logLevel: 'info' 91 | }) 92 | ) 93 | } 94 | 95 | return config 96 | }; 97 | --------------------------------------------------------------------------------