diff --git a/src/org/rascalmpl/library/IO.rsc b/src/org/rascalmpl/library/IO.rsc index a425ed725a1..3725a6a2e05 100644 --- a/src/org/rascalmpl/library/IO.rsc +++ b/src/org/rascalmpl/library/IO.rsc @@ -2,7 +2,7 @@ Copyright (c) 2009-2022 CWI All rights reserved. This program and the accompanying materials are made available under the terms of the Eclipse Public License v1.0 - which accompanies this distribution, and is available at + which accompanies this distribution, and is available a http://www.eclipse.org/legal/epl-v10.html } @contributor{Jurgen J. Vinju - Jurgen.Vinju@cwi.nl - CWI} diff --git a/src/org/rascalmpl/library/lang/html/IO.java b/src/org/rascalmpl/library/lang/html/IO.java index 152ee130645..6fa605e9a4f 100644 --- a/src/org/rascalmpl/library/lang/html/IO.java +++ b/src/org/rascalmpl/library/lang/html/IO.java @@ -212,9 +212,9 @@ private IValue toTextConstructor(TextNode elem, ISourceLocation file) { * Why go through all the trouble of building a DOM? The only reason is compliance. * Escapes, encodings, etc. all are maintained by these classes from org.jdom. */ - public IString writeHTMLString(IConstructor cons, IString charset, IConstructor escapeMode, IBool outline, IBool prettyPrint, IInteger indentAmount, IInteger maxPaddingWidth, IConstructor syntax, IBool dropOrigins) { + public IString writeHTMLString(IConstructor cons, IString charset, IConstructor escapeMode, IBool outline, IBool prettyPrint, IInteger indentAmount, IInteger maxPaddingWidth, IConstructor syntax, IBool dropOrigins, IBool normalise) { try { - Document doc = createHTMLDocument(cons, dropOrigins.getValue()); + Document doc = createHTMLDocument(cons, dropOrigins.getValue(), normalise.getValue()); doc = doc.outputSettings(createOutputSettings(charset.getValue(), escapeMode.getName(), outline.getValue(), prettyPrint.getValue(), indentAmount.intValue(), maxPaddingWidth.intValue(), syntax.getName())); return factory.string(doc.outerHtml()); @@ -230,10 +230,10 @@ public IString writeHTMLString(IConstructor cons, IString charset, IConstructor * Why go through all the trouble of building a DOM? The only reason is compliance. * Escapes, encodings, etc. all are maintained by these classes from org.w3c.dom. */ - public void writeHTMLFile(ISourceLocation file, IConstructor cons, IString charset, IConstructor escapeMode, IBool outline, IBool prettyPrint, IInteger indentAmount, IInteger maxPaddingWidth, IConstructor syntax, IBool dropOrigins ) { + public void writeHTMLFile(ISourceLocation file, IConstructor cons, IString charset, IConstructor escapeMode, IBool outline, IBool prettyPrint, IInteger indentAmount, IInteger maxPaddingWidth, IConstructor syntax, IBool dropOrigins, IBool normalise) { try (Writer out = URIResolverRegistry.getInstance().getCharacterWriter(file, charset.getValue(), false)) { - Document doc = createHTMLDocument(cons, dropOrigins.getValue()); + Document doc = createHTMLDocument(cons, dropOrigins.getValue(), normalise.getValue()); doc = doc.outputSettings(createOutputSettings(charset.getValue(), escapeMode.getName(), outline.getValue(), prettyPrint.getValue(), indentAmount.intValue(), maxPaddingWidth.intValue(), syntax.getName())); out.write(doc.outerHtml()); } @@ -257,10 +257,11 @@ private OutputSettings createOutputSettings(String charset, String escapeMode, b /** * Translates a constructor tree to a jdom DOM tree, adding nodes where necessary to complete a html element. */ - private Document createHTMLDocument(IConstructor cons, boolean dropOrigins) throws IOException { + private Document createHTMLDocument(IConstructor cons, boolean dropOrigins, boolean normalize) throws IOException { Document doc = new Document("http://localhost"); - Node node = normalise(cons, createElement(cons, dropOrigins)); + Node element = createElement(cons, dropOrigins); + Node node = normalize ? normalise(cons, element) : element; doc.appendChild(node); return doc; diff --git a/src/org/rascalmpl/library/lang/html/IO.rsc b/src/org/rascalmpl/library/lang/html/IO.rsc index 77514f12c3f..d6c7346fdf3 100644 --- a/src/org/rascalmpl/library/lang/html/IO.rsc +++ b/src/org/rascalmpl/library/lang/html/IO.rsc @@ -44,8 +44,12 @@ java HTMLElement readHTMLString(str content, loc base=|http://localhost|, bool t @description{ This function uses [JSoup's](http://www.jsoup.org) DOM functionality to yield a syntactically correct (X)HTML string. + +* `normalise`: when true arbitrary HTML elements will be nested in a and a wrapper +* `dropOrigins`: any additional `src` origin attributes will not be serialized into the HTML document +* the other options are JSoup options. } -java str writeHTMLString(HTMLElement dom, str charset="UTF-8", HTMLEscapeMode escapeMode = baseMode(), bool outline=false, bool prettyPrint=true, int indentAmount=4, int maxPaddingWidth=30, HTMLSyntax \syntax=htmlSyntax(), bool dropOrigins=true); +java str writeHTMLString(HTMLElement dom, str charset="UTF-8", HTMLEscapeMode escapeMode = baseMode(), bool outline=false, bool prettyPrint=true, int indentAmount=4, int maxPaddingWidth=30, HTMLSyntax \syntax=htmlSyntax(), bool dropOrigins=true, bool normalise=true); @synopsis{Pretty-print the HTMLElement AST to a string} @description{ @@ -53,7 +57,7 @@ This function uses [JSoup's](http://www.jsoup.org) DOM functionality to yield a syntactically correct (X)HTML file. } @javaClass{org.rascalmpl.library.lang.html.IO} -java void writeHTMLFile(loc file, HTMLElement dom, str charset="UTF-8", HTMLEscapeMode escapeMode = baseMode(), bool outline=false, bool prettyPrint=true, int indentAmount=4, int maxPaddingWidth=30, HTMLSyntax \syntax=htmlSyntax(), bool dropOrigins=true); +java void writeHTMLFile(loc file, HTMLElement dom, str charset="UTF-8", HTMLEscapeMode escapeMode = baseMode(), bool outline=false, bool prettyPrint=true, int indentAmount=4, int maxPaddingWidth=30, HTMLSyntax \syntax=htmlSyntax(), bool dropOrigins=true, bool normalise=true); @synopsis{Convenience function to visualize an HTMLElement tree in the browser} Content serve(HTMLElement elem) = html(writeHTMLString(elem)); \ No newline at end of file diff --git a/src/org/rascalmpl/library/lang/rascal/vis/ImportGraph.rsc b/src/org/rascalmpl/library/lang/rascal/vis/ImportGraph.rsc index 881ffc376b7..7e6d7a109a1 100644 --- a/src/org/rascalmpl/library/lang/rascal/vis/ImportGraph.rsc +++ b/src/org/rascalmpl/library/lang/rascal/vis/ImportGraph.rsc @@ -17,16 +17,17 @@ is clearly visible. @bootstrapParser module lang::rascal::vis::ImportGraph -import util::Reflective; -import vis::Graphs; -import lang::rascal::grammar::definition::Modules; -import lang::rascal::\syntax::Rascal; import Exception; -import util::FileSystem; -import util::IDEServices; import IO; -import analysis::graphs::Graph; import Set; +import String; +import analysis::graphs::Graph; +import lang::rascal::\syntax::Rascal; +import lang::rascal::grammar::definition::Modules; +import util::FileSystem; +import util::IDEServices; +import util::Reflective; +import vis::Graphs; @synopsis{If `projectName` is an open project in the current IDE, the visualize its import/extend graph.} void importGraph(str projectName, bool hideExternals=true) { @@ -41,23 +42,36 @@ void importGraph(loc projectRoot, bool hideExternals=true) { } @synopsis{Visualizes an import/extend graph for all the modules in the srcs roots of the current PathConfig} -void importGraph(PathConfig pcfg, bool hideExternals=true) { +void importGraph(PathConfig pcfg, bool hideExternals=true, bool hideTestModules=true) { m = getProjectModel(pcfg.srcs); // let's start with a simple graph and elaborate on details in later versions - g = { | <- sort(m.imports), hideExternals ==> to notin m.external} - + { | <- sort(m.extends), hideExternals ==> to notin m.external} - + { <"_" , to> | to <- top(m.imports + m.extends) } // pull up the top modules + g = { | <- sort(m.imports), hideExternals ==> to notin m.external, hideTestModules ==> {from, to} & m.tests == {}} + + { | <- sort(m.extends), hideExternals ==> to notin m.external, hideTestModules ==> {from, to} & m.tests == {}} ; list[str] nodeClass(str n) = [ *["external" | n in m.external], *["project" | n notin m.external] ]; + + int edgeWeight(str from, str to) { + if ( in g o gClosed, notin gClosed, notin gClosed) { + return 1; // transitive edges should not influence things. + } + else if ( in m.extends) { + // extend structure is very important + return 1; + } + else { + return 1; + } + } gClosed = g+; list[str] edgeClass(str from, str to) = [ + *["nottop" | from != "_"], *["extend" | in m.extends], *["import" | in m.imports], *["transitive" | in g o gClosed, notin gClosed, notin gClosed], @@ -66,8 +80,23 @@ void importGraph(PathConfig pcfg, bool hideExternals=true) { styles = [ cytoStyleOf( - selector=\edge(equal("source", "_")), - style=defaultEdgeStyle()[visibility="hidden"] + selector=\node(\className("hover")), + style=defaultNodeStyle()[\background-color="yellow"][color="black"] + ), + + cytoStyleOf( + selector=\node(className("hover-in")), + style=defaultNodeStyle()[\background-color="red"][color="black"] + ), + + cytoStyleOf( + selector=\node(className("hover-out")), + style=defaultNodeStyle()[\background-color="orange"][color="black"] + ), + + cytoStyleOf( + selector=\edge(className("hover-out")), + style=defaultEdgeStyle()[\line-color="orange"] ), cytoStyleOf( @@ -82,10 +111,9 @@ void importGraph(PathConfig pcfg, bool hideExternals=true) { cytoStyleOf( selector=\edge(className("transitive")), - style=defaultEdgeStyle()[opacity=".25"][\line-opacity="0.25"] + style=defaultEdgeStyle()[opacity=".25"][\line-opacity="0.50"] ) - , - +, cytoStyleOf( selector=\edge(className("cyclic")), style=defaultEdgeStyle()[opacity="1"][\line-opacity="1"][\width=10] @@ -101,14 +129,27 @@ void importGraph(PathConfig pcfg, bool hideExternals=true) { default loc modLinker(value _) = |nothing:///|; + str modTip(str name) { + return "module + ' + 'import ; + '<}><}> + 'extend ; + '<}><}>"; + } + + str modLabel(str name) = split("::", name)[-1]; + cfg = cytoGraphConfig( - \layout=defaultDagreLayout()[ranker=\network-simplex()], + \layout=defaultDagreLayout()[ranker=\network-simplex()][rankSep=100][debugDagreEdgeControlPoints=false], styles=styles, title="Rascal Import/Extend Graph", + nodeTipper=modTip, nodeClassifier=nodeClass, edgeClassifier=edgeClass, - nodeLinker=modLinker, - edgeStyle=defaultEdgeStyle()[\curve-style=taxi()] + nodeLabeler=modLabel, + edgeWeigher=edgeWeight, + nodeLinker=modLinker ); showInteractiveContent(graph(g, cfg=cfg), title=cfg.title); @@ -117,6 +158,7 @@ void importGraph(PathConfig pcfg, bool hideExternals=true) { @synopsis{Container for everything we need to know about the modules in a project to visualize it.} data ProjectModel = projectModel( set[str] modules = {}, + set[str] tests = {}, set[str] external = {}, rel[str, str] imports = {}, rel[str, str] extends = {}, @@ -133,7 +175,8 @@ ProjectModel getProjectModel(list[loc] srcs) { modules = {*m.modules | m <- models}, imports = {*m.imports | m <- models}, extends = {*m.extends | m <- models}, - files = {*m.files | m <- models} + files = {*m.files | m <- models}, + tests = {*m.tests | m <- models} ); wholeWorld.external = wholeWorld.imports<1> + wholeWorld.extends<1> - wholeWorld.modules; @@ -152,7 +195,8 @@ ProjectModel getProjectModel(loc file) { modules = {name}, imports = {name} * imps, extends = {name} * exts, - files = {} + files = {}, + tests = {name | /test/ := file.path} ); } catch ParseError(_) : diff --git a/src/org/rascalmpl/library/vis/Graphs.rsc b/src/org/rascalmpl/library/vis/Graphs.rsc index 8c79b7be5fc..f47cc612a3c 100644 --- a/src/org/rascalmpl/library/vis/Graphs.rsc +++ b/src/org/rascalmpl/library/vis/Graphs.rsc @@ -20,12 +20,13 @@ This module is quite new and may undergo some tweaks in the coming time. } module vis::Graphs -import lang::html::IO; -import lang::html::AST; -import util::IDEServices; import Content; -import ValueIO; +import IO; import Set; +import ValueIO; +import lang::html::AST; +import lang::html::IO; +import util::IDEServices; @synopsis{Optional configuration attributes for graph style and graph layout} @description{ @@ -35,11 +36,13 @@ and style properties. * title - does what it says * nodeLinker - makes nodes clickable by providing an editor location * nodeLabeler - allows simplification or elaboration on node labels beyond their identity string +* nodeTipper - allows tagging additional information to be showed on demand for a node * nodeClassifier - labels nodes with classes in order to later select them for specific styling * edgeLabeler - allows simplification or elaboration on edge labels * layout - defines and configured the graph layout algorithm * nodeStyle - defines the default style for all nodes * edgeStyle - defines the default style for all edges +* edgeWeigher - defines a function to weigh every edge * style - collects specific styles for specific ((CytoSelector)) edge/node selectors using ((CytoStyleOf)) tuples. Typically the functions passed into this configuration are closures that capture and use the original @@ -81,7 +84,15 @@ list[str] nodeClassifier(str simpson) = [ styles = [ cytoStyleOf( selector=or([\node(className("top")),\node(className("bottom"))]), - style=defaultNodeStyle()[shape=CytoNodeShape::diamond()] + style=cytoNodeStyle(shape=CytoNodeShape::diamond()) + ), + cytoStyleOf( + selector=\node(className("bottom")), + style=cytoNodeStyle(\background-color="yellow", color="black") + ), + cytoStyleOf( + selector=\node(className("top")), + style=cytoNodeStyle(\background-color="orange") ) ]; // we pick a sensible layout @@ -107,18 +118,23 @@ data CytoGraphConfig = cytoGraphConfig( str title="Graph", NodeLinker[&T] nodeLinker = defaultNodeLinker, - NodeLabeler[&T] nodeLabeler = defaultNodeLabeler, + NodeLabeler[&T] nodeLabeler = defaultNodeLabeler, + NodeTipper[&T] nodeTipper = defaultNodeTipper, NodeClassifier[&T] nodeClassifier = defaultNodeClassifier, EdgeLabeler[&T] edgeLabeler = defaultEdgeLabeler, EdgeClassifier[&T] edgeClassifier = defaultEdgeClassifier, + EdgeWeigher[&T] edgeWeigher = defaultEdgeWeigher, CytoLayout \layout = defaultCoseLayout(), CytoStyle nodeStyle = defaultNodeStyle(), CytoStyle edgeStyle = defaultEdgeStyle(), - list[CytoStyleOf] styles = [] + list[CytoStyleOf] styles = [], + list[HTMLElement] header = [tooltipCSS()], + list[HTMLElement] footer = [toEditorClick(), hoverListeners(), tooltipListeners()] ); + @synopsis{A NodeLinker maps node identities to a source location to link to} alias NodeLinker[&T] = loc (&T _id1); @@ -129,11 +145,17 @@ default loc defaultNodeLinker(&T _) = |nothing:///|; @synopsis{A NodeLabeler maps node identities to descriptive node labels} alias NodeLabeler[&T]= str (&T _id2); -@synopsis{The default node labeler searches for any `str` in the identity, or otherwise a file name of a `loc`} +@synopsis{A NodeTipper maps node identities to extended information about a node which is shown on demand} +alias NodeTipper[&T]= str (&T _id2); + +@synopsis{The default node labeler searches for any `str`` in the identity, or otherwise a file name of a `loc`} str defaultNodeLabeler(/str s) = s; str defaultNodeLabeler(loc l) = l.file != "" ? l.file : ""; default str defaultNodeLabeler(&T v) = ""; +@synopsis{The default node tipper does nothing} +default str defaultNodeTipper(&T _v) = ""; + @synopsis{A NodeClassifier maps node identities to classes that are used later to select specific layout and coloring options.} alias NodeClassifier[&T] = list[str] (&T _id3); @@ -152,6 +174,12 @@ alias EdgeLabeler[&T]= str (&T _source, &T _target); @synopsis{The default edge labeler returns the empty label for all edges.} str defaultEdgeLabeler(&T _source, &T _target) = ""; +@synopsis{An EdgeWeigher decides for each individual edge how important it is to optimize its length (shorten it) with respect to the other edges.} +alias EdgeWeigher[&T] = int(&T _source, &T _target); + +@synopsis{The default edge weigher returns 1 for all edges, so that each edge has the same importance in the optimization criterion.} +int defaultEdgeWeigher(&T _source, &T _target) = 1; + @synopsis{A graph plot from a binary list relation.} @examples{ @@ -218,57 +246,60 @@ Cytoscape cytoscape(list[CytoData] \data, CytoGraphConfig cfg=cytoGraphConfig()) cytoEdgeStyleOf(cfg.edgeStyle), *cfg.styles ], - \layout=cfg.\layout + \layout=cfg.\layout, + header=cfg.header, + footer=cfg.footer ); @synopsis{Turns a `rel[loc from, loc to]` into a graph} list[CytoData] graphData(rel[loc x, loc y] v, CytoGraphConfig cfg=cytoGraphConfig()) - = [cytodata(\node("", label=cfg.nodeLabeler(e), editor=""), classes=cfg.nodeClassifier(e)) | e <- {*v, *v}] + - [cytodata(\edge("", "", label=cfg.edgeLabeler(from, to)), classes=cfg.edgeClassifier(from,to)) | <- v] + = [cytodata(\node("", label=cfg.nodeLabeler(e), tip=cfg.nodeTipper(e), editor=""), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata(\edge("", "", weight=cfg.edgeWeigher(from, to), label=cfg.edgeLabeler(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] ; @synopsis{Turns any `rel[&T from, &T to]` into a graph} default list[CytoData] graphData(rel[&T x, &T y] v, CytoGraphConfig cfg=cytoGraphConfig()) - = [cytodata(\node("", label=cfg.nodeLabeler(e), editor=""), classes=cfg.nodeClassifier(e)) | e <- {*v, *v}] + - [cytodata(\edge("", "", label=cfg.edgeLabeler(from, to)), classes=cfg.edgeClassifier(from,to)) | <- v] + = [cytodata(\node("", label=cfg.nodeLabeler(e), tip=cfg.nodeTipper(e), editor=""), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata(\edge("", "", weight=cfg.edgeWeigher(from, to), label=cfg.edgeLabeler(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] ; @synopsis{Turns any `lrel[loc from, &L edge, loc to]` into a graph} list[CytoData] graphData(lrel[loc x, &L edge, loc y] v, CytoGraphConfig cfg=cytoGraphConfig()) - = [cytodata(\node("", label=cfg.nodeLabeler(e), editor=""), classes=cfg.nodeClassifier(e)) | e <- {*v, *v}] + - [cytodata(\edge("", "", label=""), classes=cfg.edgeClassifier(from,to)) | <- v] + = [cytodata(\node("", label=cfg.nodeLabeler(e), tip=cfg.nodeTipper(e), editor=""), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata(\edge("", "", weight=cfg.edgeWeigher(from, to), label=""), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] ; @synopsis{Turns any `lrel[&T from, &L edge, &T to]` into a graph} default list[CytoData] graphData(lrel[&T x, &L edge, &T y] v, CytoGraphConfig cfg=cytoGraphConfig()) - = [cytodata(\node("", label=cfg.nodeLabeler(e), editor=""), classes=cfg.nodeClassifier(e)) | e <- {*v, *v}] + - [cytodata(\edge("", "", label=""), classes=cfg.edgeClassifier(from,to)) | <- v] + = [cytodata(\node("", label=cfg.nodeLabeler(e), tip=cfg.nodeTipper(e), editor=""), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata(\edge("", "", label="", weight=cfg.edgeWeigher(from, to), label=cfg.edgeLabeler(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] ; @synopsis{Turns any `lrel[loc from, loc to]` into a graph} list[CytoData] graphData(lrel[loc x, loc y] v, CytoGraphConfig cfg=cytoGraphConfig()) - = [cytodata(\node("", label=cfg.nodeLabeler(e), editor=""), classes=cfg.nodeClassifier(e)) | e <- {*v, *v}] + - [cytodata(\edge("", "", label=cfg.edgeLabeler(from, to)), classes=cfg.edgeClassifier(from,to)) | <- v] + = [cytodata(\node("", label=cfg.nodeLabeler(e), tip=cfg.nodeTipper(e), editor=""), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata(\edge("", "", weight=cfg.edgeWeigher(from, to), label=cfg.edgeLabeler(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] ; @synopsis{Turns any `lrel[&T from, &T to]` into a graph} default list[CytoData] graphData(lrel[&T x, &T y] v, CytoGraphConfig cfg=cytoGraphConfig()) - = [cytodata(\node("", label=cfg.nodeLabeler(e), editor=""), classes=cfg.nodeClassifier(e)) | e <- {*v, *v}] + - [cytodata(\edge("", "", label=cfg.edgeLabeler(from, to)), classes=cfg.edgeClassifier(from,to)) | <- v] + = [cytodata(\node("", label=cfg.nodeLabeler(e), tip=cfg.nodeTipper(e), editor=""), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata(\edge("", "", weight=cfg.edgeWeigher(from, to), label=cfg.edgeLabeler(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] ; @synopsis{Turns any `rel[loc from, &L edge, loc to]` into a graph} list[CytoData] graphData(rel[loc x, &L edge, loc y] v, CytoGraphConfig cfg=cytoGraphConfig()) - = [cytodata(\node("", label=cfg.nodeLabeler(e), editor=""), classes=cfg.nodeClassifier(e)) | e <- {*v, *v}] + - [cytodata(\edge("", "", label=""), classes=cfg.edgeClassifier(from,to)) | <- v] + = [cytodata(\node("", label=cfg.nodeLabeler(e), tip=cfg.nodeTipper(e), editor=""), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata(\edge("", "", label="", weight=cfg.edgeWeigher(from, to), label=cfg.edgeLabeler(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] ; @synopsis{Turns any `rel[&T from, &L edge, &T to]` into a graph} default list[CytoData] graphData(rel[&T x, &L edge, &T y] v, CytoGraphConfig cfg=cytoGraphConfig()) - = [cytodata(\node("", label=cfg.nodeLabeler(e), editor=""), classes=cfg.nodeClassifier(e)) | e <- {*v, *v}] + - [cytodata(\edge("", "", label=""), classes=cfg.edgeClassifier(from,to)) | <- v] + = [cytodata(\node("", label=cfg.nodeLabeler(e), tip=cfg.nodeTipper(e), editor=""), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata(\edge("", "", label="", weight=cfg.edgeWeigher(from, to), label=cfg.edgeLabeler(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] ; + data CytoNodeShape = \ellipse() | \triangle() @@ -302,15 +333,17 @@ data Cytoscape = cytoscape( list[CytoData] elements = [], list[CytoStyleOf] style=[], - CytoLayout \layout = cytolayout() + CytoLayout \layout = cytolayout(), + list[HTMLElement] header = [], + list[HTMLElement] footer = [] ); data CytoData - = cytodata(CytoElement \data, list[str] classes=[]); + = cytodata(CytoElement \data, str classes=""); data CytoElement - = \node(str id, str label=id, str editor="|none:///|") - | \edge(str source, str target, str id="-", str label="") + = \node(str id, str label=id, str tip = "", str editor="|none:///|") + | \edge(str source, str target, str id="-", str label="", int weight = 1) ; data CytoHorizontalAlign @@ -368,6 +401,9 @@ data CytoStyleOf CytoStyleOf cytoNodeStyleOf(CytoStyle style) = cytoStyleOf(selector=\node(), style=style); CytoStyleOf cytoEdgeStyleOf(CytoStyle style) = cytoStyleOf(selector=\edge(), style=style); +@synopsis{flattens a list of classes to a single string} +private str flattenClasses(list[str] classes) = " <}>"[..-1]; + @synopsis{Instantiates a default node style} @description{ Because the JSON writer can not instantiate default values for keyword fields, @@ -462,14 +498,27 @@ data CytoStyle @synopsis{A combinator language that translates down to strings in JSON} @description{ * For field names you can use the names, or the dot notation for array indices and fields of objects: `"labels.0"`, `"name.first"`. -* `and` and `or` can not be nested; this will lead to failure to select anything at all. The or must be outside and the and must be inside. * `node()` selects all nodes * `edge()` selects all edges -} +} +@benefits{ +* ((CytoSelector)) represents the full power of Cytoscape selector syntax +* If multiple selectors apply to a node or an edge, the proposed styles are _merged_ up to attribute name collisions. The last one in the list always wins in case of a collision. +Merging helps in keeping CytoSelectors plain and simple. +* ((CytoSelector)) offers `hover`, `hover-in` and `hover-out` on top of the standard CytoScape selectors. These can be used to select styles for: + 1. `hover`: the node that is currently hovered. + 2. `hover-in`: edges coming into the hovered node and their source node. + 3. `hover-out`: edges going out of the hovered node and their target node. +} +@pitfalls{ +* `not`, `and`, and `or` can not be nested freely (as the grammar does suggest); this will lead to failure to select anything at all. The or must be outside and the and must be inside. You _can_ nest +one level of `and` under an outermost level of `or`. Illegal nesting will lead to an early runtime exception. +} data CytoSelector = \node() | \edge() | \id(str id) + | \not(CytoSelector sel) | \and(list[CytoSelector] conjuncts) | \or(list[CytoSelector] disjuncts) | \equal(str field, str \value) @@ -479,8 +528,49 @@ data CytoSelector | \greaterEqual(str field, int limit) | \lessEqual(str field, int limit) | \className(str) + | \hover() + | \hover-in() + | \hover-out() + | animated() + | unanimate() + | selected() + | unselected() + | selectable() + | unselectable() + | locked() + | unlocked() + | visible() + | hidden() + | transparent() + | backgrounding() + | nonbackgrounding() + | grabbed() + | free() + | grabbable() + | ungrabbable() + | active() + | inactive() + | touch() + | removed() + | inside() + | parent() + | childless() + | child() + | nonorphan() + | compound() + | loop() + | simple() ; +@synopsis{reduces short-hand `hover` to class selector} +CytoSelector hover() = className("hover"); + +@synopsis{reduces short-hand `hover-in` to class selector} +CytoSelector \hover-in() = className("hover-in"); + +@synopsis{reduces short-hand `hover-out` to class selector} +CytoSelector \hover-out() = className("hover-out"); + @synopsis{Short-hand for a node with a single condition} CytoSelector \node(CytoSelector condition) = and([\node(), condition]); @@ -491,18 +581,55 @@ CytoSelector \edge(CytoSelector condition) = and([\edge(), condition]); str more(set[str] names) = " <}>"[..-1]; @synopsis{Serialize a ((CytoSelector)) to string for client side expression.} -str formatCytoSelector(\node()) = "node"; -str formatCytoSelector(\edge()) = "edge"; -str formatCytoSelector(\id(str i)) = formatCytoSelector(equal("id", i)); -str formatCytoSelector(and(list[CytoSelector] cjs)) = "<}>"; -str formatCytoSelector(or(list[CytoSelector] cjs)) = ",<}>"[..-1]; -str formatCytoSelector(className(str class)) = "."; -str formatCytoSelector(equal(str field, str val)) = "[ = \"\"]"; -str formatCytoSelector(equal(str field, int lim)) = "[ = ]"; -str formatCytoSelector(greater(str field, int lim)) = "[ \> ]"; -str formatCytoSelector(greaterEqual(str field, int lim)) = "[ \>= ]"; -str formatCytoSelector(lessEqual(str field, int lim)) = "[ \<= ]"; -str formatCytoSelector(less(str field, int lim)) = "[ \< ]"; +str formatCytoSelector(\node(), bool nested=false) = "node"; +str formatCytoSelector(\edge(), bool nested=false) = "edge"; +str formatCytoSelector(\id(str i), bool nested=false) = formatCytoSelector(equal("id", i)); +str formatCytoSelector(\not(CytoSelector sel), bool nested=false) = !nested + ? "not()" + : str () { throw "CytoSelector `not` may not be nested under `or`, `and` or not."; }(); +str formatCytoSelector(and(list[CytoSelector] cjs), bool nested=false) = !nested + ? "<}>" + : str () { throw "CytoSelector `and` may not be nested under `or`, `and` or `not`."; }(); +str formatCytoSelector(or(list[CytoSelector] cjs), bool nested=false) = !nested + ? ",<}>"[..-1] + : str () { throw "CytoSelector `or` may not be nested under `or`, `and` or `not`."; }(); +str formatCytoSelector(className(str class), bool nested=false) = "."; +str formatCytoSelector(equal(str field, str val), bool nested=false) = "[ = \"\"]"; +str formatCytoSelector(equal(str field, int lim), bool nested=false) = "[ = ]"; +str formatCytoSelector(greater(str field, int lim), bool nested=false) = "[ \> ]"; +str formatCytoSelector(greaterEqual(str field, int lim), bool nested=false) = "[ \>= ]"; +str formatCytoSelector(lessEqual(str field, int lim), bool nested=false) = "[ \<= ]"; +str formatCytoSelector(less(str field, int lim), bool nested=false) = "[ \< ]"; +str formatCytoSelector(animated(), bool nested=false) = ":animated"; +str formatCytoSelector(unanimate(), bool nested=false) = ":unanimate"; +str formatCytoSelector(selected(), bool nested=false) = ":selected"; +str formatCytoSelector(unselected(), bool nested=false) = ":unselected"; +str formatCytoSelector(selectable(), bool nested=false) = ":selectable"; +str formatCytoSelector(unselectable(), bool nested=false) = ":unselectable"; +str formatCytoSelector(locked(), bool nested=false) = ":locked"; +str formatCytoSelector(unlocked(), bool nested=false) = ":unlocked"; +str formatCytoSelector(visible(), bool nested=false) = ":visible"; +str formatCytoSelector(hidden(), bool nested=false) = ":hidden"; +str formatCytoSelector(transparent(), bool nested=false) = ":transparent"; +str formatCytoSelector(backgrounding(), bool nested=false) = ":backgrounding"; +str formatCytoSelector(nonbackgrounding(), bool nested=false) = ":nonbackgrounding"; +str formatCytoSelector(grabbed(), bool nested=false) = ":grabbed"; +str formatCytoSelector(free(), bool nested=false) = ":free"; +str formatCytoSelector(grabbable(), bool nested=false) = ":grabbable"; +str formatCytoSelector(ungrabbable(), bool nested=false) = ":ungrabbable"; +str formatCytoSelector(active(), bool nested=false) = ":active"; +str formatCytoSelector(inactive(), bool nested=false) = ":inactive"; +str formatCytoSelector(touch(), bool nested=false) = ":touch"; +str formatCytoSelector(removed(), bool nested=false) = ":removed"; +str formatCytoSelector(inside(), bool nested=false) = ":inside"; +str formatCytoSelector(parent(), bool nested=false) = ":parent"; +str formatCytoSelector(childless(), bool nested=false) = ":childless"; +str formatCytoSelector(child(), bool nested=false) = ":child"; +str formatCytoSelector(nonorphan(), bool nested=false) = ":nonorphan"; +str formatCytoSelector(compound(), bool nested=false) = ":compound"; +str formatCytoSelector(loop(), bool nested=false) = ":loop"; +str formatCytoSelector(simple(), bool nested=false) = ":simple"; + @synopsis{Choice of different node layout algorithms.} @description{ @@ -566,6 +693,12 @@ data CytoLayout(CytoLayoutName name = dagre(), bool animate=false) ) | dagreLayout( CytoLayoutName name = dagre(), + int rankSep=-1, + int nodeSep=-1, + int edgeSep=-1, + int padding=-1, + bool useDagreEdgeControlPoints = true, + bool debugDagreEdgeControlPoints = true, num spacingFactor = .1, DagreRanker ranker = \network-simplex() // network-simples tight-tree, or longest-path ) @@ -619,7 +752,9 @@ CytoLayout defaultDagreLayout(num spacingFactor=1) name=CytoLayoutName::dagre(), animate=false, spacingFactor=spacingFactor, - ranker=\network-simplex() + ranker=\network-simplex(), + useDagreEdgeControlPoints=true, + debugDagreEdgeControlPoints=false ); @@ -638,13 +773,17 @@ Response (Request) graphServer(Cytoscape ch) { return response(writeHTMLString(text("could not edit "))); } - Response reply(get(/^\/cytoscape/)) { - return response(ch, formatCytoSelector); + Response reply(get(/^\/cytoscape-dagre.js/)) { + return fileResponse(getResource("org/rascalmpl/library/vis/cytoscape-dagre.js"), "application/javascript", ()); + } + + Response reply(get(/^\/cytoscape$/)) { + return response(ch[header=[]][footer=[]], formatCytoSelector); } // returns the main page that also contains the callbacks for retrieving data and configuration default Response reply(get(_)) { - return response(writeHTMLString(plotHTML())); + return response(writeHTMLString(plotHTML(header=ch.header, footer=ch.footer))); } return reply; @@ -660,37 +799,114 @@ This client features: This client mirrors the server defined by ((graphServer)). } -private HTMLElement plotHTML() +private HTMLElement plotHTML(list[HTMLElement] header = [], list[HTMLElement] footer = []) = html([ head([ script([], src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.umd.js"), script([], src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js"), - script([], src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.5.0/cytoscape-dagre.min.js"), + script([], src="./cytoscape-dagre.js"), style([\data("#visualization { ' width: 100%; ' height: 100%; ' position: absolute; ' top: 0px; ' left: 0px; - '}")]) + '}")]), + *header ]), body([ div([], id="visualization"), script([ \data( - "fetch(\'/cytoscape\').then(resp =\> resp.json()).then(cs =\> { + "window.cy = fetch(\'/cytoscape\').then(resp =\> resp.json()).then(cs =\> { ' cs.container = document.getElementById(\'visualization\'); - ' const cy = cytoscape(cs); - ' cy.on(\'tap\', \'node\', function (evt) { - ' var n = evt.target; - ' if (n.data(\'editor\') !== undefined) { - ' fetch(\'/editor?\' + new URLSearchParams({ - ' src: n.data(\'editor\') - ' })) ; - ' } - ' }); - '}); - '") - ], \type="text/javascript") + ' cs.layout.edgeWeight = edge =\> edge.data(\'weight\') || 1; + ' return cytoscape(cs); + '});") + ], \type="text/javascript"), + *footer, + div([], class="tooltip") ]) ]); + +HTMLElement tooltipCSS() + = style([\data( + ".tooltip { + ' position: absolute; + ' pointer-events: none; + ' padding: 6px 8px; + ' background: rgba(20,20,20,0.7); + ' color: #fff; + ' border-radius: 4px; + ' font-size: 12px; + ' white-space: pre; + ' display: none; + ' z-index: 9999 + '}" + )]); + +HTMLElement hoverListeners() + = script([\data( + "window.cy.then(cy =\> { + ' cy.on(\'mouseover\', \'node\', function(e) { + ' const sel = e.target; + ' sel.addClass(\'hover\'); + ' sel.outgoers().addClass(\'hover-out\'); + ' sel.incomers().addClass(\'hover-in\'); + ' sel.incomers(\'edge\').addClass(\'hover-in\'); + ' sel.outgoers(\'edge\').addClass(\'hover-out\'); + ' }); + ' cy.on(\'mouseout\', \'node\', function(e) { + ' const sel = e.target; + ' sel.removeClass(\'hover\'); + ' sel.outgoers().removeClass(\'hover-out\'); + ' sel.incomers().removeClass(\'hover-in\'); + ' sel.incomers(\'edge\').removeClass(\'hover-in\'); + ' sel.outgoers(\'edge\').removeClass(\'hover-out\'); + ' }); + ' return cy; + ' });" + )]); + +HTMLElement toEditorClick() + = script([\data( + "window.cy = window.cy.then(cy =\> { + ' cy.on(\'tap\', \'node\', function (evt) { + ' const n = evt.target; + ' if (n.data(\'editor\') !== undefined) { + ' fetch(\'/editor?\' + new URLSearchParams({ + ' src: n.data(\'editor\') + ' })) ; + ' } + '}); + 'return cy; + '});" + )]); + +HTMLElement tooltipListeners() + = script([\data( + "window.cy = window.cy.then(cy =\> { + ' const tooltip = document.querySelector(\'.tooltip\'); + ' cy.on(\'mouseover\', \'node\', (evt) =\> { + ' const n = evt.target; + ' const text = n.data(\'tip\'); + ' + ' if (text) { + ' tooltip.textContent = text; + ' tooltip.style.display = \'block\'; + ' } + ' }); + ' + ' cy.on(\'mouseout\', \'node\', () =\> { + ' tooltip.style.display = \'none\'; + ' }); + ' + ' cy.on(\'mousemove\', (evt) =\> { + ' const e = evt.originalEvent; + ' tooltip.style.left = (e.pageX + 12) + \'px\'; + ' tooltip.style.top = (e.pageY + 12) + \'px\'; + ' }); + ' + ' return cy; + '});" + )]); \ No newline at end of file diff --git a/src/org/rascalmpl/library/vis/cytoscape-dagre.js b/src/org/rascalmpl/library/vis/cytoscape-dagre.js new file mode 100644 index 00000000000..7633a608036 --- /dev/null +++ b/src/org/rascalmpl/library/vis/cytoscape-dagre.js @@ -0,0 +1,550 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("dagre")); + else if(typeof define === 'function' && define.amd) + define(["dagre"], factory); + else if(typeof exports === 'object') + exports["cytoscapeDagre"] = factory(require("dagre")); + else + root["cytoscapeDagre"] = factory(root["dagre"]); +})(this, function(__WEBPACK_EXTERNAL_MODULE__4__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +var impl = __webpack_require__(1); + +// registers the extension on a cytoscape lib ref +var register = function register(cytoscape) { + if (!cytoscape) { + return; + } // can't register if cytoscape unspecified + + cytoscape('layout', 'dagre', impl); // register with cytoscape.js +}; +if (typeof cytoscape !== 'undefined') { + // expose to global cytoscape (i.e. window.cytoscape) + register(cytoscape); +} +module.exports = register; + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; } +function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } +function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) { n[e] = r[e]; } return n; } +var isFunction = function isFunction(o) { + return typeof o === 'function'; +}; +var defaults = __webpack_require__(2); +var assign = __webpack_require__(3); +var dagre = __webpack_require__(4); +var EPSILON = 0.001; // what does it mean to be too close to 0? + +// constructor +// options : object containing layout options +function DagreLayout(options) { + this.options = assign({}, defaults, options); +} + +// adds visible nodes for all the edge control points. +function debugEdge(cy, id, e, options) { + if (e.points && options.debugDagreEdgeControlPoints) { + e.points.forEach(function (p, i) { + cy.add({ + data: { + id: "edgepoint_".concat(id.name, "__d").concat(i) + }, + classes: 'edgepoint', + position: { + x: p.x, + y: p.y + }, + selectable: false, + grabbable: false + }); + }); + } +} +function subtract(a, b) { + return { + x: noZero(a.x - b.x), + y: noZero(a.y - b.y) + }; +} +function product(a, b) { + return noZero(a.x * b.x) + noZero(a.y * b.y); +} +function norm(v) { + var len = Math.hypot(v.x, v.y) || 1; + return { + x: v.x / len, + y: v.y / len, + len: len + }; +} +function perp(v) { + return { + x: -v.y, + y: v.x + }; +} + +/* provides the context for mapping from dagre's x, y coordinate system + * for control points to cytoscapes coordinate system for control points + * which is relative to the straight vector from source to target node + */ +function buildEdgeFrame(src, tgt) { + var d = subtract(tgt, src); + var _norm = norm(d), + x = _norm.x, + y = _norm.y, + len = _norm.len; + var dir = { + x: x, + y: y + }; + var normal = perp(dir); + return { + src: src, + tgt: tgt, + dir: dir, + normal: normal, + len: len + }; +} +function addEdgePointStyle(cy, options) { + if (options.debugDagreEdgeControlPoints) { + cy.style().selector('node.edgepoint').style({ + 'background-color': '#ff0000', + 'width': 8, + 'height': 8, + 'shape': 'diamond' + }).update(); + } + cy.style().selector('edge[controlPointDistances]').style({ + 'curve-style': 'unbundled-bezier', + 'control-point-weights': 'data(controlPointWeights)', + 'control-point-distances': 'data(controlPointDistances)', + 'edge-distances': 'intersection', + 'edge-ends-overlap': 'false' + }).update(); +} +function noZero(x) { + if (Math.abs(x) < EPSILON) { + return x < 0 ? -EPSILON : EPSILON; + } + return x; +} +function toEdgeCoordinates(P, frame) { + var vector = subtract(P, frame.src); + var weight = noZero(product(vector, frame.dir) / frame.len); + var distance = noZero(product(vector, frame.normal)); + return { + weight: weight, + distance: distance + }; +} +function normalizeWeight(coords) { + var min = Infinity; + var max = -Infinity; + var _iterator = _createForOfIteratorHelper(coords), + _step; + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var p = _step.value; + if (p.weight < min) { + min = p.weight; + } + if (p.weight > max) { + max = p.weight; + } + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + var range = max - min || 1; + return coords.map(function (p) { + return { + distance: p.distance, + weight: (p.weight - min) / range + }; + }); +} + +/* First introduce new control points to bridge between the dagre list of + * points and the centres of cytoscape nodes. + * Then we sanitize any empty or non-existing or degenerate control points + * And finally we map the Dagre coordinates to the Cytoscape coordinated which + * are relative to the original direction vector from source to target. + * These final coordinates are stored pairwise in two arrays cpw and cpd + * which are picked up by the Bezier construction code in cytoscape. + */ +function dagreEdgeToCytoscapeEdge(dEdge, cEdge) { + var fromNode = cEdge.source().position(); + var toNode = cEdge.target().position(); + var frame = buildEdgeFrame(fromNode, toNode); + var coords = normalizeWeight(dEdge.points.map(function (p) { + return toEdgeCoordinates(p, frame); + })); + var controlPointWeights = coords.slice(1, -1).map(function (c) { + return c.weight; + }); + var controlPointDistances = coords.slice(1, -1).map(function (c) { + return c.distance; + }); + var sp = subtract(dEdge.points.at(0), fromNode); + var sourcePoint = "".concat(sp.x, "px ").concat(sp.y, "px"); + var tp = subtract(dEdge.points.at(-1), toNode); + var targetPoint = "".concat(tp.x, "px ").concat(tp.y, "px"); + var result = { + controlPointWeights: controlPointWeights, + controlPointDistances: controlPointDistances, + sourcePoint: sourcePoint, + targetPoint: targetPoint + }; + return result; +} + +// runs the layout +DagreLayout.prototype.run = function () { + var options = this.options; + var layout = this; + var cy = options.cy; // cy is automatically populated for us in the constructor + var eles = options.eles; + var getVal = function getVal(ele, val) { + return isFunction(val) ? val.apply(ele, [ele]) : val; + }; + var bb = options.boundingBox || { + x1: 0, + y1: 0, + w: cy.width(), + h: cy.height() + }; + if (bb.x2 === undefined) { + bb.x2 = bb.x1 + bb.w; + } + if (bb.w === undefined) { + bb.w = bb.x2 - bb.x1; + } + if (bb.y2 === undefined) { + bb.y2 = bb.y1 + bb.h; + } + if (bb.h === undefined) { + bb.h = bb.y2 - bb.y1; + } + var g = new dagre.graphlib.Graph({ + multigraph: true, + compound: true + }); + var gObj = {}; + var setGObj = function setGObj(name, val) { + if (val != null) { + gObj[name] = val; + } + }; + setGObj('nodesep', options.nodeSep); + setGObj('edgesep', options.edgeSep); + setGObj('ranksep', options.rankSep); + setGObj('rankdir', options.rankDir); + setGObj('align', options.align); + setGObj('ranker', options.ranker); + setGObj('acyclicer', options.acyclicer); + g.setGraph(gObj); + g.setDefaultEdgeLabel(function () { + return {}; + }); + g.setDefaultNodeLabel(function () { + return {}; + }); + + // add nodes to dagre + var nodes = eles.nodes(); + if (isFunction(options.sort)) { + nodes = nodes.sort(options.sort); + } + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + var nbb = node.layoutDimensions(options); + g.setNode(node.id(), { + width: nbb.w, + height: nbb.h, + shape: 'ellipse', + name: node.id() + }); + } + + // set compound parents + for (var _i = 0; _i < nodes.length; _i++) { + var _node = nodes[_i]; + if (_node.isChild()) { + g.setParent(_node.id(), _node.parent().id()); + } + } + + // add edges to dagre + var edges = eles.edges().stdFilter(function (edge) { + return !edge.source().isParent() && !edge.target().isParent(); // dagre can't handle edges on compound nodes + }); + if (isFunction(options.sort)) { + edges = edges.sort(options.sort); + } + for (var _i2 = 0; _i2 < edges.length; _i2++) { + var edge = edges[_i2]; + g.setEdge(edge.source().id(), edge.target().id(), { + minlen: getVal(edge, options.minLen), + weight: getVal(edge, options.edgeWeight), + name: edge.id() + }, edge.id()); + } + dagre.layout(g); + var gNodeIds = g.nodes(); + for (var _i3 = 0; _i3 < gNodeIds.length; _i3++) { + var id = gNodeIds[_i3]; + var n = g.node(id); + cy.getElementById(id).scratch().dagre = n; + } + var dagreBB; + if (options.boundingBox) { + dagreBB = { + x1: Infinity, + x2: -Infinity, + y1: Infinity, + y2: -Infinity + }; + nodes.forEach(function (node) { + var dModel = node.scratch().dagre; + dagreBB.x1 = Math.min(dagreBB.x1, dModel.x); + dagreBB.x2 = Math.max(dagreBB.x2, dModel.x); + dagreBB.y1 = Math.min(dagreBB.y1, dModel.y); + dagreBB.y2 = Math.max(dagreBB.y2, dModel.y); + }); + dagreBB.w = dagreBB.x2 - dagreBB.x1; + dagreBB.h = dagreBB.y2 - dagreBB.y1; + } else { + dagreBB = bb; + } + var constrainPos = function constrainPos(p) { + if (options.boundingBox) { + var xPct = dagreBB.w === 0 ? 0 : (p.x - dagreBB.x1) / dagreBB.w; + var yPct = dagreBB.h === 0 ? 0 : (p.y - dagreBB.y1) / dagreBB.h; + return { + x: bb.x1 + xPct * bb.w, + y: bb.y1 + yPct * bb.h + }; + } else { + return p; + } + }; + nodes.layoutPositions(layout, options, function (ele) { + ele = _typeof(ele) === "object" ? ele : this; + var dModel = ele.scratch().dagre; + return constrainPos({ + x: dModel.x, + y: dModel.y + }); + }); + if (options.useDagreEdgeControlPoints) { + addEdgePointStyle(cy, options); + g.edges().forEach(function (id) { + var cyEdge = cy.getElementById(id.name); + var dEdge = g.edge(id); + if (dEdge && dEdge.points) { + debugEdge(cy, id, dEdge, options); + cyEdge.data(dagreEdgeToCytoscapeEdge(dEdge, cyEdge)); + } + }); + } + return this; // chaining +}; +module.exports = DagreLayout; + +/***/ }), +/* 2 */ +/***/ (function(module, exports) { + +var defaults = { + // dagre algo options, uses default value on undefined + nodeSep: undefined, + // the separation between adjacent nodes in the same rank + edgeSep: undefined, + // the separation between adjacent edges in the same rank + rankSep: undefined, + // the separation between adjacent nodes in the same rank + rankDir: undefined, + // 'TB' for top to bottom flow, 'LR' for left to right, + align: undefined, + // alignment for rank nodes. Can be 'UL', 'UR', 'DL', or 'DR', where U = up, D = down, L = left, and R = right + acyclicer: undefined, + // If set to 'greedy', uses a greedy heuristic for finding a feedback arc set for a graph. + // A feedback arc set is a set of edges that can be removed to make a graph acyclic. + ranker: undefined, + // Type of algorithm to assigns a rank to each node in the input graph. + // Possible values: network-simplex, tight-tree or longest-path + minLen: function minLen(edge) { + return 1; + }, + // number of ranks to keep between the source and target of the edge + edgeWeight: function edgeWeight(edge) { + return 1; + }, + // higher weight edges are generally made shorter and straighter than lower weight edges + + // general layout options + fit: true, + // whether to fit to viewport + padding: 30, + // fit padding + spacingFactor: undefined, + // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up + nodeDimensionsIncludeLabels: false, + // whether labels should be included in determining the space used by a node + useDagreEdgeControlPoints: false, + // enable bezier curves using dagre control points + debugDagreEdgeControlPoints: false, + // visualizes dagre's edge control points as nodes + animate: false, + // whether to transition the node positions + animateFilter: function animateFilter(node, i) { + return true; + }, + // whether to animate specific nodes when animation is on; non-animated nodes immediately go to their final positions + animationDuration: 500, + // duration of animation in ms if enabled + animationEasing: undefined, + // easing of animation if enabled + boundingBox: undefined, + // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h } + transform: function transform(node, pos) { + return pos; + }, + // a function that applies a transform to the final node position + ready: function ready() {}, + // on layoutready + sort: undefined, + // a sorting function to order the nodes and edges; e.g. function(a, b){ return a.data('weight') - b.data('weight') } + // because cytoscape dagre creates a directed graph, and directed graphs use the node order as a tie breaker when + // defining the topology of a graph, this sort function can help ensure the correct order of the nodes/edges. + // this feature is most useful when adding and removing the same nodes and edges multiple times in a graph. + stop: function stop() {} // on layoutstop +}; +module.exports = defaults; + +/***/ }), +/* 3 */ +/***/ (function(module, exports) { + +// Simple, internal Object.assign() polyfill for options objects etc. + +module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt) { + for (var _len = arguments.length, srcs = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + srcs[_key - 1] = arguments[_key]; + } + srcs.forEach(function (src) { + Object.keys(src).forEach(function (k) { + return tgt[k] = src[k]; + }); + }); + return tgt; +}; + +/***/ }), +/* 4 */ +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__4__; + +/***/ }) +/******/ ]); +}); \ No newline at end of file