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