/** * Copyright (c) 2004-2006 Regents of the University of California. * See "license-prefuse.txt" for licensing terms. */ package prefuse.demos; import java.awt.Cursor; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Iterator; import javax.swing.JFrame; import javax.swing.SwingUtilities; import prefuse.Constants; import prefuse.Display; import prefuse.Visualization; import prefuse.action.ActionList; import prefuse.action.RepaintAction; import prefuse.action.assignment.ColorAction; import prefuse.action.assignment.DataColorAction; import prefuse.action.layout.Layout; import prefuse.action.layout.graph.ForceDirectedLayout; import prefuse.activity.Activity; import prefuse.controls.ControlAdapter; import prefuse.controls.PanControl; import prefuse.controls.ZoomControl; import prefuse.data.Graph; import prefuse.data.Node; import prefuse.render.DefaultRendererFactory; import prefuse.render.PolygonRenderer; import prefuse.render.Renderer; import prefuse.render.ShapeRenderer; import prefuse.util.ColorLib; import prefuse.util.GraphicsLib; import prefuse.visual.AggregateItem; import prefuse.visual.AggregateTable; import prefuse.visual.VisualGraph; import prefuse.visual.VisualItem; /** * Demo application showcasing the use of AggregateItems to * visualize groupings of nodes with in a graph visualization. * * This class uses the AggregateLayout class to compute bounding * polygons for each aggregate and the AggregateDragControl to * enable drags of both nodes and node aggregates. * * @author <a href="http://jheer.org">jeffrey heer</a> */ public class AggregateDemo extends Display { public static final String GRAPH = "graph"; public static final String NODES = "graph.nodes"; public static final String EDGES = "graph.edges"; public static final String AGGR = "aggregates"; public AggregateDemo() { // initialize display and data super(new Visualization()); initDataGroups(); // set up the renderers // draw the nodes as basic shapes Renderer nodeR = new ShapeRenderer(20); // draw aggregates as polygons with curved edges Renderer polyR = new PolygonRenderer(Constants.POLY_TYPE_CURVE); ((PolygonRenderer)polyR).setCurveSlack(0.15f); DefaultRendererFactory drf = new DefaultRendererFactory(); drf.setDefaultRenderer(nodeR); drf.add("ingroup('aggregates')", polyR); m_vis.setRendererFactory(drf); // set up the visual operators // first set up all the color actions ColorAction nStroke = new ColorAction(NODES, VisualItem.STROKECOLOR); nStroke.setDefaultColor(ColorLib.gray(100)); nStroke.add("_hover", ColorLib.gray(50)); ColorAction nFill = new ColorAction(NODES, VisualItem.FILLCOLOR); nFill.setDefaultColor(ColorLib.gray(255)); nFill.add("_hover", ColorLib.gray(200)); ColorAction nEdges = new ColorAction(EDGES, VisualItem.STROKECOLOR); nEdges.setDefaultColor(ColorLib.gray(100)); ColorAction aStroke = new ColorAction(AGGR, VisualItem.STROKECOLOR); aStroke.setDefaultColor(ColorLib.gray(200)); aStroke.add("_hover", ColorLib.rgb(255,100,100)); int[] palette = new int[] { ColorLib.rgba(255,200,200,150), ColorLib.rgba(200,255,200,150), ColorLib.rgba(200,200,255,150) }; ColorAction aFill = new DataColorAction(AGGR, "id", Constants.NOMINAL, VisualItem.FILLCOLOR, palette); // bundle the color actions ActionList colors = new ActionList(); colors.add(nStroke); colors.add(nFill); colors.add(nEdges); colors.add(aStroke); colors.add(aFill); // now create the main layout routine ActionList layout = new ActionList(Activity.INFINITY); layout.add(colors); layout.add(new ForceDirectedLayout(GRAPH, true)); layout.add(new AggregateLayout(AGGR)); layout.add(new RepaintAction()); m_vis.putAction("layout", layout); // set up the display setSize(500,500); pan(250, 250); setHighQuality(true); addControlListener(new AggregateDragControl()); addControlListener(new ZoomControl()); addControlListener(new PanControl()); // set things running m_vis.run("layout"); } private void initDataGroups() { // create sample graph // 9 nodes broken up into 3 interconnected cliques Graph g = new Graph(); for ( int i=0; i<3; ++i ) { Node n1 = g.addNode(); Node n2 = g.addNode(); Node n3 = g.addNode(); g.addEdge(n1, n2); g.addEdge(n1, n3); g.addEdge(n2, n3); } g.addEdge(0, 3); g.addEdge(3, 6); g.addEdge(6, 0); // add visual data groups VisualGraph vg = m_vis.addGraph(GRAPH, g); m_vis.setInteractive(EDGES, null, false); m_vis.setValue(NODES, null, VisualItem.SHAPE, new Integer(Constants.SHAPE_ELLIPSE)); AggregateTable at = m_vis.addAggregates(AGGR); at.addColumn(VisualItem.POLYGON, float[].class); at.addColumn("id", int.class); // add nodes to aggregates // create an aggregate for each 3-clique of nodes Iterator nodes = vg.nodes(); for ( int i=0; i<3; ++i ) { AggregateItem aitem = (AggregateItem)at.addItem(); aitem.setInt("id", i); for ( int j=0; j<3; ++j ) { aitem.addItem((VisualItem)nodes.next()); } } } public static void main(String[] argv) { JFrame frame = demo(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } public static JFrame demo() { AggregateDemo ad = new AggregateDemo(); JFrame frame = new JFrame("p r e f u s e | a g g r e g a t e d"); frame.getContentPane().add(ad); frame.pack(); return frame; } } // end of class AggregateDemo /** * Layout algorithm that computes a convex hull surrounding * aggregate items and saves it in the "_polygon" field. */ class AggregateLayout extends Layout { private int m_margin = 5; // convex hull pixel margin private double[] m_pts; // buffer for computing convex hulls public AggregateLayout(String aggrGroup) { super(aggrGroup); } /** * @see edu.berkeley.guir.prefuse.action.Action#run(edu.berkeley.guir.prefuse.ItemRegistry, double) */ public void run(double frac) { AggregateTable aggr = (AggregateTable)m_vis.getGroup(m_group); // do we have any to process? int num = aggr.getTupleCount(); if ( num == 0 ) return; // update buffers int maxsz = 0; for ( Iterator aggrs = aggr.tuples(); aggrs.hasNext(); ) maxsz = Math.max(maxsz, 4*2* ((AggregateItem)aggrs.next()).getAggregateSize()); if ( m_pts == null || maxsz > m_pts.length ) { m_pts = new double[maxsz]; } // compute and assign convex hull for each aggregate Iterator aggrs = m_vis.visibleItems(m_group); while ( aggrs.hasNext() ) { AggregateItem aitem = (AggregateItem)aggrs.next(); int idx = 0; if ( aitem.getAggregateSize() == 0 ) continue; VisualItem item = null; Iterator iter = aitem.items(); while ( iter.hasNext() ) { item = (VisualItem)iter.next(); if ( item.isVisible() ) { addPoint(m_pts, idx, item, m_margin); idx += 2*4; } } // if no aggregates are visible, do nothing if ( idx == 0 ) continue; // compute convex hull double[] nhull = GraphicsLib.convexHull(m_pts, idx); // prepare viz attribute array float[] fhull = (float[])aitem.get(VisualItem.POLYGON); if ( fhull == null || fhull.length < nhull.length ) fhull = new float[nhull.length]; else if ( fhull.length > nhull.length ) fhull[nhull.length] = Float.NaN; // copy hull values for ( int j=0; j<nhull.length; j++ ) fhull[j] = (float)nhull[j]; aitem.set(VisualItem.POLYGON, fhull); aitem.setValidated(false); // force invalidation } } private static void addPoint(double[] pts, int idx, VisualItem item, int growth) { Rectangle2D b = item.getBounds(); double minX = (b.getMinX())-growth, minY = (b.getMinY())-growth; double maxX = (b.getMaxX())+growth, maxY = (b.getMaxY())+growth; pts[idx] = minX; pts[idx+1] = minY; pts[idx+2] = minX; pts[idx+3] = maxY; pts[idx+4] = maxX; pts[idx+5] = minY; pts[idx+6] = maxX; pts[idx+7] = maxY; } } // end of class AggregateLayout /** * Interactive drag control that is "aggregate-aware" */ class AggregateDragControl extends ControlAdapter { private VisualItem activeItem; protected Point2D down = new Point2D.Double(); protected Point2D temp = new Point2D.Double(); protected boolean dragged; /** * Creates a new drag control that issues repaint requests as an item * is dragged. */ public AggregateDragControl() { } /** * @see prefuse.controls.Control#itemEntered(prefuse.visual.VisualItem, java.awt.event.MouseEvent) */ public void itemEntered(VisualItem item, MouseEvent e) { Display d = (Display)e.getSource(); d.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); activeItem = item; if ( !(item instanceof AggregateItem) ) setFixed(item, true); } /** * @see prefuse.controls.Control#itemExited(prefuse.visual.VisualItem, java.awt.event.MouseEvent) */ public void itemExited(VisualItem item, MouseEvent e) { if ( activeItem == item ) { activeItem = null; setFixed(item, false); } Display d = (Display)e.getSource(); d.setCursor(Cursor.getDefaultCursor()); } /** * @see prefuse.controls.Control#itemPressed(prefuse.visual.VisualItem, java.awt.event.MouseEvent) */ public void itemPressed(VisualItem item, MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) return; dragged = false; Display d = (Display)e.getComponent(); d.getAbsoluteCoordinate(e.getPoint(), down); if ( item instanceof AggregateItem ) setFixed(item, true); } /** * @see prefuse.controls.Control#itemReleased(prefuse.visual.VisualItem, java.awt.event.MouseEvent) */ public void itemReleased(VisualItem item, MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) return; if ( dragged ) { activeItem = null; setFixed(item, false); dragged = false; } } /** * @see prefuse.controls.Control#itemDragged(prefuse.visual.VisualItem, java.awt.event.MouseEvent) */ public void itemDragged(VisualItem item, MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) return; dragged = true; Display d = (Display)e.getComponent(); d.getAbsoluteCoordinate(e.getPoint(), temp); double dx = temp.getX()-down.getX(); double dy = temp.getY()-down.getY(); move(item, dx, dy); down.setLocation(temp); } protected static void setFixed(VisualItem item, boolean fixed) { if ( item instanceof AggregateItem ) { Iterator items = ((AggregateItem)item).items(); while ( items.hasNext() ) { setFixed((VisualItem)items.next(), fixed); } } else { item.setFixed(fixed); } } protected static void move(VisualItem item, double dx, double dy) { if ( item instanceof AggregateItem ) { Iterator items = ((AggregateItem)item).items(); while ( items.hasNext() ) { move((VisualItem)items.next(), dx, dy); } } else { double x = item.getX(); double y = item.getY(); item.setStartX(x); item.setStartY(y); item.setX(x+dx); item.setY(y+dy); item.setEndX(x+dx); item.setEndY(y+dy); } } } // end of class AggregateDragControl