/* * 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.cross.gui; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import org.jax.analyticgraph.framework.AbstractGraph2DWithAxes; import org.jax.analyticgraph.framework.GraphCoordinateConverter; import org.jax.analyticgraph.framework.SimpleGraphCoordinateConverter; import org.jax.analyticgraph.graph.AxisDescription; import org.jax.analyticgraph.graph.CategoricalAxisDescription; import org.jax.analyticgraph.graph.RegularIntervalAxisDescription; import org.jax.analyticgraph.graph.AxisDescription.AxisType; /** * For rendering effect plots * @author <A HREF="mailto:keith.sheppard@jax.org">Keith Sheppard</A> */ public class EffectPlot extends AbstractGraph2DWithAxes { /** * the colors that we can cycle through for drawing multiple lines */ private static final Color[] AVAILABLE_COLORS = new Color[] { Color.BLUE, Color.BLACK, Color.ORANGE, Color.CYAN, Color.GREEN, Color.GRAY, Color.PINK, Color.YELLOW, Color.LIGHT_GRAY}; private static final int DEFAULT_Y_AXIS_TICK_INTERVAL_SIGNIFICANT_DIGITS = 2; private static final int DEFAULT_Y_AXIS_TICK_COUNT = 10; /** * A data point for the effect plot */ public static class EffectPlotDataPoint { private final double value; private final double standardDeviation; /** * Constructor * @param value * see {@link #getValue()} * @param standardDeviation * see {@link #getStandardDeviation()} */ public EffectPlotDataPoint(double value, double standardDeviation) { this.value = value; this.standardDeviation = standardDeviation; } /** * Getter for the value * @return the value */ public double getValue() { return this.value; } /** * Getter for the standard deviation * @return the standardDeviation */ public double getStandardDeviation() { return this.standardDeviation; } /** * Get the low point of the effect bar * @return * the low point */ public double getLowerStandardDeviationBarPosition() { return this.value - this.standardDeviation; } /** * Get the high point on the effect bar * @return * the high point */ public double getUpperStandardDeviationBarPosition() { return this.value + this.standardDeviation; } } /** * The effect plot data. */ public static class EffectPlotData { private final EffectPlotDataPoint[][] effectLines; private final String[] effectLineNames; private final String[] effectPointNames; private final String xAxisName; private final String yAxisName; private final String effectLinesGroupingName; /** * Constructor * @param xAxisName * the x axis name * @param yAxisName * the y axis name * @param effectPointNames * the names of the effect points * @param effectLine * the effect line data */ public EffectPlotData( String xAxisName, String yAxisName, String[] effectPointNames, EffectPlotDataPoint[] effectLine) { this.xAxisName = xAxisName; this.yAxisName = yAxisName; this.effectLinesGroupingName = ""; this.effectPointNames = effectPointNames; this.effectLines = new EffectPlotDataPoint[][] {effectLine}; this.effectLineNames = new String[0]; } /** * Constructor * @param xAxisName * the x axis name * @param yAxisName * the y axis name * @param effectLinesGroupingName * the effect lines grouping name * @param effectPointNames * see {@link #effectPointNames} * @param effectLines * see {@link #getEffectLines()} * @param effectLineNames * see {@link #getEffectLineNames()} */ public EffectPlotData( String xAxisName, String yAxisName, String effectLinesGroupingName, String[] effectPointNames, EffectPlotDataPoint[][] effectLines, String[] effectLineNames) { this.xAxisName = xAxisName; this.yAxisName = yAxisName; this.effectLinesGroupingName = effectLinesGroupingName; this.effectPointNames = effectPointNames; this.effectLines = effectLines; this.effectLineNames = effectLineNames; } /** * Getter for the effect lines * @return * the effect lines */ public EffectPlotDataPoint[][] getEffectLines() { return this.effectLines; } /** * Getter for the effect line names * @return * the effect line names */ public String[] getEffectLineNames() { return this.effectLineNames; } /** * Getter for the effect point names * @return * the effect point names */ public String[] getEffectPointNames() { return this.effectPointNames; } /** * @return the xAxisName */ public String getXAxisName() { return this.xAxisName; } /** * @return the yAxisName */ public String getYAxisName() { return this.yAxisName; } /** * @return the effectLinesGroupingName */ public String getEffectLinesGroupingName() { return this.effectLinesGroupingName; } } private final EffectPlotData effectPlotData; private final CategoricalAxisDescription xAxisDescription; private final AxisDescription yAxisDescription; /** * The default size in pixles that we use for the cap on the effect * bars */ public static final int DEFAULT_EFFECT_BAR_CAP_SIZE_IN_PIXELS = 6; /** * @see #getEffectBarCapSizeInPixels() */ private volatile int effectBarCapSizeInPixels = DEFAULT_EFFECT_BAR_CAP_SIZE_IN_PIXELS; /** * Constructor * @param effectPlotData * the data to plot */ public EffectPlot( EffectPlotData effectPlotData) { super(new SimpleGraphCoordinateConverter()); this.effectPlotData = effectPlotData; double minYRange = EffectPlot.getMinimumValueMinusStandardDeviation( effectPlotData.getEffectLines()); double maxYRange = EffectPlot.getMaximumValuePlusStandardDeviation( effectPlotData.getEffectLines()); this.getGraphCoordinateConverter().updateGraphDimensions( 0.0, minYRange, 1.0, maxYRange - minYRange); this.xAxisDescription = new CategoricalAxisDescription( this.getGraphCoordinateConverter(), AxisType.X_AXIS, effectPlotData.getXAxisName(), effectPlotData.getEffectPointNames()); this.yAxisDescription = new RegularIntervalAxisDescription( this.getGraphCoordinateConverter(), AxisType.Y_AXIS, effectPlotData.getYAxisName(), DEFAULT_Y_AXIS_TICK_COUNT, DEFAULT_Y_AXIS_TICK_INTERVAL_SIGNIFICANT_DIGITS, true); } /** * Getter for the effect bar cap size in pixels * @return the effectBarCapSizeInPixels */ public int getEffectBarCapSizeInPixels() { return this.effectBarCapSizeInPixels; } /** * Setter for the effect bar cap size in pixels. If this is not a * multiple of 2 it will be treated as if it's the next lower multiple * of two (Eg: 9 gets treated like 8). * @param effectBarCapSizeInPixels the effectBarCapSizeInPixels to set */ public void setEffectBarCapSizeInPixels(int effectBarCapSizeInPixels) { this.effectBarCapSizeInPixels = effectBarCapSizeInPixels; } /** * This is the opposite of * {@link #getMinimumValueMinusStandardDeviation(org.jax.qtl.cross.gui.EffectPlot.EffectPlotDataPoint[][])} * @param effectLines * the effect lines we're checking * @return * the maximum value */ private static double getMaximumValuePlusStandardDeviation( EffectPlotDataPoint[][] effectLines) { double max = effectLines[0][0].getUpperStandardDeviationBarPosition(); for(EffectPlotDataPoint[] currEffectLine: effectLines) { for(EffectPlotDataPoint currEffectPoint: currEffectLine) { double currUpperBarPosition = currEffectPoint.getUpperStandardDeviationBarPosition(); if(currUpperBarPosition > max) { max = currUpperBarPosition; } } } return max; } /** * Basically this gets the lowest point that the error bar will dip to * @param effectLines * the effect lines we're checking * @return * the minimum value */ private static double getMinimumValueMinusStandardDeviation( EffectPlotDataPoint[][] effectLines) { double min = effectLines[0][0].getLowerStandardDeviationBarPosition(); for(EffectPlotDataPoint[] currEffectLine: effectLines) { for(EffectPlotDataPoint currEffectPoint: currEffectLine) { double currLowerBarPosition = currEffectPoint.getLowerStandardDeviationBarPosition(); if(currLowerBarPosition < min) { min = currLowerBarPosition; } } } return min; } /** * Getter for the effect plot data that this class is rendering * @return * the data */ public EffectPlotData getEffectPlotData() { return this.effectPlotData; } /** * {@inheritDoc} */ public void renderGraph(Graphics2D graphics2D) { // save the current color Color saveColor = graphics2D.getColor(); EffectPlotData effectPlotData = this.effectPlotData; EffectPlotDataPoint[][] effectLines = effectPlotData.getEffectLines(); for(int currLineIndex = 0; currLineIndex < effectLines.length; currLineIndex++) { graphics2D.setColor( AVAILABLE_COLORS[currLineIndex % AVAILABLE_COLORS.length]); EffectPlotDataPoint prevPoint = null; double prevPointX = 0; EffectPlotDataPoint[] currEffectLine = effectLines[currLineIndex]; for(int currPointIndex = 0; currPointIndex < currEffectLine.length; currPointIndex++) { EffectPlotDataPoint currPoint = currEffectLine[currPointIndex]; double pointX = this.xAxisDescription.getCategoryAxisPosition( currPointIndex); // render the current effect point and bars this.renderLine( graphics2D, pointX, currPoint.getUpperStandardDeviationBarPosition(), pointX, currPoint.getLowerStandardDeviationBarPosition()); this.renderPoint( graphics2D, pointX, currPoint.getValue()); // render the caps this.renderCapLine( graphics2D, pointX, currPoint.getUpperStandardDeviationBarPosition()); this.renderCapLine( graphics2D, pointX, currPoint.getLowerStandardDeviationBarPosition()); if(prevPoint != null) { // connect the center of this effect bar to the previous // effect bar this.renderLine( graphics2D, prevPointX, prevPoint.getValue(), pointX, currPoint.getValue()); } // remember the point data for the next iteration (if there // is one) prevPoint = currPoint; prevPointX = pointX; } } // render the key only if we have more than one on the graph String[] effectLineNames = effectPlotData.getEffectLineNames(); if(effectLineNames != null && effectLineNames.length > 1) { this.renderKey( graphics2D, effectPlotData.getEffectLinesGroupingName(), effectLineNames, effectPlotData.getEffectPointNames().length); } // restore the color graphics2D.setColor(saveColor); } /** * this font name comes with {@link java.awt.Font} as of java 6.0, * but we need to be java 5.0 compatible */ // TODO for now this is OK, but get rid of this when we move to 6.0 private static final String SANS_SERIF_FONT_NAME = "SansSerif"; private static final Font KEY_LABEL_FONT = new Font( SANS_SERIF_FONT_NAME, Font.PLAIN, 10); private static final Font KEY_GROUP_FONT = new Font( SANS_SERIF_FONT_NAME, Font.PLAIN, 14); private static final int KEY_BUFFER_PIXELS = 3; private static final int KEY_LINE_LENGTH_PIXELS = 20; /** * Render the key using the given line names * @param graphics2D * the graphics context to render to * @param effectLineNames * the line names in the key */ private void renderKey( Graphics2D graphics2D, String effectLineGroupName, String[] effectLineNames, int numEffectPoints) { GraphCoordinateConverter coordConverter = this.getGraphCoordinateConverter(); graphics2D.setColor(Color.BLACK); FontRenderContext frc = graphics2D.getFontRenderContext(); GlyphVector groupLabelGlyph = KEY_GROUP_FONT.createGlyphVector( frc, effectLineGroupName); // figure out the placement of the key double maxWidthPixels = 0.0; double cumulativeHeightPixels = 0.0; GlyphVector[] glyphVectors = new GlyphVector[effectLineNames.length]; for(int lineIndex = 0; lineIndex < effectLineNames.length; lineIndex++) { String currEffectLineName = effectLineNames[lineIndex]; GlyphVector glyphVector = KEY_LABEL_FONT.createGlyphVector(frc, currEffectLineName); glyphVectors[lineIndex] = glyphVector; Rectangle2D currBounds = glyphVector.getLogicalBounds(); cumulativeHeightPixels += currBounds.getHeight(); if(currBounds.getWidth() > maxWidthPixels) { maxWidthPixels = currBounds.getWidth(); } } double keyWidthPixels = KEY_BUFFER_PIXELS + maxWidthPixels + KEY_BUFFER_PIXELS + KEY_LINE_LENGTH_PIXELS + KEY_BUFFER_PIXELS; double keyHeightPixels = cumulativeHeightPixels + (2 * effectLineNames.length); // center the key between the 1st and last points double lastPosition = this.xAxisDescription.getCategoryAxisPosition( numEffectPoints - 1); double secondToLastPosition = this.xAxisDescription.getCategoryAxisPosition( numEffectPoints - 2); double keyCenterXGraph = (lastPosition + secondToLastPosition) / 2.0; double keyCenterXPixel = coordConverter.convertGraphXCoordinateToJava2DXCoordinate( keyCenterXGraph); double keyLeftXPixel = keyCenterXPixel - (keyWidthPixels / 2.0); // draw the group label double yPixelCursor = coordConverter.getAbsoluteYOffsetInPixels() + KEY_BUFFER_PIXELS; { Rectangle2D groupLogicalBounds = groupLabelGlyph.getLogicalBounds(); AffineTransform at = new AffineTransform(); at.translate( keyLeftXPixel, yPixelCursor + groupLogicalBounds.getHeight()); Shape groupLabel = at.createTransformedShape(groupLabelGlyph.getOutline()); graphics2D.fill(groupLabel); yPixelCursor += groupLabelGlyph.getLogicalBounds().getHeight() + KEY_BUFFER_PIXELS; } // iterate through the names double keyTopYPixels = yPixelCursor; // draw the box for the key Rectangle2D.Double keyRectangle = new Rectangle2D.Double( keyLeftXPixel, keyTopYPixels, keyWidthPixels, keyHeightPixels); graphics2D.setColor(Color.WHITE); graphics2D.fill(keyRectangle); graphics2D.setColor(Color.BLACK); graphics2D.draw(keyRectangle); for(int lineIndex = 0; lineIndex < glyphVectors.length; lineIndex++) { graphics2D.setColor(Color.BLACK); double currLeftMargin = keyLeftXPixel + KEY_BUFFER_PIXELS; GlyphVector currGlyph = glyphVectors[lineIndex]; Rectangle2D currLogicalBounds = currGlyph.getLogicalBounds(); AffineTransform at = new AffineTransform(); at.translate( currLeftMargin, yPixelCursor + currLogicalBounds.getHeight()); Shape currLabel = at.createTransformedShape( currGlyph.getOutline()); graphics2D.fill(currLabel); currLeftMargin += maxWidthPixels + KEY_BUFFER_PIXELS; graphics2D.setColor( AVAILABLE_COLORS[lineIndex % AVAILABLE_COLORS.length]); graphics2D.draw(new Line2D.Double( currLeftMargin, yPixelCursor + (currLogicalBounds.getHeight() / 2.0), currLeftMargin + KEY_LINE_LENGTH_PIXELS, yPixelCursor + (currLogicalBounds.getHeight() / 2.0))); yPixelCursor += currLogicalBounds.getHeight() + KEY_BUFFER_PIXELS; } } /** * Render one of the caps that sits at either end of the effect bar. * @param graphics2D * the graphics context * @param capPositionGraphX * the x position of the cap * @param capPositionGraphY * the y position of the cap */ private void renderCapLine( Graphics2D graphics2D, double capPositionGraphX, double capPositionGraphY) { GraphCoordinateConverter coordConverter = this.getGraphCoordinateConverter(); double capPositionPixleX = coordConverter.convertGraphXCoordinateToJava2DXCoordinate( capPositionGraphX); double capPositionPixleY = coordConverter.convertGraphYCoordinateToJava2DYCoordinate( capPositionGraphY); graphics2D.draw(new Line2D.Double( capPositionPixleX - this.effectBarCapSizeInPixels, capPositionPixleY, capPositionPixleX + this.effectBarCapSizeInPixels, capPositionPixleY)); } private void renderLine( Graphics2D graphics2D, double graphX1, double graphY1, double graphX2, double graphY2) { GraphCoordinateConverter coordConverter = this.getGraphCoordinateConverter(); double pixleX1 = coordConverter.convertGraphXCoordinateToJava2DXCoordinate( graphX1); double pixleY1 = coordConverter.convertGraphYCoordinateToJava2DYCoordinate( graphY1); double pixleX2 = coordConverter.convertGraphXCoordinateToJava2DXCoordinate( graphX2); double pixleY2 = coordConverter.convertGraphYCoordinateToJava2DYCoordinate( graphY2); graphics2D.draw(new Line2D.Double( pixleX1, pixleY1, pixleX2, pixleY2)); } /** * {@inheritDoc} */ public CategoricalAxisDescription getXAxisDescription() { return this.xAxisDescription; } /** * {@inheritDoc} */ public AxisDescription getYAxisDescription() { return this.yAxisDescription; } }