package prefuse.action.layout.graph; import java.awt.geom.Point2D; import java.util.Iterator; import prefuse.data.Graph; import prefuse.data.Schema; import prefuse.data.tuple.TupleSet; import prefuse.visual.NodeItem; /** * <p>Layout that computes a circular "balloon-tree" layout of a tree. * This layout places children nodes radially around their parents, and is * equivalent to a top-down flattened view of a ConeTree.</p> * * <p>The algorithm used is that of G. Melan�on and I. Herman from their * research paper Circular Drawings of Rooted Trees, Reports of the Centre for * Mathematics and Computer Sciences, Report Number INS�9817, 1998.</p> * * @author <a href="http://jheer.org">jeffrey heer</a> */ public class BalloonTreeLayout extends TreeLayout { private int m_minRadius = 2; /** * Create a new BalloonTreeLayout * @param group the data group to layout. Must resolve to a Graph * or Tree instance. */ public BalloonTreeLayout(String group) { this(group, 2); } /** * Create a new BalloonTreeLayout * @param group the data group to layout. Must resolve to a Graph * or Tree instance. * @param minRadius the minimum radius to use for a layout circle */ public BalloonTreeLayout(String group, int minRadius) { super(group); m_minRadius = minRadius; } /** * Get the minimum radius used for a layout circle. * @return the minimum layout radius */ public int getMinRadius() { return m_minRadius; } /** * Set the minimum radius used for a layout circle. * @param minRadius the minimum layout radius */ public void setMinRadius(int minRadius) { m_minRadius = minRadius; } /** * @see prefuse.action.Action#run(double) */ public void run(double frac) { Graph g = (Graph)m_vis.getGroup(m_group); initSchema(g.getNodes()); Point2D anchor = getLayoutAnchor(); NodeItem n = getLayoutRoot(); layout(n,anchor.getX(),anchor.getY()); } private void layout(NodeItem n, double x, double y) { firstWalk(n); secondWalk(n,null,x,y,1,0); } private void firstWalk(NodeItem n) { Params np = getParams(n); np.d = 0; double s = 0; Iterator childIter = n.children(); while ( childIter.hasNext() ) { NodeItem c = (NodeItem)childIter.next(); if ( !c.isVisible() ) continue; firstWalk(c); Params cp = getParams(c); np.d = Math.max(np.d,cp.r); cp.a = Math.atan(((double)cp.r)/(np.d+cp.r)); s += cp.a; } adjustChildren(np, s); setRadius(np); } private void adjustChildren(Params np, double s) { if ( s > Math.PI ) { np.c = Math.PI/s; np.f = 0; } else { np.c = 1; np.f = Math.PI - s; } } private void setRadius(Params np) { np.r = Math.max(np.d,m_minRadius) + 2*np.d; } /* private void setRadius(NodeItem n, ParamBlock np) { int numChildren = n.getChildCount(); double p = Math.PI; double fs = (numChildren==0 ? 0 : np.f/numChildren); double pr = 0; double bx = 0, by = 0; Iterator childIter = n.getChildren(); while ( childIter.hasNext() ) { NodeItem c = (NodeItem)childIter.next(); ParamBlock cp = getParams(c); p += pr + cp.a + fs; bx += (cp.r)*Math.cos(p); by += (cp.r)*Math.sin(p); pr = cp.a; } if ( numChildren != 0 ) { bx /= numChildren; by /= numChildren; } np.rx = -bx; np.ry = -by; p = Math.PI; pr = 0; np.r = 0; childIter = n.getChildren(); while ( childIter.hasNext() ) { NodeItem c = (NodeItem)childIter.next(); ParamBlock cp = getParams(c); p += pr + cp.a + fs; double x = cp.r*Math.cos(p)-bx; double y = cp.r*Math.sin(p)-by; double d = Math.sqrt(x*x+y*y) + cp.r; np.r = Math.max(np.r, (int)Math.round(d)); pr = cp.a; } if ( np.r == 0 ) np.r = m_minRadius + 2*np.d; } // private void secondWalk2(NodeItem n, NodeItem r, double x, double y, double l, double t) { ParamBlock np = getParams(n); double cost = Math.cos(t); double sint = Math.sin(t); double nx = x + l*(np.rx*cost-np.ry*sint); double ny = y + l*(np.rx*sint+np.ry*cost); setLocation(n,r,nx,ny); double dd = l*np.d; double p = Math.PI; double fs = np.f / (n.getChildCount()+1); double pr = 0; Iterator childIter = n.getChildren(); while ( childIter.hasNext() ) { NodeItem c = (NodeItem)childIter.next(); ParamBlock cp = getParams(c); double aa = np.c * cp.a; double rr = np.d * Math.tan(aa)/(1-Math.tan(aa)); p += pr + aa + fs; double xx = (l*rr+dd)*Math.cos(p)+np.rx; double yy = (l*rr+dd)*Math.sin(p)+np.ry; double x2 = xx*cost - yy*sint; double y2 = xx*sint + yy*cost; pr = aa; secondWalk2(c, n, x+x2, y+y2, l*rr/cp.r, p); } } // */ private void secondWalk(NodeItem n, NodeItem r, double x, double y, double l, double t) { setX(n, r, x); setY(n, r, y); Params np = getParams(n); int numChildren = 0; Iterator childIter = n.children(); while ( childIter.hasNext() ) { NodeItem c = (NodeItem)childIter.next(); if ( c.isVisible() ) ++numChildren; } double dd = l*np.d; double p = t + Math.PI; double fs = (numChildren==0 ? 0 : np.f/numChildren); double pr = 0; childIter = n.children(); while ( childIter.hasNext() ) { NodeItem c = (NodeItem)childIter.next(); if ( !c.isVisible() ) continue; Params cp = getParams(c); double aa = np.c * cp.a; double rr = np.d * Math.tan(aa)/(1-Math.tan(aa)); p += pr + aa + fs; double xx = (l*rr+dd)*Math.cos(p); double yy = (l*rr+dd)*Math.sin(p); pr = aa; secondWalk(c, n, x+xx, y+yy, l*np.c/*l*rr/cp.r*/, p); } } // ------------------------------------------------------------------------ // Parameters /** * The data field in which the parameters used by this layout are stored. */ public static final String PARAMS = "_balloonTreeLayoutParams"; /** * The schema for the parameters used by this layout. */ public static final Schema PARAMS_SCHEMA = new Schema(); static { PARAMS_SCHEMA.addColumn(PARAMS, Params.class); } private void initSchema(TupleSet ts) { try { ts.addColumns(PARAMS_SCHEMA); } catch ( IllegalArgumentException iae ) {} } private Params getParams(NodeItem n) { Params np = (Params)n.get(PARAMS); if ( np == null ) { np = new Params(); n.set(PARAMS, np); } return np; } /** * Wrapper class holding parameters used for each node in this layout. */ public static class Params { public int d; public int r; public double rx, ry; public double a; public double c; public double f; } } // end of class BalloonTreeLayout