com.hfg.bio.seq.graphics.ContigPlot Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com_hfg Show documentation
Show all versions of com_hfg Show documentation
com.hfg xml, html, svg, and bioinformatics utility library
package com.hfg.bio.seq.graphics;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import com.hfg.bio.seq.Exon;
import com.hfg.bio.Strand;
import com.hfg.bio.seq.Gene;
import com.hfg.bio.seq.SeqLocation;
import com.hfg.css.CSS;
import com.hfg.css.CSSProperty;
import com.hfg.graphics.TextUtil;
import com.hfg.graphics.units.GfxSize;
import com.hfg.graphics.units.GfxUnits;
import com.hfg.graphics.units.Pixels;
import com.hfg.html.Script;
import com.hfg.javascript.TooltipJS;
import com.hfg.math.Range;
import com.hfg.svg.SVG;
import com.hfg.svg.SvgGroup;
import com.hfg.svg.SvgRect;
import com.hfg.svg.SvgText;
import com.hfg.util.StringBuilderPlus;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;
//------------------------------------------------------------------------------
/**
* Visualization of genes on a contig.
*
* @author J. Alex Taylor, hairyfatguy.com
*/
//------------------------------------------------------------------------------
// com.hfg XML/HTML Coding Library
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
// [email protected]
//------------------------------------------------------------------------------
public class ContigPlot
{
private String mContigName;
private Integer mContigLength;
private SeqLocation mDisplayRange;
private boolean mAutoAdjustScale;
private boolean mDisplayLabels;
private Font mLabelFont = Font.decode("Arial-PLAIN-6");
private Font mScaleFont = Font.decode("Arial-PLAIN-8");
private Font mTitleFont = Font.decode("Arial-BOLD-10");
private List mGenes;
private GfxSize mLineLength = sDefaultLineLength;
private GfxSize mGeneHeight = sDefaultGeneHeight;
private int mLineY = 150;
// Calculated
private float mXScalingFactor;
private int mMajorTickStep;
private int mMinorTickStep;
private int mMaxFwdLabelLength;
private int mMaxRevLabelLength;
private Point mLineStart;
private Point mLineEnd;
private static FontRenderContext sFRC = new FontRenderContext(new AffineTransform(), true, true);
private static int sSvgScalePadding = 40;
private static int sMajorTickHeight = 10;
private static int sMinorTickHeight = 5;
private static GfxSize sDefaultLineLength = new Pixels(1000);
private static GfxSize sDefaultGeneHeight = new Pixels(20);
private static Color DEFAULT_COLOR = Color.BLACK;
//##########################################################################
// CONSTRUCTORS
//##########################################################################
//--------------------------------------------------------------------------
public ContigPlot(String inContigName, int inContigLength)
{
mContigName = inContigName;
mContigLength = inContigLength;
mDisplayRange = new SeqLocation(1, inContigLength);
}
//##########################################################################
// PUBLIC METHODS
//##########################################################################
//--------------------------------------------------------------------------
public ContigPlot setAutoAdjustScale(boolean inValue)
{
mAutoAdjustScale = inValue;
return this;
}
//--------------------------------------------------------------------------
public ContigPlot setDisplayLabels(boolean inValue)
{
mDisplayLabels = inValue;
return this;
}
//--------------------------------------------------------------------------
public ContigPlot addGene(Gene inValue)
{
if (null == mGenes)
{
mGenes = new ArrayList<>();
}
mGenes.add(inValue);
return this;
}
//--------------------------------------------------------------------------
public SVG toSVG()
{
if (mAutoAdjustScale)
{
adjustDisplayRange();
}
if (mDisplayLabels)
{
calculateMaxLabelLengths();
}
else
{
mMaxFwdLabelLength = mMaxRevLabelLength = 0;
}
mXScalingFactor = mLineLength.to(GfxUnits.pixels) / mDisplayRange.length().floatValue();
SVG svg = new SVG();
svg.addSubtag(new Script(new TooltipJS().generateJS()).setType("text/javascript"));
int xOffset = 10;
int yOffset = 10;
SvgGroup group = svg.addGroup()
.setTransform("translate(" + xOffset + "," + yOffset + ")");
// Add the contig name as a title
group.addText(mContigName, mTitleFont, new Point2D.Double(0, mLineY - mGeneHeight.toInt(GfxUnits.pixels) - mMaxFwdLabelLength - 13)).setClass("title");
// Draw the contig line
group.addLine(getLineStart(), getLineEnd()).setStrokeWidth(4);
// Draw genes
group.addSubtag(drawGenes());
// Draw the scale
group.addSubtag(generateXAxis());
svg.setHeight(svg.getHeight() + 25).setWidth(svg.getWidth() + 25);
return svg;
}
//--------------------------------------------------------------------------
private void adjustDisplayRange()
{
String widthString = mContigLength + "";
mMajorTickStep = (int) Math.pow(10, widthString.length() - 1);
if (0 == mMajorTickStep%mContigLength
|| mMajorTickStep > 0.5 * mContigLength)
{
mMajorTickStep = mMajorTickStep / 10;
}
Range geneRange = getGeneRange();
while (geneRange.length() < 3 * mMajorTickStep)
{
mMajorTickStep = mMajorTickStep / 10;
}
// What are the major tick values that contain the gene range?
for (int tickValue = (int) (Math.ceil(1 / mMajorTickStep) * mMajorTickStep);
tickValue < mDisplayRange.getEnd();
tickValue += mMajorTickStep)
{
if (tickValue <= geneRange.getStart())
{
mDisplayRange.setStart(tickValue <= 0 ? 1 : tickValue);
}
else if (tickValue > geneRange.getEnd())
{
mDisplayRange.setEnd(tickValue);
break;
}
}
}
//--------------------------------------------------------------------------
private Range getGeneRange()
{
Range geneRange = null;
if (CollectionUtil.hasValues(mGenes))
{
for (Gene gene : mGenes)
{
if (null == geneRange)
{
geneRange = gene.getLocation().toIntRange();
}
else
{
geneRange = geneRange.superUnion(gene.getLocation().toIntRange());
}
}
}
return geneRange;
}
//--------------------------------------------------------------------------
private Point getLineStart()
{
if (null == mLineStart)
{
mLineStart = new Point(0, mLineY);
}
return mLineStart;
}
//--------------------------------------------------------------------------
private Point getLineEnd()
{
if (null == mLineEnd)
{
mLineEnd = new Point((int) mLineLength.to(GfxUnits.pixels), mLineY);
}
return mLineEnd;
}
//--------------------------------------------------------------------------
private void calculateMaxLabelLengths()
{
int maxFwdLength = 0;
int maxRevLength = 0;
if (CollectionUtil.hasValues(mGenes))
{
for (Gene gene : mGenes)
{
Rectangle bbox = TextUtil.getStringRect(gene.getId(), mLabelFont);
if (gene.getStrand().equals(Strand.FORWARD)
&& bbox.width > maxFwdLength)
{
maxFwdLength = bbox.width;
}
else if (gene.getStrand().equals(Strand.REVERSE)
&& bbox.width > maxRevLength)
{
maxRevLength = bbox.width;
}
}
}
mMaxFwdLabelLength = maxFwdLength;
mMaxRevLabelLength = maxRevLength;
}
//--------------------------------------------------------------------------
private SvgGroup drawGenes()
{
SvgGroup genesGroup = new SvgGroup().setClass("genes");
if (CollectionUtil.hasValues(mGenes))
{
int lineHeight = (int) TextUtil.getStringRect("A", mLabelFont).getHeight();
for (Gene gene : mGenes)
{
SvgGroup geneGroup = genesGroup.addGroup().setId(gene.getId());
if (CollectionUtil.hasValues(gene.getExons()))
{
for (Exon exon : gene.getExons())
{
// Calculate exon location
int xOffset = getBoundedAndScaledLocation(exon.getLeft());
// Draw the exon
int width = getScaledWidthInPixels(getDisplayBoundedValue(exon.getLeft()),
getDisplayBoundedValue(exon.getRight()));
SvgRect svgExon = geneGroup.addRect(new Rectangle(new Point(xOffset, mLineY - (exon.getStrand().equals(Strand.FORWARD) ? mGeneHeight.toInt(GfxUnits.pixels) : 0)),
new Dimension(width, mGeneHeight.toInt(GfxUnits.pixels))));
Color color = (exon.getColor() != null ? exon.getColor() : DEFAULT_COLOR);
if (exon.getColor() != null)
{
color = exon.getColor();
}
svgExon.setFill(color);
// TooltipJS tooltip = new TooltipJS();
// tooltip.addTooltip(svgExon, getTooltipContent(gene));
}
}
else if (gene.getLocation() != null)
{
int xOffset = getBoundedAndScaledLocation(gene.getLocation().toIntRange().getStart());
int width = getScaledWidthInPixels(getDisplayBoundedValue(gene.getLocation().toIntRange().getStart()),
getDisplayBoundedValue(gene.getLocation().toIntRange().getEnd()));
int geneY = mLineY - (gene.getLocation().getStrand().equals(Strand.FORWARD) ? mGeneHeight.toInt(GfxUnits.pixels) : 0);
SvgRect svgExon = geneGroup.addRect(new Rectangle(new Point(xOffset, geneY),
new Dimension(width, mGeneHeight.toInt(GfxUnits.pixels))));
Color color = (gene.getColor() != null ? gene.getColor() : DEFAULT_COLOR);
if (gene.getColor() != null)
{
color = gene.getColor();
}
svgExon.setFill(color);
if (mDisplayLabels)
{
Rectangle bbox = TextUtil.getStringRect(gene.getId(), mLabelFont);
int labelX = (int) (xOffset + (width + lineHeight/2.0)/2.0);
int labelY = geneY + (gene.getLocation().getStrand().equals(Strand.FORWARD) ? -5 : mGeneHeight.toInt(GfxUnits.pixels) + bbox.width + 5);
int rotation = 360 - 90;
String labelTransform = "rotate(" + SVG.formatCoordinate(rotation) + " " + SVG.formatCoordinate(labelX) + " " + SVG.formatCoordinate(labelY) + ")";
SvgText label = geneGroup.addText(gene.getId(), mLabelFont, new Point(labelX, labelY))
.setTransform(labelTransform)
.setClass("gene_label");
}
}
TooltipJS tooltip = new TooltipJS();
tooltip.addTooltip(geneGroup, getTooltipContent(gene));
}
}
return genesGroup;
}
//--------------------------------------------------------------------------
private int getScaledWidthInPixels(int inStart, int inEnd)
{
int leftPixel = (int) (inStart * mXScalingFactor);
int rightPixel = (int) (inEnd * mXScalingFactor);
int width = rightPixel - leftPixel + 1;
// System.out.println("Exon: " + inStart + "-" + inEnd + " " + leftPixel + "-" + rightPixel + " " + mXScalingFactor + " (" + width + ")");/////////////////////////
// int width = Math.abs((int) ((inEnd - inStart + 1) * mXScalingFactor));
// Minimum width of 1
return (0 >= width ? 1 : width);
}
//--------------------------------------------------------------------------
private int getBoundedAndScaledLocation(int inValue)
{
int bound = (int) (getDisplayBoundedValue(inValue) * mXScalingFactor);
int start = (int) (mDisplayRange.getStart() * mXScalingFactor);
return bound - start;
// return (int)((getDisplayBoundedValue(inValue) - mDisplayStart) * mXScalingFactor);
}
//--------------------------------------------------------------------------
private int getDisplayBoundedValue(int inValue)
{
int value = inValue;
if (value < mDisplayRange.getStart())
{
value = mDisplayRange.getStart();
}
else if (value > mDisplayRange.getEnd())
{
value = mDisplayRange.getEnd();
}
return value;
}
//--------------------------------------------------------------------------
public String getTooltipContent(Gene inGene)
{
StringBuilderPlus text = new StringBuilderPlus().setDelimiter("
");
if (StringUtil.isSet(inGene.getId())
&& (null == inGene.getDescription()
|| inGene.getDescription().indexOf(inGene.getId()) < 0))
{
text.append(inGene.getId());
}
if (inGene.getDescription() != null)
{
text.delimitedAppend(inGene.getDescription());
}
text.delimitedAppend("");
if (inGene.getStrand() != null)
{
text.append("Strand: (");
text.append(inGene.getStrand().getSymbol());
text.append(") ");
}
text.append("bp.: ");
text.append(inGene.getLeft());
text.append(inGene.getLeft() != inGene.getRight() ?" - " + inGene.getRight() : "");
text.append("");
return text.toString();
}
//--------------------------------------------------------------------------
public SvgGroup generateXAxis()
{
SvgGroup scalegroup = new SvgGroup().setClass("scale").addStyle(CSS.fontSize(mScaleFont.getSize()) + CSSProperty.font_family + "=" + mScaleFont.getFamily());
Point contigLineStart = getLineStart();
double scaleLineY = contigLineStart.getY() + mMaxRevLabelLength + 50;
Point2D scaleLineStart = new Point2D.Double(contigLineStart.getX(), scaleLineY);
Point2D scaleLineEnd = new Point2D.Double(getLineEnd().getX(), scaleLineY);
// Axis
scalegroup.addLine(scaleLineStart, scaleLineEnd).setClass("axis");
NumberFormat commaFormatter = NumberFormat.getInstance();
commaFormatter.setGroupingUsed(true);
determineTickSize();
int yOffset;
for (Integer tickValue : getTickValues())
{
if (tickValue%mMajorTickStep == 0
|| tickValue == mDisplayRange.getStart()
|| tickValue == mDisplayRange.getEnd())
{
yOffset = sMajorTickHeight;
String label = commaFormatter.format(tickValue);
TextLayout layout = new TextLayout(label, mScaleFont, sFRC);
double xTranslation = ((tickValue - mDisplayRange.getStart() + 1) * mXScalingFactor)
+ layout.getBounds().getHeight() / 2;
double yTranslation = yOffset + layout.getAdvance() + 1;
double x = scaleLineStart.getX() + xTranslation;
double y = scaleLineStart.getY() + yTranslation + 5;
scalegroup.addText(label, null, new Point2D.Double(x, y)).setTransform("rotate(-90, " + x + ", " + y + ")").setClass("tickLabel");
}
else if (tickValue%mMinorTickStep == 0)
{
yOffset = sMinorTickHeight;
}
else
{
continue;
}
int xOffset = (int) ((tickValue - mDisplayRange.getStart()) * mXScalingFactor) + 1;
scalegroup.addLine(new Point2D.Double(xOffset, scaleLineStart.getY()), new Point2D.Double(xOffset, scaleLineStart.getY() + yOffset)).setClass("tick");
}
return scalegroup;
}
//--------------------------------------------------------------------------
private void determineTickSize()
{
int xAxisWidth = mDisplayRange.length().intValue();
String widthString = xAxisWidth + "";
mMajorTickStep = (int) Math.pow(10, widthString.length() - 1);
if (0 == mMajorTickStep%xAxisWidth
|| mMajorTickStep > 0.5 * xAxisWidth)
{
mMajorTickStep = mMajorTickStep / 10;
}
mMinorTickStep = mMajorTickStep / 10;
}
//--------------------------------------------------------------------------
private List getTickValues()
{
List tickValues = new ArrayList<>();
// First add the major ticks
tickValues.add(mDisplayRange.getStart());
for (int tickValue = (int) (Math.ceil(1 / mMajorTickStep) * mMajorTickStep);
tickValue < mDisplayRange.getEnd();
tickValue += mMajorTickStep)
{
if (tickValue > mDisplayRange.getStart())
{
tickValues.add(tickValue);
}
}
tickValues.add(mDisplayRange.getEnd());
// Now add the minor ticks
for (int tickValue = (int) (Math.ceil(mDisplayRange.getStart() / mMinorTickStep) * mMinorTickStep);
tickValue < mDisplayRange.getEnd();
tickValue += mMinorTickStep)
{
if (tickValue%mMajorTickStep != 0
&& tickValue != mDisplayRange.getStart())
{
tickValues.add(tickValue);
}
}
return tickValues;
}
}