Oriented associations

Would you like to see new features in Wandora?

Oriented associations

Postby Damien S. » Tue May 13, 2014 5:40 pm

Hello,

Maybe a little question for a huge dev work but, Is it possible to have oriented associations in the graph view (with arrows instead of edges ?)

In my case, I'm working to reproduce applications and data flows. I'm currently creating Data as Associaton type and input / outputs for players' Role in order to associate them.
Is there an easy way to add a arrow instead of input/output roles for each players on each association types ?

Best Regards,
Damien.
Damien S.
 
Posts: 8
Joined: Fri May 09, 2014 12:53 pm

Re: Oriented associations

Postby akivela » Wed May 14, 2014 4:43 pm

Hello Damien

Thanks for the feature request.

Unfortunately it is likely that we'll not enhance Wandora's Graph topic panel to cover arrow (role aware) edges very soon.

However, I wonder if the D3 javascript library [1] supports already such a graph visualization. You have probably already tested the d3graph service of Wandora's embedded server [2]. These days one can also use these services inside Wandora application with the Webview panel [3]. My intuition says, the d3graph visualization can be easily extended to support oriented associations. I have to look at it closer.

Kind Regards,
Aki / Wandora Team

[1] http://d3js.org/
[2] http://wandora.org/wiki/D3_graph_service_module
[3] http://wandora.org/wiki/Webview
akivela
Site Admin
 
Posts: 260
Joined: Tue Sep 18, 2007 10:20 am
Location: Helsinki, Finland

Re: Oriented associations

Postby Olli » Fri May 16, 2014 11:14 am

Hello Damien

I have looked a bit into having arrowheads in the d3 graph visualisation, and it turns out that it can be done without too much trouble. You'll need to edit a couple of files to do this. For anyone else possibly reading this in the future, I'll note that the changes to the first two files (render.js and d3graph.vhtml) will be made part of the normal Wandora distribution and need not be made after the next Wandora release.

The d3 graph visualisation files are in build/resources/server/d3graph. In there, first open the file static/js/render.js. Replace the entire contents of that file with following.

Code: Select all
function draw(json,amount) {
    var w = 900, h = 900, node, link, root;

    var force = d3.layout.force()
            .on("tick", tick)
            .charge(-2000)
            .linkDistance(40)
            .size([ w, h ])
            .gravity(0.4);

    d3.select("#mainContent").style("height",window.innerHeight+"px");

    var cont = d3.select("#chart")
            .append("svg:svg")
            .attr("viewbox","0 0 600 600")
            .attr("height",window.innerHeight)
            .attr("width",window.innerWidth)
            .style("overflow","hidden");

    cont.html(
    "<defs>"+
    "<marker id=\"TriangleEnd\" viewBox=\"0 0 10 10\" refX=\"19\" refY=\"5\" "+
    "markerUnits=\"strokeWidth\" markerWidth=\"10\" markerHeight=\"10\" orient=\"auto\">"+
    "<path class=\"marker\" d=\"M 0 0 L 10 5 L 0 10 z\" /></marker>"+
    "<marker id=\"TriangleStart\" viewBox=\"0 0 10 10\" refX=\"-9\" refY=\"5\" "+
    "markerUnits=\"strokeWidth\" markerWidth=\"10\" markerHeight=\"10\" orient=\"auto\">"+
    "<path class=\"marker\" d=\"M 10 0 L 0 5 L 10 10 z\" /></marker>"+
    "</defs>"
    );

    vis = cont.append("svg:g");
    vis.attr("width", "600");
    vis.attr("height", "600");

    root = json;
    update();


    cont.call(d3.behavior.zoom().on("zoom", function(){
        var t = d3.event.translate;
        var s = d3.event.scale;
        vis.attr("transform", "translate(" + t + ")scale(" + s + ")");
    }));


   

    var desc = d3.select('.description');
    d3.select('.info-toggle').on('click',function(){
        var isVisible = desc.classed('visible');
        desc.classed('visible',!isVisible);
        d3.select(this).classed('on',!isVisible);
    });
   
    window.addEventListener('resize', onWindowResize, false);

    function onWindowResize(){
        cont.attr("height",window.innerHeight);
        cont.attr("width",window.innerWidth);
        d3.select("#mainContent").style("height",window.innerHeight+"px");
    }
   
    function update() {
        var nodes = json.nodes, links = json.links;

        // Restart the force layout.
        force.nodes(nodes).links(links).start();

        // Update the links…
        link = vis.selectAll("line.link").data(links, function(d) {
            return d.target.id;
        });

        // Enter any new links.
        link.enter().insert("svg:line", ".node")
        .attr("class", function(d) {
                return d.class;
        }).attr("x1", function(d) {
                return d.source.x;
        }).attr("y1", function(d) {
                return d.source.y;
        }).attr("x2", function(d) {
                return d.target.x;
        }).attr("y2", function(d) {
                return d.target.y;
        }).attr("id", function(d) {
                return d.id;
        }).attr("source",function(d){
                return d.source.id;
        }).attr("target",function(d){
                return d.target.id;
        }).style("stroke",function(d){
                return  d.color;
        });

        // Exit any old links.
        link.exit().remove();

        // Update the nodes…
        node = vis.selectAll("g.node").data(nodes, function(d) {
            return d.id;
        });

        node.enter().append("svg:g")
            .attr("class", "node")
            .attr("id", function(d){return d.id;})
            .attr("cx",
                function(d) {
                    return d.x;
                }
            )
            .attr("cy", function(d) {
                return d.y;
                }
            )
            .call(force.drag)
            .on("mouseover", nodeMouseover)
            .on("mouseout", nodeMouseout);

        // Enter any new nodes.
        node.append("svg:circle")
            .attr("class", "circle")
            .attr("r", 15);

        node.append("svg:text")
            .attr("class", "nodetext")
            .text(function(d) {return decodeURIComponent(d.name.replace(/\+/g," "));});

        node.exit().remove();
    }


    function tick() {
        link.attr("x1", function(d) {
            return d.source.x;
        }).attr("y1", function(d) {
            return d.source.y;
        }).attr("x2", function(d) {
            return d.target.x;
        }).attr("y2", function(d) {
            return d.target.y;
        });

        node.attr("transform", function(d) {
            return "translate(" + d.x + "," + d.y + ")";
        });
    }
   
       
       
    function nodeMouseover(d,i) {
        //force.stop();
        d3.selectAll("g.node").style("opacity","0.3");
        d3.selectAll("line").style("opacity","0.3");
        d3.select(this).style("opacity","1.0");
        var nodeId = d.id;
        var nodeLines = d3.selectAll("line").filter(function(d){
                return d.target.id == nodeId || d.source.id == nodeId;
        });
        nodeLines.style("opacity","1.0");
        nodeLines.each(function(d) {
            var line = d;
            var nodes = d3.selectAll("g.node").filter(function(d){
                    return d.id == line.target.id || d.id == line.source.id;
            });
            nodes.style("opacity","1.0");
        });
    }
   
    function nodeMouseout(d,i) {
        //force.resume();
        d3.selectAll("line").style("opacity","1.0");
        d3.selectAll("g.node").style("opacity","1.0");
    }
}


Then save that file and open templates/d3graph.vhtml. Replace the contents of that with this

Code: Select all
#set( $wandoraClass = $topic.getTopicMap().getTopic("http://wandora.org/si/core/wandora-class") )##
#set( $ctopic = $topic )##
<!DOCTYPE HTML>
<html >
<!-- *********************************************************************** -->
<!-- ****              WANDORA EMBEDDED HTTP SERVER TEMPLATE            **** -->
<!-- ****             (c) 2012 Wandora Team            **** -->
<!-- *********************************************************************** -->

    <HEAD >
        <title>Wandora D3 Graph</title>
        <script src="${staticbase}js/d3/d3.min.js"></script>
        <script src="${staticbase}js/render.js"></script>
        <link rel="StyleSheet" href="${staticbase}style.css" type="text/css" media="screen">
    </HEAD>
    <body>
      <div id="mainContent">
        <div id="chart"></div>
        <div class="footer">
          <div class="header">
           <h1 class="heading">D3 Graph</h1>
           <button class="info-toggle"></button>
          </div>
          <div class="description">
            <p>
              Only the first 1000 topics and those associations with both
              players available are displayed. You can adjust the amount of
              topics loaded with the URL parameter "n". Nodes can be
              dragged and the view may be panned and zoomed using the mouse.
            </p>
            <div id="info">
              <table class="legend">
                <tr class="title">
                  <td colspan="2">Edge colors</td>
                </tr>
                <tr>
                  <th align="left">Association type</th>
                  <th align="left">color</th>
                </tr>
              </table>
            </div>
          </div>
        </div>
      </div>
      <script>
            draw(
#set( $topicMap = $topic.getTopicMap() )
## -------------------------------------------------------
#if( !$request.getParameter("draw_all_topics") )##
#**##set( $topics  = $listmaker.make() )##
#* *### -------------- Collect association player topics
#* *##foreach( $association in $topicMap.getAssociations() )##
#*  *##foreach( $role in $association.getRoles() )##
#*   *##if( !$topics.contains( $association.getPlayer( $role ) ) )##
#*    *##set( $temp = $topics.add( $association.getPlayer( $role ) ) )##
#*   *##end##
#*  *##end##
#* *##end##
#* *### --------------- Collect type and instance topics
#* *##foreach( $topic in $topicMap.getTopics() )##
#*  *##foreach( $type in $topic.getTypes() )##
#*   *##if( !$topics.contains( $type ) )##
#*    *##set( $temp = $topics.add( $type ) )##
#*   *##end##
#*   *##foreach( $instance in $topicMap.getTopicsOfType( $type ) )##
#*    *##if( !$topics.contains( $instance ) )##
#*     *##set( $temp = $topics.add( $instance ) )##
#*    *##end##
#*   *##end##
#*  *##end##
#* *##end##
#else##
#**##set( $topics = $topicMap.getTopics() )##
#end##
## --------------------------------------------------------
#set( $ntopics = $listmaker.make() )
#set( $i = 0 )
#set( $topicSize = $topics.toArray().size() )
#set( $topicHashMap = $mapmaker.make() )
#set( $typeList = $listmaker.make() )
#if(! $request.getParameter("n") )
  #set( $n = 1000 )
#else
  #set( $n = $request.getParameter("n"))
#end

#set ($associations = $topicMap.getAssociations() )
#set( $assocTypeList = $listmaker.make() )
#foreach($association in $associations )
  #set($assocType = $association.getType() )
  #if(!$assocTypeList.contains($assocType))
    #set($temp = $assocTypeList.add($assocType))
  #end
#end
#set( $colorMap = $mapmaker.make() )
#set($colors = ["DarkGray","DarkGoldenrod","RoyalBlue","IndianRed","Gray","Violet","MediumAquamarine","YellowGreen",
"DarkSlateGray","SlateGray","CadetBlue","BlueViolet","Magenta","Brown","SeaGreen","SandyBrown","DarkMagenta","MediumSlateBlue","Orchid",
"Teal","LimeGreen","SlateBlue","SaddleBrown","Turquoise","DarkViolet","DarkKhaki","MediumVioletRed","Yellow","Black","DarkBlue","MidnightBlue",
"Tomato","GreenYellow","Gold","MediumPurple","Silver","Lime","DarkOrange","Green","MediumSpringGreen","Purple","Salmon","MediumOrchid",
"Moccasin","DarkSalmon","Coral","LightYellow","DarkOrchid","Beige","OrangeRed","MintCream","Orange","Cornsilk","SpringGreen","Maroon","LightCyan",
"RosyBrown","Azure","LightGreen","MistyRose","SkyBlue","PaleVioletRed","Lavender","DarkGreen","LightSkyBlue","DodgerBlue","DarkOliveGreen",
"DarkRed","Crimson","LightCoral","MediumSeaGreen","Seashell","Gray","Blue","Bisque","Peru","Pink","DarkTurquoise","SteelBlue","Olive","DarkCyan","DarkSlateBlue",
"Sienna","Navy","LightGoldenrodYellow","Honeydew","Indigo","Chartreuse","CornflowerBlue","DarkSeaGreen","OldLace","DeepSkyBlue","LightSalmon","PaleGreen",
"MediumTurquoise","PaleTurquoise","Goldenrod","FireBrick","Ivory","LawnGreen","Thistle","MediumBlue","LavenderBlush","BurlyWood","Fuchsia",
"Gainsboro","Aquamarine","BlanchedAlmond","AliceBlue","Linen","HotPink","Tan","OliveDrab","DimGray","DeepPink","Chocolate","ForestGreen","Khaki","Plum"])
#foreach($assocType in $assocTypeList)
  #set($j = $i % $colors.size())
  #set($temp = $colorMap.put($assocType,$colors.get($j)))
  #set($i = $i + 1)
#end

#set($i = 0)

{
"nodes" : [
#* *##foreach ( $topic in $topics )
#*   *##set( $topicName = "$topic.getOneSubjectIdentifier().toExternalForm()" )##
#*   *##set( $topicName = $topic.getBaseName() )##
#*   *##if($i != 0)
#*     *#,
#*   *##end
#*   *##set($temp = $topicHashMap.put( $topic.getID(), $i ) )
#*   *#{
#*     *#"name" : "$urlencoder.encode($topicName)",
#*     *#"id" : "node$i"
#*   *#}
#*   *##set($i = $i + 1)
#*   *##set($temp = $ntopics.add($topic))
#*   *##if($i == $n)
#*     *##break 
#*   *##end
#* *##end
]
,"links":[
#* *##set( $i = 0 )
#* *##set( $topics = $topicMap.getTopics() )
#* *##set( $doneAssocs = $listmaker.make() )
#* *##foreach( $topic in $ntopics )
#*   *##set( $assocs = $topic.getAssociations() )
#*   *##foreach( $assoc in $assocs )
#*     *##set($roles = $assoc.getRoles())
#*     *##if(!$doneAssocs.contains($assoc) && $roles.toArray().size() >= 2)
#*       *##set($roles = $tmbox.sortTopics($roles,null) )
#*       *##if($topicHashMap.get($assoc.getPlayer($roles.toArray().get(0)).getID()) && $topicHashMap.get($assoc.getPlayer($roles.toArray().get(1)).getID()))
#*         *##if( $i != 0)
#*           *#,
#*         *##end
##
#*         *##set( $typeName = $assoc.getType().getBaseName() )
#*         *##if( $typeName ) #set( $typeName = $typeName.replaceAll("[^a-zA-Z0-9]","_")  )
#*         *##else #set( $typeName = "" ) #end
##
#*         *#{
#*           *#"source" : $topicHashMap.get($assoc.getPlayer($roles.toArray().get(0)).getID()),
#*           *#"target" : $topicHashMap.get($assoc.getPlayer($roles.toArray().get(1)).getID()),
#*           *#"class" : "assoc $typeName",
#*           *#"color" : "$colorMap.get($assoc.getType())",
#*           *#"id" : "link$i"
#*         *#}
#*         *##set($i = $i + 1)
#*         *##set( $temp = $doneAssocs.add( $assoc ) )
#*       *##end
#*     *##end
#*   *##end
#* *##end
#* *##if($topicMap.getTopics().size > 0 && $doneAssocs.size() > 0)
#*   *#,
#* *##end
#* *##set( $ntopics = $listmaker.make() )
#* *##set( $j = 0)
#* *##foreach ( $topic in $topics )
#*   *##set($temp = $ntopics.add($topic))
#*   *##set( $j = $j + 1)
#*   *##if($j == $n)
#*     *##break
#*   *##end
#* *##end
#* *##foreach( $topic in $ntopics )
#*   *##set( $types = $topic.getTypes() )
#*   *##foreach( $type in $types )
#*     *##if($topicHashMap.get($type.getID()) && $topicHashMap.get($topic.getID()))
#*       *##if($i > 0)
#*         *#,
#*       *##end
#*       *#{
#*         *#"source" : $topicHashMap.get($type.getID()),
#*         *#"target" : $topicHashMap.get($topic.getID()),
#*         *#"class" : "type",
#*         *#"id" : "link$i"
#*       *#}
#*       *##set($i = $i + 1)
#*     *##end
#*   *##end
#* *##end
]
}
);

#foreach($type in $assocTypeList)
  #set($color = $colorMap.get($type))
  var legend = d3.select(".legend")[0][0];
  var row = legend.insertRow(legend.rows.length);
  var aTypeCell = row.insertCell(0);
  aTypeCell.innerHTML = "$type.getBaseName()";
  var colorCell = row.insertCell(1);
  colorCell.innerHTML = "$color";
#end



      </script>
  </body>
</html>


Now you can add arrowheads to your graph by modifying the style sheet file in static/style.css. The graph is rendered using SVG and CSS can be used to style it like HTML. Each connection is represented by a line element. Associations have the class assoc, as well as the base name of the association type, with some characters replaced with an underscore _ . You can then style these lines with something like the following. Just copy it anywhere in the the CSS file.

Code: Select all
line.Superclass_Subclass {
    marker-end: url(#TriangleEnd);
    marker-start: url(#TriangleStart);
}


This will place an arrowhead on both ends of a superclass-subclass association. You'll probably only want to use an arrow on just one end, so remove one of the marker lines. Which one to remove depends on the roles of your association, the easiest way to figure out is to just test which one is the right one. And you'll of course want to change the Superclass_Subclass to the base name of the association type you want to modify.

You can then launch the d3graph visualisation from the menu server/browse services/d3graph. It'll prompt you to start the interval server first, just click ok for that. Then it should open the visualisation in a web browser.

I hope this helps.

Olli / Wandora Team
Olli
Site Admin
 
Posts: 10
Joined: Wed Sep 19, 2007 9:42 am

Re: Oriented associations

Postby Damien S. » Wed May 21, 2014 12:05 pm

Hello Olli,

It works perfectly.
Thanks a lot.

Two more thing,

- Is it possible to hide the "tree structure links"
For example, If I in subtopics group some applications into a Group1 topic, The d3graph draw links between Group1 and all Applications in this group.
For me, theses links are unsusal to be displayed and make the graph harder to understand.

- Is it also possible to draw a d3graph from a subtree ? and exclude some others topics (like Group1 described in the previous point) ?
In my case, I created some "technical" topics (for example flow way/input and flow way output) only necessaries to link two applications with informations.
May I exclude this part of the tree ?

Best regards.
Damien S.
 
Posts: 8
Joined: Fri May 09, 2014 12:53 pm

Re: Oriented associations

Postby Olli » Thu May 22, 2014 1:47 pm

Hi Damien,

Yes, it is possible to customise the links more. For this, you will have to modify the velocity template d3graph/templates/d3graph.vhtml. This is an Apache Velocity template that generates the html code for the webpage. The part that you need to edit is actually generating a Javascript table which is the data model for the graph. In the browser, having loaded up the graph page, if you just view the source, you'll find a large Javascript table in a script tag. It's essentially a list of nodes and links. This data is then passed on to create the visual representation. By changing what's included in this table you can make the visualisation include or exclude only certain things.

So what we need to do is modify the velocity template so that only the desired nodes and links get put in that Javascript data structure. First, to remove the instance-class relationships, you can just remove this section in the code. For me this starts on line 166, but it might be slightly different for you as our files might not be exactly identical at this point.

Code: Select all
#* *##if($topicMap.getTopics().size > 0 && $doneAssocs.size() > 0)
#*   *#,
#* *##end
#* *##set( $ntopics = $listmaker.make() )
#* *##set( $j = 0)
#* *##foreach ( $topic in $topics )
#*   *##set($temp = $ntopics.add($topic))
#*   *##set( $j = $j + 1)
#*   *##if($j == $n)
#*     *##break
#*   *##end
#* *##end
#* *##foreach( $topic in $ntopics )
#*   *##set( $types = $topic.getTypes() )
#*   *##foreach( $type in $types )
#*     *##if($topicHashMap.get($type.getID()) && $topicHashMap.get($topic.getID()))
#*       *##if($i > 0)
#*         *#,
#*       *##end
#*       *#{
#*         *#"source" : $topicHashMap.get($type.getID()),
#*         *#"target" : $topicHashMap.get($topic.getID()),
#*         *#"class" : "type",
#*         *#"id" : "link$i"
#*       *#}
#*       *##set($i = $i + 1)
#*     *##end
#*   *##end
#* *##end


Then, if you want to remove some topics completely, you need to make modifications to the $topics array. This is constructed at the start of the script section, for me, starting at line 48. You could add any kind of if clauses here to limit what kind of topics are included. For example, you could do something like #if( $topic.isOfType( $someOtherTopic) ) . The very first line of the template demonstrates how to get $someOtherTopic with a subject identifier. Look for the $topics.add( ) lines, putting the if statement around those should restrict what topics get included. Note that these are inside #set statements due to limitations in the velocity language.

You can similarly affect what associations are included. The links section of the javascript data structure is generated starting at line 134. Here you could add an if statement based on $assoc.getType() and only include certain types of association, for example.

More complicated modifications are possible too. You just need to change the code to only include the things you want visualised.

A couple of notes about the Velocity template language, in case you are not familiar with it. It's much like most other template languages. By default, everything gets output verbatim. Then you can include various control logic statements with a hash sign #. For example #if( something) ... #end . Then only if something is true, the thing between the if and end gets output. Variables begin with a dollar sign and can be used pretty freely both in # statements and anywhere else. You'll also see a lot of comments in the file. Two hash mark the rest of the line as comment and #* and *# are used to comment everything between the two. Pretty much all whitespace is commented in the file to avoid it going into the source file. This is not absolutely necessary but it avoids having long stretches of whitespace in the final generated html, admittedly at the cost of making the template less readable. A user guide for Velocity, with more information, can be found here http://velocity.apache.org/engine/devel/user-guide.html .


Olli
Olli
Site Admin
 
Posts: 10
Joined: Wed Sep 19, 2007 9:42 am


Return to Feature requests

Who is online

Users browsing this forum: No registered users and 1 guest