package prefuse.action.layout.graph; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Arrays; import prefuse.Constants; import prefuse.Display; import prefuse.data.Graph; import prefuse.data.Schema; import prefuse.data.tuple.TupleSet; import prefuse.util.ArrayLib; import prefuse.visual.NodeItem; /** * <p>TreeLayout that computes a tidy layout of a node-link tree * diagram. This algorithm lays out a rooted tree such that each * depth level of the tree is on a shared line. The orientation of the * tree can be set such that the tree goes left-to-right (default), * right-to-left, top-to-bottom, or bottom-to-top.</p> * * <p>The algorithm used is that of Christoph Buchheim, Michael J�nger, * and Sebastian Leipert from their research paper * <a href="http://citeseer.ist.psu.edu/buchheim02improving.html"> * Improving Walker's Algorithm to Run in Linear Time</a>, Graph Drawing 2002. * This algorithm corrects performance issues in Walker's algorithm, which * generalizes Reingold and Tilford's method for tidy drawings of trees to * support trees with an arbitrary number of children at any given node.</p> * * @author <a href="http://jheer.org">jeffrey heer</a> */ public class NodeLinkTreeLayout extends TreeLayout { private int m_orientation; // the orientation of the tree private double m_bspace = 5; // the spacing between sibling nodes private double m_tspace = 25; // the spacing between subtrees private double m_dspace = 50; // the spacing between depth levels private double m_offset = 50; // pixel offset for root node position private double[] m_depths = new double[10]; private int m_maxDepth = 0; private double m_ax, m_ay; // for holding anchor co-ordinates /** * Create a new NodeLinkTreeLayout. A left-to-right orientation is assumed. * @param group the data group to layout. Must resolve to a Graph instance. */ public NodeLinkTreeLayout(String group) { super(group); m_orientation = Constants.ORIENT_LEFT_RIGHT; } /** * Create a new NodeLinkTreeLayout. * @param group the data group to layout. Must resolve to a Graph instance. * @param orientation the orientation of the tree layout. One of * {@link prefuse.Constants#ORIENT_LEFT_RIGHT}, * {@link prefuse.Constants#ORIENT_RIGHT_LEFT}, * {@link prefuse.Constants#ORIENT_TOP_BOTTOM}, or * {@link prefuse.Constants#ORIENT_BOTTOM_TOP}. * @param dspace the spacing to maintain between depth levels of the tree * @param bspace the spacing to maintain between sibling nodes * @param tspace the spacing to maintain between neighboring subtrees */ public NodeLinkTreeLayout(String group, int orientation, double dspace, double bspace, double tspace) { super(group); m_orientation = orientation; m_dspace = dspace; m_bspace = bspace; m_tspace = tspace; } // ------------------------------------------------------------------------ /** * Set the orientation of the tree layout. * @param orientation the orientation value. One of * {@link prefuse.Constants#ORIENT_LEFT_RIGHT}, * {@link prefuse.Constants#ORIENT_RIGHT_LEFT}, * {@link prefuse.Constants#ORIENT_TOP_BOTTOM}, or * {@link prefuse.Constants#ORIENT_BOTTOM_TOP}. */ public void setOrientation(int orientation) { if ( orientation < 0 || orientation >= Constants.ORIENTATION_COUNT || orientation == Constants.ORIENT_CENTER ) { throw new IllegalArgumentException( "Unsupported orientation value: "+orientation); } m_orientation = orientation; } /** * Get the orientation of the tree layout. * @return the orientation value. One of * {@link prefuse.Constants#ORIENT_LEFT_RIGHT}, * {@link prefuse.Constants#ORIENT_RIGHT_LEFT}, * {@link prefuse.Constants#ORIENT_TOP_BOTTOM}, or * {@link prefuse.Constants#ORIENT_BOTTOM_TOP}. */ public int getOrientation() { return m_orientation; } /** * Set the spacing between depth levels. * @param d the depth spacing to use */ public void setDepthSpacing(double d) { m_dspace = d; } /** * Get the spacing between depth levels. * @return the depth spacing */ public double getDepthSpacing() { return m_dspace; } /** * Set the spacing between neighbor nodes. * @param b the breadth spacing to use */ public void setBreadthSpacing(double b) { m_bspace = b; } /** * Get the spacing between neighbor nodes. * @return the breadth spacing */ public double getBreadthSpacing() { return m_bspace; } /** * Set the spacing between neighboring subtrees. * @param s the subtree spacing to use */ public void setSubtreeSpacing(double s) { m_tspace = s; } /** * Get the spacing between neighboring subtrees. * @return the subtree spacing */ public double getSubtreeSpacing() { return m_tspace; } /** * Set the offset value for placing the root node of the tree. The * dimension in which this offset is applied is dependent upon the * orientation of the tree. For example, in a left-to-right orientation, * the offset will a horizontal offset from the left edge of the layout * bounds. * @param o the value by which to offset the root node of the tree */ public void setRootNodeOffset(double o) { m_offset = o; } /** * Get the offset value for placing the root node of the tree. * @return the value by which the root node of the tree is offset */ public double getRootNodeOffset() { return m_offset; } // ------------------------------------------------------------------------ /** * @see prefuse.action.layout.Layout#getLayoutAnchor() */ public Point2D getLayoutAnchor() { if ( m_anchor != null ) return m_anchor; m_tmpa.setLocation(0,0); if ( m_vis != null ) { Display d = m_vis.getDisplay(0); Rectangle2D b = this.getLayoutBounds(); switch ( m_orientation ) { case Constants.ORIENT_LEFT_RIGHT: m_tmpa.setLocation(m_offset, d.getHeight()/2.0); break; case Constants.ORIENT_RIGHT_LEFT: m_tmpa.setLocation(b.getMaxX()-m_offset, d.getHeight()/2.0); break; case Constants.ORIENT_TOP_BOTTOM: m_tmpa.setLocation(d.getWidth()/2.0, m_offset); break; case Constants.ORIENT_BOTTOM_TOP: m_tmpa.setLocation(d.getWidth()/2.0, b.getMaxY()-m_offset); break; } d.getInverseTransform().transform(m_tmpa, m_tmpa); } return m_tmpa; } private double spacing(NodeItem l, NodeItem r, boolean siblings) { boolean w = ( m_orientation == Constants.ORIENT_TOP_BOTTOM || m_orientation == Constants.ORIENT_BOTTOM_TOP ); return (siblings ? m_bspace : m_tspace) + 0.5 * ( w ? l.getBounds().getWidth() + r.getBounds().getWidth() : l.getBounds().getHeight() + r.getBounds().getHeight() ); } private void updateDepths(int depth, NodeItem item) { boolean v = ( m_orientation == Constants.ORIENT_TOP_BOTTOM || m_orientation == Constants.ORIENT_BOTTOM_TOP ); double d = ( v ? item.getBounds().getHeight() : item.getBounds().getWidth() ); if ( m_depths.length <= depth ) m_depths = ArrayLib.resize(m_depths, 3*depth/2); m_depths[depth] = Math.max(m_depths[depth], d); m_maxDepth = Math.max(m_maxDepth, depth); } private void determineDepths() { for ( int i=1; i<m_maxDepth; ++i ) m_depths[i] += m_depths[i-1] + m_dspace; } // ------------------------------------------------------------------------ /** * @see prefuse.action.Action#run(double) */ public void run(double frac) { Graph g = (Graph)m_vis.getGroup(m_group); initSchema(g.getNodes()); Arrays.fill(m_depths, 0); m_maxDepth = 0; Point2D a = getLayoutAnchor(); m_ax = a.getX(); m_ay = a.getY(); NodeItem root = getLayoutRoot(); Params rp = getParams(root); // do first pass - compute breadth information, collect depth info firstWalk(root, 0, 1); // sum up the depth info determineDepths(); // do second pass - assign layout positions secondWalk(root, null, -rp.prelim, 0); } private void firstWalk(NodeItem n, int num, int depth) { Params np = getParams(n); np.number = num; updateDepths(depth, n); boolean expanded = n.isExpanded(); if ( n.getChildCount() == 0 || !expanded ) // is leaf { NodeItem l = (NodeItem)n.getPreviousSibling(); if ( l == null ) { np.prelim = 0; } else { np.prelim = getParams(l).prelim + spacing(l,n,true); } } else if ( expanded ) { NodeItem leftMost = (NodeItem)n.getFirstChild(); NodeItem rightMost = (NodeItem)n.getLastChild(); NodeItem defaultAncestor = leftMost; NodeItem c = leftMost; for ( int i=0; c != null; ++i, c = (NodeItem)c.getNextSibling() ) { firstWalk(c, i, depth+1); defaultAncestor = apportion(c, defaultAncestor); } executeShifts(n); double midpoint = 0.5 * (getParams(leftMost).prelim + getParams(rightMost).prelim); NodeItem left = (NodeItem)n.getPreviousSibling(); if ( left != null ) { np.prelim = getParams(left).prelim + spacing(left, n, true); np.mod = np.prelim - midpoint; } else { np.prelim = midpoint; } } } private NodeItem apportion(NodeItem v, NodeItem a) { NodeItem w = (NodeItem)v.getPreviousSibling(); if ( w != null ) { NodeItem vip, vim, vop, vom; double sip, sim, sop, som; vip = vop = v; vim = w; vom = (NodeItem)vip.getParent().getFirstChild(); sip = getParams(vip).mod; sop = getParams(vop).mod; sim = getParams(vim).mod; som = getParams(vom).mod; NodeItem nr = nextRight(vim); NodeItem nl = nextLeft(vip); while ( nr != null && nl != null ) { vim = nr; vip = nl; vom = nextLeft(vom); vop = nextRight(vop); getParams(vop).ancestor = v; double shift = (getParams(vim).prelim + sim) - (getParams(vip).prelim + sip) + spacing(vim,vip,false); if ( shift > 0 ) { moveSubtree(ancestor(vim,v,a), v, shift); sip += shift; sop += shift; } sim += getParams(vim).mod; sip += getParams(vip).mod; som += getParams(vom).mod; sop += getParams(vop).mod; nr = nextRight(vim); nl = nextLeft(vip); } if ( nr != null && nextRight(vop) == null ) { Params vopp = getParams(vop); vopp.thread = nr; vopp.mod += sim - sop; } if ( nl != null && nextLeft(vom) == null ) { Params vomp = getParams(vom); vomp.thread = nl; vomp.mod += sip - som; a = v; } } return a; } private NodeItem nextLeft(NodeItem n) { NodeItem c = null; if ( n.isExpanded() ) c = (NodeItem)n.getFirstChild(); return ( c != null ? c : getParams(n).thread ); } private NodeItem nextRight(NodeItem n) { NodeItem c = null; if ( n.isExpanded() ) c = (NodeItem)n.getLastChild(); return ( c != null ? c : getParams(n).thread ); } private void moveSubtree(NodeItem wm, NodeItem wp, double shift) { Params wmp = getParams(wm); Params wpp = getParams(wp); double subtrees = wpp.number - wmp.number; wpp.change -= shift/subtrees; wpp.shift += shift; wmp.change += shift/subtrees; wpp.prelim += shift; wpp.mod += shift; } private void executeShifts(NodeItem n) { double shift = 0, change = 0; for ( NodeItem c = (NodeItem)n.getLastChild(); c != null; c = (NodeItem)c.getPreviousSibling() ) { Params cp = getParams(c); cp.prelim += shift; cp.mod += shift; change += cp.change; shift += cp.shift + change; } } private NodeItem ancestor(NodeItem vim, NodeItem v, NodeItem a) { NodeItem p = (NodeItem)v.getParent(); Params vimp = getParams(vim); if ( vimp.ancestor.getParent() == p ) { return vimp.ancestor; } else { return a; } } private void secondWalk(NodeItem n, NodeItem p, double m, int depth) { Params np = getParams(n); setBreadth(n, p, np.prelim + m); setDepth(n, p, m_depths[depth]); if ( n.isExpanded() ) { depth += 1; for ( NodeItem c = (NodeItem)n.getFirstChild(); c != null; c = (NodeItem)c.getNextSibling() ) { secondWalk(c, n, m + np.mod, depth); } } np.clear(); } private void setBreadth(NodeItem n, NodeItem p, double b) { switch ( m_orientation ) { case Constants.ORIENT_LEFT_RIGHT: case Constants.ORIENT_RIGHT_LEFT: setY(n, p, m_ay + b); break; case Constants.ORIENT_TOP_BOTTOM: case Constants.ORIENT_BOTTOM_TOP: setX(n, p, m_ax + b); break; default: throw new IllegalStateException(); } } private void setDepth(NodeItem n, NodeItem p, double d) { switch ( m_orientation ) { case Constants.ORIENT_LEFT_RIGHT: setX(n, p, m_ax + d); break; case Constants.ORIENT_RIGHT_LEFT: setX(n, p, m_ax - d); break; case Constants.ORIENT_TOP_BOTTOM: setY(n, p, m_ay + d); break; case Constants.ORIENT_BOTTOM_TOP: setY(n, p, m_ay - d); break; default: throw new IllegalStateException(); } } // ------------------------------------------------------------------------ // Params Schema /** * The data field in which the parameters used by this layout are stored. */ public static final String PARAMS = "_reingoldTilfordParams"; /** * The schema for the parameters used by this layout. */ public static final Schema PARAMS_SCHEMA = new Schema(); static { PARAMS_SCHEMA.addColumn(PARAMS, Params.class); } protected void initSchema(TupleSet ts) { ts.addColumns(PARAMS_SCHEMA); } private Params getParams(NodeItem item) { Params rp = (Params)item.get(PARAMS); if ( rp == null ) { rp = new Params(); item.set(PARAMS, rp); } if ( rp.number == -2 ) { rp.init(item); } return rp; } /** * Wrapper class holding parameters used for each node in this layout. */ public static class Params implements Cloneable { double prelim; double mod; double shift; double change; int number = -2; NodeItem ancestor = null; NodeItem thread = null; public void init(NodeItem item) { ancestor = item; number = -1; } public void clear() { number = -2; prelim = mod = shift = change = 0; ancestor = thread = null; } } } // end of class NodeLinkTreeLayout