├── .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 |
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 |
--------------------------------------------------------------------------------