From e416acb9b012b17d1efe49ad2199ea7132d874d1 Mon Sep 17 00:00:00 2001
From: Normand Briere <nbriere@noware.ca>
Date: Sat, 07 Jul 2018 11:09:43 -0400
Subject: [PATCH] timeline

---
 timeflow/data/db/filter/ConstFilter.java          |   24 
 timeflow/app/ui/ColorLegendPanel.java             |   63 
 timeflow/vis/TimeScale.java                       |   95 
 timeflow/data/db/filter/OrFilter.java             |   38 
 timeflow/views/HtmlControls.java                  |   21 
 timeflow/vis/VisualActFactory.java                |  119 
 timeflow/app/actions/DeleteUnselectedAction.java  |   40 
 timeflow/format/field/FormatURL.java              |   41 
 timeflow/model/TFListener.java                    |    5 
 timeflow/app/ui/filter/BabyHistogram.java         |  274 +
 timeflow/views/AbstractVisualizationView.java     |  244 +
 timeflow/data/analysis/FieldAnalysis.java         |   13 
 timeflow/format/file/DelimitedText.java           |  214 
 timeflow/views/TableView.java                     |  267 +
 timeflow/app/actions/TimeflowAction.java          |   36 
 timeflow/app/actions/ImportFromPasteAction.java   |   53 
 timeflow/app/ui/filter/FilterCategoryPanel.java   |  164 
 timeflow/format/field/DateTimeGuesser.java        |  115 
 timeflow/app/ui/WaitingDialog.java                |   56 
 timeflow/data/analysis/DBAnalysis.java            |   14 
 timeflow/data/db/filter/StringMatchFilter.java    |   69 
 timeflow/app/ui/StatusPanel.java                  |   98 
 timeflow/views/ListView.java                      |  266 +
 timeflow/views/SummaryView.java                   |  181 
 timeflow/app/ui/filter/FilterNumberPanel.java     |  171 
 timeflow/app/actions/DeleteFieldAction.java       |   43 
 timeflow/data/analysis/MissingValueAnalysis.java  |   44 
 timeflow/format/file/Export.java                  |   10 
 timeflow/app/ui/filter/SearchPanel.java           |   52 
 timeflow/data/db/filter/NotFilter.java            |   24 
 timeflow/util/ColorUtils.java                     |   31 
 timeflow/util/TimeIt.java                         |   34 
 timeflow/data/time/RoughTime.java                 |  128 
 timeflow/vis/Mouseover.java                       |   94 
 timeflow/views/TimelineView.java                  |  465 +
 timeflow/vis/GroupVisualAct.java                  |   98 
 timeflow/format/field/FormatDouble.java           |  103 
 timeflow/app/actions/WebDocAction.java            |   25 
 timeflow/model/ModelPanel.java                    |   36 
 timeflow/data/analysis/RangeDateAnalysis.java     |   62 
 timeflow/vis/calendar/CalendarVisuals.java        |  113 
 timeflow/app/actions/NewDataAction.java           |   27 
 timeflow/format/file/FileExtensionCatalog.java    |   28 
 timeflow/vis/timeline/AxisRenderer.java           |   88 
 timeflow/app/actions/RenameFieldAction.java       |  100 
 timeflow/data/db/Field.java                       |   50 
 timeflow/vis/timeline/TimelineSlider.java         |  239 +
 timeflow/data/time/TimeUtils.java                 |   21 
 timeflow/util/IO.java                             |   54 
 timeflow/vis/TagVisualAct.java                    |   52 
 timeflow/format/field/FieldFormatGuesser.java     |   50 
 timeflow/views/CalendarView.java                  |  304 +
 timeflow/vis/timeline/TimelineRenderer.java       |  184 
 timeflow/app/actions/DateFieldAction.java         |   24 
 timeflow/views/AbstractView.java                  |   85 
 timeflow/app/ui/GlobalDisplayPanel.java           |  174 
 timeflow/app/ui/AddFieldPanel.java                |   24 
 timeflow/vis/VisualEncoder.java                   |  138 
 timeflow/app/ui/LinkTabPane.java                  |  208 
 timeflow/model/Display.java                       |  373 +
 timeflow/data/db/DBUtils.java                     |  234 
 timeflow/model/TFModel.java                       |  303 +
 timeflow/app/ui/ImportDelimitedPanel.java         |  310 +
 timeflow/app/actions/QuitAction.java              |   32 
 timeflow/data/db/BasicAct.java                    |   89 
 timeflow/data/time/Interval.java                  |  114 
 timeflow/format/field/FieldFormat.java            |   64 
 timeflow/util/Pad.java                            |   19 
 timeflow/app/ui/EditValuePanel.java               |  111 
 timeflow/data/db/filter/FieldValueSetFilter.java  |   40 
 timeflow/format/field/FieldFormatCatalog.java     |   54 
 timeflow/vis/calendar/Grid.java                   |  375 +
 timeflow/app/ui/MassDeletePanel.java              |   80 
 timeflow/app/ui/filter/FilterDefinitionPanel.java |   10 
 timeflow/vis/VisualAct.java                       |  314 +
 timeflow/data/analysis/FrequencyAnalysis.java     |   76 
 timeflow/app/TimeflowApp.java                     |  709 ++
 timeflow/model/VirtualField.java                  |   39 
 timeflow/data/db/filter/AndFilter.java            |   50 
 timeflow/app/actions/ReorderFieldsAction.java     |   38 
 timeflow/format/field/FormatString.java           |   44 
 timeflow/format/field/FormatStringArray.java      |   59 
 timeflow/app/ui/ComponentCluster.java             |   37 
 timeflow/app/actions/DeleteSelectedAction.java    |   51 
 timeflow/format/file/HtmlFormat.java              |  143 
 timeflow/format/file/Import.java                  |    9 
 timeflow/data/db/Schema.java                      |  130 
 timeflow/data/db/ActDB.java                       |   30 
 timeflow/data/time/TimeUnit.java                  |  243 +
 timeflow/format/file/TimeflowFormat.java          |  163 
 timeflow/app/ui/DottedLine.java                   |   34 
 timeflow/app/ui/SizeLegendPanel.java              |  113 
 timeflow/vis/timeline/AxisTicMarks.java           |  117 
 timeflow/data/db/filter/ActFilter.java            |   14 
 timeflow/data/analysis/RangeNumberAnalysis.java   |   85 
 timeflow/app/TimeflowAppLauncher.java             |   52 
 timeflow/data/db/filter/ValueFilter.java          |    5 
 timeflow/format/file/DelimitedFormat.java         |  217 
 timeflow/app/ui/ReorderFieldsPanel.java           |   68 
 timeflow/app/ui/DateFieldPanel.java               |  178 
 timeflow/views/BarGraphView.java                  |  363 +
 timeflow/app/ui/filter/FilterDatePanel.java       |  173 
 timeflow/data/db/BasicDB.java                     |  129 
 timeflow/app/AppState.java                        |  107 
 timeflow/vis/calendar/GridCell.java               |   20 
 timeflow/data/db/ArrayDB.java                     |  348 +
 timeflow/app/actions/AddRecordAction.java         |   26 
 timeflow/vis/timeline/TimelineVisuals.java        |  345 +
 timeflow/format/field/DateTimeParser.java         |   39 
 timeflow/data/db/filter/MissingValueFilter.java   |   27 
 timeflow/data/db/filter/TimeIntervalFilter.java   |   35 
 timeflow/model/TFEvent.java                       |   43 
 timeflow/data/db/filter/FieldValueFilter.java     |   37 
 timeflow/data/db/ActList.java                     |   23 
 timeflow/util/DoubleBag.java                      |  145 
 timeflow/app/actions/AddFieldAction.java          |   49 
 timeflow/app/AboutWindow.java                     |   53 
 timeflow/app/actions/CopySchemaAction.java        |   29 
 timeflow/data/db/filter/NumericRangeFilter.java   |   28 
 timeflow/vis/MouseoverLabel.java                  |   32 
 timeflow/views/IntroView.java                     |  105 
 timeflow/data/db/ActComparator.java               |  118 
 timeflow/views/DescriptionView.java               |   71 
 timeflow/util/Bag.java                            |  136 
 timeflow/app/ui/EditRecordPanel.java              |  134 
 timeflow/app/ui/HtmlDisplay.java                  |   24 
 timeflow/app/ui/filter/FilterControlPanel.java    |  208 
 timeflow/vis/timeline/TimelineTrack.java          |  129 
 timeflow/data/db/Act.java                         |   25 
 timeflow/app/actions/EditSourceAction.java        |   37 
 timeflow/format/field/FormatDateTime.java         |   79 
 timeflow/app/ui/filter/FilterTitle.java           |   52 
 132 files changed, 14,288 insertions(+), 0 deletions(-)

diff --git a/timeflow/app/AboutWindow.java b/timeflow/app/AboutWindow.java
new file mode 100755
index 0000000..d3bbb8b
--- /dev/null
+++ b/timeflow/app/AboutWindow.java
@@ -0,0 +1,53 @@
+package timeflow.app;
+
+import timeflow.model.*;
+
+import java.awt.*;
+import java.io.File;
+import java.util.*;
+import javax.imageio.*;
+
+public class AboutWindow extends Window {
+	
+	Image image;
+	Display display;
+	int w=640, h=380;
+	
+	public AboutWindow(Frame owner, Display display) {
+		super(owner);
+		this.display=display;
+		
+		try
+		{
+                    // image=ImageIO.read(getClass().getClassLoader().getResourceAsStream("images/splash.jpg"));
+			image=ImageIO.read(new File("images/splash.jpg"));
+			w=image.getWidth(null);
+			h=image.getHeight(null);
+		}
+		catch (Exception e)
+		{
+			e.printStackTrace(System.out);
+		}
+		Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
+		setBounds((size.width-w)/2, (size.height-h)/2, w,h);
+		
+	}
+	
+	public void paint(Graphics g)
+	{
+		if (image!=null)
+		{
+			g.drawImage(image,0,0,null);
+			return;
+		}
+		int lx=15;
+		((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+		g.setColor(display.getColor("splash.background"));
+		g.fillRect(0,0,w,h);
+		g.setFont(display.huge());
+		g.setColor(display.getColor("splash.text"));
+		g.drawString(Display.version(), lx, 35);
+		g.setFont(display.plain());
+		g.drawString("Prototype version",lx,60);
+	}
+}
diff --git a/timeflow/app/AppState.java b/timeflow/app/AppState.java
new file mode 100755
index 0000000..c1d7fd0
--- /dev/null
+++ b/timeflow/app/AppState.java
@@ -0,0 +1,107 @@
+package timeflow.app;
+
+import timeflow.util.*;
+
+import java.io.*;
+import java.util.*;
+
+public class AppState
+{
+
+        private static final String FILE = "settings/info.txt";
+        private File currentFile, currentDir;
+        private LinkedList<File> recentFiles = new LinkedList<File>();
+
+        public AppState()
+        {
+                if (!new File(FILE).exists())
+                {
+                        System.err.println("No existing settings file found.");
+                        return;
+                }
+                try
+                {
+                        for (String line : IO.lines(FILE))
+                        {
+                                String[] t = line.split("\t");
+                                String command = t[0];
+                                String arg = t[1];
+                                if ("CURRENT_FILE".equals(command))
+                                {
+                                        currentFile = new File(arg);
+                                } else if ("RECENT_FILE".equals(command))
+                                {
+                                        recentFiles.add(new File(arg).getAbsoluteFile());
+                                } else if ("CURRENT_DIR".equals(command))
+                                {
+                                        currentDir = new File(arg);
+                                }
+                        }
+                } catch (Exception e)
+                {
+                        e.printStackTrace(System.out);
+                }
+        }
+
+        public List<File> getRecentFiles()
+        {
+                return (List<File>) recentFiles.clone();
+        }
+
+        public File getCurrentFile()
+        {
+                return currentFile;
+        }
+
+        public void setCurrentFile(File currentFile)
+        {
+                this.currentFile = currentFile.getAbsoluteFile();
+
+                // if list is big, remove one at end.
+                if (recentFiles.size() > 10)
+                {
+                        recentFiles.removeLast();
+                }
+
+                // put at front of list
+                if (recentFiles.contains(this.currentFile))
+                {
+                        recentFiles.remove(this.currentFile);
+                }
+                recentFiles.addFirst(this.currentFile);
+
+                // set current dir, too.
+                this.currentDir = currentDir;
+        }
+
+        public File getCurrentDir()
+        {
+                return currentDir;
+        }
+
+        public void setCurrentDir(File currentDir)
+        {
+                this.currentDir = currentDir;
+        }
+
+        public void save()
+        {
+                try
+                {
+                        FileOutputStream fos = new FileOutputStream(FILE);
+                        PrintStream out = new PrintStream(fos);
+                        out.println("CURRENT_FILE\t" + currentFile);
+                        out.println("CURRENT_DIR\t" + currentDir);
+                        for (File f : recentFiles)
+                        {
+                                out.println("RECENT_FILE\t" + f);
+                        }
+                        out.flush();
+                        out.close();
+                        fos.close();
+                } catch (Exception e)
+                {
+                        e.printStackTrace(System.out);
+                }
+        }
+}
diff --git a/timeflow/app/TimeflowApp.java b/timeflow/app/TimeflowApp.java
new file mode 100755
index 0000000..b7b8442
--- /dev/null
+++ b/timeflow/app/TimeflowApp.java
@@ -0,0 +1,709 @@
+package timeflow.app;
+
+import timeflow.app.ui.*;
+import timeflow.app.actions.*;
+import timeflow.app.ui.filter.*;
+import timeflow.data.db.*;
+import timeflow.data.time.RoughTime;
+import timeflow.format.field.*;
+import timeflow.format.file.*;
+import timeflow.model.*;
+import timeflow.views.*;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import timeflow.util.Pad;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.io.*;
+import java.util.ArrayList;
+
+public class TimeflowApp extends JFrame
+{
+
+        public TFModel model = new TFModel();
+        public JFileChooser fileChooser;
+        AboutWindow splash;
+        String[][] examples;
+        String[] templates;
+        AppState state = new AppState();
+        JMenu openRecent = new JMenu("Open Recent");
+        public JMenu filterMenu;
+        JMenuItem save = new JMenuItem("Save");
+        FilterControlPanel filterControlPanel;
+        LinkTabPane leftPanel;
+        TFListener filterMenuMaker = new TFListener()
+        {
+
+                @Override
+                public void note(TFEvent e)
+                {
+                        if (e.affectsSchema())
+                        {
+                                filterMenu.removeAll();
+                                for (Field f : model.getDB().getFields())
+                                {
+                                        if (f.getType() == String.class || f.getType() == String[].class
+                                                || f.getType() == Double.class || f.getType() == RoughTime.class)
+                                        {
+                                                final JCheckBoxMenuItem item = new JCheckBoxMenuItem(f.getName());
+                                                final Field field = f;
+                                                filterMenu.add(item);
+                                                item.addActionListener(new ActionListener()
+                                                {
+
+                                                        @Override
+                                                        public void actionPerformed(ActionEvent e)
+                                                        {
+                                                                filterControlPanel.setFacet(field, item.getState());
+                                                                leftPanel.setSelectedIndex(1);
+                                                        }
+                                                });
+                                        }
+                                }
+                        }
+                }
+        };
+
+        void splash(boolean visible)
+        {
+                splash.setVisible(visible);
+        }
+
+        public void init() throws Exception
+        {
+                Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
+                setBounds(0, 0, Math.min(d.width, 1200), Math.min(d.height, 900));
+                setTitle(Display.version());
+                setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+                final QuitAction quitAction = new QuitAction(this, model);
+                addWindowListener(new WindowAdapter()
+                {
+
+                        @Override
+                        public void windowClosing(WindowEvent e)
+                        {
+                                quitAction.quit();
+                        }
+
+                        public void windowStateChanged(WindowEvent e)
+                        {
+                                repaint();
+                        }
+                });
+                Image icon = Toolkit.getDefaultToolkit().getImage("images/icon.gif");
+                setIconImage(icon);
+
+                // read example directory
+                String[] ex = getVisibleFiles("settings/examples");
+                int n = ex.length;
+                examples = new String[n][2];
+                for (int i = 0; i < n; i++)
+                {
+                        String s = ex[i];
+                        int dot = s.lastIndexOf('.');
+                        if (dot >= 0 && dot < s.length() - 1);
+                        s = s.substring(0, dot);
+                        examples[i][0] = s;
+                        examples[i][1] = "settings/examples/" + ex[i];
+                }
+                templates = getVisibleFiles("settings/templates");
+                fileChooser = new JFileChooser(state.getCurrentFile());
+
+                getContentPane().setLayout(new BorderLayout());
+
+                // left tab area, with vertical gray divider.
+                JPanel leftHolder = new JPanel();
+                getContentPane().add(leftHolder, BorderLayout.WEST);
+
+                leftHolder.setLayout(new BorderLayout());
+                JPanel pad = new Pad(3, 3);
+                pad.setBackground(Color.gray);
+                leftHolder.add(pad, BorderLayout.EAST);
+
+                leftPanel = new LinkTabPane();//JTabbedPane();
+                leftHolder.add(leftPanel, BorderLayout.CENTER);
+
+                JPanel configPanel = new JPanel();
+                configPanel.setLayout(new BorderLayout());
+                filterMenu = new JMenu("Filters");
+                filterControlPanel = new FilterControlPanel(model, filterMenu);
+                final GlobalDisplayPanel displayPanel = new GlobalDisplayPanel(model, filterControlPanel);
+                configPanel.add(displayPanel, BorderLayout.NORTH);
+
+                JPanel legend = new JPanel();
+                legend.setLayout(new BorderLayout());
+                configPanel.add(legend, BorderLayout.CENTER);
+                legend.add(new SizeLegendPanel(model), BorderLayout.NORTH);
+                legend.add(new ColorLegendPanel(model), BorderLayout.CENTER);
+                leftPanel.addTab(configPanel, "Display", true);
+
+                leftPanel.addTab(filterControlPanel, "Filter", true);
+
+                // center tab area
+
+                final LinkTabPane center = new LinkTabPane();
+                getContentPane().add(center, BorderLayout.CENTER);
+
+                center.addPropertyChangeListener(new PropertyChangeListener()
+                {
+
+                        @Override
+                        public void propertyChange(PropertyChangeEvent evt)
+                        {
+                                displayPanel.showLocalControl(center.getCurrentName());
+                        }
+                });
+
+                final IntroView intro = new IntroView(model); // we refer to this a bit later.
+                final TimelineView timeline = new TimelineView(model);
+                AbstractView[] views =
+                {
+                        timeline,
+                        new CalendarView(model),
+                        new ListView(model),
+                        new TableView(model),
+                        new BarGraphView(model),
+                        intro,
+                        new DescriptionView(model),
+                        new SummaryView(model),
+                };
+
+                for (int i = 0; i < views.length; i++)
+                {
+                        center.addTab(views[i], views[i].getName(), i < 5);
+                        displayPanel.addLocalControl(views[i].getName(), views[i].getControls());
+                }
+
+                // start off with intro screen
+                center.setCurrentName(intro.getName());
+                displayPanel.showLocalControl(intro.getName());
+
+                // but then, once data is loaded, switch directly to the timeline view.
+                model.addListener(new TFListener()
+                {
+
+                        @Override
+                        public void note(TFEvent e)
+                        {
+                                if (e.type == e.type.DATABASE_CHANGE)
+                                {
+                                        if (center.getCurrentName().equals(intro.getName()))
+                                        {
+                                                center.setCurrentName(timeline.getName());
+                                                displayPanel.showLocalControl(timeline.getName());
+                                        }
+                                }
+                        }
+                });
+
+                JMenuBar menubar = new JMenuBar();
+                setJMenuBar(menubar);
+
+                JMenu fileMenu = new JMenu("File");
+                menubar.add(fileMenu);
+
+                fileMenu.add(new NewDataAction(this));
+                fileMenu.add(new CopySchemaAction(this));
+
+                JMenu templateMenu = new JMenu("New From Template");
+                fileMenu.add(templateMenu);
+                for (int i = 0; i < templates.length; i++)
+                {
+                        JMenuItem t = new JMenuItem(templates[i]);
+                        final String fileName = "settings/templates/" + templates[i];
+                        templateMenu.add(t);
+                        t.addActionListener(new ActionListener()
+                        {
+
+                                @Override
+                                public void actionPerformed(ActionEvent e)
+                                {
+                                        load(fileName, FileExtensionCatalog.get(fileName), true);
+                                }
+                        });
+                }
+
+                fileMenu.addSeparator();
+
+
+                JMenuItem open = new JMenuItem("Open...");
+                fileMenu.add(open);
+                open.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                load(new TimeflowFormat(), false);
+                        }
+                });
+
+
+                fileMenu.add(openRecent);
+                makeRecentFileMenu();
+                fileMenu.addSeparator();
+                fileMenu.add(new ImportFromPasteAction(this));
+
+                JMenuItem impDel = new JMenuItem("Import CSV/TSV...");
+                fileMenu.add(impDel);
+                impDel.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                if (checkSaveStatus())
+                                {
+                                        importDelimited();
+                                }
+                        }
+                });
+
+                fileMenu.addSeparator();
+
+                fileMenu.add(save);
+                save.setAccelerator(KeyStroke.getKeyStroke('S',
+                        Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+                save.setEnabled(false);
+                save.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                save(model.getDbFile());
+
+                        }
+                });
+                model.addListener(new TFListener()
+                {
+
+                        @Override
+                        public void note(TFEvent e)
+                        {
+                                save.setEnabled(!model.getReadOnly());
+                        }
+                });
+
+                JMenuItem saveAs = new JMenuItem("Save As...");
+                fileMenu.add(saveAs);
+                saveAs.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                saveAs();
+                        }
+                });
+
+                fileMenu.addSeparator();
+
+                JMenuItem exportTSV = new JMenuItem("Export TSV...");
+                fileMenu.add(exportTSV);
+                exportTSV.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                exportDelimited('\t');
+                        }
+                });
+                JMenuItem exportCSV = new JMenuItem("Export CSV...");
+                fileMenu.add(exportCSV);
+                exportCSV.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                exportDelimited(',');
+                        }
+                });
+                JMenuItem exportHTML = new JMenuItem("Export HTML...");
+                fileMenu.add(exportHTML);
+                exportHTML.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                exportHtml();
+                        }
+                });
+                fileMenu.addSeparator();
+                fileMenu.add(quitAction);
+
+                JMenu editMenu = new JMenu("Edit");
+                menubar.add(editMenu);
+                editMenu.add(new AddRecordAction(this));
+                editMenu.addSeparator();
+                editMenu.add(new DateFieldAction(this));
+                editMenu.add(new AddFieldAction(this));
+                editMenu.add(new RenameFieldAction(this));
+                editMenu.add(new DeleteFieldAction(this));
+                editMenu.add(new ReorderFieldsAction(this));
+                editMenu.addSeparator();
+                editMenu.add(new EditSourceAction(this));
+                editMenu.addSeparator();
+                editMenu.add(new DeleteSelectedAction(this));
+                editMenu.add(new DeleteUnselectedAction(this));
+
+                menubar.add(filterMenu);
+                model.addListener(filterMenuMaker);
+
+
+                JMenu exampleMenu = new JMenu("Examples");
+                menubar.add(exampleMenu);
+
+                for (int i = 0; i < examples.length; i++)
+                {
+                        JMenuItem example = new JMenuItem(examples[i][0]);
+                        exampleMenu.add(example);
+                        final String file = examples[i][1];
+                        example.addActionListener(new ActionListener()
+                        {
+
+                                @Override
+                                public void actionPerformed(ActionEvent e)
+                                {
+                                        load(file, FileExtensionCatalog.get(file), true);
+                                }
+                        });
+                }
+
+                JMenu helpMenu = new JMenu("Help");
+                menubar.add(helpMenu);
+
+                helpMenu.add(new WebDocAction(this));
+
+                JMenuItem about = new JMenuItem("About TimeFlow");
+                helpMenu.add(about);
+                about.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                splash(true);
+                        }
+                });
+
+                model.addListener(new TFListener()
+                {
+
+                        @Override
+                        public void note(TFEvent e)
+                        {
+                                if (e.type == TFEvent.Type.DATABASE_CHANGE)
+                                {
+                                        String name = model.getDbFile();
+                                        int n = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
+                                        if (n > 0)
+                                        {
+                                                name = name.substring(n + 1);
+                                        }
+                                        setTitle(name);
+                                }
+                        }
+                });
+        }
+
+        void makeRecentFileMenu()
+        {
+                openRecent.removeAll();
+                try
+                {
+                        for (File f : state.getRecentFiles())
+                        {
+                                final String file = f.getAbsolutePath();
+                                JMenuItem m = new JMenuItem(f.getName());
+                                openRecent.add(m);
+                                m.addActionListener(new ActionListener()
+                                {
+
+                                        @Override
+                                        public void actionPerformed(ActionEvent e)
+                                        {
+                                                load(file, FileExtensionCatalog.get(file), false);
+                                        }
+                                });
+                        }
+                } catch (Exception e)
+                {
+                        e.printStackTrace(System.out);
+                }
+        }
+
+        void exportHtml()
+        {
+                int retval = fileChooser.showSaveDialog(this);
+                if (retval == fileChooser.APPROVE_OPTION)
+                {
+                        String fileName = fileChooser.getSelectedFile().getAbsolutePath();
+                        try
+                        {
+                                FileWriter fw = new FileWriter(fileName);
+                                BufferedWriter out = new BufferedWriter(fw);
+                                new HtmlFormat().export(model, out);
+                                out.close();
+                                fw.close();
+                        } catch (Exception e)
+                        {
+                                System.out.println(e);
+                                showUserError("Couldn't save file: " + e);
+                        }
+                }
+        }
+
+        void exportDelimited(char delimiter)
+        {
+                int retval = fileChooser.showSaveDialog(this);
+                if (retval == fileChooser.APPROVE_OPTION)
+                {
+                        String fileName = fileChooser.getSelectedFile().getAbsolutePath();
+                        try
+                        {
+                                new DelimitedFormat(delimiter).write(model.getDB(), new File(fileName));
+                        } catch (Exception e)
+                        {
+                                System.out.println(e);
+                                showUserError("Couldn't save file: " + e);
+                        }
+                }
+        }
+
+        void load(Import importer, boolean readOnly)
+        {
+                if (!checkSaveStatus())
+                {
+                        return;
+                }
+                try
+                {
+                        int retval = fileChooser.showOpenDialog(this);
+                        if (retval == fileChooser.APPROVE_OPTION)
+                        {
+                                load(fileChooser.getSelectedFile().getAbsolutePath(), importer, readOnly);
+                                noteFileUse(fileChooser.getSelectedFile().getAbsolutePath());
+                        }
+                } catch (Exception e)
+                {
+                        showUserError("Couldn't read file.");
+                        System.out.println(e);
+                }
+        }
+
+        public void showImportEditor(String fileName, String[][] data)
+        {
+                final ImportDelimitedPanel editor = new ImportDelimitedPanel(model);
+                editor.setFileName(fileName);
+                editor.setData(data);
+                editor.setBounds(0, 0, 1024, 768);
+                editor.setVisible(true);
+                SwingUtilities.invokeLater(new Runnable()
+                {
+
+                        public void run()
+                        {
+                                editor.scrollToTop();
+                        }
+                });
+
+        }
+
+        void importDelimited()
+        {
+                if (!checkSaveStatus())
+                {
+                        return;
+                }
+                try
+                {
+                        int result = fileChooser.showOpenDialog(this);
+
+                        if (result == JFileChooser.APPROVE_OPTION)
+                        {
+                                File file = fileChooser.getSelectedFile();
+                                String fileName = file.getAbsolutePath();
+                                noteFileUse(fileName);
+                                String[][] data = DelimitedFormat.readArrayGuessDelim(fileName, System.out);
+                                showImportEditor(fileName, data);
+
+                        } else
+                        {
+                                System.out.println("OK, canceling import.");
+                        }
+                } catch (Exception e)
+                {
+                        showUserError("Couldn't read file format.");
+                        e.printStackTrace(System.out);
+                }
+        }
+
+        void load(final String fileName, final Import importer, boolean readOnly)
+        {
+                if (!checkSaveStatus())
+                {
+                        return;
+                }
+                try
+                {
+                        final File f = new File(fileName);
+                        ActDB db = importer.importFile(f);
+                        model.setDB(db, fileName, readOnly, TimeflowApp.this);
+                        if (!readOnly)
+                        {
+                                noteFileUse(fileName);
+                        }
+                } catch (Exception e)
+                {
+                        e.printStackTrace(System.out);
+                        showUserError("Couldn't read file.");
+                        model.noteError(this);
+                }
+        }
+
+        public boolean save(String fileName)
+        {
+                try
+                {
+                        FileWriter fw = new FileWriter(fileName);
+                        BufferedWriter out = new BufferedWriter(fw);
+                        new TimeflowFormat().export(model, out);
+                        out.close();
+                        fw.close();
+                        noteFileUse(fileName);
+                        if (!fileName.equals(model.getDbFile()))
+                        {
+                                model.setDbFile(fileName, false, this);
+                        }
+                        model.setChangedSinceSave(false);
+                        model.setReadOnly(false);
+                        save.setEnabled(true);
+                        return true;
+                } catch (Exception e)
+                {
+                        e.printStackTrace(System.out);
+                        showUserError("Couldn't save file: " + e);
+                        return false;
+                }
+        }
+
+        public boolean checkSaveStatus()
+        {
+                boolean needSave = model.isChangedSinceSave();
+                if (!needSave)
+                {
+                        return true;
+                }
+
+                Object[] options = null;
+                if (model.isReadOnly())
+                {
+                        options = new Object[]
+                        {
+                                "Save As", "Discard Changes", "Cancel"
+                        };
+                } else
+                {
+                        options = new Object[]
+                        {
+                                "Save", "Save As", "Discard Changes", "Cancel"
+                        };
+                }
+                int n = JOptionPane.showOptionDialog(
+                        this,
+                        "The current data set has unsaved changes that will be lost.\n"
+                        + "Would you like to save them before continuing?",
+                        "Save Before Closing?",
+                        JOptionPane.YES_NO_OPTION,
+                        JOptionPane.QUESTION_MESSAGE,
+                        null,
+                        options,
+                        model.isReadOnly() ? "Save As" : "Save");
+                Object result = options[n];
+                if ("Discard Changes".equals(result))
+                {
+                        return true;
+                }
+                if ("Cancel".equals(result))
+                {
+                        return false;
+                }
+                if ("Save".equals(result))
+                {
+                        return save(model.getDbFile());
+                }
+
+                // we are now at "save as..."
+                return saveAs();
+        }
+
+        public boolean saveAs()
+        {
+                File current = fileChooser.getSelectedFile();
+                if (current != null)
+                {
+                        fileChooser.setSelectedFile(new File(current.getAbsolutePath() + " (copy)"));
+                }
+                int retval = fileChooser.showSaveDialog(this);
+                if (retval == fileChooser.APPROVE_OPTION)
+                {
+                        String fileName = fileChooser.getSelectedFile().getAbsolutePath();
+                        model.setReadOnly(false);
+                        save.setEnabled(true);
+                        return save(fileName);
+                } else
+                {
+                        return false;
+                }
+        }
+
+        public void showUserError(Object o)
+        {
+                JOptionPane.showMessageDialog(this,
+                        o,
+                        "A problem occurred",
+                        JOptionPane.ERROR_MESSAGE);
+                if (o instanceof Exception)
+                {
+                        ((Exception) o).printStackTrace(System.out);
+                }
+        }
+
+        public void noteFileUse(String file)
+        {
+
+                state.setCurrentFile(new File(file));
+                state.save();
+                makeRecentFileMenu();
+
+        }
+
+        public void clearFilters()
+        {
+                filterControlPanel.clearFilters();
+        }
+
+        static String[] getVisibleFiles(String dir)
+        {
+                String[] s = new File(dir).list();
+                ArrayList<String> real = new ArrayList<String>();
+                for (int i = 0; i < s.length; i++)
+                {
+                        if (!s[i].startsWith("."))
+                        {
+                                real.add(s[i]);
+                        }
+                }
+                return (String[]) real.toArray(new String[0]);
+        }
+}
diff --git a/timeflow/app/TimeflowAppLauncher.java b/timeflow/app/TimeflowAppLauncher.java
new file mode 100755
index 0000000..f034666
--- /dev/null
+++ b/timeflow/app/TimeflowAppLauncher.java
@@ -0,0 +1,52 @@
+package timeflow.app;
+
+import timeflow.model.*;
+
+import javax.swing.*;
+import java.awt.event.*;
+
+// For some reason we have to do this in a separate class in order to
+// get the menubar working right on the Mac.
+
+public class TimeflowAppLauncher {
+	public static void main(String[] args) throws Exception
+	{
+		System.setProperty("apple.laf.useScreenMenuBar", "true");
+		System.setProperty("com.apple.mrj.application.apple.menu.about.name", "TimeFlow");
+		System.out.println("Running "+Display.version());
+		
+		try {
+	        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+	    } 
+	    catch (Exception e) {
+	       System.out.println("Can't set system look & feel");
+	    }		
+		
+		final TimeflowApp t=new TimeflowApp();
+		t.splash=new AboutWindow(t, t.model.getDisplay());
+		t.splash(true);
+		SwingUtilities.invokeLater(new Runnable() {
+			@Override
+			public void run() {
+				try
+				{
+					t.init();
+					t.setVisible(true);				
+				}
+				catch (Exception e)
+				{
+					e.printStackTrace(System.out);
+				}
+				t.splash.addMouseListener(new MouseAdapter() {
+
+					@Override
+					public void mouseClicked(MouseEvent e) {
+						t.splash.setVisible(false);
+					}}
+				);
+				t.splash(false);
+				//t.splash.message=t.model.getDisplay().version();
+			}});
+		
+	}
+}
diff --git a/timeflow/app/actions/AddFieldAction.java b/timeflow/app/actions/AddFieldAction.java
new file mode 100755
index 0000000..5af658e
--- /dev/null
+++ b/timeflow/app/actions/AddFieldAction.java
@@ -0,0 +1,49 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.TimeflowApp;
+import timeflow.app.ui.*;
+import timeflow.data.db.*;
+import timeflow.format.field.FieldFormatCatalog;
+
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.*;
+
+public class AddFieldAction extends TimeflowAction {
+
+	public AddFieldAction(TimeflowApp app)
+	{
+		super(app, "Add Field...", null, "Add a field to this database");
+	}
+	
+	@Override
+	public void actionPerformed(ActionEvent e) {
+		AddFieldPanel p=new AddFieldPanel();
+		Object[] options = {"Cancel", "Add Field"};
+		int n = JOptionPane.showOptionDialog(app,
+				p,
+				"Add New Field To Database",
+				JOptionPane.YES_NO_CANCEL_OPTION,
+				JOptionPane.QUESTION_MESSAGE,
+				null,
+				options,
+				"Add Field");
+		if (n==1)
+		{
+			String fieldName=p.name.getText();
+			TFModel model=getModel();
+			if (fieldName.trim().length()==0)
+				app.showUserError("Field names can't be all spaces!");
+			else if (model.getDB().getField(fieldName)!=null)
+				app.showUserError("That name is already taken!");
+			else
+			{
+				model.getDB().addField(fieldName, FieldFormatCatalog.javaClass((String)p.typeChoices.getSelectedItem()));
+				model.noteAddField(this);
+			}
+		}
+		else
+			System.out.println("Canceled!");
+	}
+}
diff --git a/timeflow/app/actions/AddRecordAction.java b/timeflow/app/actions/AddRecordAction.java
new file mode 100755
index 0000000..c295e6b
--- /dev/null
+++ b/timeflow/app/actions/AddRecordAction.java
@@ -0,0 +1,26 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.TimeflowApp;
+import timeflow.app.ui.*;
+import timeflow.data.db.*;
+import timeflow.format.field.FieldFormatCatalog;
+
+import java.awt.Toolkit;
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.*;
+
+public class AddRecordAction extends TimeflowAction {
+
+	public AddRecordAction(TimeflowApp app)
+	{
+		super(app, "Add Record...", null, "Add a record to this database");
+		accelerate('A');
+	}
+	
+	@Override
+	public void actionPerformed(ActionEvent e) {
+		EditRecordPanel.add(getModel());
+	}
+}
diff --git a/timeflow/app/actions/CopySchemaAction.java b/timeflow/app/actions/CopySchemaAction.java
new file mode 100755
index 0000000..30023b7
--- /dev/null
+++ b/timeflow/app/actions/CopySchemaAction.java
@@ -0,0 +1,29 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.TimeflowApp;
+import timeflow.app.ui.*;
+import timeflow.data.db.*;
+import timeflow.format.field.FieldFormatCatalog;
+
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.*;
+
+public class CopySchemaAction extends TimeflowAction {
+
+	public CopySchemaAction(TimeflowApp app)
+	{
+		super(app, "New With Same Fields", null, 
+				"Create a new, blank database with same fields as the current one.");
+	}
+	
+	public void actionPerformed(ActionEvent e) 
+	{
+		java.util.List<Field> fields=getModel().getDB().getFields();
+		ActDB db=new BasicDB("Unspecified");
+		for (Field f: fields)
+			db.addField(f.getName(), f.getType());
+		getModel().setDB(db, "[new data]", true, this);
+	}
+}
diff --git a/timeflow/app/actions/DateFieldAction.java b/timeflow/app/actions/DateFieldAction.java
new file mode 100755
index 0000000..b7e8496
--- /dev/null
+++ b/timeflow/app/actions/DateFieldAction.java
@@ -0,0 +1,24 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.TimeflowApp;
+import timeflow.app.ui.*;
+import timeflow.data.db.*;
+import timeflow.format.field.FieldFormatCatalog;
+
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.*;
+
+public class DateFieldAction extends TimeflowAction {
+
+	public DateFieldAction(TimeflowApp app)
+	{
+		super(app, "Set Date Fields...", null, "Set date fields corresponding to start, end.");
+	}
+	
+	@Override
+	public void actionPerformed(ActionEvent e) {
+		DateFieldPanel.popWindow(app.model);
+	}
+}
diff --git a/timeflow/app/actions/DeleteFieldAction.java b/timeflow/app/actions/DeleteFieldAction.java
new file mode 100755
index 0000000..c7e60cc
--- /dev/null
+++ b/timeflow/app/actions/DeleteFieldAction.java
@@ -0,0 +1,43 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.TimeflowApp;
+import timeflow.app.ui.*;
+import timeflow.data.db.*;
+
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.*;
+
+public class DeleteFieldAction extends TimeflowAction {
+
+	public DeleteFieldAction(TimeflowApp app)
+	{
+		super(app, "Delete Field...", null, "Delete a field from this database");
+	}
+	
+	@Override
+	public void actionPerformed(ActionEvent e) {
+		ArrayList<String> options=new ArrayList<String>();
+		for (Field f: getModel().getDB().getFields())
+			options.add(f.getName());
+		String[] o=(String[])options.toArray(new String[0]);
+		String fieldToDelete = (String)JOptionPane.showInputDialog(
+		                    app,
+		                    "Field to delete:",
+		                    "Delete Field",
+		                    JOptionPane.PLAIN_MESSAGE,
+		                    null,
+		                    o,
+		                    o[0]);
+
+		if (fieldToDelete!=null)
+		{
+			TFModel model=getModel();
+			Field f=model.getDB().getField(fieldToDelete);
+			model.getDB().deleteField(f);			
+			model.noteSchemaChange(this);
+			return;
+		}	
+	}
+}
diff --git a/timeflow/app/actions/DeleteSelectedAction.java b/timeflow/app/actions/DeleteSelectedAction.java
new file mode 100755
index 0000000..824ac9b
--- /dev/null
+++ b/timeflow/app/actions/DeleteSelectedAction.java
@@ -0,0 +1,51 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.ui.*;
+import timeflow.app.*;
+import timeflow.data.db.*;
+
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.*;
+
+
+public class DeleteSelectedAction extends TimeflowAction {
+
+	public DeleteSelectedAction(TimeflowApp app)
+	{
+		super(app, "Delete Selected Items...", null, "Delete the currently visible events");
+	}
+	
+	@Override
+	public void actionPerformed(ActionEvent e) {
+		
+		HashSet<Act> keepers=new HashSet<Act>(); // switching between sets and lists
+		keepers.addAll(getModel().getDB().all()); // for efficiency. maybe silly?
+		ActList selected=getModel().getActs();
+		for (Act a: selected)
+			keepers.remove(a);
+		ActList keepList=new ActList(getModel().getDB());
+		keepList.addAll(keepers);
+		
+		MassDeletePanel panel=new MassDeletePanel(getModel(), keepList, 
+				"Delete all selected items.");
+		Object[] options = {"Cancel", "Proceed"};
+		int n = JOptionPane.showOptionDialog(app,
+					panel,
+					"Delete Selected",
+					JOptionPane.YES_NO_CANCEL_OPTION,
+					JOptionPane.PLAIN_MESSAGE,
+					null,
+					options,
+					"Proceed");
+		panel.detachFromModel();
+		if (n==1)
+		{
+			panel.applyAction();
+			app.clearFilters();
+			getModel().noteSchemaChange(this);
+		}
+	}
+
+}
diff --git a/timeflow/app/actions/DeleteUnselectedAction.java b/timeflow/app/actions/DeleteUnselectedAction.java
new file mode 100755
index 0000000..638f3f6
--- /dev/null
+++ b/timeflow/app/actions/DeleteUnselectedAction.java
@@ -0,0 +1,40 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.ui.*;
+import timeflow.app.*;
+
+import java.awt.event.*;
+import javax.swing.*;
+
+
+public class DeleteUnselectedAction extends TimeflowAction {
+
+	public DeleteUnselectedAction(TimeflowApp app)
+	{
+		super(app, "Delete Unselected Items...", null, "Delete all but the currently visible events");
+	}
+	
+	@Override
+	public void actionPerformed(ActionEvent e) {
+		MassDeletePanel panel=new MassDeletePanel(getModel(), getModel().getActs(), 
+				"Delete unselected items.");
+		Object[] options = {"Cancel", "Proceed"};
+		int n = JOptionPane.showOptionDialog(app,
+					panel,
+					"Delete Unselected",
+					JOptionPane.YES_NO_CANCEL_OPTION,
+					JOptionPane.PLAIN_MESSAGE,
+					null,
+					options,
+					"Proceed");
+		panel.detachFromModel();
+		if (n==1)
+		{
+			panel.applyAction();
+			app.clearFilters();
+			getModel().noteSchemaChange(this);
+		}
+	}
+
+}
diff --git a/timeflow/app/actions/EditSourceAction.java b/timeflow/app/actions/EditSourceAction.java
new file mode 100755
index 0000000..eec0280
--- /dev/null
+++ b/timeflow/app/actions/EditSourceAction.java
@@ -0,0 +1,37 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.TimeflowApp;
+import timeflow.app.ui.*;
+
+import java.awt.event.*;
+import javax.swing.*;
+
+
+public class EditSourceAction extends TimeflowAction {
+
+	public EditSourceAction(TimeflowApp app)
+	{
+		super(app, "Edit Source/Credit Line...", null, "Edit credit line for this database");
+	}
+	
+	@Override
+	public void actionPerformed(ActionEvent e) {
+		TFModel model=getModel();
+		String source = (String)JOptionPane.showInputDialog(
+                app,
+                null,
+                "Edit Source/Credit Line",
+                JOptionPane.PLAIN_MESSAGE,
+                null,
+                null,
+                model.getDB().getSource());
+
+		if (source!=null) {
+		model.getDB().setSource(source);
+		model.noteNewSource(this);
+		return;
+		}
+	}
+
+}
diff --git a/timeflow/app/actions/ImportFromPasteAction.java b/timeflow/app/actions/ImportFromPasteAction.java
new file mode 100755
index 0000000..9b6d249
--- /dev/null
+++ b/timeflow/app/actions/ImportFromPasteAction.java
@@ -0,0 +1,53 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.TimeflowApp;
+import timeflow.app.ui.*;
+import timeflow.data.db.*;
+import timeflow.format.field.FieldFormatCatalog;
+import timeflow.format.file.DelimitedFormat;
+
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.*;
+
+public class ImportFromPasteAction extends TimeflowAction {
+
+	public ImportFromPasteAction(TimeflowApp app)
+	{
+		super(app, "Paste From Spreadsheet / HTML...", null, "Import from copy-and-pasted data.");
+	}
+	
+	public void actionPerformed(ActionEvent event) 
+	{
+		if (!app.checkSaveStatus())
+			return;
+		JTextArea text=new JTextArea(10,40);
+		JScrollPane scroll=new JScrollPane(text);
+		text.setText("Paste here! (replacing this :-)");
+		text.setSelectionStart(0);
+		text.setSelectionEnd(text.getText().length());
+		Object[] options = {"Cancel", "Import"};
+		int n = JOptionPane.showOptionDialog(app,
+					scroll,
+					"Import From Paste",
+					JOptionPane.YES_NO_CANCEL_OPTION,
+					JOptionPane.QUESTION_MESSAGE,
+					null,
+					options,
+					"Import");
+		if (n==1)
+		{
+			try
+			{
+				String pasted=text.getText();
+				String[][] data=DelimitedFormat.readArrayFromString(pasted, System.out);
+				app.showImportEditor("Paste", data);
+			}
+			catch (Exception e)
+			{
+				app.showUserError(e);
+			}
+		}
+	}
+}
diff --git a/timeflow/app/actions/NewDataAction.java b/timeflow/app/actions/NewDataAction.java
new file mode 100755
index 0000000..357fb60
--- /dev/null
+++ b/timeflow/app/actions/NewDataAction.java
@@ -0,0 +1,27 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.TimeflowApp;
+import timeflow.app.ui.*;
+import timeflow.data.db.*;
+import timeflow.format.field.FieldFormatCatalog;
+
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.*;
+
+public class NewDataAction extends TimeflowAction {
+
+	public NewDataAction(TimeflowApp app)
+	{
+		super(app, "New", null, "Create a new, blank database");
+		accelerate('N');
+
+	}
+	
+	public void actionPerformed(ActionEvent e) 
+	{
+		if (app.checkSaveStatus())
+			getModel().setDB(new BasicDB("Unspecified"), "[new data]", true, this);
+	}
+}
diff --git a/timeflow/app/actions/QuitAction.java b/timeflow/app/actions/QuitAction.java
new file mode 100755
index 0000000..bdd6726
--- /dev/null
+++ b/timeflow/app/actions/QuitAction.java
@@ -0,0 +1,32 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.TimeflowApp;
+import timeflow.app.ui.*;
+import timeflow.data.db.*;
+import timeflow.format.field.FieldFormatCatalog;
+
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.*;
+
+public class QuitAction extends TimeflowAction {
+
+	public QuitAction(TimeflowApp app, TFModel model)
+	{
+		super(app, "Quit", null, "Quit the program");
+	}
+	
+	@Override
+	public void actionPerformed(ActionEvent e) {
+		quit();
+	}
+	
+	public void quit()
+	{
+		if (app.checkSaveStatus())
+		{
+			System.exit(0);
+		}
+	}
+}
diff --git a/timeflow/app/actions/RenameFieldAction.java b/timeflow/app/actions/RenameFieldAction.java
new file mode 100755
index 0000000..1fcc073
--- /dev/null
+++ b/timeflow/app/actions/RenameFieldAction.java
@@ -0,0 +1,100 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.ui.*;
+import timeflow.app.*;
+import timeflow.data.db.*;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+import java.util.*;
+
+public class RenameFieldAction extends TimeflowAction {
+
+	public RenameFieldAction(TimeflowApp app)
+	{
+		super(app, "Rename Field...", null, "Rename a field from this database");
+	}
+	
+	@Override
+	public void actionPerformed(ActionEvent e) {
+		JPanel panel=new JPanel();
+		panel.setLayout(new GridLayout(4,1));
+		panel.add(new JLabel("Choose a field and type a new name."));
+		final JComboBox fieldChoices=new JComboBox();
+		panel.add(fieldChoices);
+		ArrayList<String> options=new ArrayList<String>();
+		for (Field f: getModel().getDB().getFields())
+			fieldChoices.addItem(f.getName());
+		JPanel inputPanel=new JPanel();
+		inputPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
+		inputPanel.add(new JLabel("New Name:"));
+		final JTextField nameField=new JTextField(20);
+		inputPanel.add(nameField);
+		nameField.requestFocus();
+		final JLabel feedback=new JLabel("(No name entered)");
+
+		nameField.addKeyListener(new KeyListener() {
+			@Override
+			public void keyPressed(KeyEvent e) {
+				// TODO Auto-generated method stub
+				
+			}
+
+			@Override
+			public void keyReleased(KeyEvent e) {
+				String name=nameField.getText();
+				Field other=getModel().getDB().getField(name);
+				//System.out.println("name="+name);
+				if (name.trim().length()==0)
+				{
+					feedback.setText("(No name entered)");
+				} else if (other!=null && !other.getName().equals(fieldChoices.getSelectedItem()))
+				{
+					feedback.setText("A field named '"+name+"' already exists.");
+				} else
+					feedback.setText("");
+			}
+
+			@Override
+			public void keyTyped(KeyEvent e) {
+				// TODO Auto-generated method stub
+				
+			}});
+		
+		panel.add(inputPanel);
+		feedback.setForeground(Color.gray);
+		panel.add(feedback);
+		
+		String[] o={"OK", "Cancel"};
+		int n = JOptionPane.showOptionDialog(
+				app,
+				panel,
+				"Rename Field",
+				JOptionPane.YES_NO_CANCEL_OPTION,
+				JOptionPane.PLAIN_MESSAGE,
+				null,
+				o,
+				o[0]);
+
+		if (n==0)
+		{
+			Field old=getModel().getDB().getField((String)fieldChoices.getSelectedItem());
+			String newName=nameField.getText();
+			Field conflict=getModel().getDB().getField(newName);
+			boolean tooSpacey=newName.trim().length()==0;
+			if (tooSpacey)
+				app.showUserError("Can't change the field name to be empty.");
+			else if (conflict!=null && conflict!=old)
+				app.showUserError("A field named '"+newName+"' already exists.");
+			else
+			{
+				getModel().getDB().renameField(old, nameField.getText());
+				getModel().noteSchemaChange(this);
+			}
+		}
+	}
+}
diff --git a/timeflow/app/actions/ReorderFieldsAction.java b/timeflow/app/actions/ReorderFieldsAction.java
new file mode 100755
index 0000000..e291a2a
--- /dev/null
+++ b/timeflow/app/actions/ReorderFieldsAction.java
@@ -0,0 +1,38 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.ui.*;
+import timeflow.app.*;
+
+import java.awt.event.*;
+import javax.swing.*;
+
+
+public class ReorderFieldsAction extends TimeflowAction {
+
+	public ReorderFieldsAction(TimeflowApp app)
+	{
+		super(app, "Reorder Fields...", null, "Edit the order of fields");
+	}
+	
+	@Override
+	public void actionPerformed(ActionEvent e) {
+		ReorderFieldsPanel panel=new ReorderFieldsPanel(getModel());
+		Object[] options = {"Cancel", "Apply"};
+		int n = JOptionPane.showOptionDialog(app,
+					panel,
+					"Reorder Fields",
+					JOptionPane.YES_NO_CANCEL_OPTION,
+					JOptionPane.PLAIN_MESSAGE,
+					null,
+					options,
+					"Apply");
+		panel.detachFromModel();
+		if (n==1)
+		{
+			panel.applyReordering();
+			getModel().noteSchemaChange(this);
+		}
+	}
+
+}
diff --git a/timeflow/app/actions/TimeflowAction.java b/timeflow/app/actions/TimeflowAction.java
new file mode 100755
index 0000000..aae5d23
--- /dev/null
+++ b/timeflow/app/actions/TimeflowAction.java
@@ -0,0 +1,36 @@
+package timeflow.app.actions;
+
+import timeflow.model.*;
+import timeflow.app.*;
+import timeflow.format.file.*;
+
+import javax.swing.*;
+
+import java.awt.Toolkit;
+import java.io.*;
+
+public abstract class TimeflowAction extends AbstractAction {
+	TimeflowApp app;
+
+    public TimeflowAction(TimeflowApp app, String text, ImageIcon icon, String desc) 
+    {
+		super(text, icon);
+		this.app=app;
+		putValue(SHORT_DESCRIPTION, desc);
+	}
+    
+	
+	protected void accelerate(char c)
+	{
+		putValue(Action.ACCELERATOR_KEY,KeyStroke.getKeyStroke(c,
+			    Toolkit.getDefaultToolkit(  ).getMenuShortcutKeyMask(  ), false));
+	}
+
+
+    protected TFModel getModel()
+    {
+    	return app.model;
+    }
+    
+
+}
diff --git a/timeflow/app/actions/WebDocAction.java b/timeflow/app/actions/WebDocAction.java
new file mode 100755
index 0000000..08d8c84
--- /dev/null
+++ b/timeflow/app/actions/WebDocAction.java
@@ -0,0 +1,25 @@
+package timeflow.app.actions;
+
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+
+import javax.swing.Action;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+
+import timeflow.app.TimeflowApp;
+import timeflow.app.ui.ReorderFieldsPanel;
+import timeflow.model.Display;
+
+public class WebDocAction extends TimeflowAction {
+	public WebDocAction(TimeflowApp app)
+	{
+		super(app, "Documentation & License Info...", null, "Read web documentation.");
+	}
+	
+	@Override
+	public void actionPerformed(ActionEvent e) {
+		Display.launchBrowser("http://wiki.github.com/FlowingMedia/TimeFlow/");
+	}
+
+}
diff --git a/timeflow/app/ui/AddFieldPanel.java b/timeflow/app/ui/AddFieldPanel.java
new file mode 100755
index 0000000..fc0d4df
--- /dev/null
+++ b/timeflow/app/ui/AddFieldPanel.java
@@ -0,0 +1,24 @@
+package timeflow.app.ui;
+
+import timeflow.app.*;
+import timeflow.format.field.FieldFormatCatalog;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.*;
+
+
+public class AddFieldPanel extends JPanel {
+	public JTextField name=new JTextField(12);
+	public JComboBox typeChoices=new JComboBox();
+	public AddFieldPanel()
+	{
+		for (String choice: FieldFormatCatalog.classNames())
+			typeChoices.addItem(choice);
+		setLayout(new GridLayout(2,2));
+		add(new JLabel("Field Name"));
+		add(name);
+		add(new JLabel("Field Type"));
+		add(typeChoices);
+	}
+}
diff --git a/timeflow/app/ui/ColorLegendPanel.java b/timeflow/app/ui/ColorLegendPanel.java
new file mode 100755
index 0000000..e8552a0
--- /dev/null
+++ b/timeflow/app/ui/ColorLegendPanel.java
@@ -0,0 +1,63 @@
+package timeflow.app.ui;
+
+import timeflow.model.*;
+import timeflow.app.ui.filter.FilterCategoryPanel;
+import timeflow.data.db.*;
+import timeflow.data.db.filter.FieldValueFilter;
+import timeflow.data.db.filter.ValueFilter;
+import timeflow.data.time.*;
+
+import timeflow.util.*;
+
+import java.awt.*;
+
+import javax.swing.JLabel;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+public class ColorLegendPanel extends ModelPanel {
+	
+
+	Field oldColor;
+
+	public ColorLegendPanel(TFModel model)
+	{
+		super(model);
+		setBackground(Color.white);
+		setLayout(new GridLayout(1,1));
+	}
+	
+	@Override
+	public void note(TFEvent e) {
+		Field color=getModel().getColorField();
+		if (color!=null && color!=oldColor)
+		{
+				removeAll();
+				final FilterCategoryPanel p=new FilterCategoryPanel("Color Legend: '"+color.getName()+"'", 
+						color, this);
+				add(p);
+				Bag<String> data=DBUtils.countValues(getModel().getDB().all(), color);
+				p.setData(data);
+				p.dataList.addListSelectionListener(new ListSelectionListener() {				
+					@Override
+					public void valueChanged(ListSelectionEvent e) {
+						ValueFilter f=(ValueFilter)p.defineFilter();	
+						getModel().setGrayFilter(f, this);
+					}
+				});
+				
+				oldColor=color;
+				revalidate();
+				return;
+		} else if (color==null)
+		{
+			removeAll();
+		}
+		repaint();
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		return new Dimension(200,400);
+	}
+}
diff --git a/timeflow/app/ui/ComponentCluster.java b/timeflow/app/ui/ComponentCluster.java
new file mode 100755
index 0000000..d32b3b6
--- /dev/null
+++ b/timeflow/app/ui/ComponentCluster.java
@@ -0,0 +1,37 @@
+package timeflow.app.ui;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class ComponentCluster extends JPanel
+{
+	int numComps=0;
+	int x1=80;
+	int width=200;
+	int compH=30;
+	DottedLine line=new DottedLine();
+	
+	public ComponentCluster(String name)
+	{
+		setBackground(Color.white);
+		setLayout(null);
+		JLabel label=new JLabel(name);
+		add(label);
+		label.setBounds(3,3,50,30);
+		add(line);
+	}
+	
+	public void addContent(JComponent c)
+	{
+		add(c);
+		c.setBorder(null);
+		c.setBounds(x1,10+numComps*compH, c.getPreferredSize().width, c.getPreferredSize().height);
+		numComps++;
+		line.setBounds(x1-10,10,1,numComps*compH-5);
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		return new Dimension(width, 20+compH*numComps);
+	}
+}
diff --git a/timeflow/app/ui/DateFieldPanel.java b/timeflow/app/ui/DateFieldPanel.java
new file mode 100755
index 0000000..f36e1f3
--- /dev/null
+++ b/timeflow/app/ui/DateFieldPanel.java
@@ -0,0 +1,178 @@
+package timeflow.app.ui;
+
+import timeflow.model.*;
+import timeflow.data.time.*;
+import timeflow.data.db.*;
+import timeflow.data.db.filter.*;
+import timeflow.format.field.*;
+import timeflow.format.file.TimeflowFormat;
+
+import javax.swing.*;
+import java.util.*;
+import java.awt.*;
+import java.awt.event.*;
+
+
+public class DateFieldPanel extends JPanel
+{
+	TFModel model;
+	int numRows;
+	HashMap<String, Integer> numBad=new HashMap<String, Integer>();
+	private static String[] mappable={VirtualField.START, VirtualField.END};
+	JLabel status=new JLabel("");
+	FieldMap[] panels=new FieldMap[mappable.length];
+	JButton submit, cancel;
+	
+	public DateFieldPanel(TFModel model, boolean hasButtons)
+	{
+		this.model=model;
+		
+		ActDB db=model.getDB();
+		numRows=db.size();
+		ActList all=db.all();
+		
+		
+		// calculate stats.
+		for (Field f: db.getFields(RoughTime.class))
+		{
+			int bad=DBUtils.count(all, new MissingValueFilter(f));
+			numBad.put(f.getName(),bad);
+		}
+		
+		setLayout(new BorderLayout());
+		JPanel top=new JPanel();
+		if (hasButtons)
+		{
+			submit=new JButton("Submit");
+			top.add(submit);
+			cancel=new JButton("Cancel");
+			top.add(cancel);
+		}
+		else
+		{
+			JLabel about=new JLabel("Dates");
+			top.add(about);
+		}
+		top.add(status);
+		status.setForeground(Color.red);
+		add(top, BorderLayout.SOUTH);
+		JPanel bottom=new JPanel();
+		add(bottom, BorderLayout.CENTER);
+		bottom.setLayout(new GridLayout(mappable.length,1));
+		
+		// add panels.
+		for (int i=0; i<mappable.length; i++)
+		{
+			panels[i]=new FieldMap(mappable[i],i==0);
+			bottom.add(panels[i]);
+		}
+		
+		
+		// to do: add a status field or something
+		// note inconsistencies, like:
+		// * no start defined.
+		// * ends after starts
+	}
+	
+	public void map()
+	{
+		ActDB db=model.getDB();
+		for (int i=0; i<panels.length; i++)
+		{
+			String choice=(String)panels[i].choices.getSelectedItem();
+			db.setAlias("None".equals(choice) ? null : db.getField(choice), panels[i].name);
+		} 
+		model.noteSchemaChange(this);
+	}
+	
+	class FieldMap extends JPanel
+	{
+		String name;
+		int bad;
+		JComboBox choices;
+		JLabel definedLabel=new JLabel("# def goes here");
+		boolean important;
+		
+		FieldMap(String name, boolean important)
+		{
+			this.name=name;
+			this.important=important;
+			setBackground(Color.white);
+			setLayout(new GridLayout(1,3));
+			
+			add(new JLabel("   "+(important? "* ":"")+VirtualField.humanName(name)));//, BorderLayout.NORTH);
+			
+			choices=new JComboBox();
+			choices.addItem("None");
+			for (Field f: model.getDB().getFields(RoughTime.class))
+			{				
+				choices.addItem(f.getName());//+"  "+percentDef+"% defined");
+			}
+			add(choices);
+			choices.addActionListener(new ActionListener() {
+				@Override
+				public void actionPerformed(ActionEvent e) {
+					showDefined();
+				}});
+			
+			add(definedLabel);
+			definedLabel.setForeground(Color.gray);
+			
+			Field current=model.getDB().getField(name);
+			if (current!=null)
+				choices.setSelectedItem(current.getName());
+			
+			showDefined();
+		}
+		
+		void showDefined()
+		{
+			String choice=(String)choices.getSelectedItem();
+			String val="";
+			boolean none="None".equals(choice);
+			int percentDef=0;
+			if (!none)
+			{
+				percentDef=(int)(100*(1-numBad.get(choice)/(double)numRows));
+				val="  "+percentDef+"% defined";
+			}
+			definedLabel.setText(val);
+			if (important)
+			{
+				if (none)
+					status.setText("   Need \"Start\" for timeline/calendar.");
+				else if (percentDef==0)
+					status.setText("   No dates defined in "+choice+".");
+				else
+					status.setText("");
+			}
+		}
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		return new Dimension(400,80+mappable.length*25);
+	}
+	
+	public static void popWindow(TFModel model)
+	{
+		final JFrame window=new JFrame("Date Fields");
+		window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+		window.getContentPane().setLayout(new GridLayout(1,1));
+		final DateFieldPanel p=new DateFieldPanel(model, true);
+		p.submit.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				p.map();
+				window.setVisible(false);
+			}});
+		p.cancel.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				window.setVisible(false);
+			}});
+		window.getContentPane().add(p);
+		window.setBounds(50,50,window.getPreferredSize().width,window.getPreferredSize().height);
+		window.setVisible(true);
+	}
+}
diff --git a/timeflow/app/ui/DottedLine.java b/timeflow/app/ui/DottedLine.java
new file mode 100755
index 0000000..8713d40
--- /dev/null
+++ b/timeflow/app/ui/DottedLine.java
@@ -0,0 +1,34 @@
+package timeflow.app.ui;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class DottedLine extends JPanel
+{
+	public void paintComponent(Graphics g)
+	{
+		int w=getSize().width, h=getSize().height;
+		g.setColor(Color.white);
+		g.fillRect(0,0,w,h);
+		g.setColor(Color.lightGray);
+		if (w>h)
+		{
+			for (int x=0; x<w; x+=4)
+			{
+				g.drawLine(x,0,x+1,0);
+			}
+		}
+		else
+		{
+			for (int y=0; y<h; y+=4)
+			{
+				g.drawLine(0,y,0,y+1);
+			}			
+		}
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		return new Dimension(1,1);
+	}
+}
diff --git a/timeflow/app/ui/EditRecordPanel.java b/timeflow/app/ui/EditRecordPanel.java
new file mode 100755
index 0000000..08d7aec
--- /dev/null
+++ b/timeflow/app/ui/EditRecordPanel.java
@@ -0,0 +1,134 @@
+package timeflow.app.ui;
+
+import timeflow.model.*;
+import timeflow.app.ui.ImportDelimitedPanel.SchemaPanel;
+import timeflow.data.time.*;
+import timeflow.data.db.*;
+
+import javax.swing.*;
+
+import java.util.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+// panel with form for editing a given database entry
+public class EditRecordPanel extends JPanel
+{
+
+        Act act;
+        HashMap<Field, EditValuePanel> fieldUI = new HashMap<Field, EditValuePanel>();
+        JButton submit, cancel;
+        Dimension idealSize = new Dimension();
+        TFModel model;
+
+        private static void edit(final TFModel model, final Act act, final boolean isAdd)
+        {
+                final JFrame window = new JFrame(isAdd ? "Add Record" : "Edit Record");
+                window.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+                final EditRecordPanel editor = new EditRecordPanel(model, act);
+                window.getContentPane().setLayout(new GridLayout(1, 1));
+                window.getContentPane().add(editor);
+                editor.submit.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                window.setVisible(false);
+                                editor.submitValues();
+                                model.noteAdd(this);
+                        }
+                });
+                editor.cancel.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                window.setVisible(false);
+                                if (isAdd)
+                                {
+                                        model.getDB().delete(act);
+                                }
+                        }
+                });
+                window.setBounds(50, 50, 700, 500);
+                window.pack();
+                window.setVisible(true);
+        }
+
+        public static void edit(TFModel model, Act act)
+        {
+                edit(model, act, false);
+        }
+
+        public static void add(TFModel model)
+        {
+                Act act = model.getDB().createAct();
+                edit(model, act, true);
+        }
+
+        public static void add(TFModel model, RoughTime r)
+        {
+                Act act = model.getDB().createAct();
+                act.set(act.getDB().getField(VirtualField.START), r);
+                edit(model, act, true);
+        }
+
+        public EditRecordPanel(TFModel model, Act act)
+        {
+                this.model = model;
+                this.act = act;
+
+                setBackground(Color.white);
+                setLayout(new BorderLayout());
+
+                JPanel buttons = new JPanel();
+                add(buttons, BorderLayout.SOUTH);
+                buttons.setBackground(Color.lightGray);
+                submit = new JButton("OK");
+                buttons.add(submit);
+                cancel = new JButton("Cancel");
+                buttons.add(cancel);
+
+                JPanel entryPanel = new JPanel();
+                JScrollPane scroller = new JScrollPane(entryPanel);
+                add(scroller, BorderLayout.CENTER);
+
+                java.util.List<Field> fields = act.getDB().getFields();
+                int n = fields.size();
+                entryPanel.setLayout(null);
+
+                DBUtils.setRecSizesFromCurrent(act.getDB());
+                int top = 0;
+
+                for (Field f : fields)
+                {
+                        EditValuePanel p = new EditValuePanel(f.getName(), act.get(f),
+                                f.getType(), f.getRecommendedSize() > 100);
+                        entryPanel.add(p);
+                        Dimension d = p.getPreferredSize();
+                        p.setBounds(0, top, d.width, d.height);
+                        top += d.height;
+                        idealSize.width = Math.max(d.width + 5, idealSize.width);
+                        idealSize.height = Math.max(top + 45, idealSize.height);
+                        fieldUI.put(f, p);
+                }
+
+        }
+
+        public Dimension getPreferredSize()
+        {
+                return idealSize;
+        }
+
+        public void submitValues()
+        {
+                for (Field f : fieldUI.keySet())
+                {
+                        act.set(f, fieldUI.get(f).getInputValue());
+                }
+                model.noteRecordChange(this);
+        }
+}
diff --git a/timeflow/app/ui/EditValuePanel.java b/timeflow/app/ui/EditValuePanel.java
new file mode 100755
index 0000000..2f148d8
--- /dev/null
+++ b/timeflow/app/ui/EditValuePanel.java
@@ -0,0 +1,111 @@
+package timeflow.app.ui;
+
+import timeflow.format.field.*;
+
+import javax.swing.*;
+import javax.swing.text.JTextComponent;
+
+import java.awt.*;
+import java.awt.event.*;
+
+public class EditValuePanel extends JPanel
+{
+
+        FieldFormat parser;
+        boolean longField;
+        JLabel feedback = new JLabel()
+        {
+
+                public Dimension getPreferredSize()
+                {
+                        Dimension d = super.getPreferredSize();
+                        return new Dimension(200, d.height);
+                }
+        };
+        static final String space = "   ";
+        JTextComponent input;
+
+        public EditValuePanel(String name, Object startValue, Class type, boolean longField)
+        {
+                parser = FieldFormatCatalog.getFormat(type);
+
+                if (longField)
+                {
+                        setLayout(new BorderLayout());
+                        JPanel top = new JPanel();
+                        top.setLayout(new GridLayout(2, 2));
+                        top.add(new JPanel());
+                        top.add(new JPanel());
+                        JLabel fieldLabel = new JLabel(space + name + "   (long)");
+                        top.add(fieldLabel);
+                        top.add(feedback);
+                        add(top, BorderLayout.NORTH);
+                        input = new JTextArea(5, 60);
+                        ((JTextArea) input).setLineWrap(true);
+                        ((JTextArea) input).setWrapStyleWord(true);
+                        JScrollPane scroller = new JScrollPane(input);
+                        scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+                        add(scroller, BorderLayout.CENTER);
+                        add(new JPanel(), BorderLayout.WEST);
+                        add(new JPanel(), BorderLayout.SOUTH);
+                } else
+                {
+                        setLayout(new GridLayout(1, 4));
+                        JLabel fieldLabel = new JLabel(space + name);
+                        add(fieldLabel);
+                        JLabel typeLabel = new JLabel(FieldFormatCatalog.humanName(type));
+                        add(typeLabel);
+                        typeLabel.setForeground(Color.gray);
+                        input = new JTextField(8);
+                        add(input);
+                        // enough room for "couldn't understand"
+                        add(feedback);
+                }
+                input.setText(startValue == null ? "" : parser.format(startValue));
+                input.addKeyListener(new KeyAdapter()
+                {
+
+                        @Override
+                        public void keyReleased(KeyEvent e)
+                        {
+                                parse();
+                        }
+                });
+                parse();
+        }
+
+        void parse()
+        {
+                try
+                {
+                        parser.parse(input.getText());
+                } catch (Exception e)
+                {
+                }
+                feedback.setText("  " + parser.feedback());
+                feedback.setForeground(parser.isUnderstood() ? Color.gray : Color.red);
+        }
+
+        public Object getInputValue()
+        {
+                try
+                {
+                        return parser.parse(input.getText());
+                } catch (Exception e)
+                {
+                        return null;
+                }
+        }
+
+        public boolean isOK()
+        {
+                return parser.isUnderstood();
+        }
+
+        public Dimension getPreferredSize()
+        {
+                Dimension d = super.getPreferredSize();
+                int w = Math.max(300, d.width);
+                return new Dimension(w, d.height);
+        }
+}
diff --git a/timeflow/app/ui/GlobalDisplayPanel.java b/timeflow/app/ui/GlobalDisplayPanel.java
new file mode 100755
index 0000000..59f4c8a
--- /dev/null
+++ b/timeflow/app/ui/GlobalDisplayPanel.java
@@ -0,0 +1,174 @@
+package timeflow.app.ui;
+
+import timeflow.app.ui.filter.FilterControlPanel;
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+import timeflow.model.*;
+
+import javax.swing.*;
+
+import timeflow.util.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.List;
+
+public class GlobalDisplayPanel extends ModelPanel
+{
+
+        JPanel encodings = new JPanel();
+        JPanel localControls = new JPanel();
+        JPanel globalControls = new JPanel();
+        CardLayout localCards = new CardLayout();
+
+        public GlobalDisplayPanel(TFModel model, FilterControlPanel filterControls)
+        {
+                super(model);
+                setBackground(Color.white);
+                setLayout(new BorderLayout());
+
+                add(localControls, BorderLayout.CENTER);
+                localControls.setBackground(Color.white);
+                localControls.setLayout(localCards);
+
+                JPanel p = new JPanel();
+                p.setBackground(Color.white);
+                p.setLayout(new BorderLayout());
+
+                JPanel globalLabel = new JPanel();
+                globalLabel.setLayout(new BorderLayout());
+
+                JPanel topLine = new Pad(2, 3);
+                topLine.setBackground(Color.gray);
+                globalLabel.add(topLine, BorderLayout.NORTH);
+
+                JPanel bottomLine = new Pad(2, 3);
+                bottomLine.setBackground(Color.gray);
+                globalLabel.add(bottomLine, BorderLayout.SOUTH);
+
+                JLabel label = new JLabel(" Global Controls", JLabel.LEFT)
+                {
+
+                        public Dimension getPreferredSize()
+                        {
+                                return new Dimension(30, 30);
+                        }
+                };
+                label.setBackground(Color.lightGray);
+                label.setForeground(Color.darkGray);
+                globalLabel.add(label, BorderLayout.CENTER);
+                p.add(globalLabel, BorderLayout.NORTH);
+
+                JPanel global = new JPanel();
+                global.setLayout(new BorderLayout());
+                global.add(new StatusPanel(model, filterControls), BorderLayout.NORTH);
+
+                encodings.setLayout(new GridLayout(4, 1));
+                encodings.setBackground(Color.white);
+                global.add(encodings, BorderLayout.CENTER);
+                p.add(global, BorderLayout.CENTER);
+                add(p, BorderLayout.SOUTH);
+
+                makeEncodingPanel();
+        }
+
+        public void showLocalControl(String name)
+        {
+                localCards.show(localControls, name);
+        }
+
+        public void addLocalControl(String name, JComponent control)
+        {
+                localControls.add(control, name);
+        }
+
+        void makeEncodingPanel()
+        {
+                encodings.removeAll();
+                ActDB db = getModel().getDB();
+                if (db == null)
+                {
+                        return;
+                }
+
+                java.util.List<Field> dimensions = DBUtils.categoryFields(db);
+                java.util.List<Field> measures = db.getFields(Double.class);
+                java.util.List<Field> fields = db.getFields();
+
+                makeChooser(VirtualField.LABEL, "Label", "None", fields); //db.getFields()); // String.class));
+                makeChooser(VirtualField.TRACK, "Groups", "None", dimensions);
+                makeChooser(VirtualField.COLOR, "Color", "Same As Groups", dimensions);
+
+                makeChooser(VirtualField.SIZE, "Dot Size", "None", measures);
+        }
+
+        private JComboBox makeChooser(final String alias, String title, String nothingLabel, List<Field> fields)
+        {
+                if (fields.size() == 0)
+                {
+                        return null;
+                }
+                JPanel panel = new JPanel();
+                panel.setBackground(Color.white);
+                panel.setLayout(new BorderLayout());
+                JPanel topPad = new Pad(10, 7);
+                topPad.setBackground(Color.white);
+                panel.add(topPad, BorderLayout.NORTH);
+
+                JPanel rightPad = new Pad(10, 10);
+                panel.add(rightPad, BorderLayout.EAST);
+                rightPad.setBackground(Color.white);
+
+                panel.add(new JLabel(" " + title)
+                {
+
+                        public Dimension getPreferredSize()
+                        {
+                                return new Dimension(60, 25);
+                        }
+                },
+                        BorderLayout.WEST);
+                final JComboBox c = new JComboBox();
+
+                if (nothingLabel != null)
+                {
+                        c.addItem(nothingLabel);
+                }
+                for (Field f : fields)
+                {
+                        c.addItem(f.getName());
+                }
+
+                Field current = getModel().getDB().getField(alias);
+                if (current != null)
+                {
+                        c.setSelectedItem(current.getName());
+                }
+                c.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                Field realField = c.getSelectedIndex() == 0
+                                        ? null : getModel().getDB().getField((String) c.getSelectedItem());
+                                getModel().setFieldAlias(realField, alias, GlobalDisplayPanel.this);
+                        }
+                });
+                c.setBackground(Color.white);
+                c.setBorder(null);
+                panel.add(c, BorderLayout.CENTER);
+                encodings.add(panel);
+                c.setBorder(null);
+                return c;
+        }
+
+        @Override
+        public void note(TFEvent e)
+        {
+                if (e.affectsSchema())
+                {
+                        makeEncodingPanel();
+                }
+        }
+}
diff --git a/timeflow/app/ui/HtmlDisplay.java b/timeflow/app/ui/HtmlDisplay.java
new file mode 100755
index 0000000..c7f0f98
--- /dev/null
+++ b/timeflow/app/ui/HtmlDisplay.java
@@ -0,0 +1,24 @@
+package timeflow.app.ui;
+
+import java.awt.Font;
+
+import javax.swing.JEditorPane;
+import javax.swing.UIManager;
+import javax.swing.text.html.HTMLDocument;
+import javax.swing.text.html.StyleSheet;
+
+public class HtmlDisplay {
+	public static JEditorPane create()
+	{
+		JEditorPane p = new JEditorPane();
+		p.setEditable(false);
+		p.setContentType("text/html");
+		
+		Font font = UIManager.getFont("Label.font");
+        String bodyRule = "body { font-family: "+font.getFamily()+"; "+
+                "font-size: " + font.getSize() + "pt; }";
+        StyleSheet styles=((HTMLDocument)p.getDocument()).getStyleSheet();
+        styles.addRule(bodyRule);
+        return p;
+	}
+}
diff --git a/timeflow/app/ui/ImportDelimitedPanel.java b/timeflow/app/ui/ImportDelimitedPanel.java
new file mode 100755
index 0000000..c626219
--- /dev/null
+++ b/timeflow/app/ui/ImportDelimitedPanel.java
@@ -0,0 +1,310 @@
+package timeflow.app.ui;
+
+import timeflow.data.time.*;
+import timeflow.data.db.*;
+import timeflow.format.field.*;
+import timeflow.format.file.DelimitedFormat;
+import timeflow.model.*;
+
+import timeflow.util.*;
+
+import javax.swing.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.text.ParseException;
+import java.util.*;
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class ImportDelimitedPanel extends JFrame 
+{
+	String fileName;
+	SchemaPanel schemaPanel;
+	boolean exitOnClose=false; // for testing!
+	JScrollPane scroller;
+	TFModel model;
+	JLabel numLinesLabel=new JLabel();
+	
+	// for testing:
+	public static void main(String[] args) throws Exception
+	{
+		System.out.println("Starting test of ImportEditor");
+		String file="data/probate.tsv";
+		String[][] data=DelimitedFormat.readArrayGuessDelim(file, System.out);
+		ImportDelimitedPanel editor=new ImportDelimitedPanel(new TFModel());
+		editor.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+		editor.setFileName(file);
+		editor.setData(data);
+		editor.setBounds(50,50,900,800);
+		editor.setVisible(true);
+		editor.exitOnClose=true;
+	}
+	
+	public ImportDelimitedPanel(final TFModel model)
+	{
+		super("Import File");
+		this.model=model;
+		setBackground(Color.white);
+		
+		setLayout(new BorderLayout());
+		JPanel top=new JPanel();
+		add(top, BorderLayout.NORTH);
+		top.setLayout(new FlowLayout(FlowLayout.LEFT));
+		top.setBackground(Color.lightGray);
+		top.add(numLinesLabel);
+		final JTextField source=new JTextField(12);
+		
+		JButton done=new JButton("Import This");
+		top.add(done);
+		done.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				model.setDB(schemaPanel.makeDB(source.getText()), fileName, true, this);
+				setVisible(false);
+				if (exitOnClose)
+					System.exit(0);
+			}});
+		
+		JButton cancel=new JButton("Cancel");
+		top.add(cancel);
+		cancel.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				setVisible(false);
+				if (exitOnClose)
+					System.exit(0);
+			}});
+		
+		top.add(new JLabel("        Enter A Source:"));		
+		top.add(source);
+		schemaPanel=new SchemaPanel();
+		schemaPanel.setBackground(Color.white);
+		scroller=new JScrollPane(schemaPanel);
+		add(scroller, BorderLayout.CENTER);
+	}
+
+	public void scrollToTop()
+	{
+		scroller.getViewport().setViewPosition(new Point(0,0));  
+	}
+	
+	public void setFileName(String fileName)
+	{
+		this.fileName=fileName;
+	}
+	
+	public void setData(String[][] data)
+	{
+		numLinesLabel.setText((data.length-1)+" records read.  ");
+		schemaPanel.display(data);
+	}
+
+	class SchemaPanel extends JPanel
+	{
+		int numFields, rows;
+		String[][] data;
+		ArrayList<FieldPanel> panels=new ArrayList<FieldPanel>();
+		
+		ActDB makeDB(String source)
+		{
+			// count number of fields that are not ignored.
+			int n=0;
+			for (FieldPanel fp: panels)
+				if (!fp.ignore.isSelected())
+					n++;
+			
+			Class[] types=new Class[n];
+			String[] fieldNames=new String[n];
+			int[] index=new int[n];
+			if (source.trim().length()==0)
+				source="[source unspecified]";
+			int i=0, j=0;
+			for (FieldPanel fp: panels)
+			{			
+				if (!fp.ignore.isSelected())
+				{
+					fieldNames[i]=fp.fieldName;
+					String typeChoice=(String)fp.typeChoices.getSelectedItem();
+					Class type=FieldFormatCatalog.javaClass(typeChoice);
+					System.out.println("Type: "+type+" for: "+typeChoice+" from "+fp.fieldName);
+					types[i]=type;
+					index[i]=j;
+					i++;
+				}
+				j++;
+			}
+			
+			ActDB db= new ArrayDB(fieldNames, types, source);
+			HashMap<Integer, StringBuffer> errors=new HashMap<Integer, StringBuffer>();
+			for (i=1; i<data.length; i++)
+			{
+				Act act=db.createAct();				
+				for (int k=0; k<n; k++)
+				{
+					j=index[k];
+					String s=data[i][j];
+					Field f=db.getField(fieldNames[k]);
+					FieldFormat format=FieldFormatCatalog.getFormat(types[k]);
+					try
+					{
+						Object o=format.parse(s);
+						act.set(f,o);
+					}
+					catch (Exception e)
+					{
+						StringBuffer b=errors.get(i-1);
+						if (b==null)
+						{
+							b=new StringBuffer();
+							errors.put(i-1,b);
+						}
+						else
+							b.append("; ");
+						b.append(f.getName()+":"+s);
+					}
+				}
+			}
+			
+			if (errors.size()>0)
+			{
+				Field error=db.addField("UNPARSED FIELDS", String.class);
+				for (int row:errors.keySet())
+				{
+					db.get(row).set(error, errors.get(row).toString());
+				}
+			}
+			
+			for (j=0; j<n; j++)
+			{
+				System.out.println(db.getField(fieldNames[j]));
+			}
+			
+			return db;
+		}
+		
+		void display(String[][] data)
+		{
+			removeAll();
+			
+			this.data=data;
+			
+			// analyze data.
+			Class[] guesses=FieldFormatGuesser.analyze(data, 1, 100);
+			
+			// go through first row, which is headers.
+			String[] headers=data[0];
+			
+			// if there are duplicate headers, add indicators.
+			HashSet<String> h=new HashSet<String>();
+			for (int i=0; i<headers.length; i++)
+			{
+				String base=headers[i];
+				int j=2;
+				String name=base;
+				while (h.contains(name))
+				{
+					name=base+" "+j;
+					j++;
+				}
+				headers[i]=name;
+				h.add(name);
+			}
+			
+			numFields=headers.length;
+			int cols=2;
+			rows=(int)Math.ceil(numFields/2.0);
+			setLayout(new GridLayout(rows, cols));
+			for (int i=0; i<numFields; i++)
+			{
+				Bag<String> vals=new Bag<String>();
+				
+				for (int j=1; j<data.length; j++)
+				{
+					vals.add(data[j][i]);
+				}
+				java.util.List<String> top=vals.listTop(5);
+				int n=top.size();
+				String[] samples=new String[n];
+				for (int j=0; j<n; j++)
+				{
+					String s=top.get(j);
+					samples[j]=(s.length()==0 ? "*MISSING*" : s)+"    ("+vals.num(s)+" times)";
+				}
+				
+				JPanel p=new JPanel();
+				add(p);
+				p.setLayout(new BorderLayout());
+				FieldPanel f=new FieldPanel(headers[i], samples, guesses[i]);
+				panels.add(f);
+				p.add(f, BorderLayout.CENTER);
+				JPanel hr=new JPanel();
+				hr.setPreferredSize(new Dimension(20,20));
+				p.add(hr, BorderLayout.SOUTH);
+			}
+		}
+		
+		public Dimension getPreferredSize()
+		{
+			return new Dimension(400,150*rows);
+		}
+	}
+	
+	class FieldPanel extends JPanel
+	{
+		JComboBox typeChoices;
+		String fieldName;
+		JCheckBox ignore;
+		JLabel fieldLabel;
+		int x1=5, y1=20, y3=150,x2=150, x3=150, x4=375, dh=2;
+
+		
+		FieldPanel(String fieldName, String[] sampleValues, Class typeGuess)
+		{
+			// just going with a null layout here, because it's a lot simpler!
+			
+			setLayout(null);
+			setBackground(Color.white);
+			this.fieldName=fieldName;
+			
+			fieldLabel=new JLabel(" \""+fieldName+'"');	
+			fieldLabel.setFont(model.getDisplay().big());
+			add(fieldLabel);
+			fieldLabel.setBounds(x1,y1,fieldLabel.getPreferredSize().width, fieldLabel.getPreferredSize().height);
+			
+			typeChoices=new JComboBox();
+			for (String choice: FieldFormatCatalog.classNames())
+				typeChoices.addItem(choice);
+			typeChoices.setSelectedItem(FieldFormatCatalog.humanName(typeGuess));
+			add(typeChoices);
+			int y2=fieldLabel.getY()+fieldLabel.getHeight()+dh+5;
+			typeChoices.setBounds(x1,y2,
+					typeChoices.getPreferredSize().width, typeChoices.getPreferredSize().height);
+				
+			ignore=new JCheckBox("Ignore Field");			
+			add(ignore);
+			ignore.setBounds(x1,typeChoices.getY()+typeChoices.getHeight()+dh,
+					ignore.getPreferredSize().width, ignore.getPreferredSize().height);
+			ignore.addActionListener(new ActionListener() {
+				@Override
+				public void actionPerformed(ActionEvent e) {
+					Color c=ignore.isSelected() ? Color.gray : Color.black;
+					fieldLabel.setForeground(c);
+					typeChoices.setForeground(c);
+				}});
+			
+			JTextArea values=new JTextArea();
+			values.setForeground(Color.gray);
+			for (int i=0; i<sampleValues.length; i++)
+				values.append(sampleValues[i]+"\n");
+			add(values);
+			values.setBounds(x3,y2,x4-x3,y3-y2);
+		}
+		
+		public Dimension getPreferredSize()
+		{
+			return new Dimension(500,150);
+		}
+	}
+}
diff --git a/timeflow/app/ui/LinkTabPane.java b/timeflow/app/ui/LinkTabPane.java
new file mode 100755
index 0000000..1f4ee01
--- /dev/null
+++ b/timeflow/app/ui/LinkTabPane.java
@@ -0,0 +1,208 @@
+package timeflow.app.ui;
+
+import javax.swing.*;
+import javax.swing.event.ChangeListener;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+
+// custom JTabbedPane-like thing.
+public class LinkTabPane extends JPanel {
+	
+	ArrayList<String> tabNames=new ArrayList<String>();
+	HashMap<String, JComponent> tabMap=new HashMap<String, JComponent>();
+	String currentName;
+	CardLayout cards=new CardLayout();
+	JPanel center=new JPanel();
+	LinkTop top=new LinkTop();
+	
+	public LinkTabPane()
+	{
+		setBackground(Color.white);
+		setLayout(new BorderLayout());
+		add(top, BorderLayout.NORTH);
+		add(center, BorderLayout.CENTER);
+		center.setLayout(cards);
+		top.addMouseListener(new MouseAdapter() {
+			@Override
+			public void mousePressed(MouseEvent e) {
+				String s=top.getName(e.getX());
+				if (s!=null)
+				{
+					String old=currentName;
+					setCurrentName(s);
+					firePropertyChange("tab", old, s);
+				}
+			}});
+	}
+	
+	public String getTitleAt(int i)
+	{
+		return tabNames.get(i);
+	}
+	
+	public void setSelectedIndex(int i)
+	{
+		setCurrentName(getTitleAt(i));
+	}
+	
+	public void addTab(JComponent component, String name, boolean left)
+	{
+		tabNames.add(name);
+		tabMap.put(name, component);
+		center.add(component, name);
+		top.addName(name, left);
+		repaint();
+		if (currentName==null)
+			currentName=name;
+	}
+	
+	public String getCurrentName()
+	{
+		return currentName;
+	}
+	
+	public void setCurrentName(final String currentName)
+	{
+		this.currentName=currentName;
+		top.repaint();
+		SwingUtilities.invokeLater(new Runnable() {public void run() {cards.show(center, currentName);}});
+
+	}
+	
+	class LinkTop extends JPanel
+	{
+		int left, right;
+		ArrayList<HotLink> leftHots=new ArrayList<HotLink>();
+		ArrayList<HotLink> rightHots=new ArrayList<HotLink>();
+		Font font=new Font("Verdana", Font.PLAIN, 14);
+		FontMetrics fm=getFontMetrics(font);
+		
+		LinkTop()
+		{
+			setLayout(new FlowLayout(FlowLayout.LEFT));
+			setBackground(new Color(220,220,220));
+		}
+		
+		public void paintComponent(Graphics g1)
+		{
+			int w=getSize().width, h=getSize().height;
+			Graphics2D g=(Graphics2D)g1;
+			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+			g.setColor(getBackground());
+			g.fillRect(0,0,w,h);
+			g.setColor(Color.gray);
+			for (int i=0; i<2; i++)
+			{
+				g.drawLine(0,i,w,i);
+				g.drawLine(0,h-1-i,w,h-1-i);
+			}
+			
+			for (HotLink hot: leftHots)
+			{
+				draw(g, hot, h, 0);
+			}
+			
+			for (HotLink hot: rightHots)
+			{
+				draw(g, hot, h, w);
+			}
+			
+			for (int i=0; i<leftHots.size(); i++)
+			{
+				
+				if (i<leftHots.size()-1)
+				{
+					HotLink hot=leftHots.get(i);
+					for (int j=0; j<1; j++)
+						g.drawLine(hot.x+hot.width-1-j, 7, hot.x+hot.width-1-j, h-7);
+				}
+			}
+			
+			for (int i=0; i<rightHots.size(); i++)
+			{
+				
+				if (i<rightHots.size()-1)
+				{
+					HotLink hot=rightHots.get(i);
+					for (int j=0; j<1; j++)
+						g.drawLine(hot.x+w-1-j, 7, hot.x+w-1-j, h-7);
+				}
+			}
+		}
+		
+		void draw(Graphics g, HotLink hot, int h, int dx)
+		{
+			int x=hot.x+dx;
+			if (hot.s.equals(currentName))
+			{
+				g.setColor(Color.lightGray);
+				g.fillRect(x,2,hot.width,h-4);
+				g.setColor(Color.gray);
+				g.drawLine(x-1, 0, x-1, h);
+				g.drawLine(x+hot.width-1, 0, x+hot.width-1, h);
+			}
+			g.setColor(Color.darkGray);
+			g.setFont(font);
+			int sw=fm.stringWidth(hot.s);
+			g.drawString(hot.s, x+(hot.width-sw)/2, h-10);
+			
+		}
+		
+		String getName(int x)
+		{
+			for (HotLink h: leftHots)
+			{
+				if (h.x<=x && h.x+h.width>x)
+					return h.s;
+			}
+			for (HotLink h: rightHots)
+			{
+				int w=getSize().width;
+				if (h.x+w<=x && h.x+h.width+w>x)
+					return h.s;
+			}
+
+			if (leftHots.size()>0)
+				return leftHots.get(leftHots.size()-1).s;
+			if (rightHots.size()>0)
+				return rightHots.get(0).s;
+			return null;
+		}
+		
+	    void addName(String name, boolean leftward)
+		{
+	    	if (leftward)
+	    	{
+	    		int x=right;
+		    	int w=fm.stringWidth(name)+24;
+		    	leftHots.add(new HotLink(name, x, 0, w, 30));
+		    	right+=w;
+	    	}
+	    	else
+	    	{
+	    		int x=left;
+		    	int w=fm.stringWidth(name)+24;
+		    	rightHots.add(new HotLink(name, x-w, 0, w, 30));
+		    	left-=w;		
+	    	}
+		}
+	    
+	    class HotLink extends Rectangle
+	    {
+	    	String s;
+	    	HotLink(String s, int x, int y, int w, int h)
+	    	{
+	    		super(x,y,w,h);
+	    		this.s=s;
+	    	}
+	    }
+		
+		public Dimension getPreferredSize()
+		{
+			return new Dimension(30,30);
+		}
+	}
+	
+}
diff --git a/timeflow/app/ui/MassDeletePanel.java b/timeflow/app/ui/MassDeletePanel.java
new file mode 100755
index 0000000..10acc13
--- /dev/null
+++ b/timeflow/app/ui/MassDeletePanel.java
@@ -0,0 +1,80 @@
+package timeflow.app.ui;
+
+import timeflow.data.db.*;
+import timeflow.model.*;
+import timeflow.views.*;
+
+import javax.swing.*;
+import javax.swing.table.*;
+
+import java.awt.*;
+import java.util.*;
+
+public class MassDeletePanel extends ModelPanel
+{
+	TableView table;
+	ActList keepers;
+	
+	public MassDeletePanel(TFModel model, ActList keepers, String title)
+	{
+		super(model);
+		this.keepers=keepers;
+		setLayout(new BorderLayout());
+		
+		JPanel top=new JPanel();
+		top.setLayout(new GridLayout(4,1));
+		top.add(new JPanel());
+		top.add(new JLabel(title));
+		int n=keepers.size();
+		String message=null;
+		if (n>1)
+			message="These are the "+n+" items that will remain.";
+		else if (n==1)
+			message="This in the only item that will remain.";
+		else 
+			message="No items will remain!";
+		
+		JLabel instructions=new JLabel(message);
+		top.add(instructions);
+		top.add(new JPanel());
+		add(top, BorderLayout.NORTH);
+		
+		table=new TableView(model);
+		model.removeListener(table);
+		add(table, BorderLayout.CENTER);
+		table.setEditable(false);
+		table.setActs(keepers);
+	}
+	
+	public void applyAction()
+	{
+		ActDB db=getModel().getDB();
+		HashSet<Act> keepSet=new HashSet<Act>();
+		keepSet.addAll(keepers);
+
+		for (Act a: db.all())
+			if (!keepSet.contains(a))
+				db.delete(a);
+		
+		// we assume the caller will decide what event to fire.
+	}
+	
+	public void detachFromModel()
+	{
+		TFModel model=getModel();
+		model.removeListener(table);
+		model.removeListener(this);
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		Dimension d=super.getPreferredSize();
+		return new Dimension(Math.max(700, d.width), 250);
+	}
+
+	@Override
+	public void note(TFEvent e) {
+		// TODO Auto-generated method stub
+		
+	}
+}
diff --git a/timeflow/app/ui/ReorderFieldsPanel.java b/timeflow/app/ui/ReorderFieldsPanel.java
new file mode 100755
index 0000000..1db474d
--- /dev/null
+++ b/timeflow/app/ui/ReorderFieldsPanel.java
@@ -0,0 +1,68 @@
+package timeflow.app.ui;
+
+import timeflow.data.db.*;
+import timeflow.model.*;
+import timeflow.views.*;
+
+import javax.swing.*;
+import javax.swing.table.*;
+
+import java.awt.*;
+import java.util.*;
+
+public class ReorderFieldsPanel extends ModelPanel
+{
+	TableView table;
+	
+	public ReorderFieldsPanel(TFModel model)
+	{
+		super(model);
+		setLayout(new BorderLayout());
+		
+		JPanel top=new JPanel();
+		top.setLayout(new GridLayout(3,1));
+		top.add(new JPanel());
+		JLabel instructions=new JLabel("Drag and drop the column headers to reorder fields.");
+		top.add(instructions);
+		top.add(new JPanel());
+		add(top, BorderLayout.NORTH);
+		
+		table=new TableView(model);
+		add(table, BorderLayout.CENTER);
+		table.setEditable(false);
+		table.setReorderable(true);
+		table.onscreen(true);
+	}
+	
+	public void applyReordering()
+	{
+		Enumeration<TableColumn> columns=table.getTable().getTableHeader().getColumnModel().getColumns();
+		ArrayList<Field> newOrder=new ArrayList<Field>();
+		while (columns.hasMoreElements())
+		{
+			TableColumn col=columns.nextElement();
+			String name=col.getHeaderValue().toString();
+			newOrder.add(getModel().getDB().getField(name));
+		}
+		getModel().getDB().setNewFieldOrder(newOrder);
+	}
+	
+	public void detachFromModel()
+	{
+		TFModel model=getModel();
+		model.removeListener(table);
+		model.removeListener(this);
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		Dimension d=super.getPreferredSize();
+		return new Dimension(Math.max(700, d.width), 250);
+	}
+
+	@Override
+	public void note(TFEvent e) {
+		// TODO Auto-generated method stub
+		
+	}
+}
diff --git a/timeflow/app/ui/SizeLegendPanel.java b/timeflow/app/ui/SizeLegendPanel.java
new file mode 100755
index 0000000..a689d4a
--- /dev/null
+++ b/timeflow/app/ui/SizeLegendPanel.java
@@ -0,0 +1,113 @@
+package timeflow.app.ui;
+
+import timeflow.model.*;
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+
+import timeflow.util.*;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+
+public class SizeLegendPanel extends ModelPanel {
+	Field sizeField;
+	double min, max;
+
+	public SizeLegendPanel(TFModel model)
+	{
+		super(model);
+		setBackground(Color.white);
+	}
+	
+	@Override
+	public void note(TFEvent e) {
+		Field size=getModel().getDB().getField(VirtualField.SIZE);
+		
+		if (size!=null && (size!=sizeField || e.affectsData()))
+		{
+			double[] minmax=DBUtils.minmax(getModel().getActs(), size);
+			min=minmax[0];
+			max=minmax[1];
+		}
+		sizeField=size;		
+		repaint();
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		return new Dimension(200,40);
+	}
+	
+	public void paintComponent(Graphics g1)
+	{
+		Graphics2D g=(Graphics2D)g1;
+		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+		int w=getSize().width;
+		int h=getSize().height;
+		TFModel model=getModel();
+		Display display=model.getDisplay();
+		g.setColor(getBackground());
+		g.setFont(display.plain());
+		g.fillRect(0,0,w,h);
+		g.setColor(Color.gray);
+		
+		if (sizeField==null)
+		{
+			return;
+		}
+		else if (Double.isNaN(min))
+		{
+			g.drawString("All values missing.",3,20);
+			return;
+		}
+		else
+		{
+			AffineTransform old=g.getTransform();
+			g.setTransform(AffineTransform.getTranslateInstance(20, 0));
+			if (min==max)
+			{
+				g.setColor(Color.gray);
+				g.fillOval(3,h/2-3,6,6);
+				g.setColor(Color.black);
+				g.setFont(display.tiny());
+				g.drawString(format(min),12,h/2+5);				
+			}
+			else
+			{
+				String leftLabel=format(min);
+				String rightLabel=format(max);
+				g.setFont(display.tiny());
+				int lw=display.tinyFontMetrics().stringWidth(leftLabel);
+				int rw=display.tinyFontMetrics().stringWidth(rightLabel);
+				g.setColor(Color.black);
+				int ty=h/2+5;;
+				g.drawString(leftLabel,2,ty);
+				g.setColor(Color.lightGray);
+				double maxAbs=Math.max(Math.abs(min), Math.abs(max));
+				int dx=8+lw;
+				for (int i=0; i<5; i++)
+				{
+					double z=(i*max+(4-i)*min)/4;
+					int r=(int)(Math.sqrt(Math.abs(z/maxAbs))*Display.MAX_DOT_SIZE);
+					if (r<1)
+						r=1;
+					if (z>0)
+						g.fillOval(dx,h/2-r,2*r,2*r);
+					else
+						g.drawOval(dx,h/2-r,2*r,2*r);
+					dx+=5+2*r;
+				}
+				g.setColor(Color.black);
+				g.drawString(rightLabel,dx+4,ty);
+			}
+			g.setTransform(old);
+		}
+	}
+	
+	String format(double x)
+	{
+		if (Math.abs(x)>10 && (max-min)>10)
+			return Display.format(Math.round(x));
+		return Display.format(x);
+	}
+}
diff --git a/timeflow/app/ui/StatusPanel.java b/timeflow/app/ui/StatusPanel.java
new file mode 100755
index 0000000..0cdf2fb
--- /dev/null
+++ b/timeflow/app/ui/StatusPanel.java
@@ -0,0 +1,98 @@
+package timeflow.app.ui;
+
+import timeflow.model.*;
+import timeflow.app.ui.filter.FilterControlPanel;
+import timeflow.data.db.*;
+import timeflow.data.db.filter.ActFilter;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.text.*;
+
+public class StatusPanel extends ModelPanel
+{
+	JLabel numLabel=new JLabel("")
+	{
+		public Dimension getPreferredSize()
+		{
+			return new Dimension(30,25);
+		}
+	};
+	JLabel filterLabel=new JLabel("")
+	{
+		public Dimension getPreferredSize()
+		{
+			return new Dimension(30,25);
+		}
+	};
+	
+	static final DecimalFormat niceFormat=new DecimalFormat("###,###");
+	
+	public StatusPanel(TFModel model, final FilterControlPanel filterControls) {
+		super(model);	
+		setLayout(new BorderLayout());
+		setBackground(new Color(245, 245, 245));
+		setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 15));
+		JPanel center=new JPanel();
+		center.setBackground(getBackground());
+		center.setLayout(new GridLayout(2,1));
+		add(center, BorderLayout.CENTER);
+		
+		center.add(numLabel);
+		numLabel.setFont(model.getDisplay().plain());
+		numLabel.setBackground(new Color(245, 245, 245));
+		
+		JPanel bottom=new JPanel();
+		center.add(bottom);
+		bottom.setLayout(new BorderLayout());
+		bottom.add(filterLabel, BorderLayout.CENTER);
+		bottom.setBackground(new Color(245, 245, 245));
+		filterLabel.setFont(model.getDisplay().plain());
+		filterLabel.setBackground(new Color(245, 245, 245));
+		filterLabel.setForeground(Color.red);
+		
+		JPanel clearPanel=new JPanel();
+		clearPanel.setBackground(new Color(245, 245, 245));
+		clearPanel.setLayout(new GridLayout(1,1));
+		JButton clear=new JButton(new ImageIcon("images/button_clear_all.gif"));
+		clear.setBorder(null);
+		clear.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				filterControls.clearFilters();
+			}
+		});
+		clearPanel.add(clear);
+		bottom.add(clearPanel, BorderLayout.EAST);
+		
+		add(new DottedLine(), BorderLayout.SOUTH);
+	}
+
+	@Override
+	public void note(TFEvent e) {
+		
+		ActDB db=getModel().getDB();
+		if (db==null || db.size()==0)
+		{
+			numLabel.setForeground(new Color(245,245,245));//Color.gray);
+			numLabel.setText("No data");
+			return;
+		}
+		int numTotal=db.size();
+		ActList acts=getModel().getActs();
+		int numShown=acts.size();
+		filterLabel.setText(numShown<numTotal ? " Filters applied" : " Not Filtering");
+		filterLabel.setForeground(numShown==numTotal ? Color.lightGray : Color.red);
+		numLabel.setForeground(numShown==0 ? Color.red : Color.darkGray);
+		String plural=(numTotal==1 ? "" : "s");
+		if (numShown==numTotal)
+			numLabel.setText(" Showing All "+niceFormat.format(numTotal)+" Event"+plural);
+		else
+			numLabel.setText(" Showing "+niceFormat.format(numShown)
+					+" / "+niceFormat.format(numTotal)+" Event"+ plural);
+		repaint();
+	}
+
+}
diff --git a/timeflow/app/ui/WaitingDialog.java b/timeflow/app/ui/WaitingDialog.java
new file mode 100755
index 0000000..9ac2736
--- /dev/null
+++ b/timeflow/app/ui/WaitingDialog.java
@@ -0,0 +1,56 @@
+package timeflow.app.ui;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.*;
+
+public class WaitingDialog extends JFrame {
+	
+	Timer timer;
+	
+	public static void main(String[] args)
+	{
+		new WaitingDialog("Testing", "Hello, world!");
+	}
+		
+	public WaitingDialog(String title, String message)
+	{
+		super(title);
+		Throbber throbber=new Throbber();
+		timer=new Timer(50, throbber);		
+		getContentPane().setLayout(new FlowLayout(FlowLayout.LEFT));
+		getContentPane().add(throbber);
+		getContentPane().add(new JLabel(message));
+		setBounds(400,400,300,150);
+		setVisible(true);
+		timer.start();
+	}
+	
+	public void stop()
+	{
+		timer.stop();
+		setVisible(false);
+	}
+	
+	class Throbber extends JPanel implements ActionListener
+	{
+		int count=0;
+		public void paintComponent(Graphics g)
+		{
+			int w=getSize().width, h=getSize().height;
+			int c=count%256;
+			g.setColor(new Color(c,c,c));
+			g.fillRect(0,0,w,h);
+		}
+		@Override
+		public void actionPerformed(ActionEvent e) {
+			repaint();
+			count+=10;
+		}
+		
+		public Dimension getPreferredSize()
+		{
+			return new Dimension(30,100);
+		}
+	}
+}
diff --git a/timeflow/app/ui/filter/BabyHistogram.java b/timeflow/app/ui/filter/BabyHistogram.java
new file mode 100755
index 0000000..fb23aac
--- /dev/null
+++ b/timeflow/app/ui/filter/BabyHistogram.java
@@ -0,0 +1,274 @@
+package timeflow.app.ui.filter;
+
+import javax.swing.*;
+
+import timeflow.data.time.Interval;
+import timeflow.model.Display;
+
+
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseMotionAdapter;
+import java.text.*;
+
+
+public class BabyHistogram extends JPanel {
+	private int[] buckets;
+	private double[] x;
+	private double min, max;
+	private int numDefined;
+	private int maxBucket;
+	private double value;
+	private static final DecimalFormat df=new DecimalFormat("###,###,###,###.##");
+	
+	
+	Point mouseHit=new Point();
+	Point mouse=new Point(-1,0);
+	enum Modify {START, END, POSITION, NONE};
+	Modify change=Modify.NONE;
+	Rectangle startRect=new Rectangle(-1,-1,0,0);
+	Rectangle endRect=new Rectangle(-1,-1,0,0);
+	Rectangle positionRect=new Rectangle(-1,-1,0,0);
+	Color sidePlain=Color.orange;
+	Color sideMouse=new Color(230,100,0);
+	double relLow=0, relHigh=1, originalLow, originalHigh;
+	
+	public void setRelRange(double relLow, double relHigh)
+	{
+		this.relLow=Math.max(0,relLow);
+		this.relHigh=Math.min(1,relHigh);
+		repaint();
+	}
+
+	public void setTrueRange(double low, double high)
+	{
+		double span=max-min;
+		if (span<=0) // nothing much to do...
+			return;
+		
+		setRelRange((low-min)/span, (high-min)/span);	
+	}
+	
+	public boolean isEverything()
+	{
+		return relLow==0 && relHigh==1;
+	}
+	
+	public double getLow()
+	{
+		return abs(relLow);
+	}
+	
+	public double getHigh()
+	{
+		return abs(relHigh);
+	}
+	
+	private double abs(double x)
+	{
+		return x*max+(1-x)*min;
+	}
+
+	public BabyHistogram(final Runnable changeAction)
+	{
+		
+		addMouseListener(new MouseAdapter() {
+
+			@Override
+			public void mousePressed(MouseEvent e) {
+				int mx=e.getX();
+				int my=e.getY();
+				int lx=lowX(), hx=highX();
+			
+				int ox=0;
+				if (Math.abs(mx-lx)<Math.abs(mx-hx))
+				{
+					change=Modify.START;
+					ox=lx;
+				}
+				else
+				{
+					change=Modify.END;
+					ox=hx;
+				}
+				mouseHit.setLocation(ox,my);
+				mouse.setLocation(mx,my);
+				originalLow=relLow;
+				originalHigh=relHigh;
+				repaint();
+			}
+
+			@Override
+			public void mouseReleased(MouseEvent e) {
+				change=Modify.NONE;
+				repaint();
+			}});
+		addMouseMotionListener(new MouseMotionAdapter() {
+
+			@Override
+			public void mouseDragged(MouseEvent e) {
+				
+				if (change==Modify.NONE)
+					return;
+				mouse.setLocation(e.getX(), e.getY());
+				int mouseDiff=mouse.x-mouseHit.x;
+				double relDiff=mouseDiff/(double)getSize().width;
+				switch (change)
+				{
+					case POSITION: 						
+							relLow=originalLow+relDiff;
+							relHigh=originalHigh+relDiff;
+
+						break;
+					case START: relLow=originalLow+relDiff;
+						break;
+					case END: relHigh=originalHigh+relDiff;
+				}
+				relLow=Math.max(0, relLow);
+				relHigh=Math.min(1, relHigh);
+				changeAction.run();
+				repaint();
+			}
+		});
+
+	}
+	
+	public void setData(double[] x)
+	{
+		relLow=0;
+		relHigh=1;
+		
+		this.x=x;
+		int n=x.length;
+		
+		// do some quick checks on the data.
+		boolean positive=true;
+		min=Double.NaN;
+		max=Double.NaN;
+		numDefined=0;
+		for (int i=0; i<n; i++)
+		{
+			double m=x[i];
+			if (!Double.isNaN(m))
+			{
+				numDefined++;
+				positive &= m>0;
+				if (Double.isNaN(min))
+				{
+					min=m;
+					max=m;
+					value=m;
+				}
+				else
+				{
+					min=Math.min(m, min);
+					max=Math.max(m, max);
+				}
+			}
+		}
+		
+		if (numDefined==0)
+			return;
+		if (min==max)
+		{
+			buckets=new int[1];
+			buckets[0]=numDefined;
+			maxBucket=numDefined;
+			return;
+		}
+		int numBuckets=(int)Math.min(50, 2*Math.sqrt(numDefined));
+		buckets=new int[numBuckets];
+		maxBucket=0;
+		for (int i=0; i<n; i++)
+		{
+			if (!Double.isNaN(x[i]))
+			{
+				int b=(int)((numBuckets-1)*(x[i]-min)/(max-min));
+				buckets[b]++;
+				maxBucket=Math.max(maxBucket, buckets[b]);
+			}
+		}
+	}
+	
+	public void paintComponent(Graphics g)
+	{
+		int w=getSize().width;
+		int h=getSize().height;
+		g.setColor(Color.white);
+		g.fillRect(0,0,w,h);
+		
+		if (x==null)
+		{			
+			say(g, "No data");
+			return;
+		}
+		
+		if (x.length==0)
+		{
+			say(g, "No values");
+			return;
+		}
+		
+		if (numDefined==0)
+		{
+			say(g, "No defined values");
+			return;
+		}
+		
+		int n=buckets.length;
+		if (n==1)
+		{
+			say(g, "All defined vals = "+df.format(value));
+			return;
+		}
+		
+		// wow, if we got here we really have a histogram and not a degenerate mess!
+		
+		Color bar=Display.barColor;
+		g.setColor(bar);
+		for (int i=0; i<n; i++)
+		{
+			int x1=(i*w)/n;
+			int x2=((i+1)*w)/n;
+			int y1=h-(buckets[i]*h)/maxBucket;
+			if (buckets[i]>0 && y1>h-2)
+				y1=h-2;
+			g.fillRect(x1,y1,x2-x1-1,h-y1);
+		}
+		
+		// now draw thumb.
+		
+		int thumb1=lowX();
+		int thumb2=highX();
+		g.setColor(Color.black);
+		g.drawLine(thumb1,0,thumb1,h);
+		g.drawLine(thumb2,0,thumb2,h);
+		g.setColor(new Color(235,235,235,160));
+		g.fillRect(0,0,thumb1,h);
+		g.fillRect(thumb2+1,0,w-thumb2-1,h);
+		g.setColor(Color.lightGray);
+		g.drawRect(0,0,w-1,h-1);
+	}
+	
+	int lowX()
+	{
+		return (int)((getSize().width-1)*relLow);
+	}
+	
+	int highX()
+	{
+		return (int)((getSize().width-1)*relHigh);
+	}
+	
+	void say(Graphics g, String s)
+	{
+		g.setColor(Color.gray);
+		g.drawString(s,5,getSize().height-5);
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		return new Dimension(200,60);
+	}
+}
diff --git a/timeflow/app/ui/filter/FilterCategoryPanel.java b/timeflow/app/ui/filter/FilterCategoryPanel.java
new file mode 100755
index 0000000..9207469
--- /dev/null
+++ b/timeflow/app/ui/filter/FilterCategoryPanel.java
@@ -0,0 +1,164 @@
+package timeflow.app.ui.filter;
+
+import timeflow.util.*;
+
+import javax.swing.*;
+
+import timeflow.data.db.*;
+import timeflow.data.db.filter.*;
+import timeflow.model.ModelPanel;
+
+import java.awt.*;
+import java.awt.event.*;
+
+public class FilterCategoryPanel extends FilterDefinitionPanel 
+{
+	public JList dataList=new JList();
+	Field field;
+	
+	public FilterCategoryPanel(final Field field, final ModelPanel parent)
+	{
+		this(field.getName(), field, parent);
+	}
+	
+	public FilterCategoryPanel(String title, final Field field, final ModelPanel parent)
+	{
+		this.field=field;
+		setLayout(new BorderLayout());
+		setBackground(Color.white);
+		setBorder(BorderFactory.createEmptyBorder(0,5,0,5));
+
+		add(new FilterTitle(title, field, parent, true), BorderLayout.NORTH);
+		
+		
+		JScrollPane scroller=new JScrollPane(dataList);
+		scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+		scroller.setBorder(null);
+		add(scroller, BorderLayout.CENTER);
+		dataList.setForeground(Color.darkGray);
+		dataList.setSelectionForeground(Color.black);
+		dataList.setSelectionBackground(new Color(220,235,255));
+		dataList.setFont(parent.getModel().getDisplay().small());
+		scroller.getVerticalScrollBar().setBackground(Color.white);
+		
+		
+		// ok, the following is ugly code to insert a new mouselistener
+		// that lets the user deselect items when they are clicked.
+		// i tried a bunch of stuff but this is all that would work--
+		// and searching the web yielded only solutions similar to this.
+		// also, there's a weird dance with consuming/not consuming events
+		// that is designed to allow a certain kind of multi-selection behavior
+		// with the mouse, while letting you scroll through items one at a time
+		// with the keyboard. this was the product of a long series of
+		// conversations with target users.
+		MouseListener[] old = dataList.getMouseListeners();
+		for (MouseListener m: old)
+		   dataList.removeMouseListener(m);
+		
+		dataList.addMouseListener(new MouseAdapter()
+		{
+		   public void mousePressed(MouseEvent e)
+		   {
+			  if (e.isControlDown() || e.isMetaDown() || e.isShiftDown())
+				  return;
+		      final int index = dataList.locationToIndex(e.getPoint());
+		      if (dataList.isSelectedIndex(index))
+		      {
+		         SwingUtilities.invokeLater(new Runnable()
+		         {
+		            public void run()
+		            {
+		               dataList.removeSelectionInterval(index, index);
+		              
+		            }
+		         });
+		         e.consume();
+		      }
+		      else
+		      {
+		    	  SwingUtilities.invokeLater(new Runnable()
+			         {
+			            public void run()
+			            {
+			            	dataList.addSelectionInterval(index, index);
+			              
+			            }
+			         });		    	  
+		    	  e.consume();
+		      }
+		   }
+		});
+
+		for (MouseListener m: old)
+		   dataList.addMouseListener(m);
+		
+		dataList.setCellRenderer(new DefaultListCellRenderer() {
+			@Override
+			public Component getListCellRendererComponent(JList list,
+					Object value, int index, boolean isSelected,
+					boolean cellHasFocus) {
+				Component c=super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+				if (field==parent.getModel().getColorField())
+				{
+					String text=value.toString();
+					int n=text.lastIndexOf('-');
+					if (n>1)
+						text=text.substring(0,n-1);
+					c.setForeground(parent.getModel().getDisplay().makeColor(text));
+				}
+				return c;
+			}});
+			
+	}
+
+	public void setData(Bag<String> data)
+	{
+		dataList.removeAll();
+		java.util.List<String> items=data.list();
+		String[] s=(String[])items.toArray(new String[0]);
+		for (int i=0; i<s.length; i++)
+		{
+			int num=data.num(s[i]);
+			if (s[i]==null || s[i].length()==0)
+				s[i]="(missing)";
+			s[i]+=" - "+num;
+		}
+		dataList.setListData(s);
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		return new Dimension(200,200);
+	}
+
+	@Override
+	public ActFilter defineFilter() {
+		Object[] o=dataList.getSelectedValues();	
+		if (o==null || o.length==0)
+			return null;
+		
+		int n=o.length;
+		String[] s=new String[n];
+		for (int i=0; i<n; i++)
+		{
+			String w=(String)o[i];
+			int m=w.lastIndexOf('-');
+			s[i]=w.substring(0, m-1);
+			if ("(missing)".equals(s[i]))
+				s[i]="";
+		}
+		
+		if (s.length==1)
+			return new FieldValueFilter(field, s[0]);
+		FieldValueSetFilter f=new FieldValueSetFilter(field);
+		for (int i=0; i<s.length; i++)
+			f.addValue(s[i]);
+		return f;
+	}
+
+	@Override
+	public void clearFilter() {
+		dataList.clearSelection();
+	}
+	
+}
diff --git a/timeflow/app/ui/filter/FilterControlPanel.java b/timeflow/app/ui/filter/FilterControlPanel.java
new file mode 100755
index 0000000..fafdb7e
--- /dev/null
+++ b/timeflow/app/ui/filter/FilterControlPanel.java
@@ -0,0 +1,208 @@
+package timeflow.app.ui.filter;
+
+import timeflow.model.*;
+import timeflow.app.ui.StatusPanel;
+import timeflow.data.db.*;
+import timeflow.data.db.filter.*;
+import timeflow.data.time.RoughTime;
+
+import timeflow.util.*;
+
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import java.awt.*;
+
+public class FilterControlPanel extends ModelPanel
+{
+	FacetSubpanel inside=new FacetSubpanel();
+	SearchPanel searchPanel;
+	boolean inverted=false;
+	JMenu menuToSyncWith;
+	
+	
+	public FilterControlPanel(TFModel model, JMenu menuToSyncWith)
+	{
+		super(model);
+		this.menuToSyncWith=menuToSyncWith;
+		searchPanel=new SearchPanel(model, this);
+		setLayout(new BorderLayout());
+		setBackground(Color.white);
+		JPanel top=new JPanel();
+		top.setBackground(Color.white);
+		top.setLayout(new BorderLayout());
+		top.setBackground(Color.white);
+		
+		top.add(new StatusPanel(model, this), BorderLayout.NORTH);
+		top.add(searchPanel, BorderLayout.CENTER);
+	
+		add(top, BorderLayout.NORTH);
+		add(inside, BorderLayout.CENTER);
+	}
+	
+	void setInverted(boolean inverted)
+	{
+		this.inverted=inverted;
+		makeFilter();
+	}
+	
+	void makeFilter()
+	{
+		AndFilter filter=new AndFilter();
+		String s=searchPanel.entry.getText();
+		if (s.length()>0)
+			filter.and(new StringMatchFilter(getModel().getDB(), s, true));
+		for (FilterDefinitionPanel f: inside.facetTable.values())
+			filter.and(f.defineFilter());
+		getModel().setFilter(inverted ? new NotFilter(filter) : filter, this);
+	}
+	
+	public void clearFilters()
+	{
+
+		searchPanel.entry.setText("");
+		for (FilterDefinitionPanel d: inside.facetTable.values())
+			d.clearFilter();
+		inverted=false;
+		searchPanel.invert.setSelected(false);
+		for (Field f:getModel().getDB().getFields())
+		{
+			inside.setFacet(f, false);
+		}
+		makeFilter();
+	}
+
+	@Override
+	public void note(TFEvent e) {
+		if (e.affectsSchema())
+		{
+			inside.clearFacets();
+			searchPanel.entry.setText("");
+		}
+	}
+	
+	public void setFacet(Field field, boolean on)
+	{
+		inside.setFacet(field, on);
+		makeFilter();
+	}
+	
+	class FacetSubpanel extends JPanel
+	{	
+		
+		ArrayList<Field> facets=new ArrayList<Field>();
+		HashMap<Field, FilterDefinitionPanel> facetTable=new HashMap<Field, FilterDefinitionPanel>();		
+		
+		FacetSubpanel()
+		{
+			setLayout(null);
+			setBackground(Color.white);
+		}
+		
+		FilterDefinitionPanel makePanel(Field field)
+		{
+			if (field.getType()==Double.class)
+			{
+				final FilterNumberPanel num=new FilterNumberPanel(field, new Runnable() {
+					@Override
+					public void run() {
+						makeFilter();
+					}}, FilterControlPanel.this);
+				num.setData(DBUtils.getValues(getModel().getDB(), field));
+				return num;
+			}
+			
+			if (field.getType()==RoughTime.class)
+			{
+				final FilterDatePanel date=new FilterDatePanel(field, new Runnable() {
+					@Override
+					public void run() {
+						makeFilter();
+					}}, FilterControlPanel.this);
+				date.setData(DBUtils.getValues(getModel().getDB(), field));
+				return date;
+			}
+			
+			final FilterCategoryPanel p= new FilterCategoryPanel(field, FilterControlPanel.this);
+			p.dataList.addListSelectionListener(new ListSelectionListener() {				
+				@Override
+				public void valueChanged(ListSelectionEvent e) {
+					makeFilter();	
+				}
+			});
+			Bag<String> data=DBUtils.countValues(getModel().getDB().all(), field);
+			p.setData(data);
+			return p;
+		}
+		
+		public void clearFacets()
+		{
+			removeAll();
+			facets.clear();
+			facetTable.clear();
+			revalidate();
+			repaint();
+		}
+		
+		public void setFacet(Field field, boolean on)
+		{
+			FilterDefinitionPanel panel=facetTable.get(field);
+			if (on == (panel!=null))
+				return;
+			if (on)
+			{
+				panel=makePanel(field);
+				add(panel);
+				facets.add(field);
+				facetTable.put(field,panel);
+			}
+			else
+			{
+				remove(panel);
+				facets.remove(field);
+				facetTable.remove(field);
+			}
+			
+			doFacetLayout();
+			if (menuToSyncWith!=null)
+				for (int i=0; i<menuToSyncWith.getItemCount(); i++)
+				{
+					JCheckBoxMenuItem item=(JCheckBoxMenuItem)menuToSyncWith.getItem(i);
+					if (item.getText().equals(field.getName()))
+					{
+						item.setSelected(on);
+					}
+				}
+			revalidate();
+			repaint();
+		}
+		
+		public void setBounds(int x, int y, int w, int h)
+		{
+			super.setBounds(x,y,w,h);
+			doFacetLayout();
+		}
+		
+		void doFacetLayout()
+		{
+			int w=getSize().width, h=getSize().height;
+			int goodSize=0;
+			for (Field f: facets)
+			{
+				FilterDefinitionPanel p=facetTable.get(f);
+				goodSize+=p.getPreferredSize().height;
+			}
+			int top=0;
+			for (Field f: facets)
+			{
+				FilterDefinitionPanel p=facetTable.get(f);
+				int pref=p.getPreferredSize().height;
+				int panelHeight=(goodSize<= h ? pref : (pref*h)/goodSize);
+				p.setBounds(0,top,w,panelHeight);
+				top+=panelHeight;
+			}
+		}
+	}
+}
diff --git a/timeflow/app/ui/filter/FilterDatePanel.java b/timeflow/app/ui/filter/FilterDatePanel.java
new file mode 100755
index 0000000..17c1801
--- /dev/null
+++ b/timeflow/app/ui/filter/FilterDatePanel.java
@@ -0,0 +1,173 @@
+package timeflow.app.ui.filter;
+
+import timeflow.data.db.*;
+import timeflow.data.db.filter.*;
+import timeflow.model.*;
+
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.text.*;
+import java.util.Date;
+
+import javax.swing.*;
+
+// in theory it should be easy to refactor this to share code with
+// NumberFilterPanel.
+// but, i'm not sure how to do it in a way that doesn't make the code
+// seem too complicated.
+
+public class FilterDatePanel  extends FilterDefinitionPanel 
+{
+	BabyHistogram histogram;
+	Field field;
+	JTextField startEntry;
+	JTextField endEntry;
+	JCheckBox nullBox;
+	Runnable action;
+	SimpleDateFormat df=new SimpleDateFormat("MMM dd yyyy");
+	
+	public FilterDatePanel(final Field field, final Runnable action, final FilterControlPanel parent)
+	{
+		this.field=field;
+		this.action=action;
+		setLayout(new BorderLayout());
+		setBorder(BorderFactory.createEmptyBorder(0,5,0,5));
+		setBackground(Color.white);
+		add(new FilterTitle(field, parent, false), BorderLayout.NORTH);
+		
+		Runnable fullAction=new Runnable()
+		{
+			public void run()
+			{
+				startEntry.setText(format(histogram.getLow()));
+				endEntry.setText(format(histogram.getHigh()));
+				action.run();
+			}
+		};
+		
+		histogram=new BabyHistogram(fullAction);
+		
+		add(histogram, BorderLayout.CENTER);
+		
+		JPanel bottomStuff=new JPanel();
+		bottomStuff.setLayout(new GridLayout(2,1));
+		add(bottomStuff, BorderLayout.SOUTH);
+		
+		JPanel lowHighPanel=new JPanel();
+		bottomStuff.add(lowHighPanel);
+		lowHighPanel.setBackground(Color.white);
+		lowHighPanel.setLayout(new BorderLayout());
+		Font small=parent.getModel().getDisplay().small();
+		
+		startEntry=new JTextField(7);
+		startEntry.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				setLowFromText();
+				action.run();
+			}});
+		lowHighPanel.add(startEntry, BorderLayout.WEST);
+		startEntry.setFont(small);
+		
+		JLabel rangeLabel=new JLabel("to", JLabel.CENTER);
+		rangeLabel.setForeground(Color.gray);
+		rangeLabel.setFont(small);
+		lowHighPanel.add(rangeLabel, BorderLayout.CENTER);
+		endEntry=new JTextField(7);
+		lowHighPanel.add(endEntry, BorderLayout.EAST);
+		endEntry.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				setHighFromText();
+				action.run();
+			}});
+		endEntry.setFont(small);
+		
+		nullBox=new JCheckBox("Include Missing Values");
+		nullBox.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				action.run();
+			}});
+		bottomStuff.add(nullBox);
+		bottomStuff.setBackground(Color.white);
+		nullBox.setBackground(Color.white);
+		nullBox.setForeground(Color.gray);
+		nullBox.setFont(small);
+		
+	}
+	
+	String format(double x)
+	{
+		Date date=new Date((long)x);
+		return df.format(date);
+	}
+	
+	void setLowFromText()
+	{
+		try
+		{
+			long low=df.parse(startEntry.getText()).getTime();
+			long high=(long)histogram.getHigh();
+			if (low>high)
+			{
+				high=low;
+				endEntry.setText(startEntry.getText());
+			}
+			histogram.setTrueRange(low,high);
+			
+		}
+		catch (Exception e)
+		{
+			
+		}
+	}
+	
+	
+	void setHighFromText()
+	{
+		try
+		{
+			long high=df.parse(endEntry.getText()).getTime();
+			double low=(long)histogram.getLow();
+			if (low>high)
+			{
+				low=high;
+				startEntry.setText(endEntry.getText());
+			}
+			histogram.setTrueRange(low,high);
+			
+		}
+		catch (Exception e)
+		{
+			
+		}		
+	}
+
+	public void setData(double[] data)
+	{
+		histogram.setData(data);
+		startEntry.setText(format(histogram.getLow()));
+		endEntry.setText(format(histogram.getHigh()));
+		repaint();
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		return new Dimension(200,160);
+	}
+
+	@Override
+	public ActFilter defineFilter() {
+		long low=(long)histogram.getLow();
+		long high=(long)histogram.getHigh();
+		boolean acceptNull=nullBox.isSelected();
+		return new TimeIntervalFilter(low, high, acceptNull, field);
+	}
+
+	@Override
+	public void clearFilter() {
+		histogram.setRelRange(0, 1);
+	}
+}
\ No newline at end of file
diff --git a/timeflow/app/ui/filter/FilterDefinitionPanel.java b/timeflow/app/ui/filter/FilterDefinitionPanel.java
new file mode 100755
index 0000000..0823209
--- /dev/null
+++ b/timeflow/app/ui/filter/FilterDefinitionPanel.java
@@ -0,0 +1,10 @@
+package timeflow.app.ui.filter;
+
+import timeflow.data.db.filter.ActFilter;
+
+import javax.swing.*;
+
+public abstract class FilterDefinitionPanel extends JPanel {
+	public abstract ActFilter defineFilter();
+	public abstract void clearFilter();
+}
diff --git a/timeflow/app/ui/filter/FilterNumberPanel.java b/timeflow/app/ui/filter/FilterNumberPanel.java
new file mode 100755
index 0000000..e950bb1
--- /dev/null
+++ b/timeflow/app/ui/filter/FilterNumberPanel.java
@@ -0,0 +1,171 @@
+package timeflow.app.ui.filter;
+
+import timeflow.data.db.*;
+import timeflow.data.db.filter.*;
+import timeflow.model.*;
+
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.*;
+
+import timeflow.util.*;
+
+public class FilterNumberPanel  extends FilterDefinitionPanel 
+{
+	BabyHistogram histogram;
+	Field field;
+	JTextField lowEntry;
+	JTextField highEntry;
+	JCheckBox nullBox;
+	Runnable action;
+	
+	public FilterNumberPanel(final Field field, final Runnable action, final FilterControlPanel parent)
+	{
+		this.field=field;
+		this.action=action;
+		setLayout(new BorderLayout());
+		setBackground(Color.white);
+		setBorder(BorderFactory.createEmptyBorder(0,5,0,5));
+		
+		
+		setBackground(Color.white);
+		add(new FilterTitle(field, parent, false), BorderLayout.NORTH);
+		
+		Runnable fullAction=new Runnable()
+		{
+			public void run()
+			{
+				lowEntry.setText(format(histogram.getLow()));
+				highEntry.setText(format(histogram.getHigh()));
+				action.run();
+			}
+		};
+		
+		histogram=new BabyHistogram(fullAction);
+		
+		add(histogram, BorderLayout.CENTER);
+		
+		JPanel bottomStuff=new JPanel();
+		bottomStuff.setLayout(new GridLayout(2,1));
+		add(bottomStuff, BorderLayout.SOUTH);
+		bottomStuff.setBackground(Color.white);
+		
+		JPanel lowHighPanel=new JPanel();
+		bottomStuff.add(lowHighPanel);
+		lowHighPanel.setBackground(Color.white);
+		
+		lowHighPanel.setLayout(new BorderLayout());
+		
+		Font small=parent.getModel().getDisplay().small();
+		lowEntry=new JTextField(7);
+		lowEntry.setFont(small);
+		lowEntry.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				setLowFromText();
+				action.run();
+			}});
+		lowHighPanel.add(lowEntry, BorderLayout.WEST);
+		JLabel rangeLabel=new JLabel("to", JLabel.CENTER);
+		
+		rangeLabel.setFont(small);
+		rangeLabel.setForeground(Color.gray);
+		lowHighPanel.add(rangeLabel, BorderLayout.CENTER);
+		highEntry=new JTextField(7);
+		lowHighPanel.add(highEntry, BorderLayout.EAST);
+		highEntry.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				setHighFromText();
+				action.run();
+			}});
+		highEntry.setFont(small);
+		
+		nullBox=new JCheckBox("Include Missing Values");
+		nullBox.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				action.run();
+			}});
+		bottomStuff.add(nullBox);
+		nullBox.setBackground(Color.white);
+		nullBox.setForeground(Color.gray);
+		nullBox.setFont(small);
+	}
+	
+	String format(double x)
+	{
+		if (Math.abs(x)>10)
+			return Display.format(Math.round(x));
+		return Display.format(x);
+	}
+	
+	void setLowFromText()
+	{
+		try
+		{
+			double low=Double.parseDouble(lowEntry.getText());
+			double high=histogram.getHigh();
+			if (low>high)
+			{
+				high=low;
+				highEntry.setText(lowEntry.getText());
+			}
+			histogram.setTrueRange(low,high);
+			
+		}
+		catch (Exception e)
+		{
+			
+		}
+	}
+	
+	
+	void setHighFromText()
+	{
+		try
+		{
+			double high=Double.parseDouble(highEntry.getText());
+			double low=histogram.getLow();
+			if (low>high)
+			{
+				low=high;
+				lowEntry.setText(highEntry.getText());
+			}
+			histogram.setTrueRange(low,high);
+			
+		}
+		catch (Exception e)
+		{
+			
+		}		
+	}
+
+	public void setData(double[] data)
+	{
+		histogram.setData(data);
+		lowEntry.setText(Display.format(histogram.getLow()));
+		highEntry.setText(Display.format(histogram.getHigh()));
+		repaint();
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		return new Dimension(200,160);
+	}
+
+	@Override
+	public ActFilter defineFilter() {
+		double low=histogram.getLow();
+		double high=histogram.getHigh();
+		boolean acceptNull=nullBox.isSelected();
+		return new NumericRangeFilter(field, low, high, acceptNull);
+	}
+
+	@Override
+	public void clearFilter() {
+		histogram.setRelRange(0, 1);
+	}
+}
\ No newline at end of file
diff --git a/timeflow/app/ui/filter/FilterTitle.java b/timeflow/app/ui/filter/FilterTitle.java
new file mode 100755
index 0000000..57b1dcc
--- /dev/null
+++ b/timeflow/app/ui/filter/FilterTitle.java
@@ -0,0 +1,52 @@
+package timeflow.app.ui.filter;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import javax.swing.*;
+
+import timeflow.app.ui.DottedLine;
+import timeflow.data.db.Field;
+import timeflow.model.ModelPanel;
+
+import timeflow.util.*;
+
+public class FilterTitle extends JPanel {
+	public FilterTitle(final Field field, final ModelPanel parent, boolean dots)
+	{
+		this(field.getName(), field, parent, dots);
+	}
+	public FilterTitle(String title, final Field field, final ModelPanel parent, boolean dots)
+	{
+		JPanel top=new JPanel();
+		top.setBackground(Color.white);
+		top.setLayout(new BorderLayout());
+		JLabel label=new JLabel(title);
+		JPanel pad=new Pad(30,30);
+		pad.setBackground(Color.white);
+		top.add(pad, BorderLayout.NORTH);
+		top.add(label, BorderLayout.CENTER);
+		label.setBackground(Color.white);
+		
+		if (parent instanceof FilterControlPanel)
+		{
+			ImageIcon redX=new ImageIcon("images/red_circle.gif");
+			JLabel close=new JLabel(redX);
+			close.setBackground(Color.white);
+			top.add(close, BorderLayout.EAST);
+			close.addMouseListener(new MouseAdapter() {
+				@Override
+				public void mousePressed(MouseEvent e) {
+					((FilterControlPanel)parent).setFacet(field, false);
+				}
+				});
+		}
+		setLayout(new BorderLayout());
+		add(top, BorderLayout.CENTER);
+		
+		if (dots)
+			add(new DottedLine(), BorderLayout.SOUTH);
+	}
+}
diff --git a/timeflow/app/ui/filter/SearchPanel.java b/timeflow/app/ui/filter/SearchPanel.java
new file mode 100755
index 0000000..eca2702
--- /dev/null
+++ b/timeflow/app/ui/filter/SearchPanel.java
@@ -0,0 +1,52 @@
+package timeflow.app.ui.filter;
+
+import javax.swing.*;
+import java.awt.event.*;
+
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+import timeflow.model.*;
+
+import java.awt.*;
+
+public class SearchPanel extends ModelPanel {
+
+	JTextField entry;
+    JCheckBox invert;
+	
+	public SearchPanel(TFModel model, final FilterControlPanel f) {
+		super(model);
+		setBackground(Color.white);
+		setBorder(BorderFactory.createEmptyBorder(15, 5,0,0));
+		setLayout(new GridLayout(1,1));
+		JPanel top=new JPanel();
+		top.setLayout(new BorderLayout());
+		add(top);
+		top.setBackground(Color.white);
+		JLabel label=model.getDisplay().label("Search");
+		top.add(label, BorderLayout.WEST);
+		entry=new JTextField(8);
+		top.add(entry, BorderLayout.CENTER);
+		entry.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				f.makeFilter();
+			}});
+
+		invert=new JCheckBox("Invert", false);
+		top.add(invert, BorderLayout.EAST);
+		invert.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				f.setInverted(invert.isSelected());
+			}});
+		invert.setFont(f.getModel().getDisplay().small());
+		invert.setForeground(Color.gray);
+		invert.setBackground(Color.white);
+	}
+
+	@Override
+	public void note(TFEvent e) {
+	}
+
+}
diff --git a/timeflow/data/analysis/DBAnalysis.java b/timeflow/data/analysis/DBAnalysis.java
new file mode 100755
index 0000000..ff571b9
--- /dev/null
+++ b/timeflow/data/analysis/DBAnalysis.java
@@ -0,0 +1,14 @@
+package timeflow.data.analysis;
+
+import timeflow.data.db.*;
+
+public interface DBAnalysis {
+	
+	public enum InterestLevel {IGNORE, BORING, INTERESTING, VERY_INTERESTING};
+	
+	public String getName();
+	public InterestLevel perform(ActList acts);	
+	public String[] getResultDescription();
+	
+	
+}
diff --git a/timeflow/data/analysis/FieldAnalysis.java b/timeflow/data/analysis/FieldAnalysis.java
new file mode 100755
index 0000000..5ecfd8e
--- /dev/null
+++ b/timeflow/data/analysis/FieldAnalysis.java
@@ -0,0 +1,13 @@
+package timeflow.data.analysis;
+
+import timeflow.data.db.*;
+
+public interface FieldAnalysis {
+		
+	public String getName();
+	public boolean canHandleType(Class type);
+	public DBAnalysis.InterestLevel perform(ActList acts, Field field);	
+	public String[] getResultDescription();
+	
+	
+}
diff --git a/timeflow/data/analysis/FrequencyAnalysis.java b/timeflow/data/analysis/FrequencyAnalysis.java
new file mode 100755
index 0000000..cad3089
--- /dev/null
+++ b/timeflow/data/analysis/FrequencyAnalysis.java
@@ -0,0 +1,76 @@
+package timeflow.data.analysis;
+
+import timeflow.util.*;
+import timeflow.data.time.*;
+import timeflow.data.analysis.DBAnalysis.InterestLevel;
+import timeflow.data.db.*;
+import java.util.*;
+
+public class FrequencyAnalysis implements FieldAnalysis {
+	String[] description;
+	
+	@Override
+	public String getName() {
+		return "Frequency Of Values";
+	}
+
+	@Override
+	public String[] getResultDescription() {
+		return description;
+	}
+
+	@Override
+	public InterestLevel perform(ActList acts, Field field) {
+		Bag<Object> bag=new Bag<Object>();
+		if (field.getType()==String[].class)
+		{
+			for (Act a: acts)
+			{
+				String[] tags=a.getTextList(field);
+				if (tags!=null)
+					for (String tag:tags)
+						bag.add(tag);
+			}
+		}
+		else
+			for (Act a: acts)
+				bag.add(a.get(field));
+		
+		int numItems=acts.size();
+		int numDistinctVals=bag.size();
+		int numNullVals=bag.num(null)+bag.num("");
+		
+		if (numItems==numDistinctVals)
+			description=new String[] {"All values are defined and unique."};
+		else if (numItems==numDistinctVals+numNullVals-1)
+			description=new String[] {"All defined values are unique."};
+		else if (numDistinctVals==1)
+			description=new String[] {"This field is always equal to "+string(bag.list().get(0))};
+		else if (numDistinctVals<4)
+		{
+			List<Object> all=bag.list();
+			description=new String[] {"This field takes only "+all.size()+" values.",
+				"which are: "+all};
+		}
+		else
+		{
+			List<Object> all=bag.list();
+			description=new String[] {"There are "+numDistinctVals+" distinct values.",
+					"Most common: \""+string(all.get(0))+"\", occurring "+bag.num(all.get(0))+" times."};
+		}
+		return InterestLevel.INTERESTING;
+	}
+	
+	private static String[] empty=new String[0];
+	static String string(Object o)
+	{
+		if (o==null || "".equals(o) || empty.equals(o))
+			return "[missing]";
+		return o.toString();
+	}
+
+	@Override
+	public boolean canHandleType(Class type) {
+		return type==Double.class || type==String.class || type==String[].class;
+	}
+}
diff --git a/timeflow/data/analysis/MissingValueAnalysis.java b/timeflow/data/analysis/MissingValueAnalysis.java
new file mode 100755
index 0000000..abbaa51
--- /dev/null
+++ b/timeflow/data/analysis/MissingValueAnalysis.java
@@ -0,0 +1,44 @@
+package timeflow.data.analysis;
+
+import timeflow.data.analysis.DBAnalysis.*;
+import timeflow.data.db.*;
+import timeflow.data.db.filter.*;
+
+public class MissingValueAnalysis implements FieldAnalysis {
+
+	int numNull;
+	int percent;
+	
+	@Override
+	public String getName() {
+		return "Missing/Blank Values";
+	}
+
+	@Override
+	public String[] getResultDescription() {
+		String s;
+		if (numNull==0)
+			s="No missing values";
+		else if (numNull==1)
+			s= "One missing value";
+		else
+			s=numNull+" missing values:  "+percent+"%";
+		return new String[] {s};
+	}
+
+	@Override
+	public InterestLevel perform(ActList acts, Field field) {
+		numNull=DBUtils.count(acts, new MissingValueFilter(field));
+		percent=(int)Math.round(100*numNull/(double)acts.size());
+		if (numNull==0)
+			return InterestLevel.IGNORE;
+		if (numNull<5)
+			return InterestLevel.VERY_INTERESTING;
+		return InterestLevel.INTERESTING;
+	}
+
+	@Override
+	public boolean canHandleType(Class type) {
+		return true;
+	}
+}
diff --git a/timeflow/data/analysis/RangeDateAnalysis.java b/timeflow/data/analysis/RangeDateAnalysis.java
new file mode 100755
index 0000000..955a62b
--- /dev/null
+++ b/timeflow/data/analysis/RangeDateAnalysis.java
@@ -0,0 +1,62 @@
+package timeflow.data.analysis;
+
+import java.sql.Date;
+
+import timeflow.data.analysis.DBAnalysis.*;
+import timeflow.data.db.*;
+import timeflow.data.db.filter.*;
+import timeflow.data.time.RoughTime;
+
+public class RangeDateAnalysis implements FieldAnalysis {
+
+	String[] description;
+	
+	@Override
+	public String getName() {
+		return "Date Range";
+	}
+
+	@Override
+	public String[] getResultDescription() {
+		return description;
+	}
+
+	@Override
+	public InterestLevel perform(ActList acts, Field field) {
+		long low=0;
+		long high=0;
+
+		boolean defined=false;
+		for (Act a: acts)
+		{
+			if (a.get(field)==null)
+				continue;
+			long x=a.getTime(field).getTime();
+			if (defined)
+			{
+				low=Math.min(low,x);
+				high=Math.max(high, x);
+			} else
+			{
+				defined=true;
+				low=x;
+				high=low;
+			}
+		}
+		if (defined)
+			description= new String[]
+	  		{
+	  			"Lowest value: "+new Date(low),
+	  			"Highest value: "+new Date(high),
+	  		};
+		else
+			description=new String[] {"No values defined."};
+		
+		return InterestLevel.INTERESTING;
+	}
+
+	@Override
+	public boolean canHandleType(Class type) {
+		return type==RoughTime.class;
+	}
+}
diff --git a/timeflow/data/analysis/RangeNumberAnalysis.java b/timeflow/data/analysis/RangeNumberAnalysis.java
new file mode 100755
index 0000000..9148110
--- /dev/null
+++ b/timeflow/data/analysis/RangeNumberAnalysis.java
@@ -0,0 +1,85 @@
+package timeflow.data.analysis;
+
+import timeflow.data.analysis.DBAnalysis.*;
+import timeflow.data.db.*;
+import timeflow.data.db.filter.*;
+import java.text.*;
+
+public class RangeNumberAnalysis implements FieldAnalysis {
+
+	String[] description;
+	static DecimalFormat intFormat=new DecimalFormat("###,###,###,###");
+	static DecimalFormat df=new DecimalFormat("###,###,###,###.##");
+	
+	@Override
+	public String getName() {
+		return "Value Range";
+	}
+
+	@Override
+	public String[] getResultDescription() {
+		return description;
+	}
+
+	@Override
+	public InterestLevel perform(ActList acts, Field field) {
+		double low=0;
+		double high=0;
+		int numZero=0;
+		double sum=0;
+		int numDefined=0;
+
+		boolean defined=false;
+		for (Act a: acts)
+		{
+			if (a.get(field)==null)
+				continue;
+
+			double x=a.getValue(field);
+			numDefined++;
+			sum+=x;
+			
+			if (x==0)
+				numZero++;
+			if (defined)
+			{
+				low=Math.min(low,x);
+				high=Math.max(high, x);
+			} else
+			{
+				defined=true;
+				low=x;
+				high=low;
+			}
+		}
+		if (defined)
+			description= new String[]
+	  		{
+				"Average: "+df.format((sum/numDefined)),
+	  			"Lowest: "+df.format(low),
+	  			"Highest: "+df.format(high),
+	  			"Number of zero values: "+df.format(numZero)
+	  		};
+		else
+			description=new String[] {"No values defined."};
+		
+		return InterestLevel.INTERESTING;
+	}
+	
+	static String format(double x)
+	{
+		
+		if (Math.abs(x)>.1)
+		{
+			if (Math.round(x)-x<.01)
+				return intFormat.format(x);
+			return df.format(x);
+		}
+		return ""+x;
+	}
+
+	@Override
+	public boolean canHandleType(Class type) {
+		return type==Double.class;
+	}
+}
diff --git a/timeflow/data/db/Act.java b/timeflow/data/db/Act.java
new file mode 100755
index 0000000..66eb753
--- /dev/null
+++ b/timeflow/data/db/Act.java
@@ -0,0 +1,25 @@
+package timeflow.data.db;
+
+import timeflow.data.time.*;
+
+import java.net.URL;
+import java.util.*;
+
+public interface Act {
+	
+	public ActDB getDB();
+	
+	public Object get(Field field);
+	public double getValue(Field field);
+	public String getString(Field field);
+	public String[] getTextList(Field field);
+	public RoughTime getTime(Field field);
+	public URL getURL(Field field);
+	
+	public void set(Field field, Object value);
+	public void setText(Field field, String text);
+	public void setTextList(Field field, String[] list);
+	public void setValue(Field field, double value);
+	public void setTime(Field field, RoughTime time);
+	public void setURL(Field field, URL url);
+}
diff --git a/timeflow/data/db/ActComparator.java b/timeflow/data/db/ActComparator.java
new file mode 100755
index 0000000..7632689
--- /dev/null
+++ b/timeflow/data/db/ActComparator.java
@@ -0,0 +1,118 @@
+package timeflow.data.db;
+
+import java.util.*;
+
+import timeflow.data.time.RoughTime;
+
+public abstract class ActComparator implements Comparator<Act> {
+	
+	protected Field field;
+	protected boolean ascending=true;
+	protected String description;
+	
+	
+	private ActComparator(Field field, String description)
+	{
+		this.field=field;
+		this.description=description;
+	}
+	
+	public String getDescription()
+	{
+		return description + (ascending ? "" : " (descending)");
+	}
+	
+	public static ActComparator by(Field field)
+	{
+		Class type=field.getType();
+		if (type==Double.class)
+			return new NumberComparator(field);
+		if (type==String[].class)
+			return new ArrayComparator(field);
+		if (type==RoughTime.class)
+			return new TimeComparator(field);
+		return new StringComparator(field);
+	}
+	
+	static class TimeComparator extends ActComparator
+	{
+		
+		TimeComparator(Field field)
+		{
+			super(field, "by time");
+		}
+
+		@Override
+		public int compare(Act o1, Act o2) {
+			RoughTime a1=o1.getTime(field);
+			RoughTime a2=o2.getTime(field);
+			if (a1==a2)
+				return 0;
+			if (a1==null)
+				return ascending ? 1 : -1;
+			if (a2==null)
+				return ascending ? -1 : 1;
+			int n=a1.compareTo(a2);
+			return ascending ? n : -n;
+		}
+	}
+
+	
+	static class ArrayComparator extends ActComparator
+	{
+		
+		ArrayComparator(Field field)
+		{
+			super(field, "by length of "+field.getName());
+		}
+
+		@Override
+		public int compare(Act o1, Act o2) {
+			int n=length(o1.getTextList(field))-length(o2.getTextList(field));
+			return ascending ? n : -n;
+		}
+		
+		static int length(String[] s)
+		{
+			return s==null ? 0 : s.length;
+		}
+	}
+	
+	static class StringComparator extends ActComparator
+	{
+		
+		StringComparator(Field field)
+		{
+			super(field, "by "+field.getName());
+		}
+
+		@Override
+		public int compare(Act o1, Act o2) {
+			int n=val(o1.getString(field)).toString().compareTo(val(o2.getString(field)).toString());
+			return ascending ? n : -n;
+		}
+		
+		String val(String s)
+		{
+			return s==null ? "" : s;
+		}
+	}
+	
+	static class NumberComparator extends ActComparator
+	{
+		
+		NumberComparator(Field field)
+		{
+			super(field, "by "+field.getName());
+		}
+
+		@Override
+		public int compare(Act o1, Act o2) {
+			double x=o1.getValue(field)-o2.getValue(field);
+			int n=x>0 ? 1 : x<0 ? -1 : 0;
+			return ascending ? n : -n;
+		}
+		
+		
+	}
+}
diff --git a/timeflow/data/db/ActDB.java b/timeflow/data/db/ActDB.java
new file mode 100755
index 0000000..46e4bb1
--- /dev/null
+++ b/timeflow/data/db/ActDB.java
@@ -0,0 +1,30 @@
+package timeflow.data.db;
+
+import java.util.*;
+
+import timeflow.data.db.filter.ActFilter;
+
+public interface ActDB extends Iterable<Act> {
+	
+	public String getSource();
+	public String getDescription();
+	public void setSource(String source);
+	public void setDescription(String description);
+	
+	public List<String> getFieldKeys();
+	public List<Field> getFields();
+	public List<Field> getFields(Class type);
+	public Field addField(String name, Class type);
+	public Field getField(String name);
+	public void deleteField(Field field);
+	public void setAlias(Field field, String name);
+	public void setNewFieldOrder(List<Field> newOrder);
+	public void renameField(Field field, String name);
+
+	public void delete(Act act);
+	public Act createAct();
+	public ActList select(ActFilter filter);
+	public ActList all();
+	public int size();
+	public Act get(int i);
+}
diff --git a/timeflow/data/db/ActList.java b/timeflow/data/db/ActList.java
new file mode 100755
index 0000000..53b9a54
--- /dev/null
+++ b/timeflow/data/db/ActList.java
@@ -0,0 +1,23 @@
+package timeflow.data.db;
+
+import java.util.*;
+
+public class ActList extends ArrayList<Act> {
+	
+	private ActDB db;
+	
+	public ActList(ActDB db)
+	{
+		this.db=db;
+	}
+	
+	public ActDB getDB()
+	{
+		return db;
+	}
+	
+	public ActList copy()
+	{
+		return (ActList)clone();
+	}
+}
diff --git a/timeflow/data/db/ArrayDB.java b/timeflow/data/db/ArrayDB.java
new file mode 100755
index 0000000..b8b570a
--- /dev/null
+++ b/timeflow/data/db/ArrayDB.java
@@ -0,0 +1,348 @@
+package timeflow.data.db;
+
+import java.net.URL;
+import java.util.*;
+
+import timeflow.data.db.filter.*;
+import timeflow.data.time.*;
+
+public class ArrayDB implements ActDB
+{
+
+        private Schema schema;
+        private List<Act> data = new ArrayList<Act>();
+        private Field[] fields;
+        private String source = "[unknown]";
+        private String description = "";
+
+        public String getDescription()
+        {
+                return description;
+        }
+
+        public void setDescription(String description)
+        {
+                this.description = description;
+        }
+
+        public String getSource()
+        {
+                return source;
+        }
+
+        public void setSource(String source)
+        {
+                this.source = source;
+        }
+
+        @Override
+        public void setAlias(Field field, String name)
+        {
+                schema.addAlias(field, name);
+        }
+
+        public ArrayDB(String[] fieldNames, Class[] types, String source)
+        {
+                this.schema = new Schema();
+                this.source = source;
+                int n = fieldNames.length;
+                fields = new Field[n];
+                for (int i = 0; i < n; i++)
+                {
+                        fields[i] = schema.add(fieldNames[i], types[i]);
+                        fields[i].index = i;
+                }
+        }
+
+        public Field[] getFieldArray()
+        {
+                return fields;
+        }
+
+        @Override
+        public Field addField(String name, Class type)
+        {
+
+                int n = fields.length;
+
+                // make new Field.
+                Field field = new Field(name, type);
+                field.index = n;
+
+                // make new array of fields.
+                Field[] moreFields = new Field[n + 1];
+                System.arraycopy(fields, 0, moreFields, 0, n);
+                moreFields[n] = field;
+                this.fields = moreFields;
+
+                // go through all the data items and expand their arrays, too.
+                for (Act d : data)
+                {
+                        IndexedAct item = (IndexedAct) d;
+                        Object[] old = item.data;
+                        item.data = new Object[n + 1];
+                        System.arraycopy(old, 0, item.data, 0, n);
+                }
+
+                //System.out.println("Field added: "+field);
+                schema.add(field);
+
+                return field;
+        }
+
+        public Field getField(String name)
+        {
+                return schema.getField(name);
+        }
+
+        @Override
+        public ActList all()
+        {
+                return select(null);
+        }
+
+        @Override
+        public Act createAct()
+        {
+                IndexedAct act = new IndexedAct(this, fields.length);
+                data.add(act);
+                return act;
+        }
+
+        @Override
+        public void delete(Act act)
+        {
+                data.remove(act);
+        }
+
+        @Override
+        public void deleteField(Field deadField)
+        {
+
+                System.out.println("Deleting: " + deadField);
+
+                schema.delete(deadField);
+                int n = fields.length;
+                int m = deadField.index;
+
+                // make new array of fields.
+                Field[] fewerFields = new Field[n - 1];
+                removeItem(fields, fewerFields, m);
+                fields = fewerFields;
+
+                // go through all the data items and contract their arrays, too.
+                for (Act d : data)
+                {
+                        IndexedAct item = (IndexedAct) d;
+                        Object[] old = item.data;
+                        item.data = new Object[n - 1];
+                        removeItem(old, item.data, m);
+                }
+
+                // change field indices
+                for (int i = 0; i < fields.length; i++)
+                {
+                        System.out.println("fields[" + i + "]=" + fields[i]);
+                        if (fields[i].index > deadField.index)
+                        {
+                                fields[i].index--;
+                        }
+                }
+        }
+
+        private static void removeItem(Object[] a, Object[] b, int m)
+        {
+                int n = a.length;
+                if (m > 0)
+                {
+                        System.arraycopy(a, 0, b, 0, m);
+                }
+                if (m < n - 1)
+                {
+                        System.arraycopy(a, m + 1, b, m, n - m - 1);
+                }
+        }
+
+        @Override
+        public List<Field> getFields(Class type)
+        {
+                return schema.getFields(type);
+        }
+
+        @Override
+        public ActList select(ActFilter filter)
+        {
+                ActList set = new ActList(this);
+                for (Act a : data)
+                {
+                        if (filter == null || filter.accept(a))
+                        {
+                                set.add(a);
+                        }
+                }
+                return set;
+        }
+
+        @Override
+        public List<Field> getFields()
+        {
+                return schema.getFields();
+        }
+
+        @Override
+        public Act get(int i)
+        {
+                return data.get(i);
+        }
+
+        @Override
+        public int size()
+        {
+                return data.size();
+        }
+
+        @Override
+        public Iterator<Act> iterator()
+        {
+                return data.iterator();
+        }
+
+        @Override
+        public List<String> getFieldKeys()
+        {
+                return schema.getKeys();
+        }
+
+        @Override
+        public void setNewFieldOrder(List<Field> newOrder)
+        {
+                schema.setNewFieldOrder(newOrder);
+        }
+
+        class IndexedAct implements Act
+        {
+
+                Object[] data;
+                ActDB db;
+
+                IndexedAct(ActDB db, int numFields)
+                {
+                        this.db = db;
+                        data = new Object[numFields];
+                }
+
+                @Override
+                public String getString(Field field)
+                {
+
+                        Object obj = data[field.index];
+
+                        if (obj == null)
+                                return null;
+
+                        if (obj instanceof String[])
+                        {
+                                String[] strings = (String[]) obj;
+                                String string = "";
+                                for (String s : strings)
+                                {
+                                        string += s + ", ";
+                                }
+                                return string;
+                        } else
+                        {
+                                return obj.toString();
+                        }
+                }
+
+                public void setText(Field field, String text)
+                {
+                        data[field.index] = text;
+                }
+
+                @Override
+                public String[] getTextList(Field field)
+                {
+                        Object obj = data[field.index];
+                        
+                        if (obj == null)
+                                return null;
+
+                        if (obj instanceof String[])
+                        {
+                                return (String[]) obj;
+                        } else
+                        {
+                                return new String[]
+                                        {
+                                                obj.toString()
+                                        };
+                        }
+                }
+
+                public void setTextList(Field field, String[] list)
+                {
+                        data[field.index] = list;
+                }
+
+                @Override
+                public double getValue(Field field)
+                {
+                        Double d = (Double) data[field.index];
+                        return d == null ? Double.NaN : d.doubleValue();
+                }
+
+                public void setValue(Field field, double value)
+                {
+                        data[field.index] = value;
+                }
+
+                @Override
+                public Object get(Field field)
+                {
+                        return data[field.index];
+                }
+
+                @Override
+                public ActDB getDB()
+                {
+                        return db;
+                }
+
+                @Override
+                public void set(Field field, Object value)
+                {
+                        data[field.index] = value;
+                }
+
+                @Override
+                public RoughTime getTime(Field field)
+                {
+                        return (RoughTime) data[field.index];
+                }
+
+                @Override
+                public void setTime(Field field, RoughTime time)
+                {
+                        data[field.index] = time;
+                }
+
+                @Override
+                public URL getURL(Field field)
+                {
+                        return (URL) data[field.index];
+                }
+
+                @Override
+                public void setURL(Field field, URL url)
+                {
+                        data[field.index] = url;
+                }
+        }
+
+        @Override
+        public void renameField(Field field, String name)
+        {
+                schema.renameField(field, name);
+        }
+}
diff --git a/timeflow/data/db/BasicAct.java b/timeflow/data/db/BasicAct.java
new file mode 100755
index 0000000..48e3eb9
--- /dev/null
+++ b/timeflow/data/db/BasicAct.java
@@ -0,0 +1,89 @@
+package timeflow.data.db;
+
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+
+import java.net.URL;
+import java.util.*;
+
+public class BasicAct implements Act {
+	
+	private HashMap data=new HashMap();
+	private ActDB db;
+	
+	public BasicAct(ActDB db)
+	{
+		this.db=db;
+	}
+	
+
+	@Override
+	public String getString(Field field) {
+		return (String)data.get(field.getName());
+	}
+	
+	public void setText(Field field, String text)
+	{
+		data.put(field.getName(), text);
+	}
+
+	@Override
+	public String[] getTextList(Field field) {
+		return (String[])data.get(field.getName());
+	}
+	
+	public void setTextList(Field field, String[] list){
+		data.put(field.getName(), list);
+	}
+
+	@Override
+	public double getValue(Field field) {
+		return (Double)data.get(field.getName());
+	}
+	
+	public void setValue(Field field, double value)
+	{
+		data.put(field.getName(), value);
+	}
+
+	@Override
+	public Object get(Field field) {
+		return data.get(field.getName());
+	}
+
+	@Override
+	public ActDB getDB() {
+		return db;
+	}
+
+	@Override
+	public void set(Field field, Object value) {
+		data.put(field.getName(), value);
+	}
+
+
+	@Override
+	public RoughTime getTime(Field field) {
+		return (RoughTime)data.get(field.getName());
+	}
+
+
+	@Override
+	public void setTime(Field field, RoughTime time) {
+		data.put(field.getName(), time);
+		
+	}
+
+
+	@Override
+	public URL getURL(Field field) {
+		return (URL)data.get(field.getName());
+	}
+
+
+	@Override
+	public void setURL(Field field, URL url) {
+		data.put(field.getName(), url);
+	}
+
+}
diff --git a/timeflow/data/db/BasicDB.java b/timeflow/data/db/BasicDB.java
new file mode 100755
index 0000000..bcd3ba2
--- /dev/null
+++ b/timeflow/data/db/BasicDB.java
@@ -0,0 +1,129 @@
+package timeflow.data.db;
+
+import java.util.*;
+
+import timeflow.data.db.*;
+import timeflow.data.db.filter.ActFilter;
+
+public class BasicDB implements ActDB {
+	
+	private Schema schema;
+	private List<Act> data=new ArrayList<Act>();
+	private String source="[unknown]";
+	private String description="";	
+	public String getDescription() {
+		return description;
+	}
+
+	public void setDescription(String description) {
+		this.description = description;
+	}
+	
+	public String getSource() {
+		return source;
+	}
+
+	public void setSource(String source) {
+		this.source = source;
+	}
+
+	public BasicDB(String source)
+	{
+		this(new Schema(), source);
+	}
+	
+	public BasicDB(Schema schema, String source)
+	{
+		this.schema=schema;
+		this.source=source;
+	}
+	
+	@Override
+	public Field addField(String name, Class type) {
+		Field field=new Field(name, type);
+		schema.add(field);
+		return field;
+	}
+	
+	public Field getField(String name)
+	{
+		return schema.getField(name);
+	}
+
+	@Override
+	public ActList all() {
+		return select(null);
+	}
+
+	@Override
+	public Act createAct() {
+		BasicAct act=new BasicAct(this);
+		data.add(act);
+		return act;
+	}
+
+	@Override
+	public void delete(Act act) {
+		data.remove(act);
+	}
+
+	@Override
+	public void deleteField(Field field) {
+		schema.delete(field);
+	}
+
+	@Override
+	public List<Field> getFields(Class type) {
+		return schema.getFields(type);
+	}
+
+	@Override
+	public ActList select(ActFilter filter) {
+		ActList set=new ActList(this);
+		for (Act a: data)
+			if (filter==null || filter.accept(a))
+				set.add(a);
+		return set;
+	}
+
+	@Override
+	public List<Field> getFields() {
+		return schema.getFields();
+	}
+
+	@Override
+	public Act get(int i) {
+		return data.get(i);
+	}
+
+	@Override
+	public int size() {
+		return data.size();
+	}
+
+	@Override
+	public Iterator<Act> iterator() {
+		return data.iterator();
+	}
+
+	@Override
+	public void setAlias(Field field, String name) {
+		schema.addAlias(field,name);
+	}
+
+	@Override
+	public List<String> getFieldKeys() {
+		return schema.getKeys();
+	}
+
+	@Override
+	public void setNewFieldOrder(List<Field> newOrder) {
+		schema.setNewFieldOrder(newOrder);
+	}
+
+	@Override
+	public void renameField(Field field, String name) {
+		schema.renameField(field, name);
+	}
+
+}
diff --git a/timeflow/data/db/DBUtils.java b/timeflow/data/db/DBUtils.java
new file mode 100755
index 0000000..3a9712e
--- /dev/null
+++ b/timeflow/data/db/DBUtils.java
@@ -0,0 +1,234 @@
+package timeflow.data.db;
+
+import timeflow.data.db.filter.*;
+import timeflow.data.time.*;
+import timeflow.util.*;
+
+import java.util.*;
+
+public class DBUtils
+{
+
+        public static void dump(Act act)
+        {
+                List<Field> fields = act.getDB().getFields();
+                for (Field f : fields)
+                {
+                        System.out.println(f.getName() + " = " + act.get(f));
+                }
+        }
+
+        public static Object get(Act act, String field)
+        {
+                return act.get(act.getDB().getField(field));
+        }
+
+        public static List<String> getFieldAliases(ActDB db)
+        {
+                ArrayList<String> list = new ArrayList<String>();
+                for (String s : db.getFieldKeys())
+                {
+                        if (!db.getField(s).getName().equals(s))
+                        {
+                                list.add(s);
+                        }
+                }
+                return list;
+        }
+
+        public static Interval range(ActList a, Field[] fields)
+        {
+                if (fields == null || fields.length == 0)
+                {
+                        return new Interval(0, 0);
+                }
+                Interval t = null;
+                for (Act act : a)
+                {
+                        for (Field f : fields)
+                        {
+                                RoughTime d = act.getTime(f);
+                                if (d != null && d.isDefined())
+                                {
+                                        if (t == null)
+                                        {
+                                                t = new Interval(d.getTime(), d.getTime());
+                                        } else
+                                        {
+                                                t.include(d.getTime());
+                                        }
+                                }
+                        }
+                }
+                return t != null ? t : new Interval(RoughTime.UNKNOWN, RoughTime.UNKNOWN);
+        }
+
+        public static Interval range(ActList a, String fieldName)
+        {
+
+                Field field = a.getDB().getField(fieldName);
+                if (field == null || a.size() == 0)
+                {
+                        return new Interval(0, 0);
+                }
+                Interval t = null;
+                for (Act act : a)
+                {
+                        RoughTime d = act.getTime(field);
+                        if (d != null && d.isDefined())
+                        {
+                                if (t == null)
+                                {
+                                        t = new Interval(d.getTime(), d.getTime());
+                                } else
+                                {
+                                        t.include(d.getTime());
+                                }
+                        }
+                }
+                return t != null ? t : new Interval(RoughTime.UNKNOWN, RoughTime.UNKNOWN);
+        }
+
+        public static List<Field> categoryFields(ActDB db)
+        {
+                List<Field> list = new ArrayList<Field>();
+                list.addAll(db.getFields());
+//                list.addAll(db.getFields(String.class));
+//                list.addAll(db.getFields(String[].class));
+                return list;
+        }
+
+        public static int count(Iterable<Act> acts, Interval i, Field field)//String fieldName)
+        {
+                return count(acts, new TimeIntervalFilter(i, field));
+        }
+
+        public static int count(Iterable<Act> acts, ActFilter filter)
+        {
+                int num = 0;
+                for (Act a : acts)
+                {
+                        if (filter.accept(a))
+                        {
+                                num++;
+                        }
+                }
+                return num;
+        }
+
+        public static double[] minmax(Iterable<Act> acts, Field field)
+        {
+                double min = Double.NaN;
+                double max = min;
+                for (Act a : acts)
+                {
+                        double x = a.getValue(field);
+                        if (Double.isNaN(min))
+                        {
+                                min = x;
+                                max = x;
+                        } else if (!Double.isNaN(x))
+                        {
+                                min = Math.min(x, min);
+                                max = Math.max(x, max);
+                        }
+                }
+                return new double[]
+                        {
+                                min, max
+                        };
+        }
+
+        public static double[] getValues(Iterable<Act> acts, Field field)
+        {
+                ArrayList<Double> list = new ArrayList<Double>();
+                if (field.getType() == Double.class)
+                {
+                        for (Act a : acts)
+                        {
+                                list.add(a.getValue(field));
+                        }
+                } else if (field.getType() == RoughTime.class)
+                {
+                        for (Act a : acts)
+                        {
+                                RoughTime r = a.getTime(field);
+                                if (r != null)
+                                {
+                                        list.add(new Double(r.getTime()));
+                                }
+                        }
+                }
+                int n = list.size();
+                double[] x = new double[n];
+                for (int i = 0; i < n; i++)
+                {
+                        x[i] = list.get(i);
+                }
+                return x;
+        }
+
+        public static Bag<String> countValues(Iterable<Act> acts, Field field)
+        {
+                Bag<String> bag = new Bag<String>();
+                if (field.getType() != String[].class)
+                {
+                        for (Act a : acts)
+                        {
+                                bag.add(a.getString(field));
+                        }
+                }
+                else // if (field.getType() == String[].class)
+                {
+                        for (Act a : acts)
+                        {
+                                String[] s = a.getTextList(field);
+                                if (s != null)
+                                {
+                                        for (int i = 0; i < s.length; i++)
+                                        {
+                                                bag.add(s[i]);
+                                        }
+                                }
+                        }
+//                } else
+//                {
+//                        throw new IllegalArgumentException("Asked to count values for non-text field: " + field);
+                }
+                return bag;
+        }
+
+        public static void setRecSizesFromCurrent(ActDB db)
+        {
+                // for String fields.
+                for (Field f : db.getFields(String.class))
+                {
+                        int max = 0;
+                        for (Act a : db)
+                        {
+                                String s = a.getString(f);
+                                if (s != null)
+                                {
+                                        max = Math.max(s.length(), max);
+                                }
+                        }
+                        f.setRecommendedSize(max);
+                }
+        }
+
+        public static Field ensureField(ActDB db, String name, Class type)
+        {
+                Field f = db.getField(name);
+                if (f == null)
+                {
+                        return db.addField(name, type);
+                } else
+                {
+                        if (f.getType() != type)
+                        {
+                                throw new IllegalArgumentException("Mismatched types: got " + type + ", expected " + f.getType());
+                        }
+                }
+                return f;
+        }
+}
diff --git a/timeflow/data/db/Field.java b/timeflow/data/db/Field.java
new file mode 100755
index 0000000..9fac035
--- /dev/null
+++ b/timeflow/data/db/Field.java
@@ -0,0 +1,50 @@
+package timeflow.data.db;
+
+
+public class Field {
+	private String name;
+	private Class type;
+	int index;
+	private int recommendedSize=-1;
+	
+	public Field(String name, Class type)
+	{
+		this.name=name;
+		this.type=type;
+	}
+	
+	public Field(String name, Class type, int recommendedSize)
+	{
+		this.name=name;
+		this.type=type;
+		this.recommendedSize=recommendedSize;
+	}
+	
+	public int getRecommendedSize() {
+		return recommendedSize;
+	}
+
+	public void setRecommendedSize(int recommendedSize) {
+		this.recommendedSize = recommendedSize;
+	}
+	
+	void setName(String name)
+	{
+		this.name=name;
+	}
+
+	public String getName()
+	{
+		return name;
+	}
+	
+	public Class getType()
+	{
+		return type;
+	}
+
+	public String toString()
+	{
+		return "[Field: name='"+name+"', type="+type+", index="+index+"]";
+	}
+}
diff --git a/timeflow/data/db/Schema.java b/timeflow/data/db/Schema.java
new file mode 100755
index 0000000..9fdcc87
--- /dev/null
+++ b/timeflow/data/db/Schema.java
@@ -0,0 +1,130 @@
+package timeflow.data.db;
+
+import java.util.*;
+
+// methods are public for testing purposes.
+public class Schema implements Iterable<Field>
+{
+
+        private Map<String, Field> schema = new HashMap<String, Field>();
+        private List<Field> fieldList = new ArrayList<Field>(); // so we preserve field order.
+
+        public Iterator<Field> iterator()
+        {
+                return fieldList.iterator();
+        }
+
+        public Field getField(String key)
+        {
+                return schema.get(key);
+        }
+
+        public List<String> getKeys()
+        {
+                return new ArrayList(schema.keySet());
+        }
+
+        public List<Field> getFields(Class type)
+        {
+                List<Field> a = new ArrayList<Field>();
+                for (Field s : fieldList)
+                {
+                        if (type == null || s.getType() == type)
+                        {
+                                a.add(s);
+                        }
+                }
+                return a;
+        }
+
+        public List<Field> getFields()
+        {
+                return getFields(null);
+        }
+
+        // not sure this actually works! removing things while iterating? to-do: test!
+        public void delete(Field field)
+        {
+                if (schema.get(field.getName()) == null)
+                {
+                        throw new IllegalArgumentException("No field exists: " + field);
+                }
+
+                Set<String> keys = new HashSet<String>(schema.keySet());
+                for (String s : keys)
+                {
+                        Field f = schema.get(s);
+                        if (f == field)
+                        {
+                                schema.remove(s);
+                        }
+                }
+
+                fieldList.remove(field);
+        }
+
+        public void addAlias(Field field, String name)
+        {
+                if (field == null)
+                {
+                        schema.remove(name);
+                        return;
+                }
+                if (!schema.values().contains(field))
+                {
+                        throw new IllegalArgumentException("Field does not exist in schema: " + field);
+                }
+                schema.put(name, field);
+        }
+
+        public Field add(String name, Class type)
+        {
+                return add(new Field(name, type));
+        }
+
+        public Field add(Field field)
+        {
+                if (schema.get(field.getName()) != null)
+                {
+                        throw new IllegalArgumentException("Schema already has field named '" + field.getName()
+                                + "', type=" + field.getType());
+                }
+                schema.put(field.getName(), field);
+                fieldList.add(field);
+                return field;
+        }
+
+        public void setNewFieldOrder(List<Field> newOrder)
+        {
+                // first, we go through and check that this really is a new ordering!
+                if (newOrder.size() != fieldList.size())
+                {
+                        throw new IllegalArgumentException("Field lists have different sizes");
+                }
+                for (Field f : newOrder)
+                {
+                        if (!fieldList.contains(f))
+                        {
+                                throw new IllegalArgumentException("New field list has unexpected field: " + f);
+                        }
+                }
+                fieldList = newOrder;
+        }
+
+        public void print()
+        {
+                System.out.println(schema);
+        }
+
+        public void renameField(Field field, String name)
+        {
+                Field old = schema.get(name);
+                if (old != null && old != field)
+                {
+                        throw new IllegalArgumentException("Can't rename a field to a name that already exists: " + name);
+                }
+                schema.remove(field);
+                field.setName(name);
+                schema.put(name, field);
+        }
+}
diff --git a/timeflow/data/db/filter/ActFilter.java b/timeflow/data/db/filter/ActFilter.java
new file mode 100755
index 0000000..c23978e
--- /dev/null
+++ b/timeflow/data/db/filter/ActFilter.java
@@ -0,0 +1,14 @@
+package timeflow.data.db.filter;
+
+import timeflow.data.db.Act;
+
+public abstract class ActFilter {
+	public abstract boolean accept(Act act);
+	
+	// in earlier versions we've wanted the UI to count the number of filters applied.
+	// because of the hierarchical way filters are defined, we need this method.
+	public int countFilters()
+	{
+		return 1;
+	}
+}
diff --git a/timeflow/data/db/filter/AndFilter.java b/timeflow/data/db/filter/AndFilter.java
new file mode 100755
index 0000000..ecdef4b
--- /dev/null
+++ b/timeflow/data/db/filter/AndFilter.java
@@ -0,0 +1,50 @@
+package timeflow.data.db.filter;
+
+import java.util.*;
+
+import timeflow.data.db.Act;
+
+public class AndFilter extends ActFilter {
+	private List<ActFilter> filters;
+	
+	public AndFilter()
+	{		
+	}
+	
+	public AndFilter(ActFilter a, ActFilter b)
+	{
+		filters=new ArrayList<ActFilter>();
+		and(a);
+		and(b);
+	}
+	
+	public void and(ActFilter a)
+	{
+		if (a==null)
+			return;
+		if (filters==null)
+			filters=new ArrayList<ActFilter>();
+		filters.add(a);
+	}
+
+	@Override
+	public boolean accept(Act act) {
+		if (filters!=null)
+			for (ActFilter f: filters)
+				if (!f.accept(act))
+					return false;
+		return true;
+	}
+	
+	public int countFilters()
+	{
+		int sum=0;
+		if (filters!=null)
+			for (ActFilter f: filters)
+				if (f!=null)
+					sum+=f.countFilters();
+		return sum;
+	}
+	
+	
+}
diff --git a/timeflow/data/db/filter/ConstFilter.java b/timeflow/data/db/filter/ConstFilter.java
new file mode 100755
index 0000000..fd8d821
--- /dev/null
+++ b/timeflow/data/db/filter/ConstFilter.java
@@ -0,0 +1,24 @@
+package timeflow.data.db.filter;
+
+import timeflow.data.db.Act;
+
+public class ConstFilter extends ActFilter {
+	
+	boolean result;
+	
+	public ConstFilter(boolean result)
+	{
+		this.result=result;
+	}
+
+	@Override
+	public boolean accept(Act act) {
+		return result;
+	}
+	
+	public int countFilters()
+	{
+		return result ? 0 : 1;
+	}
+
+}
diff --git a/timeflow/data/db/filter/FieldValueFilter.java b/timeflow/data/db/filter/FieldValueFilter.java
new file mode 100755
index 0000000..28c8af9
--- /dev/null
+++ b/timeflow/data/db/filter/FieldValueFilter.java
@@ -0,0 +1,37 @@
+package timeflow.data.db.filter;
+
+import timeflow.data.db.*;
+
+public class FieldValueFilter extends ActFilter implements ValueFilter {
+
+	private Field field;
+	private Object value;
+	
+	public FieldValueFilter(Field field, Object value)
+	{
+		this.field=field;
+		this.value=value;
+	}
+	
+	public boolean ok(Object o)
+	{
+		if (o==null)
+			return value==null;
+		if (o.equals(value))
+			return true;
+		if (o instanceof Object[])
+		{
+			Object[] s=(Object[] )o;
+			for (int i=0; i<s.length; i++)
+				if (s[i].equals(value))
+					return true;
+		}
+		return false;
+	}
+	
+	@Override
+	public boolean accept(Act act) {
+		return ok(act.get(field));	
+	}
+
+}
diff --git a/timeflow/data/db/filter/FieldValueSetFilter.java b/timeflow/data/db/filter/FieldValueSetFilter.java
new file mode 100755
index 0000000..6168213
--- /dev/null
+++ b/timeflow/data/db/filter/FieldValueSetFilter.java
@@ -0,0 +1,40 @@
+package timeflow.data.db.filter;
+
+import timeflow.data.db.*;
+
+import java.util.*;
+
+public class FieldValueSetFilter extends ActFilter implements ValueFilter {
+
+	private Field field;
+	private Set valueSet;
+	
+	public FieldValueSetFilter(Field field)
+	{
+		this.field=field;
+		valueSet=new HashSet();
+	}
+	
+	public void addValue(Object value)
+	{
+		valueSet.add(value);
+	}
+	
+	@Override
+	public boolean ok(Object o) {
+		if (o instanceof Object[])
+		{
+			Object[] s=(Object[] )o;
+			for (int i=0; i<s.length; i++)
+				if (valueSet.contains(s[i]))
+					return true;
+		}
+		return valueSet.contains(o);
+	}
+	
+	@Override
+	public boolean accept(Act act) {
+		return ok(act.get(field));
+	}
+
+}
diff --git a/timeflow/data/db/filter/MissingValueFilter.java b/timeflow/data/db/filter/MissingValueFilter.java
new file mode 100755
index 0000000..f4a4c7d
--- /dev/null
+++ b/timeflow/data/db/filter/MissingValueFilter.java
@@ -0,0 +1,27 @@
+package timeflow.data.db.filter;
+
+import timeflow.data.db.Act;
+import timeflow.data.db.Field;
+
+public class MissingValueFilter extends ActFilter {
+	private Field field;
+	private boolean text, array, number;
+	
+	public MissingValueFilter(Field field)
+	{
+		this.field=field;
+		text=field.getType()==String.class;
+		array=field.getType()==String[].class;
+		number=field.getType()==Double.class;
+	}
+
+	@Override
+	public boolean accept(Act act) {
+		Object o=act.get(field);
+		return o==null || 
+		      number && Double.isNaN(((Double)o).doubleValue()) ||
+		      text && "".equals(o) || 
+		      array && ((String[])o).length==0;
+	}
+
+}
diff --git a/timeflow/data/db/filter/NotFilter.java b/timeflow/data/db/filter/NotFilter.java
new file mode 100755
index 0000000..406430b
--- /dev/null
+++ b/timeflow/data/db/filter/NotFilter.java
@@ -0,0 +1,24 @@
+package timeflow.data.db.filter;
+
+import java.util.*;
+
+import timeflow.data.db.Act;
+
+public class NotFilter extends ActFilter {
+	private ActFilter f;
+	
+	public NotFilter(ActFilter f)
+	{
+		this.f=f;
+	}
+
+	@Override
+	public boolean accept(Act act) {
+		return f!=null && !f.accept(act);
+	}
+	
+	public int countFilters()
+	{
+		return 1+f.countFilters();
+	}
+}
diff --git a/timeflow/data/db/filter/NumericRangeFilter.java b/timeflow/data/db/filter/NumericRangeFilter.java
new file mode 100755
index 0000000..b7f74f9
--- /dev/null
+++ b/timeflow/data/db/filter/NumericRangeFilter.java
@@ -0,0 +1,28 @@
+package timeflow.data.db.filter;
+
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+
+public class NumericRangeFilter extends ActFilter {
+	
+	double low, high;
+	Field field;
+	boolean acceptNull;
+	
+	public NumericRangeFilter(Field field, double low, double high, boolean acceptNull)
+	{
+		this.low=low;
+		this.high=high;
+		this.field=field;
+		this.acceptNull=acceptNull;
+	}
+
+	@Override
+	public boolean accept(Act act) {
+		if (field==null)
+			return false;
+		double x=act.getValue(field);
+		return Double.isNaN(x) && acceptNull || x>=low && x<=high;
+	}
+
+}
diff --git a/timeflow/data/db/filter/OrFilter.java b/timeflow/data/db/filter/OrFilter.java
new file mode 100755
index 0000000..6161138
--- /dev/null
+++ b/timeflow/data/db/filter/OrFilter.java
@@ -0,0 +1,38 @@
+package timeflow.data.db.filter;
+
+import java.util.*;
+
+import timeflow.data.db.Act;
+
+public class OrFilter extends ActFilter {
+	private List<ActFilter> filters=new ArrayList<ActFilter>();
+	
+	public OrFilter(ActFilter a, ActFilter b)
+	{
+		or(a);
+		or(b);
+	}
+	
+	public void or(ActFilter a)
+	{
+		filters.add(a);
+	}
+
+	@Override
+	public boolean accept(Act act) {
+		for (ActFilter f: filters)
+			if (f.accept(act))
+				return true;
+		return false;
+	}
+	public int countFilters()
+	{
+		int sum=0;
+		if (filters!=null)
+			for (ActFilter f: filters)
+				if (f!=null)
+					sum+=f.countFilters();
+		return sum;
+	}
+	
+}
diff --git a/timeflow/data/db/filter/StringMatchFilter.java b/timeflow/data/db/filter/StringMatchFilter.java
new file mode 100755
index 0000000..6b9d650
--- /dev/null
+++ b/timeflow/data/db/filter/StringMatchFilter.java
@@ -0,0 +1,69 @@
+package timeflow.data.db.filter;
+
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+
+import java.util.regex.*;
+
+public class StringMatchFilter extends ActFilter {
+
+	private Field[] textFields;
+	private Field[] listFields;
+	private String query="";
+	private boolean isRegex=false;
+	private Pattern pattern;
+	
+	public StringMatchFilter(ActDB db, boolean isRegex)
+	{
+		this(db,"", isRegex);
+	}
+	
+	public StringMatchFilter(ActDB db, String query, boolean isRegex)
+	{
+		textFields=(Field[])db.getFields(String.class).toArray(new Field[0]);
+		listFields=(Field[])db.getFields(String[].class).toArray(new Field[0]);
+		this.isRegex=isRegex;
+		setQuery(query);
+	}
+	
+	public String getQuery()
+	{
+		return query;
+	}
+	
+	public void setQuery(String query)
+	{
+		this.query=query;
+		if (isRegex)
+		{
+			pattern=Pattern.compile(query, Pattern.CASE_INSENSITIVE+Pattern.MULTILINE+Pattern.DOTALL);
+		}
+		else
+			this.query=query.toLowerCase();
+	}
+	
+	@Override
+	public boolean accept(Act act) {
+		// check text fields
+		for (int i=0; i<textFields.length; i++)
+		{
+			String s=act.getString(textFields[i]);
+			if (s==null) continue;
+			if (isRegex ? pattern.matcher(s).find() : s.toLowerCase().contains(query))
+				return true;
+		}
+		// check list fields
+		for (int j=0; j<listFields.length; j++)
+		{
+			String[] m=act.getTextList(listFields[j]);
+			if (m!=null)
+				for (int i=0; i<m.length; i++)
+				{
+					String s=m[i];
+					if (isRegex ? pattern.matcher(s).find() : s.toLowerCase().contains(query))
+						return true;
+				}
+		}
+		return false;
+	}
+}
diff --git a/timeflow/data/db/filter/TimeIntervalFilter.java b/timeflow/data/db/filter/TimeIntervalFilter.java
new file mode 100755
index 0000000..c2a275d
--- /dev/null
+++ b/timeflow/data/db/filter/TimeIntervalFilter.java
@@ -0,0 +1,35 @@
+package timeflow.data.db.filter;
+
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+
+public class TimeIntervalFilter extends ActFilter {
+	
+	Interval interval;
+	Field timeField;
+	boolean acceptNull;
+	
+	public TimeIntervalFilter(long start, long end, boolean acceptNull, Field timeField)
+	{
+		this.interval=new Interval(start, end);
+		this.acceptNull=acceptNull;
+		this.timeField=timeField;
+	}
+	
+	public TimeIntervalFilter(Interval interval, Field timeField)
+	{
+		this.interval=interval;
+		this.timeField=timeField;
+	}
+
+	@Override
+	public boolean accept(Act act) {
+		if (timeField==null)
+			return false;
+		RoughTime t=act.getTime(timeField);
+		if (t==null)
+			return acceptNull;
+		return interval.contains(t.getTime());
+	}
+
+}
diff --git a/timeflow/data/db/filter/ValueFilter.java b/timeflow/data/db/filter/ValueFilter.java
new file mode 100755
index 0000000..9e09b0c
--- /dev/null
+++ b/timeflow/data/db/filter/ValueFilter.java
@@ -0,0 +1,5 @@
+package timeflow.data.db.filter;
+
+public interface ValueFilter {
+	public boolean ok(Object o);
+}
diff --git a/timeflow/data/time/Interval.java b/timeflow/data/time/Interval.java
new file mode 100755
index 0000000..49a53d5
--- /dev/null
+++ b/timeflow/data/time/Interval.java
@@ -0,0 +1,114 @@
+package timeflow.data.time;
+
+import java.util.*;
+
+public class Interval
+{
+
+        public long start;
+        public long end;
+
+        public Interval(long start, long end)
+        {
+                this.start = start;
+                this.end = end;
+        }
+
+        public Interval copy()
+        {
+                return new Interval(start, end);
+        }
+
+        public boolean contains(long x)
+        {
+                return x >= start && x <= end;
+        }
+
+        public boolean intersects(Interval x)
+        {
+                return intersects(x.start, x.end);
+        }
+
+        public boolean intersects(long start1, long end1)
+        {
+                return start1 <= end && end1 >= start;
+        }
+
+        public Interval subinterval(double startFraction, double endFraction)
+        {
+                return new Interval((long) (start + startFraction * length()),
+                        (long) (start + endFraction * length()));
+        }
+
+        public void setTo(long start, long end)
+        {
+                this.start = start;
+                this.end = end;
+        }
+
+        public void setTo(Interval t)
+        {
+                start = t.start;
+                end = t.end;
+        }
+
+        public void include(long time)
+        {
+                start = Math.min(start, time);
+                end = Math.max(end, time);
+        }
+
+        public void include(Interval t)
+        {
+                include(t.start);
+                include(t.end);
+        }
+
+        public void expand(long amount)
+        {
+                start -= amount;
+                end += amount;
+        }
+
+        public void add(long amount)
+        {
+                start += amount;
+                end += amount;
+        }
+
+        public long length()
+        {
+                return end - start;
+        }
+
+        public void translateTo(long newStart)
+        {
+                add(newStart - start);
+        }
+
+        public Interval intersection(Interval i)
+        {
+                start = Math.max(i.start, start);
+                end = Math.min(i.end, end);
+                return this;
+        }
+
+        public void clampInside(Interval container)
+        {
+                if (length() > container.length())
+                {
+                        throw new IllegalArgumentException("Containing interval too small: " + container + " < " + this);
+                }
+                if (start >= container.start && end <= container.end)
+                {
+                        return;
+                }
+                add(Math.max(0, container.start - start));
+                add(Math.min(0, container.end - end));
+        }
+
+        public String toString()
+        {
+                return "[Interval: From " + new Date(start) + " to " + new Date(end) + "]";
+        }
+}
diff --git a/timeflow/data/time/RoughTime.java b/timeflow/data/time/RoughTime.java
new file mode 100755
index 0000000..9cadc5a
--- /dev/null
+++ b/timeflow/data/time/RoughTime.java
@@ -0,0 +1,128 @@
+package timeflow.data.time;
+
+import java.util.Calendar;
+import java.util.Date;
+
+public class RoughTime implements Comparable {
+
+	public static final long UNKNOWN=Long.MIN_VALUE;
+	private TimeUnit units;
+	private long time;
+		
+	public RoughTime(TimeUnit units)
+	{
+		time=UNKNOWN;
+		this.units=units;
+	}
+	
+	public RoughTime(long time, TimeUnit units)
+	{
+		this.time=time;
+		this.units=units;
+	}
+	
+	public boolean isDefined()
+	{
+		return time!=UNKNOWN;
+	}
+	
+	public long getTime()
+	{
+		return time;
+	}
+	
+	public void setTime(long time)
+	{
+		this.time=time;
+	}
+	
+	public Date toDate()
+	{
+		return new Date(time);
+	}
+	
+	public boolean after(RoughTime t)
+	{
+		return t.time<time;
+	}
+	
+	public boolean before(RoughTime t)
+	{
+		return t.time>time;
+	}
+	
+	public RoughTime plus(int numUnits)
+	{
+		return plus(units, numUnits);
+	}
+	
+	public RoughTime plus(TimeUnit unit, int times)
+	{
+		RoughTime r=copy();
+		unit.addTo(r,times);
+		return r;
+	}
+	
+	public String toString()
+	{
+		if (isKnown())
+			return new Date(time).toString();
+		return "unknown";
+	}
+	
+	public boolean isKnown()
+	{
+		return time!=UNKNOWN;
+	}
+	
+	public boolean equals(Object o)
+	{
+		if (!(o instanceof RoughTime))
+			return false;
+		RoughTime t=(RoughTime)o;
+		return t.units==units  && t.time==time;
+	}
+	
+	public RoughTime copy()
+	{
+		RoughTime t=new RoughTime(time, units);
+		return t;
+	}
+	
+	public void setUnits(TimeUnit units)
+	{
+		this.units=units;
+	}
+
+	public TimeUnit getUnits() {
+		return units;
+	}
+	
+	public String format()
+	{
+		return units.formatFull(time);
+	}
+	
+	public static int compare(RoughTime t1, RoughTime t2)
+	{
+		if (t1==t2)
+			return 0;
+		if (t1==null)
+			return -1;
+		if (t2==null)
+			return 1;
+		long dt= t1.time-t2.time;
+		if (dt==0)
+			return 0;
+		if (dt>0)
+			return 1;
+		return -1;
+	}
+
+	@Override
+	public int compareTo(Object o) {
+		return compare(this, (RoughTime)o);
+	}
+
+
+}
diff --git a/timeflow/data/time/TimeUnit.java b/timeflow/data/time/TimeUnit.java
new file mode 100755
index 0000000..760b3be
--- /dev/null
+++ b/timeflow/data/time/TimeUnit.java
@@ -0,0 +1,243 @@
+package timeflow.data.time;
+
+import java.util.*;
+import java.text.*;
+
+public class TimeUnit {
+
+	public static final TimeUnit YEAR=new TimeUnit("Years", Calendar.YEAR, 365*24*60*60*1000L, "yyyy", "yyyy");
+	public static final TimeUnit MONTH=new TimeUnit("Months", Calendar.MONTH, 30*24*60*60*1000L, "MMM", "MMM yyyy");
+	public static final TimeUnit WEEK=new TimeUnit("Weeks", Calendar.WEEK_OF_YEAR, 7*24*60*60*1000L, "d", "MMM d yyyy");
+	public static final TimeUnit DAY=new TimeUnit("Days", Calendar.DAY_OF_MONTH, 24*60*60*1000L, "d", "MMM d yyyy");
+	public static final TimeUnit DAY_OF_WEEK=new TimeUnit("Days", Calendar.DAY_OF_WEEK, 24*60*60*1000L, "d", "MMM d yyyy");
+	public static final TimeUnit HOUR=new TimeUnit("Hours", Calendar.HOUR_OF_DAY, 60*60*1000L, "kk:mm", "MMM d yyyy kk:mm");
+	public static final TimeUnit MINUTE=new TimeUnit("Minutes", Calendar.MINUTE, 60*1000L, ":mm", "MMM d yyyy kk:mm");
+	public static final TimeUnit SECOND=new TimeUnit("Seconds", Calendar.SECOND, 1000L, ":ss", "MMM d yyyy kk:mm:ss");
+	public static final TimeUnit DECADE=multipleYears(10);
+	public static final TimeUnit CENTURY=multipleYears(100);
+	
+	private static final double DAY_SIZE=24*60*60*1000L;
+	
+	private int quantity;	
+	private long roughSize;
+	private SimpleDateFormat format, fullFormat;
+	private String name;
+	private int calendarCode;
+	
+	private TimeUnit()
+	{		
+	}
+	
+	private TimeUnit(String name, int calendarCode, long roughSize, String formatPattern, String fullFormatPattern)
+	{
+		this.name=name;
+		this.calendarCode=calendarCode;
+		this.roughSize=roughSize;
+		format=new SimpleDateFormat(formatPattern);
+		fullFormat=new SimpleDateFormat(fullFormatPattern);
+		quantity=1;
+	}
+	
+	public String toString()
+	{
+		return "[TimeUnit: "+name+"]";
+	}
+
+	public static TimeUnit multipleYears(int numYears)
+	{
+		TimeUnit t=new TimeUnit();
+		t.name=numYears+" Years";
+		t.calendarCode=Calendar.YEAR;
+		t.roughSize=YEAR.roughSize*numYears;
+		t.format=YEAR.format;
+		t.fullFormat=YEAR.fullFormat;
+		t.quantity=numYears;
+		return t;
+	}
+	
+	public static TimeUnit multipleWeeks(int num)
+	{
+		TimeUnit t=new TimeUnit();
+		t.name=num+" Weeks";
+		t.calendarCode=Calendar.WEEK_OF_YEAR;
+		t.roughSize=WEEK.roughSize*num;
+		t.format=WEEK.format;
+		t.fullFormat=WEEK.fullFormat;
+		t.quantity=num;
+		return t;
+	}
+	
+	public TimeUnit times(int quantity)
+	{
+		TimeUnit t=new TimeUnit();
+		t.name=quantity+" "+this.name;
+		t.calendarCode=this.calendarCode;
+		t.roughSize=this.roughSize*quantity;
+		t.format=this.format;
+		t.fullFormat=this.fullFormat;
+		t.quantity=quantity;
+		return t;
+		
+	}
+
+	
+	public int numUnitsIn(TimeUnit u)
+	{
+		return (int)Math.round(u.getRoughSize()/(double)getRoughSize());
+	}
+	
+	public boolean isDayOrLess()
+	{
+		return roughSize <= 24*60*60*1000L;
+	}
+	
+	public RoughTime roundDown(long timestamp)
+	{
+		return round(timestamp, false);
+	}
+	
+	public RoughTime roundUp(long timestamp)
+	{
+		return round(timestamp, true);
+	}
+	
+	private static final int[] calendarUnits={Calendar.SECOND, Calendar.MINUTE, Calendar.HOUR_OF_DAY, Calendar.DAY_OF_MONTH, Calendar.MONTH, Calendar.YEAR};
+	public RoughTime round(long timestamp, boolean up)
+	{
+		Calendar c=TimeUtils.cal(timestamp);
+		
+		if (calendarCode==Calendar.WEEK_OF_YEAR )
+		{
+			c.set(Calendar.DAY_OF_WEEK, c.getMinimum(Calendar.DAY_OF_WEEK));
+		}
+		else
+		{
+		
+			// set to minimum all fields of finer granularity.
+			int roundingCode=calendarCode;
+			if (calendarCode==Calendar.WEEK_OF_YEAR || calendarCode==Calendar.DAY_OF_WEEK)
+				roundingCode=Calendar.DAY_OF_MONTH;
+			for (int i=0; i<calendarUnits.length; i++)
+			{
+				if (calendarUnits[i]==roundingCode)
+					break;
+				if (i==calendarUnits.length-1)
+					throw new IllegalArgumentException("Unsupported Calendar Unit: "+calendarCode);
+				c.set(calendarUnits[i], c.getMinimum(calendarUnits[i]));
+			}
+			if (quantity>1)
+			{
+				c.set(calendarCode, quantity*(c.get(calendarCode)/quantity));
+			}
+		}
+		
+		// if rounding up, then add a unit at current granularity.
+		if (up)
+			c.add(calendarCode, quantity);
+		
+		return new RoughTime(c.getTimeInMillis(), this);
+	}
+	
+	public int get(long timestamp)
+	{
+		Calendar c= TimeUtils.cal(timestamp);
+		int n=c.get(calendarCode);
+		return quantity==1 ? n : n%quantity;
+	}
+	
+	public void addTo(RoughTime r)
+	{
+		addTo(r,1);
+	}
+	
+	public void addTo(RoughTime r, int times)
+	{
+		Calendar c=TimeUtils.cal(r.getTime());
+		c.add(calendarCode, quantity*times);
+		r.setTime(c.getTimeInMillis());
+	}
+	
+	// Finding the difference between two dates, in a given unit of time,
+	// is much subtler than you'd think! And annoyingly, the Calendar class does not do
+	// this for you, even though it actually "knows" how to do so since it
+	// can add fields.
+	//
+	// The most vexing problem is dealing with daylight savings time,
+	// which means that one day a year has 23 hours and one day has 25 hours.
+	// We also have to handle the fact that months and years aren't constant lengths.
+	//
+	// Rather than write all this ourselves, in this code we
+	// use the Calendar class to do the heavy lifting.
+	public long difference(long x, long y)
+	{
+		// If this is not one of the hard cases,
+		// just divide the timespan by the length of time unit.
+		// Note that we're not worrying about hours and daylight savings time.
+		if (calendarCode!=Calendar.YEAR && calendarCode!=Calendar.MONTH && 
+		   calendarCode!=Calendar.DAY_OF_MONTH && calendarCode!=Calendar.DAY_OF_WEEK &&
+		   calendarCode!=Calendar.WEEK_OF_YEAR)
+		{
+			return (x-y)/roughSize;
+		}
+			
+		Calendar c1=TimeUtils.cal(x), c2=TimeUtils.cal(y); 
+		int diff=0;
+		switch (calendarCode)
+		{
+			case Calendar.YEAR:
+				return (c1.get(Calendar.YEAR)-c2.get(Calendar.YEAR))/quantity;
+				
+			case Calendar.MONTH:
+				diff= 12*(c1.get(Calendar.YEAR)-c2.get(Calendar.YEAR))+
+				              c1.get(Calendar.MONTH)-c2.get(Calendar.MONTH);
+				return diff/quantity;
+				
+			case Calendar.DAY_OF_MONTH:
+			case Calendar.DAY_OF_WEEK:
+			case Calendar.DAY_OF_YEAR:
+			case Calendar.WEEK_OF_MONTH:
+			case Calendar.WEEK_OF_YEAR:
+				// This is ugly, but believe me, it beats the alternative methods :-)
+				// We use the Calendar class's knowledge of daylight savings time.
+				// and also the fact that if we calculate this naively, then we aren't going
+				// to be off by more than one in either direction.
+				int naive=(int)Math.round((x-y)/(double)roughSize);
+				c2.add(calendarCode, naive*quantity);
+				if (c1.get(calendarCode)==c2.get(calendarCode))
+					return naive/quantity;
+				c2.add(calendarCode, quantity);
+				if (c1.get(calendarCode)==c2.get(calendarCode))
+					return naive/quantity+1;
+				return naive/quantity-1;
+		}
+		throw new IllegalArgumentException("Unexpected calendar code: "+calendarCode);
+	}
+
+	public long approxNumInRange(long start, long end)
+	{
+		return 1+(end-start)/roughSize;
+	}
+	
+	public long getRoughSize() {
+		return roughSize;
+	}
+
+	public String format(Date date)
+	{
+		return format.format(date);
+	}
+
+	public String formatFull(Date date)
+	{
+		return fullFormat.format(date);
+	}
+
+	public String formatFull(long timestamp)
+	{
+		return fullFormat.format(new Date(timestamp));
+	}
+
+	public String getName() {
+		return name;
+	}
+}
diff --git a/timeflow/data/time/TimeUtils.java b/timeflow/data/time/TimeUtils.java
new file mode 100755
index 0000000..32d4f6d
--- /dev/null
+++ b/timeflow/data/time/TimeUtils.java
@@ -0,0 +1,21 @@
+package timeflow.data.time;
+
+import java.util.*;
+
+public class TimeUtils {
+	
+	public static Calendar cal(long time)
+	{
+		Calendar c=new GregorianCalendar();
+		c.setTimeInMillis(time);
+		return c;
+	}
+
+	public static Calendar cal(Date date)
+	{
+		Calendar c=new GregorianCalendar();
+		c.setTime(date);
+		return c;
+	}
+	
+}
diff --git a/timeflow/format/field/DateTimeGuesser.java b/timeflow/format/field/DateTimeGuesser.java
new file mode 100755
index 0000000..48d05c9
--- /dev/null
+++ b/timeflow/format/field/DateTimeGuesser.java
@@ -0,0 +1,115 @@
+package timeflow.format.field;
+
+import java.text.*;
+import java.util.*;
+
+import timeflow.data.time.*;
+
+public class DateTimeGuesser {
+	
+	private DateTimeParser lastGoodFormat;
+	
+	private static List<DateTimeParser> parsers=new ArrayList<DateTimeParser>();
+
+	/*
+	 
+	HANDY REFERENCE FOR SIMPLEDATEFORMAT:
+	(quoted from Java documentation)
+		 
+	Letter	Date or Time Component	Presentation	Examples
+	G	Era designator	Text	AD
+	y	Year	Year	1996; 96
+	M	Month in year	Month	July; Jul; 07
+	w	Week in year	Number	27
+	W	Week in month	Number	2
+	D	Day in year	Number	189
+	d	Day in month	Number	10
+	F	Day of week in month	Number	2
+	E	Day in week	Text	Tuesday; Tue
+	a	Am/pm marker	Text	PM
+	H	Hour in day (0-23)	Number	0
+	k	Hour in day (1-24)	Number	24
+	K	Hour in am/pm (0-11)	Number	0
+	h	Hour in am/pm (1-12)	Number	12
+	m	Minute in hour	Number	30
+	s	Second in minute	Number	55
+	S	Millisecond	Number	978
+	z	Time zone	General time zone	Pacific Standard Time; PST; GMT-08:00
+	Z	Time zone	RFC 822 time zone	-0800
+		 
+		 */
+		
+	
+	// the order of the list below matters--better not to put the year-only ones at the top,
+	// because then the guesser succeeds before it has a chance to try parsing days.
+	static
+	{
+		parsers.add(new DateTimeParser("yyyy-MM-ddzzzzzzzzzz", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("MMM dd yyyy HH:mm", TimeUnit.SECOND));
+		parsers.add(new DateTimeParser("MMM/dd/yyyy HH:mm", TimeUnit.SECOND));
+		parsers.add(new DateTimeParser("MM/dd/yy HH:mm", TimeUnit.SECOND));
+		parsers.add(new DateTimeParser("MMM dd yyyy HH:mm:ss", TimeUnit.SECOND));
+		parsers.add(new DateTimeParser("MM/dd/yyyy HH:mm:ss", TimeUnit.SECOND));
+		parsers.add(new DateTimeParser("MMM dd yyyy HH:mm:ss zzzzzzzz", TimeUnit.SECOND));
+		parsers.add(new DateTimeParser("EEE MMM dd HH:mm:ss zzzzzzzz yyyy", TimeUnit.SECOND));
+		parsers.add(new DateTimeParser("EEE MMM dd HH:mm:ss zzzzzzzz yyyy", TimeUnit.SECOND));
+		parsers.add(new DateTimeParser("MM-dd-yyyy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("yyyy-MM-dd", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("yyyyMMdd", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("MM-dd-yy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("dd-MMM-yy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("MM-dd-yyyy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("MM/dd/yy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("MM/dd/yyyy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("dd MMM yyyy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("dd MMM, yyyy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("MMM dd yyyy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("MMM dd, yyyy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("EEE MMM dd zzzzzzzz yyyy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("EEE MMM dd yyyy", TimeUnit.DAY));
+		parsers.add(new DateTimeParser("MMM-yy", TimeUnit.MONTH));
+		parsers.add(new DateTimeParser("MMM yy", TimeUnit.MONTH));
+		parsers.add(new DateTimeParser("MMM/yy", TimeUnit.MONTH));
+		parsers.add(new DateTimeParser("yyyy", TimeUnit.YEAR));
+		parsers.add(new DateTimeParser("yyyy GG", TimeUnit.YEAR));
+	}
+	
+	public DateTimeParser getLastGoodFormat()
+	{
+		return lastGoodFormat;
+	}
+
+	public RoughTime guess(String s)
+	{
+		// old code for trying the last good parser.
+		// we took this out because if the last good one was for a single year,
+		// but a new one is for years and days,
+		// all you get is the year.
+		
+		//if (lastGoodFormat!=null)
+		//try { return lastGoodFormat.parse(s); }
+		//catch (ParseException e) {}	
+		if (s==null || s.trim().length()==0)
+			return null;
+		for (DateTimeParser d: parsers)
+		{
+			try
+			{
+				RoughTime date= d.parse(s);
+				lastGoodFormat=d;
+				return date;
+			}
+			catch (ParseException e) {}
+		}
+		throw new IllegalArgumentException("Couldn't guess date: '"+s+"'");
+	}
+	
+	public static void main(String[] args)
+	{
+		DateTimeGuesser g=new DateTimeGuesser();
+		System.out.println(g.guess("2009-03-04"));
+		System.out.println(g.guess("June 10, 2010"));
+		System.out.println(g.guess("2010"));
+		System.out.println(g.guess("3/17/10"));
+	}
+}
diff --git a/timeflow/format/field/DateTimeParser.java b/timeflow/format/field/DateTimeParser.java
new file mode 100755
index 0000000..e0e45d9
--- /dev/null
+++ b/timeflow/format/field/DateTimeParser.java
@@ -0,0 +1,39 @@
+package timeflow.format.field;
+
+import timeflow.data.time.*;
+
+import java.util.*;
+import java.text.*;
+
+
+// Guesses date format and returns result
+public class DateTimeParser {
+	
+	private DateFormat format;
+	private TimeUnit units;
+	private String pattern;
+
+	public DateTimeParser(String pattern, TimeUnit units)
+	{
+		this.pattern=pattern;
+		format=new SimpleDateFormat(pattern);
+		this.units=units;
+	}
+	
+	public RoughTime parse(String s) throws ParseException
+	{
+		RoughTime f= new RoughTime(format.parse(s).getTime(), units);
+		return f;
+	}
+	
+	public TimeUnit getUnits()
+	{
+		return units;
+	}
+	
+	public String toString()
+	{
+		return "DateParser: pattern="+pattern+", units="+units;
+	}
+	
+}
diff --git a/timeflow/format/field/FieldFormat.java b/timeflow/format/field/FieldFormat.java
new file mode 100755
index 0000000..a8c7223
--- /dev/null
+++ b/timeflow/format/field/FieldFormat.java
@@ -0,0 +1,64 @@
+package timeflow.format.field;
+
+import java.net.URL;
+
+import timeflow.data.time.*;
+
+public abstract class FieldFormat {	
+	protected String lastInput;
+	protected Object lastValue;
+	protected boolean understood=true;
+	
+	double value;
+	
+	void add(double x)
+	{
+		value+=x;
+	}
+	
+	void note(String s)
+	{
+		add(scoreFormatMatch(s));
+	}
+
+	
+	protected abstract Object _parse(String s) throws Exception;
+	public abstract String format(Object o);
+	public abstract Class getType();
+	public abstract double scoreFormatMatch(String s);
+	public abstract String getHumanName();
+
+
+	public void setValue(Object o)
+	{
+		lastValue=o;
+		lastInput=o==null ? "" : format(o);
+	}
+	
+	public Object parse(String s) throws Exception
+	{
+		lastInput=s;
+		lastValue=null;
+		understood=false;
+		lastValue=_parse(s);
+		understood=true;
+		return lastValue;
+	}
+	
+	public Object getLastValue()
+	{
+		return lastValue;
+	}
+	
+	public String feedback() 
+	{
+		if (!understood)
+			return "Couldn't understand";
+		return lastValue==null ? "(missing)" : "Read: "+format(lastValue);
+	}
+	
+	public boolean isUnderstood()
+	{
+		return understood;
+	}
+}
diff --git a/timeflow/format/field/FieldFormatCatalog.java b/timeflow/format/field/FieldFormatCatalog.java
new file mode 100755
index 0000000..d65092c
--- /dev/null
+++ b/timeflow/format/field/FieldFormatCatalog.java
@@ -0,0 +1,54 @@
+package timeflow.format.field;
+
+import timeflow.data.time.*;
+import timeflow.util.*;
+
+import java.net.URL;
+import java.util.*;
+
+public class FieldFormatCatalog {
+
+	private static Map<String, FieldFormat> formatTable=new HashMap<String, FieldFormat>();
+	private static Map<Class, FieldFormat> classTable=new HashMap<Class, FieldFormat>();
+
+	static
+	{
+		for (FieldFormat f: listFormats())
+		{
+			formatTable.put(f.getHumanName(), f);
+			classTable.put(f.getType(), f);
+		}
+	}
+	
+	static FieldFormat[] listFormats()
+	{
+		return new FieldFormat[] {new FormatDateTime(), new FormatString(),
+				new FormatStringArray(), new FormatDouble(), new FormatURL()};
+	}
+	
+	public static Iterable<String> classNames()
+	{
+		return formatTable.keySet();
+	}
+	
+	public static String humanName(Class c){
+		return getFormat(c).getHumanName();
+	}
+	
+
+	public static FieldFormat getFormat(Class c) {
+		FieldFormat f= classTable.get(c);
+		if (f==null)
+			System.out.println("Warning: no FieldFormat for "+c);
+		return f;
+	}
+
+	
+	public static Class javaClass(String humanName)
+	{
+		Class  c=formatTable.get(humanName).getType();
+		if (c==null)
+			System.out.println("Warning: no class for "+humanName);
+		return c;
+	}
+}
diff --git a/timeflow/format/field/FieldFormatGuesser.java b/timeflow/format/field/FieldFormatGuesser.java
new file mode 100755
index 0000000..168f759
--- /dev/null
+++ b/timeflow/format/field/FieldFormatGuesser.java
@@ -0,0 +1,50 @@
+package timeflow.format.field;
+
+import timeflow.util.*;
+
+public class FieldFormatGuesser {
+	
+	FieldFormat[] scores;
+	
+	private FieldFormatGuesser()
+	{
+		scores=FieldFormatCatalog.listFormats();
+	}
+	public static Class[] analyze(String[][] data, int startRow, int numRows)
+	{
+		int n=data[0].length;
+		FieldFormatGuesser[] g=new FieldFormatGuesser[n];
+		for (int i=0; i<n; i++)
+			g[i]=new FieldFormatGuesser();
+		for (int i=startRow; i<startRow+numRows && i<data.length; i++)
+		{
+			for (int j=0; j<n; j++)
+				g[j].add(data[i][j]);
+		}
+		Class[] c=new Class[n];
+		for (int i=0; i<n; i++)
+			c[i]=g[i].best();
+		return c;
+	}	
+	
+	private void add(String s)
+	{
+		for (int i=0; i<scores.length; i++)
+			scores[i].note(s);
+	}
+	
+	private Class best()
+	{
+		double max=scores[0].value;
+		Class best=scores[0].getType();
+		for (int i=1; i<scores.length; i++)
+		{
+			if (scores[i].value>max)
+			{
+				max=scores[i].value;
+				best=scores[i].getType();
+			}
+		}
+		return best;
+	}
+}
diff --git a/timeflow/format/field/FormatDateTime.java b/timeflow/format/field/FormatDateTime.java
new file mode 100755
index 0000000..d16ea32
--- /dev/null
+++ b/timeflow/format/field/FormatDateTime.java
@@ -0,0 +1,79 @@
+/**
+ * 
+ */
+package timeflow.format.field;
+
+import java.text.ParseException;
+import java.util.Calendar;
+
+import timeflow.data.time.RoughTime;
+import timeflow.data.time.TimeUnit;
+import timeflow.data.time.TimeUtils;
+
+public class FormatDateTime extends FieldFormat
+{
+	DateTimeGuesser dateGuesser=new DateTimeGuesser();
+	
+	@Override
+	public String format(Object o) {
+		return ((RoughTime)o).format();
+	}
+
+	@Override
+	public Object _parse(String s) throws Exception {
+		if (s.length()==0)
+			return null;
+		Object o= readTime(s);
+		if (o==null)
+			throw new IllegalArgumentException();
+		return o;
+	}
+	
+	
+	public RoughTime readTime(Object o) throws ParseException
+	{
+		if (!(o instanceof String))
+			throw new IllegalArgumentException("Expected String, got: "+o);
+		return dateGuesser.guess((String)o);
+	}
+	DateTimeGuesser g=new DateTimeGuesser();
+	
+	@Override
+	public double scoreFormatMatch(String s) {
+		if (s==null || s.length()==0)
+			return -.05;
+		try
+		{
+			RoughTime f=g.guess(s);
+			if (f==null)
+				return -1;
+			if (g.getLastGoodFormat().getUnits()==TimeUnit.YEAR)
+			{
+				int year=TimeUtils.cal(f.getTime()).get(Calendar.YEAR);
+				if (year>2100)
+					return -1;
+				if (year>1900 && year<2050)
+					return 1;
+				if (year>2050 || year<1600)
+					return .1;
+				return .5;
+			}
+			return 2;
+		}
+		catch (Exception e)
+		{
+			return -1;
+		}
+	}
+
+
+	@Override
+	public Class getType() {
+		return RoughTime.class;
+	}
+
+	@Override
+	public String getHumanName() {
+		return "Date/Time";
+	}		
+}
\ No newline at end of file
diff --git a/timeflow/format/field/FormatDouble.java b/timeflow/format/field/FormatDouble.java
new file mode 100755
index 0000000..e57922d
--- /dev/null
+++ b/timeflow/format/field/FormatDouble.java
@@ -0,0 +1,103 @@
+/**
+ * 
+ */
+package timeflow.format.field;
+
+public class FormatDouble extends FieldFormat
+{	
+	@Override
+	public String format(Object o) {
+		return o.toString();
+	}
+
+	@Override
+	public Object _parse(String s) {
+		if (s==null || s.trim().length()==0)
+			return null;
+		return parseDouble(s);
+	}
+	
+	public static double parseDouble(String s)
+	{
+		int n=s.length();
+		if (n>3)
+		{
+			if (s.charAt(0)=='(' && s.charAt(n-1)==')')
+			{
+				s='-'+s.substring(1,n-1);
+				n--;
+			}
+		}
+		// remove $,%, etc.
+		StringBuffer b=new StringBuffer();
+		for (int i=0; i<n; i++)
+		{
+			char c=s.charAt(i);
+			if (Character.isDigit(c) || c=='-' || c=='.')
+				b.append(c);
+		}
+		
+		try
+		{
+		return Double.parseDouble(b.toString());
+		}
+		catch (RuntimeException e)
+		{
+			System.out.println("b="+b);
+			throw e;
+		}
+	}
+
+
+	@Override
+	public Class getType() {
+		return Double.class;
+	}	
+	@Override
+	public double scoreFormatMatch(String s) {
+		s=s.trim();
+		int n=s.length();
+		if (n==5) // possible zip code
+		{
+			if (s.charAt(0)=='0')
+				return -3; // gotta be a zip code!
+			return 0;
+		}
+		if (n==9) // possible zip+4, but really, who knows...
+			return 0;
+		
+		if (n==4) // possible date.
+		{
+			try
+			{
+				int x=Integer.parseInt(s);
+				if (x>1900 && x<2030)
+					return -1; // that's very likely a date.
+				if (x>1700 && x<2100)
+					return 0; // you don't know.
+				
+			}
+			catch (Exception e) {} // evidently not a date :-)
+		}
+		
+		if (n==0)
+			return -.1;
+		int ok=0;
+		int bad=0;
+		for (int i=0; i<n; i++)
+		{
+			char c=s.charAt(i);
+			if (Character.isDigit(c) || c=='.' || c==',' || c=='-' || c=='$' || c=='%')
+				ok++;
+			else
+				bad++;
+		}
+		return 4-5*bad;
+	}
+
+	@Override
+	public String getHumanName() {
+		return "Number";
+	}
+
+}
\ No newline at end of file
diff --git a/timeflow/format/field/FormatString.java b/timeflow/format/field/FormatString.java
new file mode 100755
index 0000000..ed784d3
--- /dev/null
+++ b/timeflow/format/field/FormatString.java
@@ -0,0 +1,44 @@
+/**
+ * 
+ */
+package timeflow.format.field;
+
+public class FormatString extends FieldFormat
+{	
+	@Override
+	public String format(Object o) {
+		return o.toString();
+	}
+
+	@Override
+	public Object _parse(String s) {
+		return s;
+	}		
+	
+	
+	public String feedback() 
+	{
+		if (lastValue==null)
+			return "Couldn't understand";
+		if (((String)lastValue).length()==0)
+			return "Blank";
+		return "";
+	}
+
+	@Override
+	public Class getType() {
+		return String.class;
+	}
+	
+	@Override
+	public double scoreFormatMatch(String s) {
+		return s!=null && s.length()>0 ? .1 : 0;
+	}
+
+	@Override
+	public String getHumanName() {
+		return "Text";
+	}
+
+
+}
\ No newline at end of file
diff --git a/timeflow/format/field/FormatStringArray.java b/timeflow/format/field/FormatStringArray.java
new file mode 100755
index 0000000..a2a8245
--- /dev/null
+++ b/timeflow/format/field/FormatStringArray.java
@@ -0,0 +1,59 @@
+/**
+ * 
+ */
+package timeflow.format.field;
+
+import timeflow.model.Display;
+
+public class FormatStringArray extends FieldFormat
+{
+	@Override
+	public String format(Object o) {
+		return Display.arrayToString((String[])o);
+	}
+
+	@Override
+	public Object _parse(String s) {
+		return parseList(s);
+	}	
+	
+	public static String[] parseList(String s)
+	{
+		String[] t= s.length()==0 ? new String[0] : s.split(",");
+		for (int i=0; i<t.length; i++)
+			t[i]=t[i].trim();
+		return t;
+	}
+	
+	public String feedback() 
+	{
+		if (lastValue==null)
+			return "Couldn't understand";
+		String[] s=(String[])lastValue;
+		if (s.length==0)
+			return "Empty list";
+		if (s.length==1)
+			return "One item";
+		return s.length+" items";
+	}
+
+	@Override
+	public Class getType() {
+		return String[].class;
+	}	
+
+	@Override
+	public double scoreFormatMatch(String s) {
+		double commas=-1;
+		for (int i=s.length()-1; i>=0; i--)
+			if (s.charAt(i)==',')
+				commas++;
+		return commas/s.length();
+	}
+
+	@Override
+	public String getHumanName() {
+		return "List";
+	}
+
+}
\ No newline at end of file
diff --git a/timeflow/format/field/FormatURL.java b/timeflow/format/field/FormatURL.java
new file mode 100755
index 0000000..a20922f
--- /dev/null
+++ b/timeflow/format/field/FormatURL.java
@@ -0,0 +1,41 @@
+/**
+ * 
+ */
+package timeflow.format.field;
+
+import java.net.URL;
+
+public class FormatURL extends FieldFormat
+{	
+	@Override
+	public String format(Object o) {
+		return o.toString();
+	}
+
+	@Override
+	public Object _parse(String s) throws Exception {
+		if (s.length()==0)
+			return null;
+		return new URL(s);
+	}
+
+	@Override
+	public Class getType() {
+		return URL.class;
+	}	
+	
+	@Override
+	public double scoreFormatMatch(String s) {
+		if (s==null || s.length()==0)
+			return 0;
+		if (s.startsWith("http") || s.startsWith("file://"))
+			return 5;
+		return -1;
+	}
+
+	@Override
+	public String getHumanName() {
+		return "URL";
+	}
+
+}
\ No newline at end of file
diff --git a/timeflow/format/file/DelimitedFormat.java b/timeflow/format/file/DelimitedFormat.java
new file mode 100755
index 0000000..2b9f1ac
--- /dev/null
+++ b/timeflow/format/file/DelimitedFormat.java
@@ -0,0 +1,217 @@
+package timeflow.format.file;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import timeflow.data.db.Act;
+import timeflow.data.db.ActDB;
+import timeflow.data.db.DBUtils;
+import timeflow.data.db.Field;
+import timeflow.data.time.RoughTime;
+import timeflow.model.Display;
+
+import timeflow.util.*;
+
+public class DelimitedFormat {
+	
+	char delimiter;
+	DelimitedText delimitedText;
+
+	public DelimitedFormat(char delimiter)
+	{
+		this.delimiter=delimiter;
+		delimitedText=new DelimitedText(delimiter);
+	}
+
+	public static String[][] readArrayGuessDelim(String fileName, PrintStream messages) throws Exception
+	{
+		//messages.println("DelimitedFormat: reading "+fileName);
+		String text=IO.read(fileName);
+		return readArrayFromString(text, messages);		
+	}
+	
+	public static String[][] readArrayFromString(String text, PrintStream messages) throws Exception
+	{
+		//messages.println("DelimitedFormat: reading string, length="+text.length());
+		int n=Math.min(text.length(), 1000);
+		String beginning=text.substring(0,n);
+		char c=count(beginning, '\t')>count(beginning, ',') ? '\t' : ',';
+		return new DelimitedFormat(c).readTokensFromString(text, messages);
+	}
+	
+	private static String[] removeBlankLines(String[] lines)
+	{
+		List<String> good=new ArrayList<String>();
+		for (int i=0; i<lines.length; i++)
+		{
+			if (!(lines[i]==null || lines[i].trim().length()==0))
+				good.add(lines[i]);
+		}
+		return (String[])good.toArray(new String[0]);
+	}
+	
+	private static int count(String s, char c)
+	{
+		int n=0;
+		for (int i=0; i<s.length(); i++)
+		{
+			if (s.charAt(i)==c)
+				n++;
+		}
+		return n;
+	}
+
+	public String[][] readTokensFromString(String text, PrintStream messages) throws Exception
+	{
+		
+		ArrayList<String[]> resultList=new ArrayList<String[]>();
+		Iterator<String[]> lines=delimitedText.read(text).iterator();
+		int numCols=-1;
+		while(lines.hasNext())
+		{
+			String[] r=lines.next();
+			int ri=r.length;
+			if (numCols==-1)
+				numCols=r.length;
+			else
+			{
+				if (ri>numCols)
+				{
+					messages.println("Line too long: "+ri+" > "+numCols);
+					messages.println("line="+Display.arrayToString(r));
+				}
+				else if (ri<numCols)
+				{
+					String[] old=r;
+					r=new String[numCols];
+					System.arraycopy(old,0,r,0,ri);
+					for (int j=ri; j<numCols; j++) 
+						r[j]="";
+				}
+			}
+			resultList.add(r);
+		}
+		//messages.println("# lines read: "+resultList.size());
+		return (String[][]) resultList.toArray(new String[0][]);
+	}
+	
+	public void write(ActDB db, File file) throws IOException
+	{
+		write(db, db, file);
+	}
+		
+	public void write(ActDB db, Iterable<Act> acts, File file) throws IOException
+	{
+		FileOutputStream fos=new FileOutputStream(file);
+		BufferedOutputStream b=new BufferedOutputStream(fos);
+		PrintStream out=new PrintStream(b);
+		
+		// Write data!
+		writeDelimited(db, acts, out);
+		
+		out.flush();
+		out.close();
+		b.close();
+		fos.close();
+	}
+
+	
+	void writeDelimited(ActDB db, PrintStream out)
+	{		
+		writeDelimited(db, db, out);
+	}
+	
+	public void writeDelimited(ActDB db, Iterable<Act> acts, PrintWriter out)
+	{		
+		// Write headers		
+		List<String> names=new ArrayList<String>();
+		List<Field> fields=db.getFields();
+		for (Field f: fields)
+			names.add(f.getName());
+		print(names, out);
+		
+		// Write data
+		for (Act a: acts)
+		{
+			List<String> data=new ArrayList<String>();
+			for (Field f: fields)
+				data.add(format(a.get(f)));
+			print(data, out);
+		}
+	}
+
+	
+	public void writeDelimited(ActDB db, Iterable<Act> acts, PrintStream out)
+	{		
+		// Write headers		
+		List<String> names=new ArrayList<String>();
+		List<Field> fields=db.getFields();
+		for (Field f: fields)
+			names.add(f.getName());
+		print(names, out);
+		
+		// Write data
+		for (Act a: acts)
+		{
+			List<String> data=new ArrayList<String>();
+			for (Field f: fields)
+				data.add(format(a.get(f)));
+			print(data, out);
+		}
+	}
+
+	
+	static String format(Object o)
+	{
+		if (o==null)
+			return "";
+		if (o instanceof String)
+			return (String)o;
+		if (o instanceof RoughTime)
+			return ((RoughTime)o).format();
+		if (o instanceof Number)
+			return o.toString();
+		if (o instanceof String[])
+		{
+			return writeArray((String[])o);
+		}
+		return o.toString();
+	}
+	
+	public static String writeArray(Object[] s)
+	{
+        if (s==null || s.length==0)
+        {
+        	return "";
+        }
+        StringBuffer b=new StringBuffer();
+        for (int i=0; i<s.length; i++)
+        {
+        	if (i>0)
+        		b.append(",");
+        	b.append(s[i]);
+        	
+        }
+        return b.toString();
+	}
+	
+	void print(List<String> list, PrintStream out)
+	{
+		out.println(delimitedText.write((String[])list.toArray(new String[0])));
+	}
+	
+	void print(List<String> list, PrintWriter out)
+	{
+		out.println(delimitedText.write((String[])list.toArray(new String[0])));
+	}
+
+}
+
diff --git a/timeflow/format/file/DelimitedText.java b/timeflow/format/file/DelimitedText.java
new file mode 100755
index 0000000..cbc6dce
--- /dev/null
+++ b/timeflow/format/file/DelimitedText.java
@@ -0,0 +1,214 @@
+package timeflow.format.file;
+
+import java.util.*;
+
+import timeflow.util.*;
+
+import timeflow.model.Display;
+
+public class DelimitedText {
+	private char delimiter;
+	
+	public DelimitedText(char delimiter)
+	{
+		if (delimiter=='"')
+			throw new IllegalArgumentException("Can't use quote as delimiter.");
+		this.delimiter=delimiter;
+	}
+	
+	private static boolean isBreak(char c)
+	{
+		return c=='\n' || c=='\r';
+	}
+	
+	public List<String[]> read(String text)
+	{
+		ArrayList<String[]> results=new ArrayList<String[]>();
+		int n=text.length();
+		StringBuffer currentToken=new StringBuffer();
+		ArrayList<String> currentList=new ArrayList<String>();
+		
+		boolean quoted=false;
+		for (int i=0; i<n; i++)
+		{
+			char c=text.charAt(i);
+			if (quoted)
+			{
+				if (c=='"')
+				{
+					if (i==n-1) // end of file, ignore quote.
+					{
+						quoted=false;
+						continue;
+					}
+					char next=text.charAt(i+1);
+					if (next=='"') // a quoted quote.
+					{
+						currentToken.append('"');
+						i++;
+						
+						// Alas, there is a weird special case here
+						// if the user has pasted from Excel.
+						// If a field starts with a quote, and ends with two quotes,
+						// it turns out to be ambiguous!
+						// Excel doesn't do any escaping on: "blah blah""
+						// But, it does escape: blah "\n
+						// turning it into: "blah blah""\n
+						// So if "blah blah"" occurs at the end of the line,
+						// you actually do not know which it is!
+						// In practice, our first bug report was for a literal of "blah blah""
+						// so that is what we will choose.
+						
+						//System.out.println("next++:  '"+text.charAt(i+1)+"'="+(int)text.charAt(i+1));
+						if (i<n-1 && isBreak(text.charAt(i+1)))
+						{
+							quoted=false;
+						}
+						
+						continue;
+					}
+					if (isBreak(next)) // end of line
+					{
+						quoted=false;
+						currentList.add(currentToken.toString());
+						currentToken.setLength(0);
+						results.add((String[])currentList.toArray(new String[0]));
+						currentList=new ArrayList<String>();
+						i++;
+						if (i<n-1 && isBreak(text.charAt(i+1)))
+							i++;
+						continue;
+					}
+					if (next==delimiter)
+					{
+						quoted=false;
+						continue;
+					}
+					System.out.println("a bad quote from excel: next char="+(int)next);
+					quoted=false;
+				}
+				currentToken.append(c);
+				continue;
+			}
+			
+			// ok, not quoted.
+			if (c==delimiter)
+			{
+				currentList.add(currentToken.toString());
+				currentToken.setLength(0);
+				quoted=false;
+				continue;
+			}
+			
+			// not delimiter, not in the middle of a quote.
+			if (c=='"')
+			{
+				if (currentToken.length()==0) // we are at beginning of a token, so this is a quote.
+				{
+					quoted=true;
+					continue;
+				}
+			}
+						
+			// is it a line feed? we're not in the middle of a quote, so this means a new line.
+			if (c=='\n' || c=='\r' || c=='\f')
+			{
+				currentList.add(currentToken.toString());
+				currentToken.setLength(0);
+				results.add((String[])currentList.toArray(new String[0]));
+				currentList=new ArrayList<String>();
+				if (i<n-1 && (text.charAt(i+1)=='\n' || text.charAt(i+1)=='\r'))
+					i++;
+				continue;
+			}
+			
+			// by golly, just a normal character!
+			currentToken.append(c);
+		}
+		
+		// did it just end in a blank line?
+		
+		if (currentList.size()>0 || currentToken.toString().trim().length()>0)
+		{
+			currentList.add(currentToken.toString());
+			results.add((String[])currentList.toArray(new String[0]));	
+		}
+		return results;
+	}
+	
+	public String write(String s)
+	{
+		return write(new String[] {s});
+	}
+	
+	public String write(String[] data)
+	{
+		StringBuffer b=new StringBuffer();
+		for (int i=0; i<data.length; i++)
+		{
+			// add a delimiter if necessary.
+			if (i>0)
+				b.append(delimiter);
+			
+			// if null, just don't write anything.
+			if (data[i]==null)
+				continue;
+			
+			// does it have weird characters in it?
+			boolean weird=false;
+			int n=data[i].length();
+			for (int j=0; j<n; j++)
+			{
+				char c=data[i].charAt(j);
+				if (c==delimiter || isBreak(c))
+				{
+					weird=true;
+					break;
+				}
+			}
+			
+			if (weird)
+			{
+				b.append('"');
+				for (int j=0; j<n; j++)
+				{
+					char c=data[i].charAt(j);
+					if (c=='"')
+						b.append('"');
+					b.append(c);
+				}
+				b.append('"');
+			}
+			else
+				b.append(data[i]);
+		}
+		return b.toString();
+	}
+	
+	public static String[] split(String s, char delimiter)
+	{
+		DelimitedText t= new DelimitedText(delimiter);
+		List<String[]> lines=t.read(s);
+		return lines.get(0);
+	}
+	
+	public static void main(String[] args) throws Exception
+	{
+		String bad=IO.read("test/bad-all.txt");
+		String[][] s=DelimitedFormat.readArrayFromString(bad, System.out);
+		System.out.println("len="+s.length);
+
+		/*
+		//DelimitedText c=new DelimitedText(';');
+		//List<String[]> arrays=c.read(IO.read("test/bad.txt"));
+		//List<String[]> arrays=c.read("a;b;\"x;y\";c");
+		//List<String[]> arrays=c.read("a;\"a\n\rq\";b;\"x;y\";c");
+		//List<String[]> arrays=c.read("a;b;\"with a \"\"blah\";c\nd;e;f\ng;h;i");
+		//List<String[]> arrays=c.read("a,\"b\",\"c\r\nd\"\r\ne,f,g\nh,i,j");
+		for (String[] s:arrays)
+		{
+			System.out.println("["+Display.arrayToString(s)+"]");
+		}
+		*/
+	}
+}
diff --git a/timeflow/format/file/Export.java b/timeflow/format/file/Export.java
new file mode 100755
index 0000000..24361e8
--- /dev/null
+++ b/timeflow/format/file/Export.java
@@ -0,0 +1,10 @@
+package timeflow.format.file;
+
+import timeflow.model.*;
+
+import java.io.BufferedWriter;
+
+public interface Export {
+	public String getName();
+	public void export(TFModel model, BufferedWriter out) throws Exception;
+}
diff --git a/timeflow/format/file/FileExtensionCatalog.java b/timeflow/format/file/FileExtensionCatalog.java
new file mode 100755
index 0000000..f7b8d50
--- /dev/null
+++ b/timeflow/format/file/FileExtensionCatalog.java
@@ -0,0 +1,28 @@
+package timeflow.format.file;
+
+// This is meant to be a repository for different
+// types of import functions, arranged by file extension.
+
+// We currently do not import anything but the standard file type.
+// There actually is some code that will import from JSON/XML SIMILE
+// timelines, but we have removed it from this release to simplify
+// both the application and because it would mean redistributing additional
+// third-party libraries.
+public class FileExtensionCatalog {
+
+	public static Import get(String fileName)
+	{
+		/*
+		// not in this release...
+		// but contact us if you'd like to see this.
+		// we took out the SIMILE import material as too "techie"
+		// for the first release!
+		 
+		if (fileName.endsWith("xml"))
+			return new SimileXMLFormat();
+		if (fileName.endsWith("json"))
+			return new SimileJSONFormat();
+			*/
+		return new TimeflowFormat();
+	}
+}
diff --git a/timeflow/format/file/HtmlFormat.java b/timeflow/format/file/HtmlFormat.java
new file mode 100755
index 0000000..cf50499
--- /dev/null
+++ b/timeflow/format/file/HtmlFormat.java
@@ -0,0 +1,143 @@
+package timeflow.format.file;
+
+import java.awt.Color;
+import java.io.BufferedWriter;
+import java.net.URL;
+
+import timeflow.model.*;
+import timeflow.data.db.*;
+
+public class HtmlFormat implements Export
+{
+	TFModel model;
+	java.util.List<Field> fields;
+	Field title;
+	
+	public HtmlFormat() {}
+	
+	public HtmlFormat(TFModel model)
+	{
+		setModel(model);
+	}
+	
+	public void setModel(TFModel model)
+	{
+		this.model=model;
+		fields=model.getDB().getFields();
+		title=model.getDB().getField(VirtualField.LABEL);
+	}
+	
+	@Override
+	public void export(TFModel model, BufferedWriter out) throws Exception {
+		setModel(model);
+		out.write(makeHeader());
+		for (Act a: model.getDB())
+			out.write(makeItem(a));
+		out.write(makeFooter());
+		out.flush();
+	}
+	
+	public void append(ActList acts, int start, int end, StringBuffer b)
+	{
+		for (int i=start; i<end; i++)
+		{
+			Act a=acts.get(i);
+			b.append(makeItem(a,i));
+		}
+	}
+	
+	private String makeItem(Act act)
+	{
+		return makeItem(act, -1);
+	}
+	
+	public String makeItem(Act act, int id)
+	{
+		StringBuffer page=new StringBuffer();
+		
+		
+		page.append("<tr><td valign=top align=left width=200><b>");
+		if (title!=null)
+		{
+			Field f=model.getColorField();
+			Color c=Color.black;
+			if (f!=null)
+			{
+				if (f.getType()==String.class)
+					c=model.getDisplay().makeColor(act.getString(f));
+				else
+				{
+					String[] tags=act.getTextList(f);
+					if (tags.length==0)
+						c=Color.gray;
+					else
+						c=model.getDisplay().makeColor(tags[0]);
+				}
+			}
+			
+			page.append("<font size=+1 color="+htmlColor(c)+">"+act.getString(title)+"</font><br>");
+		}
+		
+		Field startField=model.getDB().getField(VirtualField.START);
+		
+		if (startField!=null)
+		{
+			page.append("<font color=#999999>"+model.getDisplay().format(
+				act.getTime(startField))+"</font>");
+		}
+		page.append("</b><br>");
+		if (id>=0)
+			page.append("<a href=\"e"+id+"\">EDIT</a>");
+		page.append("<br></td><td valign=top>");
+		for (Field f: fields)
+		{
+			page.append("<b><font color=#003399>"+f.getName()+"</font></b>&nbsp;&nbsp;");
+			Object val=act.get(f);
+			if (val instanceof URL)
+			{
+				page.append("<a href=\""+val+"\">"+val+"</a>");
+			}
+			else
+				page.append(model.getDisplay().toString(val));
+			page.append("<br>");
+
+		}
+		page.append("<br></td></tr>");
+
+		return page.toString();
+	}
+	
+	public String makeHeader()
+	{
+		StringBuffer page=new StringBuffer();
+		page.append("<html><body><blockquote>");
+		page.append("<br>File: "+model.getDbFile()+"<br>");
+		page.append("Source: "+model.getDB().getSource()+"<br><br>");
+		page.append("<br><br>");
+		page.append("<table border=0>");
+
+		return page.toString();
+	}
+	
+	public String makeFooter()
+	{
+		return "</table></blockquote></body></html>";
+	}
+	
+	
+	static String htmlColor(Color c)
+	{
+		return '#'+hex2(c.getRed())+hex2(c.getGreen())+hex2(c.getBlue());
+	}
+	
+	private static final String hexDigits="0123456789ABCDEF";
+	private static String hex2(int n)
+	{
+		return hexDigits.charAt((n/16)%16)+""+hexDigits.charAt(n%16);
+	}
+	@Override
+	public String getName() {
+		return "HTML List";
+	}
+
+}
diff --git a/timeflow/format/file/Import.java b/timeflow/format/file/Import.java
new file mode 100755
index 0000000..36df67e
--- /dev/null
+++ b/timeflow/format/file/Import.java
@@ -0,0 +1,9 @@
+package timeflow.format.file;
+
+import java.io.File;
+import timeflow.data.db.ActDB;
+
+public interface Import {
+	public String getName();
+	public ActDB importFile(File file) throws Exception;
+}
diff --git a/timeflow/format/file/TimeflowFormat.java b/timeflow/format/file/TimeflowFormat.java
new file mode 100755
index 0000000..3778235
--- /dev/null
+++ b/timeflow/format/file/TimeflowFormat.java
@@ -0,0 +1,163 @@
+package timeflow.format.file;
+
+import timeflow.model.*;
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+import timeflow.format.field.*;
+
+import timeflow.util.*;
+
+import java.io.*;
+import java.net.URL;
+import java.util.*;
+
+public class TimeflowFormat implements Import, Export
+{
+	private static final String END_OF_SCHEMA="#TIMEFLOW\tend-metadata";
+	private static final String END_OF_METADATA="#TIMEFLOW\t====== End of Header. Data below is in tab-delimited format. =====";
+	public ActDB readFile(String fileName, PrintStream messages) throws Exception
+	{
+		return read(new File(fileName), messages);
+	}
+	
+	public static ActDB read(File file, PrintStream out) throws Exception
+	{
+		String text=IO.read(file.getAbsolutePath());		
+		DelimitedText quote=new DelimitedText('\t');
+		Iterator<String[]> lines=quote.read(text).iterator();
+		
+
+		ActDB db=null;
+		List<String> fieldNames=new ArrayList<String>();
+		List<Class> fieldTypes=new ArrayList<Class>();
+		List<Integer> fieldSizes=new ArrayList<Integer>();
+		String source="[unknown]", description="";
+		for (;;)
+		{
+			String[] t=lines.next();
+
+			if (t[1].equals("field"))
+			{
+				fieldNames.add(t[2]);
+				fieldTypes.add(FieldFormatCatalog.javaClass(t[3]));
+				if (t.length>4)
+				{
+					fieldSizes.add(Integer.parseInt(t[4]));
+				}
+				else
+					fieldSizes.add(-1);
+			}
+			else if (t[1].equals("source"))
+			{
+				source=t[2];
+			}
+			else if (t[1].equals("description"))
+			{
+				description=t[2];
+				
+			}
+			else if (t[1].equals("end-metadata"))
+				break;
+		}
+		db=new ArrayDB((String[])fieldNames.toArray(new String[0]), 
+				         (Class[])fieldTypes.toArray(new Class[0]), source);
+		db.setDescription(description);
+		for (int i=0; i<fieldNames.size(); i++)
+			if (fieldSizes.get(i)>0)
+				db.getField(fieldNames.get(i)).setRecommendedSize(fieldSizes.get(i));
+		for (;;)
+		{
+			String[] t=lines.next();
+			if (t[1].startsWith("==="))
+				break;		
+			if (t[1].equals("alias"))
+				db.setAlias(db.getField(t[3]), t[2]);
+		}
+		
+		// note: in some cases headers may be in a different order than in
+		// metadata section, so we will read these.
+		String[] headers=lines.next();
+		if (headers.length!=fieldNames.size())
+			throw new IllegalArgumentException("Different number of headers than fields!");
+		
+		
+		while (lines.hasNext())
+		{
+			String[] t=lines.next();
+			Act a=db.createAct();
+			for (int i=0; i<t.length; i++)
+			{
+				Field f=db.getField(headers[i]);
+				FieldFormat format=FieldFormatCatalog.getFormat(f.getType());
+				a.set(f, format.parse(t[i]));
+			}
+		}
+		
+		return db;
+	}
+	
+	public static void write(ActList acts, BufferedWriter bw) throws IOException
+	{
+		ActDB db=acts.getDB();
+		
+		PrintWriter out=new PrintWriter(bw);
+		
+		DelimitedText tab=new DelimitedText('\t');
+		
+		// Write version
+		out.println("#TIMEFLOW\tformat version\t1");
+		
+		// Write source of data.
+		out.println("#TIMEFLOW\tsource\t"+tab.write(db.getSource()));
+		
+		// Write description of data.
+		out.println("#TIMEFLOW\tdescription\t"+tab.write(db.getDescription()));
+		
+		// Write schema.
+		List<Field> fields=db.getFields();
+		for (Field f: fields)
+		{
+			String recSize=f.getRecommendedSize()<=0 ? "" : "\t"+f.getRecommendedSize();
+			out.println("#TIMEFLOW\tfield\t"+tab.write(f.getName())+
+					    "\t"+FieldFormatCatalog.humanName(f.getType())+recSize);
+		}
+		
+		out.println(END_OF_SCHEMA);
+		
+		// Write column mappings.
+		List<String> aliases=DBUtils.getFieldAliases(db);
+		for (String a:aliases)
+			out.println("#TIMEFLOW\talias\t"+a+"\t"+tab.write(db.getField(a).getName()));
+		
+		// Write end of header indicator
+		out.println(END_OF_METADATA);
+		
+		// Write data!
+		new DelimitedFormat('\t').writeDelimited(db, acts, out);
+		
+		out.flush();
+		out.close();
+	}
+	
+	public static void main(String[] args) throws Exception
+	{
+		System.out.println("Reading");
+		ActDB db=read(new File("test/monet.txt"), System.out);
+		System.out.println("# lines: "+db.size());
+	}
+
+	@Override
+	public String getName() {
+		return "TimeFlow Format";
+	}
+
+	@Override
+	public ActDB importFile(File file) throws Exception {
+		return read(file, System.out);
+	}
+
+	@Override
+	public void export(TFModel model, BufferedWriter out) throws Exception {
+		write(model.getDB().all(), out);
+	}
+}
diff --git a/timeflow/model/Display.java b/timeflow/model/Display.java
new file mode 100755
index 0000000..b56c86b
--- /dev/null
+++ b/timeflow/model/Display.java
@@ -0,0 +1,373 @@
+package timeflow.model;
+
+import timeflow.data.db.filter.ValueFilter;
+import timeflow.data.time.*;
+
+import java.awt.*;
+import java.util.*;
+import java.net.URI;
+import java.text.*;
+import javax.swing.*;
+
+// to do: read from a properties file!
+public class Display
+{
+
+        HashMap<String, String> strings = new HashMap<String, String>();
+        HashMap<String, Integer> ints = new HashMap<String, Integer>();
+        HashMap<String, Color> colors = new HashMap<String, Color>();
+        HashMap<Class, String> classLabel = new HashMap<Class, String>();
+        Color fallback = new Color(0, 53, 153, 128);
+        String fontName = "Verdana";
+        Font tinyFont = new Font(fontName, Font.BOLD, 9);
+        Font smallFont = new Font(fontName, Font.PLAIN, 11);
+        Font boldFont = new Font(fontName, Font.BOLD, 12);
+        Font plainFont = UIManager.getFont("Label.font");
+        Font bigFont = plainFont.deriveFont(Font.BOLD);
+        Font hugeFont = new Font(fontName, Font.BOLD, 16);
+        Font timeLabelFont = tinyFont;
+        FontMetrics hugeFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(hugeFont);
+        FontMetrics bigFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(bigFont);
+        FontMetrics plainFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(plainFont);
+        FontMetrics boldFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(boldFont);
+        FontMetrics tinyFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(tinyFont);
+        FontMetrics timeLabelFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(timeLabelFont);
+        static DecimalFormat df = new DecimalFormat("###,###,###,###.##");
+        static DecimalFormat roundFormat = new DecimalFormat("###,###,###,###");
+        public static final String MISC_CODE = "Misc.    ";
+        public static final double MAX_DOT_SIZE = 10;
+        public static final int CALENDAR_CELL_HEIGHT = 80;
+        public static Color barColor = new Color(150, 170, 200);
+        private static final double PHI = (1 + Math.sqrt(5)) / 2;
+        HashMap<String, Color> topColors = new HashMap<String, Color>();
+        static Color[] handPalette =
+        {
+                new Color(203, 31, 23),
+                new Color(237, 131, 0),
+                new Color(71, 175, 13),
+                new Color(6, 119, 207),
+                new Color(0, 188, 184),
+                new Color(209, 80, 174),
+                new Color(146, 6, 0),
+                new Color(175, 103, 0),
+                new Color(76, 124, 0),
+                new Color(0, 80, 143),
+                new Color(0, 128, 124),
+                new Color(153, 56, 126)
+        };
+        ValueFilter grayFilter;
+
+        public Display()
+        {
+
+                strings.put("other.label", "Other");
+                strings.put("null.label", "[none]");
+
+                classLabel.put(RoughTime.class, "Date/Time");
+                classLabel.put(String.class, "Text");
+                classLabel.put(Number.class, "Number");
+                classLabel.put(Double.class, "Number");
+                classLabel.put(Integer.class, "Number");
+                classLabel.put(String[].class, "List");
+
+                colors.put("chart.background", Color.white);
+                Color ui = new Color(240, 240, 240);
+                colors.put("splash.background", Color.white);
+                colors.put("splash.text", new Color(50, 50, 50));
+                colors.put("filterpanel.background", ui);
+                colors.put("visualpanel.background", ui);
+                colors.put("text.prominent", Color.black);
+                colors.put("text.normal", Color.gray);
+                colors.put("timeline.label", new Color(0, 53, 153));
+                colors.put("timeline.label.lesser", new Color(110, 153, 200));
+                colors.put("timeline.grid", new Color(240, 240, 240));
+                colors.put("timeline.grid.vertical", new Color(240, 240, 240));
+                colors.put("timeline.zebra", new Color(245, 245, 245));
+                colors.put("null.color", new Color(230, 230, 230));
+                colors.put("timeline.unspecified.color", new Color(0, 53, 153));
+                colors.put("highlight.color", new Color(0, 53, 153));
+
+                ints.put("timeline.datelabel.height", 20);
+                ints.put("timeline.item.height.min", 16);
+        }
+
+        public static void launchBrowser(String urlString)
+        {
+                if (Desktop.isDesktopSupported())
+                {
+                        Desktop desktop = Desktop.getDesktop();
+                        try
+                        {
+                                desktop.browse(new URI(urlString));
+                        } catch (Exception e2)
+                        {
+                                e2.printStackTrace();
+                        }
+                } else
+                {
+                        System.out.println("Desktop not supported!");
+                }
+        }
+
+        public static String arrayToString(Object[] s)
+        {
+                if (s == null || s.length == 0)
+                {
+                        return "";
+                }
+                StringBuffer b = new StringBuffer();
+                for (int i = 0; i < s.length; i++)
+                {
+                        if (i > 0)
+                        {
+                                b.append(", ");
+                        }
+                        b.append(s[i]);
+
+                }
+                return b.toString();
+        }
+
+        public static String version()
+        {
+                return "TimeFlow 0.5";
+        }
+
+        public Font bold()
+        {
+                return boldFont;
+        }
+
+        public Font plain()
+        {
+                return plainFont;
+        }
+
+        public Font small()
+        {
+                return smallFont;
+        }
+
+        public FontMetrics hugeFontMetrics()
+        {
+                return hugeFontMetrics;
+        }
+
+        public FontMetrics plainFontMetrics()
+        {
+                return plainFontMetrics;
+        }
+
+        public FontMetrics boldFontMetrics()
+        {
+                return boldFontMetrics;
+        }
+
+        public FontMetrics tinyFontMetrics()
+        {
+                return tinyFontMetrics;
+        }
+
+        public FontMetrics timeLabelFontMetrics()
+        {
+                return timeLabelFontMetrics;
+        }
+
+        public Font huge()
+        {
+                return hugeFont;
+        }
+
+        public Font big()
+        {
+                return bigFont;
+        }
+
+        public Font tiny()
+        {
+                return tinyFont;
+        }
+
+        public Font timeLabel()
+        {
+                return timeLabelFont;
+        }
+
+        public String getString(String s)
+        {
+                return strings.get(s);
+        }
+
+        public int getInt(String s)
+        {
+                return ints.get(s);
+        }
+
+        public Color getColor(String key)
+        {
+                if (colors.get(key) == null)
+                {
+                        throw new IllegalArgumentException("No color for " + key);
+                }
+                return colors.get(key);
+        }
+
+        public JLabel label(String s)
+        {
+                return new JLabel(s);
+        }
+
+        public String format(String s, int maxLength, boolean tryNoDots)
+        {
+                if (s == null)
+                {
+                        return "";
+                }
+                int n = s.length();
+                if (n <= maxLength)
+                {
+                        return s;
+                }
+                if (maxLength < 4)
+                {
+                        return "...";
+                }
+                if (!tryNoDots)
+                {
+                        return s.substring(0, maxLength - 3) + "...";
+                }
+                // find last space before maxLength and after maxLength/2
+                for (int j = maxLength - 1; j > maxLength / 2; j--)
+                {
+                        if (s.charAt(j) == ' ')
+                        {
+                                return s.substring(0, j);
+                        }
+                }
+                return s.length() <= maxLength ? s : (maxLength < 6 ? "" : s.substring(0, maxLength - 3) + "...");
+        }
+
+        public void refreshColors(Iterable<String> list)
+        {
+                topColors = new HashMap<String, Color>();
+                double x = .1;
+                int i = 0;
+                for (String s : list)
+                {
+                        topColors.put(s, i < handPalette.length ? handPalette[i] : palette(x));
+                        i++;
+                        x += PHI;
+                }
+        }
+
+        public Color makeColor(String text)
+        {
+                if (grayFilter != null && !grayFilter.ok(text))
+                {
+                        return new Color(200, 200, 200);//"null.color");
+                }
+                Color c = topColors.get(text);
+                return c == null ? _makeColor(text) : c;
+        }
+
+        private Color _makeColor(String text)
+        {
+                if (text == null)
+                {
+                        return getColor("null.color");
+                }
+
+                int c = Math.abs(text.hashCode());
+                double h = ((c >> 8) % 255) / 255.;
+                return palette(h);
+        }
+
+        public static Color palette(double x)
+        {
+                float h = (float) (Math.abs(x) % 1);
+                float s = .8f - .25f * h;
+                float b = .8f - .25f * h;
+                return new Color(Color.HSBtoRGB(h, s, b));
+        }
+
+        public String getMiscLabel()
+        {
+                return getString("other.label");
+        }
+
+        public String getNullLabel()
+        {
+                return getString("null.label");
+        }
+
+        public String toString(Object o)
+        {
+                if (o == null)
+                {
+                        return getNullLabel();
+                }
+                if (o instanceof Object[])
+                {
+                        return arrayToString((Object[]) o);
+                }
+                if (o instanceof RoughTime)
+                {
+                        return ((RoughTime) o).format();//UnitOfTime.format((RoughTime)o);
+                }
+                if (o instanceof Double)
+                {
+                        return df.format((Double) o);
+                }
+                return o.toString();
+        }
+
+        public static String format(double x)
+        {
+                return Math.abs(x) > 999 ? roundFormat.format(x) : df.format(x);
+        }
+
+        public String format(RoughTime time)
+        {
+                if (time == null)
+                {
+                        return getString("null.label");
+                }
+                return time.format();//UnitOfTime.format(time);
+        }
+
+        public static ArrayList<String> breakLines(String s, int lineChars, int firstOffset)
+        {
+                ArrayList<String> lines = new ArrayList<String>();
+                String[] words = s.split(" ");
+                String line = "";
+
+                for (int i = 0; i < words.length; i++)
+                {
+                        // is the word just too, too long?
+                        int n = words[i].length();
+                        if (n > lineChars - 5)
+                        {
+                                words[i] = words[i].substring(0, lineChars / 2 - 2) + "..." + words[i].substring(n - lineChars / 2 + 2, n);
+                        }
+                        if (line.length() + words[i].length() > (lines.size() == 0 ? lineChars - firstOffset : lineChars))
+                        {
+                                lines.add(line);
+                                line = "";
+                        }
+                        line += " " + words[i];
+                }
+                lines.add(line);
+                return lines;
+        }
+
+        public boolean emptyMessage(Graphics g, TFModel model)
+        {
+                if (model.getActs() == null || model.getActs().size() == 0)
+                {
+                        g.setColor(getColor("text.prominent"));
+                        g.drawString(model.getDB() == null || model.getDB().size() == 0 ? "Empty Database" : "No items found.", 10, 25);
+                        return true;
+                }
+                return false;
+        }
+}
diff --git a/timeflow/model/ModelPanel.java b/timeflow/model/ModelPanel.java
new file mode 100755
index 0000000..4326f4e
--- /dev/null
+++ b/timeflow/model/ModelPanel.java
@@ -0,0 +1,36 @@
+package timeflow.model;
+
+import javax.swing.*;
+
+public abstract class ModelPanel extends JPanel implements TFListener {
+	
+	TFModel model;
+	
+	public ModelPanel(TFModel model)
+	{
+		this.model=model;
+	}
+	
+	@Override
+	public void addNotify()
+	{
+		super.addNotify();
+		model.addListener(this);
+	}
+	
+	
+	@Override
+	public void removeNotify()
+	{
+		super.removeNotify();
+		model.removeListener(this);
+	}
+	
+	public TFModel getModel()
+	{
+		return model;
+	}
+
+	@Override
+	public abstract void note(TFEvent e);
+}
diff --git a/timeflow/model/TFEvent.java b/timeflow/model/TFEvent.java
new file mode 100755
index 0000000..6c22b87
--- /dev/null
+++ b/timeflow/model/TFEvent.java
@@ -0,0 +1,43 @@
+package timeflow.model;
+
+public class TFEvent {
+	public enum Type {DATABASE_CHANGE, ACT_ADD, ACT_DELETE, ACT_CHANGE, ERROR, SOURCE_CHANGE, DESCRIPTION_CHANGE,
+					 FIELD_ADD, FIELD_DELETE, FIELD_CHANGE, SELECTION_CHANGE, FILTER_CHANGE, VIEW_CHANGE};
+	public Type type;
+	public String message="[]";
+	public Object info;
+	public Object origin;
+	
+	public TFEvent(Type type, Object origin)
+	{
+		this.type=type;
+		this.origin=origin;
+	}
+
+	public String toString()
+	{
+		return "[TimelineEvent: type="+type+", info="+info+", message="+message+", origin="+origin+"]";
+	}
+	
+	public boolean affectsSchema()
+	{
+		switch (type){
+			case DATABASE_CHANGE: 
+			case FIELD_ADD:
+			case FIELD_DELETE:
+			case FIELD_CHANGE: return true;
+		}
+		return false;
+	}
+	
+	public boolean affectsRowSet()
+	{
+		return affectsSchema() || type==Type.ACT_CHANGE || type== Type.ACT_ADD || type== Type.ACT_DELETE
+		|| type==Type.FILTER_CHANGE;
+	}
+	
+	public boolean affectsData()
+	{
+		return type!=Type.SELECTION_CHANGE && type!=Type.VIEW_CHANGE && type!=Type.ERROR;
+	}
+}
diff --git a/timeflow/model/TFListener.java b/timeflow/model/TFListener.java
new file mode 100755
index 0000000..3268d05
--- /dev/null
+++ b/timeflow/model/TFListener.java
@@ -0,0 +1,5 @@
+package timeflow.model;
+
+public interface TFListener {
+	public void note(TFEvent e);
+}
diff --git a/timeflow/model/TFModel.java b/timeflow/model/TFModel.java
new file mode 100755
index 0000000..f4e30e4
--- /dev/null
+++ b/timeflow/model/TFModel.java
@@ -0,0 +1,303 @@
+package timeflow.model;
+
+import timeflow.data.db.*;
+import timeflow.data.db.filter.*;
+import timeflow.data.time.*;
+
+
+import java.util.*;
+
+// encapsulates all properties of a timeline model:
+// data, display properties, etc.
+// also does listening, etc.
+public class TFModel
+{
+
+        private ActDB db;
+        private ActList acts;
+        private ActFilter filter = new ConstFilter(true);
+        private ArrayList<TFListener> listeners = new ArrayList<TFListener>();
+        private Display display = new Display();
+        private String[] labelGuesses =
+        {
+                "label", "LABEL", "Label", "title", "TITLE", "Title",
+                "name", "Name", "NAME"
+        };
+        private String[] startGuesses =
+        {
+                "start", "Start", "START"
+        };
+        private String dbFile = "[unknown source]";
+        private boolean changedSinceSave;
+        private boolean readOnly;
+        private double minSize, maxSize;
+        private Interval viewInterval;
+
+        public ValueFilter getGrayFilter()
+        {
+                return display.grayFilter;
+        }
+
+        public void setGrayFilter(ValueFilter grayFilter, Object origin)
+        {
+                display.grayFilter = grayFilter;
+                fireEvent(new TFEvent(TFEvent.Type.FILTER_CHANGE, origin));
+        }
+
+        public ActFilter getFilter()
+        {
+                return filter;
+        }
+
+        public Interval getViewInterval()
+        {
+                return viewInterval;
+        }
+
+        public void setViewInterval(Interval viewInterval)
+        {
+                this.viewInterval = viewInterval;
+        }
+
+        public double getMinSize()
+        {
+                return minSize;
+        }
+
+        public void setMinSize(double minSize)
+        {
+                this.minSize = minSize;
+        }
+
+        public double getMaxSize()
+        {
+                return maxSize;
+        }
+
+        public void setMaxSize(double maxSize)
+        {
+                this.maxSize = maxSize;
+        }
+
+        public String getDbFile()
+        {
+                return dbFile;
+        }
+
+        public boolean getReadOnly()
+        {
+                return readOnly;
+        }
+
+        public void setDbFile(String dbFile, boolean readOnly, Object origin)
+        {
+                this.dbFile = dbFile;
+                fireEvent(new TFEvent(TFEvent.Type.SOURCE_CHANGE, origin));
+        }
+
+        public boolean isChangedSinceSave()
+        {
+                return changedSinceSave;
+        }
+
+        public void setChangedSinceSave(boolean changedSinceSave)
+        {
+                this.changedSinceSave = changedSinceSave;
+        }
+
+        public Display getDisplay()
+        {
+                return display;
+        }
+
+        public ActDB getDB()
+        {
+                return db;
+        }
+
+        public ActList getActs()
+        {
+                return acts;
+        }
+
+        public void addListener(TFListener t)
+        {
+                listeners.add(t);
+        }
+
+        public void removeListener(TFListener t)
+        {
+                listeners.remove(t);
+        }
+
+        public void noteNewDescription(Object origin)
+        {
+                setChangedSinceSave(true);
+                fireEvent(new TFEvent(TFEvent.Type.DESCRIPTION_CHANGE, origin));
+        }
+
+        public void noteNewSource(Object origin)
+        {
+                setChangedSinceSave(true);
+                fireEvent(new TFEvent(TFEvent.Type.SOURCE_CHANGE, origin));
+        }
+
+        public void noteRecordChange(Object origin)
+        {
+                setChangedSinceSave(true);
+                fireEvent(new TFEvent(TFEvent.Type.ACT_CHANGE, origin));
+        }
+
+        public void noteAddField(Object origin)
+        {
+                setChangedSinceSave(true);
+                fireEvent(new TFEvent(TFEvent.Type.FIELD_ADD, origin));
+        }
+
+        public void noteError(Object origin)
+        {
+                fireEvent(new TFEvent(TFEvent.Type.ERROR, origin));
+        }
+
+        public void noteDelete(Object origin)
+        {
+                setChangedSinceSave(true);
+                updateActs();
+                fireEvent(new TFEvent(TFEvent.Type.ACT_DELETE, origin));
+        }
+
+        public void noteSchemaChange(Object origin)
+        {
+                setChangedSinceSave(true);
+                updateActs();
+                fireEvent(new TFEvent(TFEvent.Type.DATABASE_CHANGE, origin)); // @TODO: make schema change? 
+        }
+
+        public void noteAdd(Object origin)
+        {
+                setChangedSinceSave(true);
+                updateActs();
+                fireEvent(new TFEvent(TFEvent.Type.ACT_ADD, origin));
+        }
+
+        public void setFilter(ActFilter filter, Object origin)
+        {
+                this.filter = filter;
+                updateActs();
+                fireEvent(new TFEvent(TFEvent.Type.FILTER_CHANGE, origin));
+        }
+
+        private void updateActs()
+        {
+                acts = db.select(filter);
+        }
+
+        public void setDB(ActDB db, String dbFile, boolean readOnly, Object origin)
+        {
+                this.db = db;
+                this.dbFile = dbFile;
+                this.readOnly = readOnly;
+                setChangedSinceSave(false);
+                this.filter = new ConstFilter(true);
+                this.acts = db.all();
+                initVisualEncodings();
+                refreshColors();
+                fireEvent(new TFEvent(TFEvent.Type.DATABASE_CHANGE, origin));
+        }
+
+        private void initVisualEncodings()
+        {
+                guessField(VirtualField.LABEL, labelGuesses, String.class);
+                guessField(VirtualField.START, startGuesses, RoughTime.class);
+                viewInterval = null;
+        }
+
+        public void refreshColors()
+        {
+                display.grayFilter = null;
+                Field colorField = getColorField();
+                if (colorField == null)
+                {
+                        return;
+                }
+                List<String> top25 = DBUtils.countValues(db, colorField).listTop(25);
+                display.refreshColors(top25);
+        }
+
+        private void guessField(String name, String[] guesses, Class type)
+        {
+                Field field = db.getField(name);
+                if (field == null)
+                {
+                        for (int i = 0; i < guesses.length; i++)
+                        {
+                                Field f = db.getField(guesses[i]);
+                                if (f != null && f.getType() == type)
+                                {
+                                        field = f;
+                                        break;
+                                }
+                        }
+                        if (field == null)
+                        {
+                                List<Field> f = db.getFields(type);
+                                if (f.size() > 0)
+                                {
+                                        field = f.get(0);
+                                }
+                        }
+                        if (field != null)
+                        {
+                                db.setAlias(field, name);
+                        }
+                }
+        }
+
+        private void fireEvent(TFEvent e)
+        {
+                // clone list before going through it, because some events can cause
+                // listeners to be added or removed.
+
+                for (TFListener t : (List<TFListener>) listeners.clone())
+                {
+                        if (t != e.origin)
+                        {
+                                t.note(e);
+                        }
+                }
+        }
+
+        public void setFieldAlias(Field field, String alias, Object origin)
+        {
+                db.setAlias(field, alias);
+                if (db.size() > 0 && field == getColorField())
+                {
+                        refreshColors();
+                }
+                fireEvent(new TFEvent(TFEvent.Type.VIEW_CHANGE, origin));
+        }
+
+        public Field getColorField()
+        {
+                if (db == null)
+                {
+                        return null;
+                }
+                Field f = db.getField(VirtualField.COLOR);
+                if (f != null)
+                {
+                        return f;
+                }
+                return db.getField(VirtualField.TRACK);
+        }
+
+        public void setReadOnly(boolean readOnly)
+        {
+                this.readOnly = readOnly;
+        }
+
+        public boolean isReadOnly()
+        {
+                return readOnly;
+        }
+}
diff --git a/timeflow/model/VirtualField.java b/timeflow/model/VirtualField.java
new file mode 100755
index 0000000..f046653
--- /dev/null
+++ b/timeflow/model/VirtualField.java
@@ -0,0 +1,39 @@
+package timeflow.model;
+
+import java.util.*;
+
+public class VirtualField {
+	public static final String LABEL="TIMEFLOW_LABEL";
+	public static final String COLOR="TIMEFLOW_COLOR";
+	public static final String SIZE="TIMEFLOW_SIZE";
+	public static final String TRACK="TIMEFLOW_TRACK";
+	public static final String START="TIMEFLOW_START";
+	public static final String LATEST_START="TIMEFLOW_LATEST_START";
+	public static final String END="TIMEFLOW_END";
+	public static final String EARLIEST_END="TIMEFLOW_EARLIEST_END";
+	
+	private static HashMap<String, String> humanNames=new HashMap<String, String>();
+	private static void tie(String a, String b) {humanNames.put(a,b);}
+	
+	static
+	{
+		tie(LABEL, "Label");
+		tie(COLOR, "Color");
+		tie(SIZE, "Size");
+		tie(TRACK, "Track");
+		tie(START, "Start");
+		tie(LATEST_START, "Latest Start");
+		tie(END, "End");
+		tie(EARLIEST_END, "Earliest End");
+	}
+	
+	public static String humanName(String s)
+	{
+		return humanNames.get(s);
+	}
+	
+	public static Iterable<String> list()
+	{
+		return humanNames.keySet();
+	}
+}
diff --git a/timeflow/util/Bag.java b/timeflow/util/Bag.java
new file mode 100755
index 0000000..a575b55
--- /dev/null
+++ b/timeflow/util/Bag.java
@@ -0,0 +1,136 @@
+package timeflow.util;
+
+import java.util.*;
+
+public class Bag<T> implements Iterable<T> {
+	HashMap<T, Count> table;
+	int max;
+	
+	public Bag()
+	{
+		table=new HashMap<T, Count>();
+	}
+	
+	public Bag(Iterable<T> i)
+	{
+		for (T x:i)
+			add(x);
+	}
+	
+	public Bag(T[] array)
+	{
+		for (int i=0; i<array.length; i++)
+			add(array[i]);
+	}
+	
+	public int getMax()
+	{
+		return max;
+	}
+	
+	public List<T> listTop(int n)
+	{
+		int count=0;
+		Iterator<T> i=list().iterator();
+		List<T> top=new ArrayList<T>();
+		while (count<n && i.hasNext())
+		{
+			top.add(i.next());
+			count++;
+		}
+		return top;
+	}
+	
+	public List<T> unordered()
+	{
+		List<T> result=new ArrayList<T>();
+		result.addAll(table.keySet());
+		return result;
+	}
+	
+	public List<T> list()
+	{
+		List<T> result=new ArrayList<T>();
+		result.addAll(table.keySet());
+		
+		Collections.sort(result, new Comparator<T>()
+				{
+					public int compare(T x, T y)
+					{
+						return num(y)-num(x);
+					}
+				});
+		return result;
+	}
+	
+	public int num(T x)
+	{
+		Count c=table.get(x);
+		if (c!=null)
+			return c.num;
+		else
+			return 0;
+	}
+	
+	public int add(T x)
+	{		
+		Count c=table.get(x);
+		int n=0;
+		if (c!=null)
+			n=++c.num;
+		else
+		{
+			table.put(x, new Count(1));
+			n=1;
+		}
+		max=Math.max(n,max);
+		return n;
+	}
+	
+	class Count
+	{
+		int num;
+		public Count(int num)
+		{
+			this.num=num;
+		}
+	}
+	
+	public int size()
+	{
+		return table.size();
+	}
+	
+	public int removeLessThan(int cut)
+	{
+		
+		Set<T> small=new HashSet<T>();
+		for (T x: table.keySet())
+		{
+			if (num(x)<cut)
+				small.add(x);
+		}
+		for (T x:small)
+			table.remove(x);
+		return small.size();
+	}
+	
+	public static void main(String[] args)
+	{
+		Bag<String> b=new Bag<String>();
+		b.add("a");
+		b.add("b");
+		b.add("a");
+		System.out.println(b.num("a"));
+		System.out.println(b.num("b"));
+		System.out.println(b.num("c"));
+		List<String> s=b.list();
+		for (int i=0; i<s.size(); i++)
+			System.out.println(s.get(i)+": "+b.num(s.get(i)));
+	}
+
+	@Override
+	public Iterator<T> iterator() {
+		return table.keySet().iterator();
+	}
+}
diff --git a/timeflow/util/ColorUtils.java b/timeflow/util/ColorUtils.java
new file mode 100755
index 0000000..dffd8ec
--- /dev/null
+++ b/timeflow/util/ColorUtils.java
@@ -0,0 +1,31 @@
+package timeflow.util;
+
+import java.awt.*;
+import java.awt.image.*;
+
+public class ColorUtils
+{
+	
+	public static Color alpha(Color c, int a)
+	{
+		return new Color(c.getRed(), c.getGreen(), c.getBlue(),a);
+	}
+	
+	public static Color interpolate(Color x, Color y, double u)
+	{
+		return new Color(interp(x.getRed(), y.getRed(), u), 
+				         interp(x.getGreen(), y.getGreen(), u),
+				         interp(x.getBlue(), y.getBlue(), u));
+	}
+	
+	private static int interp(int x, int y, double u)
+	{
+		return (int)(y*u+x*(1-u));
+	}
+	
+	public static float[] hsb(Color c)
+	{
+		return Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), new float[3]);
+	}
+	
+}
diff --git a/timeflow/util/DoubleBag.java b/timeflow/util/DoubleBag.java
new file mode 100755
index 0000000..088b36d
--- /dev/null
+++ b/timeflow/util/DoubleBag.java
@@ -0,0 +1,145 @@
+package timeflow.util;
+
+import java.util.*;
+
+public class DoubleBag<T> implements Iterable<T> {
+	private HashMap<T, Count> table;
+	private double max;
+	
+	public DoubleBag()
+	{
+		table=new HashMap<T, Count>();
+	}
+
+	public double getMax()
+	{
+		return max;
+	}
+	
+	public List<T> listTop(int n, boolean useSum)
+	{
+		int count=0;
+		Iterator<T> i=list(useSum).iterator();
+		List<T> top=new ArrayList<T>();
+		while (count<n && i.hasNext())
+		{
+			top.add(i.next());
+			count++;
+		}
+		return top;
+	}
+	
+	public List<T> list(final boolean useSum)
+	{
+		List<T> result=new ArrayList<T>();
+		result.addAll(table.keySet());
+		
+		Collections.sort(result, new Comparator<T>()
+				{
+					public int compare(T x, T y)
+					{
+						double d= useSum ? num(y)-num(x) : average(y)-average(x);
+						return d>0 ? 1 : (d<0 ? -1 : 0);
+					}
+				});
+		return result;
+	}
+	
+	public double num(T x)
+	{
+		Count c=table.get(x);
+		if (c!=null)
+			return c.num;
+		else
+			return 0;
+	}
+	
+	public double average(T x)
+	{
+		Count c=table.get(x);
+		return c.num/c.vals;
+	}
+	
+	public void add(T x, double z)
+	{
+		if (Double.isNaN(z))
+			return;
+		Count c=table.get(x);
+		double sum=z;
+		if (c!=null)
+		{
+			 c.add(z);
+			 sum=c.num;
+		}
+		else
+		{
+			table.put(x, new Count(z));
+		}
+		max=Math.max(sum, max);
+	}
+	
+	class Count
+	{
+		double num;
+		int vals;
+		
+		public Count(double num)
+		{
+			this.num=num;
+			vals=1;
+		}
+		
+		public double add(double x)
+		{
+			vals++;
+			return num+=x;
+		}
+	}
+	
+	public int size()
+	{
+		return table.size();
+	}
+	
+	
+	public List<T> unordered()
+	{
+		List<T> result=new ArrayList<T>();
+		result.addAll(table.keySet());
+		return result;
+	}
+
+	
+	public int removeLessThan(int cut)
+	{
+		
+		Set<T> small=new HashSet<T>();
+		for (T x: table.keySet())
+		{
+			if (num(x)<cut)
+				small.add(x);
+		}
+		for (T x:small)
+			table.remove(x);
+		return small.size();
+	}
+	
+	public static void main(String[] args)
+	{
+		DoubleBag<String> b=new DoubleBag<String>();
+		b.add("a",1);
+		b.add("b",2);
+		b.add("a",3);
+		System.out.println(b.num("a"));
+		System.out.println(b.num("b"));
+		System.out.println(b.num("c"));
+		List<String> s=b.list(true);
+		for (int i=0; i<s.size(); i++)
+			System.out.println(s.get(i)+": "+b.num(s.get(i)));
+	}
+
+	@Override
+	public Iterator<T> iterator() {
+		return table.keySet().iterator();
+	}
+}
diff --git a/timeflow/util/IO.java b/timeflow/util/IO.java
new file mode 100755
index 0000000..888299b
--- /dev/null
+++ b/timeflow/util/IO.java
@@ -0,0 +1,54 @@
+package timeflow.util;
+
+import java.io.*;
+import java.util.*;
+
+public class IO {
+	
+	public static ArrayList<String> lines(String fileName) throws IOException
+	{
+		ArrayList<String> a=new ArrayList<String>();
+		String line=null;
+		FileReader fr=new FileReader(fileName);
+		BufferedReader in=new BufferedReader(fr);
+		while (null != (line=in.readLine()))
+			a.add(line);
+		in.close();
+		fr.close();
+		return a;
+	}
+	
+	public static String[] lineArray(String fileName) throws IOException
+	{
+		ArrayList<String> a=lines(fileName);
+		return (String[])a.toArray(new String[0]);
+	}
+	
+	public static String read(File file) throws IOException
+	{
+		char[] buffer = new char[1024];
+		int n = 0;
+		StringBuilder builder = new StringBuilder();
+		FileReader reader = new FileReader(file);
+		BufferedReader b = new BufferedReader(reader);
+		while ((n = b.read(buffer, 0, buffer.length)) != -1) 
+			builder.append(buffer, 0, n);
+		b.close();
+		reader.close();
+		return builder.toString();
+	}
+	
+	public static String read(String fileName) throws IOException
+	{
+		char[] buffer = new char[1024];
+		int n = 0;
+		StringBuilder builder = new StringBuilder();
+		FileReader reader = new FileReader(fileName);
+		BufferedReader b = new BufferedReader(reader);
+		while ((n = b.read(buffer, 0, buffer.length)) != -1) 
+			builder.append(buffer, 0, n);
+		b.close();
+		reader.close();
+		return builder.toString();
+	}
+}
diff --git a/timeflow/util/Pad.java b/timeflow/util/Pad.java
new file mode 100755
index 0000000..ce9b3d9
--- /dev/null
+++ b/timeflow/util/Pad.java
@@ -0,0 +1,19 @@
+package timeflow.util;
+
+import java.awt.*;
+import javax.swing.*;
+
+public class Pad extends JPanel {
+	Dimension pref;
+	
+	public Pad(int w, int h)
+	{
+		pref=new Dimension(w,h);
+		setBackground(Color.white);
+	}
+	
+	public Dimension getPreferredSize()
+	{
+		return pref;
+	}
+}
diff --git a/timeflow/util/TimeIt.java b/timeflow/util/TimeIt.java
new file mode 100755
index 0000000..f78ccb0
--- /dev/null
+++ b/timeflow/util/TimeIt.java
@@ -0,0 +1,34 @@
+package timeflow.util;
+
+import java.util.*;
+
+public class TimeIt {
+	public static long last;
+	static HashMap<Object, Long> marks=new HashMap<Object, Long>();
+	
+	public static void mark()
+	{
+		last=System.currentTimeMillis();
+	}
+	
+	public static void sinceLast()
+	{
+		long now=System.currentTimeMillis();
+		System.out.println("TimeIt: "+(now-last));
+		last=now;
+	}
+	
+	public static void since(Object o)
+	{
+		long now=System.currentTimeMillis();
+		System.out.println("TimeIt: "+o+": "+(now-last));
+		last=now;
+	}
+	
+	public static void mark(Object o)
+	{
+		long now=System.currentTimeMillis();
+		marks.put(o, System.currentTimeMillis());
+		last=now;
+	}
+}
diff --git a/timeflow/views/AbstractView.java b/timeflow/views/AbstractView.java
new file mode 100755
index 0000000..a56a933
--- /dev/null
+++ b/timeflow/views/AbstractView.java
@@ -0,0 +1,85 @@
+package timeflow.views;
+
+import javax.swing.*;
+
+import timeflow.data.db.ActDB;
+import timeflow.model.*;
+
+import java.awt.*;
+
+// superclass of all timeline views
+public abstract class AbstractView extends ModelPanel
+{
+
+        protected boolean ignoreEventsWhenInvisible = true;
+        JPanel panel;
+        ActDB lastDrawn, lastNotified;
+
+        public AbstractView(TFModel model)
+        {
+                super(model);
+        }
+
+        public void paintComponent(Graphics g)
+        {
+                g.drawString(getName(), 10, 30);
+        }
+
+        public final JComponent getControls()
+        {
+                if (panel != null)
+                {
+                        return panel;
+                }
+
+                panel = new JPanel();
+                panel.setLayout(new BorderLayout());
+                panel.add(_getControls(), BorderLayout.CENTER);
+                JLabel controlLabel = new JLabel(" " + getName() + " Controls")
+                {
+
+                        public Dimension getPreferredSize()
+                        {
+                                return new Dimension(30, 30);
+                        }
+                };
+                controlLabel.setBackground(Color.lightGray);
+                controlLabel.setForeground(Color.darkGray);
+                panel.add(controlLabel, BorderLayout.NORTH);
+
+                return panel;
+        }
+
+        protected JComponent _getControls()
+        {
+                return new JLabel("local: " + getName());
+        }
+
+        public abstract String getName();
+
+        protected abstract void _note(TFEvent e);
+
+        protected abstract void onscreen(boolean majorChangeHappened);
+
+        @Override
+        public final void note(TFEvent e)
+        {
+                lastNotified = getModel().getDB();
+                if (isVisible() || !ignoreEventsWhenInvisible)
+                {
+                        _note(e);
+                        lastDrawn = lastNotified;
+                }
+        }
+
+        @Override
+        public void setVisible(boolean visible)
+        {
+                super.setVisible(visible);
+                if (visible && getModel().getDB() != null)
+                {
+                        onscreen(lastDrawn != lastNotified);
+                        lastDrawn = lastNotified;
+                }
+        }
+}
diff --git a/timeflow/views/AbstractVisualizationView.java b/timeflow/views/AbstractVisualizationView.java
new file mode 100755
index 0000000..51026d6
--- /dev/null
+++ b/timeflow/views/AbstractVisualizationView.java
@@ -0,0 +1,244 @@
+package timeflow.views;
+
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+import timeflow.model.*;
+import timeflow.vis.*;
+import timeflow.app.ui.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.image.BufferedImage;
+import java.net.URL;
+import java.util.*;
+
+import javax.swing.*;
+
+public abstract class AbstractVisualizationView extends JPanel
+{
+
+        Image buffer;
+        Graphics2D graphics;
+        Point mouse = new Point(-10000, 0), firstMouse = new Point();
+        boolean mouseIsDown;
+        ArrayList<Mouseover> objectLocations = new ArrayList<Mouseover>();
+        TFModel model;
+        Act selectedAct;
+        RoughTime selectedTime;
+        Set<JMenuItem> urlItems = new HashSet<JMenuItem>();
+
+        public AbstractVisualizationView(TFModel model)
+        {
+                this.model = model;
+
+                // deal with mouseovers.
+                addMouseMotionListener(new MouseMotionListener()
+                {
+
+                        @Override
+                        public void mouseDragged(MouseEvent e)
+                        {
+                                mouse.setLocation(e.getX(), e.getY());
+                                repaint();
+                        }
+
+                        @Override
+                        public void mouseMoved(MouseEvent e)
+                        {
+                                mouse.setLocation(e.getX(), e.getY());
+                                repaint();
+                        }
+                });
+
+
+                final JPopupMenu popup = new JPopupMenu();
+                final JMenuItem edit = new JMenuItem("Edit");
+                edit.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                EditRecordPanel.edit(getModel(), selectedAct);
+                        }
+                });
+                popup.add(edit);
+
+                final JMenuItem delete = new JMenuItem("Delete");
+                popup.add(delete);
+                delete.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                getModel().getDB().delete(selectedAct);
+                                getModel().noteDelete(this);
+                        }
+                });
+
+                final JMenuItem add = new JMenuItem("New...");
+                popup.add(add);
+                add.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                EditRecordPanel.add(getModel(), selectedTime);
+                        }
+                });
+
+                // deal with right-click.
+                addMouseListener(new MouseAdapter()
+                {
+
+                        public void mousePressed(MouseEvent e)
+                        {
+                                pop(e);
+                        }
+
+                        public void mouseReleased(MouseEvent e)
+                        {
+                                pop(e);
+                        }
+
+                        private void pop(MouseEvent e)
+                        {
+                                if (e.isPopupTrigger())
+                                {
+                                        Point p = new Point(e.getX(), e.getY());
+                                        Mouseover o = find(p);
+                                        boolean onAct = o != null && o.thing instanceof VisualAct;
+                                        if (onAct)
+                                        {
+                                                VisualAct v = (VisualAct) o.thing;
+                                                selectedAct = v.getAct();
+                                                String name = " '" + v.getLabel() + "'";
+                                                edit.setText("Edit" + name + "...");
+                                                delete.setText("Delete" + name);
+                                                edit.setEnabled(true);
+                                                delete.setEnabled(true);
+                                        } else
+                                        {
+                                                edit.setEnabled(false);
+                                                edit.setText("Edit Event");
+                                                delete.setEnabled(false);
+                                                delete.setText("Delete Event");
+                                        }
+                                        selectedTime = getTime(p);
+                                        if (selectedTime != null || onAct)
+                                        {
+                                                add.setEnabled(selectedTime != null);
+                                                add.setText(selectedTime == null ? "Add" : "Add Event At " + selectedTime.format() + "...");
+
+                                                java.util.List<Field> urlFields = getModel().getDB().getFields(URL.class);
+                                                if (urlFields.size() > 0)
+                                                {
+                                                        // remove any old items.
+                                                        for (JMenuItem m : urlItems)
+                                                        {
+                                                                popup.remove(m);
+                                                        }
+                                                        urlItems.clear();
+
+                                                        if (onAct)
+                                                        {
+                                                                Act a = ((VisualAct) o.thing).getAct();
+                                                                for (Field f : urlFields)
+                                                                {
+                                                                        final URL url = a.getURL(f);
+                                                                        JMenuItem go = new JMenuItem("Go to " + url);
+                                                                        go.addActionListener(new ActionListener()
+                                                                        {
+
+                                                                                @Override
+                                                                                public void actionPerformed(ActionEvent e)
+                                                                                {
+                                                                                        Display.launchBrowser(url.toString());
+                                                                                }
+                                                                        });
+                                                                        popup.add(go);
+                                                                        urlItems.add(go);
+                                                                }
+                                                        }
+                                                }
+
+                                                popup.show(e.getComponent(), p.x, p.y);
+                                        }
+                                }
+                        }
+                });
+        }
+
+        public RoughTime getTime(Point p)
+        {
+                return null;
+        }
+
+        public TFModel getModel()
+        {
+                return model;
+        }
+
+        @Override
+        public void setBounds(int x, int y, int w, int h)
+        {
+                super.setBounds(x, y, w, h);
+                if (w > 0 && h > 0)
+                {
+                        if (graphics != null)
+                        {
+                                graphics.dispose();
+                        }
+                        buffer = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+                        graphics = (Graphics2D) buffer.getGraphics();
+                        drawVisualization();
+                        repaint();
+                }
+
+        }
+
+        void drawVisualization()
+        {
+                drawVisualization(graphics);
+        }
+
+        protected abstract void drawVisualization(Graphics2D g);
+
+        protected boolean paintOnTop(Graphics2D g, int w, int h)
+        {
+                return false;
+        }
+
+        protected Mouseover find(Point p)
+        {
+                for (Mouseover o : objectLocations)
+                {
+                        if (o.contains(mouse))
+                        {
+                                return o;
+                        }
+                }
+                return null;
+        }
+
+        public final void paintComponent(Graphics g1)
+        {
+                Graphics2D g = (Graphics2D) g1;
+                g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+                g.drawImage(buffer, 0, 0, null);
+                
+                int w = getSize().width, h = getSize().height;
+                if (paintOnTop(g, w, h))
+                {
+                        return;
+                }
+                
+                Mouseover highlight = find(mouse);
+                if (highlight != null)
+                {
+                        highlight.draw(g, w, h, getModel().getDisplay());
+                }
+        }
+}
diff --git a/timeflow/views/BarGraphView.java b/timeflow/views/BarGraphView.java
new file mode 100755
index 0000000..a55ad05
--- /dev/null
+++ b/timeflow/views/BarGraphView.java
@@ -0,0 +1,363 @@
+package timeflow.views;
+
+import timeflow.model.*;
+import timeflow.views.ListView.LinkIt;
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+
+import javax.swing.*;
+
+import timeflow.util.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+
+public class BarGraphView extends AbstractView
+{
+
+        BarGraph graph = new BarGraph();
+        JPanel controls;
+        ArrayList<BarData> bars;
+
+        enum Aggregate
+        {
+
+                TOTAL, AVERAGE, COUNT
+        };
+        Aggregate agg;
+        JComboBox splitFieldChoice, numFieldChoice;
+
+        public BarGraphView(TFModel model)
+        {
+                super(model);
+
+                setLayout(new BorderLayout());
+                controls = new JPanel();
+                add(controls, BorderLayout.NORTH);
+                controls.setLayout(null);
+                controls.setBackground(Color.white);
+
+                JScrollPane scrollPane = new JScrollPane(graph);
+                add(scrollPane, BorderLayout.CENTER);
+
+                makeTop();
+        }
+
+        protected JComponent _getControls()
+        {
+                return controls;
+        }
+
+        void makeTop()
+        {
+                int x = 10, y = 10;
+                int ch = 25, pad = 5, cw = 160;
+
+                controls.removeAll();
+                TFModel model = getModel();
+                if (model.getDB() == null || model.getDB().size() == 0)
+                {
+                        JLabel empty = new JLabel("Empty database");
+                        controls.add(empty);
+                        empty.setBounds(x, y, cw, ch);
+                        return;
+                }
+
+                JLabel top = new JLabel("For each value of");
+                controls.add(top);
+                top.setBounds(x, y, cw, ch);
+                y += ch + pad;
+
+                splitFieldChoice = new JComboBox();
+                String splitSelection = null;
+                for (Field f : DBUtils.categoryFields(model.getDB()))
+                {
+                        splitFieldChoice.addItem(f.getName());
+                        if (f == graph.splitField)
+                        {
+                                splitSelection = f.getName();
+                        }
+                }
+                controls.add(splitFieldChoice);
+                splitFieldChoice.setBounds(x, y, cw, ch);
+                y += ch + 3 * pad;
+
+                if (splitSelection != null)
+                {
+                        splitFieldChoice.setSelectedItem(splitSelection);
+                } else if (getModel().getColorField() != null)
+                {
+                        splitFieldChoice.setSelectedItem(getModel().getColorField().getName());
+                }
+                splitFieldChoice.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                graph.redo();
+                        }
+                });
+
+                JLabel showLabel = new JLabel("show");
+                controls.add(showLabel);
+                showLabel.setBounds(x, y, cw, ch);
+                y += ch + pad;
+
+                numFieldChoice = new JComboBox();
+                numFieldChoice.addItem("Number of events");
+                final ArrayList<Field> valueFields = new ArrayList<Field>();
+                for (Field f : model.getDB().getFields(Double.class))
+                {
+                        numFieldChoice.addItem("Total: " + f.getName());
+                        numFieldChoice.addItem("Average: " + f.getName());
+                        valueFields.add(f);
+                }
+                controls.add(numFieldChoice);
+                numFieldChoice.setBounds(x, y, cw, ch);
+
+                boolean chosen = false;
+                for (int i = 0; i < numFieldChoice.getItemCount(); i++)
+                {
+                        if (numFieldChoice.getItemAt(i).equals(graph.lastValueMenuChoice))
+                        {
+                                numFieldChoice.setSelectedIndex(i);
+                                chosen = true;
+                        }
+                }
+                if (!chosen)
+                {
+                        Field size = getModel().getDB().getField(VirtualField.SIZE);
+                        if (size != null)
+                        {
+                                numFieldChoice.setSelectedItem("Total: " + size.getName());
+                        }
+                }
+                numFieldChoice.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+
+                                graph.redo();
+                        }
+                });
+                revalidate();
+                repaint();
+        }
+
+        void reset()
+        {
+                makeTop();
+                graph.redo();
+                revalidate();
+                repaint();
+        }
+
+        @Override
+        protected void _note(TFEvent e)
+        {
+                if (e.affectsSchema())
+                {
+                        reset();
+                } else
+                {
+                        graph.redo();
+                }
+                repaint();
+        }
+
+        @Override
+        public String getName()
+        {
+                return "Bar Graph";
+        }
+
+        @Override
+        protected void onscreen(boolean majorChange)
+        {
+                reset();
+        }
+
+        class BarData
+        {
+
+                Object thing;
+                double num;
+
+                BarData(Object thing, double num)
+                {
+                        this.thing = thing;
+                        this.num = num;
+                }
+        }
+
+        class BarGraph extends JPanel
+        {
+
+                int numVals = 0;
+                int rowHeight = 30;
+                int barHeight = 20;
+                int labelX = 10, barLeft = 300, barRight;
+                int topY = 45;
+                int numX = 210;
+                Field splitField, valueField;
+                String lastValueMenuChoice;
+                double min, max;
+
+                void redo()
+                {
+                        bars = new ArrayList<BarData>();
+                        splitField = getModel().getDB().getField((String) splitFieldChoice.getSelectedItem());
+                        if (splitField != null)
+                        {
+                                int n = numFieldChoice.getSelectedIndex();
+
+                                if (n == 0)
+                                {
+                                        agg = Aggregate.COUNT;
+                                } else
+                                {
+                                        agg = n % 2 == 1 ? Aggregate.TOTAL : Aggregate.AVERAGE;
+                                }
+
+                                if (agg == Aggregate.COUNT)
+                                {
+                                        Bag<String> bag = DBUtils.countValues(getModel().getActs(), splitField);
+                                        for (String s : bag.list())
+                                        {
+                                                bars.add(new BarData(s, bag.num(s)));
+                                        }
+                                } else
+                                {
+                                        lastValueMenuChoice = (String) numFieldChoice.getSelectedItem();
+                                        int colon = lastValueMenuChoice.indexOf(':');
+                                        valueField = getModel().getDB().getField(lastValueMenuChoice.substring(colon + 2));
+                                        DoubleBag<String> bag = new DoubleBag<String>();
+                                        for (Act a : getModel().getActs())
+                                        {
+                                                if (splitField.getType() == String.class)
+                                                {
+                                                        bag.add(a.getString(splitField), a.getValue(valueField));
+                                                } else
+                                                {
+                                                        String[] tags = a.getTextList(splitField);
+                                                        for (String tag : tags)
+                                                        {
+                                                                bag.add(tag, a.getValue(valueField));
+                                                        }
+                                                }
+                                        }
+                                        boolean isSum = agg == Aggregate.TOTAL;
+                                        for (String s : bag.list(isSum))
+                                        {
+                                                bars.add(new BarData(s, isSum ? bag.num(s) : bag.average(s)));
+                                        }
+                                }
+                        }
+                        revalidate();
+                        repaint();
+                }
+
+                public void paintComponent(Graphics g1)
+                {
+                        Graphics2D g = (Graphics2D) g1;
+                        int w = getSize().width, h = getSize().height;
+                        g.setColor(Color.white);
+                        g.fillRect(0, 0, w, h);
+                        TFModel model = getModel();
+                        Display display = model.getDisplay();
+
+                        if (display.emptyMessage(g, model))
+                        {
+                                return;
+                        }
+
+                        if (bars == null)
+                        {
+                                return;
+                        }
+
+                        if (bars.size() == 0)
+                        {
+                                g.setColor(Color.gray);
+                                g.drawString("(No data selected.)", 10, 30);
+                                return;
+                        }
+
+                        int n = bars.size();
+                        max = bars.get(0).num;
+                        min = Math.min(0, bars.get(n - 1).num);
+
+
+                        barRight = w - 30;
+
+                        int zero = scaleX(0);
+                        boolean isInColor = (splitField != null && getModel().getColorField() == splitField);
+
+                        // draw header
+                        int titleY = topY - 15;
+                        g.setColor(Color.black);
+                        g.setFont(display.big());
+                        g.drawString(splitField.getName().toUpperCase(), labelX, titleY);
+                        String aggLabel = agg.toString();
+                        if (agg != Aggregate.COUNT)
+                        {
+                                aggLabel += " " + valueField.getName().toUpperCase();
+                        }
+                        g.drawString(aggLabel, barLeft, titleY);
+                        g.setFont(display.plain());
+                        FontMetrics fm = display.plainFontMetrics();
+                        // draw bars
+
+                        for (int i = 0; i < n; i++)
+                        {
+                                int y = topY + i * rowHeight;
+                                int ty = y + barHeight;
+                                BarData data = bars.get(i);
+
+                                Color c = null;
+
+                                g.setColor(Color.gray);
+
+                                // label value
+                                boolean missing = data.thing == null || (data.thing.toString().length() == 0);
+                                String label = missing ? "[missing]"
+                                        : display.format(display.toString(data.thing), 25, false);
+                                if (isInColor)
+                                {
+                                        g.setColor(missing ? Color.gray : display.makeColor(data.thing.toString()));
+                                        display.makeColor(label);
+                                }
+                                g.drawString(label, labelX, ty);
+
+                                // label number
+                                String numLabel = display.format(data.num);
+                                g.drawString(numLabel, numX + 70 - fm.stringWidth(numLabel), ty);
+
+                                // draw bar.
+                                g.setColor(missing ? Color.lightGray : (isInColor ? c : Display.barColor));
+                                int x = scaleX(data.num);
+                                int a = Math.min(x, zero);
+                                int b = Math.max(x, zero);
+                                g.fillRect(a, y + 5, b - a, barHeight);
+                        }
+                }
+
+                int scaleX(double x)
+                {
+                        if (max == min)
+                        {
+                                return barLeft;
+                        }
+                        return (int) (barLeft + (barRight - barLeft) * (x - min) / (max - min));
+                }
+
+                public Dimension getPreferredSize()
+                {
+                        return new Dimension(400, 100 + rowHeight * (bars == null ? 0 : bars.size()));
+                }
+        }
+}
diff --git a/timeflow/views/CalendarView.java b/timeflow/views/CalendarView.java
new file mode 100755
index 0000000..354b939
--- /dev/null
+++ b/timeflow/views/CalendarView.java
@@ -0,0 +1,304 @@
+package timeflow.views;
+
+import timeflow.app.ui.ComponentCluster;
+import timeflow.data.db.*;
+import timeflow.data.time.Interval;
+import timeflow.data.time.RoughTime;
+import timeflow.model.*;
+import timeflow.vis.*;
+import timeflow.vis.calendar.*;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.AdjustmentEvent;
+import java.awt.event.AdjustmentListener;
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.Date;
+
+public class CalendarView extends AbstractView {
+
+	CalendarPanel calendarPanel;
+	ScrollingCalendar scroller;
+	CalendarVisuals visuals;
+	ActDB lastDB;
+	JPanel controls;
+	
+	@Override
+	public JComponent _getControls()
+	{
+		return controls;
+	}
+	
+	public CalendarView(TFModel model)
+	{
+		super(model);
+		calendarPanel=new CalendarPanel(model);
+		scroller=new ScrollingCalendar();
+		setLayout(new GridLayout(1,1));
+		add(scroller);
+		
+		controls=new JPanel();
+		controls.setLayout(new BorderLayout());
+
+		ComponentCluster units=new ComponentCluster("Grid");
+		controls.add(units, BorderLayout.NORTH);
+		
+		ButtonGroup unitGroup=new ButtonGroup();
+		
+		JRadioButton days=new JRadioButton(new ImageIcon("images/button_days.gif"),true);
+		days.setSelectedIcon(new ImageIcon("images/button_days_selected.gif"));
+		units.addContent(days);
+		days.addActionListener(new LayoutSetter(CalendarVisuals.Layout.DAY));
+		unitGroup.add(days);
+
+		JRadioButton months=new JRadioButton(new ImageIcon("images/button_months.gif"),false);
+		months.setSelectedIcon(new ImageIcon("images/button_months_selected.gif"));
+		units.addContent(months);
+		months.addActionListener(new LayoutSetter(CalendarVisuals.Layout.MONTH));
+		unitGroup.add(months);
+		
+		JRadioButton years=new JRadioButton(new ImageIcon("images/button_years.gif"),false);
+		years.setSelectedIcon(new ImageIcon("images/button_years_selected.gif"));
+		units.addContent(years);
+		years.addActionListener(new LayoutSetter(CalendarVisuals.Layout.YEAR));	
+		unitGroup.add(years);
+		
+		
+		ComponentCluster showCluster=new ComponentCluster("Show");
+		controls.add(showCluster, BorderLayout.CENTER);
+		
+		ButtonGroup group=new ButtonGroup();
+		JRadioButton icon=new JRadioButton(new ImageIcon("images/button_dots.gif"),true);
+		icon.setSelectedIcon(new ImageIcon("images/button_dots_selected.gif"));
+		showCluster.addContent(icon);
+		icon.addActionListener(new DrawStyleSetter(CalendarVisuals.DrawStyle.ICON));
+		group.add(icon);
+		
+		JRadioButton label=new JRadioButton(new ImageIcon("images/button_labels.gif"),false);
+		label.setSelectedIcon(new ImageIcon("images/button_labels_selected.gif"));
+		showCluster.addContent(label);
+		label.addActionListener(new DrawStyleSetter(CalendarVisuals.DrawStyle.LABEL));						
+		group.add(label);
+
+		ComponentCluster layout=new ComponentCluster("Layout");
+		controls.add(layout, BorderLayout.SOUTH);
+		
+		ButtonGroup layoutGroup=new ButtonGroup();
+		JRadioButton loose=new JRadioButton(new ImageIcon("images/button_expanded.gif"),true);
+		loose.setSelectedIcon(new ImageIcon("images/button_expanded_selected.gif"));
+		layout.addContent(loose);
+		loose.addActionListener(new FitStyleSetter(CalendarVisuals.FitStyle.LOOSE));
+		layoutGroup.add(loose);
+		
+		JRadioButton tight=new JRadioButton(new ImageIcon("images/button_compressed.gif"),false);
+		tight.setSelectedIcon(new ImageIcon("images/button_compressed_selected.gif"));
+		layout.addContent(tight);
+		tight.addActionListener(new FitStyleSetter(CalendarVisuals.FitStyle.TIGHT));
+		layoutGroup.add(tight);
+	}
+	
+	class LayoutSetter implements ActionListener
+	{
+		CalendarVisuals.Layout layout;
+		LayoutSetter(CalendarVisuals.Layout layout)
+		{
+			this.layout=layout;
+		}
+		@Override
+		public void actionPerformed(ActionEvent e) {
+			setLayoutStyle(layout);
+		}		
+	}
+	
+	class DrawStyleSetter implements ActionListener
+	{
+		CalendarVisuals.DrawStyle style;
+		DrawStyleSetter(CalendarVisuals.DrawStyle style)
+		{
+			this.style=style;
+		}
+		@Override
+		public void actionPerformed(ActionEvent e) {
+			setDrawStyle(style);
+		}		
+	}
+	
+	class FitStyleSetter implements ActionListener
+	{
+		CalendarVisuals.FitStyle style;
+		FitStyleSetter(CalendarVisuals.FitStyle style)
+		{
+			this.style=style;
+		}
+		@Override
+		public void actionPerformed(ActionEvent e) {
+			setFitStyle(style);
+		}		
+	}
+	
+	@Override
+	public String getName() {
+		return "Calendar";
+	}
+
+	private void redraw(boolean fresh)
+	{
+		visuals.makeGrid(fresh);
+		calendarPanel.drawVisualization();
+		repaint();
+	}
+
+	void setLayoutStyle(CalendarVisuals.Layout layout)
+	{
+		visuals.setLayoutStyle(layout);
+		calendarPanel.drawVisualization();
+		revalidate();
+		repaint();
+	}
+
+	void setDrawStyle(CalendarVisuals.DrawStyle style)
+	{
+		visuals.setDrawStyle(style);
+		calendarPanel.drawVisualization();
+		revalidate();
+		repaint();
+	}
+
+	void setFitStyle(CalendarVisuals.FitStyle style)
+	{
+		visuals.setFitStyle(style);
+		calendarPanel.drawVisualization();
+		revalidate();
+		repaint();
+	}
+	
+	@Override
+	protected void onscreen(boolean majorChange)
+	{
+		visuals.initAllButGrid();
+		scroller.calibrate(true);
+		revalidate();
+		ActDB db=getModel().getDB();
+		redraw(majorChange);
+		scroller.calibrate(majorChange);
+		lastDB=db;
+	}
+	
+	@Override
+	protected void _note(TFEvent e) {
+		int oldHeight=calendarPanel.getPreferredSize().height;
+		visuals.note(e);
+		
+		calendarPanel.drawVisualization();
+		calendarPanel.repaint();
+		if (e.affectsData() || oldHeight!=calendarPanel.getPreferredSize().height)
+		{
+			SwingUtilities.invokeLater(new Runnable() {
+				public void run() {scroller.calibrate(false); revalidate();}
+			});
+		}
+		revalidate();
+	}
+	
+	public void setBounds(int x, int y, int w, int h)
+	{
+		super.setBounds(x,y,w,h);
+		if (visuals==null || visuals.grid==null)
+			return;
+		calendarPanel.drawVisualization();
+		calendarPanel.repaint();
+	}
+	
+	class ScrollingCalendar extends JPanel
+	{
+		JScrollBar bar;
+		public ScrollingCalendar()
+		{
+			setLayout(new BorderLayout());
+			add(calendarPanel, BorderLayout.CENTER);
+			bar=new JScrollBar(JScrollBar.VERTICAL);
+			add(bar, BorderLayout.EAST);
+			bar.addAdjustmentListener(new AdjustmentListener() {			
+				@Override
+				public void adjustmentValueChanged(AdjustmentEvent e) {
+					visuals.grid.setDY(bar.getValue());
+					
+					// set time in model.
+					RoughTime startTime=visuals.grid.getFirstDrawnTime();
+					Interval viewInterval=getModel().getViewInterval();
+					if (viewInterval!=null)
+					{
+						viewInterval.translateTo(startTime.getTime());
+					}
+					
+					calendarPanel.drawVisualization();
+					calendarPanel.repaint();
+				}
+			});
+		}
+		
+		public void setBounds(int x, int y, int w, int h)
+		{
+			if (x==getX() && y==getY() && w==getWidth() && h==getHeight())
+				return;
+			super.setBounds(x,y,w,h);
+			calibrate(false);
+		}
+		
+		void calibrate(boolean forceValue)
+		{
+			if (visuals==null || visuals.grid==null)
+				return;
+			int height=getSize().height;
+			int desired=visuals.grid.getCalendarHeight();
+			bar.setVisible(desired>height);
+			if (desired>height)
+			{
+				bar.setMinimum(0);
+				bar.setMaximum(desired);
+				bar.setVisibleAmount(height);
+				Interval view=getModel().getViewInterval();
+				if (view!=null && forceValue)
+				{					
+					double s=visuals.grid.getScrollFraction();
+					double maxFraction=(desired-height)/(double)desired;
+					int value=(int)((s/maxFraction)*desired);
+					bar.setValue(value);
+				}
+				
+			}
+		}
+	}
+	
+	class CalendarPanel extends AbstractVisualizationView
+	{		
+
+		CalendarPanel(TFModel model)
+		{
+			super(model);
+			setBackground(Color.white);
+			visuals=new CalendarVisuals(getModel());
+		}	
+
+		public RoughTime getTime(Point p)
+		{
+			return visuals.grid.getTime(p.x, p.y); 
+		}
+		
+		protected void drawVisualization(Graphics2D g)
+		{
+			g.setBackground(Color.white);
+			g.fillRect(0,0,getSize().width, getSize().height);
+
+			getModel().getDisplay().emptyMessage(g, model);
+			if (model.getDB()==null)
+				return;
+			visuals.setBounds(0,0,getSize().width,getSize().height);	
+			objectLocations=new ArrayList<Mouseover>();
+			visuals.render(g, objectLocations);
+		}
+	}
+}
diff --git a/timeflow/views/DescriptionView.java b/timeflow/views/DescriptionView.java
new file mode 100755
index 0000000..45d66c7
--- /dev/null
+++ b/timeflow/views/DescriptionView.java
@@ -0,0 +1,71 @@
+package timeflow.views;
+
+import timeflow.model.*;
+import timeflow.util.Pad;
+import timeflow.data.db.*;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+public class DescriptionView extends AbstractView {
+
+	JTextArea content;
+	JComponent controls;
+	
+	public DescriptionView(TFModel model) {
+		super(model);
+		setLayout(new BorderLayout());
+		JPanel left=new Pad(5,5);
+		left.setBackground(Color.white);
+		add(left, BorderLayout.WEST);
+		JPanel right=new Pad(5,5);
+		right.setBackground(Color.white);
+		add(right, BorderLayout.EAST);
+		JPanel top=new JPanel();
+		add(top, BorderLayout.NORTH);
+		top.setLayout(new FlowLayout(FlowLayout.LEFT));
+		top.add(new JLabel("Notes & Comments on This Data:"));
+		content=new JTextArea();
+		content.setLineWrap(true);
+		content.setWrapStyleWord(true);
+		add(content, BorderLayout.CENTER);
+		content.addKeyListener(new KeyAdapter() {
+			@Override
+			public void keyReleased(KeyEvent e) {
+				getModel().getDB().setDescription(content.getText());
+				getModel().noteNewDescription(DescriptionView.this);
+			}});
+		controls=new HtmlControls("Each TimeFlow data set<br> comes with a free-form <br> "+
+				"description area. <p>This is a good place to write<br> notes "+
+				"about sources, how the data<br>was cleaned, etc.");
+	}
+	
+	@Override
+	public JComponent _getControls()
+	{
+		return controls;
+	}
+	
+	@Override
+	protected void _note(TFEvent e) {
+		if (e.type==TFEvent.Type.DESCRIPTION_CHANGE || e.type==TFEvent.Type.DATABASE_CHANGE)
+		{
+			content.setText(getModel().getDB().getDescription());
+			repaint();
+		}
+	}
+
+	@Override
+	public String getName() {
+		return "Notes";
+	}
+
+	@Override
+	protected void onscreen(boolean majorChange) {
+		ActDB db=getModel().getDB();
+		content.setText(db.getDescription());
+		content.requestFocus();
+	}
+}
diff --git a/timeflow/views/HtmlControls.java b/timeflow/views/HtmlControls.java
new file mode 100755
index 0000000..534e5ba
--- /dev/null
+++ b/timeflow/views/HtmlControls.java
@@ -0,0 +1,21 @@
+package timeflow.views;
+
+import java.awt.Color;
+import java.awt.GridLayout;
+
+import javax.swing.*;
+
+import timeflow.app.ui.HtmlDisplay;
+
+public class HtmlControls extends JPanel {
+	
+	public HtmlControls(String text)
+	{
+		JEditorPane html=HtmlDisplay.create();
+		html.setText(text);
+		setBackground(Color.white);
+		setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
+		setLayout(new GridLayout(1,1));
+		add(html);
+	}
+}
diff --git a/timeflow/views/IntroView.java b/timeflow/views/IntroView.java
new file mode 100755
index 0000000..8540401
--- /dev/null
+++ b/timeflow/views/IntroView.java
@@ -0,0 +1,105 @@
+package timeflow.views;
+
+import timeflow.app.ui.EditRecordPanel;
+import timeflow.data.analysis.*;
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+import timeflow.format.field.FieldFormatCatalog;
+import timeflow.model.*;
+import timeflow.views.*;
+import timeflow.views.ListView.LinkIt;
+
+import java.awt.*;
+import java.io.File;
+import java.net.URL;
+import java.util.Date;
+
+import javax.imageio.ImageIO;
+import javax.swing.JComponent;
+import javax.swing.JEditorPane;
+import javax.swing.JScrollPane;
+import javax.swing.UIManager;
+import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.HyperlinkListener;
+import javax.swing.table.TableModel;
+import javax.swing.text.html.HTMLDocument;
+import javax.swing.text.html.StyleSheet;
+
+import timeflow.util.*;
+
+public class IntroView extends AbstractView
+{
+
+        private JComponent controls;
+        Image image;
+        Image repeat;
+
+        public IntroView(TFModel model)
+        {
+                super(model);
+                setBackground(Color.white);
+                try
+                {
+                        image = ImageIO.read(new File("images/intro.gif"));
+                        repeat = ImageIO.read(new File("images/repeat.gif"));
+                } catch (Exception e)
+                {
+                        System.out.println("Couldn't load images.");
+                        e.printStackTrace(System.out);
+                }
+                makeHtml();
+        }
+
+        public void paintComponent(Graphics g)
+        {
+                g.setColor(Color.white);
+                int w = getSize().width, h = getSize().height;
+                g.fillRect(0, 0, w, h);
+                // draw image and extensible background, so it looks cool on a big screen.
+                if (image != null && repeat != null)
+                {
+                        int ih = image.getHeight(null);
+                        int iw = image.getWidth(null);
+                        int rw = repeat.getWidth(null);
+                        g.drawImage(image, 0, 0, null);
+                        for (int x = iw; x < w; x += rw)
+                        {
+                                g.drawImage(repeat, x, 0, null);
+                        }
+                }
+        }
+
+        void makeHtml()
+        {
+                try
+                {
+                        String sidebar = IO.read("settings/sidebar.html");
+                        controls = new HtmlControls(sidebar);
+                } catch (Exception e)
+                {
+                        e.printStackTrace(System.out);
+                }
+        }
+
+        @Override
+        public JComponent _getControls()
+        {
+                return controls;
+        }
+
+        @Override
+        protected void onscreen(boolean majorChange)
+        {
+        }
+
+        protected void _note(TFEvent e)
+        {
+                // do nothing.
+        }
+
+        @Override
+        public String getName()
+        {
+                return "About";
+        }
+}
diff --git a/timeflow/views/ListView.java b/timeflow/views/ListView.java
new file mode 100755
index 0000000..ec87f1c
--- /dev/null
+++ b/timeflow/views/ListView.java
@@ -0,0 +1,266 @@
+package timeflow.views;
+
+import timeflow.app.ui.EditRecordPanel;
+import timeflow.app.ui.HtmlDisplay;
+import timeflow.data.db.*;
+import timeflow.format.file.HtmlFormat;
+import timeflow.model.*;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.table.*;
+import javax.swing.text.html.*;
+
+import timeflow.util.*;
+
+import java.net.URI;
+import java.net.URL;
+import java.util.*;
+
+public class ListView extends AbstractView {
+
+	private JEditorPane listDisplay;
+	private JComboBox sortMenu=new JComboBox();
+	private ActComparator sort;//=ActComparator.byTime();
+	private int maxPerPage=50;
+	private int pageStart=0;
+	private int lastSize=0;
+	private ActList acts;
+	private Field sortField;
+	
+	private JLabel pageLabel=new JLabel("Page", JLabel.LEFT);
+	private JComboBox pageMenu=new JComboBox();
+	private boolean changing=false;
+	
+	private JPanel controls;
+	
+	public ListView(TFModel model)
+	{
+		super(model);
+		
+		listDisplay=HtmlDisplay.create();
+		listDisplay.addHyperlinkListener(new LinkIt());		
+		JScrollPane scrollPane = new JScrollPane(listDisplay);
+		setLayout(new BorderLayout());
+		add(scrollPane, BorderLayout.CENTER);
+		
+		
+		controls=new JPanel();
+		controls.setLayout(null);
+		controls.setBackground(Color.white);
+		
+		int x=10, y=10;
+		int ch=25, pad=5, cw=160;
+		JLabel sortLabel=new JLabel("Sort Order", JLabel.LEFT);
+		controls.add(sortLabel);
+		sortLabel.setBounds(x,y,cw,ch);
+		y+=ch+pad;
+		
+		controls.add(sortMenu);	
+		sortMenu.setBounds(x,y,cw,ch);
+		y+=ch+3*pad;
+		
+		controls.add(pageLabel);
+		pageLabel.setBounds(x,y,cw,ch);
+		y+=ch+pad;
+		controls.add(pageMenu);
+		pageMenu.setBounds(x,y,cw,ch);
+		
+		showPageMenu(false);
+		pageMenu.addActionListener(pageListener);				
+		sortMenu.addActionListener(sortListener);
+	}
+	
+	protected JComponent _getControls()
+	{
+		return controls;
+	}
+	
+	ActionListener sortListener=new ActionListener() {
+		@Override
+		public void actionPerformed(ActionEvent e) {
+			if (changing || sortMenu.getItemCount()<=0) // this means the action was fired after all items removed.
+				return;
+			sortField=getModel().getDB().getField((String)sortMenu.getSelectedItem());
+			sort=sortField==null ? null : ActComparator.by(sortField);
+			setToFirstPage();
+			makeList();
+		}};
+	
+	ActionListener pageListener=new ActionListener() {
+		@Override
+		public void actionPerformed(ActionEvent e) {
+			if (changing)
+				return;
+			pageStart=maxPerPage*pageMenu.getSelectedIndex();
+			System.out.println(e.getActionCommand());
+			makeList();
+		}};
+	
+	@Override
+	protected void onscreen(boolean majorChange)
+	{
+		_note(null);
+	}
+	
+	public void _note(TFEvent e) {
+		changing=true;
+		if (e==null || e.affectsSchema() || e.affectsRowSet())
+		{
+			sortMenu.removeActionListener(sortListener);
+			sortMenu.removeAllItems();
+			pageStart=0;
+			java.util.List<Field> fields=getModel().getDB().getFields();
+			Field firstField=null;
+			if (fields.size()>0)
+				firstField=fields.get(0);
+			for (Field f: fields)
+			{
+				sortMenu.addItem(f.getName());
+			}
+			sortField=getModel().getDB().getField(VirtualField.START);
+			if (sortField!=null)
+				sortMenu.setSelectedItem(sortField.getName());
+			else
+				sortField=firstField;
+			sortMenu.addActionListener(sortListener);
+			sort=null;
+		}
+		if (e!=null && e.affectsData())
+		{
+			setToFirstPage();
+		}
+		changing=false;
+		makeList();
+	}
+	
+	private void setToFirstPage()
+	{
+		pageStart=0;
+		if (pageMenu.isVisible())
+		{
+			pageMenu.removeActionListener(pageListener);
+			pageMenu.setSelectedIndex(0);
+			pageMenu.addActionListener(pageListener);
+		}
+	}
+	
+	void showPageMenu(boolean visible)
+	{
+		pageLabel.setVisible(visible);
+		pageMenu.setVisible(visible);
+		if (visible)
+		{
+			pageMenu.removeActionListener(pageListener);
+			pageMenu.setSelectedIndex(pageStart/maxPerPage);
+			pageMenu.addActionListener(pageListener);
+		}
+	}
+	
+	
+	void makeList()
+	{
+		HtmlFormat html=new HtmlFormat();
+		html.setModel(getModel());
+		StringBuffer page=new StringBuffer();
+		
+		page.append(html.makeHeader());
+		
+		
+		ActList as=getModel().getActs();
+		if (as==null || as.size()==0 && getModel().getDB().size()==0)
+		{
+			page.append("<tr><td><h1><font color=#003399>Empty Database</font></h1></td></tr>");
+			showPageMenu(false);
+		}
+		else
+		{
+			
+			if (sort==null)
+			{
+				Field timeField=getModel().getDB().getField(VirtualField.START);
+				if (timeField!=null)			
+					sort=ActComparator.by(timeField);
+			}
+
+			acts=as.copy();
+			if (sort!=null)
+				Collections.sort(acts, sort);
+			
+			boolean pages=acts.size()>maxPerPage;
+			int last=Math.min(acts.size(), pageStart+maxPerPage);
+			if (pages)
+			{
+				int n=acts.size();
+				if (lastSize!=n)
+				{
+					pageMenu.removeActionListener(pageListener);
+					pageMenu.removeAllItems();
+					for (int i=0; i*maxPerPage<n;i++)
+					{
+						pageMenu.addItem("Items "+((i*maxPerPage)+1)+" to "+
+								Math.min(n, (i+1)*maxPerPage));
+					}
+					pageMenu.addActionListener(pageListener);
+					lastSize=n;
+				}
+			}
+			showPageMenu(pages);
+			
+			page.append("<tr><td><h1><font color=#003399>"+(pages? (pageStart+1)+"-"+(last) +" of ": "")+acts.size()+" Events</font></h1>");
+			page.append("<br><br></td></tr>");
+
+			for (int i=pageStart; i<last; i++)
+			{
+				Act a=acts.get(i);
+				page.append(html.makeItem(a,i));
+			}
+		}
+		page.append(html.makeFooter());
+		listDisplay.setText(page.toString());
+		listDisplay.setCaretPosition(0);
+		repaint();
+	}
+	
+	
+	
+	
+	@Override
+	public String getName() {
+		return "List";
+	}
+	
+	static class ArrayRenderer extends DefaultTableCellRenderer {
+	    public void setValue(Object value) {
+	    	setText(Display.arrayToString((Object[])value));
+	    }
+	}
+	
+	public class LinkIt implements HyperlinkListener 
+	{
+		public void hyperlinkUpdate(HyperlinkEvent e) 
+		{
+			if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) 
+				return;
+			
+			String s=e.getDescription();
+			System.out.println(s);
+			if (s.length()>0)
+			{
+				char c=s.charAt(0);
+				if (c=='e') // code for "edit"
+				{
+					int i=Integer.parseInt(s.substring(1));
+					EditRecordPanel.edit(getModel(), acts.get(i));
+					return;
+				}
+				
+			}
+			Display.launchBrowser(e.getURL().toString());
+			
+		} 
+	} 
+}
diff --git a/timeflow/views/SummaryView.java b/timeflow/views/SummaryView.java
new file mode 100755
index 0000000..b51b638
--- /dev/null
+++ b/timeflow/views/SummaryView.java
@@ -0,0 +1,181 @@
+package timeflow.views;
+
+import timeflow.app.ui.HtmlDisplay;
+import timeflow.data.analysis.*;
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+import timeflow.format.field.FieldFormatCatalog;
+import timeflow.model.*;
+
+
+import java.awt.*;
+import java.util.Date;
+
+import javax.swing.*;
+
+
+public class SummaryView extends AbstractView {
+	
+	private JEditorPane analysisDisplay;
+	private FieldAnalysis[] fieldAnalyzers=new FieldAnalysis[]
+    {
+			new MissingValueAnalysis(), new RangeDateAnalysis(), 
+			new RangeNumberAnalysis(), new FrequencyAnalysis()
+    };
+	private int numItems;
+	private int numFiltered;
+	private Interval range;
+	private JComponent controls;
+	
+	public SummaryView(TFModel model)
+	{
+		super(model);
+		analysisDisplay=HtmlDisplay.create();
+		JScrollPane scrollPane = new JScrollPane(analysisDisplay);
+		setLayout(new GridLayout(1,1));
+		add(scrollPane);
+		
+		controls=new HtmlControls("This report gives a <br> statistical breakdown<br> "+
+				"of your data. <p> Reading the summary often helps<br> you find "+
+				"data errors.");
+	}
+	
+	@Override
+	public JComponent _getControls()
+	{
+		return controls;
+	}
+	
+	void makeHtml()
+	{
+		Display d=getModel().getDisplay();
+		ActDB db=getModel().getDB();
+		ActList acts=getModel().getActs();
+		StringBuffer page=new StringBuffer();
+		page.append("<blockquote>");
+		if (getModel().getDB()==null)
+		{
+			page.append("<h1><font color=#003399>No data loaded.</font></h1>");
+		}
+		else
+		{
+			page.append("<BR><BR><BR>File: "+getModel().getDbFile()+"<br>");
+			page.append("Source: "+getModel().getDB().getSource()+"<br><br>");
+			page.append("Description: "+getModel().getDB().getDescription()+"<br><br>");
+			page.append("<br><br>");
+			page.append("<table border=0>");
+			
+			
+			page.append("<tr><td valign=top align=left width=100><b>");
+			page.append("<font size=+1>Data</font><br>");
+			
+			page.append("</td><td align=top>");
+			append(page, "Total events", ""+numItems);
+			
+			if (numItems>0)
+			{
+				append(page, "Total selected", ""+numFiltered);
+				if (numFiltered>0)
+				{
+					append(page, "Earliest",  new Date(range.start).toString());
+					append(page, "Latest",  new Date(range.end).toString());
+				}
+			}
+			page.append("<br></td></tr>");
+			
+			page.append("<tr><td valign=top align=left width=100><b>");
+			page.append("<font size=+1>Fields</font><br>");
+			
+			page.append("</td><td align=top>");
+			for (Field f: getModel().getDB().getFields())
+			{
+					append(page, f.getName(),FieldFormatCatalog.humanName(f.getType())+fieldLabel(f));
+			}
+			page.append("<br></td></tr>");
+			
+				
+			page.append("</table>");
+
+			if (numFiltered>0)
+			{
+				page.append("<h1>Statistics (for "+acts.size()+" items)</h1>");
+				for (Field field: db.getFields())
+				{
+					page.append("<h2>"+field.getName()+"</h2>");
+					page.append("<ul>");
+					for (int i=0; i<fieldAnalyzers.length; i++)
+					{
+						FieldAnalysis fa=fieldAnalyzers[i];
+						if (fa.canHandleType(field.getType()))
+						{
+							page.append("<li>");
+							page.append("<b><font color=#808080>"+fa.getName()+"</font></b><br>");
+							fa.perform(acts, field);
+							String[] s=fa.getResultDescription();
+							for (int j=0; j<s.length; j++)
+							{
+								page.append(s[j]);
+								page.append("<br>");
+							}
+							page.append("</li>");
+						}
+					}
+					page.append("</ul>");
+				}
+			}
+		}
+		page.append("</blockquote>");
+		analysisDisplay.setText(page.toString());
+		analysisDisplay.setCaretPosition(0);
+	}
+	
+	static void append(StringBuffer page, String label, String value)
+	{
+		page.append("<b><font color=#808080>"+label+"</font></b>&nbsp;&nbsp;&nbsp;&nbsp;");
+		page.append(value);
+		page.append("<br>");
+	}
+	
+	@Override
+	protected void onscreen(boolean majorChange)
+	{
+		_note(null);
+	}
+
+	protected void _note(TFEvent e) {
+		recalculate();
+		makeHtml();
+		repaint();
+	}
+	
+	String fieldLabel(Field f)
+	{
+		StringBuffer b=new StringBuffer("<b>");
+		ActDB db=getModel().getDB();
+		for (String v: VirtualField.list())
+		{
+			if (db.getField(v)!=null && db.getField(v).getName().equals(f.getName()))
+				b.append(" (Shown in visualization as "+VirtualField.humanName(v)+")");
+		}
+		b.append("</b>");
+		return b.toString();
+	}
+	
+	void recalculate()
+	{
+		ActList acts=getModel().getActs();
+		if (acts==null)
+		{
+			numItems=0;
+			return;
+		}
+		numFiltered=acts.size();
+		numItems=getModel().getDB().size();
+		range=DBUtils.range(acts, VirtualField.START);
+	}
+
+	@Override
+	public String getName() {
+		return "Summary";
+	}
+}
diff --git a/timeflow/views/TableView.java b/timeflow/views/TableView.java
new file mode 100755
index 0000000..4ce1ff1
--- /dev/null
+++ b/timeflow/views/TableView.java
@@ -0,0 +1,267 @@
+package timeflow.views;
+
+import timeflow.app.ui.HtmlDisplay;
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+import timeflow.format.field.DateTimeGuesser;
+import timeflow.model.*;
+
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.*;
+import javax.swing.event.TableModelListener;
+import javax.swing.table.*;
+import java.util.*;
+
+public class TableView extends AbstractView {
+
+	private JTable table=new JTable();
+	private int colorColumn=-1, labelColumn=-1;
+	private Font font, bold;
+	private boolean editable=true;
+	private JPanel controls;
+	
+	public TableView(TFModel model)
+	{
+		super(model);
+		
+		JScrollPane scrollPane = new JScrollPane(table);
+		table.setFillsViewportHeight(true);
+		table.setAutoCreateRowSorter(true);
+		font=model.getDisplay().plain();
+		bold=model.getDisplay().bold();
+		table.setFont(font);
+		final int fh=getFontMetrics(font).getHeight();
+		table.setRowHeight(fh+12);
+		table.setShowGrid(false);
+		table.getTableHeader().setPreferredSize(new Dimension(10, fh+15));
+		table.getTableHeader().setFont(font);
+		setReorderable(false);
+		setLayout(new GridLayout(1,1));
+		add(scrollPane);	
+		
+		controls=new HtmlControls("Use the table view for<br> a rapid overview<br> "+
+				"of your data. <p>You can click<br> on the headers to sort the columns,<br> "+
+				"and you can edit data<br> directly in the table cells.");
+	}
+	
+	@Override
+	public JComponent _getControls()
+	{
+		return controls;
+	}
+	
+	public void setEditable(boolean editable)
+	{
+		this.editable=editable;
+	}
+	
+	public JTable getTable()
+	{
+		return table;
+	}
+	
+	@Override
+	public void onscreen(boolean majorChange)
+	{
+		_note(null);
+	}
+	
+	@Override
+	protected void _note(TFEvent e) {
+		setActs(getModel().getActs());		
+	}
+	
+	public void setActs(ActList acts)
+	{
+		TableModel t=new TimelineTableModel(acts);
+		table.setModel(t);
+		ActTableRenderer r=new ActTableRenderer(acts);
+		table.setDefaultRenderer(Object.class, r);
+		table.setDefaultRenderer(Double.class, r);
+		table.setDefaultEditor(String[].class, new StringArrayEditor());
+		table.setDefaultEditor(RoughTime.class, new RoughTimeEditor());
+		repaint();
+	}
+	
+	@Override
+	public String getName() {
+		return "Table";
+	}
+
+	
+	class ActTableRenderer extends DefaultTableCellRenderer {
+		
+		ActList acts;
+		Color zebra=new Color(240,240,240);
+		boolean color=false;
+		Color dataColor;
+		
+		ActTableRenderer(ActList acts)
+		{
+			this.acts=acts;
+		}
+		
+	    public void setValue(Object value) {
+	    	if (value==null)
+	    	{
+	    		super.setValue("");
+	    		return;
+	    	}
+	    	setHorizontalAlignment(value instanceof Double ? SwingConstants.RIGHT : SwingConstants.LEFT);
+	    	super.setValue(getModel().getDisplay().toString(value));
+	    }
+
+	    public Component getTableCellRendererComponent(JTable table, Object value,
+	            boolean isSelected, boolean hasFocus, int rowIndex, int vColIndex) {
+
+	    	if (vColIndex==labelColumn || vColIndex==colorColumn)
+	    		setFont(bold);
+	    	else
+	    		setFont(font);
+	    	setBackground(rowIndex%2==0 ? Color.white : zebra);
+	    	color=vColIndex==colorColumn || vColIndex==labelColumn;
+	    	Field colorField=null;
+	    	if (color)
+	    	{
+	    		colorField=getModel().getColorField();
+	    		color &= colorField!=null;
+	    	}
+	    	if (color)
+	    	{
+		    	int actIndex=table.convertRowIndexToModel(rowIndex);
+		    	Act act=acts.get(actIndex);
+		    	
+		    	if (colorField==null || colorField.getType()!=String.class)
+		    		dataColor=getModel().getDisplay().getColor("timeline.unspecified.color");
+		    	else
+		    		dataColor=getModel().getDisplay().makeColor(act.getString(colorField));
+		    	setForeground(dataColor);
+		    	setValue(value);
+	    	}
+	    	else
+	    	{
+	    		setForeground(Color.black);
+	    		setValue(value);
+	    	}
+	        return this;
+	    }	    
+	}
+	
+	class TimelineTableModel implements TableModel
+	{
+		ActList acts;
+		Field[] fields;
+		
+		TimelineTableModel(ActList acts)
+		{
+			this.acts=acts;
+			ArrayList<Field> a=new ArrayList<Field>();
+			int i=0;
+			Field colorField=getModel().getColorField();
+			Field labelField=getModel().getDB().getField(VirtualField.LABEL);
+			for (Field f:acts.getDB().getFields())
+			{
+				a.add(f);
+				if (f==colorField)
+					colorColumn=i;
+				if (f==labelField)
+					labelColumn=i;
+				i++;
+			}
+			fields=(Field[])a.toArray(new Field[0]);
+		}
+		
+		@Override
+		public void addTableModelListener(TableModelListener l) {
+		}
+
+		@Override
+		public Class<?> getColumnClass(int columnIndex) {
+			return fields[columnIndex].getType();
+		}
+
+		@Override
+		public int getColumnCount() {
+			return fields.length;
+		}
+
+		@Override
+		public String getColumnName(int columnIndex) {
+			return fields[columnIndex].getName();
+		}
+
+		@Override
+		public int getRowCount() {
+			return acts.size();
+		}
+
+		@Override
+		public Object getValueAt(int rowIndex, int columnIndex) {
+			return acts.get(rowIndex).get(fields[columnIndex]);
+		}
+
+		@Override
+		public boolean isCellEditable(int rowIndex, int columnIndex) {
+			return editable;
+		}
+
+		@Override
+		public void removeTableModelListener(TableModelListener l) {
+		}
+
+		@Override
+		public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
+			acts.get(rowIndex).set(fields[columnIndex], aValue);
+			getModel().noteRecordChange(TableView.this);
+		}
+	}
+	
+	public class StringArrayEditor extends AbstractCellEditor implements TableCellEditor 
+	{
+	    JComponent component = new JTextField();
+
+	    public Component getTableCellEditorComponent(JTable table, Object value,
+	            boolean isSelected, int rowIndex, int vColIndex) {
+	        ((JTextField)component).setText(getModel().getDisplay().toString(value));
+	        return component;
+	    }
+
+	    public Object getCellEditorValue() 
+	    {
+	        String s= ((JTextField)component).getText();
+	        String[] tags=s.split(",");
+	        for (int i=0; i<tags.length; i++)
+	        	tags[i]=tags[i].trim();
+	        return tags;
+	    }
+	}
+	
+	public class RoughTimeEditor extends AbstractCellEditor implements TableCellEditor 
+	{
+	    JComponent component = new JTextField();
+	    DateTimeGuesser guesser=new DateTimeGuesser();
+
+	    public Component getTableCellEditorComponent(JTable table, Object value,
+	            boolean isSelected, int rowIndex, int vColIndex) {
+	    	
+	    	RoughTime r=(RoughTime)value;
+	    	JTextField t=((JTextField)component);
+	        t.setText(r==null ? "" : r.format());
+	        return component;
+	    }
+
+	    public Object getCellEditorValue() 
+	    {
+	        String s= ((JTextField)component).getText();
+	        return s.trim().length()==0 ? null : guesser.guess(s);
+	    }
+	}
+
+	public void setReorderable(boolean allow) {
+		table.getTableHeader().setReorderingAllowed(allow);
+	}
+
+}
diff --git a/timeflow/views/TimelineView.java b/timeflow/views/TimelineView.java
new file mode 100755
index 0000000..778ece2
--- /dev/null
+++ b/timeflow/views/TimelineView.java
@@ -0,0 +1,465 @@
+package timeflow.views;
+
+import timeflow.app.ui.ComponentCluster;
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+import timeflow.model.*;
+import timeflow.views.CalendarView.CalendarPanel;
+import timeflow.views.CalendarView.ScrollingCalendar;
+import timeflow.vis.*;
+import timeflow.vis.timeline.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.image.*;
+
+import javax.swing.*;
+
+import java.util.*;
+
+public class TimelineView extends AbstractView
+{
+        AxisRenderer grid;
+        TimelineRenderer timeline;
+        TimelineVisuals visuals;
+        TimelinePanel timelinePanel;
+        JButton fit;
+        ScrollingTimeline scroller;
+        JPanel controls;
+        JScrollBar bar;
+
+        //JScrollPane scrollpane;
+        public JComponent _getControls()
+        {
+                return controls;
+        }
+
+        public TimelineView(TFModel model)
+        {
+                super(model);
+                visuals = new TimelineVisuals(model);
+                grid = new AxisRenderer(visuals);
+                timeline = new TimelineRenderer(visuals);
+
+                timelinePanel = new TimelinePanel(model);
+                scroller = new ScrollingTimeline();
+                setLayout(new BorderLayout());
+                add(scroller, BorderLayout.CENTER);
+
+                JPanel bottom = new JPanel();
+                bottom.setLayout(new BorderLayout());
+                add(bottom, BorderLayout.SOUTH);
+
+                TimelineSlider slider = new TimelineSlider(visuals, 24 * 60 * 60 * 1000L, new Runnable()
+                {
+
+                        @Override
+                        public void run()
+                        {
+                                redraw();
+                        }
+                });
+                bottom.add(slider, BorderLayout.CENTER);
+
+                controls = new JPanel();
+                controls.setBackground(Color.white);
+                controls.setLayout(new BorderLayout());//new GridLayout(2,1));
+
+                // top part of grid: zoom buttons.
+                ComponentCluster buttons = new ComponentCluster("Zoom");
+                ImageIcon zoomOutIcon = new ImageIcon("images/zoom_out.gif");
+                JButton zoomOut = new JButton(zoomOutIcon);
+                buttons.addContent(zoomOut);
+                zoomOut.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                Interval zoom = visuals.getViewInterval().subinterval(-1, 2).intersection(visuals.getGlobalInterval());
+                                moveTime(zoom);
+                        }
+                });
+
+                ImageIcon zoomOut100Icon = new ImageIcon("images/zoom_out_100.gif");
+                JButton zoomOutAll = new JButton(zoomOut100Icon);
+                buttons.addContent(zoomOutAll);
+                zoomOutAll.addActionListener(new ActionListener()
+                {
+
+                        @Override
+                        public void actionPerformed(ActionEvent e)
+                        {
+                                moveTime(visuals.getGlobalInterval());
+                        }
+                });
+
+                /*
+                // UI for zooming to precisely fit the visible selection.
+                // No one seemed to think this was so important, but we may want it again some day.
+                // if you uncomment this, then also uncomment the line in reset().
+                ImageIcon zoomSelection=new ImageIcon("images/zoom_selection.gif");
+                fit=new JButton(zoomSelection);
+                fit.setBackground(Color.white);
+                buttons.addContent(fit);
+                fit.setEnabled(false);
+                fit.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                moveTime(visuals.getFitToVisibleRange());
+                }});
+                 */
+                controls.add(buttons, BorderLayout.NORTH);
+
+                // ok, now do second part of grid: layout style buttons.
+                ComponentCluster layoutPanel = new ComponentCluster("Layout");
+
+                ButtonGroup layoutGroup = new ButtonGroup();
+
+                ImageIcon diagonalIcon = new ImageIcon("images/layout_diagonal.gif");
+                JRadioButton diagonal = new JRadioButton(diagonalIcon, true);
+                diagonal.setSelectedIcon(new ImageIcon("images/layout_diagonal_selected.gif"));
+                layoutPanel.addContent(diagonal);
+                diagonal.addActionListener(new LayoutSetter(TimelineVisuals.Layout.TIGHT));
+                layoutGroup.add(diagonal);
+
+                ImageIcon looseIcon = new ImageIcon("images/layout_loose.gif");
+                JRadioButton loose = new JRadioButton(looseIcon, false);
+                loose.setSelectedIcon(new ImageIcon("images/layout_loose_selected.gif"));
+                layoutPanel.addContent(loose);
+                loose.addActionListener(new LayoutSetter(TimelineVisuals.Layout.LOOSE));
+                layoutGroup.add(loose);
+
+                ImageIcon graphIcon = new ImageIcon("images/layout_graph.gif");
+                JRadioButton graph = new JRadioButton(graphIcon, false);
+                graph.setSelectedIcon(new ImageIcon("images/layout_graph_selected.gif"));
+                layoutPanel.addContent(graph);
+                graph.addActionListener(new LayoutSetter(TimelineVisuals.Layout.GRAPH));
+                layoutGroup.add(graph);
+
+                controls.add(layoutPanel, BorderLayout.CENTER);
+        }
+
+        class LayoutSetter implements ActionListener
+        {
+                TimelineVisuals.Layout layout;
+
+                LayoutSetter(TimelineVisuals.Layout layout)
+                {
+                        this.layout = layout;
+                }
+
+                @Override
+                public void actionPerformed(ActionEvent e)
+                {
+                        visuals.setLayoutStyle(layout);
+                        redraw();
+                }
+        }
+
+        void moveTime(Interval interval)
+        {
+                new TimeAnimator(interval).start();
+        }
+
+        void redraw()
+        {
+                visuals.layout();
+                timelinePanel.drawVisualization();
+                repaint();
+        }
+
+        @Override
+        protected void onscreen(boolean majorChange)
+        {
+                visuals.init(majorChange);
+                reset(majorChange);
+                redraw();
+                scroller.calibrate();
+        }
+
+        @Override
+        protected void _note(TFEvent e)
+        {
+                visuals.note(e);
+                reset(e.affectsRowSet());
+        }
+
+        void reset(boolean forceViewChange)
+        {
+                if (forceViewChange || getModel().getViewInterval() == null)
+                {
+                        int numSelected = getModel().getActs().size();
+                        int numVisible = DBUtils.count(getModel().getActs(), visuals.getViewInterval(),
+                                getModel().getDB().getField(VirtualField.START));
+                        if (numVisible < 10 && numSelected > numVisible)
+                        {
+                                moveTime(visuals.getFitToVisibleRange());
+                        }
+                }
+                // uncomment this if we are using the fit button again.
+                //fit.setEnabled(getModel().getActs().size()<getModel().getDB().size());		
+                redraw();
+                scroller.calibrate();
+        }
+
+        class TimeAnimator extends Thread
+        {
+                Interval i1, i2;
+
+                TimeAnimator(Interval i2)
+                {
+                        this.i1 = visuals.getViewInterval();
+                        this.i2 = i2;
+                }
+
+                TimeAnimator(Interval i1, Interval i2)
+                {
+                        this.i1 = i1;
+                        this.i2 = i2;
+                }
+
+                public void run()
+                {
+                        int n = 15;
+                        for (int i = 0; i < n; i++)
+                        {
+                                long start = ((n - i) * i1.start + i * i2.start) / n;
+                                long end = ((n - i) * i1.end + i * i2.end) / n;
+                                try
+                                {
+                                        visuals.setTimeBounds(start, end);
+                                        redraw();
+                                        
+                                        sleep(20);
+                                } catch (Exception e)
+                                {
+                                }
+                        }
+                }
+        }
+
+        class ScrollingTimeline extends JPanel
+        {
+                //JScrollBar bar;
+
+                public ScrollingTimeline()
+                {
+                        setLayout(new BorderLayout());
+                        //scrollpane = new JScrollPane(timelinePanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+                        //add(scrollpane, BorderLayout.CENTER);
+                        add(timelinePanel, BorderLayout.CENTER);
+                        bar = new JScrollBar(JScrollBar.VERTICAL);
+                        add(bar, BorderLayout.EAST);
+                        //bar = scrollpane.getVerticalScrollBar();
+                        bar.addAdjustmentListener(new AdjustmentListener()
+                        {
+
+                                @Override
+                                public void adjustmentValueChanged(AdjustmentEvent e)
+                                {
+                                        timeline.setDY(bar.getValue());
+                                        timelinePanel.drawVisualization();
+                                        //timelinePanel.repaint();
+                                }
+                        });
+                }
+
+                public void setBounds(int x, int y, int w, int h)
+                {
+                        super.setBounds(x, y, w, h);
+                        calibrate();
+                }
+
+                void calibrate()
+                {
+//                    timelinePanel.setBorder(BorderFactory.createLineBorder(Color.red, 2));
+                        //javax.swing.border.Border b = scrollpane.getViewportBorder();
+                        //scrollpane.setViewportBorder(BorderFactory.createLineBorder(Color.red, 2));
+//                    scrollpane.getViewport().setBounds(0,0,100,100); //.setViewSize(new Dimension(100, visuals.getFullHeight()));
+//			if (true) // visuals==null)
+//				return;
+                        final int height = getSize().height * 90 / 100; // room for bottom
+                        final int desired = Math.max(height, visuals.getFullHeight());
+//                        bar.setVisible(desired > height);
+//                        bar.setMinimum(0);   // is this double setting necessary?
+//                        bar.setMaximum(desired); // more testing is needed, it solved problems
+//                        bar.setVisibleAmount(height);  // on certain Macs are one point.
+                        SwingUtilities.invokeLater(new Runnable()
+                        {
+
+                                @Override
+                                public void run()
+                                {
+                                        bar.setMinimum(0);
+                                        bar.setMaximum(desired);
+                                        bar.setVisibleAmount(height);
+                                }
+                        });
+                }
+        }
+
+        class TimelinePanel extends AbstractVisualizationView
+        {
+                public TimelinePanel(TFModel model)
+                {
+                        super(model);
+                        
+                        addMouseListener(new MouseAdapter()
+                        {
+                                @Override
+                                public void mouseClicked(MouseEvent e)
+                                {
+                                        if (e.getClickCount() == 2)
+                                        {
+                                                moveTime(visuals.getViewInterval().subinterval(.333, .667));
+                                        }
+                                }
+
+                                @Override
+                                public void mouseExited(MouseEvent e)
+                                {
+                                        mouse.setLocation(new Point(-1, -1));
+                                        repaint();
+                                }
+
+                                @Override
+                                public void mousePressed(MouseEvent e)
+                                {
+                                        // was this a right-click or ctrl-click? ignore.
+                                        if (e.isPopupTrigger())
+                                        {
+                                                return;
+                                        }
+                                        // did we click on a date label?
+                                        for (Mouseover o : objectLocations)
+                                        {
+                                                if (o.contains(e.getX(), e.getY()) && o.thing instanceof Interval)
+                                                {
+                                                        moveTime((Interval) o.thing);
+                                                        return;
+                                                }
+                                        }
+                                        // if not, prepare
+                                        firstMouse.setLocation(e.getX(), e.getY());
+                                        mouseIsDown = true;
+                                        repaint();
+                                }
+
+                                @Override
+                                public void mouseReleased(MouseEvent event)
+                                {
+                                        if (!mouseIsDown) // this means we had clicked on a date label.
+                                        {
+                                                return;
+                                        }
+                                        
+                                        mouseIsDown = false;
+                                        int a = firstMouse.x;
+                                        int b = mouse.x;
+                                        
+                                        long start = visuals.getTimeScale().toTime(a);
+                                        long end = visuals.getTimeScale().toTime(b);
+                                        if (b - a < 0)
+                                        {
+                                                Interval total = visuals.getGlobalInterval();
+                                                
+                                                long t = start;
+                                                start = end;
+                                                end = t;
+                                                
+                                                // reversed
+                                                long S = visuals.getTimeScale().getInterval().start;
+                                                long E = visuals.getTimeScale().getInterval().end;
+                                                
+                                                // start*M + B = S
+                                                // end*M + B = E
+                                                double M = (double)(S - E) / (start - end);
+                                                long B = E - (long)(end * M);
+                                                start = (long)(S*M) + B;
+                                                end = (long)(E*M) + B;
+                                                if (start < total.start)
+                                                {
+                                                        start = total.start;
+                                                }
+                                                if (end > total.end)
+                                                {
+                                                        end = total.end;
+                                                }
+                                        }
+                                        
+                                        moveTime(new Interval(start, end));
+                                }
+                        });
+                        
+                        addMouseWheelListener(new MouseAdapter()
+                        {
+                                @Override
+                                public void mouseWheelMoved(MouseWheelEvent e)
+                                {
+                                        //bar.setValue(bar.getValue() + e.getWheelRotation() * 10);
+                                        visuals.scale *= Math.exp(e.getWheelRotation()/10.0);
+                                        visuals.scale = visuals.layout();
+                                        //System.out.println(visuals.scale);
+                                        timelinePanel.drawVisualization();
+                                        scroller.calibrate();
+                                        repaint();
+                                }
+                        });
+                }
+
+                public RoughTime getTime(Point p)
+                {
+                        TimeScale scale = visuals.getTimeScale();
+                        long timestamp = scale.toTime(p.x);
+                        return new RoughTime(timestamp, TimeUnit.DAY);
+                }
+
+                protected void drawVisualization(Graphics2D g)
+                {
+                        if (g == null)
+                        {
+                                return;
+                        }
+                        
+                        g.setColor(Color.white);
+                        g.fillRect(0, 0, 2 * getSize().width, getSize().height);
+                        visuals.setBounds(0, 0, getSize().width, getSize().height);
+                        objectLocations = new ArrayList<Mouseover>();
+                        timeline.render(g, objectLocations);
+                        grid.render(g, objectLocations);
+                }
+
+                protected boolean paintOnTop(Graphics2D g, int w, int h)
+                {
+                        if (!mouseIsDown)
+                        {
+                                return false;
+                        }
+                        
+                        int a = mouse.x;
+                        int b = firstMouse.x;
+                        if (a > b)
+                        {
+                                g.setColor(new Color(255, 255, 120, 100));
+                                g.fillRect(b, 0, a - b, h);
+                        }
+                        else
+                        {
+                                g.setColor(new Color(255, 120, 255, 100));
+                                g.fillRect(a, 0, b - a, h);
+                        }
+                        
+                        g.setColor(Color.blue);
+                        g.drawLine(a, 0, a, h);
+                        g.drawLine(b, 0, b, h);
+                        return true;
+                }
+        }
+
+        @Override
+        public String getName()
+        {
+                return "Timeline";
+        }
+}
diff --git a/timeflow/vis/GroupVisualAct.java b/timeflow/vis/GroupVisualAct.java
new file mode 100755
index 0000000..ce2d71d
--- /dev/null
+++ b/timeflow/vis/GroupVisualAct.java
@@ -0,0 +1,98 @@
+package timeflow.vis;
+
+import java.awt.*;
+import java.util.*;
+
+import timeflow.data.db.*;
+import timeflow.model.Display;
+import timeflow.model.VirtualField;
+import timeflow.util.*;
+
+public class GroupVisualAct extends VisualAct
+{
+	private ArrayList<Act> group=new ArrayList<Act>();
+	private boolean mixed=false;
+	private DoubleBag<Color> colorBag;
+	int numActs=0;
+	double total=0;
+	
+	public GroupVisualAct(java.util.List<VisualAct> vacts, boolean mixed, Rectangle bounds)
+	{
+		super(vacts.get(0).act);
+		int n=vacts.size();
+		
+		VisualAct proto=vacts.get(0);
+		
+		this.color=proto.color;
+		this.trackString=proto.trackString;
+		this.visible=proto.visible;
+		this.x=proto.x;
+		this.y=proto.y;
+		
+		this.spaceToRight=proto.spaceToRight;
+		this.start=proto.start;
+		this.group=new ArrayList<Act>();
+		this.label="Group of "+n+" events";
+		this.mouseOver=this.label;
+		this.colorBag=new DoubleBag<Color>();
+		Field sizeField=act.getDB().getField(VirtualField.SIZE);
+		for (VisualAct v: vacts)
+		{
+			numActs++;
+			if(sizeField!=null)
+				total+=v.act.getValue(sizeField);
+			this.size+=v.size;
+			this.colorBag.add(v.color, v.size);
+		}
+		this.size=Math.sqrt(this.size);
+		this.mixed=mixed;		
+	}
+
+	public int getNumActs()
+	{
+		return numActs;
+	}
+	
+	public void add(Act secondAct)
+	{
+		if (group==null)
+		{
+			group=new ArrayList<Act>();
+			if (act!=null)
+				group.add(act);
+		}
+		group.add(secondAct);
+	}
+
+	public void draw(Graphics2D g, int ox, int oy, int r, Rectangle maxFill, boolean showDuration)
+	{
+		if (!mixed)
+		{
+			g.setColor(color);
+			g.fillOval(ox,oy-r,2*r,2*r);
+			g.drawOval(ox-2,oy-r-2,2*r+3,2*r+3);
+		}
+		else
+		{
+			java.util.List<Color> colors=colorBag.listTop(8, true);
+			double total=0;
+			for (Color c: colors)
+				total+=colorBag.num(c);
+							
+			// now draw pie chart thing.
+			double angle=0;
+			int pieCenterX=ox+r;
+			int pieCenterY=oy;
+			for (Color c: colors)
+			{
+				double num=colorBag.num(c);
+				double sa=(360*angle)/total;
+				int startAngle=(int)(sa);
+				int arcAngle=(int)(((360*(angle+num)))/total-sa);
+				g.setColor(c);
+				g.fillArc(pieCenterX-r,pieCenterY-r,2*r,2*r,startAngle,arcAngle);
+				angle+=num;
+			}		
+		}
+	}
+}
diff --git a/timeflow/vis/Mouseover.java b/timeflow/vis/Mouseover.java
new file mode 100755
index 0000000..adf3009
--- /dev/null
+++ b/timeflow/vis/Mouseover.java
@@ -0,0 +1,94 @@
+package timeflow.vis;
+
+import timeflow.model.*;
+
+import java.awt.*;
+import java.util.ArrayList;
+
+public class Mouseover extends Rectangle {
+	public Object thing;
+	public Mouseover(Object thing, int x, int y, int w, int h)
+	{
+		super(x,y,w,h);
+		this.thing=thing;
+	}
+	
+	public void draw(Graphics2D g, int maxW, int maxH, Display display)
+	{
+		g.setColor(new Color(0,53,153));
+		g.setColor(new Color(255,255,0,100));
+		g.fill(this);
+	}
+	
+	protected void draw(Graphics2D g, int maxW, int maxH, Display display, ArrayList labels, int numLines)
+	{
+		if (labels==null || labels.size()==0)
+			return;
+		
+		// draw a background box.
+		
+		// find max number of chars, very very roughly!
+		int boxW=50;
+		for (int i=0; i<labels.size(); i+=2)
+		{
+			if (labels.get(i) instanceof String[])
+				boxW=300;
+			else if (labels.get(i) instanceof String)
+			{
+				boxW=Math.max(boxW, 50+50*((String)labels.get(i)).length());
+			}
+		}	
+		
+		
+		boxW=Math.min(350, boxW);
+		int boxH=18*numLines+10;
+		int mx=this.x+this.width+5;
+		int my=this.y+this.height+35;		
+		
+		// put box in a place where it does not obscure the data
+		// or go off screen.
+		if (my+boxH>maxH-10)
+		{
+			my=Math.max(10,this.y-boxH-5);
+		}
+		if (mx+boxW>maxW-10)
+		{
+			mx=Math.max(10,this.x-boxW-10);
+		}
+		int ty=my;
+		g.setColor(new Color(0,0,0,70));
+		g.fillRoundRect(mx-11, my-16, boxW, boxH,12,12);
+		g.setColor(Color.white);
+		g.fillRoundRect(mx-15, my-20, boxW, boxH,12,12);
+		g.setColor(Color.darkGray);
+		g.drawRoundRect(mx-15, my-20, boxW, boxH,12,12);
+		
+		// finally, draw the darn labels.
+		for (int i=0; i<labels.size(); i+=2)
+		{
+			g.setFont(display.bold());
+			String field=(String)labels.get(i);
+			g.drawString(field,mx,ty);
+			int sw=display.boldFontMetrics().stringWidth(field);
+			g.setFont(display.plain());
+			Object o=labels.get(i+1);
+			if (o instanceof String)
+			{
+				g.drawString((String)o,mx+sw+9,ty);
+				ty+=18;
+			}
+			else
+			{
+				ArrayList<String> lines=(ArrayList<String>)o;
+				int dx=sw+9;
+				for (String line: lines)
+				{
+					g.drawString((String)line,mx+dx,ty);
+					ty+=18;
+					dx=0;
+				}
+				ty+=5;
+			}
+		}		
+	}
+}
diff --git a/timeflow/vis/MouseoverLabel.java b/timeflow/vis/MouseoverLabel.java
new file mode 100755
index 0000000..5ffa41b
--- /dev/null
+++ b/timeflow/vis/MouseoverLabel.java
@@ -0,0 +1,32 @@
+package timeflow.vis;
+
+import java.awt.Graphics2D;
+import java.util.ArrayList;
+
+import timeflow.data.db.Act;
+import timeflow.data.db.ActDB;
+import timeflow.data.db.Field;
+import timeflow.model.Display;
+
+public class MouseoverLabel extends Mouseover {
+
+	String label1, label2;
+	
+	public MouseoverLabel(String label1, String label2, int x, int y, int w, int h) {
+		super(label1, x, y, w, h);
+		this.label1=label1;
+		this.label2=label2;
+	}
+
+
+	public void draw(Graphics2D g, int maxW, int maxH, Display display)
+	{
+		super.draw(g, maxW, maxH, display);
+		ArrayList labels=new ArrayList();
+		labels.add(label1);
+		labels.add(label2);
+		int numLines=1;
+		draw(g, maxW, maxH, display, labels, numLines);
+	}
+}
+
diff --git a/timeflow/vis/TagVisualAct.java b/timeflow/vis/TagVisualAct.java
new file mode 100755
index 0000000..a93cab1
--- /dev/null
+++ b/timeflow/vis/TagVisualAct.java
@@ -0,0 +1,52 @@
+package timeflow.vis;
+
+import timeflow.data.db.*;
+
+import java.awt.*;
+import java.util.*;
+
+public class TagVisualAct extends VisualAct {
+	
+	Color[] colors;
+	private static Color[] nullColors={new Color(230,230,230)};
+
+	public TagVisualAct(Act act) {
+		super(act);
+	}
+	
+	public void setColors(Color[] colors)
+	{
+		this.colors=colors;
+		this.color=colors.length>0 ? colors[0] : Color.gray;
+	}
+
+	public void draw(Graphics2D g, int ox, int oy, int r, Rectangle maxFill, boolean showDuration)
+	{
+		if (colors==null)
+		{
+			super.draw(g, ox, oy, r, maxFill, showDuration);
+			return;
+		}
+
+		Color[] c= colors==null || colors.length==0 ? nullColors : colors;
+		int tx=ox-r;
+		int side=2*r;
+		for (int i=0; i<c.length; i++)
+		{
+			g.setColor(c[i]);
+			int y0=-5+(oy-r)+(2*side*i)/c.length;
+			int y1=-5+(oy-r)+(2*side*(i+1))/c.length;
+			if (size>=0)
+				g.fillRect(tx,y0,side+2,y1-y0);
+			else
+				g.drawRect(tx,y0,side+2,y1-y0);
+		}
+		
+		if (end!=null && showDuration)
+		{
+			int lineY=y+6;
+			g.fillRect(getX(), lineY, getEndX()-getX(), 2);
+			g.drawLine(getX(), lineY, getX(), lineY-4);
+		}
+	}
+}
diff --git a/timeflow/vis/TimeScale.java b/timeflow/vis/TimeScale.java
new file mode 100755
index 0000000..cad23cd
--- /dev/null
+++ b/timeflow/vis/TimeScale.java
@@ -0,0 +1,95 @@
+package timeflow.vis;
+
+import timeflow.data.time.*;
+
+import java.util.*;
+
+public class TimeScale
+{
+        private double low, high;
+        private Interval interval;
+
+        public TimeScale()
+        {
+                low = 0;
+                high = 100;
+                interval = new Interval(new Date(0).getTime(), new Date().getTime());
+        }
+
+        public Interval getInterval()
+        {
+                return interval;
+        }
+
+        public void setNumberRange(double low, double high)
+        {
+                this.low = low;
+                this.high = high;
+        }
+
+        public void setDateRange(Interval t)
+        {
+                setDateRange(t.start, t.end);
+        }
+
+        public void setDateRange(long first, long last)
+        {
+                interval.setTo(first, last);
+        }
+
+        public boolean containsDate(long date)
+        {
+                return interval.contains(date);
+        }
+
+        public boolean containsNum(double x)
+        {
+                return x >= low && x <= high;
+        }
+
+        public long duration()
+        {
+                return interval.length();
+        }
+
+        public double toNum(long time)
+        {
+                return low + (high - low) * (time - interval.start) / (double) duration();
+        }
+
+        public long spaceToTime(double space)
+        {
+                return (long) (space * duration() / (high - low));
+        }
+
+        public int toInt(long time)
+        {
+                return (int) toNum(time);
+        }
+
+        public long toTime(double num)
+        {
+                double millis = interval.start + duration() * (num - low) / (high - low);
+                return (long) millis;
+        }
+
+        public double getLow()
+        {
+                return low;
+        }
+
+        public void setLow(double low)
+        {
+                this.low = low;
+        }
+
+        public double getHigh()
+        {
+                return high;
+        }
+
+        public void setHigh(double high)
+        {
+                this.high = high;
+        }
+}
diff --git a/timeflow/vis/VisualAct.java b/timeflow/vis/VisualAct.java
new file mode 100755
index 0000000..4c3569b
--- /dev/null
+++ b/timeflow/vis/VisualAct.java
@@ -0,0 +1,314 @@
+package timeflow.vis;
+
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+import timeflow.model.*;
+import timeflow.vis.timeline.TimelineTrack;
+
+import java.awt.*;
+import java.util.*;
+
+import timeflow.util.*;
+
+public class VisualAct implements Comparable
+{
+
+        Color color;
+        String label;
+        String mouseOver;
+        double size = 1;
+        String trackString;
+        TimelineTrack track;
+        boolean visible;
+        Act act;
+        int x, y;
+        int spaceToRight;
+        RoughTime start, end;
+        int endX;
+
+        public VisualAct(Act act)
+        {
+                this.act = act;
+        }
+
+        public int getX()
+        {
+                return x;
+        }
+
+        public int getDotR()
+        {
+                return Math.max(1, (int) Math.abs(size));
+        }
+
+        public void setX(int x)
+        {
+                this.x = x;
+        }
+
+        public int getY()
+        {
+                return y;
+        }
+
+        public void setY(int y)
+        {
+                this.y = y;
+        }
+
+        public void draw(Graphics2D g, int ox, int oy, int r, Rectangle maxFill, boolean showDuration)
+        {
+                g.setColor(getColor());
+                if (size >= 0)
+                {
+                        // Slow!
+                        g.fillOval(ox, y - r, 2 * r, 2 * r);
+                } else
+                {
+                        // Slow!
+                        g.drawOval(ox, y - r, 2 * r, 2 * r);
+                }
+                if (end != null && showDuration)
+                {
+                        int lineY = y + 6;
+                        g.fillRect(getX(), lineY, getEndX() - getX(), 2);
+                        g.drawLine(getX(), lineY, getX(), lineY - 4);
+                }
+
+        }
+
+        public Mouseover draw(Graphics2D g, Rectangle maxFill, Rectangle bounds,
+                Display display, boolean text, boolean showDuration)
+        {
+                if (!isVisible())
+                {
+                        return null;
+                }
+
+                if (x > bounds.x + bounds.width && (end == null || endX > bounds.x + bounds.width)
+                        || x < bounds.x - 200 && (end == null || endX < bounds.x - 200))
+                {
+                        return null;
+                }
+
+                g.setFont(display.plain());
+
+                int r = getDotR();
+                if (r <= 0)
+                {
+                        r = 1;
+                }
+                if (r > 30)
+                {
+                        r = 30;
+                }
+                int ox = text ? x - 2 * r : x;
+
+                draw(g, ox, y - 2, r, maxFill, showDuration);
+
+
+                if (!text)
+                {
+                        return new VisualActMouseover(ox - 2, y - r - 4, 4 + 2 * r, 4 + 2 * r);
+                }
+
+                int labelSpace = getSpaceToRight() - 12;
+                int sw = 0;
+                if (labelSpace > 50)
+                {
+                        String s = display.format(getLabel(), labelSpace / 8, true);
+                        int n = s.indexOf(' ');
+                        int tx = x + 5;
+                        int ty = y + 4;
+                        if (n < 1)
+                        {
+                                g.drawString(s, tx, ty);
+                        } else
+                        {
+                                String first = s.substring(0, n);
+                                g.drawString(first, tx, ty);
+                                Color c = ColorUtils.interpolate(g.getColor(), Color.white, .33);
+                                g.setColor(c);
+                                g.drawString(s.substring(n), tx + display.plainFontMetrics().stringWidth(first), ty);
+                        }
+                        sw = display.plainFontMetrics().stringWidth(s);
+                }
+
+                return new VisualActMouseover(x - 3 - 2 * r, y - r - 8, 14 + sw, r + 13 + 2 * r);
+        }
+
+        public Act getAct()
+        {
+                return act;
+        }
+
+        public boolean isVisible()
+        {
+                return visible;
+        }
+
+        public void setVisible(boolean visible)
+        {
+                this.visible = visible;
+        }
+
+        public RoughTime getStart()
+        {
+                return start;
+        }
+
+        public void setStart(RoughTime start)
+        {
+                this.start = start;
+        }
+
+        public Color getColor()
+        {
+                return color;
+        }
+
+        public void setColor(Color color)
+        {
+                this.color = color;
+        }
+
+        public String getLabel()
+        {
+                return label;
+        }
+
+        public void setLabel(String label)
+        {
+                this.label = label;
+        }
+
+        public String getMouseOver()
+        {
+                return mouseOver;
+        }
+
+        public void setMouseOver(String mouseOver)
+        {
+                this.mouseOver = mouseOver;
+        }
+
+        public double getSize()
+        {
+                return size;
+        }
+
+        public void setSize(double size)
+        {
+                this.size = size;
+        }
+
+        public String getTrackString()
+        {
+                return trackString == null ? "" : trackString;
+        }
+
+        public void setTrackString(String track)
+        {
+                this.trackString = track;
+        }
+
+        public TimelineTrack getTrack()
+        {
+                return track;
+        }
+
+        public void setTrack(TimelineTrack track)
+        {
+                this.track = track;
+        }
+
+        public void setSpaceToRight(int spaceToRight)
+        {
+                this.spaceToRight = spaceToRight;
+        }
+
+        public int getSpaceToRight()
+        {
+                return spaceToRight;
+        }
+
+        public int getEndX()
+        {
+                return endX;
+        }
+
+        public void setEndX(int endX)
+        {
+                this.endX = endX;
+        }
+
+        public RoughTime getEnd()
+        {
+                return end;
+        }
+
+        public void setEnd(RoughTime end)
+        {
+                this.end = end;
+        }
+
+        @Override
+        public int compareTo(Object o)
+        {
+                return RoughTime.compare(start, ((VisualAct) o).start);
+                //start.compareTo(((VisualAct)o).start);
+        }
+
+        class VisualActMouseover extends Mouseover
+        {
+
+                public VisualActMouseover(int x, int y, int w, int h)
+                {
+                        super(VisualAct.this, x, y, w, h);
+                }
+
+                public void draw(Graphics2D g, int maxW, int maxH, Display display)
+                {
+                        super.draw(g, maxW, maxH, display);
+                        Act a = getAct();
+                        ActDB db = a.getDB();
+                        java.util.List<Field> fields = db.getFields();
+                        ArrayList labels = new ArrayList();
+                        int charWidth = 40;
+                        int numLines = 1;
+                        if (VisualAct.this instanceof GroupVisualAct)
+                        {
+                                GroupVisualAct gv = (GroupVisualAct) VisualAct.this;
+                                labels.add(gv.getNumActs() + "");
+                                labels.add("items");
+                                Field sizeField = db.getField(VirtualField.SIZE);
+                                if (sizeField != null)
+                                {
+                                        labels.add("Total " + sizeField.getName());
+                                        double t = ((GroupVisualAct) (VisualAct.this)).total;
+                                        labels.add(Display.format(t));
+                                        numLines++;
+                                }
+                        } else
+                        {
+                                for (Field f : fields)
+                                {
+                                        labels.add(f.getName());
+                                        Object val = a.get(f);
+                                        String valString = display.toString(val);
+                                        if (f.getName().length() + valString.length() + 2 > charWidth)
+                                        {
+                                                ArrayList<String> lines = Display.breakLines(valString, charWidth, 2 + f.getName().length());
+                                                labels.add(lines);
+                                                numLines += lines.size() + 1;
+                                        } else
+                                        {
+                                                labels.add(valString);
+                                                numLines++;
+                                        }
+                                }
+                        }
+                        draw(g, maxW, maxH, display, labels, numLines);
+                }
+        }
+}
diff --git a/timeflow/vis/VisualActFactory.java b/timeflow/vis/VisualActFactory.java
new file mode 100755
index 0000000..bd267b6
--- /dev/null
+++ b/timeflow/vis/VisualActFactory.java
@@ -0,0 +1,119 @@
+package timeflow.vis;
+
+import java.awt.Color;
+import java.awt.Rectangle;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+
+import timeflow.data.db.Act;
+import timeflow.data.db.ActDB;
+import timeflow.data.db.ActList;
+import timeflow.data.db.Field;
+import timeflow.model.TFModel;
+import timeflow.model.VirtualField;
+
+public class VisualActFactory
+{
+        // create one VisualAct per Act
+
+        private static java.util.List<VisualAct> create(ActList acts)
+        {
+                java.util.List<VisualAct> list = new ArrayList<VisualAct>();
+                for (Act a : acts)
+                {
+                        VisualAct v = new TagVisualAct(a);
+                        list.add(v);
+                }
+                return list;
+        }
+
+        // create one VisualAct per Act/tag combo.
+        public static java.util.List<VisualAct> create(ActList acts, Field tagField, boolean multipleColors)
+        {
+                if (tagField == null || tagField.getType() == String.class)
+                {
+                        return create(acts);
+                }
+                java.util.List<VisualAct> list = new ArrayList<VisualAct>();
+                for (Act a : acts)
+                {
+                        String[] tags = a.getTextList(tagField);
+                        if (tags == null || tags.length < 2)
+                        {
+                                VisualAct v = new TagVisualAct(a);
+                                if (tags != null && tags.length == 1)
+                                {
+                                        v.setTrackString(tags[0]);
+                                }
+                                list.add(v);
+                        } else
+                        {
+                                for (String tag : tags)
+                                {
+                                        VisualAct v = multipleColors ? new TagVisualAct(a) : new VisualAct(a);
+                                        v.setTrackString(tag);
+                                        list.add(v);
+                                }
+                        }
+                }
+                return list;
+        }
+
+        public static Collection<VisualAct> makeEmFit(TFModel model, ArrayList<VisualAct> vacts, Rectangle bounds)
+        {
+                // Does everything fit? Because, if so, we're already good to go.
+                int area = bounds.width * bounds.height;
+                int room = area / 200;
+                if (vacts.size() <= room)
+                {
+                        return vacts;
+                }
+
+                ArrayList<VisualAct> results = new ArrayList<VisualAct>();
+
+                // OK. If:
+                //     * there's room for more than one item, and  
+                //     * there's more than one color in use,
+                //
+                // Then let's see how many colors there are. Maybe we can do one bubble per color.
+                ActDB db = model.getDB();
+                if (room > 1 && (db.getField(VirtualField.COLOR) != null || db.getField(VirtualField.TRACK) != null))
+                {
+                        HashMap<Color, ArrayList<VisualAct>> colorGroupings = new HashMap<Color, ArrayList<VisualAct>>();
+                        for (VisualAct v : vacts)
+                        {
+                                Color c = v.color;
+                                ArrayList<VisualAct> grouping = colorGroupings.get(c);
+                                if (grouping == null)
+                                {
+                                        grouping = new ArrayList<VisualAct>();
+                                        colorGroupings.put(c, grouping);
+                                }
+                                grouping.add(v);
+                        }
+
+                        if (colorGroupings.size() <= room) // Great! The colors fit. We now return one group VisualAct per color.
+                        {
+                                for (Color c : colorGroupings.keySet())
+                                {
+                                        ArrayList<VisualAct> grouping = colorGroupings.get(c);
+                                        if (grouping.size() == 1)
+                                        {
+                                                results.add(grouping.get(0));
+                                        } else if (grouping.size() > 1)
+                                        {
+                                                results.add(new GroupVisualAct(grouping, false, bounds));
+                                        }
+                                }
+                                return results;
+                        }
+                }
+
+                // OK, too bad, even that doesn't fit. We will just create one fat VisualAct
+                // that descibes the aggregate. C'est la vie!
+
+                results.add(new GroupVisualAct(vacts, true, bounds));
+                return results;
+        }
+}
diff --git a/timeflow/vis/VisualEncoder.java b/timeflow/vis/VisualEncoder.java
new file mode 100755
index 0000000..9deee56
--- /dev/null
+++ b/timeflow/vis/VisualEncoder.java
@@ -0,0 +1,138 @@
+package timeflow.vis;
+
+import timeflow.data.db.*;
+import timeflow.model.*;
+
+import java.awt.Color;
+import java.util.*;
+
+public class VisualEncoder
+{
+
+        private TFModel model;
+        private java.util.List<VisualAct> visualActs = new ArrayList<VisualAct>();
+        private double maxSize = 0;
+
+        public VisualEncoder(TFModel model)
+        {
+                this.model = model;
+        }
+
+        public java.util.List<VisualAct> getVisualActs()
+        {
+                return visualActs;
+        }
+
+        public void createVisualActs()
+        {
+                Field colorField = model.getDB().getField(VirtualField.COLOR);
+                Field trackField = model.getDB().getField(VirtualField.TRACK);
+                boolean multipleColors = colorField != null && colorField.getType() == String[].class && colorField != trackField;
+                visualActs = VisualActFactory.create(model.getDB().all(), trackField, multipleColors);
+                Collections.sort(visualActs);
+        }
+
+        public List<VisualAct> apply()
+        {
+
+                ActList visibleActs = model.getActs();
+                Field start = model.getDB().getField(VirtualField.START);
+                Field end = model.getDB().getField(VirtualField.END);
+                Field size = model.getDB().getField(VirtualField.SIZE);
+                if (size != null)
+                {
+                        double[] minmax = DBUtils.minmax(model.getActs(), size);
+                        maxSize = Math.max(Math.abs(minmax[0]), Math.abs(minmax[1]));
+                }
+
+                // apply color, label, visibility, etc.
+                for (VisualAct v : visualActs)
+                {
+                        Act a = v.getAct();
+                        v.setStart(start == null ? null : a.getTime(start));
+                        v.setEnd(end == null ? null : a.getTime(end));
+                        v.setVisible(visibleActs.contains(a) && v.getStart() != null && v.getStart().isDefined());
+                        apply(v);
+                }
+
+                return visualActs;
+        }
+
+        public void apply(VisualAct v)
+        {
+                Display display = model.getDisplay();
+                ActDB db = model.getDB();
+                Act a = v.getAct();
+                Field label = db.getField(VirtualField.LABEL);
+                Field track = db.getField(VirtualField.TRACK);
+                Field color = db.getField(VirtualField.COLOR);
+                Field size = db.getField(VirtualField.SIZE);
+
+                if (label == null)
+                {
+                        v.setLabel("");
+                } else
+                {
+                        v.setLabel(a.getString(label));
+                }
+
+                double s = Display.MAX_DOT_SIZE;
+                if (size == null || maxSize == 0)
+                {
+                        v.setSize(s / 3);
+                } else
+                {
+                        double z = s * Math.sqrt(Math.abs(a.getValue(size)) / maxSize);
+                        if (a.getValue(size) < 0)
+                        {
+                                z = -z;
+                        }
+                        v.setSize(z);
+                }
+
+                // For setting the track:
+                // This is a little complicated, but if the track is set to
+                // tags (that is, track.getType()==String[].class) then
+                // the track string was set earlier so it doesn't need to be set now.
+                if (track == null)
+                {
+                        v.setTrackString("");
+                } else if (track.getType() == String.class)
+                {
+                        v.setTrackString(a.getString(track));
+                }
+
+                if (color == null || color == track)
+                {
+                        if (track == null)
+                        {
+                                v.setColor(display.getColor("timeline.unspecified.color"));
+                        } else
+                        {
+                                v.setColor(display.makeColor(v.getTrackString()));
+                        }
+                } else
+                {
+                        if (color.getType() == String[].class)
+                        {
+                                String[] tags = a.getTextList(color);
+                                if (tags == null || tags.length == 0)
+                                {
+                                        ((TagVisualAct) v).setColors(new Color[0]);
+                                } else
+                                {
+                                        int n = tags.length;
+                                        Color[] c = new Color[n];
+                                        for (int i = 0; i < n; i++)
+                                        {
+                                                c[i] = display.makeColor(tags[i]);
+                                        }
+                                        ((TagVisualAct) v).setColors(c);
+                                }
+                        } else
+                        {
+                                v.setColor(display.makeColor(a.getString(color)));
+                        }
+                }
+        }
+}
\ No newline at end of file
diff --git a/timeflow/vis/calendar/CalendarVisuals.java b/timeflow/vis/calendar/CalendarVisuals.java
new file mode 100755
index 0000000..d37f51c
--- /dev/null
+++ b/timeflow/vis/calendar/CalendarVisuals.java
@@ -0,0 +1,113 @@
+package timeflow.vis.calendar;
+
+import java.awt.*;
+import java.util.Collection;
+
+
+import timeflow.model.*;
+import timeflow.vis.*;
+import timeflow.data.time.*;
+import timeflow.data.db.*;
+
+public class CalendarVisuals {
+	VisualEncoder encoder;
+	TFModel model;
+	Rectangle bounds=new Rectangle();
+	public Grid grid;
+	
+	public enum Layout {DAY, WEEK, MONTH, YEAR};
+	private Layout layoutStyle=Layout.DAY;
+	
+	public enum DrawStyle {LABEL, ICON};
+	DrawStyle drawStyle=DrawStyle.ICON;
+	
+	public enum FitStyle {LOOSE, TIGHT};
+	FitStyle fitStyle=FitStyle.LOOSE;
+	
+	public CalendarVisuals(TFModel model)
+	{
+		this.model=model;
+		encoder=new VisualEncoder(model);
+	}	
+	
+	public void render(Graphics2D g, Collection<Mouseover> objectLocations)
+	{
+		grid.render(g, model.getDisplay(), bounds, this, objectLocations);
+	}	
+	
+	public Interval getGlobalInterval()
+	{
+		return DBUtils.range(model.getDB().all(), VirtualField.START);
+	}
+	
+	public Rectangle getBounds() {
+		return bounds;
+	}
+
+	public void setBounds(int x, int y, int w, int h) {
+		bounds.setBounds(x,y,w,h);
+		if (grid==null)
+			makeGrid(true);
+	}
+	
+	public void setDrawStyle(DrawStyle drawStyle)
+	{
+		this.drawStyle=drawStyle;
+	}
+	
+	public void setLayoutStyle(Layout layoutStyle)
+	{
+		this.layoutStyle=layoutStyle;
+		makeGrid(true);
+	}
+	
+	public void setFitStyle(FitStyle fitStyle)
+	{
+		this.fitStyle=fitStyle;
+		makeGrid(true);
+	}
+	
+	public void makeGrid(boolean fresh)
+	{
+		Interval interval=getGlobalInterval();
+		int dy=0;
+		if (grid!=null)
+			dy=grid.dy;
+		switch(layoutStyle)
+		{
+			case DAY: grid=new Grid(TimeUnit.WEEK, TimeUnit.DAY_OF_WEEK, interval); break;
+			case WEEK: grid=new Grid(TimeUnit.multipleWeeks(8), TimeUnit.WEEK, interval); break;
+			case MONTH: grid=new Grid(TimeUnit.YEAR, TimeUnit.MONTH, interval); break;
+			case YEAR: grid=new Grid(TimeUnit.DECADE, TimeUnit.YEAR, interval); 
+		}		
+		grid.makeCells(encoder.getVisualActs());
+		if (!fresh)
+			grid.dy=dy;
+	}
+
+	public void init()
+	{
+		note(new TFEvent(TFEvent.Type.DATABASE_CHANGE,null));
+	}
+	
+	public void initAllButGrid()
+	{
+		encoder.createVisualActs();
+		encoder.apply();
+	}
+	
+	public void note(TFEvent e) {
+		if (e.affectsRowSet())
+		{
+			encoder.createVisualActs();
+		}
+
+		updateVisuals(e.affectsRowSet());
+	}
+		
+	public void updateVisuals(boolean fresh)
+	{
+		encoder.apply();
+		makeGrid(fresh);
+	}
+}
diff --git a/timeflow/vis/calendar/Grid.java b/timeflow/vis/calendar/Grid.java
new file mode 100755
index 0000000..0dd2881
--- /dev/null
+++ b/timeflow/vis/calendar/Grid.java
@@ -0,0 +1,375 @@
+package timeflow.vis.calendar;
+
+import timeflow.data.time.*;
+import timeflow.model.Display;
+import timeflow.vis.*;
+
+import java.util.*;
+import java.awt.*;
+import java.text.*;
+
+public class Grid
+{
+
+        TimeUnit rowUnit, columnUnit;
+        RoughTime startRow, endRow;
+        Interval interval;
+        HashMap<Long, GridCell> cells;
+        int[] screenGridX;
+        int cellHeight = 80, cellWidth, numCols, numRows;
+        Rectangle bounds = new Rectangle();
+        int dy;
+        static final DateFormat dayOfWeek = new SimpleDateFormat("EEE");
+        static final DateFormat month = new SimpleDateFormat("MMM d");
+        static final String[] day =
+        {
+                "SUN", "MON", "TUES", "WED", "THURS", "FRI", "SAT"
+        };
+
+        Grid(TimeUnit rowUnit, TimeUnit columnUnit, Interval interval)
+        {
+                this.rowUnit = rowUnit;
+                this.columnUnit = columnUnit;
+                numCols = columnUnit.numUnitsIn(rowUnit);
+                setInterval(interval);
+        }
+
+        public void setDY(int dy)
+        {
+                this.dy = dy;
+        }
+
+        public int getCalendarHeight()
+        {
+                return bounds.height + bounds.y + 20;
+        }
+
+        public RoughTime getTime(int x, int y)
+        {
+                y += dy;
+                if (!bounds.contains(x, y))
+                {
+                        return null;
+                }
+
+                // find grid coordinates.
+                int gridX = (x - bounds.x) / cellWidth;
+                int gridY = (y - bounds.y) / cellHeight;
+
+                return startRow.plus(rowUnit, gridY).plus(columnUnit, gridX);
+        }
+
+        public double getScrollFraction()
+        {
+                double x = (getFirstDrawnTime().getTime() - startRow.getTime()) / (double) (endRow.getTime() - startRow.getTime());
+                if (x < 0)
+                {
+                        return 0;
+                }
+                if (x > 1)
+                {
+                        return 1;
+                }
+                return x;
+        }
+
+        public RoughTime getFirstDrawnTime()
+        {
+                int gridY = (dy - bounds.y) / cellHeight;
+
+                return startRow.plus(rowUnit, gridY);
+        }
+
+        private Point getGridCorner(long timestamp)
+        {
+                int diff = (int) columnUnit.difference(timestamp, startRow.getTime());
+                int gridX = diff % numCols;
+                int gridY = diff / numCols;
+                return new Point(gridX, gridY);
+        }
+
+        public Rectangle getCell(long timestamp)
+        {
+                Point p = getGridCorner(timestamp);
+                return new Rectangle(bounds.x + p.x * cellWidth, bounds.y + p.y * cellHeight - dy,
+                        cellWidth, cellHeight);
+        }
+
+        void setInterval(Interval interval)
+        {
+                this.interval = interval;
+                startRow = rowUnit.roundDown(interval.start);
+                endRow = rowUnit.roundDown(interval.end);
+                numRows = 1 + (int) (rowUnit.difference(endRow.getTime(), startRow.getTime()));
+
+                // the next line fixes a problem with multi-century data sets.
+                // it works, but there's probably a better way to do this :-)
+                if (numRows > 50 && rowUnit.getRoughSize() >= TimeUnit.YEAR.getRoughSize())
+                {
+                        numRows++;
+                }
+        }
+
+        void makeCells(java.util.List<VisualAct> visualActs)
+        {
+                cells = new HashMap<Long, GridCell>();
+                for (VisualAct v : visualActs)
+                {
+                        if (v.getStart() == null)
+                        {
+                                continue;
+                        }
+                        long timestamp = v.getStart().getTime();
+                        RoughTime timeKey = columnUnit.roundDown(timestamp);
+                        GridCell cell = cells.get(timeKey.getTime());
+                        if (cell == null)
+                        {
+                                cell = new GridCell(timeKey);
+                                cells.put(timeKey.getTime(), cell);
+                                Point p = getGridCorner(timestamp);
+                                cell.gridX = p.x;
+                                cell.gridY = p.y;
+                        }
+                        cell.visualActs.add(v);
+                }
+        }
+
+        void render(Graphics2D g, Display display, Rectangle screenBounds, CalendarVisuals visuals,
+                Collection<Mouseover> objectLocations)
+        {
+                g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+                int left = 110, right = 20;
+                int padY = 50;
+                boolean shouldLabel = visuals.drawStyle == CalendarVisuals.DrawStyle.LABEL;
+
+                cellWidth = (screenBounds.width - left - right) / numCols;
+
+                boolean fitTight = visuals.fitStyle == CalendarVisuals.FitStyle.TIGHT;
+                int idealHeight = fitTight ? 12 : Display.CALENDAR_CELL_HEIGHT;
+                cellHeight = Math.max(idealHeight, (screenBounds.height - padY - 10) / numRows);
+                this.bounds.setBounds(left, padY, numCols * cellWidth, numRows * cellHeight);
+
+                g.setColor(new Color(240, 240, 240));//Color.white);
+                g.fill(screenBounds);
+                g.setColor(new Color(245, 245, 245));
+                g.drawRect(bounds.x, bounds.y - dy, bounds.width, bounds.height);
+                g.setFont(display.bold());
+
+                // draw vertical grid lines.
+                Color gridColor = new Color(220, 220, 220);
+
+                for (int i = 0; i <= numCols; i++)
+                {
+                        int x = bounds.x + i * cellWidth;
+                        g.setColor(gridColor);
+                        g.drawLine(x, bounds.y - dy, x, bounds.y + bounds.height - dy);
+                        if (rowUnit == TimeUnit.WEEK && i < 7)
+                        {
+                                g.setColor(Color.gray);
+                                g.drawString(day[i], x, bounds.y - dy - 6);
+                        }
+                }
+
+                // horizontal grid lines.
+                RoughTime labelTime = startRow.copy();
+                int lastLabelY = -100;
+                int lastYear = -1000000;
+                FontMetrics fm = display.boldFontMetrics();
+                int skipped = 0;
+                for (int i = 0; i < numRows; i++)
+                {
+                        int y = bounds.y + i * cellHeight;
+                        if (y - dy > -50)
+                        {
+                                if (skipped > 0)
+                                {
+                                        rowUnit.addTo(labelTime, skipped);
+                                        skipped = 0;
+                                }
+                                g.setColor(gridColor);
+                                g.drawLine(bounds.x, y - dy, bounds.x + bounds.width, y - dy);
+                                if (y - lastLabelY > 30 || lastLabelY < 0)
+                                {
+                                        String label = null;
+                                        if (rowUnit == TimeUnit.WEEK)
+                                        {
+                                                int year = TimeUtils.cal(labelTime.getTime()).get(Calendar.YEAR);
+                                                if (year != lastYear)
+                                                {
+                                                        label = labelTime.format();
+                                                } else
+                                                {
+                                                        label = month.format(labelTime.toDate());
+                                                }
+                                                lastYear = year;
+                                        } else
+                                        {
+                                                label = labelTime.format();
+                                        }
+                                        g.setColor(Color.gray);
+                                        g.drawString(label, bounds.x - fm.stringWidth(label) - 15, y + 15 - dy);
+                                        lastLabelY = y;
+                                }
+                                if (y - dy > screenBounds.height)
+                                {
+                                        break;
+                                }
+
+                                // now draw, in gray, the labels for each of the boxes.
+                                if (!fitTight)
+                                {
+                                        RoughTime gridLabel = labelTime.copy();
+                                        int labelH = 13;
+                                        for (int j = 0; j < numCols; j++)
+                                        {
+
+                                                g.setColor(Color.gray);
+                                                g.setFont(display.bold());
+                                                String label = columnUnit.format(gridLabel.toDate());
+                                                g.drawString(label, bounds.x + j * cellWidth + 3, y - dy + labelH);
+                                                columnUnit.addTo(gridLabel);
+                                        }
+                                }
+                                rowUnit.addTo(labelTime);
+                        } else
+                        {
+                                skipped++;
+                        }
+                }
+
+                // draw a frame around the whole thing.
+                g.setColor(Color.darkGray);
+                g.drawRect(bounds.x, bounds.y - dy, bounds.width, bounds.height);
+
+                // draw backgrounds
+                for (GridCell cell : cells.values())
+                {
+                        // are any visible?
+                        boolean visible = false;
+                        for (VisualAct v : cell.visualActs)
+                        {
+                                if (v.isVisible())
+                                {
+                                        visible = true;
+                                        break;
+                                }
+                        }
+                        int cx = bounds.x + cell.gridX * cellWidth;
+                        int cy = bounds.y + cell.gridY * cellHeight - dy;
+
+                        if (cy < screenBounds.y - 50 || cy > screenBounds.y + screenBounds.height + 50)
+                        {
+                                continue;
+                        }
+
+                        // label top of cell.
+                        int labelH = 0;
+                        g.setColor(new Color(240, 240, 240));
+
+                        if (visible)
+                        {
+                                g.setColor(Color.white);
+                                g.fillRect(cx + 1, cy + 1, cellWidth - 1, cellHeight - 1);
+                        }
+                        if (cellHeight > 42)
+                        {
+                                labelH = 13;
+                                g.setColor(Color.darkGray);
+                                g.setFont(display.bold());
+                                String label = columnUnit.format(cell.time.toDate());
+                                g.drawString(label, cx + 3, cy + labelH);
+                        }
+
+                }
+
+
+
+                // draw items.
+                int mx = 10, my = shouldLabel ? 18 : 10;
+                for (GridCell cell : cells.values())
+                {
+
+                        int cx = bounds.x + cell.gridX * cellWidth;
+                        int cy = bounds.y + cell.gridY * cellHeight - dy;
+
+                        if (cy < screenBounds.y - 50 || cy > screenBounds.y + screenBounds.height + 50)
+                        {
+                                continue;
+                        }
+
+                        // label top of cell.
+                        int labelH = cellHeight > 42 ? 13 : 0;
+
+                        // now draw the items in the cell.
+                        // old, non-aggregation code:
+
+                        // START AGGREGATION CODE
+
+                        ArrayList<VisualAct> visibleActs = new ArrayList<VisualAct>();
+                        for (VisualAct v : cell.visualActs)
+                        {
+                                if (v.isVisible())
+                                {
+                                        visibleActs.add(v);
+                                }
+                        }
+                        Iterator<VisualAct> vacts =
+                                VisualActFactory.makeEmFit(visuals.model, visibleActs, new Rectangle(cx, cy, cellWidth, cellHeight)).iterator();
+
+                        // END AGGREGATION CODE
+
+                        int leftX = 6;
+                        int cdx = leftX;
+                        int topDotY = Math.min(labelH + 16, cellHeight / 2);
+                        int cdy = topDotY;
+                        while (vacts.hasNext())
+                        {
+                                VisualAct v = vacts.next();
+                                if (!v.isVisible())
+                                {
+                                        continue;
+                                }
+
+                                // set x,y, room to right.
+                                int x = cx + cdx;
+                                int y = cy + cdy;
+
+                                int space = cellWidth - 20;
+                                v.setX(x);
+                                v.setY(y);
+                                v.setSpaceToRight(space);
+                                Mouseover o = v.draw(g, new Rectangle(cx + 1, cy + labelH + 1, cellWidth - 2, cellHeight - 2 - labelH),
+                                        bounds, display, shouldLabel, false);
+                                if (o != null)
+                                {
+                                        objectLocations.add(o);
+                                }
+
+                                // go to next location. if we're labeling, we do this vertically.
+                                // otherwise, left-to-right, then top-to-bottom.
+
+                                if (shouldLabel)
+                                {
+                                        cdy += my;
+                                        if (cdy > cellHeight - 2 - my)
+                                        {
+                                                g.drawString("...", x, y + my);
+                                                break;
+                                        }
+                                } else
+                                {
+                                        cdx += mx;
+                                        if (cdx > cellWidth - mx / 2 - 2 && vacts.hasNext())
+                                        {
+                                                cdx = leftX;
+                                                cdy += my;
+                                                if (cdy > cellHeight - my / 2)
+                                                {
+                                                        break;
+                                                }
+                                        }
+                                }
+                        }
+                }
+        }
+}
diff --git a/timeflow/vis/calendar/GridCell.java b/timeflow/vis/calendar/GridCell.java
new file mode 100755
index 0000000..44dd7ef
--- /dev/null
+++ b/timeflow/vis/calendar/GridCell.java
@@ -0,0 +1,20 @@
+package timeflow.vis.calendar;
+
+
+import timeflow.vis.*;
+import timeflow.data.time.*;
+
+import java.util.*;
+import java.awt.*;
+
+public class GridCell {
+	ArrayList<VisualAct> visualActs=new ArrayList<VisualAct>();
+	Rectangle bounds;
+	RoughTime time;
+	int gridX, gridY;
+	
+	GridCell(RoughTime time)
+	{
+		this.time=time;
+	}
+}
diff --git a/timeflow/vis/timeline/AxisRenderer.java b/timeflow/vis/timeline/AxisRenderer.java
new file mode 100755
index 0000000..7d020cb
--- /dev/null
+++ b/timeflow/vis/timeline/AxisRenderer.java
@@ -0,0 +1,88 @@
+package timeflow.vis.timeline;
+
+import java.awt.*;
+import java.util.*;
+
+import timeflow.data.time.Interval;
+import timeflow.data.time.TimeUtils;
+import timeflow.model.*;
+import timeflow.vis.Mouseover;
+import timeflow.vis.TimeScale;
+
+public class AxisRenderer {
+	
+	TimelineVisuals visuals;
+	
+	public AxisRenderer(TimelineVisuals visuals)
+	{
+		this.visuals=visuals;
+	}
+	
+	public void render(Graphics2D g, Collection<Mouseover> objectLocations)
+	{		
+		TFModel model=visuals.getModel();
+		g.setColor(model.getDisplay().getColor("chart.background"));
+		Rectangle bounds=visuals.getBounds();
+		
+		TimeScale scale=visuals.getTimeScale();
+		java.util.List<AxisTicMarks> t=AxisTicMarks.allRelevant(scale.getInterval());
+		
+		int dateLabelH=model.getDisplay().getInt("timeline.datelabel.height");
+		int y=bounds.y+bounds.height-dateLabelH;	
+		
+		// draw in reverse order so bigger granularity at top.
+		int n=t.size();
+		for (int i=0; i<n; i++)
+		{
+			render(t.get(i), g, bounds.x, y, dateLabelH-1, bounds.y, i==0, objectLocations);
+			y-=dateLabelH;
+		}
+	}
+	
+	void render(AxisTicMarks t, Graphics2D g, int x, int y, int h, int top, boolean full, Collection<Mouseover> objectLocations)
+	{
+		TFModel model=visuals.getModel();
+
+		int n=t.tics.size();
+		for (int i=0; i<n-1; i++)
+		{
+			
+			long start=t.tics.get(i);
+			long end=t.tics.get(i+1);
+			
+			int x0=Math.max(x,visuals.getTimeScale().toInt(start));			
+			int x1=visuals.getTimeScale().toInt(end);
+			
+			int dayOfWeek=TimeUtils.cal(start).get(Calendar.DAY_OF_WEEK);
+			
+			g.setColor(t.unit.isDayOrLess() && (dayOfWeek==1 || dayOfWeek==7) ? 
+					new Color(245,245,245) : new Color(240,240,240));
+
+			g.fillRect(x0, y, x1-x0-1, h);
+			g.setColor(Color.white);
+			g.drawLine(x1-1, y, x1-1, y+h);
+			g.drawLine(x0,y+h,x1,y+h);
+			objectLocations.add(new Mouseover(new Interval(start,end), x0, y, x1-x0-1, h));
+			
+			g.setFont(model.getDisplay().timeLabel());
+			String label=full? t.unit.formatFull(start) : t.unit.format(new Date(start));
+			int tx=x0+3;
+			int ty=y+h-5;
+			g.setColor(full ? Color.darkGray : Color.gray);
+			int sw=model.getDisplay().timeLabelFontMetrics().stringWidth(label);
+			if (sw<x1-tx-3)
+				g.drawString(label, tx,ty);
+			else
+			{
+				int c=label.indexOf(':');
+				if (c>0)
+				{
+					label=label.substring(0,c);
+					sw=model.getDisplay().timeLabelFontMetrics().stringWidth(label);
+					if (sw<x1-tx-3)
+						g.drawString(label, tx,ty);
+				}
+			}
+		}
+	}
+}
diff --git a/timeflow/vis/timeline/AxisTicMarks.java b/timeflow/vis/timeline/AxisTicMarks.java
new file mode 100755
index 0000000..85a16dd
--- /dev/null
+++ b/timeflow/vis/timeline/AxisTicMarks.java
@@ -0,0 +1,117 @@
+package timeflow.vis.timeline;
+
+import java.util.*;
+
+import timeflow.data.time.*;
+
+public class AxisTicMarks {
+	public TimeUnit unit;
+	public List<Long> tics;
+	
+	private static final TimeUnit[] units={
+		TimeUnit.YEAR, TimeUnit.MONTH, TimeUnit.DAY, TimeUnit.HOUR, TimeUnit.MINUTE, TimeUnit.SECOND
+	};
+	
+	private static final TimeUnit[] histUnits={
+		TimeUnit.YEAR.times(100), TimeUnit.YEAR.times(50), TimeUnit.YEAR.times(25), 
+			TimeUnit.YEAR.times(10), TimeUnit.YEAR.times(5), TimeUnit.YEAR.times(2), TimeUnit.YEAR,
+		TimeUnit.MONTH.times(6), TimeUnit.MONTH.times(3), TimeUnit.MONTH.times(2), TimeUnit.MONTH, 
+		TimeUnit.WEEK, TimeUnit.DAY.times(2), TimeUnit.DAY,
+
+		TimeUnit.HOUR, 
+		TimeUnit.MINUTE, 
+		TimeUnit.SECOND
+	};
+	
+	public AxisTicMarks(TimeUnit unit, long start, long end)
+	{
+		this.unit=unit;
+		tics=new ArrayList<Long>();
+		RoughTime r=unit.roundDown(start);
+		tics.add(r.getTime());
+		do
+		{
+			unit.addTo(r);
+			tics.add(r.getTime());
+		} while (r.getTime()<end);	
+	}
+	
+	
+	
+	public static List<AxisTicMarks> allRelevant(Interval interval)
+	{
+		return allRelevant(interval.start, interval.end);
+	}
+	
+	public static List<AxisTicMarks> allRelevant(long start, long end)
+	{
+		return allRelevant(start, end, 40);
+	}
+	
+	public static AxisTicMarks histoTics(long start, long end)
+	{
+		for (int i=histUnits.length-1; i>=0; i--)
+		{
+			TimeUnit u=histUnits[i];
+			long estimate=u.approxNumInRange(start, end);		
+			if (estimate<200 || i==0)
+			{
+				AxisTicMarks t=new AxisTicMarks(u, start, end);
+				return t;
+			}
+		}
+		return null;
+	}
+	
+	public static List<AxisTicMarks> allRelevant(long start, long end, long maxTics)
+	{
+		List<AxisTicMarks> list=new ArrayList<AxisTicMarks>();
+		
+		
+		for (int i=0; i<units.length; i++)
+		{
+			TimeUnit u=units[i];
+			long estimate=u.approxNumInRange(start, end);
+			
+			if (estimate<maxTics)
+			{
+				AxisTicMarks t=new AxisTicMarks(u, start, end);
+				if (list.size()>0)
+				{
+					AxisTicMarks last=list.get(0);
+					if (last.tics.size()==t.tics.size())
+						list.remove(0);
+				}
+				list.add(t);
+				
+			}
+		}
+		while (list.size()>2)
+			list.remove(0);
+		
+		if (list.size()==0) // uh oh! must be many years. we will add in bigger increments.
+		{
+			long length=end-start;
+			long size=365*24*60*60*1000L;
+			int m=1;
+			maxTics=15;
+			while (m<2000000000 && length/(m*size)>maxTics)
+			{
+				if (length/(2*m*size)<=maxTics)
+				{
+					m*=2;
+					break;
+				}
+				if (length/(5*m*size)<=maxTics)
+				{
+					m*=5;
+					break;
+				}
+				m*=10;
+			}	
+			AxisTicMarks t=new AxisTicMarks(TimeUnit.multipleYears(m), start, end);
+			list.add(t);
+		}	
+		return list;
+	}
+}
diff --git a/timeflow/vis/timeline/TimelineRenderer.java b/timeflow/vis/timeline/TimelineRenderer.java
new file mode 100755
index 0000000..4c115a5
--- /dev/null
+++ b/timeflow/vis/timeline/TimelineRenderer.java
@@ -0,0 +1,184 @@
+package timeflow.vis.timeline;
+
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+import timeflow.model.*;
+import timeflow.vis.Mouseover;
+import timeflow.vis.MouseoverLabel;
+import timeflow.vis.VisualAct;
+
+import timeflow.util.*;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.util.*;
+import java.util.List;
+
+public class TimelineRenderer
+{
+
+        private TimelineVisuals visuals;
+        private int dy;
+
+        public TimelineRenderer(TimelineVisuals visuals)
+        {
+                this.visuals = visuals;
+        }
+
+        public void setDY(int dy)
+        {
+                this.dy = dy;
+        }
+
+        public void render(Graphics2D g, Collection<Mouseover> objectLocations)
+        {
+                AffineTransform old = g.getTransform();
+                g.setTransform(AffineTransform.getTranslateInstance(0, -dy));
+
+                g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+                TFModel model = visuals.getModel();
+                Display display = model.getDisplay();
+                ActDB db = model.getDB();
+
+                if (display.emptyMessage(g, model))
+                {
+                        return;
+                }
+
+                // need to check this, because resize events don't (and shouldn't) register with central TFModel.
+                visuals.layoutIfChanged();
+
+                java.util.List<VisualAct> visualActs = visuals.getVisualActs();
+
+                if (visualActs == null || visualActs.size() == 0)
+                {
+                        g.drawString("No data", 10, 30);
+                        return;
+                }
+
+                Rectangle bounds = visuals.getBounds();
+                boolean colorTrackLabels = db.getField(VirtualField.COLOR) == null || db.getField(VirtualField.COLOR).equals(db.getField(VirtualField.TRACK));
+
+                // draw tracks, if more than 1.
+                if (visuals.trackList.size() > 1)
+                {
+                        boolean zebra = true;
+
+                        for (TimelineTrack t : visuals.trackList)
+                        {
+                                if (zebra)
+                                {
+                                        g.setColor(display.getColor("timeline.zebra"));
+                                        g.fillRect(bounds.x, t.y0, bounds.width, t.y1 - t.y0);
+                                }
+                                zebra = !zebra;
+                                g.setColor(display.getColor("timeline.grid"));
+                                g.drawLine(bounds.x, t.y0, bounds.x + bounds.width, t.y0);
+                        }
+                }
+
+                Interval screenInterval = visuals.getViewInterval().subinterval(-.5, 1.5);
+                AxisTicMarks tics = AxisTicMarks.histoTics(screenInterval.start, screenInterval.end);
+                for (TimelineTrack t : visuals.trackList)
+                {
+                        // now... if not in graph mode, just draw items
+                        if (visuals.getLayoutStyle() != TimelineVisuals.Layout.GRAPH)//max<(t.y1-t.y0)/20)
+                        {
+                                for (VisualAct v : t.visualActs)
+                                {
+                                        Mouseover o = v.draw(g, null, bounds, display, true, true);
+                                        if (o != null)
+                                        {
+                                                o.y -= dy;
+                                                objectLocations.add(o);
+                                        }
+                                }
+                                continue;
+                        }
+
+                if (true) continue;
+                
+                        // draw bars. to do so, we make a histogram of visible items.
+                        t.histogram = new DoubleBag<Long>();
+                        for (VisualAct v : t.visualActs)
+                        {
+                                long time = v.getStart().getTime();
+                                if (screenInterval.contains(time))
+                                {
+                                        t.histogram.add(tics.unit.roundDown(v.getStart().getTime()).getTime(), 1);//v.getSize());
+                                }
+                        }
+
+                        // get max of items.
+                        double max = t.histogram.getMax();
+
+                        // now draw bars on screen.
+                        Color fg = colorTrackLabels ? model.getDisplay().makeColor(t.label) : Color.gray;
+                        if (visuals.trackList.size() < 2)
+                        {
+                                fg = Color.gray;
+                        }
+
+                        List<Long> keys = t.histogram.unordered();
+                        Collections.sort(keys);
+                        for (Long r : keys)
+                        {
+                                double num = t.histogram.num(r);
+                                int x1 = visuals.getTimeScale().toInt(r);
+                                int x2 = visuals.getTimeScale().toInt(tics.unit.roundUp(r + 1).getTime());
+                                int barY = t.y1 - (int) (.9 * ((t.y1 - t.y0) * num) / max);
+                                g.setColor(new Color(230, 230, 230));
+                                int m = 12;
+                                g.fillRoundRect(x1 + 3, barY + 3, x2 - x1 - 1, t.y1 - barY, m, m);
+
+                                g.setColor(fg);
+                                g.fillRoundRect(x1, barY, x2 - x1 - 1, t.y1 - barY, m, m);
+
+                                MouseoverLabel mouse = new MouseoverLabel("" + Math.round(num), "items", x1, barY, x2 - x1 - 1, t.y1 - barY);
+                                objectLocations.add(mouse);
+                        }
+                }
+
+                // finally label the tracks. we do this last so that the labels go on top of the data.
+
+                if (visuals.trackList.size() > 1)
+                {
+                        boolean zebra = false;
+                        FontMetrics hugeFm = display.hugeFontMetrics();
+                        for (TimelineTrack t : visuals.trackList)
+                        {
+
+                                // now label the track.
+                                //if (t.y1 - t.y0 > 23)
+                                {
+                                        Color fg = colorTrackLabels ? model.getDisplay().makeColor(t.label) : Color.darkGray;
+                                        Color bg = zebra ? display.getColor("timeline.zebra") : Color.white;
+
+                                        String label = t.label;
+                                        if (label.equals(Display.MISC_CODE))
+                                        {
+                                                label = display.getMiscLabel();
+                                        } else if (label.length() == 0)
+                                        {
+                                                label = display.getNullLabel();
+                                        } else
+                                        {
+                                                label = display.format(label, 20, false);
+                                        }
+
+                                        // draw background.
+                                        g.setColor(bg);
+                                        int sw = hugeFm.stringWidth(label);
+                                        g.fillRect(0, t.y1 - 20, sw + 8, 19);
+
+                                        // draw foreground (actual label)	
+                                        g.setFont(display.huge());
+                                        g.setColor(fg);
+                                        g.drawString(label, 2, t.y1); //  - 5);
+                                }
+                                zebra = !zebra;
+                        }
+                }
+                g.setTransform(old);
+        }
+}
diff --git a/timeflow/vis/timeline/TimelineSlider.java b/timeflow/vis/timeline/TimelineSlider.java
new file mode 100755
index 0000000..005db28
--- /dev/null
+++ b/timeflow/vis/timeline/TimelineSlider.java
@@ -0,0 +1,239 @@
+package timeflow.vis.timeline;
+
+import timeflow.data.db.*;
+import timeflow.data.time.*;
+import timeflow.model.*;
+import timeflow.vis.TimeScale;
+import timeflow.vis.VisualAct;
+import timeflow.vis.timeline.*;
+
+import timeflow.util.*;
+
+import java.awt.*;
+import javax.swing.*;
+import java.awt.event.*;
+
+public class TimelineSlider extends ModelPanel
+{
+
+        TimelineVisuals visuals;
+        Interval original;
+        long minRange;
+        int ew = 10;
+        int eventRadius = 2;
+        TimeScale scale;
+        Point mouseHit = new Point();
+        Point mouse = new Point(-1, 0);
+
+        enum Modify
+        {
+
+                START, END, POSITION, NONE
+        };
+        Modify change = Modify.NONE;
+        Rectangle startRect = new Rectangle(-1, -1, 0, 0);
+        Rectangle endRect = new Rectangle(-1, -1, 0, 0);
+        Rectangle positionRect = new Rectangle(-1, -1, 0, 0);
+        Color sidePlain = Color.orange;
+        Color sideMouse = new Color(230, 100, 0);
+
+        public TimelineSlider(final TimelineVisuals visuals, final long minRange, final Runnable action)
+        {
+                super(visuals.getModel());
+
+                this.minRange = minRange;
+                this.visuals = visuals;
+
+                addMouseListener(new MouseAdapter()
+                {
+
+                        @Override
+                        public void mousePressed(MouseEvent e)
+                        {
+                                int mx = e.getX();
+                                int my = e.getY();
+                                if (positionRect.contains(mx, my))
+                                {
+                                        change = Modify.POSITION;
+                                } else if (startRect.contains(mx, my))
+                                {
+                                        change = Modify.START;
+                                } else if (endRect.contains(mx, my))
+                                {
+                                        change = Modify.END;
+                                } else
+                                {
+                                        change = Modify.NONE;
+                                }
+                                mouseHit.setLocation(mx, my);
+                                original = window().copy();
+                                mouse.setLocation(mx, my);
+                                repaint();
+                        }
+
+                        @Override
+                        public void mouseReleased(MouseEvent e)
+                        {
+                                change = Modify.NONE;
+                                repaint();
+                        }
+                });
+                addMouseMotionListener(new MouseMotionAdapter()
+                {
+
+                        @Override
+                        public void mouseDragged(MouseEvent e)
+                        {
+
+                                if (change == Modify.NONE)
+                                {
+                                        return;
+                                }
+                                mouse.setLocation(e.getX(), e.getY());
+                                int mouseDiff = mouse.x - mouseHit.x;
+                                Interval limits = visuals.getGlobalInterval();
+                                long timeDiff = scale.spaceToTime(mouseDiff);
+
+                                switch (change)
+                                {
+                                        case POSITION:
+                                                window().translateTo(original.start + timeDiff);
+                                                window().clampInside(limits);
+                                                break;
+                                        case START:
+                                                window().start = Math.min(original.start + timeDiff, original.end - minRange);
+                                                window().start = Math.max(window().start, limits.start);
+                                                break;
+                                        case END:
+                                                window().end = Math.max(original.end + timeDiff, original.start + minRange);
+                                                window().end = Math.min(window().end, limits.end);
+                                }
+                                getModel().setViewInterval(window());
+                                action.run();
+                                repaint();
+                        }
+                });
+        }
+
+        private Interval window()
+        {
+                return visuals.getViewInterval();
+        }
+
+        @Override
+        public Dimension getPreferredSize()
+        {
+                return new Dimension(600, 30);
+        }
+
+        public void setMinRange(long minRange)
+        {
+                this.minRange = minRange;
+        }
+
+        @Override
+        public void note(TFEvent e)
+        {
+                repaint();
+        }
+
+        void setTimeInterval(Interval interval)
+        {
+                window().setTo(interval);
+                repaint();
+        }
+
+        public void paintComponent(Graphics g1)
+        {
+                int w = getSize().width, h = getSize().height;
+                Graphics2D g = (Graphics2D) g1;
+
+                long start = System.currentTimeMillis();
+
+                // draw main backdrop.
+                g.setColor(Color.white);
+                g.fillRect(0, 0, w, h);
+
+                if (visuals.getModel() == null || visuals.getModel().getActs() == null)
+                {
+                        g.setColor(Color.darkGray);
+                        g.drawString("No data for timeline.", 5, 20);
+                        return;
+                }
+
+                scale = new TimeScale();
+                scale.setDateRange(visuals.getGlobalInterval());
+                scale.setNumberRange(ew, w - ew);
+
+
+                // draw the area for the central "thumb".
+                int lx = scale.toInt(window().start);
+                int rx = scale.toInt(window().end);
+                g.setColor(change == Modify.POSITION ? new Color(255, 255, 120) : new Color(255, 245, 200));
+                positionRect.setBounds(lx, 0, rx - lx, h);
+                g.fill(positionRect);
+
+                // Figure out how best to draw events.
+                // If there are too many, we just draw a kind of histogram of frequency,
+                // rather than using the timeline layout.
+                int slotW = 2 * eventRadius;
+                int slotNum = w / slotW + 1;
+                int[] slots = new int[slotNum];
+                int mostInSlot = 0;
+                for (VisualAct v : visuals.getVisualActs())
+                {
+                        if (!v.isVisible())
+                        {
+                                continue;
+                        }
+                        int x = scale.toInt(v.getStart().getTime());
+                        int s = x / slotW;
+                        if (s >= 0 && s < slotNum)
+                        {
+                                slots[s]++;
+                                mostInSlot = Math.max(mostInSlot, slots[s]);
+                        }
+                }
+                if (mostInSlot > 30)
+                {
+                        g.setColor(Color.gray);
+                        for (int i = 0; i < slots.length; i++)
+                        {
+                                int sh = (h * slots[i]) / mostInSlot;
+                                g.fillRect(slotW * i, h - sh, slotW, sh);
+                        }
+                } else
+                {
+                        // draw individual events.
+                        for (VisualAct v : visuals.getVisualActs())
+                        {
+                                if (!v.isVisible())
+                                {
+                                        continue;
+                                }
+                                g.setColor(v.getColor());
+                                int x = scale.toInt(v.getStart().getTime());
+
+                                int y = eventRadius + (int) (v.getY() * h) / (visuals.getBounds().height - 2 * eventRadius);
+                                g.fillRect(x - 1, y - eventRadius, 2 * eventRadius, 3);
+                                if (v.getEnd() != null)
+                                {
+                                        int endX = scale.toInt(v.getEnd().getTime());
+                                        g.drawLine(x, y, endX, y);
+                                }
+                        }
+                }
+
+                g.setColor(Color.gray);
+                g.drawLine(0, 0, w, 0);
+                g.drawLine(0, h - 1, w, h - 1);
+
+                // draw "expansion" areas on sides of thumb.
+                startRect.setBounds(positionRect.x - ew, 1, ew, h - 2);
+                g.setColor(change == Modify.START ? sideMouse : sidePlain);
+                g.fill(startRect);
+                endRect.setBounds(positionRect.x + positionRect.width, 1, ew, h - 2);
+                g.setColor(change == Modify.END ? sideMouse : sidePlain);
+                g.fill(endRect);
+        }
+}
diff --git a/timeflow/vis/timeline/TimelineTrack.java b/timeflow/vis/timeline/TimelineTrack.java
new file mode 100755
index 0000000..e86c2f4
--- /dev/null
+++ b/timeflow/vis/timeline/TimelineTrack.java
@@ -0,0 +1,129 @@
+package timeflow.vis.timeline;
+
+import timeflow.vis.VisualAct;
+import timeflow.data.time.*;
+import timeflow.util.*;
+
+import java.util.*;
+
+public class TimelineTrack implements Comparable
+{
+
+        String label;
+        List<VisualAct> visualActs = new ArrayList<VisualAct>();
+        int y0, y1;
+        DoubleBag<Long> histogram;
+
+        TimelineTrack(String label)
+        {
+                this.label = label;
+        }
+
+        void add(VisualAct v)
+        {
+                visualActs.add(v);
+        }
+
+        int size()
+        {
+                return visualActs.size();
+        }
+
+        // assumes a>b>0
+        int gcd(int a, int b)
+        {
+                int mod = a % b;
+                if (mod == 0)
+                {
+                        return b;
+                }
+                return gcd(b, mod);
+        }
+
+        int nearAndRelPrime(int target, int modulus)
+        {
+                if (target < 2)
+                {
+                        return 1;
+                }
+                while (gcd(modulus, target) > 1)
+                {
+                        target--;
+                }
+                return target;
+        }
+
+        // top and height are in proportion of total height of frame.
+        void layout(double top, double height, TimelineVisuals visuals)
+        {
+                int n = visualActs.size();
+                if (n == 0)
+                {
+                        return;
+                }
+                
+                int labelHeight = 0; // 80;
+                int fh = visuals.getBounds().height - labelHeight;
+                int fy = visuals.getBounds().y;
+                int cellH = visuals.getModel().getDisplay().getInt("timeline.item.height.min");
+
+                int fontHeight = 12;
+                
+                y0 = fy + (int) (fh * top);
+                y1 = fy + (int) (fh * (top + height));
+                //int mid = (y0 + y1) / 2;
+                int iy0 = y0; // Math.min(y0 + 5, mid);
+                int iy1 = y1; // Math.max(y1 - 15, mid);
+
+                int numCells = Math.max(1, (iy1 - iy0) / cellH);
+
+                VisualAct[] rights = new VisualAct[numCells];
+
+                int step = nearAndRelPrime((int) (.61803399 * numCells), numCells);
+                int i = 0;
+                VisualAct last = null;
+                for (VisualAct v : visualActs)
+                {
+                        if (!v.isVisible() || !v.getTrack().equals(this))
+                        {
+                                continue;
+                        }
+                        v.setSpaceToRight(1000);
+
+                        double num = visuals.getTimeScale().toNum(v.getStart().getTime());
+                        int x = (int) num;
+
+                        int cell = numCells < 2 ? 0 : (i % numCells);
+                        int y = iy0 + cell * cellH; // (iy1 - iy0 < 12 ? 0 : cell * cellH);
+                        v.setX(x);
+                        v.setY(y + fontHeight);
+
+                        if (v.getEnd() != null)
+                        {
+                                v.setEndX((int) visuals.getTimeScale().toNum(v.getEnd().getTime()));
+                        }
+
+                        if (rights[cell] != null)
+                        {
+                                int space = x - rights[cell].getX();
+                                rights[cell].setSpaceToRight(space);
+                        }
+                        rights[cell] = v;
+                        if ((last != null && v.getStart().getTime() == last.getStart().getTime())
+                                || visuals.getLayoutStyle() == TimelineVisuals.Layout.TIGHT)
+                        {
+                                i++;
+                        } else
+                        {
+                                i += step;
+                        }
+                        last = v;
+                }
+        }
+
+        @Override
+        public int compareTo(Object o)
+        {
+                return ((TimelineTrack) o).size() - size();
+        }
+}
diff --git a/timeflow/vis/timeline/TimelineVisuals.java b/timeflow/vis/timeline/TimelineVisuals.java
new file mode 100755
index 0000000..4e54d8a
--- /dev/null
+++ b/timeflow/vis/timeline/TimelineVisuals.java
@@ -0,0 +1,345 @@
+package timeflow.vis.timeline;
+
+import timeflow.data.db.*;
+import timeflow.data.db.filter.*;
+import timeflow.data.time.*;
+import timeflow.model.*;
+import timeflow.vis.*;
+
+import java.util.*;
+import java.awt.*;
+
+/*
+ * A VisualEncoding takes the info about which fields to translate to
+ * which visual aspects, and applies that to particular Acts.
+ */
+public class TimelineVisuals
+{
+
+        private Map<String, TimelineTrack> trackTable = new HashMap<String, TimelineTrack>();
+        ArrayList<TimelineTrack> trackList = new ArrayList<TimelineTrack>();
+        private TimeScale timeScale = new TimeScale();
+        private Rectangle bounds = new Rectangle();
+        private boolean frameChanged;
+        private int numShown = 0;
+        private Interval globalInterval;
+
+        public double scale = 1;
+        
+        public enum Layout
+        {
+
+                TIGHT, LOOSE, GRAPH
+        };
+        private Layout layoutStyle = Layout.TIGHT;
+        private VisualEncoder encoder;
+        private TFModel model;
+        private int fullHeight;
+
+        public int getFullHeight()
+        {
+                return fullHeight;
+        }
+
+        public TimelineVisuals(TFModel model)
+        {
+                this.model = model;
+                encoder = new VisualEncoder(model);
+        }
+
+        public TimeScale getTimeScale()
+        {
+                return timeScale;
+        }
+
+        public Rectangle getBounds()
+        {
+                return bounds;
+        }
+
+        public void setBounds(int x, int y, int w, int h)
+        {
+                bounds.setBounds(x, y, w, h);
+                timeScale.setLow(x);
+                timeScale.setHigh(x + w);
+                frameChanged = true;
+        }
+
+        public Layout getLayoutStyle()
+        {
+                return layoutStyle;
+        }
+
+        public void setLayoutStyle(Layout style)
+        {
+                layoutStyle = style;
+                layout();
+        }
+
+        public Interval getFitToVisibleRange()
+        {
+                ActList acts = model.getActs();
+
+                // add a little bit to the right so we can see labels...
+                ActDB db = getModel().getDB();
+                Field endField = db.getField(VirtualField.END);
+                Interval i = null;
+                if (endField == null)
+                {
+                        i = DBUtils.range(acts, VirtualField.START);
+                } else
+                {
+                        i = DBUtils.range(acts, new Field[]
+                                {
+                                        db.getField(VirtualField.START), endField
+                                });
+                }
+                if (i.length() == 0)
+                {
+                        i.expand(globalInterval.length() / 20);
+                }
+                i = i.subinterval(-.05, 1.1);
+                i.intersection(globalInterval);
+                return i;
+        }
+
+        public void fitToVisible()
+        {
+                Interval i = getFitToVisibleRange();
+                setTimeBounds(i.start, i.end);
+        }
+
+        public void zoomOut()
+        {
+                setTimeBounds(globalInterval.start, globalInterval.end);
+        }
+
+        public void setTimeBounds(long first, long last)
+        {
+                timeScale.setDateRange(first, last);
+                frameChanged = true;
+                model.setViewInterval(new Interval(first, last));
+        }
+
+        public Interval getGlobalInterval()
+        {
+                if (globalInterval == null && model != null && model.getDB() != null)
+                {
+                        createGlobalInterval();
+                }
+                return globalInterval;
+        }
+
+        public void createGlobalInterval()
+        {
+                globalInterval = DBUtils.range(model.getDB().all(), VirtualField.START).subinterval(-.05, 1.1);
+        }
+
+        public Interval getViewInterval()
+        {
+                return timeScale.getInterval();
+        }
+
+        public java.util.List<VisualAct> getVisualActs()
+        {
+                return encoder.getVisualActs();
+        }
+
+        public void layoutIfChanged()
+        {
+                if (frameChanged)
+                {
+                        layout();
+                }
+        }
+
+        public void init(boolean majorChange)
+        {
+                note(new TFEvent(majorChange ? TFEvent.Type.DATABASE_CHANGE : TFEvent.Type.ACT_CHANGE, null));
+        }
+
+        public void note(TFEvent e)
+        {
+                ActList all = null;
+                if (e.type == TFEvent.Type.DATABASE_CHANGE)
+                {
+                        all = model.getDB().all();
+                        createGlobalInterval();
+                        Interval i = guessInitialViewInterval(all, globalInterval);
+                        setTimeBounds(i.start, i.end);
+                }
+                if (e.affectsRowSet())
+                {
+                        all = model.getDB().all();
+                        encoder.createVisualActs();
+                        createGlobalInterval();
+                } else
+                {
+                        encoder.createVisualActs();
+                }
+                Interval v = model.getViewInterval();
+                if (v != null && v.start != timeScale.getInterval().start)
+                {
+                        timeScale.getInterval().translateTo(v.start);
+                }
+                updateVisuals();
+        }
+
+        private Interval guessInitialViewInterval(ActList acts, Interval fullRange)
+        {
+                if (acts.size() < 50)
+                {
+                        return fullRange.copy();
+                }
+
+                Interval best = null;
+                int most = -1;
+                double d = Math.max(.1, 50. / acts.size());
+                d = Math.min(1. / 3, d);
+                for (double x = 0; x < 1 - d; x += d / 4)
+                {
+                        Interval i = fullRange.subinterval(x, x + d);
+                        TimeIntervalFilter f = new TimeIntervalFilter(i, getModel().getDB().getField(VirtualField.START));
+                        int num = 0;
+                        for (Act a : acts)
+                        {
+                                if (f.accept(a))
+                                {
+                                        num++;
+                                }
+                        }
+                        if (num > most)
+                        {
+                                most = num;
+                                best = i;
+                        }
+                }
+                return best;
+        }
+
+        public void updateVisuals()
+        {
+                scale = 1;
+                updateVisualEncoding();
+                layout();
+        }
+
+        public TFModel getModel()
+        {
+                return model;
+        }
+
+        public int getNumTracks()
+        {
+                return trackList.size();
+        }
+
+        public double layout()
+        {
+                ActList acts = model.getActs();
+                if (acts == null)
+                {
+                        return scale;
+                }
+
+                double labelHeight = 30;
+                
+                double min = bounds.height == 0 ? 0 : labelHeight / bounds.height;
+                double cellH = (double)getModel().getDisplay().getInt("timeline.item.height.min") / bounds.height;
+                
+                double maxCount = 0;
+                
+                // Set the minimum scale
+                for (TimelineTrack t : trackList)
+                {
+                        //double height = t.size() * scale / (double) numShown;
+
+                        maxCount = Math.max(t.size(), maxCount);
+                }
+
+                scale = Math.min(cellH * numShown, scale);
+                scale = Math.max(min * numShown / maxCount, scale);
+                
+                double top = 0;
+                for (TimelineTrack t : trackList)
+                {
+                        double height = Math.max(min, t.size() * scale / (double) numShown);
+                        t.layout(top, height, this);
+                        top += height;
+                }
+                fullHeight = (int) (top * bounds.height);
+
+                Collections.sort(trackList);
+                frameChanged = false;
+                
+                return scale;
+        }
+
+        private void updateVisualEncoding()
+        {
+                java.util.List<VisualAct> acts = encoder.apply();
+
+                // now arrange on tracks
+                trackTable = new HashMap<String, TimelineTrack>();
+                trackList = new ArrayList<TimelineTrack>();
+                numShown = 0;
+                for (VisualAct v : acts)
+                {
+                        if (!v.isVisible())
+                        {
+                                continue;
+                        }
+                        numShown++;
+                        String s = v.getTrackString();
+                        TimelineTrack t = trackTable.get(s);
+                        if (t == null)
+                        {
+                                t = new TimelineTrack(s);
+                                trackTable.put(s, t);
+                                trackList.add(t);
+                        }
+                        t.add(v);
+                        v.setTrack(t);
+                }
+
+                /*
+                // the following code is no longer used, but could come in handy again one day...
+                
+                // If there is more than one "small" track, then we will coalesce them into
+                // one bigger "miscellaneous" track.
+                int minSize=numShown/30;//Math.max(3,numShown/30);
+                ArrayList<TimelineTrack> small=new ArrayList<TimelineTrack>();
+                for (TimelineTrack t: trackList)
+                {
+                if (t.size()<minSize)
+                small.add(t);
+                }
+                if (small.size()>1)
+                {
+                // create a new Track for "miscellaneous."
+                TimelineTrack misc=new TimelineTrack(Display.MISC_CODE);
+                trackList.add(misc);
+                trackTable.put(misc.label, misc);
+                
+                // remove the old tracks.
+                for (TimelineTrack t:small)
+                {
+                trackList.remove(t);
+                trackTable.remove(t.label);
+                for (VisualAct v: t.visualActs)
+                {
+                v.setTrack(misc);
+                misc.add(v);
+                }
+                }
+                // sort miscellaneous items in time order.
+                //Collections.sort(misc.visualActs);
+                }
+                 */
+
+                for (TimelineTrack t : trackList)
+                {
+                        Collections.sort(t.visualActs);
+                }
+        }
+}

--
Gitblit v1.6.2