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-2008, 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, 2008, 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:         fireChangeEvent();
 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:         fireChangeEvent();
 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:         fireChangeEvent();
 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, and sends a {@link RendererChangeEvent} to all registered
 236:      * listeners.
 237:      *
 238:      * @param use the flag.
 239:      *
 240:      * @see #getUseOutlinePaint()
 241:      */
 242:     public void setUseOutlinePaint(boolean use) {
 243:         this.useOutlinePaint = use;
 244:         fireChangeEvent();
 245:     }
 246: 
 247:     // SHAPES FILLED
 248: 
 249:     /**
 250:      * Returns the flag used to control whether or not the shape for an item
 251:      * is filled. The default implementation passes control to the
 252:      * <code>getSeriesShapesFilled</code> method. You can override this method
 253:      * if you require different behaviour.
 254:      *
 255:      * @param series the series index (zero-based).
 256:      * @param item   the item index (zero-based).
 257:      * @return A boolean.
 258:      */
 259:     public boolean getItemShapeFilled(int series, int item) {
 260:         return getSeriesShapesFilled(series);
 261:     }
 262: 
 263:     /**
 264:      * Returns the flag used to control whether or not the shapes for a series
 265:      * are filled.
 266:      *
 267:      * @param series the series index (zero-based).
 268:      * @return A boolean.
 269:      */
 270:     public boolean getSeriesShapesFilled(int series) {
 271:         Boolean flag = this.seriesShapesFilled.getBoolean(series);
 272:         if (flag != null) {
 273:             return flag.booleanValue();
 274:         }
 275:         else {
 276:             return this.baseShapesFilled;
 277:         }
 278: 
 279:     }
 280: 
 281:     /**
 282:      * Sets the 'shapes filled' flag for a series and sends a
 283:      * {@link RendererChangeEvent} to all registered listeners.
 284:      *
 285:      * @param series the series index (zero-based).
 286:      * @param filled the flag.
 287:      */
 288:     public void setSeriesShapesFilled(int series, Boolean filled) {
 289:         this.seriesShapesFilled.setBoolean(series, filled);
 290:         fireChangeEvent();
 291:     }
 292: 
 293:     /**
 294:      * Sets the 'shapes filled' flag for a series and sends a
 295:      * {@link RendererChangeEvent} to all registered listeners.
 296:      *
 297:      * @param series the series index (zero-based).
 298:      * @param filled the flag.
 299:      */
 300:     public void setSeriesShapesFilled(int series, boolean filled) {
 301:         this.seriesShapesFilled.setBoolean(series,
 302:                 BooleanUtilities.valueOf(filled));
 303:         fireChangeEvent();
 304:     }
 305: 
 306:     /**
 307:      * Returns the base 'shape filled' attribute.
 308:      *
 309:      * @return The base flag.
 310:      */
 311:     public boolean getBaseShapesFilled() {
 312:         return this.baseShapesFilled;
 313:     }
 314: 
 315:     /**
 316:      * Sets the base 'shapes filled' flag and sends a
 317:      * {@link RendererChangeEvent} to all registered listeners.
 318:      *
 319:      * @param flag the flag.
 320:      */
 321:     public void setBaseShapesFilled(boolean flag) {
 322:         this.baseShapesFilled = flag;
 323:         fireChangeEvent();
 324:     }
 325: 
 326:     /**
 327:      * Returns <code>true</code> if the renderer should use the fill paint
 328:      * setting to fill shapes, and <code>false</code> if it should just
 329:      * use the regular paint.
 330:      *
 331:      * @return A boolean.
 332:      */
 333:     public boolean getUseFillPaint() {
 334:         return this.useFillPaint;
 335:     }
 336: 
 337:     /**
 338:      * Sets the flag that controls whether the fill paint is used to fill
 339:      * shapes, and sends a {@link RendererChangeEvent} to all
 340:      * registered listeners.
 341:      *
 342:      * @param flag the flag.
 343:      */
 344:     public void setUseFillPaint(boolean flag) {
 345:         this.useFillPaint = flag;
 346:         fireChangeEvent();
 347:     }
 348: 
 349:     /**
 350:      * Draw a single data item.
 351:      *
 352:      * @param g2  the graphics device.
 353:      * @param state  the renderer state.
 354:      * @param dataArea  the area in which the data is drawn.
 355:      * @param plot  the plot.
 356:      * @param domainAxis  the domain axis.
 357:      * @param rangeAxis  the range axis.
 358:      * @param dataset  the dataset.
 359:      * @param row  the row index (zero-based).
 360:      * @param column  the column index (zero-based).
 361:      * @param pass  the pass index.
 362:      */
 363:     public void drawItem(Graphics2D g2, CategoryItemRendererState state,
 364:             Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
 365:             ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
 366:             int pass) {
 367: 
 368:         // do nothing if item is not visible
 369:         if (!getItemVisible(row, column)) {
 370:             return;
 371:         }
 372: 
 373:         PlotOrientation orientation = plot.getOrientation();
 374: 
 375:         MultiValueCategoryDataset d = (MultiValueCategoryDataset) dataset;
 376:         List values = d.getValues(row, column);
 377:         if (values == null) {
 378:             return;
 379:         }
 380:         int valueCount = values.size();
 381:         for (int i = 0; i < valueCount; i++) {
 382:             // current data point...
 383:             double x1;
 384:             if (this.useSeriesOffset) {
 385:                 x1 = domainAxis.getCategorySeriesMiddle(dataset.getColumnKey(
 386:                         column), dataset.getRowKey(row), dataset,
 387:                         this.itemMargin, dataArea, plot.getDomainAxisEdge());
 388:             }
 389:             else {
 390:                 x1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
 391:                         dataArea, plot.getDomainAxisEdge());
 392:             }
 393:             Number n = (Number) values.get(i);
 394:             double value = n.doubleValue();
 395:             double y1 = rangeAxis.valueToJava2D(value, dataArea,
 396:                     plot.getRangeAxisEdge());
 397: 
 398:             Shape shape = getItemShape(row, column);
 399:             if (orientation == PlotOrientation.HORIZONTAL) {
 400:                 shape = ShapeUtilities.createTranslatedShape(shape, y1, x1);
 401:             }
 402:             else if (orientation == PlotOrientation.VERTICAL) {
 403:                 shape = ShapeUtilities.createTranslatedShape(shape, x1, y1);
 404:             }
 405:             if (getItemShapeFilled(row, column)) {
 406:                 if (this.useFillPaint) {
 407:                     g2.setPaint(getItemFillPaint(row, column));
 408:                 }
 409:                 else {
 410:                     g2.setPaint(getItemPaint(row, column));
 411:                 }
 412:                 g2.fill(shape);
 413:             }
 414:             if (this.drawOutlines) {
 415:                 if (this.useOutlinePaint) {
 416:                     g2.setPaint(getItemOutlinePaint(row, column));
 417:                 }
 418:                 else {
 419:                     g2.setPaint(getItemPaint(row, column));
 420:                 }
 421:                 g2.setStroke(getItemOutlineStroke(row, column));
 422:                 g2.draw(shape);
 423:             }
 424:         }
 425: 
 426:     }
 427: 
 428:     /**
 429:      * Returns a legend item for a series.
 430:      *
 431:      * @param datasetIndex  the dataset index (zero-based).
 432:      * @param series  the series index (zero-based).
 433:      *
 434:      * @return The legend item.
 435:      */
 436:     public LegendItem getLegendItem(int datasetIndex, int series) {
 437: 
 438:         CategoryPlot cp = getPlot();
 439:         if (cp == null) {
 440:             return null;
 441:         }
 442: 
 443:         if (isSeriesVisible(series) && isSeriesVisibleInLegend(series)) {
 444:             CategoryDataset dataset = cp.getDataset(datasetIndex);
 445:             String label = getLegendItemLabelGenerator().generateLabel(
 446:                     dataset, series);
 447:             String description = label;
 448:             String toolTipText = null;
 449:             if (getLegendItemToolTipGenerator() != null) {
 450:                 toolTipText = getLegendItemToolTipGenerator().generateLabel(
 451:                         dataset, series);
 452:             }
 453:             String urlText = null;
 454:             if (getLegendItemURLGenerator() != null) {
 455:                 urlText = getLegendItemURLGenerator().generateLabel(
 456:                         dataset, series);
 457:             }
 458:             Shape shape = lookupSeriesShape(series);
 459:             Paint paint = lookupSeriesPaint(series);
 460:             Paint fillPaint = (this.useFillPaint
 461:                     ? getItemFillPaint(series, 0) : paint);
 462:             boolean shapeOutlineVisible = this.drawOutlines;
 463:             Paint outlinePaint = (this.useOutlinePaint
 464:                     ? getItemOutlinePaint(series, 0) : paint);
 465:             Stroke outlineStroke = lookupSeriesOutlineStroke(series);
 466:             LegendItem result = new LegendItem(label, description, toolTipText,
 467:                     urlText, true, shape, getItemShapeFilled(series, 0),
 468:                     fillPaint, shapeOutlineVisible, outlinePaint, outlineStroke,
 469:                     false, new Line2D.Double(-7.0, 0.0, 7.0, 0.0),
 470:                     getItemStroke(series, 0), getItemPaint(series, 0));
 471:             result.setDataset(dataset);
 472:             result.setDatasetIndex(datasetIndex);
 473:             result.setSeriesKey(dataset.getRowKey(series));
 474:             result.setSeriesIndex(series);
 475:             return result;
 476:         }
 477:         return null;
 478: 
 479:     }
 480: 
 481:     /**
 482:      * Tests this renderer for equality with an arbitrary object.
 483:      *
 484:      * @param obj the object (<code>null</code> permitted).
 485:      * @return A boolean.
 486:      */
 487:     public boolean equals(Object obj) {
 488:         if (obj == this) {
 489:             return true;
 490:         }
 491:         if (!(obj instanceof ScatterRenderer)) {
 492:             return false;
 493:         }
 494:         ScatterRenderer that = (ScatterRenderer) obj;
 495:         if (!ObjectUtilities.equal(this.seriesShapesFilled,
 496:                 that.seriesShapesFilled)) {
 497:             return false;
 498:         }
 499:         if (this.baseShapesFilled != that.baseShapesFilled) {
 500:             return false;
 501:         }
 502:         if (this.useFillPaint != that.useFillPaint) {
 503:             return false;
 504:         }
 505:         if (this.drawOutlines != that.drawOutlines) {
 506:             return false;
 507:         }
 508:         if (this.useOutlinePaint != that.useOutlinePaint) {
 509:             return false;
 510:         }
 511:         if (this.useSeriesOffset != that.useSeriesOffset) {
 512:             return false;
 513:         }
 514:         if (this.itemMargin != that.itemMargin) {
 515:             return false;
 516:         }
 517:         return super.equals(obj);
 518:     }
 519: 
 520:     /**
 521:      * Returns an independent copy of the renderer.
 522:      *
 523:      * @return A clone.
 524:      *
 525:      * @throws CloneNotSupportedException  should not happen.
 526:      */
 527:     public Object clone() throws CloneNotSupportedException {
 528:         ScatterRenderer clone = (ScatterRenderer) super.clone();
 529:         clone.seriesShapesFilled
 530:                 = (BooleanList) this.seriesShapesFilled.clone();
 531:         return clone;
 532:     }
 533: 
 534:     /**
 535:      * Provides serialization support.
 536:      *
 537:      * @param stream the output stream.
 538:      * @throws java.io.IOException if there is an I/O error.
 539:      */
 540:     private void writeObject(ObjectOutputStream stream) throws IOException {
 541:         stream.defaultWriteObject();
 542: 
 543:     }
 544: 
 545:     /**
 546:      * Provides serialization support.
 547:      *
 548:      * @param stream the input stream.
 549:      * @throws java.io.IOException    if there is an I/O error.
 550:      * @throws ClassNotFoundException if there is a classpath problem.
 551:      */
 552:     private void readObject(ObjectInputStream stream)
 553:             throws IOException, ClassNotFoundException {
 554:         stream.defaultReadObject();
 555: 
 556:     }
 557: 
 558: }