package prefuse.action.layout.graph; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import prefuse.data.Graph; import prefuse.data.Schema; import prefuse.data.tuple.TupleSet; import prefuse.data.util.TreeNodeIterator; import prefuse.visual.NodeItem; import prefuse.visual.VisualItem; /** * <p> * TreeLayout instance computing a TreeMap layout that optimizes for low * aspect ratios of visualized tree nodes. TreeMaps are a form of space-filling * layout that represents nodes as boxes on the display, with children nodes * represented as boxes placed within their parent's box. * </p> * <p> * This particular algorithm is taken from Bruls, D.M., C. Huizing, and * J.J. van Wijk, "Squarified Treemaps" In <i>Data Visualization 2000, * Proceedings of the Joint Eurographics and IEEE TCVG Sumposium on * Visualization</i>, 2000, pp. 33-42. Available online at: * <a href="http://www.win.tue.nl/~vanwijk/stm.pdf"> * http://www.win.tue.nl/~vanwijk/stm.pdf</a>. * </p> * <p> * For more information on TreeMaps in general, see * <a href="http://www.cs.umd.edu/hcil/treemap-history/"> * http://www.cs.umd.edu/hcil/treemap-history/</a>. * </p> * * @version 1.0 * @author <a href="http://jheer.org">jeffrey heer</a> */ public class SquarifiedTreeMapLayout extends TreeLayout { // column value in which layout stores area information public static final String AREA = "_area"; public static final Schema AREA_SCHEMA = new Schema(); static { AREA_SCHEMA.addColumn(AREA, double.class); } private static Comparator s_cmp = new Comparator() { public int compare(Object o1, Object o2) { double s1 = ((VisualItem)o1).getDouble(AREA); double s2 = ((VisualItem)o2).getDouble(AREA); return ( s1>s2 ? 1 : (s1<s2 ? -1 : 0)); } }; private ArrayList m_kids = new ArrayList(); private ArrayList m_row = new ArrayList(); private Rectangle2D m_r = new Rectangle2D.Double(); private double m_frame; // space between parents border and children /** * Creates a new SquarifiedTreeMapLayout with no spacing between * parent areas and their enclosed children. * @param group the data group to layout. Must resolve to a Graph instance. */ public SquarifiedTreeMapLayout(String group) { this(group, 0); } /** * Creates a new SquarifiedTreeMapLayout with the specified spacing between * parent areas and their enclosed children. * @param frame the amount of desired framing space between * parent areas and their enclosed children. * @param group the data group to layout. Must resolve to a Graph instance. */ public SquarifiedTreeMapLayout(String group, double frame) { super(group); setFrameWidth(frame); } /** * Sets the amount of desired framing space between parent rectangles and * their enclosed children. Use a value of 0 to remove frames altogether. * If you adjust the frame value, you must re-run the layout to see the * change reflected. Negative frame values are not allowed and will result * in an IllegalArgumentException. * @param frame the frame width, 0 for no frames */ public void setFrameWidth(double frame) { if ( frame < 0 ) throw new IllegalArgumentException( "Frame value must be greater than or equal to 0."); m_frame = frame; } /** * Gets the amount of desired framing space, in pixels, between * parent rectangles and their enclosed children. * @return the frame width */ public double getFrameWidth() { return m_frame; } /** * @see prefuse.action.Action#run(double) */ public void run(double frac) { // setup NodeItem root = getLayoutRoot(); Rectangle2D b = getLayoutBounds(); m_r.setRect(b.getX(), b.getY(), b.getWidth()-1, b.getHeight()-1); // process size values computeAreas(root); // layout root node setX(root, null, 0); setY(root, null, 0); root.setBounds(0, 0, m_r.getWidth(), m_r.getHeight()); // layout the tree updateArea(root, m_r); layout(root, m_r); } /** * Compute the pixel areas of nodes based on their size values. */ private void computeAreas(NodeItem root) { int leafCount = 0; // ensure area data column exists Graph g = (Graph)m_vis.getGroup(m_group); TupleSet nodes = g.getNodes(); nodes.addColumns(AREA_SCHEMA); // reset all sizes to zero Iterator iter = new TreeNodeIterator(root); while ( iter.hasNext() ) { NodeItem n = (NodeItem)iter.next(); n.setDouble(AREA, 0); } // set raw sizes, compute leaf count iter = new TreeNodeIterator(root, false); while ( iter.hasNext() ) { NodeItem n = (NodeItem)iter.next(); double area = 0; if ( n.getChildCount() == 0 ) { area = n.getSize(); ++leafCount; } else if ( n.isExpanded() ) { NodeItem c = (NodeItem)n.getFirstChild(); for (; c!=null; c = (NodeItem)c.getNextSibling()) { area += c.getDouble(AREA); ++leafCount; } } n.setDouble(AREA, area); } // scale sizes by display area factor Rectangle2D b = getLayoutBounds(); double area = (b.getWidth()-1)*(b.getHeight()-1); double scale = area/root.getDouble(AREA); iter = new TreeNodeIterator(root); while ( iter.hasNext() ) { NodeItem n = (NodeItem)iter.next(); n.setDouble(AREA, n.getDouble(AREA)*scale); } } /** * Compute the tree map layout. */ private void layout(NodeItem p, Rectangle2D r) { // create sorted list of children Iterator childIter = p.children(); while ( childIter.hasNext() ) m_kids.add(childIter.next()); Collections.sort(m_kids, s_cmp); // do squarified layout of siblings double w = Math.min(r.getWidth(),r.getHeight()); squarify(m_kids, m_row, w, r); m_kids.clear(); // clear m_kids // recurse childIter = p.children(); while ( childIter.hasNext() ) { NodeItem c = (NodeItem)childIter.next(); if ( c.getChildCount() > 0 && c.getDouble(AREA) > 0 ) { updateArea(c,r); layout(c, r); } } } private void updateArea(NodeItem n, Rectangle2D r) { Rectangle2D b = n.getBounds(); if ( m_frame == 0.0 ) { // if no framing, simply update bounding rectangle r.setRect(b); return; } // compute area loss due to frame double dA = 2*m_frame*(b.getWidth()+b.getHeight()-2*m_frame); double A = n.getDouble(AREA) - dA; // compute renormalization factor double s = 0; Iterator childIter = n.children(); while ( childIter.hasNext() ) s += ((NodeItem)childIter.next()).getDouble(AREA); double t = A/s; // re-normalize children areas childIter = n.children(); while ( childIter.hasNext() ) { NodeItem c = (NodeItem)childIter.next(); c.setDouble(AREA, c.getDouble(AREA)*t); } // set bounding rectangle and return r.setRect(b.getX()+m_frame, b.getY()+m_frame, b.getWidth()-2*m_frame, b.getHeight()-2*m_frame); return; } private void squarify(List c, List row, double w, Rectangle2D r) { double worst = Double.MAX_VALUE, nworst; int len; while ( (len=c.size()) > 0 ) { // add item to the row list, ignore if negative area VisualItem item = (VisualItem) c.get(len-1); double a = item.getDouble(AREA); if (a <= 0.0) { c.remove(len-1); continue; } row.add(item); nworst = worst(row, w); if ( nworst <= worst ) { c.remove(len-1); worst = nworst; } else { row.remove(row.size()-1); // remove the latest addition r = layoutRow(row, w, r); // layout the current row w = Math.min(r.getWidth(),r.getHeight()); // recompute w row.clear(); // clear the row worst = Double.MAX_VALUE; } } if ( row.size() > 0 ) { r = layoutRow(row, w, r); // layout the current row row.clear(); // clear the row } } private double worst(List rlist, double w) { double rmax = Double.MIN_VALUE, rmin = Double.MAX_VALUE, s = 0.0; Iterator iter = rlist.iterator(); while ( iter.hasNext() ) { double r = ((VisualItem)iter.next()).getDouble(AREA); rmin = Math.min(rmin, r); rmax = Math.max(rmax, r); s += r; } s = s*s; w = w*w; return Math.max(w*rmax/s, s/(w*rmin)); } private Rectangle2D layoutRow(List row, double w, Rectangle2D r) { double s = 0; // sum of row areas Iterator rowIter = row.iterator(); while ( rowIter.hasNext() ) s += ((VisualItem)rowIter.next()).getDouble(AREA); double x = r.getX(), y = r.getY(), d = 0; double h = w==0 ? 0 : s/w; boolean horiz = (w == r.getWidth()); // set node positions and dimensions rowIter = row.iterator(); while ( rowIter.hasNext() ) { NodeItem n = (NodeItem)rowIter.next(); NodeItem p = (NodeItem)n.getParent(); if ( horiz ) { setX(n, p, x+d); setY(n, p, y); } else { setX(n, p, x); setY(n, p, y+d); } double nw = n.getDouble(AREA)/h; if ( horiz ) { setNodeDimensions(n,nw,h); d += nw; } else { setNodeDimensions(n,h,nw); d += nw; } } // update space available in rectangle r if ( horiz ) r.setRect(x,y+h,r.getWidth(),r.getHeight()-h); else r.setRect(x+h,y,r.getWidth()-h,r.getHeight()); return r; } private void setNodeDimensions(NodeItem n, double w, double h) { n.setBounds(n.getX(), n.getY(), w, h); } } // end of class SquarifiedTreeMapLayout