/*
* Copyright (c) 2009 The Jackson Laboratory
*
* This software was developed by Gary Churchill's Lab at The Jackson
* Laboratory (see http://research.jax.org/faculty/churchill).
*
* 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.qtl.graph;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;
import javax.swing.JPopupMenu;
import javax.swing.ToolTipManager;
import org.jax.qtl.cross.Cross;
import org.jax.qtl.cross.GeneticMap;
import org.jax.qtl.cross.GeneticMarker;
import org.jax.qtl.cross.gui.ShowEffectPlotAction;
import org.jax.qtl.scan.ScanTwoResult;
import org.jax.qtl.scan.ScanTwoResult.MarkerIndexPair;
import org.jax.qtl.scan.ScanTwoResult.ScanTwoGeneticMarker;
import org.jax.qtl.util.Tools;
/**
* <p>Title: QTL data analysis</p>
*
* <p>Description: </p>
*
* <p>Company: The Jackson Laboratory</p>
*
* @author Lei Wu
* @version 1.0
*/
@SuppressWarnings("all")
public class ScantwoPlot extends OneDimensionPlot
{
/**
*
*/
private static final long serialVersionUID = -4492867589691649975L;
private static final Logger LOG = Logger.getLogger(
ScantwoPlot.class.getName());
private final ScanTwoResult scantwo;
private final GeneticMap[] geneticMaps;
private int selectedPhenoIndex; // selected pheno index
private double minlodLower, maxlodLower, minlodUpper, maxlodUpper;
private final String[] chromosomeNames; // selected chromosome names for this plot
// graph parameters
private Color[] colorMap;
private double[][] lods;
private int[] numMarkersOnEachChromosome;
private List<List<ScanTwoGeneticMarker>> markersPerChromosome;
private List<ScanTwoGeneticMarker> markers;
private boolean plotColorScale;
private int colorBarWidth = 10, colorBarSpace = 80;
/**
* our mouse listener
*/
private final MouseListener containerComponentMouseListener =
new MouseListener()
{
public void mouseClicked(MouseEvent event)
{
if(event.isPopupTrigger())
{
ScantwoPlot.this.showPopupMenu(event.getPoint());
}
}
public void mousePressed(MouseEvent event)
{
if(event.isPopupTrigger())
{
ScantwoPlot.this.showPopupMenu(event.getPoint());
}
}
public void mouseReleased(MouseEvent event)
{
if(event.isPopupTrigger())
{
ScantwoPlot.this.showPopupMenu(event.getPoint());
}
}
public void mouseEntered(MouseEvent e)
{
// no-op
}
public void mouseExited(MouseEvent e)
{
// no-op
}
};
public ScantwoPlot(
ScanTwoResult scantwo,
GeneticMap[] geneticMaps,
int selectedPhenoIndex,
int upperLodIndex,
int lowerLodIndex,
int[] selectedChromosomes,
boolean plotColorScale)
{
super();
this.addMouseListener(this.containerComponentMouseListener);
// select all chromosomes (temp)
// TODO this is pretty questionable
int numChromosome = selectedChromosomes.length;
selectedChromosomes = new int[numChromosome];
for (int i=0; i<numChromosome; i++)
selectedChromosomes[i] = i;
// end
this.drawOutlineBox = false;
this.scantwo = scantwo;
this.geneticMaps = geneticMaps;
this.selectedPhenoIndex = selectedPhenoIndex;
this.plotColorScale = plotColorScale;
// use selectedChromosomes to get chromosomeNames, numMarkersOnEachChromosome
int numSelectedChr = selectedChromosomes.length;
this.numMarkersOnEachChromosome = new int[numSelectedChr];
this.chromosomeNames = new String[numSelectedChr];
String[] allChromosomeNames = scantwo.getScannedChromosomeNames();
int totalMarkers = 0;
this.markersPerChromosome = scantwo.getGeneticMarkersPerChromosome();
for (int i=0; i<numSelectedChr; i++) {
this.numMarkersOnEachChromosome[i] = this.markersPerChromosome.get(selectedChromosomes[i]).size();
totalMarkers += this.numMarkersOnEachChromosome[i];
this.chromosomeNames[i] = allChromosomeNames[selectedChromosomes[i]];
}
// get selectedMarkers
this.markers = new ArrayList<ScanTwoGeneticMarker>(totalMarkers);
int markerIndex = 0;
for (int i=0; i<numSelectedChr; i++) {
this.markers.addAll(this.markersPerChromosome.get(selectedChromosomes[i]));
}
int preferedWidth = 500, preferedHeight = 500;
if (plotColorScale) {
preferedWidth += this.colorBarSpace;
}
// set the initial size
setPreferredSize(new Dimension(preferedWidth, preferedHeight));
// calculate the lods used in this plot based on user selection
getLodsInThisPlot(totalMarkers, upperLodIndex, lowerLodIndex);
// get min and max lod of the given dataset
setMinMaxValue();
this.colorMap = Tools.makeColormap(NUM_COLORS);
// TODO add interaction back in
// add GenoDataSelectionChangeListener to all markers in this scantwo result
// for (int i=0; i<totalMarkers; i++) {
// markers[i].addGenoDataSelectionChangeListener(this);
// }
// set selection box "color"
this.selectionBoxColor = new Color(0,0,0,30);
// set title and labels
setTitle("Two QTL Genome Scan - " +
scantwo.toString() + " - " +
scantwo.getScannedPhenotypeNames()[selectedPhenoIndex] +
" (" + LOD_TYPE[upperLodIndex] + " / " + LOD_TYPE[lowerLodIndex] + ")");
setXlabel("Chromosome (Maximum lod = " + FOUR_DIGIT_FORMATTER.format(this.maxlodLower) + ")");
setYlabel("Chromosome (Maximum lod = " + FOUR_DIGIT_FORMATTER.format(this.maxlodUpper) + ")");
}
/**
* Show the popup menu at the given click point
* @param popupPoint
* the click point
*/
private void showPopupMenu(Point popupPoint)
{
Iterator dotIter = this.allDots.iterator();
while(dotIter.hasNext())
{
Dot currDot = (Dot)dotIter.next();
if(currDot.getShape().contains(popupPoint))
{
ScanTwoResult scanTwoResult = this.scantwo;
Cross parentCross = scanTwoResult.getParentCross();
String phenotypeName = scanTwoResult.findScannedPhenotypeNameForScanColumn(
scanTwoResult.getScannedPhenotypeNames()[this.selectedPhenoIndex]);
GeneticMarker trueMarker1 = this.getNearestTrueMarker(
currDot.getMarker1());
GeneticMarker trueMarker2 = this.getNearestTrueMarker(
currDot.getMarker2());
if(parentCross != null && phenotypeName != null &&
trueMarker1 != null && trueMarker2 != null)
{
JPopupMenu popupMenu = new JPopupMenu();
popupMenu.add(new ShowEffectPlotAction(
parentCross,
phenotypeName,
trueMarker1,
trueMarker2));
popupMenu.show(
this,
popupPoint.x,
popupPoint.y);
}
else
{
LOG.warning(
"can't show effect plot since we dont have all " +
"of the data we need: parentCross=" + parentCross +
" phenotypeName=" + phenotypeName +
" marker1=" + trueMarker1 +
" marker2=" + trueMarker2);
}
}
}
}
/**
* Get the true genetic marker that is nearest the given genetic marker
* (which may be a true marker or a pseudo marker)
* @param marker
* the marker we're trying to get as close as possible to
* @return
* the nearest true marker
*/
private GeneticMarker getNearestTrueMarker(GeneticMarker marker)
{
GeneticMarker nearestTrueMarker = null;
double nearestCmDistance = Double.MAX_VALUE;
double markerPositionInCm = marker.getMarkerPositionCentimorgans();
for(GeneticMap currMap: this.geneticMaps)
{
// don't bother with the map unless the chromosome names match up
if(currMap.getChromosomeName().equals(marker.getChromosomeName()))
{
for(GeneticMarker currTrueMarker: currMap.getMarkerPositions())
{
double currCmDistance = Math.abs(
markerPositionInCm -
currTrueMarker.getMarkerPositionCentimorgans());
if(nearestTrueMarker == null || currCmDistance < nearestCmDistance)
{
nearestTrueMarker = currTrueMarker;
nearestCmDistance = currCmDistance;
}
}
}
}
return nearestTrueMarker;
}
/**
* Calculate the lod used in this plot based on LOD type user chosen.
* @param upperLodIndex int
* @param lowerLodIndex int
*/
private void getLodsInThisPlot(int totalMarkers, int upperLodIndex, int lowerLodIndex) {
this.lods = new double[totalMarkers][totalMarkers];
// used the same logic as in scantwoDot
for (int r=0; r<totalMarkers; r++) { // lower-left corner in data, upper-left corner in plot
for (int c = 0; c <= r; c++) {
if (r==c) {
this.lods[r][c] = this.scantwo.getScanOneLod(
this.selectedPhenoIndex,
r);
}
else {
switch (upperLodIndex) {
case LOD_FULL:
this.lods[r][c] = this.scantwo.getFullLod(
this.selectedPhenoIndex,
new ScanTwoResult.MarkerIndexPair(c, r));
break;
case LOD_ADD:
this.lods[r][c] = this.scantwo.getAdditiveLod(
this.selectedPhenoIndex,
new ScanTwoResult.MarkerIndexPair(c, r));
break;
case LOD_COND_INT:
this.lods[r][c] = this.scantwo.getFullVersusScanOneLod(
this.selectedPhenoIndex,
new ScanTwoResult.MarkerIndexPair(c, r));
break;
case LOD_COND_ADD:
this.lods[r][c] = this.scantwo.getAdditiveVersusScanOneLod(
this.selectedPhenoIndex,
new ScanTwoResult.MarkerIndexPair(c, r));
break;
case LOD_INT:
this.lods[r][c] = this.scantwo.getFullVersusAdditiveLod(
this.selectedPhenoIndex,
new ScanTwoResult.MarkerIndexPair(c, r));
break;
}
}
}
}
for (int r=0; r<totalMarkers; r++) { // upper-right corner in data, lower-right corner in plot
for (int c = r; c <totalMarkers; c++) {
if (c > r) { // it is handled above for (c==r)
switch (lowerLodIndex) {
case LOD_FULL:
this.lods[r][c] = this.scantwo.getFullLod(
this.selectedPhenoIndex,
new ScanTwoResult.MarkerIndexPair(r, c));
break;
case LOD_ADD:
this.lods[r][c] = this.scantwo.getAdditiveLod(
this.selectedPhenoIndex,
new ScanTwoResult.MarkerIndexPair(r, c));
break;
case LOD_COND_INT:
this.lods[r][c] = this.scantwo.getFullVersusScanOneLod(
this.selectedPhenoIndex,
new ScanTwoResult.MarkerIndexPair(r, c));
break;
case LOD_COND_ADD:
this.lods[r][c] = this.scantwo.getAdditiveVersusScanOneLod(
this.selectedPhenoIndex,
new ScanTwoResult.MarkerIndexPair(r, c));
break;
case LOD_INT:
this.lods[r][c] = this.scantwo.getFullVersusAdditiveLod(
this.selectedPhenoIndex,
new ScanTwoResult.MarkerIndexPair(r, c));
break;
}
}
}
}
}
/**
* set the maximum and minimum value on this graph for upper and lower triangle.
* @param selectedChromosomes int[]
*/
void setMinMaxValue() {
int numMarkers = this.lods.length;
for (int i = 0; i < numMarkers; i++) {
for (int j = 0; j < numMarkers; j++) {
if (i != j) {
double lod = this.lods[i][j];
if (i < j) { // upper
if (this.minlodLower > lod) this.minlodLower = lod;
if (this.maxlodLower < lod) this.maxlodLower = lod;
}
else { // lower
if (this.minlodUpper > lod) this.minlodUpper = lod;
if (this.maxlodUpper < lod) this.maxlodUpper = lod;
}
}
}
}
}
private int graphWidth;
private int graphHeight;
/**
* plot the scantwo result
*/
void plot() {
this.allDots = new HashSet<Dot>();
int leftConerX = this.inset.left;
int leftConerY = this.inset.top;
int width = this.plotWidth;
if (this.plotColorScale)
width -= this.colorBarSpace;
int height = this.plotHeight;
// total number of rows, columns (rows=columns=numMarkers)
int numMarkers = this.lods.length;
int gridWidth = width/numMarkers;
int gridHeight = height/numMarkers;
width = gridWidth * numMarkers;
height = gridHeight * numMarkers;
this.graphWidth = width;
this.graphHeight = height;
// if need to plot color scale
if (this.plotColorScale) {
plotColorScaleBar(leftConerX, leftConerY, height);
}
// draw grids
for (int row=0; row<numMarkers; row++) {
for (int col=0; col<numMarkers; col++) {
int x = gridWidth * col + leftConerX;
int y = leftConerY + height - gridHeight * (row+1);
Shape currentGrid = new Rectangle2D.Double(x,y,width/numMarkers,height/numMarkers);
Dot dot = new Dot(this.markers.get(row), this.markers.get(col), currentGrid);
this.allDots.add(dot);
boolean isLower = true;
if (row==col) // on diagnal
this.big.setColor(Color.blue);
else {
if (row > col) { // lower in data, upper in plot
isLower = false;
}
this.big.setColor(getColor(this.lods[row][col], isLower));
}
this.big.fill(currentGrid);
// TODO add interaction back in
// if (markers[row].isSelected() && markers[col].isSelected() && (row!=col)) {
// big.setColor(Color.white);
// big.setStroke(new BasicStroke(1.8f));
// big.draw(currentGrid);
// }
}
}
// draw chromosome dividers, (numChr - 1) crossed lines
int numChr = this.numMarkersOnEachChromosome.length;
int cumulatedNumMarkers = 0;
int lastx = leftConerX, lasty = leftConerY + height;
for (int i=0; i<numChr; i++) {
cumulatedNumMarkers += this.numMarkersOnEachChromosome[i];
int x = cumulatedNumMarkers * gridWidth + leftConerX;
int y = leftConerY + height - cumulatedNumMarkers * gridHeight;
int tickx = lastx + (this.numMarkersOnEachChromosome[i] * gridWidth)/2;
int ticky = lasty - (this.numMarkersOnEachChromosome[i] * gridHeight)/2;
lastx = x;
lasty = y;
this.big.setColor(this.normalColor);
this.big.setStroke(this.normalLinetype);
// draw vertical lines
this.big.drawLine(x, leftConerY, x, leftConerY + height);
// draw horizontal lines
this.big.drawLine(leftConerX, y, leftConerX + width, y);
// draw ticks
this.big.drawLine(tickx, leftConerY + height, tickx, leftConerY + height + this.tickHeight); // y axis ticks
this.big.drawLine(leftConerX - this.tickHeight, ticky, leftConerX, ticky); // x axis ticks
// label width and height
Rectangle2D labelBounds = this.tickLabelFont.getStringBounds(this.chromosomeNames[i], this.context);
double labelWidth = labelBounds.getWidth();
double labelHeight = labelBounds.getHeight();
// find the right place to start to draw labels on y axis
float yAxisLabelStartX = leftConerX - (float) labelWidth - this.tickHeight - this.labelToTick;
float yAxisLabelStartY = (float) (ticky + labelHeight / 2 - 1); // -1 is only for looks prettier
// draw tick label on y axis
this.big.setFont(this.tickLabelFont);
this.big.drawString(this.chromosomeNames[i], yAxisLabelStartX, yAxisLabelStartY); // vertical
// find the right place to start to draw labels on x axis
float xAxisLabelStartX = tickx - (float)labelWidth/2 + 0.5f; // 0.5f is only for looks prettier
float xAxisLabelStartY = leftConerY + height + this.tickHeight + (float)labelHeight + this.labelToTick;
// draw tick label on x axis
this.big.drawString(this.chromosomeNames[i], xAxisLabelStartX, xAxisLabelStartY); // horizontal
}
// last two outlines in left and bottom of the plot
this.big.drawLine(leftConerX, leftConerY, leftConerX, leftConerY+height); // vertical
this.big.drawLine(leftConerX, leftConerY+height, leftConerX+width, leftConerY+height); // horizontal
}
// for MouseMotionListener
public void mouseMoved(MouseEvent e)
{
int markerCount = this.markers.size();
int x = e.getX();
int y = e.getY();
double heightRatio =
(this.graphHeight - (y - this.inset.top))/(double)this.graphHeight;
double widthRatio =
(x - this.inset.left)/(double)this.graphWidth;
int xMarkerIndex =
(int)Math.floor((markerCount + 1) * widthRatio);
int yMarkerIndex =
(int)Math.floor((markerCount + 1) * heightRatio);
if(xMarkerIndex < markerCount && yMarkerIndex < markerCount &&
xMarkerIndex >= 0 && yMarkerIndex >= 0)
{
final String tip;
if(xMarkerIndex == yMarkerIndex)
{
ScanTwoGeneticMarker marker = this.markers.get(xMarkerIndex);
if(marker.isXChromosome())
{
tip =
"<html>" + marker.getMarkerName()
+ "<p>Scanone X LOD: "
+ FOUR_DIGIT_FORMATTER.format(this.scantwo.getScanOneXLod(
this.selectedPhenoIndex,
xMarkerIndex));
}
else
{
tip =
"<html>" + marker.getMarkerName()
+ "<p>Scanone LOD: "
+ FOUR_DIGIT_FORMATTER.format(this.scantwo.getScanOneLod(
this.selectedPhenoIndex,
xMarkerIndex));
}
}
else
{
ScanTwoGeneticMarker xMarker = this.markers.get(xMarkerIndex);
ScanTwoGeneticMarker yMarker = this.markers.get(yMarkerIndex);
final MarkerIndexPair markerIndexPair;
if(xMarkerIndex < yMarkerIndex)
{
markerIndexPair = new MarkerIndexPair(xMarkerIndex, yMarkerIndex);
}
else
{
markerIndexPair = new MarkerIndexPair(yMarkerIndex, xMarkerIndex);
}
tip =
"<html>" + xMarker.getMarkerName()
+ ":" + yMarker.getMarkerName()
+ "<p>Full LOD: "
+ FOUR_DIGIT_FORMATTER.format(this.scantwo.getFullLod(
this.selectedPhenoIndex,
markerIndexPair))
+ "<p>Add LOD: "
+ FOUR_DIGIT_FORMATTER.format(this.scantwo.getAdditiveLod(
this.selectedPhenoIndex,
markerIndexPair))
+ "<p>Cond-Int LOD: "
+ FOUR_DIGIT_FORMATTER.format(this.scantwo.getFullVersusScanOneLod(
this.selectedPhenoIndex,
markerIndexPair))
+ "<p>Cond-Add LOD: "
+ FOUR_DIGIT_FORMATTER.format(this.scantwo.getAdditiveVersusScanOneLod(
this.selectedPhenoIndex,
markerIndexPair))
+ "<p>Epstasis LOD: "
+ FOUR_DIGIT_FORMATTER.format(this.scantwo.getFullVersusAdditiveLod(
this.selectedPhenoIndex,
markerIndexPair));
}
ToolTipManager.sharedInstance().setEnabled(true);
this.setToolTipText(tip);
setCursor(this.handCursor);
return;
}
}
private void plotColorScaleBar(int leftConerX, int leftConerY, int height) {
// color bar
int x = leftConerX + this.plotWidth - this.colorBarWidth - (this.colorBarSpace - this.colorBarWidth) / 2;
for (int i = 0; i < 256; i++) {
this.big.setColor(this.colorMap[i]);
double y = leftConerY + height - height * i / 256.0;
this.big.fill(new Rectangle2D.Double(x, y, this.colorBarWidth, height / 256.0));
}
// tick for upper triangle
int maxIntLod = (int) this.maxlodUpper;
this.big.setColor(this.normalColor);
this.big.setFont(this.tickLabelFont);
for (int i = 0; i <= maxIntLod; i++) {
Rectangle2D labelBounds = this.tickLabelFont.getStringBounds(i + "", this.context);
double y = leftConerY + height - height * i / this.maxlodUpper;
this.big.drawLine(x - this.tickHeight, (int) y, x, (int) y);
this.big.drawString(i + "", (float) (x - this.tickHeight - labelBounds.getWidth() - this.labelToTick), (float) (y - 1 + labelBounds.getHeight() / 2)); // -1 is only for prettier
}
// tick for lower triangle
maxIntLod = (int) this.maxlodLower;
for (int i = 0; i <= maxIntLod; i++) {
// if ((maxIntLod > 5) && (maxIntLod % (maxIntLod/5) == 0)) {
Rectangle2D labelBounds = this.tickLabelFont.getStringBounds(i + "", this.context);
double y = leftConerY + height - height * i / this.maxlodLower;
this.big.drawLine(x + this.colorBarWidth + this.tickHeight, (int) y, x + this.colorBarWidth, (int) y);
this.big.drawString(i + "", (float) (x + this.colorBarWidth + this.tickHeight + this.labelToTick), (float) (y - 1 + labelBounds.getHeight() / 2)); // -1 is only for prettier
// }
}
this.big.drawRect(x, leftConerY, this.colorBarWidth, height);
}
private Color getColor(double value, boolean isLower) {
int index = 0;
if (isLower)
index = (int)((value-this.minlodLower)/((this.maxlodLower-this.minlodLower)/NUM_COLORS))-1;
else
index = (int)((value-this.minlodUpper)/((this.maxlodUpper-this.minlodUpper)/NUM_COLORS))-1;
if (index == -1) index = 0;
if (index > 255) index = 255;
return this.colorMap[index];
}
}