/** * Copyright Copyright 2014 Simon Andrews * * This file is part of BamQC. * * BamQC 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. * * BamQC 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 BamQC; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ /* * Changelog: * - Piero Dalle Pezze: Class creation. */ package uk.ac.babraham.BamQC.Graphs; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.util.ArrayList; import java.util.List; import java.util.Random; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JWindow; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.event.MouseInputAdapter; import org.apache.commons.math3.util.Precision; import uk.ac.babraham.BamQC.Utilities.LinearRegression; /** * A class for drawing a scatter plot. * @author Piero Dalle Pezze * */ public class ScatterGraph extends JPanel { private static final long serialVersionUID = -7292512222510200683L; protected String xLabel; protected String yLabel; protected double[] data; protected double[] xCategories; protected String[] toolTipLabels; protected String graphTitle; protected double minX; protected double maxX; protected double xInterval; protected double minY; protected double maxY; protected double yInterval; protected int height = -1; protected int width = -1; // TOOL TIPS management private List<Rectangle> rectangles = null; private List<String> tips = null; private JWindow toolTip = null; private JLabel label = new JLabel(); private Tipster tipster = null; public ScatterGraph(double[] data, double[] xCategories, String[] toolTipLabels, String xLabel, String yLabel, String graphTitle) { initialise(data, xCategories, toolTipLabels, xLabel, yLabel, graphTitle); } public ScatterGraph(double[] data, String[] xCategories, String[] toolTipLabels, String xLabel, String yLabel, String graphTitle) { double[] myCategories = new double[xCategories.length]; for (int i=0; i<xCategories.length; i++) { myCategories[i] = Double.parseDouble(xCategories[i]); } initialise(data, myCategories, toolTipLabels, xLabel, yLabel, graphTitle); } private void initialise(double[] data, double[] xCategories, String[] toolTipLabels, String xLabel, String yLabel, String graphTitle) { this.data = data; this.xCategories = xCategories; this.toolTipLabels = toolTipLabels; this.xLabel = xLabel; this.yLabel = yLabel; this.graphTitle = graphTitle; // calculate minX-maxX, minY-maxY and xInterval-yInterval double[] minmax = new double[]{Double.MAX_VALUE, Double.MIN_VALUE}; calculateMinMax(this.data, minmax); minY = minmax[0]; maxY = minmax[1] + minmax[1]*0.1; // let's give some extra 10% space yInterval = findOptimalYInterval(maxY); minmax = new double[]{Double.MAX_VALUE, Double.MIN_VALUE}; calculateMinMax(this.xCategories, minmax); minX = minmax[0]; maxX = minmax[1] + minmax[1]*0.1; // let's give some extra 10% space xInterval = findOptimalYInterval(maxX); // TOOL TIPS management label.setHorizontalAlignment(JLabel.CENTER); label.setOpaque(true); label.setBackground(Color.WHITE); label.setBorder(UIManager.getBorder("ToolTip.border")); if(!GraphicsEnvironment.isHeadless()) { toolTip = new JWindow(); toolTip.add(label); // Tool tips tipster = new Tipster(this); addMouseMotionListener(tipster); } setOpaque(true); } private double findOptimalYInterval(double max) { int base = 1; double[] divisions = new double[] { 0.5, 1, 2, 2.5, 5 }; while (true) { for (int d = 0; d < divisions.length; d++) { double tester = base * divisions[d]; if (max / tester <= 10) { return tester; } } base *= 10; } } private void calculateMinMax(double[] myData, double[] minmax) { if(myData.length == 1) { // let's deal with this case separately. if(myData[0] >= 0) { minmax[0] = 0.0d; minmax[1] = myData[0]; } else { minmax[0] = myData[0]; minmax[1] = 0.0d; } return; } for(int i=0; i<myData.length; i++) { if(minmax[0] > myData[i]) { minmax[0] = myData[i]; } else if(minmax[1] < myData[i]) { minmax[1] = myData[i]; } } if(minmax[0] > 0) minmax[0] = 0.0d; } @Override public Dimension getPreferredSize() { return new Dimension(800, 600); } @Override public Dimension getMinimumSize() { return new Dimension(100, 200); } @Override public int getHeight() { if (height < 0) { return super.getHeight(); } return height; } @Override public int getWidth() { if (width < 0) { return super.getWidth(); } return width; } @Override protected void paintComponent(Graphics g) { g.setColor(Color.WHITE); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(Color.BLACK); if (g instanceof Graphics2D) { ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } double yStart, xStart; if (minY % yInterval == 0) { yStart = minY; } else { yStart = yInterval * (((int) minY / yInterval) + 1); } if (minX % xInterval == 0) { xStart = minX; } else { xStart = xInterval * (((int) minX / xInterval) + 1); } int xOffset = 0; // Draw the yLabel on the left of the yAxis int yLabelRightShift = 12; if(yLabel == null || yLabel.isEmpty()) { yLabelRightShift = 0; } else { if (g instanceof Graphics2D) { Graphics2D g2 = (Graphics2D)g; AffineTransform orig = g2.getTransform(); g2.rotate(-Math.PI/2); g2.setColor(Color.BLACK); g2.drawString(yLabel, -getY(-yInterval)/2 - (g.getFontMetrics().stringWidth(yLabel)/2), yLabelRightShift); g2.setTransform(orig); } } // Draw the y axis labels int lastYLabelEnd = Integer.MAX_VALUE; for (double i=yStart; i<=maxY; i+=yInterval) { String label = "" + i; label = label.replaceAll(".0$", ""); // Don't leave trailing .0s where we don't need them. // Calculate the new xOffset depending on the widest ylabel. int width = g.getFontMetrics().stringWidth(label); if (width > xOffset) { xOffset = width; } // place the y axis labels so that they don't overlap when the plot is resized. int baseNumberHeight = g.getFontMetrics().getHeight(); int baseNumberPosition = getY(i)+(baseNumberHeight/2); if (baseNumberPosition + baseNumberHeight < lastYLabelEnd) { // Draw the y axis labels g.drawString(label, yLabelRightShift+6, baseNumberPosition); lastYLabelEnd = baseNumberPosition + 2; } } // Give the x axis a bit of breathing space xOffset = xOffset + yLabelRightShift + 8; // Now draw horizontal lines across from the y axis g.setColor(new Color(180,180,180)); for (double i=yStart; i<=maxY; i+=yInterval) { g.drawLine(xOffset, getY(i), getWidth()-10, getY(i)); } g.setColor(Color.BLACK); // Draw the graph title int titleWidth = g.getFontMetrics().stringWidth(graphTitle); g.drawString(graphTitle, (xOffset + ((getWidth() - (xOffset + 10)) / 2)) - (titleWidth / 2), 30); // Draw the xLabel under the xAxis g.drawString(xLabel, (getWidth() / 2) - (g.getFontMetrics().stringWidth(xLabel) / 2), getHeight() - 5); // Now draw the data points double baseWidth = (getWidth() - (xOffset + 10)) / (maxX-minX); // System.out.println("Base Width is "+baseWidth); // Let's find the longest label, and then work out how often we can draw labels int lastXLabelEnd = 0; // Draw the x axis labels for (double i=xStart; i<=maxX; i+=xInterval) { g.setColor(Color.BLACK); String baseNumber = "" + i; baseNumber = baseNumber.replaceAll(".0$", ""); // Don't leave trailing .0s where we don't need them. // Calculate the new xOffset depending on the widest ylabel. int baseNumberWidth = g.getFontMetrics().stringWidth(baseNumber); int baseNumberPosition = (int)(xOffset + (baseWidth * i) - (baseNumberWidth / 2)); if (baseNumberPosition > lastXLabelEnd) { g.drawString(baseNumber, baseNumberPosition, getHeight() - 25); lastXLabelEnd = baseNumberPosition + baseNumberWidth + 5; } // Now draw vertical lines across from the y axis g.setColor(new Color(180,180,180)); g.drawLine((int)(xOffset + (baseWidth * i)), getHeight() - 40, (int)(xOffset + (baseWidth * i)), 40); g.setColor(Color.BLACK); } // Now draw the axes g.drawLine(xOffset, getHeight() - 40, getWidth() - 10, getHeight() - 40); g.drawLine(xOffset, getHeight() - 40, xOffset, 40); // Initialise the arrays containing the tooltips rectangles = new ArrayList<Rectangle>(); tips = new ArrayList<String>(); g.setColor(Color.BLUE); // Draw the data points double ovalSize = 5; // We distinguish two inputs since the x label does not start from 0. // used for computing the actual line points as if they were starting from 0. double[] inputVar = new double[data.length]; double[] responseVar = new double[data.length]; for (int d = 0; d < data.length; d++) { double x = getX(xCategories[d], xOffset)-ovalSize/2; double y = getY(data[d])-ovalSize/2; g.fillOval((int)x, (int)y, (int)(ovalSize), (int)(ovalSize)); g.drawString(toolTipLabels[d], (int)x+2, (int)y+16); inputVar[d] = Double.valueOf(xCategories[d]); responseVar[d] = data[d]; // Tool tips Rectangle r = new Rectangle((int)x, (int)y, (int)(ovalSize), (int)(ovalSize)); rectangles.add(r); tips.add(toolTipLabels[d]); } g.setColor(Color.BLACK); // Draw the intercept // WARNING: Is drawing a least squares regression line asserting that "the distribution follows a power law" correct? // This is our case if we plot log-log.. // It seems not in this paper (Appendix A) http://arxiv.org/pdf/0706.1062v2.pdf if(data.length > 1) { LinearRegression linReg = new LinearRegression(inputVar, responseVar); double intercept = linReg.intercept(); double slope = linReg.slope(); double rSquare = linReg.R2(); // Let's now calculate the two points (x1, y1) and (xn, yn) // (x1, y1). We need to skip the areas where x1<minY and y1>maxY double x1 = minX; double y1 = slope*minX + intercept; if(y1 < minY) { x1 = (minY - intercept)/slope; y1 = minY; } else if(y1 > maxY) { x1 = (maxY - intercept)/slope; y1 = maxY; } // (xn, yn). maxX which essentially is inputVar[inputVar.length-1] double xn = maxX; double yn = slope*maxX + intercept; if (g instanceof Graphics2D) { ((Graphics2D)g).setStroke(new BasicStroke(1.5f)); } g.setColor(Color.RED); g.drawLine(getX(x1, xOffset), getY(y1), getX(xn, xOffset), getY(yn)); g.setColor(Color.BLACK); if (g instanceof Graphics2D) { ((Graphics2D)g).setStroke(new BasicStroke(1)); } // Draw the legend for the intercept String legendString = "y = " + Precision.round(slope, 3) + "x"; if(intercept < 0) legendString += " - " + Precision.round(-intercept, 3); else legendString += " + " + Precision.round(intercept, 3); int width = g.getFontMetrics().stringWidth(legendString); // First draw a box to put the legend in g.setColor(Color.WHITE); g.fillRect(xOffset+10, 45, width+8, 35); g.setColor(Color.LIGHT_GRAY); g.drawRect(xOffset+10, 45, width+8, 35); // Now draw the legend label g.setColor(Color.RED); g.drawString(legendString, xOffset+13, 60); g.drawString("R^2 = " + Precision.round(rSquare, 3), xOffset+13, 76); g.setColor(Color.BLACK); } } private int getY(double y) { return (getHeight() - 40) - (int) (((getHeight() - 80) / (maxY - minY)) * y); } private int getX(double x, int xOffset) { return xOffset + (int) (((getWidth() - 40) / (maxX - minX)) * x); } /////////////////////// // TOOL TIPS management /////////////////////// public void showToolTip(int index, Point p) { if(GraphicsEnvironment.isHeadless()) { return; } p.setLocation(p.getX()+10, p.getY()+25); label.setText(tips.get(index)); toolTip.pack(); toolTip.setLocation(p); toolTip.setVisible(true); } public void hideToolTip() { if(GraphicsEnvironment.isHeadless()) { return; } toolTip.dispose(); } public boolean isToolTipShowing() { if(GraphicsEnvironment.isHeadless()) { return false; } return toolTip.isShowing(); } class Tipster extends MouseInputAdapter { private ScatterGraph toolTips; public Tipster(ScatterGraph tt) { toolTips = tt; } @Override public void mouseMoved(MouseEvent e) { if(GraphicsEnvironment.isHeadless()) { return; } Point p = e.getPoint(); boolean traversing = false; for(int j = 0; j < toolTips.rectangles.size(); j++) { Rectangle r = toolTips.rectangles.get(j); if(r.contains(p)) { SwingUtilities.convertPointToScreen(p, toolTips); toolTips.showToolTip(j, p); traversing = true; break; } } if(!traversing && toolTips.isToolTipShowing()) toolTips.hideToolTip(); } } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { Random r = new Random(); int sampleSize = 1000; double[] data = new double[sampleSize]; double[] xCategories = new double[sampleSize]; String[] toolTipsLabels = new String[sampleSize]; for(int i=0; i<sampleSize; i++) { data[i] = Math.log((r.nextGaussian()*1.5 + 10)*i + 50); xCategories[i] = Math.log(i + 50); toolTipsLabels[i] = String.valueOf(i); } String xLabel = "xLabel"; String yLabel = "yLabel"; //String yLabel = null; String graphTitle = "Graph Title"; JFrame frame = new JFrame(); ScatterGraph scatterGraph = new ScatterGraph(data, xCategories, toolTipsLabels, xLabel, yLabel, graphTitle); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(500, 500); frame.add(scatterGraph); frame.setLocationRelativeTo(null); frame.setVisible(true); } }); } }