/* =========================================================== * JFreeChart : a free chart library for the Java(tm) platform * =========================================================== * * (C) Copyright 2000-2014, by Object Refinery Limited and Contributors. * * Project Info: http://www.jfree.org/jfreechart/index.html * * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * ---------------- * BarRenderer.java * ---------------- * (C) Copyright 2002-2014, by Object Refinery Limited. * * Original Author: David Gilbert (for Object Refinery Limited); * Contributor(s): Christian W. Zuckschwerdt; * Peter Kolb (patches 2497611, 2791407); * * Changes * ------- * 14-Mar-2002 : Version 1 (DG); * 23-May-2002 : Added tooltip generator to renderer (DG); * 29-May-2002 : Moved tooltip generator to abstract super-class (DG); * 25-Jun-2002 : Changed constructor to protected and removed redundant * code (DG); * 26-Jun-2002 : Added axis to initialise method, and record upper and lower * clip values (DG); * 24-Sep-2002 : Added getLegendItem() method (DG); * 09-Oct-2002 : Modified constructor to include URL generator (DG); * 05-Nov-2002 : Base dataset is now TableDataset not CategoryDataset (DG); * 10-Jan-2003 : Moved get/setItemMargin() method up from subclasses (DG); * 17-Jan-2003 : Moved plot classes into a separate package (DG); * 25-Mar-2003 : Implemented Serializable (DG); * 01-May-2003 : Modified clipping to allow for dual axes and datasets (DG); * 12-May-2003 : Merged horizontal and vertical bar renderers (DG); * 12-Jun-2003 : Updates for item labels (DG); * 30-Jul-2003 : Modified entity constructor (CZ); * 02-Sep-2003 : Changed initialise method to fix bug 790407 (DG); * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); * 07-Oct-2003 : Added renderer state (DG); * 27-Oct-2003 : Merged drawHorizontalItem() and drawVerticalItem() * methods (DG); * 28-Oct-2003 : Added support for gradient paint on bars (DG); * 14-Nov-2003 : Added 'maxBarWidth' attribute (DG); * 10-Feb-2004 : Small changes inside drawItem() method to ease cut-and-paste * overriding (DG); * 19-Mar-2004 : Fixed bug introduced with separation of tool tip and item * label generators. Fixed equals() method (DG); * 11-May-2004 : Fix for null pointer exception (bug id 951127) (DG); * 05-Nov-2004 : Modified drawItem() signature (DG); * 26-Jan-2005 : Provided override for getLegendItem() method (DG); * 20-Apr-2005 : Generate legend labels, tooltips and URLs (DG); * 18-May-2005 : Added configurable base value (DG); * 09-Jun-2005 : Use addItemEntity() method from superclass (DG); * 01-Dec-2005 : Update legend item to use/not use outline (DG); * ------------: JFreeChart 1.0.x --------------------------------------------- * 06-Dec-2005 : Fixed bug 1374222 (JDK 1.4 specific code) (DG); * 11-Jan-2006 : Fixed bug 1401856 (bad rendering for non-zero base) (DG); * 04-Aug-2006 : Fixed bug 1467706 (missing item labels for zero value * bars) (DG); * 04-Dec-2006 : Fixed bug in rendering to non-primary axis (DG); * 13-Dec-2006 : Add support for GradientPaint display in legend items (DG); * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG); * 11-May-2007 : Check for visibility in getLegendItem() (DG); * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() (DG); * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG); * 07-May-2008 : If minimumBarLength is > 0.0, extend the non-base end of the * bar (DG); * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG); * 24-Jun-2008 : Added barPainter mechanism (DG); * 26-Jun-2008 : Added crosshair support (DG); * 13-Aug-2008 : Added shadowPaint attribute (DG); * 14-Jan-2009 : Added support for seriesVisible flags (PK); * 03-Feb-2009 : Added defaultShadowsVisible flag - see patch 2511330 (PK); * 15-Jun-2012 : Removed JCommon dependencies (DG); * */ package org.jfree.chart.renderer.category; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Shape; import java.awt.Stroke; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import org.jfree.chart.LegendItem; import org.jfree.chart.axis.CategoryAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.ui.GradientPaintTransformer; import org.jfree.chart.ui.RectangleEdge; import org.jfree.chart.ui.StandardGradientPaintTransformer; import org.jfree.chart.util.ObjectUtils; import org.jfree.chart.util.PaintUtils; import org.jfree.chart.util.PublicCloneable; import org.jfree.chart.entity.EntityCollection; import org.jfree.chart.event.RendererChangeEvent; import org.jfree.chart.labels.CategoryItemLabelGenerator; import org.jfree.chart.labels.ItemLabelAnchor; import org.jfree.chart.labels.ItemLabelPosition; import org.jfree.chart.plot.CategoryPlot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.PlotRenderingInfo; import org.jfree.chart.text.TextUtilities; import org.jfree.chart.util.SerialUtils; import org.jfree.data.Range; import org.jfree.data.category.CategoryDataset; /** * A {@link CategoryItemRenderer} that draws individual data items as bars. * The example shown here is generated by the <code>BarChartDemo1.java</code> * program included in the JFreeChart Demo Collection: * <br><br> * <img src="../../../../../images/BarRendererSample.png" * alt="BarRendererSample.png"> */ public class BarRenderer extends AbstractCategoryItemRenderer implements Cloneable, PublicCloneable, Serializable { /** For serialization. */ private static final long serialVersionUID = 6000649414965887481L; /** The default item margin percentage. */ public static final double DEFAULT_ITEM_MARGIN = 0.20; /** * Constant that controls the minimum width before a bar has an outline * drawn. */ public static final double BAR_OUTLINE_WIDTH_THRESHOLD = 3.0; /** * The default bar painter assigned to each new instance of this renderer. * * @since 1.0.11 */ private static BarPainter defaultBarPainter = new StandardBarPainter(); /** * Returns the default bar painter. * * @return The default bar painter. * * @since 1.0.11 */ public static BarPainter getDefaultBarPainter() { return BarRenderer.defaultBarPainter; } /** * Sets the default bar painter. * * @param painter the painter (<code>null</code> not permitted). * * @since 1.0.11 */ public static void setDefaultBarPainter(BarPainter painter) { if (painter == null) { throw new IllegalArgumentException("Null 'painter' argument."); } BarRenderer.defaultBarPainter = painter; } /** * The default value for the initialisation of the shadowsVisible flag. */ private static boolean defaultShadowsVisible = true; /** * Returns the default value for the <code>shadowsVisible</code> flag. * * @return A boolean. * * @see #setDefaultShadowsVisible(boolean) * * @since 1.0.13 */ public static boolean getDefaultShadowsVisible() { return BarRenderer.defaultShadowsVisible; } /** * Sets the default value for the shadows visible flag. * * @param visible the new value for the default. * * @see #getDefaultShadowsVisible() * * @since 1.0.13 */ public static void setDefaultShadowsVisible(boolean visible) { BarRenderer.defaultShadowsVisible = visible; } /** The margin between items (bars) within a category. */ private double itemMargin; /** A flag that controls whether or not bar outlines are drawn. */ private boolean drawBarOutline; /** The maximum bar width as a percentage of the available space. */ private double maximumBarWidth; /** The minimum bar length (in Java2D units). */ private double minimumBarLength; /** * An optional class used to transform gradient paint objects to fit each * bar. */ private GradientPaintTransformer gradientPaintTransformer; /** * The fallback position if a positive item label doesn't fit inside the * bar. */ private ItemLabelPosition positiveItemLabelPositionFallback; /** * The fallback position if a negative item label doesn't fit inside the * bar. */ private ItemLabelPosition negativeItemLabelPositionFallback; /** The upper clip (axis) value for the axis. */ private double upperClip; // TODO: this needs to move into the renderer state /** The lower clip (axis) value for the axis. */ private double lowerClip; // TODO: this needs to move into the renderer state /** The base value for the bars (defaults to 0.0). */ private double base; /** * A flag that controls whether the base value is included in the range * returned by the findRangeBounds() method. */ private boolean includeBaseInRange; /** * The bar painter (never <code>null</code>). * * @since 1.0.11 */ private BarPainter barPainter; /** * The flag that controls whether or not shadows are drawn for the bars. * * @since 1.0.11 */ private boolean shadowsVisible; /** * The shadow paint. * * @since 1.0.11 */ private transient Paint shadowPaint; /** * The x-offset for the shadow effect. * * @since 1.0.11 */ private double shadowXOffset; /** * The y-offset for the shadow effect. * * @since 1.0.11 */ private double shadowYOffset; /** * Creates a new bar renderer with default settings. */ public BarRenderer() { super(); this.base = 0.0; this.includeBaseInRange = true; this.itemMargin = DEFAULT_ITEM_MARGIN; this.drawBarOutline = false; this.maximumBarWidth = 1.0; // 100 percent, so it will not apply unless changed this.positiveItemLabelPositionFallback = null; this.negativeItemLabelPositionFallback = null; this.gradientPaintTransformer = new StandardGradientPaintTransformer(); this.minimumBarLength = 0.0; setDefaultLegendShape(new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0)); this.barPainter = getDefaultBarPainter(); this.shadowsVisible = getDefaultShadowsVisible(); this.shadowPaint = Color.GRAY; this.shadowXOffset = 4.0; this.shadowYOffset = 4.0; } /** * Returns the base value for the bars. The default value is * <code>0.0</code>. * * @return The base value for the bars. * * @see #setBase(double) */ public double getBase() { return this.base; } /** * Sets the base value for the bars and sends a {@link RendererChangeEvent} * to all registered listeners. * * @param base the new base value. * * @see #getBase() */ public void setBase(double base) { this.base = base; fireChangeEvent(); } /** * Returns the item margin as a percentage of the available space for all * bars. * * @return The margin percentage (where 0.10 is ten percent). * * @see #setItemMargin(double) */ public double getItemMargin() { return this.itemMargin; } /** * Sets the item margin and sends a {@link RendererChangeEvent} to all * registered listeners. The value is expressed as a percentage of the * available width for plotting all the bars, with the resulting amount to * be distributed between all the bars evenly. * * @param percent the margin (where 0.10 is ten percent). * * @see #getItemMargin() */ public void setItemMargin(double percent) { this.itemMargin = percent; fireChangeEvent(); } /** * Returns a flag that controls whether or not bar outlines are drawn. * * @return A boolean. * * @see #setDrawBarOutline(boolean) */ public boolean isDrawBarOutline() { return this.drawBarOutline; } /** * Sets the flag that controls whether or not bar outlines are drawn and * sends a {@link RendererChangeEvent} to all registered listeners. * * @param draw the flag. * * @see #isDrawBarOutline() */ public void setDrawBarOutline(boolean draw) { this.drawBarOutline = draw; fireChangeEvent(); } /** * Returns the maximum bar width, as a percentage of the available drawing * space. * * @return The maximum bar width. * * @see #setMaximumBarWidth(double) */ public double getMaximumBarWidth() { return this.maximumBarWidth; } /** * Sets the maximum bar width, which is specified as a percentage of the * available space for all bars, and sends a {@link RendererChangeEvent} to * all registered listeners. * * @param percent the percent (where 0.05 is five percent). * * @see #getMaximumBarWidth() */ public void setMaximumBarWidth(double percent) { this.maximumBarWidth = percent; fireChangeEvent(); } /** * Returns the minimum bar length (in Java2D units). The default value is * 0.0. * * @return The minimum bar length. * * @see #setMinimumBarLength(double) */ public double getMinimumBarLength() { return this.minimumBarLength; } /** * Sets the minimum bar length and sends a {@link RendererChangeEvent} to * all registered listeners. The minimum bar length is specified in Java2D * units, and can be used to prevent bars that represent very small data * values from disappearing when drawn on the screen. Typically you would * set this to (say) 0.5 or 1.0 Java 2D units. Use this attribute with * caution, however, because setting it to a non-zero value will * artificially increase the length of bars representing small values, * which may misrepresent your data. * * @param min the minimum bar length (in Java2D units, must be >= 0.0). * * @see #getMinimumBarLength() */ public void setMinimumBarLength(double min) { if (min < 0.0) { throw new IllegalArgumentException("Requires 'min' >= 0.0"); } this.minimumBarLength = min; fireChangeEvent(); } /** * Returns the gradient paint transformer (an object used to transform * gradient paint objects to fit each bar). * * @return A transformer (<code>null</code> possible). * * @see #setGradientPaintTransformer(GradientPaintTransformer) */ public GradientPaintTransformer getGradientPaintTransformer() { return this.gradientPaintTransformer; } /** * Sets the gradient paint transformer and sends a * {@link RendererChangeEvent} to all registered listeners. * * @param transformer the transformer (<code>null</code> permitted). * * @see #getGradientPaintTransformer() */ public void setGradientPaintTransformer( GradientPaintTransformer transformer) { this.gradientPaintTransformer = transformer; fireChangeEvent(); } /** * Returns the fallback position for positive item labels that don't fit * within a bar. * * @return The fallback position (<code>null</code> possible). * * @see #setPositiveItemLabelPositionFallback(ItemLabelPosition) */ public ItemLabelPosition getPositiveItemLabelPositionFallback() { return this.positiveItemLabelPositionFallback; } /** * Sets the fallback position for positive item labels that don't fit * within a bar, and sends a {@link RendererChangeEvent} to all registered * listeners. * * @param position the position (<code>null</code> permitted). * * @see #getPositiveItemLabelPositionFallback() */ public void setPositiveItemLabelPositionFallback( ItemLabelPosition position) { this.positiveItemLabelPositionFallback = position; fireChangeEvent(); } /** * Returns the fallback position for negative item labels that don't fit * within a bar. * * @return The fallback position (<code>null</code> possible). * * @see #setPositiveItemLabelPositionFallback(ItemLabelPosition) */ public ItemLabelPosition getNegativeItemLabelPositionFallback() { return this.negativeItemLabelPositionFallback; } /** * Sets the fallback position for negative item labels that don't fit * within a bar, and sends a {@link RendererChangeEvent} to all registered * listeners. * * @param position the position (<code>null</code> permitted). * * @see #getNegativeItemLabelPositionFallback() */ public void setNegativeItemLabelPositionFallback( ItemLabelPosition position) { this.negativeItemLabelPositionFallback = position; fireChangeEvent(); } /** * Returns the flag that controls whether or not the base value for the * bars is included in the range calculated by * {@link #findRangeBounds(CategoryDataset)}. * * @return <code>true</code> if the base is included in the range, and * <code>false</code> otherwise. * * @since 1.0.1 * * @see #setIncludeBaseInRange(boolean) */ public boolean getIncludeBaseInRange() { return this.includeBaseInRange; } /** * Sets the flag that controls whether or not the base value for the bars * is included in the range calculated by * {@link #findRangeBounds(CategoryDataset)}. If the flag is changed, * a {@link RendererChangeEvent} is sent to all registered listeners. * * @param include the new value for the flag. * * @since 1.0.1 * * @see #getIncludeBaseInRange() */ public void setIncludeBaseInRange(boolean include) { if (this.includeBaseInRange != include) { this.includeBaseInRange = include; fireChangeEvent(); } } /** * Returns the bar painter. * * @return The bar painter (never <code>null</code>). * * @see #setBarPainter(BarPainter) * * @since 1.0.11 */ public BarPainter getBarPainter() { return this.barPainter; } /** * Sets the bar painter for this renderer and sends a * {@link RendererChangeEvent} to all registered listeners. * * @param painter the painter (<code>null</code> not permitted). * * @see #getBarPainter() * * @since 1.0.11 */ public void setBarPainter(BarPainter painter) { if (painter == null) { throw new IllegalArgumentException("Null 'painter' argument."); } this.barPainter = painter; fireChangeEvent(); } /** * Returns the flag that controls whether or not shadows are drawn for * the bars. * * @return A boolean. * * @since 1.0.11 */ public boolean getShadowsVisible() { return this.shadowsVisible; } /** * Sets the flag that controls whether or not shadows are * drawn by the renderer. * * @param visible the new flag value. * * @since 1.0.11 */ public void setShadowVisible(boolean visible) { this.shadowsVisible = visible; fireChangeEvent(); } /** * Returns the shadow paint. * * @return The shadow paint. * * @see #setShadowPaint(Paint) * * @since 1.0.11 */ public Paint getShadowPaint() { return this.shadowPaint; } /** * Sets the shadow paint and sends a {@link RendererChangeEvent} to all * registered listeners. * * @param paint the paint (<code>null</code> not permitted). * * @see #getShadowPaint() * * @since 1.0.11 */ public void setShadowPaint(Paint paint) { if (paint == null) { throw new IllegalArgumentException("Null 'paint' argument."); } this.shadowPaint = paint; fireChangeEvent(); } /** * Returns the shadow x-offset. * * @return The shadow x-offset. * * @since 1.0.11 */ public double getShadowXOffset() { return this.shadowXOffset; } /** * Sets the x-offset for the bar shadow and sends a * {@link RendererChangeEvent} to all registered listeners. * * @param offset the offset. * * @since 1.0.11 */ public void setShadowXOffset(double offset) { this.shadowXOffset = offset; fireChangeEvent(); } /** * Returns the shadow y-offset. * * @return The shadow y-offset. * * @since 1.0.11 */ public double getShadowYOffset() { return this.shadowYOffset; } /** * Sets the y-offset for the bar shadow and sends a * {@link RendererChangeEvent} to all registered listeners. * * @param offset the offset. * * @since 1.0.11 */ public void setShadowYOffset(double offset) { this.shadowYOffset = offset; fireChangeEvent(); } /** * Returns the lower clip value. This value is recalculated in the * initialise() method. * * @return The value. */ public double getLowerClip() { // TODO: this attribute should be transferred to the renderer state. return this.lowerClip; } /** * Returns the upper clip value. This value is recalculated in the * initialise() method. * * @return The value. */ public double getUpperClip() { // TODO: this attribute should be transferred to the renderer state. return this.upperClip; } /** * Initialises the renderer and returns a state object that will be passed * to subsequent calls to the drawItem method. This method gets called * once at the start of the process of drawing a chart. * * @param g2 the graphics device. * @param dataArea the area in which the data is to be plotted. * @param plot the plot. * @param rendererIndex the renderer index. * @param info collects chart rendering information for return to caller. * * @return The renderer state. */ @Override public CategoryItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, CategoryPlot plot, int rendererIndex, PlotRenderingInfo info) { CategoryItemRendererState state = super.initialise(g2, dataArea, plot, rendererIndex, info); // get the clipping values... ValueAxis rangeAxis = plot.getRangeAxisForDataset(rendererIndex); this.lowerClip = rangeAxis.getRange().getLowerBound(); this.upperClip = rangeAxis.getRange().getUpperBound(); // calculate the bar width calculateBarWidth(plot, dataArea, rendererIndex, state); return state; } /** * Calculates the bar width and stores it in the renderer state. * * @param plot the plot. * @param dataArea the data area. * @param rendererIndex the renderer index. * @param state the renderer state. */ protected void calculateBarWidth(CategoryPlot plot, Rectangle2D dataArea, int rendererIndex, CategoryItemRendererState state) { CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex); CategoryDataset dataset = plot.getDataset(rendererIndex); if (dataset != null) { int columns = dataset.getColumnCount(); int rows = state.getVisibleSeriesCount() >= 0 ? state.getVisibleSeriesCount() : dataset.getRowCount(); double space = 0.0; PlotOrientation orientation = plot.getOrientation(); if (orientation == PlotOrientation.HORIZONTAL) { space = dataArea.getHeight(); } else if (orientation == PlotOrientation.VERTICAL) { space = dataArea.getWidth(); } double maxWidth = space * getMaximumBarWidth(); double categoryMargin = 0.0; double currentItemMargin = 0.0; if (columns > 1) { categoryMargin = domainAxis.getCategoryMargin(); } if (rows > 1) { currentItemMargin = getItemMargin(); } double used = space * (1 - domainAxis.getLowerMargin() - domainAxis.getUpperMargin() - categoryMargin - currentItemMargin); if ((rows * columns) > 0) { state.setBarWidth(Math.min(used / (rows * columns), maxWidth)); } else { state.setBarWidth(Math.min(used, maxWidth)); } } } /** * Calculates the coordinate of the first "side" of a bar. This will be * the minimum x-coordinate for a vertical bar, and the minimum * y-coordinate for a horizontal bar. * * @param plot the plot. * @param orientation the plot orientation. * @param dataArea the data area. * @param domainAxis the domain axis. * @param state the renderer state (has the bar width precalculated). * @param row the row index. * @param column the column index. * * @return The coordinate. */ protected double calculateBarW0(CategoryPlot plot, PlotOrientation orientation, Rectangle2D dataArea, CategoryAxis domainAxis, CategoryItemRendererState state, int row, int column) { // calculate bar width... double space; if (orientation == PlotOrientation.HORIZONTAL) { space = dataArea.getHeight(); } else { space = dataArea.getWidth(); } double barW0 = domainAxis.getCategoryStart(column, getColumnCount(), dataArea, plot.getDomainAxisEdge()); int seriesCount = state.getVisibleSeriesCount() >= 0 ? state.getVisibleSeriesCount() : getRowCount(); int categoryCount = getColumnCount(); if (seriesCount > 1) { double seriesGap = space * getItemMargin() / (categoryCount * (seriesCount - 1)); double seriesW = calculateSeriesWidth(space, domainAxis, categoryCount, seriesCount); barW0 = barW0 + row * (seriesW + seriesGap) + (seriesW / 2.0) - (state.getBarWidth() / 2.0); } else { barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), dataArea, plot.getDomainAxisEdge()) - state.getBarWidth() / 2.0; } return barW0; } /** * Calculates the coordinates for the length of a single bar. * * @param value the value represented by the bar. * * @return The coordinates for each end of the bar (or <code>null</code> if * the bar is not visible for the current axis range). */ protected double[] calculateBarL0L1(double value) { double lclip = getLowerClip(); double uclip = getUpperClip(); double barLow = Math.min(this.base, value); double barHigh = Math.max(this.base, value); if (barHigh < lclip) { // bar is not visible return null; } if (barLow > uclip) { // bar is not visible return null; } barLow = Math.max(barLow, lclip); barHigh = Math.min(barHigh, uclip); return new double[] {barLow, barHigh}; } /** * Returns the range of values the renderer requires to display all the * items from the specified dataset. This takes into account the range * of values in the dataset, plus the flag that determines whether or not * the base value for the bars should be included in the range. * * @param dataset the dataset (<code>null</code> permitted). * @param includeInterval include the interval if the dataset has one? * * @return The range (or <code>null</code> if the dataset is * <code>null</code> or empty). */ @Override public Range findRangeBounds(CategoryDataset dataset, boolean includeInterval) { if (dataset == null) { return null; } Range result = super.findRangeBounds(dataset, includeInterval); if (result != null) { if (this.includeBaseInRange) { result = Range.expandToInclude(result, this.base); } } return result; } /** * Returns a legend item for a series. * * @param datasetIndex the dataset index (zero-based). * @param series the series index (zero-based). * * @return The legend item (possibly <code>null</code>). */ @Override public LegendItem getLegendItem(int datasetIndex, int series) { CategoryPlot cp = getPlot(); if (cp == null) { return null; } // check that a legend item needs to be displayed... if (!isSeriesVisible(series) || !isSeriesVisibleInLegend(series)) { return null; } CategoryDataset dataset = cp.getDataset(datasetIndex); String label = getLegendItemLabelGenerator().generateLabel(dataset, series); String description = label; String toolTipText = null; if (getLegendItemToolTipGenerator() != null) { toolTipText = getLegendItemToolTipGenerator().generateLabel( dataset, series); } String urlText = null; if (getLegendItemURLGenerator() != null) { urlText = getLegendItemURLGenerator().generateLabel(dataset, series); } Shape shape = lookupLegendShape(series); Paint paint = lookupSeriesPaint(series); Paint outlinePaint = lookupSeriesOutlinePaint(series); Stroke outlineStroke = lookupSeriesOutlineStroke(series); LegendItem result = new LegendItem(label, description, toolTipText, urlText, true, shape, true, paint, isDrawBarOutline(), outlinePaint, outlineStroke, false, new Line2D.Float(), new BasicStroke(1.0f), Color.BLACK); result.setLabelFont(lookupLegendTextFont(series)); Paint labelPaint = lookupLegendTextPaint(series); if (labelPaint != null) { result.setLabelPaint(labelPaint); } result.setDataset(dataset); result.setDatasetIndex(datasetIndex); result.setSeriesKey(dataset.getRowKey(series)); result.setSeriesIndex(series); if (this.gradientPaintTransformer != null) { result.setFillPaintTransformer(this.gradientPaintTransformer); } return result; } /** * Draws the bar for a single (series, category) data item. * * @param g2 the graphics device. * @param state the renderer state. * @param dataArea the data area. * @param plot the plot. * @param domainAxis the domain axis. * @param rangeAxis the range axis. * @param dataset the dataset. * @param row the row index (zero-based). * @param column the column index (zero-based). * @param pass the pass index. */ @Override public void drawItem(Graphics2D g2, CategoryItemRendererState state, Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, int pass) { // nothing is drawn if the row index is not included in the list with // the indices of the visible rows... int visibleRow = state.getVisibleSeriesIndex(row); if (visibleRow < 0) { return; } // nothing is drawn for null values... Number dataValue = dataset.getValue(row, column); if (dataValue == null) { return; } final double value = dataValue.doubleValue(); PlotOrientation orientation = plot.getOrientation(); double barW0 = calculateBarW0(plot, orientation, dataArea, domainAxis, state, visibleRow, column); double[] barL0L1 = calculateBarL0L1(value); if (barL0L1 == null) { return; // the bar is not visible } RectangleEdge edge = plot.getRangeAxisEdge(); double transL0 = rangeAxis.valueToJava2D(barL0L1[0], dataArea, edge); double transL1 = rangeAxis.valueToJava2D(barL0L1[1], dataArea, edge); // in the following code, barL0 is (in Java2D coordinates) the LEFT // end of the bar for a horizontal bar chart, and the TOP end of the // bar for a vertical bar chart. Whether this is the BASE of the bar // or not depends also on (a) whether the data value is 'negative' // relative to the base value and (b) whether or not the range axis is // inverted. This only matters if/when we apply the minimumBarLength // attribute, because we should extend the non-base end of the bar boolean positive = (value >= this.base); boolean inverted = rangeAxis.isInverted(); double barL0 = Math.min(transL0, transL1); double barLength = Math.abs(transL1 - transL0); double barLengthAdj = 0.0; if (barLength > 0.0 && barLength < getMinimumBarLength()) { barLengthAdj = getMinimumBarLength() - barLength; } double barL0Adj = 0.0; RectangleEdge barBase; if (orientation == PlotOrientation.HORIZONTAL) { if (positive && inverted || !positive && !inverted) { barL0Adj = barLengthAdj; barBase = RectangleEdge.RIGHT; } else { barBase = RectangleEdge.LEFT; } } else { if (positive && !inverted || !positive && inverted) { barL0Adj = barLengthAdj; barBase = RectangleEdge.BOTTOM; } else { barBase = RectangleEdge.TOP; } } // draw the bar... Rectangle2D bar; if (orientation == PlotOrientation.HORIZONTAL) { bar = new Rectangle2D.Double(barL0 - barL0Adj, barW0, barLength + barLengthAdj, state.getBarWidth()); } else { bar = new Rectangle2D.Double(barW0, barL0 - barL0Adj, state.getBarWidth(), barLength + barLengthAdj); } if (getShadowsVisible()) { this.barPainter.paintBarShadow(g2, this, row, column, bar, barBase, true); } this.barPainter.paintBar(g2, this, row, column, bar, barBase); CategoryItemLabelGenerator generator = getItemLabelGenerator(row, column); if (generator != null && isItemLabelVisible(row, column)) { drawItemLabel(g2, dataset, row, column, plot, generator, bar, (value < 0.0)); } // submit the current data point as a crosshair candidate int datasetIndex = plot.findDatasetIndex(dataset); updateCrosshairValues(state.getCrosshairState(), dataset.getRowKey(row), dataset.getColumnKey(column), value, datasetIndex, barW0, barL0, orientation); // add an item entity, if this information is being collected EntityCollection entities = state.getEntityCollection(); if (entities != null) { addItemEntity(entities, dataset, row, column, bar); } } /** * Calculates the available space for each series. * * @param space the space along the entire axis (in Java2D units). * @param axis the category axis. * @param categories the number of categories. * @param series the number of series. * * @return The width of one series. */ protected double calculateSeriesWidth(double space, CategoryAxis axis, int categories, int series) { double factor = 1.0 - getItemMargin() - axis.getLowerMargin() - axis.getUpperMargin(); if (categories > 1) { factor = factor - axis.getCategoryMargin(); } return (space * factor) / (categories * series); } /** * Draws an item label. This method is overridden so that the bar can be * used to calculate the label anchor point. * * @param g2 the graphics device. * @param data the dataset. * @param row the row. * @param column the column. * @param plot the plot. * @param generator the label generator. * @param bar the bar. * @param negative a flag indicating a negative value. */ protected void drawItemLabel(Graphics2D g2, CategoryDataset data, int row, int column, CategoryPlot plot, CategoryItemLabelGenerator generator, Rectangle2D bar, boolean negative) { String label = generator.generateLabel(data, row, column); if (label == null) { return; // nothing to do } Font labelFont = getItemLabelFont(row, column); g2.setFont(labelFont); Paint paint = getItemLabelPaint(row, column); g2.setPaint(paint); // find out where to place the label... ItemLabelPosition position; if (!negative) { position = getPositiveItemLabelPosition(row, column); } else { position = getNegativeItemLabelPosition(row, column); } // work out the label anchor point... Point2D anchorPoint = calculateLabelAnchorPoint( position.getItemLabelAnchor(), bar, plot.getOrientation()); if (isInternalAnchor(position.getItemLabelAnchor())) { Shape bounds = TextUtilities.calculateRotatedStringBounds(label, g2, (float) anchorPoint.getX(), (float) anchorPoint.getY(), position.getTextAnchor(), position.getAngle(), position.getRotationAnchor()); if (bounds != null) { if (!bar.contains(bounds.getBounds2D())) { if (!negative) { position = getPositiveItemLabelPositionFallback(); } else { position = getNegativeItemLabelPositionFallback(); } if (position != null) { anchorPoint = calculateLabelAnchorPoint( position.getItemLabelAnchor(), bar, plot.getOrientation()); } } } } if (position != null) { TextUtilities.drawRotatedString(label, g2, (float) anchorPoint.getX(), (float) anchorPoint.getY(), position.getTextAnchor(), position.getAngle(), position.getRotationAnchor()); } } /** * Calculates the item label anchor point. * * @param anchor the anchor. * @param bar the bar. * @param orientation the plot orientation. * * @return The anchor point. */ private Point2D calculateLabelAnchorPoint(ItemLabelAnchor anchor, Rectangle2D bar, PlotOrientation orientation) { Point2D result = null; double offset = getItemLabelAnchorOffset(); double x0 = bar.getX() - offset; double x1 = bar.getX(); double x2 = bar.getX() + offset; double x3 = bar.getCenterX(); double x4 = bar.getMaxX() - offset; double x5 = bar.getMaxX(); double x6 = bar.getMaxX() + offset; double y0 = bar.getMaxY() + offset; double y1 = bar.getMaxY(); double y2 = bar.getMaxY() - offset; double y3 = bar.getCenterY(); double y4 = bar.getMinY() + offset; double y5 = bar.getMinY(); double y6 = bar.getMinY() - offset; if (anchor == ItemLabelAnchor.CENTER) { result = new Point2D.Double(x3, y3); } else if (anchor == ItemLabelAnchor.INSIDE1) { result = new Point2D.Double(x4, y4); } else if (anchor == ItemLabelAnchor.INSIDE2) { result = new Point2D.Double(x4, y4); } else if (anchor == ItemLabelAnchor.INSIDE3) { result = new Point2D.Double(x4, y3); } else if (anchor == ItemLabelAnchor.INSIDE4) { result = new Point2D.Double(x4, y2); } else if (anchor == ItemLabelAnchor.INSIDE5) { result = new Point2D.Double(x4, y2); } else if (anchor == ItemLabelAnchor.INSIDE6) { result = new Point2D.Double(x3, y2); } else if (anchor == ItemLabelAnchor.INSIDE7) { result = new Point2D.Double(x2, y2); } else if (anchor == ItemLabelAnchor.INSIDE8) { result = new Point2D.Double(x2, y2); } else if (anchor == ItemLabelAnchor.INSIDE9) { result = new Point2D.Double(x2, y3); } else if (anchor == ItemLabelAnchor.INSIDE10) { result = new Point2D.Double(x2, y4); } else if (anchor == ItemLabelAnchor.INSIDE11) { result = new Point2D.Double(x2, y4); } else if (anchor == ItemLabelAnchor.INSIDE12) { result = new Point2D.Double(x3, y4); } else if (anchor == ItemLabelAnchor.OUTSIDE1) { result = new Point2D.Double(x5, y6); } else if (anchor == ItemLabelAnchor.OUTSIDE2) { result = new Point2D.Double(x6, y5); } else if (anchor == ItemLabelAnchor.OUTSIDE3) { result = new Point2D.Double(x6, y3); } else if (anchor == ItemLabelAnchor.OUTSIDE4) { result = new Point2D.Double(x6, y1); } else if (anchor == ItemLabelAnchor.OUTSIDE5) { result = new Point2D.Double(x5, y0); } else if (anchor == ItemLabelAnchor.OUTSIDE6) { result = new Point2D.Double(x3, y0); } else if (anchor == ItemLabelAnchor.OUTSIDE7) { result = new Point2D.Double(x1, y0); } else if (anchor == ItemLabelAnchor.OUTSIDE8) { result = new Point2D.Double(x0, y1); } else if (anchor == ItemLabelAnchor.OUTSIDE9) { result = new Point2D.Double(x0, y3); } else if (anchor == ItemLabelAnchor.OUTSIDE10) { result = new Point2D.Double(x0, y5); } else if (anchor == ItemLabelAnchor.OUTSIDE11) { result = new Point2D.Double(x1, y6); } else if (anchor == ItemLabelAnchor.OUTSIDE12) { result = new Point2D.Double(x3, y6); } return result; } /** * Returns <code>true</code> if the specified anchor point is inside a bar. * * @param anchor the anchor point. * * @return A boolean. */ private boolean isInternalAnchor(ItemLabelAnchor anchor) { return anchor == ItemLabelAnchor.CENTER || anchor == ItemLabelAnchor.INSIDE1 || anchor == ItemLabelAnchor.INSIDE2 || anchor == ItemLabelAnchor.INSIDE3 || anchor == ItemLabelAnchor.INSIDE4 || anchor == ItemLabelAnchor.INSIDE5 || anchor == ItemLabelAnchor.INSIDE6 || anchor == ItemLabelAnchor.INSIDE7 || anchor == ItemLabelAnchor.INSIDE8 || anchor == ItemLabelAnchor.INSIDE9 || anchor == ItemLabelAnchor.INSIDE10 || anchor == ItemLabelAnchor.INSIDE11 || anchor == ItemLabelAnchor.INSIDE12; } /** * Tests this instance for equality with an arbitrary object. * * @param obj the object (<code>null</code> permitted). * * @return A boolean. */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof BarRenderer)) { return false; } BarRenderer that = (BarRenderer) obj; if (this.base != that.base) { return false; } if (this.itemMargin != that.itemMargin) { return false; } if (this.drawBarOutline != that.drawBarOutline) { return false; } if (this.maximumBarWidth != that.maximumBarWidth) { return false; } if (this.minimumBarLength != that.minimumBarLength) { return false; } if (!ObjectUtils.equal(this.gradientPaintTransformer, that.gradientPaintTransformer)) { return false; } if (!ObjectUtils.equal(this.positiveItemLabelPositionFallback, that.positiveItemLabelPositionFallback)) { return false; } if (!ObjectUtils.equal(this.negativeItemLabelPositionFallback, that.negativeItemLabelPositionFallback)) { return false; } if (!this.barPainter.equals(that.barPainter)) { return false; } if (this.shadowsVisible != that.shadowsVisible) { return false; } if (!PaintUtils.equal(this.shadowPaint, that.shadowPaint)) { return false; } if (this.shadowXOffset != that.shadowXOffset) { return false; } if (this.shadowYOffset != that.shadowYOffset) { return false; } return super.equals(obj); } /** * Provides serialization support. * * @param stream the output stream. * * @throws IOException if there is an I/O error. */ private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); SerialUtils.writePaint(this.shadowPaint, stream); } /** * Provides serialization support. * * @param stream the input stream. * * @throws IOException if there is an I/O error. * @throws ClassNotFoundException if there is a classpath problem. */ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); this.shadowPaint = SerialUtils.readPaint(stream); } }