diff --git a/src/org/rascalmpl/library/vis/Graphs.rsc b/src/org/rascalmpl/library/vis/Graphs.rsc index d499153b4d7..610972bacbd 100644 --- a/src/org/rascalmpl/library/vis/Graphs.rsc +++ b/src/org/rascalmpl/library/vis/Graphs.rsc @@ -22,6 +22,7 @@ module vis::Graphs import Content; import IO; +import Node; import Set; import ValueIO; import lang::html::AST; @@ -122,19 +123,27 @@ data CytoGraphConfig = cytoGraphConfig( NodeLabeler[&T] nodeLabeler = defaultNodeLabeler, NodeTipper[&T] nodeTipper = defaultNodeTipper, NodeClassifier[&T] nodeClassifier = defaultNodeClassifier, + NodeConfigurator[&T] nodeConfigurator = defaultNodeConfigurator, EdgeLabeler[&T] edgeLabeler = defaultEdgeLabeler, EdgeTipper[&T] edgeTipper = defaultEdgeTipper, EdgeClassifier[&T] edgeClassifier = defaultEdgeClassifier, EdgeWeigher[&T] edgeWeigher = defaultEdgeWeigher, + EdgeConfigurator[&T] edgeConfigurator = defaultEdgeConfigurator, - CytoLayout \layout = defaultCoseLayout(), + CytoLayout \layout = defaultDagreLayout(), CytoStyle nodeStyle = defaultNodeStyle(), CytoStyle edgeStyle = defaultEdgeStyle(), list[CytoStyleOf] styles = [], list[HTMLElement] header = [tooltipCSS()], - list[HTMLElement] footer = [toEditorClick(), hoverListeners(), tooltipListeners()] + list[HTMLElement] footer = [ + toEditorClick(), + hoverListeners(), + tooltipListeners(), + *dynamicCoseProperties(\layout.name), + *relayoutOnDrop(\layout.name) + ] ); @@ -149,7 +158,20 @@ default loc defaultNodeLinker(&T _) = |nothing:///|; alias NodeLabeler[&T]= str (&T _id2); @synopsis{A NodeTipper maps node identities to extended information about a node which is shown on demand} -alias NodeTipper[&T]= str (&T _id2); +alias NodeTipper[&T] = str (&T _id2); + +@synopsis{A NodeConfig produces layour configuration per node} +@description{ +Different layout engines can store additional configuration information +here for later use during layout. For example: which nodes are in the same layer, +or how "sticky" a node is compared to others. +} +alias NodeConfigurator[&T] = NodeConfig (&T id); + +@synopsis{To be extended for storing node specific layout information} +data NodeConfig = nodeConfig(); + +default NodeConfig defaultNodeConfigurator(&T _n) = nodeConfig(); @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; @@ -189,6 +211,13 @@ 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{To be extended for storing node specific layout information} +data EdgeConfig = edgeConfig(); + +@synopsis{An EdgeConfig produces layout configuration information per edge} +alias EdgeConfigurator[&T] = EdgeConfig (&T _from, &T _to); + +default EdgeConfig defaultEdgeConfigurator(&T _from, &T _to) = edgeConfig(); @synopsis{A graph plot from a binary list relation.} @examples{ @@ -262,50 +291,123 @@ Cytoscape cytoscape(list[CytoData] \data, CytoGraphConfig cfg=cytoGraphConfig()) @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), 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), tip=cfg.edgeTipper(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] + = [cytodata( + \node("", + label=cfg.nodeLabeler(e), + tip=cfg.nodeTipper(e), + editor="", + \node-config=cfg.nodeConfigurator(e) + ), + classes=flattenClasses(cfg.nodeClassifier(e) + )) | e <- {*v, *v}] + + [cytodata( + \edge( + "", + "", + weight=cfg.edgeWeigher(from, to), + label=cfg.edgeLabeler(from, to), + tip=cfg.edgeTipper(from, to), + \edge-config=cfg.edgeConfigurator(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), 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), tip=cfg.edgeTipper(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] + = [cytodata(\node("", label=cfg.nodeLabeler(e), tip=cfg.nodeTipper(e), editor="", \node-config=cfg.nodeConfigurator(e)), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata(\edge("", "", weight=cfg.edgeWeigher(from, to), label=cfg.edgeLabeler(from, to), tip=cfg.edgeTipper(from, to), \edge-config=cfg.edgeConfigurator(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), tip=cfg.nodeTipper(e), editor=""), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + - [cytodata(\edge("", "", weight=cfg.edgeWeigher(from, to), label="", tip=cfg.edgeTipper(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] + = [cytodata(\node("", label=cfg.nodeLabeler(e), tip=cfg.nodeTipper(e), editor="", \node-config=cfg.nodeConfigurator(e)), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata( + edge("", "", + weight=cfg.edgeWeigher(from, to), + label="", tip=cfg.edgeTipper(from, to), + \edge-config=cfg.edgeConfigurator(from, to) + ), + 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), 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), tip=cfg.edgeTipper(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] + = [cytodata( + \node("", + label=cfg.nodeLabeler(e), + tip=cfg.nodeTipper(e), + editor="", \node-config=cfg.nodeConfigurator(e)), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata(\edge("", "", label="", weight=cfg.edgeWeigher(from, to), label=cfg.edgeLabeler(from, to), \edge-config=cfg.edgeConfigurator(from, to), tip=cfg.edgeTipper(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), 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), tip=cfg.edgeTipper(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] + = [cytodata(\node("", label=cfg.nodeLabeler(e), tip=cfg.nodeTipper(e), editor="", \node-config=cfg.nodeConfigurator(e)), classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata(\edge("", "", weight=cfg.edgeWeigher(from, to), label=cfg.edgeLabeler(from, to), tip=cfg.edgeTipper(from, to), \edge-config=cfg.edgeConfigurator(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), 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), tip=cfg.edgeTipper(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] + = [cytodata( + \node("", + label=cfg.nodeLabeler(e), + tip=cfg.nodeTipper(e), + editor="", + \node-config=cfg.nodeConfigurator(e) + ), + classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata( + \edge("", "", + weight=cfg.edgeWeigher(from, to), + label=cfg.edgeLabeler(from, to), + tip=cfg.edgeTipper(from, to), + \edge-config=cfg.edgeConfigurator(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), 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), tip=cfg.edgeTipper(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] + = [cytodata( + \node("", + label=cfg.nodeLabeler(e), + tip=cfg.nodeTipper(e), + editor="", + \node-config=cfg.nodeConfigurator(e) + ), + classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata( + \edge("", "", + label="", + weight=cfg.edgeWeigher(from, to), + label=cfg.edgeLabeler(from, to), + tip=cfg.edgeTipper(from, to), + \edge-config=cfg.edgeConfigurator(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), 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), tip=cfg.edgeTipper(from, to)), classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] + = [cytodata( + \node("", + label=cfg.nodeLabeler(e), + tip=cfg.nodeTipper(e), + editor="", + \node-config=cfg.nodeConfigurator(e) + ), + classes=flattenClasses(cfg.nodeClassifier(e))) | e <- {*v, *v}] + + [cytodata( + \edge("", "", + label="", + weight=cfg.edgeWeigher(from, to), + label=cfg.edgeLabeler(from, to), + tip=cfg.edgeTipper(from, to), + \edge-config=cfg.edgeConfigurator(from, to) + ), + classes=flattenClasses(cfg.edgeClassifier(from,to))) | <- v] ; @@ -351,8 +453,8 @@ data CytoData = cytodata(CytoElement \data, str classes=""); data CytoElement - = \node(str id, str label=id, str tip = "", str editor="|none:///|") - | \edge(str source, str target, str id="-", str label="", str tip = "", int weight = 1) + = \node(str id, str label=id, str tip = "", str editor="|none:///|", NodeConfig \node-config = nodeConfig()) + | \edge(str source, str target, str id="-", str label="", str tip = "", int weight = 1, EdgeConfig \edge-config = edgeConfig()) ; data CytoHorizontalAlign @@ -418,7 +520,7 @@ private str flattenClasses(list[str] classes) = " <}> Because the JSON writer can not instantiate default values for keyword fields, we have to do it manually here. } -CytoStyle defaultNodeStyle() +default CytoStyle defaultNodeStyle(CytoLayoutName _layout=\dagre()) = cytoNodeStyle( visibility = "visible", /* hidden, collapse */ opacity = "1", @@ -439,7 +541,7 @@ CytoStyle defaultNodeStyle() Because the JSON writer can not instantiate default values for keyword fields we have to do it manually here. } -CytoStyle defaultEdgeStyle() +default CytoStyle defaultEdgeStyle() = cytoEdgeStyle( visibility = "visible", /* hidden, collapse */ opacity = "1", @@ -701,16 +803,42 @@ data CytoLayoutName | circle() | breadthfirst() | cose() + | fcose() | dagre() ; +data Position = pos(int x, int y); + +@synopsis{This property is added for cose and fcose, for every node differently.} +data NodeConfig(int nodeRepulsion = 2048); + +@synopsis{These are added for cose and fcose, for every edge differently.} +data EdgeConfig(int edgeElasticity= 100, int nestingFactor= 5); + +data CoseNodeConstraint[&T] + = coseNodeConstraint(&T nodeId, Position position); + +data CoseAligmentConstraint[&T] + = coseAligmentConstraint(list[list[&T]] horizontal, list[list[&T]] vertical); + +data CoseRelativeConstraint[&T] + = vertical(&T top, &T bottom, int gap) + | horizontal(&T left, &T right, int gap) + ; + @synopsis{An alias for dagre layout for documentation purposes.} @description{ Dagre is a hierarchical graph layout. } CytoLayoutName hierarchical() = dagre(); -data CytoLayout(CytoLayoutName name = dagre(), bool animate=false) +data CytoAnimate + = \true() + | \false() + | \end() + ; + +data CytoLayout(CytoLayoutName name = dagre(), CytoAnimate animate=\false(), real animateThreshold = 0.0, int animationDuration=500, bool animationEasing=false) = cytolayout() | breadthfirstLayout( CytoLayoutName name = CytoLayoutName::breadthfirst(), @@ -733,7 +861,48 @@ data CytoLayout(CytoLayoutName name = dagre(), bool animate=false) num spacingFactor=.1 ) | coseLayout( - CytoLayoutName name = cose() + CytoLayoutName name = cose(), + CytoAnimate animate=\true(), + int idealEdgeLength= 100, + int nodeOverlap= 20, + int refresh= 1, + bool fit= true, + int padding= 30, + bool randomize= false, + int componentSpacing= 100, + int gravity= 80, + int numIter= 5000, + int initialTemp= 200, + real coolingFactor= 0.995, + real minTemp= 0.1 + ) + | fcoseLayout( + CytoLayoutName name = fcose(), + CytoAnimate animate=\true(), + CoseLayoutQuality quality = \default(), + bool randomize = true, + bool fit = true, + int padding= 30, + bool nodeDimensionsIncludeLabels = true, + bool uniformNodeDimensions= !nodeDimensionsIncludeLabels, + bool packComponents= true, + bool samplingType= true, + int sampleSize = 25, + int nodeSeparation= 75, + real piTol= 0.0000001, + real nestingFactor = 0.1, + int numIter = 2500, + bool tile = true, + int tilingPaddingVertical = 10, + int tilingPaddingHorizontal = 10, + real gravity = 0.25, + real gravityRangeCompound = 1.5, + real gravityCompound = 1.0, + real gravityRange = 3.8, + real initialEnergyOnIncremental = 0.3, + list[CoseNodeConstraint[value]] fixedNodeConstraint = [], + CoseAligmentConstraint[value] alignmentConstraint = coseAligmentConstraint([],[]), + list[CoseRelativeConstraint[value]] relativePlacementConstraint = [] ) | dagreLayout( CytoLayoutName name = dagre(), @@ -744,10 +913,22 @@ data CytoLayout(CytoLayoutName name = dagre(), bool animate=false) bool useDagreEdgeControlPoints = true, bool automaticDagreEdgeStyle = true, num spacingFactor = .1, - DagreRanker ranker = \network-simplex() // network-simples tight-tree, or longest-path + DagreRanker ranker = \network-simplex() ) ; +data CoseLayoutStep + = \all() + | \transformed() + | \enforced() + ; + +data CoseLayoutQuality + = draft() + | \default() + | proof() + ; + data DagreRanker = \network-simplex() | \tight-tree() @@ -757,14 +938,62 @@ data DagreRanker CytoLayout defaultCoseLayout() = coseLayout( name=cose(), - animate=false + animate=\true(), + animateThreshold=0.1, + animationDuration=500, + idealEdgeLength= 100, + nodeOverlap= 20, + refresh= 1, + fit= true, + padding= 30, + randomize= false, + componentSpacing= 100, + nodeRepulsion= 400000, + edgeElasticity= 100, + nestingFactor= 5, + gravity= 80, + numIter= 500, + initialTemp= 200, + coolingFactor= 0.9995, + minTemp= 0.1 + ) + ; + +CytoLayout defaultFcoseLayout() + = fcoseLayout( + name=fcose(), + animate=\true(), + quality = \default(), + randomize = false, + fit = true, + padding= 30, + nodeDimensionsIncludeLabels = true, + uniformNodeDimensions= false, + packComponents= true, + samplingType= true, + sampleSize = 25, + nodeSeparation= 75, + piTol= 0.0000001, + nestingFactor = 0.1, + numIter = 2500, + tile = true, + tilingPaddingVertical = 10, + tilingPaddingHorizontal = 10, + gravity = 0.25, + gravityRangeCompound = 1.5, + gravityCompound = 1.0, + gravityRange = 3.8, + initialEnergyOnIncremental = 0.3, + fixedNodeConstraint = [], + alignmentConstraint = coseAligmentConstraint([],[]), + relativePlacementConstraint = [] ) ; CytoLayout defaultCircleLayout(bool avoidOverlap=true, num spacingFactor=.1) = circleLayout( name = CytoLayoutName::circle(), - animate=false, + animate=\false(), avoidOverlap=avoidOverlap, spacingFactor=spacingFactor ); @@ -772,7 +1001,7 @@ CytoLayout defaultCircleLayout(bool avoidOverlap=true, num spacingFactor=.1) CytoLayout defaultGridLayout(int rows=2, int cols=rows, bool avoidOverlap=true, num spacingFactor=.1) = gridLayout( name=CytoLayoutName::grid(), - animate=false, + animate=\false(), rows=rows, cols=cols, avoidOverlap=avoidOverlap, @@ -784,7 +1013,7 @@ CytoLayout defaultBreadthfirstLayout(num spacingFactor=.1, bool circle=false, bo = breadthfirstLayout( name=CytoLayoutName::breadthfirst(), - animate=false, + animate=\false(), spacingFactor=spacingFactor, circle=circle, grid=grid, @@ -794,7 +1023,7 @@ CytoLayout defaultBreadthfirstLayout(num spacingFactor=.1, bool circle=false, bo CytoLayout defaultDagreLayout(num spacingFactor=1) = dagreLayout( name=CytoLayoutName::dagre(), - animate=false, + animate=\false(), spacingFactor=spacingFactor, ranker=\network-simplex(), useDagreEdgeControlPoints=true, @@ -845,6 +1074,9 @@ private HTMLElement plotHTML(list[HTMLElement] header = [], list[HTMLElement] fo 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@3.0.0/cytoscape-dagre.min.js"), + script([], src="https://unpkg.com/layout-base/layout-base.js"), + script([], src="https://unpkg.com/cose-base/cose-base.js"), + script([], src="https://unpkg.com/cytoscape-fcose/cytoscape-fcose.js"), style([\data("#visualization { ' width: 100%; ' height: 100%; @@ -885,6 +1117,32 @@ HTMLElement tooltipCSS() '}" )]); +default list[HTMLElement] dynamicCoseProperties(CytoLayoutName _) = []; + +list[HTMLElement] dynamicCoseProperties(fcose()) + = [script([\data( + "windows.cy.then(cy =\> { + ' cy.options.nodeRepulsion = (node) =\> node.data(\'nodeConfig\')?.nodeRepulsion ?? 2048; + ' cy.options.idealEdgeLength = (edge) =\> edge.data(\'edgeConfig\')?.idealEdgeLength) ?? 50; + ' cy.options.edgeElasticity = (edge) =\> edge.data(\'edgeConfig\')?.edgeElasticity) ?? 0.45; + '});" + )])]; + +default list[HTMLElement] relayoutOnDrop(CytoLayoutName _) = []; + +list[HTMLElement] relayoutOnDrop(CytoLayoutName \layout) + = [script([\data( + "window.cy.then(cy =\> { + ' cy.on(\'free\', \'node\', (evt) =\> { + ' cy.layout({ + ' options: cy.options.layout, + ' name: \'\' + ' }).run(); + ' }) + '});" + )])] + when \layout in {cose(), fcose()}; + HTMLElement hoverListeners() = script([\data( "window.cy.then(cy =\> { @@ -924,14 +1182,13 @@ HTMLElement toEditorClick() = script([\data( "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; + ' const n = evt.target; + ' if (n.data(\'editor\') !== undefined) { + ' fetch(\'/editor?\' + new URLSearchParams({ + ' src: n.data(\'editor\') + ' })) ; + ' } + ' }); '});" )]);