Source for org.jfree.chart.renderer.category.ScatterRenderer

   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:  * ScatterRenderer.java
  29:  * --------------------
  30:  * (C) Copyright 2007, by Object Refinery Limited and Contributors.
  31:  *
  32:  * Original Author:  David Gilbert (for Object Refinery Limited);
  33:  * Contributor(s):   David Forslund;
  34:  *
  35:  * Changes
  36:  * -------
  37:  * 08-Oct-2007 : Version 1, based on patch 1780779 by David Forslund (DG);
  38:  * 11-Oct-2007 : Renamed ScatterRenderer (DG);
  39:  *
  40:  */
  41: 
  42: package org.jfree.chart.renderer.category;
  43: 
  44: import java.awt.Graphics2D;
  45: import java.awt.Paint;
  46: import java.awt.Shape;
  47: import java.awt.Stroke;
  48: import java.awt.geom.Line2D;
  49: import java.awt.geom.Rectangle2D;
  50: import java.io.IOException;
  51: import java.io.ObjectInputStream;
  52: import java.io.ObjectOutputStream;
  53: import java.io.Serializable;
  54: import java.util.List;
  55: 
  56: import org.jfree.chart.LegendItem;
  57: import org.jfree.chart.axis.CategoryAxis;
  58: import org.jfree.chart.axis.ValueAxis;
  59: import org.jfree.chart.event.RendererChangeEvent;
  60: import org.jfree.chart.plot.CategoryPlot;
  61: import org.jfree.chart.plot.PlotOrientation;
  62: import org.jfree.data.category.CategoryDataset;
  63: import org.jfree.data.statistics.MultiValueCategoryDataset;
  64: import org.jfree.util.BooleanList;
  65: import org.jfree.util.BooleanUtilities;
  66: import org.jfree.util.ObjectUtilities;
  67: import org.jfree.util.PublicCloneable;
  68: import org.jfree.util.ShapeUtilities;
  69: 
  70: /**
  71:  * A renderer that handles the multiple values from a 
  72:  * {@link MultiValueCategoryDataset} by plotting a shape for each value for 
  73:  * each given item in the dataset.
  74:  * 
  75:  * @since 1.0.7
  76:  */
  77: public class ScatterRenderer extends AbstractCategoryItemRenderer
  78:         implements Cloneable, PublicCloneable, Serializable {
  79: 
  80:     /**
  81:      * A table of flags that control (per series) whether or not shapes are
  82:      * filled.
  83:      */
  84:     private BooleanList seriesShapesFilled;
  85: 
  86:     /**
  87:      * The default value returned by the getShapeFilled() method.
  88:      */
  89:     private boolean baseShapesFilled;
  90: 
  91:     /**
  92:      * A flag that controls whether the fill paint is used for filling
  93:      * shapes.
  94:      */
  95:     private boolean useFillPaint;
  96: 
  97:     /**
  98:      * A flag that controls whether outlines are drawn for shapes.
  99:      */
 100:     private boolean drawOutlines;
 101: 
 102:     /**
 103:      * A flag that controls whether the outline paint is used for drawing shape
 104:      * outlines - if not, the regular series paint is used.
 105:      */
 106:     private boolean useOutlinePaint;
 107: 
 108:     /**
 109:      * A flag that controls whether or not the x-position for each item is
 110:      * offset within the category according to the series.
 111:      */
 112:     private boolean useSeriesOffset;
 113: 
 114:     /**
 115:      * The item margin used for series offsetting - this allows the positioning
 116:      * to match the bar positions of the {@link BarRenderer} class.
 117:      */
 118:     private double itemMargin;
 119: 
 120:     /**
 121:      * Constructs a new renderer.
 122:      */
 123:     public ScatterRenderer() {
 124:         this.seriesShapesFilled = new BooleanList();
 125:         this.baseShapesFilled = true;
 126:         this.useFillPaint = false;
 127:         this.drawOutlines = false;
 128:         this.useOutlinePaint = false;
 129:         this.useSeriesOffset = true;
 130:         this.itemMargin = 0.20;
 131:     }
 132: 
 133:     /**
 134:      * Returns the flag that controls whether or not the x-position for each
 135:      * data item is offset within the category according to the series.
 136:      * 
 137:      * @return A boolean.
 138:      * 
 139:      * @see #setUseSeriesOffset(boolean)
 140:      */
 141:     public boolean getUseSeriesOffset() {
 142:         return this.useSeriesOffset;
 143:     }
 144:     
 145:     /**
 146:      * Sets the flag that controls whether or not the x-position for each 
 147:      * data item is offset within its category according to the series, and
 148:      * sends a {@link RendererChangeEvent} to all registered listeners.
 149:      * 
 150:      * @param offset  the offset.
 151:      * 
 152:      * @see #getUseSeriesOffset()
 153:      */
 154:     public void setUseSeriesOffset(boolean offset) {
 155:         this.useSeriesOffset = offset;
 156:         notifyListeners(new RendererChangeEvent(this));
 157:     }
 158:     
 159:     /**
 160:      * Returns the item margin, which is the gap between items within a 
 161:      * category (expressed as a percentage of the overall category width).  
 162:      * This can be used to match the offset alignment with the bars drawn by 
 163:      * a {@link BarRenderer}).
 164:      * 
 165:      * @return The item margin.
 166:      * 
 167:      * @see #setItemMargin(double)
 168:      * @see #getUseSeriesOffset()
 169:      */
 170:     public double getItemMargin() {
 171:         return this.itemMargin;
 172:     }
 173:     
 174:     /**
 175:      * Sets the item margin, which is the gap between items within a category
 176:      * (expressed as a percentage of the overall category width), and sends
 177:      * a {@link RendererChangeEvent} to all registered listeners.
 178:      * 
 179:      * @param margin  the margin (0.0 <= margin < 1.0).
 180:      * 
 181:      * @see #getItemMargin()
 182:      * @see #getUseSeriesOffset()
 183:      */
 184:     public void setItemMargin(double margin) {
 185:         if (margin < 0.0 || margin >= 1.0) {
 186:             throw new IllegalArgumentException("Requires 0.0 <= margin < 1.0.");
 187:         }
 188:         this.itemMargin = margin;
 189:         notifyListeners(new RendererChangeEvent(this));
 190:     }
 191: 
 192:     /**
 193:      * Returns <code>true</code> if outlines should be drawn for shapes, and
 194:      * <code>false</code> otherwise.
 195:      *
 196:      * @return A boolean.
 197:      * 
 198:      * @see #setDrawOutlines(boolean)
 199:      */
 200:     public boolean getDrawOutlines() {
 201:         return this.drawOutlines;
 202:     }
 203: 
 204:     /**
 205:      * Sets the flag that controls whether outlines are drawn for
 206:      * shapes, and sends a {@link RendererChangeEvent} to all registered
 207:      * listeners.
 208:      * <p/>
 209:      * In some cases, shapes look better if they do NOT have an outline, but
 210:      * this flag allows you to set your own preference.
 211:      *
 212:      * @param flag the flag.
 213:      * 
 214:      * @see #getDrawOutlines()
 215:      */
 216:     public void setDrawOutlines(boolean flag) {
 217:         this.drawOutlines = flag;
 218:         notifyListeners(new RendererChangeEvent(this));
 219:     }
 220: 
 221:     /**
 222:      * Returns the flag that controls whether the outline paint is used for
 223:      * shape outlines.  If not, the regular series paint is used.
 224:      *
 225:      * @return A boolean.
 226:      * 
 227:      * @see #setUseOutlinePaint(boolean)
 228:      */
 229:     public boolean getUseOutlinePaint() {
 230:         return this.useOutlinePaint;
 231:     }
 232: 
 233:     /**
 234:      * Sets the flag that controls whether the outline paint is used for shape
 235:      * outlines.
 236:      *
 237:      * @param use the flag.
 238:      * 
 239:      * @see #getUseOutlinePaint()
 240:      */
 241:     public void setUseOutlinePaint(boolean use) {
 242:         this.useOutlinePaint = use;
 243:         notifyListeners(new RendererChangeEvent(this));
 244:     }
 245: 
 246:     // SHAPES FILLED
 247: 
 248:     /**
 249:      * Returns the flag used to control whether or not the shape for an item
 250:      * is filled. The default implementation passes control to the
 251:      * <code>getSeriesShapesFilled</code> method. You can override this method
 252:      * if you require different behaviour.
 253:      *
 254:      * @param series the series index (zero-based).
 255:      * @param item   the item index (zero-based).
 256:      * @return A boolean.
 257:      */
 258:     public boolean getItemShapeFilled(int series, int item) {
 259:         return getSeriesShapesFilled(series);
 260:     }
 261: 
 262:     /**
 263:      * Returns the flag used to control whether or not the shapes for a series
 264:      * are filled.
 265:      *
 266:      * @param series the series index (zero-based).
 267:      * @return A boolean.
 268:      */
 269:     public boolean getSeriesShapesFilled(int series) {
 270:         Boolean flag = this.seriesShapesFilled.getBoolean(series);
 271:         if (flag != null) {
 272:             return flag.booleanValue();
 273:         } 
 274:         else {
 275:             return this.baseShapesFilled;
 276:         }
 277: 
 278:     }
 279: 
 280:     /**
 281:      * Sets the 'shapes filled' flag for a series.
 282:      *
 283:      * @param series the series index (zero-based).
 284:      * @param filled the flag.
 285:      */
 286:     public void setSeriesShapesFilled(int series, Boolean filled) {
 287:         this.seriesShapesFilled.setBoolean(series, filled);
 288:         notifyListeners(new RendererChangeEvent(this));
 289:     }
 290: 
 291:     /**
 292:      * Sets the 'shapes filled' flag for a series.
 293:      *
 294:      * @param series the series index (zero-based).
 295:      * @param filled the flag.
 296:      */
 297:     public void setSeriesShapesFilled(int series, boolean filled) {
 298:         this.seriesShapesFilled.setBoolean(series, 
 299:                 BooleanUtilities.valueOf(filled));
 300:         notifyListeners(new RendererChangeEvent(this));
 301:     }
 302: 
 303:     /**
 304:      * Returns the base 'shape filled' attribute.
 305:      *
 306:      * @return The base flag.
 307:      */
 308:     public boolean getBaseShapesFilled() {
 309:         return this.baseShapesFilled;
 310:     }
 311: 
 312:     /**
 313:      * Sets the base 'shapes filled' flag.
 314:      *
 315:      * @param flag the flag.
 316:      */
 317:     public void setBaseShapesFilled(boolean flag) {
 318:         this.baseShapesFilled = flag;
 319:         notifyListeners(new RendererChangeEvent(this));
 320:     }
 321: 
 322:     /**
 323:      * Returns <code>true</code> if the renderer should use the fill paint
 324:      * setting to fill shapes, and <code>false</code> if it should just
 325:      * use the regular paint.
 326:      *
 327:      * @return A boolean.
 328:      */
 329:     public boolean getUseFillPaint() {
 330:         return this.useFillPaint;
 331:     }
 332: 
 333:     /**
 334:      * Sets the flag that controls whether the fill paint is used to fill
 335:      * shapes, and sends a {@link RendererChangeEvent} to all
 336:      * registered listeners.
 337:      *
 338:      * @param flag the flag.
 339:      */
 340:     public void setUseFillPaint(boolean flag) {
 341:         this.useFillPaint = flag;
 342:         notifyListeners(new RendererChangeEvent(this));
 343:     }
 344:     
 345:     /**
 346:      * Draw a single data item.
 347:      *
 348:      * @param g2  the graphics device.
 349:      * @param state  the renderer state.
 350:      * @param dataArea  the area in which the data is drawn.
 351:      * @param plot  the plot.
 352:      * @param domainAxis  the domain axis.
 353:      * @param rangeAxis  the range axis.
 354:      * @param dataset  the dataset.
 355:      * @param row  the row index (zero-based).
 356:      * @param column  the column index (zero-based).
 357:      * @param pass  the pass index.
 358:      */
 359:     public void drawItem(Graphics2D g2, CategoryItemRendererState state,
 360:             Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
 361:             ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
 362:             int pass) {
 363: 
 364:         // do nothing if item is not visible
 365:         if (!getItemVisible(row, column)) {
 366:             return;   
 367:         }
 368: 
 369:         PlotOrientation orientation = plot.getOrientation();
 370: 
 371:         MultiValueCategoryDataset d = (MultiValueCategoryDataset) dataset;
 372:         List values = d.getValues(row, column);
 373:         if (values == null) {
 374:             return;
 375:         }
 376:         int valueCount = values.size();
 377:         for (int i = 0; i < valueCount; i++) {
 378:             // current data point...
 379:             double x1;
 380:             if (this.useSeriesOffset) {
 381:                 x1 = domainAxis.getCategorySeriesMiddle(dataset.getColumnKey(
 382:                         column), dataset.getRowKey(row), dataset, 
 383:                         this.itemMargin, dataArea, plot.getDomainAxisEdge());
 384:             }
 385:             else {
 386:                 x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 
 387:                         dataArea, plot.getDomainAxisEdge());
 388:             }
 389:             Number n = (Number) values.get(i);
 390:             double value = n.doubleValue();
 391:             double y1 = rangeAxis.valueToJava2D(value, dataArea, 
 392:                     plot.getRangeAxisEdge());
 393: 
 394:             Shape shape = getItemShape(row, column);
 395:             if (orientation == PlotOrientation.HORIZONTAL) {
 396:                 shape = ShapeUtilities.createTranslatedShape(shape, y1, x1);
 397:             }
 398:             else if (orientation == PlotOrientation.VERTICAL) {
 399:                 shape = ShapeUtilities.createTranslatedShape(shape, x1, y1);
 400:             }
 401:             if (getItemShapeFilled(row, column)) {
 402:                 if (this.useFillPaint) {
 403:                     g2.setPaint(getItemFillPaint(row, column));
 404:                 }
 405:                 else {
 406:                     g2.setPaint(getItemPaint(row, column));   
 407:                 }
 408:                 g2.fill(shape);
 409:             }
 410:             if (this.drawOutlines) {
 411:                 if (this.useOutlinePaint) {
 412:                     g2.setPaint(getItemOutlinePaint(row, column));   
 413:                 }
 414:                 else {
 415:                     g2.setPaint(getItemPaint(row, column));
 416:                 }
 417:                 g2.setStroke(getItemOutlineStroke(row, column));
 418:                 g2.draw(shape);
 419:             }
 420:         }
 421: 
 422:     }
 423:     
 424:     /**
 425:      * Returns a legend item for a series.
 426:      *
 427:      * @param datasetIndex  the dataset index (zero-based).
 428:      * @param series  the series index (zero-based).
 429:      *
 430:      * @return The legend item.
 431:      */
 432:     public LegendItem getLegendItem(int datasetIndex, int series) {
 433: 
 434:         CategoryPlot cp = getPlot();
 435:         if (cp == null) {
 436:             return null;
 437:         }
 438: 
 439:         if (isSeriesVisible(series) && isSeriesVisibleInLegend(series)) {
 440:             CategoryDataset dataset = cp.getDataset(datasetIndex);
 441:             String label = getLegendItemLabelGenerator().generateLabel(
 442:                     dataset, series);
 443:             String description = label;
 444:             String toolTipText = null; 
 445:             if (getLegendItemToolTipGenerator() != null) {
 446:                 toolTipText = getLegendItemToolTipGenerator().generateLabel(
 447:                         dataset, series);   
 448:             }
 449:             String urlText = null;
 450:             if (getLegendItemURLGenerator() != null) {
 451:                 urlText = getLegendItemURLGenerator().generateLabel(
 452:                         dataset, series);   
 453:             }
 454:             Shape shape = lookupSeriesShape(series);
 455:             Paint paint = lookupSeriesPaint(series);
 456:             Paint fillPaint = (this.useFillPaint 
 457:                     ? getItemFillPaint(series, 0) : paint);
 458:             boolean shapeOutlineVisible = this.drawOutlines;
 459:             Paint outlinePaint = (this.useOutlinePaint 
 460:                     ? getItemOutlinePaint(series, 0) : paint);
 461:             Stroke outlineStroke = lookupSeriesOutlineStroke(series);
 462:             LegendItem result = new LegendItem(label, description, toolTipText, 
 463:                     urlText, true, shape, getItemShapeFilled(series, 0),
 464:                     fillPaint, shapeOutlineVisible, outlinePaint, outlineStroke,
 465:                     false, new Line2D.Double(-7.0, 0.0, 7.0, 0.0),
 466:                     getItemStroke(series, 0), getItemPaint(series, 0));
 467:             result.setDataset(dataset);
 468:             result.setDatasetIndex(datasetIndex);
 469:             result.setSeriesKey(dataset.getRowKey(series));
 470:             result.setSeriesIndex(series);
 471:             return result;
 472:         }
 473:         return null;
 474: 
 475:     }
 476: 
 477:     /**
 478:      * Tests this renderer for equality with an arbitrary object.
 479:      *
 480:      * @param obj the object (<code>null</code> permitted).
 481:      * @return A boolean.
 482:      */
 483:     public boolean equals(Object obj) {
 484:         if (obj == this) {
 485:             return true;
 486:         }
 487:         if (!(obj instanceof ScatterRenderer)) {
 488:             return false;
 489:         }
 490:         ScatterRenderer that = (ScatterRenderer) obj;
 491:         if (!ObjectUtilities.equal(this.seriesShapesFilled,
 492:                 that.seriesShapesFilled)) {
 493:             return false;
 494:         }
 495:         if (this.baseShapesFilled != that.baseShapesFilled) {
 496:             return false;
 497:         }
 498:         if (this.useFillPaint != that.useFillPaint) {
 499:             return false;
 500:         }
 501:         if (this.drawOutlines != that.drawOutlines) {
 502:             return false;
 503:         }
 504:         if (this.useOutlinePaint != that.useOutlinePaint) {
 505:             return false;
 506:         }
 507:         if (this.useSeriesOffset != that.useSeriesOffset) {
 508:             return false;
 509:         }
 510:         if (this.itemMargin != that.itemMargin) {
 511:             return false;
 512:         }
 513:         return super.equals(obj);
 514:     }
 515: 
 516:     /**
 517:      * Returns an independent copy of the renderer.
 518:      * 
 519:      * @return A clone.
 520:      * 
 521:      * @throws CloneNotSupportedException  should not happen.
 522:      */
 523:     public Object clone() throws CloneNotSupportedException {
 524:         ScatterRenderer clone = (ScatterRenderer) super.clone();
 525:         clone.seriesShapesFilled 
 526:                 = (BooleanList) this.seriesShapesFilled.clone();
 527:         return clone;
 528:     }
 529: 
 530:     /**
 531:      * Provides serialization support.
 532:      *
 533:      * @param stream the output stream.
 534:      * @throws java.io.IOException if there is an I/O error.
 535:      */
 536:     private void writeObject(ObjectOutputStream stream) throws IOException {
 537:         stream.defaultWriteObject();
 538: 
 539:     }
 540: 
 541:     /**
 542:      * Provides serialization support.
 543:      *
 544:      * @param stream the input stream.
 545:      * @throws java.io.IOException    if there is an I/O error.
 546:      * @throws ClassNotFoundException if there is a classpath problem.
 547:      */
 548:     private void readObject(ObjectInputStream stream)
 549:             throws IOException, ClassNotFoundException {
 550:         stream.defaultReadObject();
 551: 
 552:     }
 553: 
 554: }