/**
* Copyright Copyright 2010-12 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.util.ArrayList;
import java.util.List;
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 uk.ac.babraham.BamQC.Utilities.FormatNumber;
/**
*
* @author Piero Dalle Pezze
*
*/
public class LineWithHorizontalBarGraph extends JPanel {
private static final long serialVersionUID = -5947375412672203276L;
protected String [] barLabels;
protected String xTitle;
protected String xLabel;
protected String barDataLabel;
protected String [] xCategories;
protected double [] lineData;
protected double [] barData;
protected String graphTitle;
protected double minY;
protected double maxY;
protected double maxX;
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;
protected static final Color [] COLOURS = new Color[] {new Color(220,0,0), new Color(0,0,220), new Color(0,220,0), Color.DARK_GRAY, Color.MAGENTA, Color.ORANGE,Color.YELLOW,Color.CYAN,Color.PINK,Color.LIGHT_GRAY};
public LineWithHorizontalBarGraph(double[] barData, double[] lineData, double minY, double maxY, String xLabel, String[] barLabels, String xTitle, int[] xCategories, String graphTitle, String barDataLabel) {
this(barData,lineData,minY,maxY,xLabel,barLabels, xTitle,new String[0],graphTitle, barDataLabel);
this.xCategories = new String [xCategories.length];
for (int i=0;i<xCategories.length;i++) {
this.xCategories[i] = ""+xCategories[i];
}
}
public LineWithHorizontalBarGraph(double[] barData, double[] lineData, double minY, double maxY, String xLabel, String[] barLabels, String xTitle, String[] xCategories, String graphTitle, String barDataLabel) {
this.barData = barData;
this.lineData = lineData;
this.minY = 0-minY*(1.5*minY);
this.maxY = maxY*(1.5*maxY);
this.barLabels = barLabels;
this.xTitle = xTitle;
this.xLabel = xLabel;
this.xCategories = xCategories;
this.graphTitle = graphTitle;
this.barDataLabel = barDataLabel;
this.yInterval = findOptimalYInterval(maxY);
// 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 [] {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;
}
}
@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);
}
int xOffsetLineGraph = 0;
double midY = minY+((maxY-minY)/2);
String label = xTitle;
int width = g.getFontMetrics().stringWidth(label);
if (width > xOffsetLineGraph) {
xOffsetLineGraph = width;
}
g.drawString(label, 2, (int)getY(midY)+(g.getFontMetrics().getAscent()/2));
// calculate maxX
maxX=0;
for(int i=0; i<barData.length; i++) {
maxX = maxX + barData[i];
}
// Give the x axis a bit of breathing space
xOffsetLineGraph += 5;
// Draw the graph title
int titleWidth = g.getFontMetrics().stringWidth(graphTitle);
g.drawString(graphTitle, (xOffsetLineGraph + ((getWidth()-(xOffsetLineGraph+10))/2)) - (titleWidth/2), 30);
// Draw the xLabel under the xAxis
g.drawString(xLabel, (getWidth()/2) - (g.getFontMetrics().stringWidth(xLabel)/2), getHeight()-5);
// Draw the label for the bar data
g.drawString(barDataLabel, xOffsetLineGraph, 78);
// Now draw the horizontal bar (1st plot)
// First we need to find the widest label
int chrPosition = 0;
String chrPositionStr = "";
int leftSpace = g.getFontMetrics().stringWidth("");
// Add 3px either side for a bit of space;
leftSpace += 6;
double xPos = 0;
double xOffsetBarGraph = 0;
// set y coordinates
int yPos = +80;
int yOffset = getHeight()/10;
double cumulativeXOffset = 0;
// Initialise the arrays containing the tooltips
rectangles = new ArrayList<Rectangle>();
tips = new ArrayList<String>();
// Draw the first plot
for(int i=0; i<barData.length; i++) {
double xValue = getX(Math.min(barData[i], maxX), leftSpace);
// set xPos and xOffset
if(cumulativeXOffset==0)
xPos=leftSpace;
else
xPos=cumulativeXOffset;
xOffsetBarGraph=xValue-leftSpace;
// increase the cumulative X offset to get a measure for this plot,
// as we need this for scaling the second plot.
cumulativeXOffset = xPos+xOffsetBarGraph;
// draw the stacked horizontal bar scaffolds
Rectangle r = new Rectangle((int)xPos, yPos, (int)xOffsetBarGraph, yOffset);
g.setColor(new Color(200,0,0));
g.fillRect((int)r.getX(), (int)r.getY(), (int)r.getWidth(), (int)r.getHeight());
g.setColor(Color.BLACK);
g.drawRect((int)r.getX(), (int)r.getY(), (int)r.getWidth(), (int)r.getHeight());
// TOOL TIPS management
// add rectangle coordinates and tooltip to these two lists
//chrPositionStr = FormatNumber.compactIntegerRange(chrPosition, (int)barData[i]);
chrPositionStr = FormatNumber.convertToScientificNotation(chrPosition) + "-" +
FormatNumber.convertToScientificNotation(chrPosition+(int)(barData[i]));
chrPositionStr = chrPositionStr.replaceAll(".0$", ""); // Don't leave trailing .0s where we don't need them.
rectangles.add(r);
tips.add(barLabels[i] + " : " + chrPositionStr);
chrPosition = chrPosition + (int)barData[i];
// draw grey lines to annotate the second plots
g.setColor(new Color(230, 230, 230));
g.drawLine((int)cumulativeXOffset, getHeight()-40-1, (int)cumulativeXOffset, yPos+getHeight()/10);
g.setColor(Color.BLACK);
}
// Now draw horizontal lines across from the y axis (2nd plot)
// First draw faint boxes over alternating bases so you can see which is which
// Let's find the longest label, and then work out how often we can draw labels
double lastY = 0;
// Now draw the data points
// Set the width for the plot line
double baseWidth = 1.0*(cumulativeXOffset)/lineData.length;
int lastXLabelEnd = 0;
for(int i=0; i<lineData.length; i++) {
//String baseNumber = ""+xCategories[i];
//baseNumber = FormatNumber.compactInteger(baseNumber);
String baseNumber = FormatNumber.convertToScientificNotation(xCategories[i]);
baseNumber = baseNumber.replaceAll(".0$", ""); // Don't leave trailing .0s where we don't need them.
int baseNumberWidth = g.getFontMetrics().stringWidth(baseNumber);
int baseNumberPosition = (int)((baseWidth/2)+xOffsetLineGraph+(baseWidth*i)-(baseNumberWidth/2));
if (baseNumberPosition > lastXLabelEnd) {
g.drawString(baseNumber,baseNumberPosition, getHeight()-25);
lastXLabelEnd = baseNumberPosition+baseNumberWidth+5;
}
}
// Draw an horizontal line behind the line graph.
g.setColor(new Color(128,128,128));
g.drawLine(xOffsetLineGraph, (int)getY(midY), getWidth()-10, (int)getY(midY));
g.setColor(Color.BLACK);
// Now draw the axes
// x axis
g.drawLine(xOffsetLineGraph, getHeight()-40, getWidth()-10,getHeight()-40);
// y axis
g.drawLine(xOffsetLineGraph, getHeight()-40, xOffsetLineGraph, 80);
// Now draw the datasets
if (g instanceof Graphics2D) {
((Graphics2D)g).setStroke(new BasicStroke(2));
}
g.setColor(COLOURS[0]);
// First check whether we are starting with points having 0 coverage.
int i=0;
lastY = getY(lineData[i]);
for (; i<lineData.length && Double.isInfinite(lineData[i]); i++) { }
if(i<lineData.length) {
// This point has non-zero coverage
lastY = getY(lineData[i]);
}
for(i++; i<lineData.length; i++) {
if (Double.isNaN(lineData[i])) break;
// Check whether we have points with null coverage (the commented code removes
// an additional spike found at the beginning.
if (Double.isInfinite(lineData[i]) ) {
g.setColor(Color.BLUE);
g.drawLine((int)((baseWidth/2)+xOffsetLineGraph+(baseWidth*(i-1))), (int)getY(minY*0.40), (int)((baseWidth/2)+xOffsetLineGraph+(baseWidth*i)), (int)getY(minY*0.40));
g.setColor(COLOURS[0]);
lastY = getY(midY);
continue;
}
double thisY = getY(lineData[i]);
g.drawLine((int)((baseWidth/2)+xOffsetLineGraph+(baseWidth*(i-1))), (int)lastY, (int)((baseWidth/2)+xOffsetLineGraph+(baseWidth*i)), (int)thisY);
lastY = thisY;
}
}
private double getY(double y) {
int totalPlotArea = getHeight()-160;
return (getHeight()-30) - ((totalPlotArea/(maxY-minY))*(y-minY));
}
// Leave this to return a double instead of an int. It scales correctly when the plot changes size.
private double getX(double value, int longestLabel) {
int lengthToUse = getWidth()-(longestLabel+20);
double proportion = 1.0*value/maxX;
return longestLabel+(lengthToUse*proportion);
}
///////////////////////
// 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 LineWithHorizontalBarGraph toolTips;
public Tipster(LineWithHorizontalBarGraph 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();
}
}
}