From ed044b083f7f730e6d64a6633c17670d616be1c8 Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Tue, 20 Sep 2016 12:14:18 +0200 Subject: [PATCH 1/2] Added Cypher import (first pass) --- cypher.js | 142 +++++++++++++++++++++++++++++++++++++++++++++--- graph-editor.js | 65 ++++++++++++++++++++-- index.html | 13 +++-- 3 files changed, 201 insertions(+), 19 deletions(-) diff --git a/cypher.js b/cypher.js index ccf3608..b19dbe1 100644 --- a/cypher.js +++ b/cypher.js @@ -1,4 +1,5 @@ -function cypher(model) { +function Cypher() { + this.format = function(model) { function props(element) { var props = {}; element.properties().list().forEach(function (property) { @@ -12,6 +13,7 @@ function cypher(model) { } function quote(name) { + if (name == undefined || name == null || name.trim() == "") return null; return isIdentifier(name) ? name : "`" + name + "`"; } @@ -30,17 +32,141 @@ function cypher(model) { var statements = []; model.nodeList().forEach(function (node) { - statements.push("(" + quote(node.id) +" :" + quote(node.caption() || "Node") + " " + render(props(node)) + ") "); + var labels = node.caption().split(/[:\s]+/).map(quote).filter(function(l) { return l !== undefined }).map(function(l) { return ":" + l;}).join(""); + statements.push("(" + quote(""+node.id) + labels + " " + render(props(node)) + ") "); }); model.relationshipList().forEach(function (rel) { statements.push("(" + quote(rel.start.id) + - ")-[:`" + quote(rel.relationshipType()||"RELATED_TO") + - // " " + TODO render(props(rel)) + - "`]->("+ quote(rel.end.id) +")" + ")-[:" + quote(rel.relationshipType()||"RELATED_TO") + + " " + render(props(rel)) + + "]->("+ quote(rel.end.id) +")" ); }); if (statements.length==0) return ""; return "CREATE \n " + statements.join(",\n "); -}; -if (typeof exports != "undefined") exports.cypher=cypher -gd.cypher=function(model) {return cypher(model || this.model());} +} +this.parse = function(cypher, opts) { + if (typeof(cypher) != "string") { + console.log("Cannot parse",cypher) + return {nodes:[],links:[]} + } + var time = Date.now(); + var keep_names = opts && opts.keep_names; + var nodes = {} + var rels = [] + var PARENS = /(\s*\),?\s*|\s*\(\s*)/; + function toArray(map) { + var res = []; + for (var k in map) { + if (map.hasOwnProperty(k)) { + res.push(map[k]); + } + } + return res; + } + function splitClean(str, pattern, clean) { + if (clean) { str = str.replace(clean,""); } + var r = str.split(pattern) + .map(function(s) { return s.trim(); }) + .filter(function(s) { return s.trim().length > 0 && !s.match(pattern); }); + return r; + } + function keyIndex(key,map) { + var count=0; + for (k in map) { + if (key == k) return count; + count+=1; + } + return -1; + } + cypher = cypher.replace(/CREATE/ig,""); + var parts = splitClean(cypher,PARENS); + var id=0; + var lastNode, lastRel; + var NODE_PATTERN=/^\s*(`[^`]+`|\w+)\s*((?::\w+|:`[^`]+`)*)\s*(\{.+\})?\s*$/; + var REL_PATTERN=/^(<)?\s*-\s*(?:\[(`[^`]+`|\w+)?\s*(:(?:`[^`]+`|[\w]+))?\s*(\{.+\})?\])?\s*-\s*(>)?$/; + var PROP_PATTERN=/^\s*`?(\w+)`?\s*:\s*(".+?"|'.+?'|\[.+?\]|.+?)\s*(,\s*|$)/; + var ARRAY_VALUES_PATTERN=/^\s*(".+?"|'.+?'|.+?)\s*(,\s*|$)/; + parts.forEach(function(p,i) { + function parseProps(node,props) { + function escapeQuotes(value) { + value = value.trim().replace(/(^|\W)'([^']*?)'(\W|$)/g,'$1"$2"$3'); + if (value[0]=='"') value = '"'+value.substring(1,value.length-1).replace(/"/g,'\\"') + '"'; + return value; + } + function parseArray(value) { + value = value.substring(1,value.length-1); // eliminate [] + var res=""; + while (_val = value.match(ARRAY_VALUES_PATTERN)) { + value = value.substring(_val[0].length); // next part + var element = escapeQuotes(_val[1]); + if (res!="") res += ","; + res += element; + } + return "[" + res + "]"; + } + function isArray(value) { return value[0] == "["; } + var prop = null; + props = props.substring(1,props.length-1); // eliminate {} + while (prop = props.match(PROP_PATTERN)) { + props = props.substring(prop[0].length); // next part + var pname = prop[1]; + var value = prop[2]; + value = isArray(value) ? parseArray(value) : escapeQuotes(value); + node[pname]=JSON.parse(value); + } + return node; + } + function parseInner(m) { + var name=m[1] ? m[1].replace(/`/g,"") : m[1]; + var labels=[]; + + var props=""; // TODO ugly + if (m.length > 1) { + if (m[2] && m[2][0]==":") labels = splitClean(m[2],/:/,/`/g); /*//*/ + else props=m[2] || ""; + if (m.length>2 && m[3] && m[3][0]=="{") props=m[3]; + } + + return parseProps( {_id:id,_name:name,_labels:labels}, props); + } + var m = null; + if (m = p.match(NODE_PATTERN)) { + var node = parseInner(m); + var name=node["_name"]; + if (!keep_names) delete(node["_name"]); + if (!nodes[name]) { + nodes[name]=node; + id += 1; + } + lastNode=name; + if (lastRel) { + if (lastRel.source===null) lastRel.source=keyIndex(name,nodes); + if (lastRel.target===null) lastRel.target=keyIndex(name,nodes); + } + } else { + if (m = p.match(REL_PATTERN)){ + var incoming = m[1]=="<" && m[5]!=">"; + m.splice(5,1); m.splice(1,1); + var rel=parseInner(m); + rel["_type"]=rel["_labels"][0]; + if (!keep_names) delete(rel["_name"]); + delete(rel["_id"]);delete(rel["_labels"]); + rel["source"]= incoming ? null : keyIndex(lastNode,nodes); + rel["target"]= incoming ? keyIndex(lastNode,nodes) : null; + lastRel=rel; + rels.push(rel); + } + } + }) + if (opts && opts.measure) console.log("time",Date.now()-time); + return {nodes: toArray(nodes), links: rels}; + } +} + +if (typeof exports != "undefined") { + exports.cypher=Cypher +} + +gd.formatCypher=function(model) {return new Cypher().format(model || this.model());} +gd.parseCypher=function(model) {return new Cypher().parse(model || this.model());} diff --git a/graph-editor.js b/graph-editor.js index 4d4fd6d..9f82614 100644 --- a/graph-editor.js +++ b/graph-editor.js @@ -322,7 +322,9 @@ window.onload = function() } d3.selectAll( ".btn.cancel" ).on( "click", cancelModal ); - d3.selectAll( ".modal" ).on( "keyup", function() { if ( d3.event.keyCode === 27 ) cancelModal(); } ); + d3.selectAll( ".modal" ).on( "keyup", + function() { if ( d3.event.keyCode === 27 ) cancelModal(); } + ); function appendModalBackdrop() { @@ -338,7 +340,7 @@ window.onload = function() var markup = formatMarkup(); d3.select( "textarea.code" ) - .attr( "rows", markup.split( "\n" ).length * 2 ) + .attr( "rows", Math.max(10,markup.split( "\n" ).length * 2) ) .node().value = markup; }; @@ -360,8 +362,6 @@ window.onload = function() cancelModal(); }; - d3.select( "#save_markup" ).on( "click", useMarkupFromMarkupEditor ); - var exportSvg = function () { var rawSvg = new XMLSerializer().serializeToString(d3.select("#canvas svg" ).node()); @@ -380,6 +380,56 @@ window.onload = function() return true; }; + var useCypherFromEditor = function () + { + var cypher = d3.select( ".export-cypher .modal-body textarea.code" ).node().value; + d3Model = gd.parseCypher( cypher ); + graphModel = modelFromD3( d3Model ); + save(formatMarkup()); + draw(); + cancelModal(); + }; + + var modelFromD3 = function( data ) { + function convert(value) { + if (typeof(value) == "string" && value.length > 20) return value.substring(0,20)+" ..."; + return value; + } + var width = 500; + var height = 500; + var progress = 0; + var selection = d3.select("#tmp ul.graph-diagram-markup"); + var model = gd.markup.parse(selection); // only to copy style attributes + data.nodes.forEach(function(nodeData) { + var id = parseInt(nodeData["_id"]); + var node = model.createNode(id); + node.class("node"); + var angle = 0.6 * Math.PI * progress; + node.x(nodeData["x"] || Math.cos(angle) * width * 0.3 * Math.round(1 + progress / 3) + width); + node.y(nodeData["y"] || Math.sin(angle) * height * 0.3 * Math.round(1 + progress / 3) + height); + progress += 1; + node.caption(nodeData["_labels"].join(" ")) + Object.keys(nodeData).forEach(function(prop) { + if (!["_id","class","x","y","_labels"].includes(prop)) { + var value = ; + if (typeof(value) == "string" && value.length > 20) value = value.substring(0,20)+" ..."; + node.properties().set(prop,convert(nodeData[prop])); + } + }) + }) + data.links.forEach(function(relData) { + var rel = model.createRelationship(model.lookupNode(relData["source"]),model.lookupNode(relData["target"])); + rel.class("relationship"); + rel.relationshipType(relData["_type"]) + Object.keys(relData).forEach(function(prop) { + if (!["_id","class","x","y","_type","source","target"].includes(prop)) { + rel.properties().set(prop,convert(relData[prop])); + } + }) + }) + return model; + } + d3.select( "#open_console" ).on( "click", openConsoleWithCypher ); var exportCypher = function () @@ -387,9 +437,9 @@ window.onload = function() appendModalBackdrop(); d3.select( ".modal.export-cypher" ).classed( "hide", false ); - var statement = gd.cypher(graphModel); + var statement = gd.formatCypher(graphModel); d3.select( ".export-cypher .modal-body textarea.code" ) - .attr( "rows", statement.split( "\n" ).length ) + .attr( "rows", Math.max(10,statement.split( "\n" ).length) ) .node().value = statement; }; @@ -429,5 +479,8 @@ window.onload = function() d3.event.stopPropagation(); } ); + d3.select( "#save_markup" ).on( "click", useMarkupFromMarkupEditor ); + d3.select( "#save_cypher" ).on( "click", useCypherFromEditor ); + draw(); }; diff --git a/index.html b/index.html index a0852ca..bd918b9 100644 --- a/index.html +++ b/index.html @@ -26,7 +26,7 @@ ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName( 'script' )[0]; s.parentNode.insertBefore( ga, s ); - })(); + }) // (); @@ -49,7 +49,7 @@

Edit/Export markup

- +
+ +
\ No newline at end of file From 47ff1907a7154404930d9c5d8bc6aa05d519364c Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Tue, 20 Sep 2016 12:24:29 +0200 Subject: [PATCH 2/2] fixed WIP bug --- graph-editor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graph-editor.js b/graph-editor.js index 9f82614..8220584 100644 --- a/graph-editor.js +++ b/graph-editor.js @@ -411,9 +411,9 @@ window.onload = function() node.caption(nodeData["_labels"].join(" ")) Object.keys(nodeData).forEach(function(prop) { if (!["_id","class","x","y","_labels"].includes(prop)) { - var value = ; + var value = nodeData[prop]; if (typeof(value) == "string" && value.length > 20) value = value.substring(0,20)+" ..."; - node.properties().set(prop,convert(nodeData[prop])); + node.properties().set(prop,convert(value)); } }) })