3

I am currently developing a tool for visualization of metagenomics data using graphs and so the Java JUNG graph visualization library.

I encounter a delay when there are around 1000 nodes being shown, either by moving the camera around or dragging some of the nodes.

Is there any hack can that be used to improve this situation? I read something about dividing the window in chunks, and to only work with chunks of the panel that are being shown, but I cannot understand this.

Thank you.

Vlad L
  • 33
  • 5

2 Answers2

4

The question might be considered as too broad, because there are simply too many degrees of freedom for the optimization. And there are questions that are at least related (Improve the rendering of a JUNG graph , JUNG cannot display large graphs? or others), if not duplicates.

However, I'll try to answer it here:

In general, with JUNG, you can create a nice graph, with impressive default functionality (interaction), and many features, easily and with a few lines of code. In this regard, JUNG does not primarily aim at painting graphs with 1000's of vertices. Instead, it aims at painting a graph with dozens (or maybe few 100's) vertices and edges nicely.

(In fact, painting a graph with >1000 vertices rarely makes sense at all, from a theoretical, information visualization standpoint. You won't be able to visually extract any information from such a graph - at least, not without excesssive zooming and panning)

When you want to render a graph with many vertices and many edges, there are options to increase the performance. (You did not say anything about the number of edges. In many cases, these are the most expensive thing!).

From my experience, the single most important thing for improving the rendering performance is to....

disable anti-aliasing!

Seriously, this is really expensive. In JUNG, this can be done with

visualizationViewer.getRenderingHints().remove(
    RenderingHints.KEY_ANTIALIASING)

Beyond that, there are many options to increase the performance, but of course, they all depend on which visual feature you want to sacrifice. Below is an example that shows a graph with 2500 vertices and 5000 edges. By default, it's horribly slow. The improvePerformance method contains several options of how to make the visualization faster. Even when only disabling anti-aliasing, the performance is acceptable on my (rather slow) machine.

Edited/extended in response to the comments:

import java.awt.Dimension;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Point2D;
import java.util.Random;

import javax.swing.JFrame;
import javax.swing.SwingUtilities;

import org.apache.commons.collections15.Predicate;

import edu.uci.ics.jung.algorithms.layout.FRLayout;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.graph.DirectedSparseGraph;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.util.Context;
import edu.uci.ics.jung.graph.util.Pair;
import edu.uci.ics.jung.visualization.Layer;
import edu.uci.ics.jung.visualization.RenderContext;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import edu.uci.ics.jung.visualization.control.DefaultModalGraphMouse;
import edu.uci.ics.jung.visualization.decorators.EdgeShape;
import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer;
import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator;

public class JungPerformance 
{
    public static void main(String[] args) 
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        Graph<String, String> g = createGraph();

        Dimension size = new Dimension(800,800);
        VisualizationViewer<String, String> vv = 
            new VisualizationViewer<String, String>(
                new FRLayout<String, String>(g, size));
        DefaultModalGraphMouse<String, Double> graphMouse = 
            new DefaultModalGraphMouse<String, Double>();
        vv.setGraphMouse(graphMouse); 

        improvePerformance(vv);

        f.getContentPane().add(vv);
        f.setSize(size);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    // This method summarizes several options for improving the painting
    // performance. Enable or disable them depending on which visual features
    // you want to sacrifice for the higher performance.
    private static <V, E> void improvePerformance(
        VisualizationViewer<V, E> vv)
    {
        // Probably the most important step for the pure rendering performance:
        // Disable anti-aliasing
        vv.getRenderingHints().remove(RenderingHints.KEY_ANTIALIASING);

        // Skip vertices that are not inside the visible area. 
        doNotPaintInvisibleVertices(vv);

        // May be helpful for performance in general, but not appropriate 
        // when there are multiple edges between a pair of nodes: Draw
        // the edges not as curves, but as straight lines:
        vv.getRenderContext().setEdgeShapeTransformer(new EdgeShape.Line<V,E>());

        // May be helpful for painting performance: Omit the arrow heads
        // of directed edges
        Predicate<Context<Graph<V, E>, E>> edgeArrowPredicate = 
            new Predicate<Context<Graph<V,E>,E>>()
        {
            @Override
            public boolean evaluate(Context<Graph<V, E>, E> arg0)
            {
                return false;
            }
        };
        vv.getRenderContext().setEdgeArrowPredicate(edgeArrowPredicate);

    }

    // Skip all vertices that are not in the visible area. 
    // NOTE: See notes at the end of this method!
    private static <V, E> void doNotPaintInvisibleVertices(
        VisualizationViewer<V, E> vv)
    {
        Predicate<Context<Graph<V, E>, V>> vertexIncludePredicate = 
            new Predicate<Context<Graph<V,E>,V>>()
        {
            Dimension size = new Dimension();

            @Override
            public boolean evaluate(Context<Graph<V, E>, V> c)
            {
                vv.getSize(size);
                Point2D point = vv.getGraphLayout().transform(c.element);
                Point2D transformed = 
                    vv.getRenderContext().getMultiLayerTransformer()
                        .transform(point);
                if (transformed.getX() < 0 || transformed.getX() > size.width)
                {
                    return false;
                }
                if (transformed.getY() < 0 || transformed.getY() > size.height)
                {
                    return false;
                }
                return true;
            }
        };
        vv.getRenderContext().setVertexIncludePredicate(vertexIncludePredicate);

        // NOTE: By default, edges will NOT be included in the visualization
        // when ONE of their vertices is NOT included in the visualization.
        // This may look a bit odd when zooming and panning over the graph.
        // Calling the following method will cause the edges to be skipped
        // ONLY when BOTH their vertices are NOT included in the visualization,
        // which may look nicer and more intuitive
        doPaintEdgesAtLeastOneVertexIsVisible(vv);
    }

    // See note at end of "doNotPaintInvisibleVertices"
    private static <V, E> void doPaintEdgesAtLeastOneVertexIsVisible(
        VisualizationViewer<V, E> vv)
    {
        vv.getRenderer().setEdgeRenderer(new BasicEdgeRenderer<V, E>()
        {
            @Override
            public void paintEdge(RenderContext<V,E> rc, Layout<V, E> layout, E e) 
            {
                GraphicsDecorator g2d = rc.getGraphicsContext();
                Graph<V,E> graph = layout.getGraph();
                if (!rc.getEdgeIncludePredicate().evaluate(
                        Context.<Graph<V,E>,E>getInstance(graph,e)))
                    return;

                Pair<V> endpoints = graph.getEndpoints(e);
                V v1 = endpoints.getFirst();
                V v2 = endpoints.getSecond();
                if (!rc.getVertexIncludePredicate().evaluate(
                        Context.<Graph<V,E>,V>getInstance(graph,v1)) && 
                    !rc.getVertexIncludePredicate().evaluate(
                        Context.<Graph<V,E>,V>getInstance(graph,v2)))
                    return;

                Stroke new_stroke = rc.getEdgeStrokeTransformer().transform(e);
                Stroke old_stroke = g2d.getStroke();
                if (new_stroke != null)
                    g2d.setStroke(new_stroke);

                drawSimpleEdge(rc, layout, e);

                // restore paint and stroke
                if (new_stroke != null)
                    g2d.setStroke(old_stroke);
            }
        });
    }


    public static Graph<String, String> createGraph() 
    {
        Random random = new Random(0);
        int numVertices = 2500;
        int numEdges = 5000;
        Graph<String, String> g = new DirectedSparseGraph<String, String>();
        for (int i=0; i<numVertices; i++)
        {
            g.addVertex("v"+i);
        }
        for (int i=0; i<numEdges; i++)
        {
            int v0 = random.nextInt(numVertices);
            int v1 = random.nextInt(numVertices);
            g.addEdge("e"+i, "v"+v0, "v"+v1);
        }
        return g;
    }    
}
Community
  • 1
  • 1
Marco13
  • 53,703
  • 9
  • 80
  • 159
  • Thank you sir, disabling anti-aliasing improved the performance. It takes a couple of minutes to load the data and still far from the 100.000 mark, but later on the interaction seems to be fluent. The context of the project is related with genomics, this is why it is interesting to show as much as possible data. Have you ever considered an improvement such as: not taking into account the vertices that are not visible in the window? Say iterating over the vertices and if their (x,y) coordinate is out of the camera ignore it? – Vlad L Jun 19 '16 at 10:16
  • The nice thing about JUNG is: You can influence basically **everything**. The pluggable renderer and the fact that nearly all classes and methods are `public` and may be overridden allows you to do the weirdest things. Often, this means that you have to dig a bit through the source code and see how the classes are actually used. I have edited/extended the code to show how vertices/edges that are not visible may be skipped. But note that you always have to consider possible side-effects. I'm just giving hints about **possible** approaches here. – Marco13 Jun 19 '16 at 11:31
  • Thank you again sir. I still found out one problem with the code above. The thing is, when you zoom-in/out it seems that the layout dimensions are not modified, therefore there is a blank zone around the vertexes because the algorithm is applied to the original dimensions of the layout. I am trying to figure out how to get the dimensions of the view every time it is scaled, but that seems impossible. Maybe modifying the manually every time there is a scale event will do the work. Have you ever encountered such problem? – Vlad L Jun 26 '16 at 16:51
  • A minor change: `.transform(Layer.LAYOUT, point);` should have just been `.transform(point);` (if this does not fix the problem, I'll have to review it more thoroughly - sorry, I haven't used JUNG sooo actively recently...) – Marco13 Jun 26 '16 at 17:47
  • Damn it.. it works just fine. I just implemented it using a scaler with an implicit size of the drawing panel, but that modification is by far simpler. Thank you again! But then, what does .transform(point) exactly do? Because I tried changing it to .transform(Layer.VIEW, point), but that did not solve the problem. – Vlad L Jun 26 '16 at 18:22
  • Again, I'd have to refresh my memories about the internal workings of JUNG, but IIRC, the "multilayer transformer" applies the transformations for the vertices for the layout and for the view. When doing `transform(Layer.LAYOUT, ...)` (or analogously, with `VIEW`), then only *one* of these transformations will be applied, resulting in wrong vertex positions (roughly ... don't pin me down to the details - I had not used this vertex predicate before answering this question...) – Marco13 Jun 26 '16 at 19:21
  • Marco13's suggestions here were the impetus behind my making a new graph rendering system based on jung. His suggestions allow drawing of much larger graphs. I should also confess that the transform/inverseTransform Layer.LAYOUT etc stuff mentioned above is all my fault! :) –  Oct 11 '19 at 10:08
  • @TomNelson There is (at least) one other repo where the development of JUNG is continued: https://github.com/jrtom/jung (I think he (also?) was one of the developers behind the original JUNG...). I haven't looked into the details, but wonder whether there is enough overlap to justify funneling the efforts of both your projects into one repo...? – Marco13 Oct 11 '19 at 13:09
0

@Marco13's answer is a good one. I will add (as one of JUNG's authors) that JUNG's current major flaw in terms of visualization scaling is a lack of good spatial data structures. As a result, both force-directed layouts and interactive visualization for larger graphs can be pretty slow.

At some point we'll get around to addressing that (patches welcome :) ).

Joshua O'Madadhain
  • 2,704
  • 1
  • 14
  • 18
  • (Sorry, the comments are the wrong place to discuss this, but I just sumbled over this answer, and I'd delete this comment later) : The development of JUNG seemed dormant recently (which may be "OK", because it is a time-tested library that does its job). Are there any specific plans to address these issues, e.g. data structures for "large" graphs etc? – Marco13 Aug 01 '16 at 10:42
  • Marco: development on JUNG is active but effectively invisible at the moment; watch the GitHub site for updates. :). We would like to address this issue but other planned improvements are in front of it. If you'd like to work with us on this we'd be happy to. – Joshua O'Madadhain Aug 01 '16 at 18:07
  • Yes, after seeing your answer, I noticed that it's now on github, and there was a new release recently - but I also saw "few" commits. I thought that many of the Java 8 features (`Function`) would fit nicely into the concepts of JUNG, and could help to "modernize" it (maybe not `jung3`, but ... y'know ;-)), but I noticed that Guava was chosen instead. Is there any roadmap for the future development? – Marco13 Aug 01 '16 at 18:23