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