package prefuse.demos; import java.awt.Font; import java.awt.Shape; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import javax.swing.BorderFactory; import javax.swing.JFrame; import prefuse.Constants; import prefuse.Display; import prefuse.Visualization; import prefuse.action.Action; import prefuse.action.ActionList; import prefuse.action.RepaintAction; import prefuse.action.animate.ColorAnimator; import prefuse.action.animate.LocationAnimator; import prefuse.action.assignment.ColorAction; import prefuse.action.layout.AxisLayout; import prefuse.activity.Activity; import prefuse.activity.ActivityAdapter; import prefuse.activity.SlowInSlowOutPacer; import prefuse.data.Schema; import prefuse.data.Table; import prefuse.data.Tuple; import prefuse.data.event.TupleSetListener; import prefuse.data.expression.FunctionExpression; import prefuse.data.expression.FunctionTable; import prefuse.data.expression.Predicate; import prefuse.data.expression.parser.ExpressionParser; import prefuse.data.io.DataIOException; import prefuse.data.io.DelimitedTextTableReader; import prefuse.data.query.SearchQueryBinding; import prefuse.data.search.SearchTupleSet; import prefuse.data.tuple.TupleSet; import prefuse.render.DefaultRendererFactory; import prefuse.render.ShapeRenderer; import prefuse.render.LabelRenderer; import prefuse.util.ColorLib; import prefuse.util.FontLib; import prefuse.util.PrefuseLib; import prefuse.util.ui.JSearchPanel; import prefuse.visual.VisualItem; import prefuse.visual.VisualTable; /** * Re-implementation of Ben Fry's Zipdecode. Check out the * original at <a href="http://acg.media.mit.edu/people/fry/zipdecode/"> * http://acg.media.mit.edu/people/fry/zipdecode/</a>. * * This demo showcases creating new functions in the prefuse expression * language, creating derived columns, and provides an example of using * a dedicated focus set of items to support more efficient data handling. * * @author <a href="http://jheer.org">jeffrey heer</a> */ public class ZipDecode extends Display implements Constants { public static final String ZIPCODES = "/zipcode.txt"; public static final String STATES = "/state.txt"; // data groups private static final String DATA = "data"; private static final String LABELS = "labels"; private static final String FOCUS = Visualization.FOCUS_ITEMS; public static class StateLookupFunction extends FunctionExpression { private static Table s_states; static { try { s_states = new DelimitedTextTableReader().readTable(STATES); } catch ( Exception e ) { e.printStackTrace(); } } public StateLookupFunction() { super(1); } public String getName() { return "STATE"; } public Class getType(Schema s) { return String.class; } public Object get(Tuple t) { int code = s_states.index("code").get(param(0).getInt(t)); return s_states.getString(code, "alpha"); } } // add state function to the FunctionTable static { FunctionTable.addFunction("STATE", StateLookupFunction.class); } public ZipDecode(final Table t) { super(new Visualization()); // this predicate makes sure only the continental states are included Predicate filter = (Predicate)ExpressionParser.parse( "state >= 1 && state <= 56 && state != 2 && state != 15"); VisualTable vt = m_vis.addTable(DATA, t, filter, getDataSchema()); // zip codes are loaded in as integers, so lets create a derived // column that has correctly-formatted 5 digit strings vt.addColumn("zipstr", "LPAD(zip,5,'0')"); // now add a formatted label to show within the visualization vt.addColumn("label", "CONCAT(CAP(city),', ',STATE(state),' ',zipstr)"); // create a filter controlling label appearance Predicate loneResult = (Predicate)ExpressionParser.parse( "INGROUP('_search_') AND GROUPSIZE('_search_')=1 AND " + "LENGTH(QUERY('_search_'))=5"); // add a table of visible city,state,zip labels // this is a derived table, overriding only the fields that need to // have unique values and inheriting all other data values from the // data table. in particular, we want to inherit the x,y coordinates. m_vis.addDerivedTable(LABELS, DATA, loneResult, getLabelSchema()); // -- renderers ------------------------------------------------------- DefaultRendererFactory rf = new DefaultRendererFactory(); rf.setDefaultRenderer(new ShapeRenderer(1)); // 1 pixel rectangles rf.add("INGROUP('labels')", new LabelRenderer("label") { public Shape getShape(VisualItem item) { // set horizontal alignment based on x-coordinate position setHorizontalAlignment(item.getX()>getWidth()/2 ? RIGHT:LEFT); // now return shape as usual return super.getShape(item); } }); m_vis.setRendererFactory(rf); // -- actions --------------------------------------------------------- ActionList layout = new ActionList(); layout.add(new AxisLayout(DATA, "lat", Y_AXIS)); layout.add(new AxisLayout(DATA, "lon", X_AXIS)); m_vis.putAction("layout", layout); // the update list updates the colors of data points and sets the visual // properties for any labels. Color updating is limited only to the // current focus items, ensuring faster performance. final Action update = new ZipColorAction(FOCUS); m_vis.putAction("update", update); // animate a change in color in the interface. this animation is quite // short, only 200ms, so that it does not impede with interaction. // color animation of data points looks only at the focus items, // ensuring faster performance. ActionList animate = new ActionList(200); animate.add(new ColorAnimator(FOCUS, VisualItem.FILLCOLOR)); animate.add(new ColorAnimator(LABELS, VisualItem.TEXTCOLOR)); animate.add(new RepaintAction()); animate.addActivityListener(new ActivityAdapter() { public void activityCancelled(Activity a) { // if animation is canceled, set colors to final state update.run(1.0); } }); m_vis.putAction("animate", animate); // update items after a resize of the display, animating them to their // new locations. this animates all data points, so is noticeably slow. ActionList resize = new ActionList(1500); resize.setPacingFunction(new SlowInSlowOutPacer()); resize.add(new LocationAnimator(DATA)); resize.add(new LocationAnimator(LABELS)); resize.add(new RepaintAction()); m_vis.putAction("resize", resize); // -- display --------------------------------------------------------- setSize(720, 360); setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); setBackground(ColorLib.getGrayscale(50)); setFocusable(false); // -- search ---------------------------------------------------------- // zipcode text search is performed using a prefix based search, // provided by a search dynamic query. to make this application run // more efficiently, we optimize data handling by taking all search // results (both added and removed) and shuttling them into a // focus set. we work with this reduced set for updating and // animating color changes in the action definitions above. // create a final reference to the focus set, so that the following // search listener can access it. final TupleSet focus = m_vis.getFocusGroup(FOCUS); // create the search query binding SearchQueryBinding searchQ = new SearchQueryBinding(vt, "zipstr"); final SearchTupleSet search = searchQ.getSearchSet(); // create the listener that collects search results into a focus set search.addTupleSetListener(new TupleSetListener() { public void tupleSetChanged(TupleSet t, Tuple[] add, Tuple[] rem) { m_vis.cancel("animate"); // invalidate changed tuples, add them all to the focus set focus.clear(); for ( int i=0; i<add.length; ++i ) { // ((VisualItem)add[i]).setValidated(false); focus.addTuple(add[i]); } for ( int i=0; i<rem.length; ++i ) { //((VisualItem)rem[i]).setValidated(false); focus.addTuple(rem[i]); } m_vis.run("update"); m_vis.run("animate"); } }); m_vis.addFocusGroup(Visualization.SEARCH_ITEMS, search); // create and parameterize a search panel for searching on zip code final JSearchPanel searcher = searchQ.createSearchPanel(); searcher.setLabelText("zip>"); // the search box label searcher.setShowCancel(false); // don't show the cancel query button searcher.setShowBorder(false); // don't show the search box border searcher.setFont(FontLib.getFont("Georgia", Font.PLAIN, 22)); searcher.setBackground(ColorLib.getGrayscale(50)); searcher.setForeground(ColorLib.getColor(100,100,75)); add(searcher); // add the search box as a sub-component of the display searcher.setBounds(10, getHeight()-40, 120, 30); addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent e) { m_vis.run("layout"); m_vis.run("update"); m_vis.run("resize"); searcher.setBounds(10, getHeight()-40, 120, 30); invalidate(); } }); // -- launch ---------------------------------------------------------- m_vis.run("layout"); m_vis.run("animate"); } private static Schema getDataSchema() { Schema s = PrefuseLib.getVisualItemSchema(); s.setDefault(VisualItem.INTERACTIVE, false); s.setDefault(VisualItem.FILLCOLOR, ColorLib.rgb(100,100,75)); return s; } private static Schema getLabelSchema() { Schema s = PrefuseLib.getMinimalVisualSchema(); s.setDefault(VisualItem.INTERACTIVE, false); // default font is 16 point Georgia s.addInterpolatedColumn( VisualItem.FONT, Font.class, FontLib.getFont("Georgia",16)); // default fill color should be invisible s.addInterpolatedColumn(VisualItem.FILLCOLOR, int.class); s.setInterpolatedDefault(VisualItem.FILLCOLOR, 0); s.addInterpolatedColumn(VisualItem.TEXTCOLOR, int.class); // default text color is white s.setInterpolatedDefault(VisualItem.TEXTCOLOR, ColorLib.gray(255)); // default start text color is fully transparent s.setDefault(VisualItem.STARTTEXTCOLOR, ColorLib.gray(255,0)); return s; } // ------------------------------------------------------------------------ public static void main(String[] args) { String datafile = ZIPCODES; if ( args.length > 0 ) datafile = args[0]; try { JFrame frame = demo(datafile); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } catch ( Exception e ) { e.printStackTrace(); System.exit(1); } } public static JFrame demo() { try { return demo(ZIPCODES); } catch ( Exception e ) { return null; } } public static JFrame demo(String table) throws DataIOException { DelimitedTextTableReader tr = new DelimitedTextTableReader(); Table t = tr.readTable(table); ZipDecode zd = new ZipDecode(t); JFrame frame = new JFrame("p r e f u s e | z i p d e c o d e"); frame.getContentPane().add(zd); frame.pack(); return frame; } public static class ZipColorAction extends ColorAction { public ZipColorAction(String group) { super(group, VisualItem.FILLCOLOR); } public int getColor(VisualItem item) { System.out.println("getting color"); if ( item.isInGroup(Visualization.SEARCH_ITEMS) ) { return ColorLib.gray(255); } else { return ColorLib.rgb(100,100,75); } } } } // end of class ZipDecode