Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors. 6: * 7: * Project Info: http://www.jfree.org/jfreechart/index.html 8: * 9: * This library is free software; you can redistribute it and/or modify it 10: * under the terms of the GNU Lesser General Public License as published by 11: * the Free Software Foundation; either version 2.1 of the License, or 12: * (at your option) any later version. 13: * 14: * This library is distributed in the hope that it will be useful, but 15: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17: * License for more details. 18: * 19: * You should have received a copy of the GNU Lesser General Public 20: * License along with this library; if not, write to the Free Software 21: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22: * USA. 23: * 24: * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 25: * in the United States and other countries.] 26: * 27: * ------------------------- 28: * StackedXYBarRenderer.java 29: * ------------------------- 30: * (C) Copyright 2004-2007, by Andreas Schroeder and Contributors. 31: * 32: * Original Author: Andreas Schroeder; 33: * Contributor(s): David Gilbert (for Object Refinery Limited); 34: * 35: * $Id: StackedXYBarRenderer.java,v 1.10.2.5 2007/03/21 10:04:20 mungady Exp $ 36: * 37: * Changes 38: * ------- 39: * 01-Apr-2004 : Version 1 (AS); 40: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 41: * getYValue() (DG); 42: * 15-Aug-2004 : Added drawBarOutline to control draw/don't-draw bar 43: * outlines (BN); 44: * 10-Sep-2004 : drawBarOutline attribute is now inherited from XYBarRenderer 45: * and double primitives are retrieved from the dataset rather 46: * than Number objects (DG); 47: * 07-Jan-2005 : Updated for method name change in DatasetUtilities (DG); 48: * 25-Jan-2005 : Modified to handle negative values correctly (DG); 49: * ------------- JFREECHART 1.0.x --------------------------------------------- 50: * 06-Dec-2006 : Added support for GradientPaint (DG); 51: * 15-Mar-2007 : Added renderAsPercentages option (DG); 52: * 53: */ 54: 55: package org.jfree.chart.renderer.xy; 56: 57: import java.awt.GradientPaint; 58: import java.awt.Graphics2D; 59: import java.awt.Paint; 60: import java.awt.geom.Rectangle2D; 61: 62: import org.jfree.chart.axis.ValueAxis; 63: import org.jfree.chart.entity.EntityCollection; 64: import org.jfree.chart.event.RendererChangeEvent; 65: import org.jfree.chart.labels.ItemLabelAnchor; 66: import org.jfree.chart.labels.ItemLabelPosition; 67: import org.jfree.chart.labels.XYItemLabelGenerator; 68: import org.jfree.chart.plot.CrosshairState; 69: import org.jfree.chart.plot.PlotOrientation; 70: import org.jfree.chart.plot.PlotRenderingInfo; 71: import org.jfree.chart.plot.XYPlot; 72: import org.jfree.data.Range; 73: import org.jfree.data.general.DatasetUtilities; 74: import org.jfree.data.xy.IntervalXYDataset; 75: import org.jfree.data.xy.TableXYDataset; 76: import org.jfree.data.xy.XYDataset; 77: import org.jfree.ui.RectangleEdge; 78: import org.jfree.ui.TextAnchor; 79: 80: /** 81: * A bar renderer that displays the series items stacked. 82: * The dataset used together with this renderer must be a 83: * {@link org.jfree.data.xy.IntervalXYDataset} and a 84: * {@link org.jfree.data.xy.TableXYDataset}. For example, the 85: * dataset class {@link org.jfree.data.xy.CategoryTableXYDataset} 86: * implements both interfaces. 87: */ 88: public class StackedXYBarRenderer extends XYBarRenderer { 89: 90: /** For serialization. */ 91: private static final long serialVersionUID = -7049101055533436444L; 92: 93: /** A flag that controls whether the bars display values or percentages. */ 94: private boolean renderAsPercentages; 95: 96: /** 97: * Creates a new renderer. 98: */ 99: public StackedXYBarRenderer() { 100: this(0.0); 101: } 102: 103: /** 104: * Creates a new renderer. 105: * 106: * @param margin the percentual amount of the bars that are cut away. 107: */ 108: public StackedXYBarRenderer(double margin) { 109: super(margin); 110: this.renderAsPercentages = false; 111: 112: // set the default item label positions, which will only be used if 113: // the user requests visible item labels... 114: ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER, 115: TextAnchor.CENTER); 116: setBasePositiveItemLabelPosition(p); 117: setBaseNegativeItemLabelPosition(p); 118: setPositiveItemLabelPositionFallback(null); 119: setNegativeItemLabelPositionFallback(null); 120: } 121: 122: /** 123: * Returns <code>true</code> if the renderer displays each item value as 124: * a percentage (so that the stacked bars add to 100%), and 125: * <code>false</code> otherwise. 126: * 127: * @return A boolean. 128: * 129: * @see #setRenderAsPercentages(boolean) 130: * 131: * @since 1.0.5 132: */ 133: public boolean getRenderAsPercentages() { 134: return this.renderAsPercentages; 135: } 136: 137: /** 138: * Sets the flag that controls whether the renderer displays each item 139: * value as a percentage (so that the stacked bars add to 100%), and sends 140: * a {@link RendererChangeEvent} to all registered listeners. 141: * 142: * @param asPercentages the flag. 143: * 144: * @see #getRenderAsPercentages() 145: * 146: * @since 1.0.5 147: */ 148: public void setRenderAsPercentages(boolean asPercentages) { 149: this.renderAsPercentages = asPercentages; 150: notifyListeners(new RendererChangeEvent(this)); 151: } 152: 153: /** 154: * Returns <code>2</code> to indicate that this renderer requires two 155: * passes for drawing (item labels are drawn in the second pass so that 156: * they always appear in front of all the bars). 157: * 158: * @return <code>2</code>. 159: */ 160: public int getPassCount() { 161: return 2; 162: } 163: 164: /** 165: * Initialises the renderer and returns a state object that should be 166: * passed to all subsequent calls to the drawItem() method. Here there is 167: * nothing to do. 168: * 169: * @param g2 the graphics device. 170: * @param dataArea the area inside the axes. 171: * @param plot the plot. 172: * @param data the data. 173: * @param info an optional info collection object to return data back to 174: * the caller. 175: * 176: * @return A state object. 177: */ 178: public XYItemRendererState initialise(Graphics2D g2, 179: Rectangle2D dataArea, 180: XYPlot plot, 181: XYDataset data, 182: PlotRenderingInfo info) { 183: return new XYBarRendererState(info); 184: } 185: 186: /** 187: * Returns the range of values the renderer requires to display all the 188: * items from the specified dataset. 189: * 190: * @param dataset the dataset (<code>null</code> permitted). 191: * 192: * @return The range (<code>null</code> if the dataset is <code>null</code> 193: * or empty). 194: */ 195: public Range findRangeBounds(XYDataset dataset) { 196: if (dataset != null) { 197: if (this.renderAsPercentages) { 198: return new Range(0.0, 1.0); 199: } 200: else { 201: return DatasetUtilities.findStackedRangeBounds( 202: (TableXYDataset) dataset); 203: } 204: } 205: else { 206: return null; 207: } 208: } 209: 210: /** 211: * Draws the visual representation of a single data item. 212: * 213: * @param g2 the graphics device. 214: * @param state the renderer state. 215: * @param dataArea the area within which the plot is being drawn. 216: * @param info collects information about the drawing. 217: * @param plot the plot (can be used to obtain standard color information 218: * etc). 219: * @param domainAxis the domain axis. 220: * @param rangeAxis the range axis. 221: * @param dataset the dataset. 222: * @param series the series index (zero-based). 223: * @param item the item index (zero-based). 224: * @param crosshairState crosshair information for the plot 225: * (<code>null</code> permitted). 226: * @param pass the pass index. 227: */ 228: public void drawItem(Graphics2D g2, 229: XYItemRendererState state, 230: Rectangle2D dataArea, 231: PlotRenderingInfo info, 232: XYPlot plot, 233: ValueAxis domainAxis, 234: ValueAxis rangeAxis, 235: XYDataset dataset, 236: int series, 237: int item, 238: CrosshairState crosshairState, 239: int pass) { 240: 241: if (!(dataset instanceof IntervalXYDataset 242: && dataset instanceof TableXYDataset)) { 243: String message = "dataset (type " + dataset.getClass().getName() 244: + ") has wrong type:"; 245: boolean and = false; 246: if (!IntervalXYDataset.class.isAssignableFrom(dataset.getClass())) { 247: message += " it is no IntervalXYDataset"; 248: and = true; 249: } 250: if (!TableXYDataset.class.isAssignableFrom(dataset.getClass())) { 251: if (and) { 252: message += " and"; 253: } 254: message += " it is no TableXYDataset"; 255: } 256: 257: throw new IllegalArgumentException(message); 258: } 259: 260: IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset; 261: double value = intervalDataset.getYValue(series, item); 262: if (Double.isNaN(value)) { 263: return; 264: } 265: 266: // if we are rendering the values as percentages, we need to calculate 267: // the total for the current item. Unfortunately here we end up 268: // repeating the calculation more times than is strictly necessary - 269: // hopefully I'll come back to this and find a way to add the 270: // total(s) to the renderer state. The other problem is we implicitly 271: // assume the dataset has no negative values...perhaps that can be 272: // fixed too. 273: double total = 0.0; 274: if (this.renderAsPercentages) { 275: total = DatasetUtilities.calculateStackTotal( 276: (TableXYDataset) dataset, item); 277: value = value / total; 278: } 279: 280: double positiveBase = 0.0; 281: double negativeBase = 0.0; 282: 283: for (int i = 0; i < series; i++) { 284: double v = dataset.getYValue(i, item); 285: if (!Double.isNaN(v)) { 286: if (this.renderAsPercentages) { 287: v = v / total; 288: } 289: if (v > 0) { 290: positiveBase = positiveBase + v; 291: } 292: else { 293: negativeBase = negativeBase + v; 294: } 295: } 296: } 297: 298: double translatedBase; 299: double translatedValue; 300: RectangleEdge edgeR = plot.getRangeAxisEdge(); 301: if (value > 0.0) { 302: translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 303: edgeR); 304: translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 305: dataArea, edgeR); 306: } 307: else { 308: translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 309: edgeR); 310: translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 311: dataArea, edgeR); 312: } 313: 314: RectangleEdge edgeD = plot.getDomainAxisEdge(); 315: double startX = intervalDataset.getStartXValue(series, item); 316: if (Double.isNaN(startX)) { 317: return; 318: } 319: double translatedStartX = domainAxis.valueToJava2D(startX, dataArea, 320: edgeD); 321: 322: double endX = intervalDataset.getEndXValue(series, item); 323: if (Double.isNaN(endX)) { 324: return; 325: } 326: double translatedEndX = domainAxis.valueToJava2D(endX, dataArea, edgeD); 327: 328: double translatedWidth = Math.max(1, Math.abs(translatedEndX 329: - translatedStartX)); 330: double translatedHeight = Math.abs(translatedValue - translatedBase); 331: if (getMargin() > 0.0) { 332: double cut = translatedWidth * getMargin(); 333: translatedWidth = translatedWidth - cut; 334: translatedStartX = translatedStartX + cut / 2; 335: } 336: 337: Rectangle2D bar = null; 338: PlotOrientation orientation = plot.getOrientation(); 339: if (orientation == PlotOrientation.HORIZONTAL) { 340: bar = new Rectangle2D.Double(Math.min(translatedBase, 341: translatedValue), translatedEndX, translatedHeight, 342: translatedWidth); 343: } 344: else if (orientation == PlotOrientation.VERTICAL) { 345: bar = new Rectangle2D.Double(translatedStartX, 346: Math.min(translatedBase, translatedValue), 347: translatedWidth, translatedHeight); 348: } 349: 350: if (pass == 0) { 351: Paint itemPaint = getItemPaint(series, item); 352: if (getGradientPaintTransformer() 353: != null && itemPaint instanceof GradientPaint) { 354: GradientPaint gp = (GradientPaint) itemPaint; 355: itemPaint = getGradientPaintTransformer().transform(gp, bar); 356: } 357: g2.setPaint(itemPaint); 358: g2.fill(bar); 359: if (isDrawBarOutline() 360: && Math.abs(translatedEndX - translatedStartX) > 3) { 361: g2.setStroke(getItemStroke(series, item)); 362: g2.setPaint(getItemOutlinePaint(series, item)); 363: g2.draw(bar); 364: } 365: 366: // add an entity for the item... 367: if (info != null) { 368: EntityCollection entities = info.getOwner().getEntityCollection(); 369: if (entities != null) { 370: addEntity(entities, bar, dataset, series, item, 371: bar.getCenterX(), bar.getCenterY()); 372: } 373: } 374: } 375: else if (pass == 1) { 376: // handle item label drawing, now that we know all the bars have 377: // been drawn... 378: if (isItemLabelVisible(series, item)) { 379: XYItemLabelGenerator generator = getItemLabelGenerator(series, 380: item); 381: drawItemLabel(g2, dataset, series, item, plot, generator, bar, 382: value < 0.0); 383: } 384: } 385: 386: } 387: 388: /** 389: * Tests this renderer for equality with an arbitrary object. 390: * 391: * @param obj the object (<code>null</code> permitted). 392: * 393: * @return A boolean. 394: */ 395: public boolean equals(Object obj) { 396: if (obj == this) { 397: return true; 398: } 399: if (!(obj instanceof StackedXYBarRenderer)) { 400: return false; 401: } 402: StackedXYBarRenderer that = (StackedXYBarRenderer) obj; 403: if (this.renderAsPercentages != that.renderAsPercentages) { 404: return false; 405: } 406: return super.equals(obj); 407: } 408: 409: /** 410: * Returns a hash code for this instance. 411: * 412: * @return A hash code. 413: */ 414: public int hashCode() { 415: int result = super.hashCode(); 416: result = result * 37 + (this.renderAsPercentages ? 1 : 0 ); 417: return result; 418: } 419: 420: }