/******************************************************************************* * GenPlay, Einstein Genome Analyzer * Copyright (C) 2009, 2014 Albert Einstein College of Medicine * * This program 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 program 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 program. If not, see <http://www.gnu.org/licenses/>. * Authors: Julien Lajugie <julien.lajugie@einstein.yu.edu> * Nicolas Fourel <nicolas.fourel@einstein.yu.edu> * Eric Bouhassira <eric.bouhassira@einstein.yu.edu> * * Website: <http://genplay.einstein.yu.edu> ******************************************************************************/ package edu.yu.einstein.genplay.gui.customComponent.scatterPlot; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dialog.ModalityType; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.geom.Point2D; import java.util.List; import javax.swing.JDialog; import javax.swing.JPanel; import edu.yu.einstein.genplay.dataStructure.enums.GraphType; import edu.yu.einstein.genplay.util.Images; import edu.yu.einstein.genplay.util.NumberFormats; import edu.yu.einstein.genplay.util.colors.Colors; /** * Dialog window showing a scatter plot chart. * @author Julien Lajugie * @version 0.1 */ public class ScatterPlotPane extends JPanel { private static final long serialVersionUID = -7553855067333108714L; // generated ID private static final int DEFAULT_WIDTH = 1000; // preferred width of the dialog private static final int DEFAULT_HEIGHT = 700; // preferred height of the dialog private static final int MINIMUM_WIDTH = 700; // minimum width of the dialog private static final int MINIMUM_HEIGHT = 500; // minimum height of the dialog private static final int PAD = 100; // padding between the graph and the dialog private static final int LENGEND_PAD = 20; // padding between the legend and the dialog private static final int LEGEND_INSET = 10; // inset between the legend rectangle and the legend text private static final Color LEGEND_BACKGROUND = new Color(228, 236, 247); // background color private static final String X_AXIS_PREFIX = "X-Axis: "; // prefix name of the x axis private static final String Y_AXIS_PREFIX = "Y-Axis: "; // prefix name of the y axis /** * Shows a {@link ScatterPlotPane}. * @param parent parent component. Can be null * @param xAxisName name of the X-Axis * @param yAxisName name of the Y-Axis * @param chartData data to plot */ public static void showDialog(Component parent, String xAxisName, String yAxisName, List<ScatterPlotData> chartData) { ScatterPlotPane scatterPlotPane = new ScatterPlotPane(parent, xAxisName, yAxisName, chartData); JDialog scatterPlotDialog = new JDialog(); scatterPlotDialog.setContentPane(scatterPlotPane); scatterPlotDialog.setModalityType(ModalityType.APPLICATION_MODAL); scatterPlotDialog.setTitle("Scatter Plot"); scatterPlotDialog.setIconImages(Images.getApplicationImages()); scatterPlotDialog.setPreferredSize(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT)); scatterPlotDialog.setMinimumSize(new Dimension(MINIMUM_WIDTH, MINIMUM_HEIGHT)); scatterPlotDialog.pack(); scatterPlotDialog.setLocationRelativeTo(parent); scatterPlotDialog.setVisible(true); scatterPlotDialog.dispose(); } private final ScatterPlotAxis xAxis; // x axis private final ScatterPlotAxis yAxis; // y axis private final List<ScatterPlotData> data; // data to plot private GraphType chartType; // type of chart private int legendWidth; // with of the legend /** * Private constructor. Creates an instance of a {@link ScatterPlotData} * @param parent parent component. Can be null * @param xAxisName name of the X-Axis * @param yAxisName name of the Y-Axis * @param data data to plot */ private ScatterPlotPane(Component parent, String xAxisName, String yAxisName, List<ScatterPlotData> data) { this.data = data; double[] dataBounds = findDataBounds(data); xAxis = new ScatterPlotAxis(dataBounds[0], dataBounds[1], ScatterPlotAxis.HORIZONTAL, xAxisName); yAxis = new ScatterPlotAxis(dataBounds[2], dataBounds[3], ScatterPlotAxis.VERTICAL, yAxisName); chartType = GraphType.BAR; legendWidth = computeLegendWidth(); // show the menu when right click addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == 3){ ScatterPlotMenu spm = new ScatterPlotMenu(ScatterPlotPane.this); spm.show(ScatterPlotPane.this, e.getX(), e.getY()); } } }); // show the data values for the mouse position addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { Rectangle clip = new Rectangle(PAD, PAD, getWidth() - (2 * PAD), getHeight() - (2 * PAD)); // we print the tooltip text only if the cursor is inside the chart area if (clip.contains(e.getPoint())) { Point2D p = getDataPoint(e.getPoint()); setToolTipText("(" + NumberFormats.getScoreFormat().format(p.getX()) + " : " + NumberFormats.getScoreFormat().format(p.getY()) + ")"); } else { setToolTipText(null); } } }); setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR)); } /** * Adds data to the list of ScatterPlotData showed in the component * @param dataToAdd list of {@link ScatterPlotData} to add */ public void addData(List<ScatterPlotData> dataToAdd) { data.addAll(dataToAdd); this.repaint(); } /** * Adds data to the list of ScatterPlotData showed in the component * @param dataToAdd {@link ScatterPlotData} to add */ public void addData(ScatterPlotData dataToAdd) { data.add(dataToAdd); findDataBounds(data); legendWidth = computeLegendWidth(); this.repaint(); } /** * @return the width of the legend text in pixels */ private int computeLegendWidth() { FontMetrics fm = getFontMetrics(getFont()); int legendWidth = fm.stringWidth(X_AXIS_PREFIX + xAxis.getName()); legendWidth = Math.max(legendWidth, fm.stringWidth(Y_AXIS_PREFIX + yAxis.getName())); for (ScatterPlotData currentPlot: data) { if (data != null) { legendWidth = Math.max(legendWidth, fm.stringWidth(currentPlot.getName()) + 30); } } return legendWidth; } /** * Method to plot the data points as a bar chart * @param g (Graphics) */ private void drawBarGraphics(Graphics g) { for (ScatterPlotData currentScatterPlot: data) { g.setColor(currentScatterPlot.getColor()); for (int j = 0; j < (currentScatterPlot.getData().length - 1); j++) { Point current = null; Point next = null; if ((currentScatterPlot.getData()[j][0] >= xAxis.getMin()) && (currentScatterPlot.getData()[j][0] <= xAxis.getMax())) { if (currentScatterPlot.getData()[j][1] > yAxis.getMax()) { current = new Point(getTranslatedPoint(currentScatterPlot.getData()[j][0], yAxis.getMax())); next = new Point(getTranslatedPoint(currentScatterPlot.getData()[j + 1][0], yAxis.getMax())); } else if (currentScatterPlot.getData()[j][1] < yAxis.getMin()) { current = new Point(getTranslatedPoint(currentScatterPlot.getData()[j][0], yAxis.getMin())); next = new Point(getTranslatedPoint(currentScatterPlot.getData()[j + 1][0], yAxis.getMin())); } else { current = new Point(getTranslatedPoint(currentScatterPlot.getData()[j][0], currentScatterPlot.getData()[j][1])); next = new Point(getTranslatedPoint(currentScatterPlot.getData()[j + 1][0], currentScatterPlot.getData()[j][1])); g.drawLine(current.x, current.y, next.x, next.y); } g.drawLine(current.x, current.y, current.x, getTranslatedPoint(current.x, 0).y); g.drawLine(next.x, next.y, next.x, getTranslatedPoint(current.x, 0).y); } } } } /** * Draws the background of the chart * @param g {@link Graphics} * @param clip rectangle where to draw the chart */ private void drawChartBackground(Graphics g, Rectangle clip) { g.setColor(Colors.WHITE); g.fillRect(clip.x, clip.y, clip.width, clip.height); } /** * Method to plot the data points as a curve chart * @param g (Graphics) */ private void drawCurveGraphics(Graphics g) { for (ScatterPlotData currentScatterPlot: data) { g.setColor(currentScatterPlot.getColor()); for (int j = 0; j < (currentScatterPlot.getData().length - 1); j++) { Point current = new Point(getTranslatedPoint(currentScatterPlot.getData()[j][0], currentScatterPlot.getData()[j][1])); Point nexttonext = new Point(getTranslatedPoint(currentScatterPlot.getData()[j + 1][0], currentScatterPlot.getData()[j + 1][1])); g.drawLine(current.x, current.y, nexttonext.x, nexttonext.y); if ((currentScatterPlot.getData()[j][0] < xAxis.getMin()) || (currentScatterPlot.getData()[j][0] > xAxis.getMax()) || (currentScatterPlot.getData()[j + 1][1] <= yAxis.getMax())) { // we erase the part of the line that is not in the chart area // this means that the curve needs to be drawn before the axis and ticks and legend g.setColor(getBackground()); g.fillRect(0, 0, getWidth(), PAD); g.fillRect(0, getHeight() - PAD, getWidth(), PAD); g.setColor(currentScatterPlot.getColor()); } } } } /** * Draws the scatter plots. The style is defined by the graphics type parameter. * @param g {@link Graphics} object of the component * @param clip rectangle where to draw the chart */ private void drawGraphics(Graphics g, Rectangle clip, GraphType graphicsType) { switch (graphicsType) { case BAR: drawBarGraphics(g); break; case CURVE: drawCurveGraphics(g); break; case DENSE: throw new IllegalArgumentException("Can't draw a dense scatter curve"); case POINTS: drawPointGraphics(g); break; } } /** * Draws the Legend for the scatter plot * @param g {@link Graphics} */ private void drawLegend(Graphics g) { // set the color of the legend rectangle g.setColor(LEGEND_BACKGROUND); int lineHeight = g.getFontMetrics().getHeight() + 2; // height of a legend line // draw the legend rectangle g.fillRect(getWidth() - legendWidth - LENGEND_PAD - (LEGEND_INSET * 2), LENGEND_PAD, legendWidth + (LEGEND_INSET * 2), ((data.size() + 2 ) * lineHeight) + (LEGEND_INSET * 2)); g.setColor(Colors.BLACK); g.drawRect(getWidth() - legendWidth - LENGEND_PAD - (LEGEND_INSET * 2), LENGEND_PAD, legendWidth + (LEGEND_INSET * 2), ((data.size() + 2 ) * lineHeight) + (LEGEND_INSET * 2)); // search the position where to start the text of the legend Point p = new Point(getWidth() - legendWidth - LENGEND_PAD - LEGEND_INSET, LENGEND_PAD + LEGEND_INSET + g.getFontMetrics().getHeight()); // draw the axis names g.drawString(X_AXIS_PREFIX + xAxis.getName(), p.x, p.y); // draw X-Axis legend g.drawString(Y_AXIS_PREFIX + yAxis.getName(), p.x, p.y + lineHeight); // draw Y-Axis legend // draw graph name legend for (int i = 0; i < data.size(); i++) { Color graphColor = data.get(i).getColor(); String graphName = data.get(i).getName(); g.setColor(graphColor); int yLine = p.y + ((i + 2) * lineHeight); // y position of the current line g.drawLine(p.x, yLine - 5, p.x + 25, yLine - 5); // draw a line with the color of the graph g.setColor(Colors.BLACK); g.drawString(graphName, p.x + 30, yLine); // draw the name of the graph } } /** * Method to plot the data points as a point chart * @param g (Graphics) */ private void drawPointGraphics(Graphics g) { for (ScatterPlotData currentScatterPlot: data) { g.setColor(currentScatterPlot.getColor()); for (int j = 0; j < (currentScatterPlot.getData().length - 1); j++) { Point current = new Point(getTranslatedPoint(currentScatterPlot.getData()[j][0], currentScatterPlot.getData()[j][1])); Point next = new Point(getTranslatedPoint(currentScatterPlot.getData()[j + 1][0], currentScatterPlot.getData()[j][1])); if ((currentScatterPlot.getData()[j][0] >= xAxis.getMin()) && (currentScatterPlot.getData()[j][0] <= xAxis.getMax()) && (currentScatterPlot.getData()[j][1] >= yAxis.getMin()) && (currentScatterPlot.getData()[j][1] <= yAxis.getMax())) { g.drawLine(current.x, current.y, next.x, next.y); } } } } /** * @param data * @return the bounds of the specified data in an array of double organized this way:<br>{xMin, xMax, yMin, yMax} */ private double[] findDataBounds(List<ScatterPlotData> data) { double[] bounds = new double[4]; bounds[0] = Double.POSITIVE_INFINITY; bounds[1] = Double.NEGATIVE_INFINITY; bounds[2] = Double.POSITIVE_INFINITY; bounds[3] = Double.NEGATIVE_INFINITY; for (ScatterPlotData currentPlot: data) { if (data != null) { for (int i = 0; i < currentPlot.getData().length; i++) { bounds[0] = Math.min(bounds[0], currentPlot.getData()[i][0]); bounds[1] = Math.max(bounds[1], currentPlot.getData()[i][0]); bounds[2] = Math.min(bounds[2], currentPlot.getData()[i][1]); bounds[3] = Math.max(bounds[3], currentPlot.getData()[i][1]); } } } return bounds; } /** * @return the chart type */ public final GraphType getChartType() { return chartType; } /** * @return the data of the charts */ public final List<ScatterPlotData> getData() { return data; } /** * @param screenPoint {@link Point} of a screen position * @return original data Point corresponding to the specified screen position */ private Point2D getDataPoint(Point screenPoint) { Point2D.Double retPoint = new Point2D.Double(); retPoint.x = (((screenPoint.x - PAD) * (xAxis.getMax() - xAxis.getMin())) / (getWidth() - (2 * PAD))) + xAxis.getMin(); retPoint.y = (-1 * ((((screenPoint.y - getHeight()) + PAD) * (yAxis.getMax() - yAxis.getMin())) / (getHeight() - (2 * PAD)))) + yAxis.getMin(); return retPoint; } /** * Method to translate the coordinates of the data point to the point on screen * @param x x component of a data point * @param y y component of a data point * @return Point */ private Point getTranslatedPoint(double x, double y) { Point translatedPoint = new Point(); int realWidth = getWidth() - (2 * PAD); int realHeight = getHeight() - (2 * PAD); Rectangle clip = new Rectangle(PAD, PAD, realWidth, realHeight); translatedPoint.x = xAxis.dataValueToScreenPosition(x, clip); translatedPoint.y = yAxis.dataValueToScreenPosition(y, clip); return translatedPoint; } /** * @return the xAxis */ public final ScatterPlotAxis getxAxis() { return xAxis; } /** * @return the yAxis */ public final ScatterPlotAxis getyAxis() { return yAxis; } @Override public void paint(Graphics g) { super.paint(g); // set where inside the dialog, the chart needs to be drawn Rectangle clip = new Rectangle(PAD, PAD, getWidth() - (2 * PAD), getHeight() - (2 * PAD)); // draw the background of the chart drawChartBackground(g, clip); // set the position of the axis in case they moved if (yAxis.isLogScale()) { xAxis.setPosition(yAxis.dataValueToScreenPosition(1, clip)); } else { xAxis.setPosition(yAxis.dataValueToScreenPosition(0, clip)); } if (xAxis.isLogScale()) { yAxis.setPosition(xAxis.dataValueToScreenPosition(1, clip)); } else { yAxis.setPosition(xAxis.dataValueToScreenPosition(0, clip)); } // draw the grid xAxis.drawGrid(g, clip); yAxis.drawGrid(g, clip); // draw the curve drawGraphics(g, clip, chartType); // draw the axis xAxis.drawAxis(g, clip); yAxis.drawAxis(g, clip); // draw minor units xAxis.drawMinorUnit(g, clip); yAxis.drawMinorUnit(g, clip); // draw major units xAxis.drawMajorUnit(g, clip); yAxis.drawMajorUnit(g, clip); // draw the chart legend drawLegend(g); } /** * @param chartType the chart type to set */ public final void setChartType(GraphType chartType) { this.chartType = chartType; } }