org.jax.maanova.test.gui.VolcanoPlotPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.jax.maanova.test.gui.VolcanoPlotPanel.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.test.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.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.AbstractAction;
import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComboBox;
import javax.swing.JFrame;
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.ProbesetRow;
import org.jax.maanova.madata.gui.AddGeneListDialog;
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.maanova.test.MaanovaTestResult;
import org.jax.maanova.test.MaanovaTestStatisticSubtype;
import org.jax.maanova.test.MaanovaTestStatisticType;
import org.jax.maanova.test.MaanovaTestStatistics;
import org.jax.r.RUtilities;
import org.jax.util.datastructure.SequenceUtilities;
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;
import org.jfree.data.xy.XYDataset;

/**
 * Panel used to render a volcano plot
 * @author <A HREF="mailto:keith.sheppard@jax.org">Keith Sheppard</A>
 */
public class VolcanoPlotPanel extends JPanel {
    /**
     * every {@link java.io.Serializable} is supposed to have one of these
     */
    private static final long serialVersionUID = 2887360960196852317L;

    private static final double MIN_PVALUE_THRESHOLD = 1e-9;

    /**
     * logger for this class
     */
    private static final Logger LOG = Logger.getLogger(VolcanoPlotPanel.class.getName());

    private static final int CURSOR_Y_OFFSET = 16;

    private final MaanovaTestResult maanovaTestResult;

    private final JMenuItem saveSelectedPointsMenuItem;

    private final DisplayTestResultsAction displayTestResultsAction;

    private final JPanel controlPanel;

    private final JToolTip toolTip;

    private final MaanovaChartPanel chartPanel;

    private final JComboBox statisticTypeComboBox;

    private final JComboBox statisticSubtypeComboBox;

    private final JComboBox testNumberComboBox;

    private XYProbeData cachedXYData = null;

    private volatile int[] selectedIndices = new int[0];

    private final SimpleChartConfigurationDialog chartConfigurationDialog;

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

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

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

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

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

    private final SaveChartAction saveGraphImageAction = new SaveChartAction();

    private volatile Rectangle2D viewArea = null;

    private volatile boolean dragToSelect;

    private volatile boolean dragToZoom;

    private volatile boolean showTooltip;

    private final TestStatisticItem[] availableTestStatistics;

    /**
     * Constructor
     * @param parent the parent frame
     * @param maanovaTestResult
     *          the test result to plot
     */
    public VolcanoPlotPanel(JFrame parent, MaanovaTestResult maanovaTestResult) {
        this(parent, maanovaTestResult, 0, new int[0]);
    }

    /**
     * Constructor
     * @param parent the parent frame
     * @param maanovaTestResult
     *          the test result to plot
     * @param initialTestIndex
     *          the initial test index to use
     * @param selectedIndices
     *          the initially selected indices
     */
    public VolcanoPlotPanel(JFrame parent, MaanovaTestResult maanovaTestResult, int initialTestIndex,
            int[] selectedIndices) {
        selectedIndices = SequenceUtilities.uniqueInts(selectedIndices);

        this.chartConfigurationDialog = new SimpleChartConfigurationDialog(parent);
        this.chartConfigurationDialog.addOkActionListener(new ActionListener() {
            /**
             * {@inheritDoc}
             */
            public void actionPerformed(ActionEvent e) {
                VolcanoPlotPanel.this.updateDataPoints();
            }
        });

        this.maanovaTestResult = maanovaTestResult;
        this.saveSelectedPointsMenuItem = new JMenuItem("Save Selected Points to Gene List");
        this.saveSelectedPointsMenuItem.setEnabled(selectedIndices != null && selectedIndices.length >= 1);
        this.saveSelectedPointsMenuItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                VolcanoPlotPanel.this.saveSelectedPoints();
            }
        });

        this.displayTestResultsAction = new DisplayTestResultsAction("Show Results Table", maanovaTestResult);
        this.selectedIndices = selectedIndices;

        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.addComponentListener(this.chartComponentListener);
        this.chartPanel.addAreaSelectionListener(this.areaSelectionListener);
        this.chartPanel.addMouseListener(this.chartMouseListener);
        chartAndControlPanel.add(this.chartPanel, BorderLayout.CENTER);

        this.controlPanel = new JPanel(new FlowLayout());

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

        this.statisticTypeComboBox = new JComboBox();
        for (MaanovaTestStatisticType testStatType : MaanovaTestStatisticType.values()) {
            this.statisticTypeComboBox.addItem(testStatType);
        }
        this.statisticTypeComboBox.addItemListener(updateDataItemListener);
        this.controlPanel.add(this.statisticTypeComboBox);

        List<MaanovaTestStatisticSubtype> availableTestSubtypes = new ArrayList<MaanovaTestStatisticSubtype>();
        this.statisticSubtypeComboBox = new JComboBox();
        MaanovaTestStatistics fStat = this.maanovaTestResult.getStatistics(MaanovaTestStatisticType.F_STAT);
        for (MaanovaTestStatisticSubtype statSubtype : MaanovaTestStatisticSubtype.values()) {
            if (fStat.hasTestStatistic(statSubtype)) {
                availableTestSubtypes.add(statSubtype);

                if (statSubtype != MaanovaTestStatisticSubtype.F_OBSERVED) {
                    this.statisticSubtypeComboBox.addItem(statSubtype);
                }
            }
        }
        this.statisticSubtypeComboBox.addItemListener(updateDataItemListener);
        this.controlPanel.add(this.statisticSubtypeComboBox);

        int testStatCount = availableTestSubtypes.size() * MaanovaTestStatisticType.values().length;
        this.availableTestStatistics = new TestStatisticItem[testStatCount];
        for (int i = 0; i < MaanovaTestStatisticType.values().length; i++) {
            for (int j = 0; j < availableTestSubtypes.size(); j++) {
                int flatIndex = i * availableTestSubtypes.size() + j;
                this.availableTestStatistics[flatIndex] = new TestStatisticItem(
                        MaanovaTestStatisticType.values()[i], availableTestSubtypes.get(j));
            }
        }

        int testCount = fStat.getContrastCount();
        if (testCount == 1) {
            this.testNumberComboBox = null;
        } else {
            this.testNumberComboBox = new JComboBox();
            for (int i = 1; i <= testCount; i++) {
                this.testNumberComboBox.addItem("Test Number " + i);
            }
            this.testNumberComboBox.setSelectedIndex(initialTestIndex);
            this.testNumberComboBox.addItemListener(updateDataItemListener);
            this.controlPanel.add(this.testNumberComboBox);
        }

        chartAndControlPanel.add(this.controlPanel, BorderLayout.NORTH);

        JMenuBar menu = this.createMenu();
        this.add(menu, BorderLayout.NORTH);

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

        this.chartPanel.addMouseMotionListener(this.myMouseMotionListener);
        this.chartPanel.setLayout(null);

        this.toolTip = new JToolTip();
    }

    private void saveSelectedPoints() {
        int[] currSelectedIndices = this.selectedIndices;
        String[] probeIds = this.maanovaTestResult.getParentExperiment().getProbesetIds();
        List<String> selectedGenes = new ArrayList<String>(currSelectedIndices.length);
        for (int i : currSelectedIndices) {
            selectedGenes.add(probeIds[i]);
        }

        AddGeneListDialog dialog = new AddGeneListDialog(
                (JFrame) org.jax.util.gui.SwingUtilities.getContainingWindow(this),
                this.maanovaTestResult.getParentExperiment(), selectedGenes);
        dialog.pack();
        dialog.setVisible(true);
    }

    /**
     * Forget about the axis labeling and the zoom level
     */
    private void forgetGraphState() {
        this.chartConfigurationDialog.setChartTitle("Volcano Plot for " + this.maanovaTestResult.toString());
        this.chartConfigurationDialog.setXAxisLabel("Fold Change");
        this.chartConfigurationDialog
                .setYAxisLabel("-log10(" + this.getSelectedStatisticSubtype().toString() + ")");

        this.viewArea = null;
    }

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

    @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) {
                VolcanoPlotPanel.this.chartConfigurationDialog.setVisible(true);
            }
        });
        toolsMenu.add(configureGraphItem);
        toolsMenu.addSeparator();

        toolsMenu.add(new AbstractAction("Clear Selections") {
            /**
             * {@inheritDoc}
             */
            public void actionPerformed(ActionEvent e) {
                VolcanoPlotPanel.this.setSelectedIndices(new int[0]);
            }
        });
        toolsMenu.addSeparator();

        ButtonGroup dragButtonGroup = new ButtonGroup();
        JCheckBoxMenuItem selectModeCheckBox = new JCheckBoxMenuItem("Drag Cursor to Select");
        selectModeCheckBox.setSelected(true);
        this.dragToSelect = true;
        selectModeCheckBox.addItemListener(new ItemListener() {
            /**
             * {@inheritDoc}
             */
            public void itemStateChanged(ItemEvent e) {
                VolcanoPlotPanel.this.dragToSelect = e.getStateChange() == ItemEvent.SELECTED;
            }
        });
        dragButtonGroup.add(selectModeCheckBox);
        toolsMenu.add(selectModeCheckBox);

        JCheckBoxMenuItem zoomModeCheckBox = new JCheckBoxMenuItem("Drag Cursor to Zoom");
        zoomModeCheckBox.addItemListener(new ItemListener() {
            /**
             * {@inheritDoc}
             */
            public void itemStateChanged(ItemEvent e) {
                VolcanoPlotPanel.this.dragToZoom = e.getStateChange() == ItemEvent.SELECTED;
            }
        });
        dragButtonGroup.add(zoomModeCheckBox);
        toolsMenu.add(zoomModeCheckBox);
        toolsMenu.addSeparator();

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

        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) {
                VolcanoPlotPanel.this.showTooltip = e.getStateChange() == ItemEvent.SELECTED;
                VolcanoPlotPanel.this.clearProbePopup();
            }
        });
        toolsMenu.add(showTooltipCheckbox);
        toolsMenu.addSeparator();

        toolsMenu.add(this.displayTestResultsAction);
        toolsMenu.addSeparator();

        toolsMenu.add(this.saveSelectedPointsMenuItem);

        JMenu selectPointsFromLisMenu = new JMenu("Select Points From Gene List");
        List<String> geneListNames = this.maanovaTestResult.getParentExperiment().getGeneListNames();
        if (geneListNames.isEmpty()) {
            JMenuItem noListsMenuItem = new JMenuItem("No Gene Lists Available");
            noListsMenuItem.setEnabled(false);
            selectPointsFromLisMenu.add(noListsMenuItem);
        } else {
            for (final String geneListName : geneListNames) {
                JMenuItem currGeneListMenuItem = new JMenuItem(
                        RUtilities.fromRIdentifierToReadableName(geneListName));
                currGeneListMenuItem.addActionListener(new ActionListener() {
                    /**
                     * {@inheritDoc}
                     */
                    public void actionPerformed(ActionEvent e) {
                        VolcanoPlotPanel.this.selectedIndicesFromGeneList(geneListName);
                    }
                });
                selectPointsFromLisMenu.add(currGeneListMenuItem);
            }
        }
        toolsMenu.add(selectPointsFromLisMenu);

        menuBar.add(toolsMenu);

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

        return menuBar;
    }

    private void selectedIndicesFromGeneList(String geneListName) {
        MicroarrayExperiment experiment = this.maanovaTestResult.getParentExperiment();
        this.setSelectedIndices(experiment.getIndicesForGeneListNamed(geneListName));
    }

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

        if (this.dragToSelect) {
            this.setSelectedIndices(this.getIndicesInArea(chartArea));
        } else if (this.dragToZoom) {
            this.viewArea = chartArea;
            this.updateDataPoints();
        }
    }

    private void setSelectedIndices(int[] selectedIndices) {
        selectedIndices = SequenceUtilities.uniqueInts(selectedIndices);
        if (!Arrays.equals(selectedIndices, this.selectedIndices)) {
            this.selectedIndices = selectedIndices;
            this.saveSelectedPointsMenuItem.setEnabled(selectedIndices != null && selectedIndices.length >= 1);

            this.updateDataPoints();
        }
    }

    /**
     * Getter for the indices that fall in the given area
     * @param area  the area (using chart coordinates)
     * @return  the indices
     */
    private int[] getIndicesInArea(Rectangle2D area) {
        XYProbeData xyData = this.getXYData();
        double[] xData = xyData.getXData();
        double[] yData = xyData.getYData();
        int[] probeIndices = xyData.getProbeIndices();

        int selectedCount = 0;
        int[] mySelectedIndices = new int[xData.length];
        for (int i = 0; i < xData.length; i++) {
            if (area.contains(xData[i], yData[i])) {
                mySelectedIndices[selectedCount] = probeIndices[i];
                selectedCount++;
            }
        }

        // trim the array to size
        if (selectedCount == mySelectedIndices.length) {
            return mySelectedIndices;
        } else {
            int[] trimmedArray = new int[selectedCount];
            for (int i = 0; i < trimmedArray.length; i++) {
                trimmedArray[i] = mySelectedIndices[i];
            }
            return trimmedArray;
        }
    }

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

            // find the nearest probe
            XYProbeData xyProbeData = this.getXYData();
            double[][] xyData = new double[][] { xyProbeData.getXData(), xyProbeData.getYData() };
            int nearestDotIndex = PlotUtil.getNearestDataIndex(xyData, chartPoint.getX(), chartPoint.getY());

            if (nearestDotIndex == -1) {
                this.clearProbePopup();
            } else {
                Point2D probeJava2DCoord = this.getJava2DCoordinates(xyData, 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], e.getX(), e.getY());
                } else {
                    this.clearProbePopup();
                }
            }
        }
    }

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

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

        ProbesetRow nearestProbeset = this.maanovaTestResult.getProbesetRow(nearestProbeIndex,
                this.getSelectedTestNumber(), this.availableTestStatistics);

        if (nearestProbeset == null) {
            LOG.severe("Failed to lookup probeset data for index: " + nearestProbeIndex);
        } else {
            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(nearestProbeset.getId());
            tableRowsString.append(rowStop);
            for (int i = 0; i < nearestProbeset.getValues().length; i++) {
                tableRowsString.append(rowStart);
                tableRowsString.append(this.availableTestStatistics[i].toString());
                tableRowsString.append(':');
                tableRowsString.append(cellDelimiter);
                tableRowsString.append(nearestProbeset.getValues()[i]);
            }
            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 Point2D getJava2DCoordinates(double[][] xyData, int dotIndex) {
        final double graphX = xyData[0][dotIndex];
        final double graphY = xyData[1][dotIndex];

        final XYPlot plot = (XYPlot) this.chartPanel.getChart().getPlot();
        final ChartRenderingInfo renderingInfo = this.chartPanel.getChartRenderingInfo();

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

    /**
     * Getter for the selected test statistics
     * @return
     *          the selected test statistics
     */
    private MaanovaTestStatistics getSelectedTestStatistics() {
        return this.maanovaTestResult.getStatistics(this.getSelectedStatisticType());
    }

    private MaanovaTestStatisticType getSelectedStatisticType() {
        return (MaanovaTestStatisticType) this.statisticTypeComboBox.getSelectedItem();
    }

    private MaanovaTestStatisticSubtype getSelectedStatisticSubtype() {
        return (MaanovaTestStatisticSubtype) this.statisticSubtypeComboBox.getSelectedItem();
    }

    private int getSelectedTestNumber() {
        if (this.testNumberComboBox == null) {
            return 0;
        } else {
            return this.testNumberComboBox.getSelectedIndex();
        }
    }

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

        DefaultXYDataset xyDataSet = new DefaultXYDataset();
        int[] selectedIndices = this.selectedIndices;
        if (selectedIndices.length == 0) {
            xyDataSet.addSeries("data", new double[][] { xyData.getXData(), xyData.getYData() });
        } else {
            double[] xData = xyData.getXData();
            double[] yData = xyData.getYData();
            int[] dataIndices = xyData.getProbeIndices();

            double[] normalXData = new double[xData.length - selectedIndices.length];
            double[] normalYData = new double[xData.length - selectedIndices.length];

            double[] selectedXData = new double[selectedIndices.length];
            double[] selectedYData = new double[selectedIndices.length];

            int selectionIndex = 0;
            for (int i = 0; i < xData.length; i++) {
                if (selectionIndex < selectedIndices.length && dataIndices[i] == selectedIndices[selectionIndex]) {
                    // this is one of the selected points
                    selectedXData[selectionIndex] = xData[i];
                    selectedYData[selectionIndex] = yData[i];

                    selectionIndex++;
                } else {
                    // this is not a selected point
                    normalXData[i - selectionIndex] = xData[i];
                    normalYData[i - selectionIndex] = yData[i];
                }
            }

            xyDataSet.addSeries("data", new double[][] { normalXData, normalYData });
            xyDataSet.addSeries("selected data", new double[][] { selectedXData, selectedYData });
        }

        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.createSimpleScatterPlotRenderer());
        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) {
            this.cachedXYData = this.createXYData(this.getSelectedTestNumber(), this.getSelectedTestStatistics(),
                    this.getSelectedStatisticSubtype());
        }

        return this.cachedXYData;
    }

    /**
     * Creates volcano plot data points from a JFreeChart {@link XYDataset}
     * from the given test statistics.
     * @param plotIndex
     *          the index of test that we want data points for
     * @param testStatistics
     *          the test statistics to extract from
     * @param testStatisticSubtype
     *          the subtype to extract from
     * @return
     *          the XY points for the volcano plot
     */
    private XYProbeData createXYData(int plotIndex, MaanovaTestStatistics testStatistics,
            MaanovaTestStatisticSubtype testStatisticSubtype) {
        Double[] objXValues = this.maanovaTestResult.getFoldChangeValues(plotIndex);
        Double[] objYValues = testStatistics.getValues(testStatisticSubtype, plotIndex);

        // check the array lengths which should be the same if everything is OK
        if (objXValues.length != objYValues.length) {
            throw new IllegalArgumentException("There is a missmatch between the number of X (" + objXValues.length
                    + ") and Y (" + objYValues.length + ") values");
        }

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

        if (nonNullCount != objXValues.length && LOG.isLoggable(Level.WARNING)) {
            LOG.warning(
                    "Found " + (objXValues.length - nonNullCount) + " NaN data points in the volcano 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 < objXValues.length; objArraysIndex++) {
            if (objXValues[objArraysIndex] != null && objYValues[objArraysIndex] != null) {
                double yVal = objYValues[objArraysIndex];
                if (yVal < MIN_PVALUE_THRESHOLD) {
                    yVal = MIN_PVALUE_THRESHOLD;
                }
                primXValues[primitiveArraysIndex] = objXValues[objArraysIndex];
                primYValues[primitiveArraysIndex] = -Math.log10(yVal);
                probeIndices[primitiveArraysIndex] = objArraysIndex;

                primitiveArraysIndex++;
            }
        }

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

    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) {
            assert xData.length == yData.length;
            assert yData.length == probeIndices.length;

            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;
        }
    }
}