Search code examples
javagraphvisualizationjung

JUNG - Large graph visualization


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.


Solution

  • 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;
        }    
    }