Java tutorial
/* * 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.fit.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.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.fit.FitMaanovaResult; 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.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 panel that plots residuals for a {@link FitMaanovaResult}. This is * analogous to the resiplot(...) method in R * @author <A HREF="mailto:keith.sheppard@jax.org">Keith Sheppard</A> */ public class ResidualPlotPanel extends JPanel { /** * every {@link java.io.Serializable} is supposed to have one of these */ private static final long serialVersionUID = -2931761054889899035L; private static final Logger LOG = Logger.getLogger(ResidualPlotPanel.class.getName()); private static final int CURSOR_Y_OFFSET = 16; private final FitMaanovaResult fitMaanovaResult; private final int dyeCount; private final int arrayCount; private final MaanovaChartPanel chartPanel; private final JToolTip toolTip; private final JPanel controlPanel; private final JComboBox dyeComboBox; private XYProbeData[] cachedXYData = null; private final MouseMotionListener myMouseMotionListener = new MouseMotionAdapter() { /** * {@inheritDoc} */ @Override public void mouseMoved(MouseEvent e) { ResidualPlotPanel.this.mouseMoved(e); } }; private final MouseListener chartMouseListener = new MouseAdapter() { /** * {@inheritDoc} */ @Override public void mousePressed(MouseEvent e) { ResidualPlotPanel.this.clearProbePopup(); } /** * {@inheritDoc} */ @Override public void mouseExited(MouseEvent e) { ResidualPlotPanel.this.clearProbePopup(); } }; private final ComponentListener chartComponentListener = new ComponentAdapter() { /** * {@inheritDoc} */ @Override public void componentResized(ComponentEvent e) { ResidualPlotPanel.this.saveGraphImageAction.setSize(e.getComponent().getSize()); } }; private final AreaSelectionListener areaSelectionListener = new AreaSelectionListener() { /** * {@inheritDoc} */ public void areaSelected(Rectangle2D area) { ResidualPlotPanel.this.areaSelected(area); } }; private final SaveChartAction saveGraphImageAction = new SaveChartAction(); private volatile Rectangle2D viewArea = null; private volatile boolean showTooltip; private final SimpleChartConfigurationDialog chartConfigurationDialog; /** * Constructor * @param parent * the parent frame * @param fitMaanovaResult * the fitmaanova result that we'll plot residuals for */ public ResidualPlotPanel(JFrame parent, FitMaanovaResult fitMaanovaResult) { this.chartConfigurationDialog = new SimpleChartConfigurationDialog(parent); this.chartConfigurationDialog.addOkActionListener(new ActionListener() { /** * {@inheritDoc} */ public void actionPerformed(ActionEvent e) { ResidualPlotPanel.this.updateDataPoints(); } }); this.fitMaanovaResult = fitMaanovaResult; this.dyeCount = this.fitMaanovaResult.getParentExperiment().getDyeCount(); this.arrayCount = this.fitMaanovaResult.getParentExperiment().getMicroarrayCount(); 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); this.chartPanel.addMouseMotionListener(this.myMouseMotionListener); this.chartPanel.setLayout(null); chartAndControlPanel.add(this.chartPanel, BorderLayout.CENTER); ItemListener updateDataItemListener = new ItemListener() { /** * {@inheritDoc} */ public void itemStateChanged(ItemEvent e) { ResidualPlotPanel.this.forgetGraphState(); ResidualPlotPanel.this.updateDataPoints(); } }; if (this.dyeCount <= 1) { this.controlPanel = null; this.dyeComboBox = null; } else { this.dyeComboBox = new JComboBox(); for (int i = 0; i < this.dyeCount; i++) { this.dyeComboBox.addItem("Dye #" + (i + 1)); } this.dyeComboBox.addItemListener(updateDataItemListener); this.controlPanel = new JPanel(new FlowLayout()); this.controlPanel.add(this.dyeComboBox); chartAndControlPanel.add(this.controlPanel, BorderLayout.NORTH); } this.add(this.createMenu(), BorderLayout.NORTH); this.forgetGraphState(); this.updateDataPoints(); this.toolTip = new JToolTip(); } /** * Forget about the axis labeling and the zoom level */ private void forgetGraphState() { this.chartConfigurationDialog.setChartTitle("Residual Plot for " + this.fitMaanovaResult.toString()); this.chartConfigurationDialog.setXAxisLabel("Y Hat"); this.chartConfigurationDialog.setYAxisLabel("Residual"); this.viewArea = null; } private void updateDataPoints() { this.cachedXYData = null; XYProbeData[] xyData = this.getXYData(); DefaultXYDataset xyDataSet = new DefaultXYDataset(); for (int arrayIndex = 0; arrayIndex < xyData.length; arrayIndex++) { XYProbeData currData = xyData[arrayIndex]; xyDataSet.addSeries(arrayIndex, 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 int getSelectedDyeIndex() { if (this.dyeComboBox == null) { return 0; } else { return this.dyeComboBox.getSelectedIndex(); } } private synchronized XYProbeData[] getXYData() { if (this.cachedXYData == null) { this.cachedXYData = this.createXYData(this.getSelectedDyeIndex()); } return this.cachedXYData; } private XYProbeData[] createXYData(int dyeIndex) { XYProbeData[] probeData = new XYProbeData[this.arrayCount]; for (int arrayIndex = 0; arrayIndex < this.arrayCount; arrayIndex++) { Double[] dataValues = this.fitMaanovaResult.getParentExperiment().getData(dyeIndex, arrayIndex); Double[] yHatValues = this.fitMaanovaResult.getYHatValues(dyeIndex, arrayIndex); // check the array lengths which should be the same if everything is OK if (dataValues.length != yHatValues.length) { throw new IllegalArgumentException("There is a missmatch between the number of data points (" + dataValues.length + ") and y-hat (" + yHatValues.length + ") values"); } // first count all non-null pairings int nonNullCount = 0; for (int i = 0; i < dataValues.length; i++) { if (dataValues[i] != null && yHatValues[i] != null) { nonNullCount++; } } if (nonNullCount != dataValues.length && LOG.isLoggable(Level.WARNING)) { LOG.warning("Found " + (dataValues.length - nonNullCount) + " NaN data points in the residual 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 < dataValues.length; objArraysIndex++) { if (dataValues[objArraysIndex] != null && yHatValues[objArraysIndex] != null) { double data = dataValues[objArraysIndex]; double yHat = yHatValues[objArraysIndex]; double residual = data - yHat; primXValues[primitiveArraysIndex] = yHat; primYValues[primitiveArraysIndex] = residual; probeIndices[primitiveArraysIndex] = objArraysIndex; primitiveArraysIndex++; } } probeData[arrayIndex] = new XYProbeData(primXValues, primYValues, probeIndices); } return probeData; } @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) { ResidualPlotPanel.this.chartConfigurationDialog.setVisible(true); } }); toolsMenu.add(configureGraphItem); toolsMenu.addSeparator(); toolsMenu.add(new AbstractAction("Zoom Out") { /** * {@inheritDoc} */ public void actionPerformed(ActionEvent e) { ResidualPlotPanel.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) { ResidualPlotPanel.this.showTooltip = e.getStateChange() == ItemEvent.SELECTED; ResidualPlotPanel.this.clearProbePopup(); } }); toolsMenu.add(showTooltipCheckbox); menuBar.add(toolsMenu); // the help menu JMenu helpMenu = new JMenu("Help"); JMenuItem helpMenuItem = new JMenuItem("Help...", new ImageIcon(ResidualPlotAction.class.getResource("/images/action/help-16x16.png"))); helpMenuItem.addActionListener(new ActionListener() { /** * {@inheritDoc} */ public void actionPerformed(ActionEvent e) { Maanova.getInstance().showHelp("residual-plot", ResidualPlotPanel.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 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 nearestArrayIndex = -1; int nearestDotIndex = -1; for (int arrayIndex = 0; arrayIndex < xyProbeData.length; arrayIndex++) { double[] currXData = xyProbeData[arrayIndex].getXData(); double[] currYData = xyProbeData[arrayIndex].getYData(); for (int dotIndex = 0; dotIndex < currXData.length; dotIndex++) { double currDist = chartPoint.distanceSq(currXData[dotIndex], currYData[dotIndex]); if (currDist < nearestDistance) { nearestDistance = currDist; nearestArrayIndex = arrayIndex; nearestDotIndex = dotIndex; } } } if (nearestArrayIndex == -1) { this.clearProbePopup(); } else { XYProbeData nearestArrayData = xyProbeData[nearestArrayIndex]; Point2D probeJava2DCoord = this.getJava2DCoordinates(nearestArrayData.getXData()[nearestDotIndex], nearestArrayData.getYData()[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(nearestArrayIndex, nearestArrayData.getProbeIndices()[nearestDotIndex], nearestArrayData.getXData()[nearestDotIndex], nearestArrayData.getYData()[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 nearestArrayIndex, int nearestProbesetIndex, double nearestProbesetYHat, double nearestProbesetResidual, int pixelX, int pixelY) { if (this.toolTip.getParent() == null) { this.chartPanel.add(this.toolTip); } String nearestProbesetID = this.fitMaanovaResult.getProbesetId(nearestProbesetIndex); if (nearestProbesetID == null) { LOG.severe("Failed to lookup probeset name"); } 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("Array #:"); tableRowsString.append(cellDelimiter); tableRowsString.append(nearestArrayIndex + 1); tableRowsString.append(rowStop); tableRowsString.append(rowStart); tableRowsString.append("ID:"); tableRowsString.append(cellDelimiter); tableRowsString.append(nearestProbesetID); tableRowsString.append(rowStop); tableRowsString.append(rowStart); tableRowsString.append("Y Hat:"); tableRowsString.append(cellDelimiter); tableRowsString.append(nearestProbesetYHat); tableRowsString.append(rowStop); tableRowsString.append(rowStart); tableRowsString.append("Residual:"); tableRowsString.append(cellDelimiter); tableRowsString.append(nearestProbesetResidual); 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 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; } } }