From: Nathaniel G. A. <nat...@us...> - 2004-10-23 17:44:57
|
Update of /cvsroot/jcharts/krysalis-jcharts/src/java/org/krysalis/jcharts/legend In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv4384/src/java/org/krysalis/jcharts/legend Added Files: Legend.java Log Message: needs a lot more work --- NEW FILE: Legend.java --- /*********************************************************************************************** * 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 nat...@us... * * 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.legend; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Shape; import java.awt.Stroke; import java.awt.BasicStroke; 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.1 2004/10/23 17:44:48 nathaniel_auvil Exp $ ****************************************************************************************/ public class Legend implements HTMLTestable, Serializable { private Chart chart; private LegendProperties legendProperties; private ArrayList items; 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; // for Bar chart legend shape - size is automatically calculated later on private Rectangle2D barShape = new Rectangle2D.Double( 0, 0, 0, 0 ); private Stroke barStroke = new BasicStroke(); ChartType chartType; ArrayList shapes, fillPointsFlags, strokes, pointOutlinePaints, labels, paints; /*************************************************************************************** * * @param chart * @param legendProperties **************************************************************************************/ public Legend( Chart chart, LegendProperties legendProperties ) { this.chart = chart; this.legendProperties = legendProperties; this.items= new ArrayList(); } /************************************************************************************** * * @param x *************************************************************************************/ private void setX( float x ) { this.x = x; } /************************************************************************************** * * @param y *************************************************************************************/ private 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 ) { //TODO this probably should be moved into the Item Class. this.textProcessor.addLabel( iAxisPlotDataSet.getLegendLabel( i ), this.legendProperties.getChartFont().getFont(), this.chart.getGraphics2D().getFontRenderContext() ); if( iAxisPlotDataSet.getChartType().equals( ChartType.POINT ) || iAxisPlotDataSet.getChartType().equals( ChartType.POINT_RHS ) ) { PointChartProperties pointChartProperties = (PointChartProperties) iAxisPlotDataSet.getChartTypeProperties(); Symbol symbol= new Symbol( pointChartProperties.getShape( i ), iAxisPlotDataSet.getPaint( i ), pointChartProperties.getPointOutlinePaints( i ) ); this.items.add( new Item( symbol, iAxisPlotDataSet.getLegendLabel( i ) ) ); } else if( iAxisPlotDataSet.getChartType().equals( ChartType.LINE ) || iAxisPlotDataSet.getChartType().equals( ChartType.LINE_RHS ) ) { LineChartProperties lineChartProperties = (LineChartProperties) iAxisPlotDataSet.getChartTypeProperties(); Shape shape; if( lineChartProperties.getShapes() != null ) { shape= lineChartProperties.getShapes()[ i ]; } else { shape= null; } LineSymbol lineSymbol= new LineSymbol( shape, iAxisPlotDataSet.getPaint( i ), lineChartProperties.getLineStrokes()[ i ] ); this.items.add( new Item( lineSymbol, iAxisPlotDataSet.getLegendLabel( i ) ) ); } else { Symbol symbol= new Symbol( PointChartProperties.SHAPE_SQUARE, iAxisPlotDataSet.getPaint( i ), this.legendProperties.getIconBorderPaint() ); this.items.add( new Item( symbol, iAxisPlotDataSet.getLegendLabel( 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 ) { //TODO this probably should be moved into the Item Class. this.textProcessor.addLabel( iPieChartDataSet.getLegendLabel( i ), this.legendProperties.getChartFont().getFont(), this.chart.getGraphics2D().getFontRenderContext() ); Symbol symbol= new Symbol( PointChartProperties.SHAPE_SQUARE, iPieChartDataSet.getPaint( i ), this.legendProperties.getIconBorderPaint() ); this.items.add( new Item( symbol, iPieChartDataSet.getLegendLabel( 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; 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 < this.shapes.size(); i++ ) { Shape shape; //---get the bounds of the shape if( this.shapes.get( i ) != null ) { shape = (Shape) this.shapes.get( i ); } else { shape = PointChartProperties.SHAPE_SQUARE; } this.iconSide = Math.max( this.iconSide, (float) shape.getBounds2D().getWidth() ); /* //---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 //TODO this is NOT correct. What about a combo chart? if( chartType == ChartType.LINE ) { //width += this.legendProperties.getIconLineStrokeLength() * 2 * this.numColumns; width += this.legendProperties.getIconLineStrokeLength() * 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; Shape shape = (Shape) this.shapes.get( labelIndex ); //---get the original transform so can reset it AffineTransform affineTransform = g2d.getTransform(); //LOOP for( int j = 0; j < this.numRows; j++ ) { //LOOP for( int i = 0; i < this.numColumns; i++ ) { rectangle.x = posX; shape = (Shape) this.shapes.get( labelIndex ); //---display icon g2d.setPaint( (Paint) this.paints.get( labelIndex ) ); //TODO still need to draw a line for Line Charts if( shape != null ) { //TODO what about scatter plots? //TODO what about NULL shapes on a Line Chart? if( this.shapes.size() > 0 && this.shapes.size() > labelIndex && !shape.equals( barShape ) ) { //---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 ); //---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 ); } } } else { //---get the bounds of the shape Rectangle2D shapeBounds = shape.getBounds2D(); double xOffset = shapeBounds.getWidth() / 2; double yOffset = shapeBounds.getHeight() / 2; g2d.setStroke( (Stroke) this.strokes.get( labelIndex ) ); g2d.draw( new Line2D.Double( 0, yOffset, this.legendProperties.getIconLineStrokeLength(), yOffset ) ); // 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 ); g2d.translate( xOffset, 0 ); //line.x1 = xOffset; //g2d.draw( line ); //---Line Charts fill with the same Paint as the lines are drawn with 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 ); } } //---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 ); } posX += iconAndPaddingWidth; } } //---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( "X", Float.toString( this.x ) ); htmlGenerator.addTableRow( "Y", Float.toString( this.y ) ); htmlGenerator.addTableRow( "numColumns", Integer.toString( this.numColumns ) ); htmlGenerator.addTableRow( "numRows", Integer.toString( this.numRows ) ); 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(); } } |