Source for org.jfree.chart.renderer.xy.StackedXYBarRenderer

   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: }