/***********************************************************************************************
* Copyright 2002 (C) Nathaniel G. Auvil. All Rights Reserved.
*
* Redistribution and use of this software and associated documentation ("Software"), with or
* without modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain copyright statements and notices.
* Redistributions must also contain a copy of this document.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. The name "jCharts" or "Nathaniel G. Auvil" must not be used to endorse or promote
* products derived from this Software without prior written permission of Nathaniel G.
* Auvil. For written permission, please contact nathaniel_auvil@users.sourceforge.net
*
* 4. Products derived from this Software may not be called "jCharts" nor may "jCharts" appear
* in their names without prior written permission of Nathaniel G. Auvil. jCharts is a
* registered trademark of Nathaniel G. Auvil.
*
* 5. Due credit should be given to the jCharts Project (http://jcharts.sourceforge.net/).
*
* THIS SOFTWARE IS PROVIDED BY Nathaniel G. Auvil AND CONTRIBUTORS ``AS IS'' AND ANY
* EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* jCharts OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
* IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
************************************************************************************************/
package org.krysalis.jcharts;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import org.krysalis.jcharts.Chart;
import org.krysalis.jcharts.chartData.interfaces.IAxisDataSeries;
import org.krysalis.jcharts.chartData.interfaces.IAxisPlotDataSet;
import org.krysalis.jcharts.chartData.interfaces.IData;
import org.krysalis.jcharts.chartData.interfaces.IPieChartDataSet;
import org.krysalis.jcharts.chartData.processors.TextProcessor;
import org.krysalis.jcharts.properties.LegendAreaProperties;
import org.krysalis.jcharts.properties.LegendProperties;
import org.krysalis.jcharts.properties.LineChartProperties;
import org.krysalis.jcharts.properties.PointChartProperties;
import org.krysalis.jcharts.test.HTMLGenerator;
import org.krysalis.jcharts.test.HTMLTestable;
import org.krysalis.jcharts.types.ChartType;
/*************************************************************************************
*
* @author Nathaniel Auvil, Sandor Dornbush, Sundar Balasubramanian
* @version $Id: Legend.java,v 1.10 2004/05/31 16:26:13 nathaniel_auvil Exp $
************************************************************************************/
final public class Legend implements HTMLTestable, Serializable
{
private Chart chart;
private LegendProperties legendProperties;
private float iconSide;
//---derived values
private float widestLabelAndColumnPadding;
private int numColumns;
private int numRows;
private TextProcessor textProcessor;
private float x;
private float y;
private float width = 0;
private float height = 0;
//---used to extract the legendLabels and paints from the data set and make them easy to loop through
private ArrayList labels;
private ArrayList paints;
private ArrayList shapes = new ArrayList();
private ArrayList fillPointsFlags = new ArrayList();
private ArrayList pointOutlinePaints = new ArrayList();
private ChartType chartType;
private PointChartProperties pointChartProperties;
private LineChartProperties lineChartProperties;
/*********************************************************************************************
*
* @param chart
* @deprecated
**********************************************************************************************/
public Legend( Chart chart )
{
this.chart = chart;
}
/*********************************************************************************************
*
* @param chart
* @param legendProperties
**********************************************************************************************/
public Legend( Chart chart, LegendProperties legendProperties )
{
this.chart = chart;
this.legendProperties = legendProperties;
}
public void setX( float x )
{
this.x = x;
}
public void setY( float y )
{
this.y = y;
}
/*****************************************************************************************
*
* @param iAxisDataSeries
* @param chartTitleHeight
****************************************************************************************/
public void computeLegendXY( IAxisDataSeries iAxisDataSeries, float chartTitleHeight )
{
//---PROCESS the size needed for drawing the legend.
this.calculateDrawingValues( iAxisDataSeries );
if( (this.getLegendProperties().getPlacement() == LegendAreaProperties.RIGHT)
|| (this.getLegendProperties().getPlacement() == LegendAreaProperties.LEFT) )
{
if( this.getHeight() > this.chart.getImageHeight() - this.chart.getChartProperties().getEdgePadding() * 2 )
{
this.setY( this.chart.getChartProperties().getEdgePadding() );
}
else
{
this.setY( (this.chart.getImageHeight() / 2) - (this.getHeight() / 2) );
}
if( this.getLegendProperties().getPlacement() == LegendAreaProperties.RIGHT )
{
this.setX( this.chart.getImageWidth() - this.getWidth() - this.chart.getChartProperties().getEdgePadding() );
}
else //---else, LegendAreaProperties.LEFT
{
this.setX( this.chart.getChartProperties().getEdgePadding() );
}
}
else //---LegendAreaProperties.BOTTOM, OR LegendAreaProperties.TOP
{
if( this.getWidth() + this.chart.getChartProperties().getEdgePadding() * 2 > this.chart.getImageWidth() )
{
this.setX( this.chart.getChartProperties().getEdgePadding() );
}
else
{
this.setX( (this.chart.getImageWidth() / 2) - (this.getWidth() / 2) );
}
if( this.getLegendProperties().getPlacement() == LegendAreaProperties.BOTTOM )
{
this.setY( this.chart.getImageHeight() - this.getHeight() - this.chart.getChartProperties().getEdgePadding() );
}
else //---else, LegendAreaProperties.TOP
{
this.setY( this.chart.getChartProperties().getEdgePadding() + chartTitleHeight );
}
}
}
/**********************************************************************************************
* Central method for processing data; try to minimize looping.
* 1) calculate the maximum height of labels
* 2) find the maximum label width
*
* @param iAxisDataSeries
**********************************************************************************************/
private void processData( IAxisDataSeries iAxisDataSeries )
{
this.textProcessor = new TextProcessor();
Iterator iterator = iAxisDataSeries.getIAxisPlotDataSetIterator();
//LOOP
while( iterator.hasNext() )
{
this.processLegendLabels( (IAxisPlotDataSet) iterator.next() );
}
}
/**********************************************************************************************
* Central method for processing data; try to minimize looping.
* 1) calculate the maximum height of labels
* 2) find the maximum label width
*
* @param iPieChartDataSet
**********************************************************************************************/
private void processData( IPieChartDataSet iPieChartDataSet )
{
this.textProcessor = new TextProcessor();
this.processLegendLabels( iPieChartDataSet );
}
/**********************************************************************************************
* Method for processing data for AxisPlot datasets; try to minimize
* looping.
* 1) calculate the maximum height of labels
* 2) find the maximum label width
*
* @param iAxisPlotDataSet
* *********************************************************************************************/
private void processLegendLabels( IAxisPlotDataSet iAxisPlotDataSet )
{
for( int i = 0; i < iAxisPlotDataSet.getNumberOfLegendLabels(); i++ )
{
//---StockChartDataSets could have NULLs depending on the data
if( iAxisPlotDataSet.getLegendLabel( i ) != null )
{
this.textProcessor.addLabel( iAxisPlotDataSet.getLegendLabel( i ), this.legendProperties.getChartFont().getFont(), this.chart.getGraphics2D().getFontRenderContext() );
//---pair labels with paints to get around ugly piechart vs axischart data structure mess
this.labels.add( iAxisPlotDataSet.getLegendLabel( i ) );
this.paints.add( iAxisPlotDataSet.getPaint( i ) );
if( iAxisPlotDataSet.getChartType().equals( ChartType.POINT ) )
{
this.chartType = ChartType.POINT;
this.pointChartProperties = (PointChartProperties) iAxisPlotDataSet.getChartTypeProperties();
this.shapes.add( pointChartProperties.getShape( i ) );
this.fillPointsFlags.add( new Boolean( pointChartProperties.getFillPointsFlag( i ) ) );
this.pointOutlinePaints.add( pointChartProperties.getPointOutlinePaints( i ) );
}
if( iAxisPlotDataSet.getChartType().equals( ChartType.LINE ) )
{
this.chartType = ChartType.LINE;
this.lineChartProperties = (LineChartProperties) iAxisPlotDataSet.getChartTypeProperties();
if( lineChartProperties.getShapes() != null )
{
this.shapes.add( lineChartProperties.getShapes()[i] );
}
}
}
}
}
/**********************************************************************************************
* Method for processing data for PieCharts; try to minimize looping.
* 1) calculate the maximum height of labels
* 2) find the maximum label width
* @param iPieChartDataSet
* ********************************************************************************************/
private void processLegendLabels( IPieChartDataSet iPieChartDataSet )
{
for( int i = 0; i < iPieChartDataSet.getNumberOfLegendLabels(); i++ )
{
//---StockChartDataSets could have NULLs depending on the data
if( iPieChartDataSet.getLegendLabel( i ) != null )
{
this.textProcessor.addLabel( iPieChartDataSet.getLegendLabel( i ), this.legendProperties.getChartFont().getFont(), this.chart.getGraphics2D().getFontRenderContext() );
//---pair labels with paints to get around ugly piechart vs axischart data structure mess
this.labels.add( iPieChartDataSet.getLegendLabel( i ) );
this.paints.add( iPieChartDataSet.getPaint( i ) );
}
}
}
/************************************************************************************************
*
*************************************************************************************************/
public LegendProperties getLegendProperties()
{
return this.legendProperties;
}
/************************************************************************************************
* Calculates the width and height needed to display the Legend. Use the getWidth() and
* getHeight() methods to extract this information.
*
* @param iData can pass either the IPieChartDataSet or the IChartDataSeries to this.
************************************************************************************************/
public void calculateDrawingValues( IData iData )
{
int numberOfLabels;
this.labels = new ArrayList();
this.paints = new ArrayList();
if( iData instanceof IAxisDataSeries )
{
IAxisDataSeries iAxisDataSeries = (IAxisDataSeries) iData;
this.processData( iAxisDataSeries );
numberOfLabels = iAxisDataSeries.getTotalNumberOfDataSets();
}
else
{
IPieChartDataSet iPieChartDataSet = (IPieChartDataSet) iData;
this.processData( iPieChartDataSet );
numberOfLabels = iPieChartDataSet.getNumberOfLegendLabels();
}
//---make the icon proportional to the Font being used.
this.iconSide = (float) .50 * this.textProcessor.getTallestLabel();
//---for POINT and LINE charts, set iconSide to max width of legend shapes
if( (chartType == ChartType.POINT) || (chartType == ChartType.LINE) )
{
//for( int i = 0; i < numberOfLabels; i++ )
for( int i = 0; i < this.shapes.size(); i++ )
{
//---get the bounds of the shape
try
{
Double shapeWidthDouble = new Double( ( ( (Shape) this.shapes.get( i ) ).getBounds2D().getWidth() ) );
float shapeWidth = shapeWidthDouble.floatValue();
this.iconSide = Math.max(this.iconSide, shapeWidth);
}
catch (NullPointerException npe)
{
// Looks like in 0.74 it was quite acceptable to make shape = null
// we should probably catch all these and render a "null" shape to the legend
System.err.println("Warning: legend shape is null");
npe.printStackTrace();
}
}
}
this.determineWidthAndHeight( numberOfLabels );
}
/********************************************************************************************
*
********************************************************************************************/
public float getWidth()
{
return this.width;
}
/********************************************************************************************
*
********************************************************************************************/
public int getHeight()
{
//why not return a float here?
return ((int) Math.ceil( this.height ));
}
/**********************************************************************************************
* Determines the dimensions needed for the Legend and creates the image for it.
*
**********************************************************************************************/
private void determineWidthAndHeight( int numberOfLabels )
{
//---start with the padding no matter how many columns or specified width
width = this.legendProperties.getEdgePadding() * 2;
height = width;
//---if don't care how many columns or the number of labels is less than num columns specified, all in one row.
if( this.legendProperties.getNumColumns() == LegendAreaProperties.COLUMNS_AS_MANY_AS_NEEDED
|| this.legendProperties.getNumColumns() >= numberOfLabels )
{
this.numColumns = numberOfLabels;
width += this.textProcessor.getTotalLabelWidths();
this.numRows = 1;
}
//---else, more than one row
else
{
//---one less addition to do when looping.
this.widestLabelAndColumnPadding = this.textProcessor.getWidestLabel() + this.legendProperties.getColumnPadding();
if( legendProperties.getNumColumns() == LegendAreaProperties.COLUMNS_FIT_TO_IMAGE )
{
// calculate that the columns match exactly
float actualWidth = legendProperties.getSize().width;
float widestLabelColumnAndIcon =
widestLabelAndColumnPadding +
iconSide +
legendProperties.getIconPadding() +
legendProperties.getColumnPadding();
numColumns = (int) (actualWidth / widestLabelColumnAndIcon);
numColumns = Math.min( numColumns, numberOfLabels );
}
else
{
numColumns = this.legendProperties.getNumColumns();
}
width += this.textProcessor.getWidestLabel() * this.numColumns;
this.numRows = (int) Math.ceil( (double) numberOfLabels / (double) this.numColumns );
}
//---account for icons
width += (this.iconSide + this.legendProperties.getIconPadding()) * this.numColumns;
//---account for space between each column
width += this.legendProperties.getColumnPadding() * (this.numColumns - 1);
//---account for lineStrokes for LINE charts
if( chartType == ChartType.LINE)
{
width += this.legendProperties.getIconLineStrokeLength() * 2 * this.numColumns;
}
//---account for each row
height += (this.textProcessor.getTallestLabel() * this.numRows);
//---account for each row padding
height += (this.legendProperties.getRowPadding() * (this.numRows - 1));
}
/**********************************************************************************************
* Renders the legend.
*
**********************************************************************************************/
public void render()
{
Graphics2D g2d = this.chart.getGraphics2D();
//---get the bounds of the image
Rectangle2D.Float rectangle = new Rectangle2D.Float( this.x, this.y, width - 1, this.height - 1 );
//---fill the background of the Legend with the specified Paint
if( this.legendProperties.getBackgroundPaint() != null )
{
g2d.setPaint( this.legendProperties.getBackgroundPaint() );
g2d.fill( rectangle );
}
//---draw Legend border
if( this.legendProperties.getBorderStroke() != null )
{
this.legendProperties.getBorderStroke().draw( g2d, rectangle );
}
//---dont think we want this so text will be clean but leave commented out.
//g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF );
//---set the font and text color.
g2d.setFont( this.legendProperties.getChartFont().getFont() );
//---icon coordinates
rectangle.y += this.legendProperties.getEdgePadding() + (this.textProcessor.getTallestLabel() / 2) - (this.iconSide / 2);
rectangle.width = this.iconSide;
rectangle.height = this.iconSide;
float posX = this.x + this.legendProperties.getEdgePadding();
float fontY = rectangle.y + rectangle.height;
//---pre calculate utility values
float yIncrement = this.textProcessor.getTallestLabel() + this.legendProperties.getRowPadding();
float iconAndPaddingWidth = this.iconSide + this.legendProperties.getIconPadding();
int labelIndex = 0;
//LOOP
for( int j = 0; j < this.numRows; j++ )
{
//LOOP
for( int i = 0; i < this.numColumns; i++ )
{
rectangle.x = posX;
//---display icon
g2d.setPaint( (Paint) this.paints.get( labelIndex ) );
// only Point and Line Charts will have shapes drawn
if( this.shapes.size() > 0 && this.shapes.size() > labelIndex )
{
//Shape shape = (Shape)this.shapes.get( labelIndex);
//---get the original transform so can reset it
AffineTransform affineTransform = g2d.getTransform();
//---translate the Shape into position
g2d.translate( rectangle.x, rectangle.y );
if( this.fillPointsFlags.size() > 0 )
{
if( ((Boolean) this.fillPointsFlags.get( labelIndex )).booleanValue() )
{
g2d.fill( (Shape) this.shapes.get( labelIndex ) );
//---if we are filling the points, see if we should outline the Shape
//---applicable only to POINt charts
if( this.pointOutlinePaints.get( labelIndex ) != null )
{
g2d.setPaint( (Paint) this.pointOutlinePaints.get( labelIndex ) );
g2d.draw( (Shape) this.shapes.get( labelIndex ) );
}
}
}
else
{
// for Point Charts, only draw shape
if( chartType == ChartType.POINT)
{
g2d.draw( (Shape) this.shapes.get( labelIndex ) );
} else
// chartType == ChartType.LINE
// for Line Charts, fill the shape
{
//---get the bounds of the shape
Rectangle2D shapeBounds = ( (Shape) this.shapes.get( labelIndex ) ).getBounds2D();
double XOffset = shapeBounds.getWidth() / 2;
double YOffset = shapeBounds.getHeight() / 2;
g2d.setStroke(this.lineChartProperties.getLineStrokes()[ labelIndex]);
Line2D.Double line = new Line2D.Double(0, YOffset, this.legendProperties.getIconLineStrokeLength(), YOffset);
g2d.draw( line );
// move posX to account for the lineStroke before the shape. for example, ---o
posX += this.legendProperties.getIconLineStrokeLength();
//---translate the Shape to adjust for the IconLineStrokeLength
g2d.translate( this.legendProperties.getIconLineStrokeLength() - XOffset , 0 );
line.x1 = XOffset;
g2d.draw( line );
g2d.fill( (Shape) this.shapes.get( labelIndex ) );
//---border around icon
if( this.legendProperties.getIconBorderStroke() != null && this.pointOutlinePaints.size() != 0 )
{
if( this.pointOutlinePaints != null ) {
g2d.setStroke( this.legendProperties.getIconBorderStroke() );
g2d.setPaint( (Paint) this.pointOutlinePaints.get( labelIndex ) );
g2d.draw( (Shape) this.shapes.get( labelIndex ) );
}
}
// move posX to account for the lineStroke after the shape. for example, o---
posX += this.legendProperties.getIconLineStrokeLength();
}
}
//---reset original transform
g2d.setTransform( affineTransform );
}
// for other charts, just draw a rectangle
else
{
g2d.fill( rectangle );
//---border around icon
if( this.legendProperties.getIconBorderStroke() != null )
{
g2d.setStroke( this.legendProperties.getIconBorderStroke() );
g2d.setPaint( this.legendProperties.getIconBorderPaint() );
g2d.draw( rectangle );
}
}
//---draw the label
g2d.setPaint( this.legendProperties.getChartFont().getPaint() );
posX += iconAndPaddingWidth;
g2d.drawString( (String) this.labels.get( labelIndex ), posX, fontY );
if( this.legendProperties.getNumColumns() == LegendAreaProperties.COLUMNS_AS_MANY_AS_NEEDED
|| this.legendProperties.getNumColumns() >= this.labels.size() )
{
//---each column is as wide as it needs to be
posX += this.textProcessor.getTextTag( labelIndex ).getWidth() + this.legendProperties.getColumnPadding();
}
else
{
//---all columns have the same width
posX += this.widestLabelAndColumnPadding;
}
labelIndex++;
//---if no more labels, we are done.
if( labelIndex == this.labels.size() ) break;
}
posX = this.x + this.legendProperties.getEdgePadding();
fontY += yIncrement;
rectangle.y += yIncrement;
}
}
/*********************************************************************************************
* Enables the testing routines to display the contents of this Object.
*
* @param htmlGenerator
**********************************************************************************************/
public void toHTML( HTMLGenerator htmlGenerator )
{
htmlGenerator.legendTableStart();
htmlGenerator.addTableRow( "Width", Float.toString( this.width ) );
htmlGenerator.addTableRow( "Height", Float.toString( this.height ) );
htmlGenerator.addTableRow( "Icon Side", Float.toString( this.iconSide ) );
htmlGenerator.innerTableRowStart();
this.legendProperties.toHTML( htmlGenerator );
htmlGenerator.innerTableRowEnd();
htmlGenerator.legendTableEnd();
}
}