├── .gitignore ├── img └── group-user-country.jpg ├── readme.adoc ├── css └── layout.css ├── index.html └── js └── grouping.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /img/group-user-country.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j-examples/neo4j-grouping/HEAD/img/group-user-country.jpg -------------------------------------------------------------------------------- /readme.adoc: -------------------------------------------------------------------------------- 1 | == Neo4j Graph Grouping Demo 2 | 3 | Based on work by http://twitter.com/kc1s[Martin Junghanns] and Max Kiessling for the https://github.com/dbs-leipzig/gradoop_demo#graph-grouping[Gradoop Demo]. 4 | 5 | Uses the user defined procedure `apoc.group.nodes` to compute the grouping. 6 | 7 | Run it live here: https://rawgit.com/neo4j-examples/neo4j-grouping/master/index.html[Grouping Demo] 8 | 9 | You can select: 10 | 11 | * the database connection 12 | * labels to be used in grouping (default all) 13 | * relationship-types to be used (default all) 14 | * properties to group by 15 | * aggregation operations for nodes and relationships, by default either are counted 16 | 17 | image::img/group-user-country.jpg[] 18 | 19 | === Libraries used 20 | 21 | * Neo4j Javascript Driver 22 | * Cytoscape with Dagre for Visualization 23 | * jquery, select2 and spectre for UI 24 | 25 | === License 26 | 27 | Apache License v2 -------------------------------------------------------------------------------- /css/layout.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=PT+Sans"); 2 | html, body { 3 | height: 100%; } 4 | 5 | body { 6 | color: #525564; 7 | font-family: 'PT Sans', sans-serif; 8 | display: flex; 9 | flex-direction: column; } 10 | 11 | .content-wrapper { 12 | flex: 1 0 auto; } 13 | 14 | .page-header { 15 | padding: .3rem .5rem; 16 | height: 40px; 17 | position: fixed; 18 | top: 0; 19 | width: 100%; 20 | z-index: 200; 21 | background-color: #f8f9fa; 22 | border-bottom: 1px solid #eeeeee; } 23 | 24 | .page-header .navbar-logo { 25 | height: 30px; } 26 | 27 | .navbar-spacer { 28 | min-height: 50px; } 29 | 30 | footer { 31 | padding-top: 20px; 32 | padding-bottom: 20px; 33 | width: 100%; 34 | color: #acb3c2; 35 | background-color: #454d5d; } 36 | footer a { 37 | color: #f0f1f4; } 38 | 39 | .section-blue { 40 | width: 100%; 41 | background-color: #eff4f7; 42 | padding-top: 30px; 43 | padding-bottom: 30px; } 44 | 45 | .section-white { 46 | width: 100%; 47 | background-color: #ffffff; 48 | padding-top: 10px; 49 | padding-bottom: 10px; } 50 | 51 | form fieldset { 52 | padding-top: 8px; } 53 | 54 | form fieldset .header { 55 | font-size: 1.0rem; 56 | } 57 | 58 | .large-canvas { 59 | width: 100%; 60 | margin: auto; 61 | padding: 0; 62 | position: relative; 63 | box-sizing: border-box; } 64 | 65 | .large-canvas canvas { 66 | width: content-box; 67 | height: 200px; } 68 | 69 | .CodeMirror { 70 | border: .1rem solid #c4c9d3; } 71 | 72 | #canvas { 73 | min-height: 700px; 74 | height: 100%; 75 | width: 100%; 76 | display: block; } 77 | 78 | #cypher-result-table tbody { 79 | font-size: 11px; } 80 | 81 | .option-tooltip { 82 | color: #aaaaaa; 83 | border: 1px solid; 84 | border-radius: 50%; 85 | width: 10px; 86 | height: 10px; 87 | /* font-size: 1rem;*/ 88 | padding: 2px 3px; 89 | /* top: -10px; */ } 90 | 91 | .select2-container .select2-selection--single { 92 | height: 1.8rem; 93 | } 94 | 95 | .select2-container--default .select2-selection--single, .select2-container--default .select2-selection--multiple { 96 | border-radius: 0; 97 | width: 100%; } 98 | 99 | .select2-container .select2-search--inline { 100 | margin-top: 0; } 101 | 102 | .qtip-content { 103 | font-size: 14px; 104 | line-height: 1.2; 105 | } 106 | 107 | label { 108 | margin-top: 1px; 109 | margin-bottom: 1px; 110 | margin-left: 5px; 111 | margin-right: 10px; 112 | padding: 1px; 113 | } 114 | .form-label { 115 | padding: 2px; 116 | vertical-align: middle; 117 | } 118 | /*# sourceMappingURL=layout.css.map */ 119 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | Neo4j Demo | Grouping 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 57 | 58 | 59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | Input Parameters 69 |
70 |
71 |
72 |
73 | 74 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |
87 |
88 | 89 | 90 | 91 |
92 |
93 | 97 | 99 |
100 | 101 |
102 | 107 | 109 |
110 |
111 | 112 |
113 |
114 | Node Grouping Parameters 115 |
116 |
117 | 118 |
119 | 123 | 125 |
126 | 127 |
128 | 132 | 134 |
135 |
136 | 137 |
138 |
139 | Relationships Grouping Parameters 140 |
141 |
142 | 143 |
144 | 148 | 150 |
151 | 152 |
153 | 157 | 159 |
160 |
161 |
162 |
163 | 166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 | 193 |
194 |
195 |
196 |
197 |
198 | 199 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /js/grouping.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014 - 2018 Leipzig University (Database Research Group) 3 | * Copyright © 2018 Neo4j Inc (Adaption by Michael Hunger) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /**--------------------------------------------------------------------------------------------------------------------- 19 | * Global Values 20 | *-------------------------------------------------------------------------------------------------------------------*/ 21 | /** 22 | * Prefixes of the aggregation functions 23 | */ 24 | let aggrPrefixes = ['min ', 'max ', 'sum ']; // ,'avg ','collect ', 'count_*' 25 | 26 | /** 27 | * Map of all possible values for the nodeLabelKey to a color in RGB format. 28 | * @type {{}} 29 | */ 30 | let colorMap = {}; 31 | 32 | /** 33 | * Buffers the last graph response from the server to improve redrawing speed. 34 | */ 35 | let bufferedData; 36 | 37 | /** 38 | * True, if the graph layout should be force based 39 | * @type {boolean} 40 | */ 41 | let useForceLayout = true; 42 | 43 | /** 44 | * True, if the default label should be used 45 | * @type {boolean} 46 | */ 47 | let useDefaultLabel = true; 48 | 49 | /** 50 | * Maximum value for the count attribute of vertices 51 | * @type {number} 52 | */ 53 | let maxNodeCount = 0; 54 | 55 | /** 56 | * Maximum value for the count attribute of relationships 57 | * @type {number} 58 | */ 59 | let maxRelationshipCount = 0; 60 | 61 | 62 | /**--------------------------------------------------------------------------------------------------------------------- 63 | * Callbacks 64 | *-------------------------------------------------------------------------------------------------------------------*/ 65 | /** 66 | * Reload the database properties whenever the database selection is changed 67 | */ 68 | $(document).on("change", "#databaseName", fillFields); 69 | 70 | $(document).on("click", "#connect", loadDatabaseProperties); 71 | 72 | /** 73 | * When the 'Show whole graph' button is clicked, send a request to the server for the whole graph 74 | */ 75 | $(document).on("click",'#showWholeGraph', function(e) { 76 | e.preventDefault(); 77 | let btn = $(this); 78 | btn.addClass("loading"); 79 | const session = driver.session(); 80 | session.run("MATCH (n)-[r]->(m) WITH * LIMIT 150 RETURN collect(distinct m)+collect(distinct n) as nodes, collect(distinct r) as rels") 81 | .then(data => { 82 | useDefaultLabel = true; 83 | useForceLayout = false; 84 | drawGraph(fromCypher(data), true); 85 | btn.removeClass("loading"); 86 | session.close(); 87 | }); 88 | }); 89 | 90 | $(document).on("click",'#showMetaGraph', function(e) { 91 | e.preventDefault(); 92 | let btn = $(this); 93 | btn.addClass("loading"); 94 | const session = driver.session(); 95 | session.run("CALL apoc.meta.graph() yield nodes, relationships as rels return *") 96 | .then(data => { 97 | useDefaultLabel = true; 98 | useForceLayout = false; 99 | drawGraph(fromCypher(data), true); 100 | btn.removeClass("loading"); 101 | session.close(); 102 | }); 103 | }); 104 | 105 | /** 106 | * Whenever one of the view options is changed, redraw the graph 107 | */ 108 | $(document).on("change", '.redraw', function() { 109 | drawGraph(bufferedData, false); 110 | }); 111 | 112 | function fromCypher(data) { 113 | return data.records.map(r => ({ 114 | nodes: r.get('nodes').map(n => ({ 115 | group: 'nodes', 116 | data: {properties: convertNumbers(n.properties), id: n.identity.toNumber(), label: n.labels[0]}, 117 | labels: n.labels 118 | })), 119 | relationships: r.get('rels').map(r => ({ 120 | group: 'edges', 121 | data: { 122 | properties: convertNumbers(r.properties), 123 | id: r.identity.toNumber(), 124 | label: r.type, 125 | source: r.start.toNumber(), 126 | target: r.end.toNumber() 127 | }, 128 | type: r.type 129 | })) 130 | }))[0]; 131 | } 132 | 133 | /** 134 | * When the 'Execute' button is clicked, construct a request and send it to the server 135 | */ 136 | $(document).on('click', ".execute-button", function () { 137 | let btn = $(this); 138 | btn.addClass("loading"); 139 | let reqData = { 140 | dbName: getSelectedDatabase(), 141 | nodeKeys: getValues("#nodePropertyKeys"), 142 | relationshipKeys: getValues("#relationshipPropertyKeys"), 143 | nodeAggrFuncs: getValues("#nodeAggrFuncs"), 144 | relationshipAggrFuncs: getValues("#relationshipAggrFuncs"), 145 | nodeFilters: getValues("#nodeFilters"), 146 | relationshipFilters: getValues("#relationshipFilters"), 147 | filterAllRelationships: getValues("#relationshipFilters") === ["none"] 148 | }; 149 | /* 150 | reqData.nodeFilters=["User"]; 151 | reqData.relationshipFilters=["KNOWS"]; 152 | reqData.nodeKeys=["country"]; 153 | reqData.relationshipKeys=[]; 154 | reqData.nodeAggrFuncs=["count *","max age"]; 155 | reqData.relationshipAggrFuncs=["count *"]; 156 | */ 157 | let query = "call apoc.nodes.group($labels,$properties,$grouping,$config) yield node, relationship return collect(distinct node) as nodes, collect(distinct relationship) as rels"; 158 | 159 | let config = {}; 160 | if ((reqData.relationshipFilters||[]).length > 0) config['includeRels'] = reqData.relationshipFilters; 161 | // orphans, exclude rels, self-relationships 162 | 163 | 164 | let nodeAggr = reqData.nodeAggrFuncs.length > 0 ? reqData.nodeAggrFuncs.map(a => a.split(" ")).reduce(multiMerge, {}) : {"*":"count"}; 165 | let relationshipAggr = reqData.relationshipAggrFuncs.length > 0 ? reqData.relationshipAggrFuncs.map(a => a.split(" ")).reduce(multiMerge,{}) : {"*":"count"}; 166 | 167 | let params = { 168 | labels: reqData.nodeFilters.length > 0 ? reqData.nodeFilters : ['*'], 169 | properties: reqData.nodeKeys.concat(reqData.relationshipKeys), 170 | grouping: [nodeAggr, relationshipAggr], config: config 171 | }; 172 | 173 | const session = driver.session(); 174 | session.run(query, params).then(data => { 175 | useDefaultLabel = false; 176 | useForceLayout = true; 177 | drawGraph(fromCypher(data), true); 178 | btn.removeClass('loading'); 179 | session.close(); 180 | }); 181 | }); 182 | 183 | function multiMerge(agg, pair) { 184 | let x = (agg[pair[1]] || []); 185 | x.push(pair[0]); 186 | agg[pair[1]] = x; 187 | return agg; 188 | } 189 | 190 | function convertNumbers(data) { 191 | for (key in Object.keys(data)) { 192 | let val = data[key]; 193 | if (neo4j.v1.isInt(val)) data[key] = val.toInt(); 194 | } 195 | return data; 196 | } 197 | 198 | /** 199 | * Runs when the DOM is ready 200 | */ 201 | $(document).ready(function () { 202 | window.connections={}; 203 | window.driver = null; 204 | connections['localhost'] = {url:"bolt://localhost", user:"neo4j", password:"test"}; 205 | connections['community'] = {url:"bolt://138.197.15.1:7687", user:"all", password:"readonly"}; 206 | cy = buildCytoscape(); 207 | $('select').select2(); 208 | }); 209 | 210 | /**--------------------------------------------------------------------------------------------------------------------- 211 | * Graph Drawing 212 | *-------------------------------------------------------------------------------------------------------------------*/ 213 | function buildCytoscape() { 214 | return cytoscape({ 215 | container: document.getElementById('canvas'), 216 | style: cytoscape.stylesheet() 217 | .selector('node') 218 | .css({ 219 | // define label content and font 220 | 'content': function (node) { 221 | 222 | let labelString = getLabel(node, getNodeLabelKey(), useDefaultLabel); 223 | 224 | let properties = node.data('properties'); 225 | let values = Object.keys(properties).map(k => properties[k]).filter(v => v != null).join(", "); 226 | // todo order of aggregation + 227 | labelString += '\n('+ values + ')'; 228 | 229 | if (labelString.length > 20) labelString = labelString.substring(0,20)+"..."; 230 | /* 231 | if (properties['count_*'] != null) { 232 | labelString += ' (' + properties['count_*'] + ')'; 233 | } 234 | */ 235 | return labelString; 236 | }, 237 | // if the count shall effect the node size, set font size accordingly 238 | 'font-size': function (node) { 239 | if ($('#showCountAsSize').is(':checked')) { 240 | let count = node.data('properties')['count_*']; 241 | if (count != null) { 242 | count = count / maxNodeCount; 243 | // surface of vertices is proportional to count 244 | return Math.max(2, Math.sqrt(count * 10000 / Math.PI)); 245 | } 246 | } 247 | return 10; 248 | }, 249 | 'text-valign': 'center', 250 | 'color': 'black', 251 | // this function changes the text color according to the background color 252 | // unnecessary atm because only light colors can be generated 253 | /* function (vertices) { 254 | let label = getLabel(vertices, nodeLabelKey, useDefaultLabel); 255 | let bgColor = colorMap[label]; 256 | if (bgColor[0] + bgColor[1] + (bgColor[2] * 0.7) < 300) { 257 | return 'white'; 258 | } 259 | return 'black'; 260 | },*/ 261 | // set background color according to color map 262 | 'background-color': function (node) { 263 | let label = getLabel(node, getNodeLabelKey(), useDefaultLabel); 264 | let color = colorMap[label]; 265 | let result = '#'; 266 | result += ('0' + color[0].toString(16)).substr(-2); 267 | result += ('0' + color[1].toString(16)).substr(-2); 268 | result += ('0' + color[2].toString(16)).substr(-2); 269 | return result; 270 | }, 271 | 272 | /* size of vertices can be determined by property count 273 | count specifies that the node stands for 274 | 1 or more other vertices */ 275 | 'width': function (node) { 276 | if ($('#showCountAsSize').is(':checked')) { 277 | let count = node.data.properties['count_*']; 278 | if (count !== null) { 279 | count = count / maxNodeCount; 280 | // surface of node is proportional to count 281 | return Math.sqrt(count * 1000000 / Math.PI) + 'px'; 282 | } 283 | } 284 | return '60px'; 285 | 286 | }, 287 | 'height': function (node) { 288 | if ($('#showCountAsSize').is(':checked')) { 289 | let count = node.data.properties['count_*']; 290 | if (count !== null) { 291 | count = count / maxNodeCount; 292 | // surface of node is proportional to count 293 | return Math.sqrt(count * 1000000 / Math.PI) + 'px'; 294 | } 295 | } 296 | return '60px'; 297 | }, 298 | 'text-wrap': 'wrap' 299 | }) 300 | .selector('edge') 301 | .css({ 302 | 'curve-style': 'bezier', 303 | // layout of relationship and relationship label 304 | 'content': function (relationship) { 305 | 306 | if (!$('#showRelationshipLabels').is(':checked')) { 307 | return ''; 308 | } 309 | 310 | let labelString = getLabel(relationship, getRelationshipLabelKey(), useDefaultLabel); 311 | 312 | let properties = relationship.data('properties'); 313 | 314 | if (properties['count_*'] !== null) { 315 | labelString += ' (' + properties['count_*'] + ')'; 316 | } 317 | 318 | return labelString; 319 | }, 320 | // if the count shall effect the node size, set font size accordingly 321 | 'font-size': function (relationship) { 322 | if ($('#showCountAsSize').is(':checked')) { 323 | let count = node.data('properties')['count_*']; 324 | if (count !== null) { 325 | count = count / maxNodeCount; 326 | // surface of vertices is proportional to count 327 | return Math.max(2, Math.sqrt(count * 10000 / Math.PI)); 328 | } 329 | } 330 | return 10; 331 | }, 332 | 'line-color': '#999', 333 | // width of relationships can be determined by property count 334 | // count specifies that the relationship represents 1 or more other relationships 335 | 'width': function (relationship) { 336 | if ($('#showCountAsSize').is(':checked')) { 337 | let count = relationship.data('properties')['count_*']; 338 | if (count !== null) { 339 | count = count / maxRelationshipCount; 340 | return Math.sqrt(count * 1000); 341 | } 342 | } 343 | return 2; 344 | }, 345 | 'target-arrow-shape': 'triangle', 346 | 'target-arrow-color': '#000' 347 | }) 348 | // properties of relationships and vertices in special states, e.g. invisible or faded 349 | .selector('.faded') 350 | .css({ 351 | 'opacity': 0.25, 352 | 'text-opacity': 0 353 | }) 354 | .selector('.invisible') 355 | .css({ 356 | 'opacity': 0, 357 | 'text-opacity': 0 358 | }), 359 | ready: function () { 360 | window.cy = this; 361 | cy.elements().unselectify(); 362 | /* if a node is selected, fade all relationships and vertices 363 | that are not in direct neighborhood of the node */ 364 | cy.on('tap', 'node', function (e) { 365 | let node = e.cyTarget; 366 | let neighborhood = node.neighborhood().add(node); 367 | 368 | cy.elements().addClass('faded'); 369 | neighborhood.removeClass('faded'); 370 | }); 371 | // remove fading by clicking somewhere else 372 | cy.on('tap', function (e) { 373 | 374 | if (e.cyTarget === cy) { 375 | cy.elements().removeClass('faded'); 376 | } 377 | }); 378 | } 379 | }); 380 | } 381 | 382 | /** 383 | * function called when the server returns the data 384 | * @param data graph data 385 | * @param initial indicates whether the data is drawn initially 386 | */ 387 | function drawGraph(data, initial = true) { 388 | // lists of vertices and relationships 389 | let nodes = data.nodes; 390 | let relationships = data.relationships; 391 | 392 | if(initial) { 393 | // buffer the data to speed up redrawing 394 | bufferedData = data; 395 | 396 | // compute maximum count of all vertices, used for scaling the node sizes 397 | maxNodeCount = nodes.reduce((acc, node) => { 398 | return Math.max(acc, Number(node.data.properties['count_*']||0)) 399 | }, 0); 400 | 401 | let labels = new Set(nodes.map((node) => { 402 | return (!useDefaultLabel && getNodeLabelKey() !== 'label') ? 403 | node['data']['properties'][getNodeLabelKey()] : node['data']['label'] 404 | })); 405 | 406 | // generate random colors for the node labels 407 | console.log(labels); 408 | generateRandomColors(labels); 409 | console.log(colorMap); 410 | // compute maximum count of all relationships, used for scaling the relationship sizes 411 | maxRelationshipCount = relationships.reduce((acc, relationship) => { 412 | return Math.max(acc, Number(relationship.data.properties['count_*']||0)) 413 | }, 0); 414 | } 415 | 416 | cy.elements().remove(); 417 | cy.add(nodes); 418 | cy.add(relationships); 419 | 420 | if ($('#hideNullGroups').is(':checked')) { 421 | hideNullGroups(); 422 | } 423 | 424 | if ($('#hideDisconnected').is(':checked')) { 425 | hideDisconnected(); 426 | } 427 | 428 | addQtip(); 429 | 430 | cy.layout(chooseLayout()); 431 | } 432 | 433 | 434 | function chooseLayout() { 435 | // options for the force layout 436 | let cose = { 437 | name: 'cose', 438 | 439 | // called on `layoutready` 440 | ready: function () { 441 | }, 442 | 443 | // called on `layoutstop` 444 | stop: function () { 445 | }, 446 | 447 | // whether to animate while running the layout 448 | animate: true, 449 | 450 | // number of iterations between consecutive screen positions update (0 -> 451 | // only updated on the end) 452 | refresh: 4, 453 | 454 | // whether to fit the network view after when done 455 | fit: true, 456 | 457 | // padding on fit 458 | padding: 30, 459 | 460 | // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h } 461 | boundingBox: undefined, 462 | 463 | // whether to randomize node positions on the beginning 464 | randomize: true, 465 | 466 | // whether to use the JS console to print debug messages 467 | debug: false, 468 | 469 | // node repulsion (non overlapping) multiplier 470 | nodeRepulsion: 8000000, 471 | 472 | // node repulsion (overlapping) multiplier 473 | nodeOverlap: 10, 474 | 475 | // ideal relationship (non nested) length 476 | idealRelationshipLength: 1, 477 | 478 | // divisor to compute relationship forces 479 | relationshipElasticity: 100, 480 | 481 | // nesting factor (multiplier) to compute ideal relationship length for nested relationships 482 | nestingFactor: 5, 483 | 484 | // gravity force (constant) 485 | gravity: 250, 486 | 487 | // maximum number of iterations to perform 488 | numIter: 100, 489 | 490 | // initial temperature (maximum node displacement) 491 | initialTemp: 200, 492 | 493 | // cooling factor (how the temperature is reduced between consecutive iterations 494 | coolingFactor: 0.95, 495 | 496 | // lower temperature threshold (below this point the layout will end) 497 | minTemp: 1.0 498 | }; 499 | 500 | let radialRandom = { 501 | name: 'preset', 502 | positions: function() { 503 | 504 | let r = random() * 1000001; 505 | let theta = random() * 2 * (Math.PI); 506 | return { 507 | x: Math.sqrt(r) * Math.sin(theta), 508 | y: Math.sqrt(r) * Math.cos(theta) 509 | }; 510 | }, 511 | zoom: undefined, 512 | pan: undefined, 513 | fit: true, 514 | padding: 30, 515 | animate: false, 516 | animationDuration: 500, 517 | animationEasing: undefined, 518 | ready: undefined, 519 | stop: undefined 520 | }; 521 | 522 | if (useForceLayout) { 523 | return cose; 524 | } else { 525 | return radialRandom; 526 | } 527 | } 528 | 529 | /** 530 | * Add a custom Qtip to the vertices and relationships of the graph. 531 | */ 532 | function addQtip() { 533 | cy.elements().qtip({ 534 | content: function () { 535 | let qtipText = ''; 536 | for (let [key, value] of Object.entries(this.data())) { 537 | if (key !== 'properties' && key !== 'pie_parameters') { 538 | qtipText += key + ' : ' + value + '
'; 539 | } 540 | } 541 | for (let [key, value] of Object.entries(this.data('properties'))) { 542 | qtipText += key + ' : ' + value + '
'; 543 | } 544 | return qtipText; 545 | }, 546 | position: { 547 | my: 'top center', 548 | at: 'bottom center' 549 | }, 550 | style: { 551 | classes: 'MyQtip' 552 | } 553 | }); 554 | } 555 | 556 | /** 557 | * Hide all vertices and relationships, that have a NULL property. 558 | */ 559 | function hideNullGroups() { 560 | let nodeKeys = getValues("#nodePropertyKeys"); 561 | let relationshipKeys = getValues("#relationshipPropertyKeys"); 562 | 563 | let nodes = []; 564 | for(let i = 0; i < cy.nodes().length; i++) { 565 | nodes[i] = cy.nodes()[i] 566 | } 567 | 568 | let relationships = []; 569 | for(let i = 0; i < cy.relationships().length; i++) { 570 | relationships[i] = cy.relationships()[i]; 571 | } 572 | 573 | nodes 574 | .filter(node => nodeKeys.find((key) => node.data().properties[key] === "NULL")) 575 | .forEach(node => node.remove()); 576 | 577 | relationships 578 | .filter(relationship => relationshipKeys.find((key) => relationship.data().properties[key] === "NULL")) 579 | .forEach(relationship => relationship.remove()); 580 | } 581 | 582 | /** 583 | * Function to hide all disconnected vertices (vertices without relationships). 584 | */ 585 | function hideDisconnected() { 586 | let nodes = []; 587 | for(let i = 0; i < cy.nodes().length; i++) { 588 | nodes[i] = cy.nodes()[i] 589 | } 590 | 591 | nodes.filter(node => { 592 | return (cy.relationships('[source="' + node.id() + '"]').length === 0) 593 | && (cy.relationships('[target="' + node.id() + '"]').length === 0) 594 | }).forEach(node => node.remove()); 595 | } 596 | 597 | /**--------------------------------------------------------------------------------------------------------------------- 598 | * UI Initialization 599 | *-------------------------------------------------------------------------------------------------------------------*/ 600 | 601 | function fillFields() { 602 | let databaseName = getSelectedDatabase() || 'localhost'; 603 | let con = connections[databaseName] || connections["localhost"]; 604 | $('#url').val(con.url); 605 | $('#user').val(con.user); 606 | $('#password').val(con.password); 607 | $('#showMetaGraph').attr('disabled','disabled'); 608 | $('#showWholeGraph').attr('disabled','disabled'); 609 | $('#execute').attr('disabled','disabled'); 610 | } 611 | 612 | /** 613 | * Initialize the database menu according to the selected database 614 | */ 615 | function loadDatabaseProperties() { 616 | if (driver !== null) driver.close(); 617 | driver = neo4j.v1.driver($('#url').val(), neo4j.v1.auth.basic($('#user').val(), $('#password').val())); 618 | const session = driver.session(); 619 | session.run(` 620 | CALL db.labels() yield label return label as name, 'label' as type 621 | UNION ALL 622 | CALL db.relationshipTypes() yield relationshipType return relationshipType as name, 'type' as type 623 | UNION ALL 624 | CALL db.propertyKeys() yield propertyKey return propertyKey as name, 'prop' as type 625 | `).then(result => { 626 | let labels = result.records.filter(r => r.get('type') === 'label').map(record => record.get('name')); 627 | let types = result.records.filter(r => r.get('type') === 'type').map(record => record.get('name')); 628 | let keys = result.records.filter(r => r.get('type') === 'prop').map(record => ({name:record.get('name'), numerical:true})); // TODO 629 | session.close(); 630 | let data = {nodeLabels:labels, relationshipLabels:types,nodeKeys:keys, relationshipKeys:keys}; 631 | initializeFilterKeyMenus(data); 632 | initializePropertyKeyMenus(data); 633 | initializeAggregateFunctionMenus(data); 634 | $('#showMetaGraph').removeAttr('disabled'); 635 | $('#showWholeGraph').removeAttr('disabled'); 636 | $('#execute').removeAttr('disabled'); 637 | }); 638 | return false; 639 | /* 640 | $.post('http://localhost:2342/keys/' + databaseName, function(response) { 641 | initializeFilterKeyMenus(response); 642 | initializePropertyKeyMenus(response); 643 | initializeAggregateFunctionMenus(response); 644 | }, "json"); 645 | */ 646 | } 647 | 648 | /** 649 | * Initialize the filter menus with the labels 650 | * @param keys labels of the input vertices 651 | */ 652 | function initializeFilterKeyMenus(keys) { 653 | let nodeFilters = $('#nodeFilters'); 654 | let relationshipFilters = $('#relationshipFilters'); 655 | 656 | // clear previous entries 657 | nodeFilters.html(""); 658 | relationshipFilters.html(""); 659 | 660 | 661 | // add one entry per node label 662 | keys.nodeLabels.forEach(label => { 663 | nodeFilters.append($("")) 664 | }); 665 | 666 | keys.relationshipLabels.forEach(label => { 667 | relationshipFilters.append($("")) 668 | }); 669 | relationshipFilters.append($("")) 670 | 671 | } 672 | 673 | /** 674 | * Initialize the key propertyKeys menus. 675 | * @param keys array of node and relationship keys 676 | */ 677 | function initializePropertyKeyMenus(keys) { 678 | // get the propertyKeys menus in their current form 679 | let nodePropertyKeys = $('#nodePropertyKeys'); 680 | let relationshipPropertyKeys = $('#relationshipPropertyKeys'); 681 | 682 | // clear previous entries 683 | nodePropertyKeys.html(""); 684 | relationshipPropertyKeys.html(""); 685 | 686 | // add default key (label) 687 | nodePropertyKeys.append($("")); 688 | relationshipPropertyKeys.append($("")); 689 | 690 | // add one entry per property key 691 | keys.nodeKeys.forEach(key => { 692 | nodePropertyKeys.append($("")) 693 | }); 694 | 695 | keys.relationshipKeys.forEach(key => { 696 | relationshipPropertyKeys.append($("")) 697 | }); 698 | } 699 | 700 | /** 701 | * initialize the aggregate function propertyKeys menu 702 | */ 703 | function initializeAggregateFunctionMenus(keys) { 704 | let nodeAggrFuncs = $('#nodeAggrFuncs'); 705 | let relationshipAggrFuncs = $('#relationshipAggrFuncs'); 706 | 707 | // clear previous entries 708 | nodeAggrFuncs.html(""); 709 | relationshipAggrFuncs.html(""); 710 | 711 | // add default key (label) 712 | nodeAggrFuncs.append($("")); 713 | relationshipAggrFuncs.append($("")); 714 | 715 | // add one entry per property key 716 | keys.nodeKeys 717 | .filter(k => {return k.numerical}) 718 | .forEach(key => { 719 | aggrPrefixes.forEach(prefix => { 720 | let functionName = prefix + key.name; 721 | nodeAggrFuncs.append($("")) 722 | }); 723 | }); 724 | 725 | keys.relationshipKeys 726 | .filter(k => {return k.numerical}) 727 | .forEach(key => { 728 | aggrPrefixes.forEach(prefix => { 729 | let functionName = prefix + key.name; 730 | relationshipAggrFuncs.append($("")) 731 | }); 732 | }); 733 | } 734 | 735 | /**--------------------------------------------------------------------------------------------------------------------- 736 | * Utility Functions 737 | *-------------------------------------------------------------------------------------------------------------------*/ 738 | 739 | var seed = 32453; 740 | function random(s) { 741 | if (!s) s=seed; 742 | var x; 743 | do { 744 | x = Math.sin(s++) * 10000; 745 | x = x - Math.floor(x); 746 | } while(x < 0.15 || x > 0.9); 747 | seed = s; 748 | return (x-0.15) * 1 / 0.75; 749 | } 750 | /** 751 | * Generate a random color for each label 752 | * @param labels array of labels 753 | */ 754 | // todo stable colors per label 755 | function generateRandomColors(labels) { 756 | colorMap = {}; 757 | random(32453); 758 | labels.forEach(function (label) { 759 | let r = 0; 760 | let g = 0; 761 | let b = 0; 762 | while (r + g + b < 382) { 763 | r = Math.floor((random() * 255)); 764 | g = Math.floor((random() * 255)); 765 | b = Math.floor((random() * 255)); 766 | } 767 | colorMap[label] = [r, g, b]; 768 | }); 769 | } 770 | 771 | /** 772 | * Get the label of the given element, either the default label ('label') or the value of the 773 | * given property key 774 | * @param element the element whose label is needed 775 | * @param key key of the non-default label 776 | * @param useDefaultLabel boolean specifying if the default label shall be used 777 | * @returns {string} the label of the element 778 | */ 779 | function getLabel(element, key, useDefaultLabel) { 780 | let label = ''; 781 | if (!useDefaultLabel && key !== 'label') { 782 | label += element.data('properties')[key]; 783 | } else { 784 | label += element.data('label'); 785 | } 786 | return label; 787 | } 788 | 789 | /** 790 | * get the selected database 791 | * @returns selected database name 792 | */ 793 | function getSelectedDatabase() { 794 | return $('#databaseName').val(); 795 | } 796 | 797 | /** 798 | * Retrieve the values of the specified element as Array 799 | * @param element the html element 800 | * @returns {Array} 801 | */ 802 | function getValues(element) { 803 | return $(element).val() || [] 804 | } 805 | 806 | function getNodePropertyKeys() { 807 | return getValues("#nodePropertyKeys"); 808 | } 809 | /** 810 | * Property keys that are used to specify the node and relationship labels. 811 | */ 812 | function getNodeLabelKey() { 813 | let values = getNodePropertyKeys(); 814 | return values.length === 0 ? "label" : values[0]; 815 | } 816 | 817 | function getRelationshipPropertyKeys() { 818 | return getValues("#relationshipPropertyKeys"); 819 | } 820 | function getRelationshipLabelKey() { 821 | let values = getRelationshipPropertyKeys(); 822 | return values.length === 0 ? "label" : values[0]; 823 | } 824 | --------------------------------------------------------------------------------