edu.purdue.cc.bionet.ui.DistributionAnalysisDisplayPanel.java Source code

Java tutorial

Introduction

Here is the source code for edu.purdue.cc.bionet.ui.DistributionAnalysisDisplayPanel.java

Source

/*
    
This file is part of BioNet.
    
BioNet is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
BioNet is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with BioNet.  If not, see <http://www.gnu.org/licenses/>.
    
*/

package edu.purdue.cc.bionet.ui;

import edu.purdue.bbc.util.CurveFitting;
import edu.purdue.bbc.util.Language;
import edu.purdue.bbc.util.NumberList;
import edu.purdue.bbc.util.Range;
import edu.purdue.bbc.util.Settings;
import edu.purdue.bbc.util.SparseMatrix;
import edu.purdue.bbc.util.Statistics;
import edu.purdue.bbc.util.equation.Equation;
import edu.purdue.bbc.util.equation.Polynomial;
import edu.purdue.cc.bionet.io.SaveImageAction;
import edu.purdue.cc.bionet.util.Experiment;
import edu.purdue.cc.bionet.util.Molecule;
import edu.purdue.cc.bionet.util.Sample;
import edu.purdue.cc.bionet.util.SampleGroup;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Paint;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.swing.ButtonGroup;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTree;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import java.awt.event.KeyEvent;
import java.awt.Component;
import java.awt.Frame;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.LegendItem;
import org.jfree.chart.LegendItemCollection;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.axis.TickUnits;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.labels.XYItemLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.CrosshairState;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.category.BoxAndWhiskerRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYItemRendererState;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.statistics.BoxAndWhiskerCalculator;
import org.jfree.data.statistics.BoxAndWhiskerItem;
import org.jfree.data.statistics.DefaultBoxAndWhiskerCategoryDataset;
import org.jfree.data.statistics.DefaultBoxAndWhiskerXYDataset;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

import it.cnr.imaa.essi.lablib.gui.checkboxtree.CheckboxTree;
import it.cnr.imaa.essi.lablib.gui.checkboxtree.TreeCheckingEvent;
import it.cnr.imaa.essi.lablib.gui.checkboxtree.TreeCheckingListener;
import it.cnr.imaa.essi.lablib.gui.checkboxtree.TreeCheckingModel;

import org.apache.log4j.Logger;

/**
 * A class for comparative analysis view of multiple experiments.
 */
public class DistributionAnalysisDisplayPanel extends AbstractDisplayPanel
        implements ComponentListener, ActionListener {

    private JMenuBar menuBar;
    private JMenu curveFittingMenu;
    private JMenu groupsMenu;
    private JMenu viewMenu;
    private JMenuItem chooseSampleGroupsMenuItem;
    private JMenuItem removeSampleGroupsMenuItem;
    private JRadioButtonMenuItem noFitButton;
    private JRadioButtonMenuItem robustFitButton;
    private JRadioButtonMenuItem chiSquareFitButton;
    private JMenuItem hideOutliersViewMenuItem;

    private Collection<Experiment> experiments;
    private Set<Molecule> molecules;
    private Set<Sample> samples;
    private JSplitPane mainSplitPane;
    private JSplitPane graphSplitPane;
    private ExperimentSelectorTreePanel selectorTree;
    private JPanel topPanel;
    private JPanel bottomPanel;
    private JPanel experimentGraphPanel;
    private JPanel fitSelectorPanel;
    private ButtonGroup fitButtonGroup;
    private boolean graphSplitPaneDividerLocationSet = false;

    private static final int MOLECULE = 1;
    private static final int EXPERIMENT = 2;
    private static final int SAMPLE = 3;

    /**
     * Creates a new DistributionAnalysisDisplayPanel.
     */
    public DistributionAnalysisDisplayPanel() {
        super(new BorderLayout());
        Language language = Settings.getLanguage();
        this.menuBar = new JMenuBar();
        this.curveFittingMenu = new JMenu(language.get("Curve Fitting"));
        this.curveFittingMenu.setMnemonic(KeyEvent.VK_C);
        this.groupsMenu = new JMenu(language.get("Groups"));
        this.groupsMenu.setMnemonic(KeyEvent.VK_G);
        this.viewMenu = new JMenu(language.get("View"));
        this.viewMenu.setMnemonic(KeyEvent.VK_V);
        this.removeSampleGroupsMenuItem = new JMenuItem(language.get("Reset Sample Groups"), KeyEvent.VK_R);
        this.chooseSampleGroupsMenuItem = new JMenuItem(language.get("Choose Sample Groups"), KeyEvent.VK_C);
        this.fitSelectorPanel = new JPanel(new GridLayout(4, 1));
        this.noFitButton = new JRadioButtonMenuItem(language.get("No Fit"));
        this.robustFitButton = new JRadioButtonMenuItem(language.get("Robust Linear Fit"));
        this.chiSquareFitButton = new JRadioButtonMenuItem(language.get("Chi Square Fit"));
        this.fitButtonGroup = new ButtonGroup();
        this.fitButtonGroup.add(this.noFitButton);
        this.fitButtonGroup.add(this.robustFitButton);
        this.fitButtonGroup.add(this.chiSquareFitButton);
        this.noFitButton.setSelected(true);
        this.hideOutliersViewMenuItem = new JMenuItem(language.get("Hide Current Outliers"));
        this.curveFittingMenu.add(this.noFitButton);
        this.curveFittingMenu.add(this.robustFitButton);
        this.curveFittingMenu.add(this.chiSquareFitButton);
        this.groupsMenu.add(this.removeSampleGroupsMenuItem);
        this.groupsMenu.add(this.chooseSampleGroupsMenuItem);
        this.viewMenu.add(this.hideOutliersViewMenuItem);
        this.chooseSampleGroupsMenuItem.addActionListener(this);
        this.removeSampleGroupsMenuItem.addActionListener(this);
        this.hideOutliersViewMenuItem.addActionListener(this);
        this.add(menuBar, BorderLayout.NORTH);
        this.menuBar.add(this.curveFittingMenu);
        this.menuBar.add(this.groupsMenu);
        this.menuBar.add(this.viewMenu);
    }

    /**
     * Creates a new view containing the specified experiments.
     * 
     * @param experiments The experiments to display in this view.
     * @return A boolean indicating whether creating the view was successful.
     */
    public boolean createView(Collection<Experiment> experiments) {
        this.experiments = experiments;
        this.molecules = new TreeSet<Molecule>();
        this.samples = new TreeSet<Sample>();
        for (Experiment e : experiments) {
            this.molecules.addAll(e.getMolecules());
            this.samples.addAll(e.getSamples());
        }
        SampleGroup sampleGroup = new SampleGroup(Settings.getLanguage().get("All Samples"), this.samples);
        ArrayList<SampleGroup> sampleGroups = new ArrayList<SampleGroup>();
        sampleGroups.add(sampleGroup);
        Language language = Settings.getLanguage();

        this.topPanel = new JPanel(new BorderLayout());
        this.bottomPanel = new JPanel(new GridLayout(1, 1));
        this.experimentGraphPanel = new JPanel(new GridLayout(1, 1));

        this.selectorTree = new ExperimentSelectorTreePanel(experiments);
        this.mainSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
        this.graphSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);

        JPanel topRightPanel = new JPanel(new BorderLayout());
        topRightPanel.add(this.fitSelectorPanel, BorderLayout.NORTH);
        topRightPanel.add(new JPanel(), BorderLayout.CENTER);
        this.topPanel.add(topRightPanel, BorderLayout.EAST);
        this.topPanel.add(this.experimentGraphPanel, BorderLayout.CENTER);

        this.add(mainSplitPane, BorderLayout.CENTER);
        this.mainSplitPane.setLeftComponent(this.selectorTree);
        this.mainSplitPane.setDividerLocation(200);
        this.mainSplitPane.setRightComponent(this.graphSplitPane);
        this.graphSplitPane.setTopComponent(this.topPanel);
        this.graphSplitPane.setBottomComponent(this.bottomPanel);
        this.setSampleGroups(sampleGroups);

        this.addComponentListener(this);

        return true;
    }

    /**
     * Adds an Experiment to this panel.
     * 
     * @param experiment The experiment to be added.
     * @return A boolean indicating whether the operation was successful.
     */
    public boolean addExperiment(Experiment experiment) {
        this.molecules.addAll(experiment.getMolecules());
        return experiments.add(experiment);
    }

    /**
     * Removes an experiment from the panel.
     * 
     * @param experiment The expeiment to be removed.
     * @return A boolean indicating whether or not the operation was successful.
     */
    public boolean removeExperiment(Experiment experiment) {
        return experiments.remove(experiment);
    }

    /**
     * Gets the title for this panel.
     * 
     * @return The title for this panel.
     */
    public String getTitle() {
        return Settings.getLanguage().get("Distribution Analysis");
    }

    /**
     * Sets the SampleGroups for this panel.
     * 
     * @param sampleGroups The new set of groups
     */
    public void setSampleGroups(Collection<SampleGroup> sampleGroups) {

        super.setSampleGroups(sampleGroups);

        // clear the listeners
        CheckboxTree tree = this.selectorTree.getTree();
        for (TreeSelectionListener t : tree.getTreeSelectionListeners()) {
            tree.removeTreeSelectionListener(t);
            // CheckboxTree doesn't have a method for getting TreeCheckingListeners,
            // so we'll try this and catch any exceptions.
            try {
                tree.removeTreeCheckingListener((TreeCheckingListener) t);
            } catch (ClassCastException e) {
            }
        }
        for (ItemListener i : this.noFitButton.getItemListeners()) {
            this.noFitButton.removeItemListener(i);
        }
        for (ItemListener i : this.robustFitButton.getItemListeners()) {
            this.robustFitButton.removeItemListener(i);
        }
        for (ItemListener i : this.chiSquareFitButton.getItemListeners()) {
            this.chiSquareFitButton.removeItemListener(i);
        }

        this.experimentGraphPanel.removeAll();
        this.bottomPanel.removeAll();
        int cols = (int) Math.ceil(Math.sqrt(sampleGroups.size()));
        int rows = (int) Math.ceil(sampleGroups.size() / cols);
        GridLayout layout = (GridLayout) this.experimentGraphPanel.getLayout();
        layout.setRows(rows);
        layout.setColumns(cols);
        layout = (GridLayout) this.bottomPanel.getLayout();
        layout.setRows(rows);
        layout.setColumns(cols);

        TreePath path = selectorTree.getTree().getSelectionPath();
        int level = path.getPathCount();
        DefaultMutableTreeNode selectedNode = null;
        if (level > MOLECULE) {
            selectedNode = (DefaultMutableTreeNode) path.getPathComponent(MOLECULE);
        }

        for (SampleGroup sampleGroup : sampleGroups) {
            SampleGraph sampleGraph = new SampleGraph(experiments, sampleGroup);
            ExperimentGraph experimentGraph = new ExperimentGraph(experiments, sampleGroup);
            this.bottomPanel.add(sampleGraph);
            this.selectorTree.getTree().addTreeSelectionListener(sampleGraph);
            this.experimentGraphPanel.add(experimentGraph, BorderLayout.CENTER);
            // add the listeners
            this.selectorTree.getTree().addTreeCheckingListener(sampleGraph);
            this.selectorTree.getTree().addTreeSelectionListener(experimentGraph);
            this.selectorTree.getTree().addTreeCheckingListener(experimentGraph);
            this.noFitButton.addItemListener(experimentGraph);
            this.robustFitButton.addItemListener(experimentGraph);
            this.chiSquareFitButton.addItemListener(experimentGraph);
            if (selectedNode != null) {
                sampleGraph.setGraph(selectedNode);
                experimentGraph.setGraph(selectedNode);
            }
        }
        this.removeSampleGroupsMenuItem.setEnabled(sampleGroups != null && sampleGroups.size() > 1);
        this.experimentGraphPanel.validate();
        this.bottomPanel.validate();
    }

    public void deselectOutliers() {
        Iterator<TreeNode> moleculeIterator = this.selectorTree
                .checkedDescendantIterator(this.selectorTree.getRoot(), MOLECULE);

        while (moleculeIterator.hasNext()) {
            DefaultMutableTreeNode moleculeNode = (DefaultMutableTreeNode) moleculeIterator.next();
            Molecule molecule = (Molecule) moleculeNode.getUserObject();

            Iterator<TreeNode> experimentIterator = this.selectorTree.checkedDescendantIterator(moleculeNode,
                    EXPERIMENT);
            while (experimentIterator.hasNext()) {
                DefaultMutableTreeNode experimentNode = (DefaultMutableTreeNode) experimentIterator.next();

                Iterator<TreeNode> sampleIterator = this.selectorTree.checkedDescendantIterator(experimentNode,
                        SAMPLE);
                TreeSet<Sample> sampleSet = new TreeSet<Sample>();
                while (sampleIterator.hasNext()) {
                    DefaultMutableTreeNode sampleNode = (DefaultMutableTreeNode) sampleIterator.next();
                    sampleSet.add((Sample) sampleNode.getUserObject());
                }

                for (SampleGroup group : this.getSampleGroups()) {
                    Collection<Sample> sampleUnion = new TreeSet<Sample>(group);
                    sampleUnion.retainAll(sampleSet);
                    Range range = Statistics.regularRange(molecule.getValues(sampleUnion).toDoubleArray());
                    sampleIterator = this.selectorTree.checkedDescendantIterator(experimentNode, SAMPLE);

                    while (sampleIterator.hasNext()) {
                        DefaultMutableTreeNode sampleNode = (DefaultMutableTreeNode) sampleIterator.next();
                        Sample sample = (Sample) sampleNode.getUserObject();
                        if (sampleUnion.contains(sample)
                                && !range.contains(molecule.getValue(sample).doubleValue()))
                            this.selectorTree.uncheck(sampleNode);
                    }
                }
            }
        }
    }

    public void componentHidden(ComponentEvent e) {
    }

    public void componentMoved(ComponentEvent e) {
    }

    public void componentResized(ComponentEvent e) {
    }

    public void componentShown(ComponentEvent e) {
        if (!this.graphSplitPaneDividerLocationSet) {
            this.graphSplitPaneDividerLocationSet = true;
            this.graphSplitPane.setDividerLocation(0.5);
        }
    }

    /**
     * The actionPerformed method of the ActionListener interface.
     * 
     * @see ActionListener#actionPerformed( ActionEvent )
     * @param e The event which triggered this action.
     */
    public void actionPerformed(ActionEvent e) {
        Logger logger = Logger.getLogger(getClass());
        Language language = Settings.getLanguage();
        Object source = e.getSource();
        if (source == this.chooseSampleGroupsMenuItem) {
            // Choose sample groups.
            Component frame = this;
            while (!(frame instanceof Frame) && frame != null) {
                frame = frame.getParent();
            }
            Collection<SampleGroup> groups = SampleGroupDialog.showInputDialog((Frame) frame,
                    Settings.getLanguage().get("Choose groups"), this.samples);
            if (groups != null) {

                if (this.getSampleGroups() != null) {
                    for (SampleGroup group : this.getSampleGroups()) {
                        logger.debug(group.toString());
                        for (Sample sample : group) {
                            logger.debug("\t" + sample.toString());
                        }
                    }
                }
                this.setSampleGroups(groups);
            }
        } else if (source == this.removeSampleGroupsMenuItem) {
            Collection<SampleGroup> groups = new ArrayList<SampleGroup>();
            groups.add(new SampleGroup("", this.samples));
            this.setSampleGroups(groups);
        } else if (source == this.hideOutliersViewMenuItem) {
            this.deselectOutliers();
        }
    }

    // =========================== PRIVATE CLASSES ===============================
    // ====================== ExperimentSelectorTreePanel ========================
    /**
     * A class for displaying the selector tree
     */
    private class ExperimentSelectorTreePanel extends CheckboxTreePanel {
        public static final int MOLECULE = 1;
        public static final int EXPERIMENT = 2;
        public static final int SAMPLE = 3;

        /**
         * Creates a new ExperimentSelectorTreePanel based on the passed in 
         * Experiments.
         * 
         * @param experiments The Experiment data to be shown in the tree.
         */
        public ExperimentSelectorTreePanel(Collection<Experiment> experiments) {
            super(new DefaultMutableTreeNode(Settings.getLanguage().get("All Molecules")));
            TreeNode selectedNode = null;

            // first get all possible group names
            TreeSet<Molecule> molecules = new TreeSet<Molecule>();
            for (Experiment e : experiments) {
                molecules.addAll(e.getMolecules());
            }
            for (Molecule molecule : molecules) {
                DefaultMutableTreeNode moleculeNode = new DefaultMutableTreeNode(molecule);
                if (selectedNode == null)
                    selectedNode = moleculeNode;

                for (Experiment e : experiments) {
                    DefaultMutableTreeNode experimentNode = new DefaultMutableTreeNode(e);
                    for (Sample sample : e.getSamples()) {
                        DefaultMutableTreeNode sampleNode = new DefaultMutableTreeNode(sample);
                        experimentNode.add(sampleNode);
                    }
                    moleculeNode.add(experimentNode);
                }
                this.getRoot().add(moleculeNode);
            }
            this.check(this.getRoot());
            this.reload();
            this.tree.setSelectionRow(1);
        }

        /**
         * Returns a mapping of Samples that are checked along with their values
         * 
         * @param node A tree node which contains the sample nodes to be retrieved.
         * @return A Map containing the requested values.
         */
        public Collection<Sample> getSamplesFiltered(DefaultMutableTreeNode node) {
            Collection<Sample> returnValue = new ArrayList<Sample>();
            Iterator<TreeNode> nodeIter = this.checkedDescendantIterator(node, SAMPLE);
            while (nodeIter.hasNext()) {
                returnValue.add((Sample) ((DefaultMutableTreeNode) nodeIter.next()).getUserObject());
            }
            return returnValue;
        }
    }

    // =========================== ExperimentGraph ===============================
    /**
     * A class for displaying molecular data across experiments
     */
    private class ExperimentGraph extends JPanel
            implements TreeSelectionListener, TreeCheckingListener, ItemListener {
        private JFreeChart chart;
        private SortedSet<Experiment> experiments;
        private SampleGroup sampleGroup;
        private XYDataset fitDataset;
        private Equation fitEquation;
        private JLabel equationLabel;
        private LegendItemCollection legendItems;
        //      private LegendItemCollection singleExperimentLegendItems;
        private Stroke stroke;

        /**
         * Creates a new ExperimentGraph panel to show data from the passed in 
         * Experiments.
         * 
         * @param experiments A collection of experiments to show data from.
         */
        public ExperimentGraph(Collection<Experiment> experiments, SampleGroup sampleGroup) {
            super();
            this.equationLabel = new JLabel();
            this.experiments = new TreeSet(experiments);
            this.sampleGroup = sampleGroup;
            //this.fitDataset = new XYDataset( );
            this.stroke = new BasicStroke(2);
            //         this.singleExperimentLegendItems = new LegendItemCollection( );
            this.legendItems = new LegendItemCollection();
            //         LegendItem meanLegendItem = 
            //            new LegendItem( "Mean", null, null, null,
            //                             new Ellipse2D.Double( 0.0, 0.0, 4.0, 4.0 ),
            //                             Color.BLACK, 
            //                             this.stroke, Color.BLACK);
            //         LegendItem medianLegendItem = 
            //            new LegendItem( "Median", null, null, null,
            //                            new Line2D.Double( 0.0, 0.0, 9.0, 0.0 ),
            //                            this.stroke, Color.BLACK );
            //         LegendItem minMaxLegendItem1 = 
            //            new LegendItem( "Min/Max without Outliers", null, null, null,
            //                            new Line2D.Double( 0.0, 0.0, 9.0, 0.0 ),
            //                            this.stroke, Color.RED );
            //         LegendItem minMaxLegendItem2 = 
            //            new LegendItem( "Min/Max without Outliers", null, null, null,
            //                            new Line2D.Double( 0.0, 0.0, 9.0, 0.0 ),
            //                            this.stroke, Color.BLUE );
            //         LegendItem outlierLegendItem = 
            //            new LegendItem( "Outlier", null, null, null,
            //                            new Ellipse2D.Double( 0.0, 0.0, 4.0, 4.0 ),
            //                            Color.WHITE, 
            //                            this.stroke, Color.BLACK);
            //         this.singleExperimentLegendItems.add( meanLegendItem );
            //         this.singleExperimentLegendItems.add( medianLegendItem );
            //         this.singleExperimentLegendItems.add( minMaxLegendItem1 );
            //         this.singleExperimentLegendItems.add( outlierLegendItem );
            //         this.legendItems.add( meanLegendItem );
            //         this.legendItems.add( medianLegendItem );
            //         this.legendItems.add( minMaxLegendItem2 );
            //         this.legendItems.add( outlierLegendItem );
            this.setLayout(null);
            this.add(this.equationLabel);
            // add a context menu for saving the graph to an image
            new ContextMenu(this).add(new SaveImageAction(this));
        }

        /**
         * Sets the graph to display data from each experiment on the passed in 
         * molecule.
         *
         * This could be a String with the molecule ID, or an instance of the 
         * Molecule.
         * 
         * @param node The TreeNode containing the molecule object to be graphed.
         */
        public boolean setGraph(DefaultMutableTreeNode node) {
            Language language = Settings.getLanguage();
            Logger logger = Logger.getLogger(getClass());
            Molecule molecule = (Molecule) node.getUserObject();
            DefaultBoxAndWhiskerXYDataset boxDataSet = new DefaultBoxAndWhiskerXYDataset(new Integer(1));
            NumberList fitValues = new NumberList();
            int expCount = 0;
            int minTime = Integer.MAX_VALUE;
            NumberList timeValues = new NumberList();
            timeValues.add(Double.NaN);
            for (int i = 0; i < node.getChildCount(); i++) {
                DefaultMutableTreeNode expNode = (DefaultMutableTreeNode) node.getChildAt(i);
                Experiment e = (Experiment) expNode.getUserObject();
                Collection<Sample> samples = selectorTree.getSamplesFiltered(expNode);
                samples.retainAll(this.sampleGroup);
                long lastTime = Long.MIN_VALUE;
                for (Sample sample : samples) {
                    long thisTime = Integer.parseInt(sample.getAttribute("time"));
                    if (lastTime != thisTime) {
                        timeValues.add(thisTime);
                        lastTime = thisTime;
                        expCount++;
                    }
                    while (fitValues.size() <= expCount) {
                        fitValues.add(Double.NaN);
                    }
                    if (selectorTree.isChecked(node)) {
                        try {
                            NumberList values = molecule.getValues(samples);
                            BoxAndWhiskerItem item = calculateBoxAndWhiskerStatistics(values);
                            boxDataSet.add(new Date((long) expCount), item);

                            logger.debug("BoxAndWhisker Values: " + values);
                            logger.debug("Box plot data: " + item);
                            logger.debug("Min: " + Statistics.min(values.toDoubleArray()) + " Max: "
                                    + Statistics.max(values.toDoubleArray()));
                            logger.debug("Min regular: " + item.getMinRegularValue() + " Max regular: "
                                    + item.getMaxRegularValue());
                            logger.debug("Outliers: " + item.getOutliers());

                            // robust fit uses the median instead of the mean.
                            if (robustFitButton.isSelected()) {
                                fitValues.set(expCount, item.getMedian());
                            } else {
                                fitValues.set(expCount, item.getMean());
                            }
                        } catch (IllegalArgumentException exc) {
                            // ignore this error.
                        }
                    }
                }
            }

            TickUnits tickUnits = new TickUnits();
            for (int i = 1; i < timeValues.size(); i++) {
                tickUnits.add(new CustomTickUnit((double) (i), timeValues));
            }

            if (expCount < 1) { // nothing to graph.
                this.chart = null;
                return false;
            }
            // find the equation for the fitting curve
            this.fitEquation = null;
            if (robustFitButton.isSelected()) {
                this.fitEquation = CurveFitting.linearFit(fitValues.toDoubleArray());
            }
            if (chiSquareFitButton.isSelected()) {
                this.fitEquation = CurveFitting.chiSquareFit(fitValues.toDoubleArray());
            }
            XYSeries fitSeries = new XYSeries(language.get("Fit Line"));
            if (fitEquation != null) {
                this.equationLabel.setText(("<html>y = " + fitEquation.toString("<sup>%s</sup>") + "</html>")
                        .replace("e<sup>", "<i>e</i><sup>"));
                for (double i = 1; i <= expCount; i += 0.01) {
                    try {
                        fitSeries.add(i, fitEquation.solve(i));
                    } catch (IllegalArgumentException exc) {
                        Logger.getLogger(getClass()).debug(
                                "Unable to find solution for " + fitEquation.toString() + " where x=" + i, exc);
                    }
                }
            } else {
                this.equationLabel.setText("");
            }
            this.chart = ChartFactory.createBoxAndWhiskerChart(
                    String.format(language.get("%s across time") + " - %s", molecule.toString(),
                            this.sampleGroup.toString()), //title
                    language.get("Time"), // x axis label
                    language.get("Response"), // y axis label
                    boxDataSet, // plot data
                    true // show legend
            );
            this.chart.getTitle().setFont(new Font("Arial", Font.BOLD, 18));
            XYSeriesCollection fitDataset = new XYSeriesCollection();
            fitDataset.addSeries(fitSeries);
            XYPlot plot = this.chart.getXYPlot();
            plot.setBackgroundPaint(Color.WHITE);
            plot.setRangeGridlinePaint(Color.GRAY);
            plot.setDomainGridlinePaint(Color.GRAY);
            ValueAxis domainAxis = new NumberAxis();
            plot.setDomainAxis(domainAxis);
            Range range = molecule.getValues(selectorTree.getSamplesFiltered(node)).getRange().scale(1.1);
            plot.getRangeAxis().setRange(range.getMin(), range.getMax());
            domainAxis.setStandardTickUnits(tickUnits);
            domainAxis.setVerticalTickLabels(true);
            domainAxis.setRange(0.5, timeValues.size() - 0.5);
            XYItemRenderer boxRenderer = plot.getRenderer();
            boxRenderer.setSeriesStroke(0, this.stroke);
            boxRenderer.setSeriesOutlineStroke(0, this.stroke);

            plot.setDataset(1, fitDataset);
            XYLineAndShapeRenderer lineRenderer = new XYLineAndShapeRenderer();
            lineRenderer.setSeriesShapesVisible(0, false);
            lineRenderer.setSeriesStroke(0, this.stroke);
            plot.setRenderer(1, lineRenderer);

            //         if ( expCount > 1 ) {
            plot.setFixedLegendItems(this.legendItems);
            //         } else {
            //            plot.setFixedLegendItems( this.singleExperimentLegendItems );
            //         }
            return true;
        }

        private BoxAndWhiskerItem calculateBoxAndWhiskerStatistics(NumberList values) {
            double[] v = values.toDoubleArray();
            double[] outliers = Statistics.outliers(v);
            Range regularRange = Statistics.regularRange(v);
            Range quartileRange = Statistics.quartileRange(v);
            return new BoxAndWhiskerItem(Statistics.mean(v), Statistics.median(v), quartileRange.getMin(),
                    quartileRange.getMax(), regularRange.getMin(), regularRange.getMax(), Statistics.min(v),
                    Statistics.max(v), new NumberList(outliers));
        }

        /**
         * Draws the graph.
         * 
         * @param g The Graphics object of this component.
         */
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (chart != null) {
                Dimension size = this.getSize(null);
                BufferedImage drawing = this.chart.createBufferedImage(size.width, size.height);
                g.drawImage(drawing, 0, 0, Color.WHITE, this);
                FontMetrics f = g.getFontMetrics();
                this.equationLabel.setBounds(size.width - 300, 0, 300, 100);
                this.equationLabel.repaint();
            }
        }

        /**
         * The valueChanged method of the TreeSelectionListener interface
         * 
         * @param e The event which triggered this action.
         */
        public void valueChanged(TreeSelectionEvent e) {
            TreePath path = e.getPath();
            int level = path.getPathCount();
            if (level > MOLECULE) {
                this.setGraph((DefaultMutableTreeNode) path.getPathComponent(MOLECULE));
            } else {
                this.chart = null;
            }
            this.repaint();
        }

        /**
         * The valueChanged method of the TreeCheckingListener interface.
         * 
         * @param e The event which triggered this action.
         */
        public void valueChanged(TreeCheckingEvent e) {
            if (selectorTree.getTree().isSelectionEmpty()) {
                selectorTree.getTree().setSelectionRow(0);
            }
            TreePath path = selectorTree.getTree().getSelectionPath();
            int level = path.getPathCount();
            if (level > MOLECULE) {
                this.setGraph((DefaultMutableTreeNode) path.getPathComponent(MOLECULE));
            } else {
                this.chart = null;
            }
            this.repaint();
        }

        /**
         * The itemStateChanged method of the ItemListener interface
         * 
         * @param e The event which triggered this action.
         */
        public void itemStateChanged(ItemEvent e) {
            if (e.getStateChange() == ItemEvent.SELECTED) {
                TreePath path = selectorTree.getTree().getSelectionPath();
                if (path != null) {
                    int level = path.getPathCount();
                    if (level > MOLECULE) {
                        this.setGraph((DefaultMutableTreeNode) path.getPathComponent(MOLECULE));
                    } else {
                        this.chart = null;
                    }
                    this.repaint();
                }
            }
        }
    }

    // ============================ SampleGraph ==================================
    /**
     * A class for showing sample concentrations in a graph.
     */
    private class SampleGraph extends JPanel implements TreeSelectionListener, TreeCheckingListener {

        private SortedSet<Experiment> experiments;
        private SampleGroup sampleGroup;
        private JFreeChart chart;
        private Object selectedObject;

        public SampleGraph(Collection<Experiment> experiments, SampleGroup sampleGroup) {
            super();
            this.experiments = new TreeSet<Experiment>(experiments);
            this.sampleGroup = sampleGroup;
            // add a context menu for saving the graph to an image
            new ContextMenu(this).add(new SaveImageAction(this));
        }

        /**
         * Sets the graph to display data from the given object in the Node
         * 
         * @param node The node to show information about.
         */
        public boolean setGraph(DefaultMutableTreeNode node) {
            Language language = Settings.getLanguage();
            XYSeriesCollection dataset = new XYSeriesCollection();
            //         LegendItemCollection legendItems = new LegendItemCollection( );
            CustomXYLineAndShapeRenderer renderer = new CustomXYLineAndShapeRenderer(false, true);
            int nodeLevel = node.getLevel();
            Logger.getLogger(getClass()).debug("Selected Node level: " + nodeLevel);
            Collection<Sample> samples = null;
            Molecule molecule = null;

            if (nodeLevel == MOLECULE || nodeLevel == EXPERIMENT) {
                if (nodeLevel == MOLECULE) {
                    molecule = (Molecule) node.getUserObject();
                } else {
                    molecule = (Molecule) ((DefaultMutableTreeNode) node.getParent()).getUserObject();
                }
                samples = new TreeSet<Sample>(new SampleValueComparator());
                Iterator<TreeNode> sampleNodeIter = selectorTree.checkedDescendantIterator(node, SAMPLE);
                int index = 0;
                while (sampleNodeIter.hasNext()) {
                    samples.add((Sample) ((DefaultMutableTreeNode) sampleNodeIter.next()).getUserObject());
                }
                samples.retainAll(this.sampleGroup);
                XYSeries data = null;
                int time = Integer.MIN_VALUE;
                int series = 0;
                TreeSet currentSamples = new TreeSet<Sample>();
                for (Sample sample : samples) {
                    int newTime = sample.getIntAttribute("time");

                    if (time != newTime) {
                        if (data != null) {
                            dataset.addSeries(data);
                            renderer.setSeriesOutlierInfo(series,
                                    Statistics.regularRange(molecule.getValues(currentSamples).toDoubleArray()));
                            series++;
                        }
                        time = newTime;
                        currentSamples = new TreeSet<Sample>();
                        data = new XYSeries(language.get("Time " + time));
                    }
                    currentSamples.add(sample);
                    Number value = molecule.getValue(sample);
                    data.add(index, value);
                    index++;
                }
                if (data == null) {
                    this.chart = null;
                    return false;
                }
                renderer.setSeriesOutlierInfo(series,
                        Statistics.regularRange(molecule.getValues(currentSamples).toDoubleArray()));
                dataset.addSeries(data);

            } else {
                this.chart = null;
                return false;
            }
            if (dataset.getSeriesCount() < 1) {
                this.chart = null;
                return false;
            }

            this.chart = ChartFactory.createScatterPlot(
                    String.format(language.get("%s sample concentrations") + " - %s", node.toString(),
                            this.sampleGroup.toString()), // title
                    language.get("Sample"), // x axis label
                    language.get("Response"), // y axis label
                    dataset, // plot data
                    PlotOrientation.VERTICAL, // Plot Orientation
                    true, // show legend
                    false, // use tooltips
                    false // configure chart to generate URLs (?)
            );

            this.chart.getTitle().setFont(new Font("Arial", Font.BOLD, 18));
            XYPlot plot = this.chart.getXYPlot();
            plot.getDomainAxis().setRange(-0.5, samples.size() - 0.5);
            plot.setRenderer(renderer);
            // this is a single experiment graph, so pick the color to be consistent
            // with the multi-experiment graph.
            int experimentCount = node.getParent().getChildCount();
            int index = 0;
            renderer.setSeriesPaint(0, Color.getHSBColor(0.5f, 1.0f, 0.5f));
            renderer.setSeriesStroke(0, new BasicStroke(2));
            renderer.setSeriesShapesVisible(0, true);

            plot.setBackgroundPaint(Color.WHITE);
            plot.setRangeGridlinePaint(Color.GRAY);
            plot.setDomainGridlinePaint(Color.GRAY);
            TickUnits tickUnits = new TickUnits();
            double tickIndex = 0.0;
            List<Sample> sampleList = new ArrayList<Sample>(samples);
            for (Sample sample : sampleList) {
                tickUnits.add(new CustomTickUnit(tickIndex, sampleList));
                tickIndex++;
            }
            plot.getDomainAxis().setStandardTickUnits(tickUnits);
            plot.getDomainAxis().setVerticalTickLabels(true);
            Range range = molecule.getValues(samples).getRange().scale(1.1);
            if (Double.compare(range.size(), 0) > 0)
                plot.getRangeAxis().setRange(range.getMin(), range.getMax());
            LegendItemCollection legendItems = plot.getLegendItems();
            if (legendItems == null) {
                legendItems = new LegendItemCollection();
            }
            legendItems.add(renderer.getOutlierLegendItem());
            plot.setFixedLegendItems(legendItems);
            return true;
        }

        /**
         * The valueChanged method of the TreeSelectionListener interface
         * 
         * @param e The event which triggered this action.
         */
        public void valueChanged(TreeSelectionEvent e) {
            TreePath path = e.getPath();
            if (path != null) {
                int level = path.getPathCount();
                if (level > EXPERIMENT) {
                    this.setGraph((DefaultMutableTreeNode) path.getPathComponent(EXPERIMENT));
                } else if (level > MOLECULE) {
                    this.setGraph((DefaultMutableTreeNode) path.getPathComponent(MOLECULE));
                } else {
                    this.chart = null;
                }
                this.repaint();
            }
        }

        /**
         * The valueChanged method of the TreeCheckingListner interface.
         * 
         * @param e The event which triggered this action.
         */
        public void valueChanged(TreeCheckingEvent e) {
            this.valueChanged(new TreeSelectionEvent(e.getSource(), selectorTree.getTree().getSelectionPath(),
                    false, null, null));
        }

        /**
         * Draws the graph.
         * 
         * @param g The Graphics object of this component.
         */
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (chart != null) {
                Dimension size = this.getSize(null);
                BufferedImage drawing = this.chart.createBufferedImage(size.width, size.height);
                g.drawImage(drawing, 0, 0, Color.WHITE, this);
            }
        }

        // =============== SampleValueComparator ===========================
        private class SampleValueComparator implements Comparator<Sample> {

            public int compare(Sample s1, Sample s2) {
                int returnValue = (int) Math.signum(
                        Integer.parseInt(s1.getAttribute("Time")) - Integer.parseInt(s2.getAttribute("Time")));
                if (returnValue == 0) {
                    returnValue = s1.toString().compareTo(s2.toString());
                }
                return returnValue;
            }
        }
    }

    // ================== CustomXyLineAndShapeRenderer ===========================

    /**
     * A customized version of XYLineAndShapeRenderer
     */
    private class CustomXYLineAndShapeRenderer extends XYLineAndShapeRenderer {
        private List<Range> outlierInfo;
        private SparseMatrix<Paint> itemShapePaints;
        private Paint outlierPaint;
        private Shape outlierShape;
        private Stroke outlierStroke;
        private XYDataset dataset;
        private int currentPass;

        public CustomXYLineAndShapeRenderer() {
            super();
            this.outlierInfo = new ArrayList<Range>();
            this.outlierPaint = Color.RED;
        }

        public CustomXYLineAndShapeRenderer(boolean lines, boolean shapes) {
            super(lines, shapes);
            this.outlierInfo = new ArrayList<Range>();
            this.outlierPaint = Color.RED;
            this.outlierStroke = new BasicStroke(2);
            this.outlierShape = new Polygon( // Star shape;
                    new int[] { 4, 1, 0, -1, -4, -1, 0, 1 }, new int[] { 0, 1, 4, 1, 0, -1, -4, -1 }, 8);
            this.itemShapePaints = new SparseMatrix<Paint>();
        }

        /**
         * Draws an item on the graph. This method is being overridden only to
         * keep track of which pass we are on, as there doesn't seem to be an
         * easy way to determine the current pass.
         * 
         * @param g2 - the graphics device.
         * @param state - the renderer state.
         * @param dataArea - the area within which the data is being drawn.
         * @param info - collects information about the drawing.
         * @param plot - the plot (can be used to obtain standard color 
         *   information etc).
         * @param domainAxis - the domain axis.
         * @param rangeAxis - the range axis.
         * @param dataset - the dataset.
         * @param series - the series index (zero-based).
         * @param item - the item index (zero-based).
         * @param crosshairState - crosshair information for the plot (null 
         *   permitted).
         * @param pass - the pass index.
         * @see XYLineAndShapeRenderer#drawItem( Graphics2D, XYItemRenderState,
         *   Rectangle2D, PlotRenderingInfo, XYPlot, ValueAxis, ValueAxis XYDataset,
         *  int, int, CrosshairState, int )
         */
        @Override
        public void drawItem(Graphics2D g2, XYItemRendererState state, Rectangle2D dataArea, PlotRenderingInfo info,
                XYPlot plot, ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, int series, int item,
                CrosshairState crosshairState, int pass) {
            this.currentPass = pass;
            super.drawItem(g2, state, dataArea, info, plot, domainAxis, rangeAxis, dataset, series, item,
                    crosshairState, pass);
        }

        /**
         * Sets the outlier information for a series.
         * 
         * @param index The index of the series to set the outlier information for.
         * @param item A BoxandWhiskerItem which will be used to determine outliers.
         */
        public void setSeriesOutlierInfo(int index, Range item) {
            while (index >= outlierInfo.size()) {
                outlierInfo.add(null);
            }
            outlierInfo.set(index, item);
        }

        /**
         * Returns the series outlier information for this series.
         * 
         * @param index The index of the series.
         * @return The Range used to calculate outliers.
         */
        public Range getSeriesOutlierInfo(int index) {
            if (index >= outlierInfo.size())
                return null;
            return outlierInfo.get(index);
        }

        /**
         * Determines if the given item in a series is an outlier.
         * 
         * @param series The series the item belongs to.
         * @param item The item in question.
         * @return A boolean indicating whether the item is an outlier.
         */
        private boolean isOutlier(int series, int item) {
            if (this.dataset == null) {
                this.dataset = this.getPlot().getDataset(0);
            }
            Range stats = this.getSeriesOutlierInfo(series);
            if (stats == null)
                return false;
            double y = this.dataset.getYValue(series, item);
            return (!stats.contains(y));
        }

        /**
         * Overrides XYLineAndShapeRenderer.getItemShape( ).
         * 
         * @param row The series to get the Shape for.
         * @param column The item to get the Shape for.
         * @return The appropriate Shape for this item.
         */
        public Shape getItemShape(int row, int column) {
            if (this.isOutlier(row, column)) {
                return this.outlierShape;
            } else {
                return super.getItemShape(row, column);
            }
        }

        /**
         * Overrides XYLineAndShapeRenderer.getItemPaint( ). This method
         * returns a different paint for outliers.
         * 
         * @param row The series to get the Paint for.
         * @param column The item to get the Paint  for.
         * @return The appropriate Paint for this item.
         */
        //    @Override
        //      public Paint getItemPaint( int row, int column ) {
        //         if ( this.isItemPass( this.currentPass )) {
        //            if ( this.isOutlier( row, column )) {
        //               return this.outlierPaint;
        //            } else {
        //               Paint returnValue = this.itemShapePaints.get( row, column );
        //               if ( returnValue == null )
        //                  return super.getItemPaint( row, column );
        //               return returnValue;
        //            }
        //         } else {
        //            return super.getItemPaint( row, column );
        //         }
        //      }

        /**
         * Sets the shape color for a particular item
         * 
         * @param row The row (or series) of the item
         * @param column The column of the item.
         * @param paint The Paint to use when drawing the item.
         */
        public void setItemShapePaint(int row, int column, Paint paint) {
            this.itemShapePaints.set(row, column, paint);
        }

        /**
         * Returns an appropriate LegendItem for the Outliers
         * 
         * @return A LegendItem for the outliers.
         */
        public LegendItem getOutlierLegendItem() {
            return new LegendItem("Outlier", // label
                    null, // description
                    null, // toolTipText
                    null, // urlText
                    this.outlierShape, // shape
                    this.outlierPaint, // fillPaint
                    this.outlierStroke, // outlineStroke
                    this.outlierPaint // outlinePaint
            );
        }
    }
}