package beast.app.beauti;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.io.PrintWriter;
import java.io.StringWriter;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.UIManager;
import org.apache.commons.math.MathException;
import beast.app.draw.BEASTObjectInputEditor;
import beast.core.BEASTInterface;
import beast.core.Input;
import beast.evolution.tree.TreeDistribution;
import beast.math.distributions.MRCAPrior;
import beast.math.distributions.ParametricDistribution;
public class ParametricDistributionInputEditor extends BEASTObjectInputEditor {
public ParametricDistributionInputEditor(BeautiDoc doc) {
super(doc);
}
private static final long serialVersionUID = 1L;
boolean useDefaultBehavior;
boolean mayBeUnstable;
@Override
public Class<?> type() {
//return ParametricDistributionInputEditor.class;
return ParametricDistribution.class;
}
@Override
public void init(Input<?> input, BEASTInterface beastObject, int itemNr, ExpandOption isExpandOption, boolean addButtons) {
useDefaultBehavior = !((beastObject instanceof beast.math.distributions.Prior) || beastObject instanceof MRCAPrior || beastObject instanceof TreeDistribution);
// if (useDefaultBehavior && false) {
// super.init(input, beastObject, isExpandOption, addButtons);
// } else {
m_bAddButtons = addButtons;
m_input = input;
m_beastObject = beastObject;
this.itemNr = itemNr;
if (input.get() != null) {
super.init(input, beastObject, itemNr, ExpandOption.TRUE, addButtons);
}
add(createGraph());
// }
} // init
@Override
/** suppress combobox **/
protected void addComboBox(JComponent box, Input<?> input, BEASTInterface beastObject) {
if (useDefaultBehavior) {
super.addComboBox(box, input, beastObject);
}
}
@Override
/** suppress input label**/
protected void addInputLabel() {
if (useDefaultBehavior) {
super.addInputLabel();
}
}
/**
* maps most significant digit to nr of ticks on graph *
*/
final static int[] NR_OF_TICKS = new int[]{5, 10, 8, 6, 8, 10, 6, 7, 8, 9, 10};
/* class for drawing information for a parametric distribution **/
class PDPanel extends JPanel {
// the length in pixels of a tick
private static final int TICK_LENGTH = 5;
// the right margin
private static final int RIGHT_MARGIN = 20;
// the margin to the left of y-labels
private static final int MARGIN_LEFT_OF_Y_LABELS = 5;
// the top margin
private static final int TOP_MARGIN = 10;
int m_nTicks;
private static final long serialVersionUID = 1L;
@Override
public void paintComponent(java.awt.Graphics g) {
Graphics2D g2d = (Graphics2D)g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
final int width = getWidth();
final int height = getHeight();
final int labeloffset = 50;
ParametricDistribution m_distr = (ParametricDistribution)m_input.get();
if (m_distr == null) {
return;
}
try {
m_distr.initAndValidate();
} catch (Exception e1) {
// ignore
}
double minValue = 0.1;
double maxValue = 1;
try {
minValue = m_distr.inverseCumulativeProbability(0.01);
maxValue = m_distr.inverseCumulativeProbability(0.99);
} catch (Exception e) {
// use defaults
}
double xRange = maxValue - minValue;
// adjust yMax so that the ticks come out right
double x0 = minValue;
int k = 0;
double f = xRange;
double f2 = x0;
while (f > 10) {
f /= 10;
f2 /= 10;
k++;
}
while (f < 1 && f > 0) {
f *= 10;
f2 *= 10;
k--;
}
f = Math.ceil(f);
f2 = Math.floor(f2);
// final int NR_OF_TICKS_X = NR_OF_TICKS[(int) f];
for (int i = 0; i < k; i++) {
f *= 10;
f2 *= 10;
}
for (int i = k; i < 0; i++) {
f /= 10;
f2 /= 10;
}
//double adjXRange = f;
xRange = xRange + minValue - f2;
xRange = adjust(xRange);
final int NR_OF_TICKS_X = m_nTicks;
minValue = f2; //xRange = adjXRange;
int points;
if (!m_distr.isIntegerDistribution()) {
points = 100;
} else {
points = (int) (xRange);
}
int[] xPoints = new int[points];
int[] yPoints = new int[points];
double[] fyPoints = new double[points];
double yMax = 0;
for (int i = 0; i < points; i++) {
//try {
fyPoints[i] = getDensityForPlot(m_distr, minValue + (xRange * i) / points);
//}
if (Double.isInfinite(fyPoints[i]) || Double.isNaN(fyPoints[i])) {
fyPoints[i] = 0;
}
//fyPoints[i] = Math.exp(m_distr.logDensity(minValue + (xRange * i)/points));
yMax = Math.max(yMax, fyPoints[i]);
}
yMax = adjust(yMax);
final int NR_OF_TICKS_Y = m_nTicks;
// draw ticks on edge
Font font = g.getFont();
Font smallFont = new Font(font.getName(), font.getStyle(), font.getSize() * 2/3);
g.setFont(smallFont);
// collect the ylabels and the maximum label width in small font
String[] ylabels = new String[NR_OF_TICKS_Y+1];
int maxLabelWidth = 0;
FontMetrics sfm = getFontMetrics(smallFont);
for (int i = 0; i <= NR_OF_TICKS_Y; i++) {
ylabels[i] = format(yMax * i / NR_OF_TICKS_Y);
int stringWidth = sfm.stringWidth(ylabels[i]);
if (stringWidth > maxLabelWidth) maxLabelWidth = stringWidth;
}
// collect the xlabels
String[] xlabels = new String[NR_OF_TICKS_X+1];
for (int i = 0; i <= NR_OF_TICKS_X; i++) {
xlabels[i] = format(minValue + xRange * i / NR_OF_TICKS_X);
}
int maxLabelHeight = sfm.getMaxAscent()+sfm.getMaxDescent();
int leftMargin = maxLabelWidth + TICK_LENGTH + 1 + MARGIN_LEFT_OF_Y_LABELS;
int bottomMargin = maxLabelHeight + TICK_LENGTH + 1;
int graphWidth = width - leftMargin - RIGHT_MARGIN;
int graphHeight = height - TOP_MARGIN - bottomMargin - labeloffset;
// DRAW GRAPH PAPER
g.setColor(Color.WHITE);
g.fillRect(leftMargin, TOP_MARGIN, graphWidth, graphHeight);
g.setColor(Color.BLACK);
g.drawRect(leftMargin, TOP_MARGIN, graphWidth, graphHeight);
for (int i = 0; i < points; i++) {
xPoints[i] = leftMargin + graphWidth * i / points;
yPoints[i] = 1 + (int) (TOP_MARGIN + graphHeight - graphHeight * fyPoints[i] / yMax);
}
if (!m_distr.isIntegerDistribution()) {
g.drawPolyline(xPoints, yPoints, points);
} else {
int y0 = 1 + TOP_MARGIN + graphHeight;
int dotDiameter = graphHeight/20;
for (int i=0; i<points; i++) {
g.drawLine(xPoints[i], y0, xPoints[i], yPoints[i]);
g.fillOval(xPoints[i]-dotDiameter/2, yPoints[i]-dotDiameter/2, dotDiameter, dotDiameter);
}
}
for (int i = 0; i <= NR_OF_TICKS_X; i++) {
int x = leftMargin + i * graphWidth / NR_OF_TICKS_X;
g.drawLine(x, TOP_MARGIN + graphHeight, x, TOP_MARGIN + graphHeight + TICK_LENGTH);
g.drawString(xlabels[i], x-sfm.stringWidth(xlabels[i])/2, TOP_MARGIN + graphHeight + TICK_LENGTH + 1 + sfm.getMaxAscent());
}
// draw the y labels and ticks
for (int i = 0; i <= NR_OF_TICKS_Y; i++) {
int y = TOP_MARGIN + graphHeight - i * graphHeight / NR_OF_TICKS_Y;
g.drawLine(leftMargin - TICK_LENGTH, y, leftMargin, y);
g.drawString(ylabels[i], leftMargin - TICK_LENGTH - 1 - sfm.stringWidth(ylabels[i]), y + 3);
}
int fontHeight = font.getSize() * 10 / 12;
g.setFont(new Font(font.getName(), font.getStyle(), fontHeight));
try {
FontMetrics fontMetrics = g.getFontMetrics();
String[] strs = new String[]{"2.5% Quantile", "5% Quantile", "Median", "95% Quantile", "97.5% Quantile"};
Double[] quantiles = new Double[]{0.025, 0.05, 0.5, 0.95, 0.975};
mayBeUnstable = false;
for (k = 0; k < 5; k++) {
int y = TOP_MARGIN + graphHeight + bottomMargin + g.getFontMetrics().getMaxAscent() + k * fontHeight;
try {
g.drawString(format(m_distr.inverseCumulativeProbability(quantiles[k])), graphWidth / 2 + leftMargin, y);
} catch (MathException e) {
g.drawString("not available", graphWidth / 2 + leftMargin, y);
}
g.drawString(strs[k], graphWidth / 2 - fontMetrics.stringWidth(strs[k]) + leftMargin - fontHeight, y);
}
if (mayBeUnstable) {
int x = graphWidth * 3/ 4 + leftMargin; int y =TOP_MARGIN + graphHeight + bottomMargin + fontHeight;
g.drawString("* numbers", x, y + 2*fontHeight);
g.drawString("may not be", x, y + 3*fontHeight);
g.drawString("accurate", x, y + 4*fontHeight);
}
try {
g.drawString("mean " + format(m_distr.getMean()), graphWidth * 3/ 4 + leftMargin, TOP_MARGIN + graphHeight + bottomMargin + fontHeight);
} catch (RuntimeException e) {
// catch in case it is not implemented.
}
} catch (Exception e) {
// probably something wrong with the parameters of the parametric distribution
g.drawString("Improper parameters", leftMargin, TOP_MARGIN + graphHeight + bottomMargin + g.getFontMetrics().getMaxAscent());
}
}
private String format(double value) {
StringWriter writer = new StringWriter();
PrintWriter pw = new PrintWriter(writer);
pw.printf("%.3g", value);
if (value != 0.0 && Math.abs(value) / 1000 < 1e-320) { // 2e-6 = 2 * AbstractContinuousDistribution.solverAbsoluteAccuracy
mayBeUnstable = true;
pw.printf("*");
}
pw.flush();
return writer.toString();
}
private double adjust(double yMax) {
// adjust yMax so that the ticks come out right
int k = 0;
double y = yMax;
while (y > 10) {
y /= 10;
k++;
}
while (y < 1 && y > 0) {
y *= 10;
k--;
}
y = Math.ceil(y);
m_nTicks = NR_OF_TICKS[(int) y];
for (int i = 0; i < k; i++) {
y *= 10;
}
for (int i = k; i < 0; i++) {
y /= 10;
}
return y;
}
}
/**
* Returns the density of pDistr at x when pDistr is a density of a
* continuous variable, but returns the probability of the closest
* integer when pDistr is a probability distribution over an integer-valued
* parameter.
*
* @param pDistr
* @param x
* @return density at x or probability of closest integer to x
*/
private double getDensityForPlot(ParametricDistribution pDistr, double x) {
if (pDistr.isIntegerDistribution()) {
return pDistr.density((int) Math.round(x));
} else {
return pDistr.density(x);
}
}
private Component createGraph() {
JPanel panel = new PDPanel();
int fsize = UIManager.getFont("Label.font").getSize();
Dimension size = new Dimension(200 * fsize / 13, 200 * fsize / 13);
panel.setSize(size);
panel.setPreferredSize(size);
panel.setMinimumSize(size);
Box box = Box.createHorizontalBox();
box.setBorder(BorderFactory.createEmptyBorder());
box.add(panel);
return box;
}
@Override
public void validate() {
super.validate();
repaint();
}
}