org.jax.maanova.madata.gui.ArrayScatterPlotPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.jax.maanova.madata.gui.ArrayScatterPlotPanel.java

Source

/*
 * Copyright (c) 2010 The Jackson Laboratory
 * 
 * This 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.
 *
 * This software 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 this software.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.jax.maanova.madata.gui;

import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JToolTip;

import org.jax.maanova.Maanova;
import org.jax.maanova.madata.MicroarrayExperiment;
import org.jax.maanova.madata.MicroarrayExperimentDesign;
import org.jax.maanova.plot.AreaSelectionListener;
import org.jax.maanova.plot.MaanovaChartPanel;
import org.jax.maanova.plot.PlotUtil;
import org.jax.maanova.plot.SaveChartAction;
import org.jax.maanova.plot.SimpleChartConfigurationDialog;
import org.jax.util.gui.MessageDialogUtilities;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartRenderingInfo;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.xy.DefaultXYDataset;

/**
 * A scatter plot that compares intensities for two arrays at a time
 * @author <A HREF="mailto:keith.sheppard@jax.org">Keith Sheppard</A>
 */
public class ArrayScatterPlotPanel extends JPanel {
    /**
     * every {@link java.io.Serializable} is supposed to have one of these
     */
    private static final long serialVersionUID = 7778496395222070792L;

    private static final int CURSOR_Y_OFFSET = 16;

    private static final Logger LOG = Logger.getLogger(ArrayScatterPlotPanel.class.getName());

    private final MicroarrayExperiment experiment;

    private final int dyeCount;

    private final MaanovaChartPanel chartPanel;

    private final JToolTip toolTip;
    private volatile boolean showTooltip;

    private final JPanel controlPanel;

    private final JComboBox array1ComboBox;

    private final JComboBox array2ComboBox;

    private XYProbeData cachedXYData = null;

    private final MouseMotionListener myMouseMotionListener = new MouseMotionAdapter() {
        /**
         * {@inheritDoc}
         */
        @Override
        public void mouseMoved(MouseEvent e) {
            ArrayScatterPlotPanel.this.mouseMoved(e);
        }
    };

    private final MouseListener chartMouseListener = new MouseAdapter() {
        /**
         * {@inheritDoc}
         */
        @Override
        public void mousePressed(MouseEvent e) {
            ArrayScatterPlotPanel.this.clearProbePopup();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void mouseExited(MouseEvent e) {
            ArrayScatterPlotPanel.this.clearProbePopup();
        }
    };

    private final ComponentListener chartComponentListener = new ComponentAdapter() {
        /**
         * {@inheritDoc}
         */
        @Override
        public void componentResized(ComponentEvent e) {
            ArrayScatterPlotPanel.this.saveGraphImageAction.setSize(e.getComponent().getSize());
        }
    };

    private final AreaSelectionListener areaSelectionListener = new AreaSelectionListener() {
        /**
         * {@inheritDoc}
         */
        public void areaSelected(Rectangle2D area) {
            ArrayScatterPlotPanel.this.areaSelected(area);
        }
    };

    private final SaveChartAction saveGraphImageAction = new SaveChartAction();

    private volatile Rectangle2D viewArea = null;

    private final SimpleChartConfigurationDialog chartConfigurationDialog;

    /**
     * Constructor
     * @param parent
     *          the parent frame
     * @param experiment
     *          the microarray experiment that we're going to be plotting data
     *          for
     */
    public ArrayScatterPlotPanel(JFrame parent, MicroarrayExperiment experiment) {
        this.chartConfigurationDialog = new SimpleChartConfigurationDialog(parent);
        this.chartConfigurationDialog.addOkActionListener(new ActionListener() {
            /**
             * {@inheritDoc}
             */
            public void actionPerformed(ActionEvent e) {
                ArrayScatterPlotPanel.this.updateDataPoints();
            }
        });

        this.experiment = experiment;
        this.dyeCount = experiment.getDyeCount();

        this.setLayout(new BorderLayout());

        JPanel chartAndControlPanel = new JPanel(new BorderLayout());
        this.add(chartAndControlPanel, BorderLayout.CENTER);

        this.chartPanel = new MaanovaChartPanel();
        this.chartPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
        this.chartPanel.addMouseMotionListener(this.myMouseMotionListener);
        this.chartPanel.addMouseListener(this.chartMouseListener);
        this.chartPanel.addComponentListener(this.chartComponentListener);
        this.chartPanel.addAreaSelectionListener(this.areaSelectionListener);
        this.chartPanel.setLayout(null);
        chartAndControlPanel.add(this.chartPanel, BorderLayout.CENTER);

        ItemListener updateDataItemListener = new ItemListener() {
            /**
             * {@inheritDoc}
             */
            public void itemStateChanged(ItemEvent e) {
                ArrayScatterPlotPanel.this.forgetGraphState();
                ArrayScatterPlotPanel.this.updateDataPoints();
            }
        };

        this.array1ComboBox = this.initializeArrayComboBox(this.dyeCount);
        this.array1ComboBox.addItemListener(updateDataItemListener);
        this.array2ComboBox = this.initializeArrayComboBox(this.dyeCount);
        this.array2ComboBox.setSelectedIndex(1);
        this.array2ComboBox.addItemListener(updateDataItemListener);

        this.controlPanel = new JPanel(new FlowLayout());
        this.controlPanel.add(this.array1ComboBox);
        this.controlPanel.add(new JLabel("vs."));
        this.controlPanel.add(this.array2ComboBox);
        chartAndControlPanel.add(this.controlPanel, BorderLayout.NORTH);

        this.add(this.createMenu(), BorderLayout.NORTH);

        this.forgetGraphState();
        this.updateDataPoints();

        this.toolTip = new JToolTip();
    }

    private void mouseMoved(MouseEvent e) {
        if (this.showTooltip) {
            Point2D chartPoint = this.chartPanel.toChartPoint(e.getPoint());

            // find the nearest probe
            XYProbeData xyProbeData = this.getXYData();
            double nearestDistance = Double.POSITIVE_INFINITY;
            int nearestDotIndex = -1;
            double[] xData = xyProbeData.getXData();
            double[] yData = xyProbeData.getYData();
            for (int dotIndex = 0; dotIndex < xData.length; dotIndex++) {
                double currDist = chartPoint.distanceSq(xData[dotIndex], yData[dotIndex]);
                if (currDist < nearestDistance) {
                    nearestDistance = currDist;
                    nearestDotIndex = dotIndex;
                }
            }

            if (nearestDotIndex == -1) {
                this.clearProbePopup();
            } else {
                Point2D probeJava2DCoord = this.getJava2DCoordinates(xData[nearestDotIndex],
                        yData[nearestDotIndex]);
                double java2DDist = probeJava2DCoord.distance(e.getX(), e.getY());

                // is the probe close enough to be worth showing (in pixel distance)
                if (java2DDist <= PlotUtil.SCATTER_PLOT_DOT_SIZE_PIXELS * 2) {
                    this.showProbePopup(xyProbeData.getProbeIndices()[nearestDotIndex], xData[nearestDotIndex],
                            yData[nearestDotIndex], e.getX(), e.getY());
                } else {
                    this.clearProbePopup();
                }
            }
        }
    }

    private Point2D getJava2DCoordinates(double graphX, double graphY) {
        final XYPlot plot = (XYPlot) this.chartPanel.getChart().getPlot();
        final ChartRenderingInfo renderingInfo = this.chartPanel.getChartRenderingInfo();

        return PlotUtil.toJava2DCoordinates(plot, renderingInfo, graphX, graphY);
    }

    private void clearProbePopup() {
        if (this.toolTip.getParent() != null) {
            this.chartPanel.remove(this.toolTip);
            this.chartPanel.repaint();
        }
    }

    private void showProbePopup(int nearestProbesetIndex, double firstArrayIntensity, double secondArrayIntensity,
            int pixelX, int pixelY) {
        if (this.toolTip.getParent() == null) {
            this.chartPanel.add(this.toolTip);
        }

        String nearestProbesetID = this.experiment.getProbesetId(nearestProbesetIndex);

        if (nearestProbesetID == null) {
            LOG.severe("Failed to lookup probeset name");
        } else {
            int firstArrayNumber = this.array1ComboBox.getSelectedIndex() + 1;
            int secondArrayNumber = this.array2ComboBox.getSelectedIndex() + 1;

            final String rowStart = "<tr><td>";
            final String rowStop = "</td></tr>";
            final String cellDelimiter = "</td><td>";
            StringBuilder tableRowsString = new StringBuilder("<html><table>");
            tableRowsString.append(rowStart);
            tableRowsString.append("ID:");
            tableRowsString.append(cellDelimiter);
            tableRowsString.append(nearestProbesetID);
            tableRowsString.append(rowStop);
            tableRowsString.append(rowStart);
            tableRowsString.append("Array " + firstArrayNumber + ":");
            tableRowsString.append(cellDelimiter);
            tableRowsString.append(firstArrayIntensity);
            tableRowsString.append(rowStop);
            tableRowsString.append(rowStart);
            tableRowsString.append("Array " + secondArrayNumber + ":");
            tableRowsString.append(cellDelimiter);
            tableRowsString.append(secondArrayIntensity);
            tableRowsString.append(rowStop);
            tableRowsString.append("</table></html>");

            this.toolTip.setTipText(tableRowsString.toString());

            // if the tool tip goes off the right edge of the screen, move it to the
            // left side of the cursor
            final int tooltipX;
            if (pixelX + this.toolTip.getPreferredSize().width > this.chartPanel.getWidth()) {
                tooltipX = pixelX - this.toolTip.getPreferredSize().width;
            } else {
                tooltipX = pixelX;
            }

            final int tooltipY;
            if (pixelY + this.toolTip.getPreferredSize().height + CURSOR_Y_OFFSET > this.chartPanel.getHeight()) {
                tooltipY = (pixelY - this.toolTip.getPreferredSize().height) - CURSOR_Y_OFFSET;
            } else {
                tooltipY = pixelY + CURSOR_Y_OFFSET;
            }
            this.toolTip.setLocation(tooltipX, tooltipY);

            this.toolTip.setSize(this.toolTip.getPreferredSize());
        }
    }

    private JComboBox initializeArrayComboBox(int dyeCount) {
        JComboBox arrayComboBox = new JComboBox();
        MicroarrayExperimentDesign design = this.experiment.getDesign();
        String[] arrayCol = design.getColumnNamed(MicroarrayExperimentDesign.ARRAY_COL_NAME);
        if (dyeCount == 1) {
            for (int i = 0; i < arrayCol.length; i++) {
                arrayComboBox.addItem(arrayCol[i]);
            }
        } else {
            String[] dyeCol = design.getColumnNamed(MicroarrayExperimentDesign.DYE_COL_NAME);
            if (dyeCol.length != arrayCol.length) {
                String errorMessage = "Expected the number of design terms to be the same for \""
                        + MicroarrayExperimentDesign.ARRAY_COL_NAME + "\" and \""
                        + MicroarrayExperimentDesign.DYE_COL_NAME + "\" but a missmatch was found: "
                        + arrayCol.length + " vs. " + dyeCol.length;
                LOG.severe(errorMessage);
                MessageDialogUtilities.error(this, errorMessage, "Design Term Count Missmatch");
            } else {
                for (int i = 0; i < arrayCol.length; i++) {
                    arrayComboBox.addItem(arrayCol[i] + ", " + dyeCol[i]);
                }
            }
        }

        return arrayComboBox;
    }

    /**
     * Forget about the axis labeling and the zoom level
     */
    private void forgetGraphState() {
        this.chartConfigurationDialog.setChartTitle("Array Comparison Scatter Plot");
        this.chartConfigurationDialog.setXAxisLabel(this.array1ComboBox.getSelectedItem().toString());
        this.chartConfigurationDialog.setYAxisLabel(this.array2ComboBox.getSelectedItem().toString());

        this.viewArea = null;
    }

    private void updateDataPoints() {
        this.cachedXYData = null;
        XYProbeData currData = this.getXYData();

        DefaultXYDataset xyDataSet = new DefaultXYDataset();
        xyDataSet.addSeries("data", new double[][] { currData.getXData(), currData.getYData() });

        JFreeChart scatterPlot = ChartFactory.createScatterPlot(this.chartConfigurationDialog.getChartTitle(),
                this.chartConfigurationDialog.getXAxisLabel(), this.chartConfigurationDialog.getYAxisLabel(),
                xyDataSet, PlotOrientation.VERTICAL, false, false, false);

        XYPlot xyPlot = (XYPlot) scatterPlot.getPlot();
        xyPlot.setRenderer(PlotUtil.createMonochromeScatterPlotRenderer());
        if (this.viewArea != null) {
            PlotUtil.rescaleXYPlot(this.viewArea, xyPlot);
        }

        this.saveGraphImageAction.setChart(scatterPlot);
        this.chartPanel.setChart(scatterPlot);
    }

    private synchronized XYProbeData getXYData() {
        if (this.cachedXYData == null) {
            int index1 = this.array1ComboBox.getSelectedIndex();
            int index2 = this.array2ComboBox.getSelectedIndex();

            int array1 = index1 / this.dyeCount;
            int dye1 = index1 % this.dyeCount;

            int array2 = index2 / this.dyeCount;
            int dye2 = index2 % this.dyeCount;

            this.cachedXYData = this.createXYData(array1, dye1, array2, dye2);
        }

        return this.cachedXYData;
    }

    private XYProbeData createXYData(int array1, int dye1, int array2, int dye2) {
        Double[] xValues = this.experiment.getData(dye1, array1);
        Double[] yValues = this.experiment.getData(dye2, array2);

        // check the array lengths which should be the same if everything is OK
        if (xValues.length != yValues.length) {
            throw new IllegalArgumentException("There is a missmatch between the number of data points ("
                    + xValues.length + ") and (" + yValues.length + ")");
        }

        // first count all non-null pairings
        int nonNullCount = 0;
        for (int i = 0; i < xValues.length; i++) {
            if (xValues[i] != null && yValues[i] != null) {
                nonNullCount++;
            }
        }

        if (nonNullCount != xValues.length && LOG.isLoggable(Level.WARNING)) {
            LOG.warning("Found " + (xValues.length - nonNullCount) + " NaN data points in the scatter plot data");
        }

        // OK, now convert to primitive arrays
        double[] primXValues = new double[nonNullCount];
        double[] primYValues = new double[nonNullCount];
        int[] probeIndices = new int[nonNullCount];
        int primitiveArraysIndex = 0;
        for (int objArraysIndex = 0; objArraysIndex < xValues.length; objArraysIndex++) {
            if (xValues[objArraysIndex] != null && yValues[objArraysIndex] != null) {
                double x = xValues[objArraysIndex];
                double y = yValues[objArraysIndex];
                primXValues[primitiveArraysIndex] = x;
                primYValues[primitiveArraysIndex] = y;
                probeIndices[primitiveArraysIndex] = objArraysIndex;

                primitiveArraysIndex++;
            }
        }

        return new XYProbeData(primXValues, primYValues, probeIndices);
    }

    @SuppressWarnings("serial")
    private JMenuBar createMenu() {
        JMenuBar menuBar = new JMenuBar();

        // the file menu
        JMenu fileMenu = new JMenu("File");
        fileMenu.add(this.saveGraphImageAction);
        menuBar.add(fileMenu);

        // the tools menu
        JMenu toolsMenu = new JMenu("Tools");
        JMenuItem configureGraphItem = new JMenuItem("Configure Graph...");
        configureGraphItem.addActionListener(new ActionListener() {
            /**
             * {@inheritDoc}
             */
            public void actionPerformed(ActionEvent e) {
                ArrayScatterPlotPanel.this.chartConfigurationDialog.setVisible(true);
            }
        });
        toolsMenu.add(configureGraphItem);
        toolsMenu.addSeparator();

        toolsMenu.add(new AbstractAction("Zoom Out") {
            /**
             * {@inheritDoc}
             */
            public void actionPerformed(ActionEvent e) {
                ArrayScatterPlotPanel.this.autoRangeChart();
            }
        });

        JCheckBoxMenuItem showTooltipCheckbox = new JCheckBoxMenuItem("Show Info Popup for Nearest Point");
        showTooltipCheckbox.setSelected(true);
        this.showTooltip = true;
        showTooltipCheckbox.addItemListener(new ItemListener() {
            /**
             * {@inheritDoc}
             */
            public void itemStateChanged(ItemEvent e) {
                ArrayScatterPlotPanel.this.showTooltip = e.getStateChange() == ItemEvent.SELECTED;
                ArrayScatterPlotPanel.this.clearProbePopup();
            }
        });
        toolsMenu.add(showTooltipCheckbox);
        menuBar.add(toolsMenu);

        // the help menu
        JMenu helpMenu = new JMenu("Help");
        JMenuItem helpMenuItem = new JMenuItem("Help...");
        Icon helpIcon = new ImageIcon(ArrayScatterPlotPanel.class.getResource("/images/action/help-16x16.png"));
        helpMenuItem.setIcon(helpIcon);
        helpMenuItem.addActionListener(new ActionListener() {
            /**
             * {@inheritDoc}
             */
            public void actionPerformed(ActionEvent e) {
                Maanova.getInstance().showHelp("array-scatter-plot", ArrayScatterPlotPanel.this);
            }
        });
        helpMenu.add(helpMenuItem);
        menuBar.add(helpMenu);

        return menuBar;
    }

    private void autoRangeChart() {
        this.viewArea = null;
        this.updateDataPoints();
    }

    private void areaSelected(Rectangle2D area) {
        Rectangle2D chartArea = this.chartPanel.toChartRectangle(area);

        this.viewArea = chartArea;
        this.updateDataPoints();
    }

    private class XYProbeData {
        private final double[] xData;

        private final double[] yData;

        private final int[] probeIndices;

        /**
         * Constructor
         * @param xData the x axis data
         * @param yData the y axis data
         * @param probeIndices  the indices for the corresponding probes
         */
        public XYProbeData(double[] xData, double[] yData, int[] probeIndices) {
            this.xData = xData;
            this.yData = yData;
            this.probeIndices = probeIndices;
        }

        /**
         * Getter for the probe indices
         * @return the probeIndices
         */
        public int[] getProbeIndices() {
            return this.probeIndices;
        }

        /**
         * Getter for the X data
         * @return the xData
         */
        public double[] getXData() {
            return this.xData;
        }

        /**
         * Getter for the Y data
         * @return the yData
         */
        public double[] getYData() {
            return this.yData;
        }
    }
}