001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * -------------------- 028 * ScatterRenderer.java 029 * -------------------- 030 * (C) Copyright 2007-2009, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): David Forslund; 034 * Peter Kolb (patches 2497611, 2791407); 035 * 036 * Changes 037 * ------- 038 * 08-Oct-2007 : Version 1, based on patch 1780779 by David Forslund (DG); 039 * 11-Oct-2007 : Renamed ScatterRenderer (DG); 040 * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG); 041 * 14-Jan-2009 : Added support for seriesVisible flags (PK); 042 * 16-May-2009 : Patch 2791407 - findRangeBounds() override (PK); 043 * 044 */ 045 046 package org.jfree.chart.renderer.category; 047 048 import java.awt.Graphics2D; 049 import java.awt.Paint; 050 import java.awt.Shape; 051 import java.awt.Stroke; 052 import java.awt.geom.Line2D; 053 import java.awt.geom.Rectangle2D; 054 import java.io.IOException; 055 import java.io.ObjectInputStream; 056 import java.io.ObjectOutputStream; 057 import java.io.Serializable; 058 import java.util.List; 059 060 import org.jfree.chart.LegendItem; 061 import org.jfree.chart.axis.CategoryAxis; 062 import org.jfree.chart.axis.ValueAxis; 063 import org.jfree.chart.event.RendererChangeEvent; 064 import org.jfree.chart.plot.CategoryPlot; 065 import org.jfree.chart.plot.PlotOrientation; 066 import org.jfree.data.Range; 067 import org.jfree.data.category.CategoryDataset; 068 import org.jfree.data.statistics.MultiValueCategoryDataset; 069 import org.jfree.util.BooleanList; 070 import org.jfree.util.BooleanUtilities; 071 import org.jfree.util.ObjectUtilities; 072 import org.jfree.util.PublicCloneable; 073 import org.jfree.util.ShapeUtilities; 074 075 /** 076 * A renderer that handles the multiple values from a 077 * {@link MultiValueCategoryDataset} by plotting a shape for each value for 078 * each given item in the dataset. The example shown here is generated by 079 * the <code>ScatterRendererDemo1.java</code> program included in the 080 * JFreeChart Demo Collection: 081 * <br><br> 082 * <img src="../../../../../images/ScatterRendererSample.png" 083 * alt="ScatterRendererSample.png" /> 084 * 085 * @since 1.0.7 086 */ 087 public class ScatterRenderer extends AbstractCategoryItemRenderer 088 implements Cloneable, PublicCloneable, Serializable { 089 090 /** 091 * A table of flags that control (per series) whether or not shapes are 092 * filled. 093 */ 094 private BooleanList seriesShapesFilled; 095 096 /** 097 * The default value returned by the getShapeFilled() method. 098 */ 099 private boolean baseShapesFilled; 100 101 /** 102 * A flag that controls whether the fill paint is used for filling 103 * shapes. 104 */ 105 private boolean useFillPaint; 106 107 /** 108 * A flag that controls whether outlines are drawn for shapes. 109 */ 110 private boolean drawOutlines; 111 112 /** 113 * A flag that controls whether the outline paint is used for drawing shape 114 * outlines - if not, the regular series paint is used. 115 */ 116 private boolean useOutlinePaint; 117 118 /** 119 * A flag that controls whether or not the x-position for each item is 120 * offset within the category according to the series. 121 */ 122 private boolean useSeriesOffset; 123 124 /** 125 * The item margin used for series offsetting - this allows the positioning 126 * to match the bar positions of the {@link BarRenderer} class. 127 */ 128 private double itemMargin; 129 130 /** 131 * Constructs a new renderer. 132 */ 133 public ScatterRenderer() { 134 this.seriesShapesFilled = new BooleanList(); 135 this.baseShapesFilled = true; 136 this.useFillPaint = false; 137 this.drawOutlines = false; 138 this.useOutlinePaint = false; 139 this.useSeriesOffset = true; 140 this.itemMargin = 0.20; 141 } 142 143 /** 144 * Returns the flag that controls whether or not the x-position for each 145 * data item is offset within the category according to the series. 146 * 147 * @return A boolean. 148 * 149 * @see #setUseSeriesOffset(boolean) 150 */ 151 public boolean getUseSeriesOffset() { 152 return this.useSeriesOffset; 153 } 154 155 /** 156 * Sets the flag that controls whether or not the x-position for each 157 * data item is offset within its category according to the series, and 158 * sends a {@link RendererChangeEvent} to all registered listeners. 159 * 160 * @param offset the offset. 161 * 162 * @see #getUseSeriesOffset() 163 */ 164 public void setUseSeriesOffset(boolean offset) { 165 this.useSeriesOffset = offset; 166 fireChangeEvent(); 167 } 168 169 /** 170 * Returns the item margin, which is the gap between items within a 171 * category (expressed as a percentage of the overall category width). 172 * This can be used to match the offset alignment with the bars drawn by 173 * a {@link BarRenderer}). 174 * 175 * @return The item margin. 176 * 177 * @see #setItemMargin(double) 178 * @see #getUseSeriesOffset() 179 */ 180 public double getItemMargin() { 181 return this.itemMargin; 182 } 183 184 /** 185 * Sets the item margin, which is the gap between items within a category 186 * (expressed as a percentage of the overall category width), and sends 187 * a {@link RendererChangeEvent} to all registered listeners. 188 * 189 * @param margin the margin (0.0 <= margin < 1.0). 190 * 191 * @see #getItemMargin() 192 * @see #getUseSeriesOffset() 193 */ 194 public void setItemMargin(double margin) { 195 if (margin < 0.0 || margin >= 1.0) { 196 throw new IllegalArgumentException("Requires 0.0 <= margin < 1.0."); 197 } 198 this.itemMargin = margin; 199 fireChangeEvent(); 200 } 201 202 /** 203 * Returns <code>true</code> if outlines should be drawn for shapes, and 204 * <code>false</code> otherwise. 205 * 206 * @return A boolean. 207 * 208 * @see #setDrawOutlines(boolean) 209 */ 210 public boolean getDrawOutlines() { 211 return this.drawOutlines; 212 } 213 214 /** 215 * Sets the flag that controls whether outlines are drawn for 216 * shapes, and sends a {@link RendererChangeEvent} to all registered 217 * listeners. 218 * <p/> 219 * In some cases, shapes look better if they do NOT have an outline, but 220 * this flag allows you to set your own preference. 221 * 222 * @param flag the flag. 223 * 224 * @see #getDrawOutlines() 225 */ 226 public void setDrawOutlines(boolean flag) { 227 this.drawOutlines = flag; 228 fireChangeEvent(); 229 } 230 231 /** 232 * Returns the flag that controls whether the outline paint is used for 233 * shape outlines. If not, the regular series paint is used. 234 * 235 * @return A boolean. 236 * 237 * @see #setUseOutlinePaint(boolean) 238 */ 239 public boolean getUseOutlinePaint() { 240 return this.useOutlinePaint; 241 } 242 243 /** 244 * Sets the flag that controls whether the outline paint is used for shape 245 * outlines, and sends a {@link RendererChangeEvent} to all registered 246 * listeners. 247 * 248 * @param use the flag. 249 * 250 * @see #getUseOutlinePaint() 251 */ 252 public void setUseOutlinePaint(boolean use) { 253 this.useOutlinePaint = use; 254 fireChangeEvent(); 255 } 256 257 // SHAPES FILLED 258 259 /** 260 * Returns the flag used to control whether or not the shape for an item 261 * is filled. The default implementation passes control to the 262 * <code>getSeriesShapesFilled</code> method. You can override this method 263 * if you require different behaviour. 264 * 265 * @param series the series index (zero-based). 266 * @param item the item index (zero-based). 267 * @return A boolean. 268 */ 269 public boolean getItemShapeFilled(int series, int item) { 270 return getSeriesShapesFilled(series); 271 } 272 273 /** 274 * Returns the flag used to control whether or not the shapes for a series 275 * are filled. 276 * 277 * @param series the series index (zero-based). 278 * @return A boolean. 279 */ 280 public boolean getSeriesShapesFilled(int series) { 281 Boolean flag = this.seriesShapesFilled.getBoolean(series); 282 if (flag != null) { 283 return flag.booleanValue(); 284 } 285 else { 286 return this.baseShapesFilled; 287 } 288 289 } 290 291 /** 292 * Sets the 'shapes filled' flag for a series and sends a 293 * {@link RendererChangeEvent} to all registered listeners. 294 * 295 * @param series the series index (zero-based). 296 * @param filled the flag. 297 */ 298 public void setSeriesShapesFilled(int series, Boolean filled) { 299 this.seriesShapesFilled.setBoolean(series, filled); 300 fireChangeEvent(); 301 } 302 303 /** 304 * Sets the 'shapes filled' flag for a series and sends a 305 * {@link RendererChangeEvent} to all registered listeners. 306 * 307 * @param series the series index (zero-based). 308 * @param filled the flag. 309 */ 310 public void setSeriesShapesFilled(int series, boolean filled) { 311 this.seriesShapesFilled.setBoolean(series, 312 BooleanUtilities.valueOf(filled)); 313 fireChangeEvent(); 314 } 315 316 /** 317 * Returns the base 'shape filled' attribute. 318 * 319 * @return The base flag. 320 */ 321 public boolean getBaseShapesFilled() { 322 return this.baseShapesFilled; 323 } 324 325 /** 326 * Sets the base 'shapes filled' flag and sends a 327 * {@link RendererChangeEvent} to all registered listeners. 328 * 329 * @param flag the flag. 330 */ 331 public void setBaseShapesFilled(boolean flag) { 332 this.baseShapesFilled = flag; 333 fireChangeEvent(); 334 } 335 336 /** 337 * Returns <code>true</code> if the renderer should use the fill paint 338 * setting to fill shapes, and <code>false</code> if it should just 339 * use the regular paint. 340 * 341 * @return A boolean. 342 */ 343 public boolean getUseFillPaint() { 344 return this.useFillPaint; 345 } 346 347 /** 348 * Sets the flag that controls whether the fill paint is used to fill 349 * shapes, and sends a {@link RendererChangeEvent} to all 350 * registered listeners. 351 * 352 * @param flag the flag. 353 */ 354 public void setUseFillPaint(boolean flag) { 355 this.useFillPaint = flag; 356 fireChangeEvent(); 357 } 358 359 /** 360 * Returns the range of values the renderer requires to display all the 361 * items from the specified dataset. This takes into account the range 362 * between the min/max values, possibly ignoring invisible series. 363 * 364 * @param dataset the dataset (<code>null</code> permitted). 365 * 366 * @return The range (or <code>null</code> if the dataset is 367 * <code>null</code> or empty). 368 */ 369 public Range findRangeBounds(CategoryDataset dataset) { 370 return findRangeBounds(dataset, true); 371 } 372 373 /** 374 * Draw a single data item. 375 * 376 * @param g2 the graphics device. 377 * @param state the renderer state. 378 * @param dataArea the area in which the data is drawn. 379 * @param plot the plot. 380 * @param domainAxis the domain axis. 381 * @param rangeAxis the range axis. 382 * @param dataset the dataset. 383 * @param row the row index (zero-based). 384 * @param column the column index (zero-based). 385 * @param pass the pass index. 386 */ 387 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 388 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 389 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 390 int pass) { 391 392 // do nothing if item is not visible 393 if (!getItemVisible(row, column)) { 394 return; 395 } 396 int visibleRow = state.getVisibleSeriesIndex(row); 397 if (visibleRow < 0) { 398 return; 399 } 400 int visibleRowCount = state.getVisibleSeriesCount(); 401 402 PlotOrientation orientation = plot.getOrientation(); 403 404 MultiValueCategoryDataset d = (MultiValueCategoryDataset) dataset; 405 List values = d.getValues(row, column); 406 if (values == null) { 407 return; 408 } 409 int valueCount = values.size(); 410 for (int i = 0; i < valueCount; i++) { 411 // current data point... 412 double x1; 413 if (this.useSeriesOffset) { 414 x1 = domainAxis.getCategorySeriesMiddle(column, 415 dataset.getColumnCount(), visibleRow, visibleRowCount, 416 this.itemMargin, dataArea, plot.getDomainAxisEdge()); 417 } 418 else { 419 x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 420 dataArea, plot.getDomainAxisEdge()); 421 } 422 Number n = (Number) values.get(i); 423 double value = n.doubleValue(); 424 double y1 = rangeAxis.valueToJava2D(value, dataArea, 425 plot.getRangeAxisEdge()); 426 427 Shape shape = getItemShape(row, column); 428 if (orientation == PlotOrientation.HORIZONTAL) { 429 shape = ShapeUtilities.createTranslatedShape(shape, y1, x1); 430 } 431 else if (orientation == PlotOrientation.VERTICAL) { 432 shape = ShapeUtilities.createTranslatedShape(shape, x1, y1); 433 } 434 if (getItemShapeFilled(row, column)) { 435 if (this.useFillPaint) { 436 g2.setPaint(getItemFillPaint(row, column)); 437 } 438 else { 439 g2.setPaint(getItemPaint(row, column)); 440 } 441 g2.fill(shape); 442 } 443 if (this.drawOutlines) { 444 if (this.useOutlinePaint) { 445 g2.setPaint(getItemOutlinePaint(row, column)); 446 } 447 else { 448 g2.setPaint(getItemPaint(row, column)); 449 } 450 g2.setStroke(getItemOutlineStroke(row, column)); 451 g2.draw(shape); 452 } 453 } 454 455 } 456 457 /** 458 * Returns a legend item for a series. 459 * 460 * @param datasetIndex the dataset index (zero-based). 461 * @param series the series index (zero-based). 462 * 463 * @return The legend item. 464 */ 465 public LegendItem getLegendItem(int datasetIndex, int series) { 466 467 CategoryPlot cp = getPlot(); 468 if (cp == null) { 469 return null; 470 } 471 472 if (isSeriesVisible(series) && isSeriesVisibleInLegend(series)) { 473 CategoryDataset dataset = cp.getDataset(datasetIndex); 474 String label = getLegendItemLabelGenerator().generateLabel( 475 dataset, series); 476 String description = label; 477 String toolTipText = null; 478 if (getLegendItemToolTipGenerator() != null) { 479 toolTipText = getLegendItemToolTipGenerator().generateLabel( 480 dataset, series); 481 } 482 String urlText = null; 483 if (getLegendItemURLGenerator() != null) { 484 urlText = getLegendItemURLGenerator().generateLabel( 485 dataset, series); 486 } 487 Shape shape = lookupLegendShape(series); 488 Paint paint = lookupSeriesPaint(series); 489 Paint fillPaint = (this.useFillPaint 490 ? getItemFillPaint(series, 0) : paint); 491 boolean shapeOutlineVisible = this.drawOutlines; 492 Paint outlinePaint = (this.useOutlinePaint 493 ? getItemOutlinePaint(series, 0) : paint); 494 Stroke outlineStroke = lookupSeriesOutlineStroke(series); 495 LegendItem result = new LegendItem(label, description, toolTipText, 496 urlText, true, shape, getItemShapeFilled(series, 0), 497 fillPaint, shapeOutlineVisible, outlinePaint, outlineStroke, 498 false, new Line2D.Double(-7.0, 0.0, 7.0, 0.0), 499 getItemStroke(series, 0), getItemPaint(series, 0)); 500 result.setLabelFont(lookupLegendTextFont(series)); 501 Paint labelPaint = lookupLegendTextPaint(series); 502 if (labelPaint != null) { 503 result.setLabelPaint(labelPaint); 504 } 505 result.setDataset(dataset); 506 result.setDatasetIndex(datasetIndex); 507 result.setSeriesKey(dataset.getRowKey(series)); 508 result.setSeriesIndex(series); 509 return result; 510 } 511 return null; 512 513 } 514 515 /** 516 * Tests this renderer for equality with an arbitrary object. 517 * 518 * @param obj the object (<code>null</code> permitted). 519 * @return A boolean. 520 */ 521 public boolean equals(Object obj) { 522 if (obj == this) { 523 return true; 524 } 525 if (!(obj instanceof ScatterRenderer)) { 526 return false; 527 } 528 ScatterRenderer that = (ScatterRenderer) obj; 529 if (!ObjectUtilities.equal(this.seriesShapesFilled, 530 that.seriesShapesFilled)) { 531 return false; 532 } 533 if (this.baseShapesFilled != that.baseShapesFilled) { 534 return false; 535 } 536 if (this.useFillPaint != that.useFillPaint) { 537 return false; 538 } 539 if (this.drawOutlines != that.drawOutlines) { 540 return false; 541 } 542 if (this.useOutlinePaint != that.useOutlinePaint) { 543 return false; 544 } 545 if (this.useSeriesOffset != that.useSeriesOffset) { 546 return false; 547 } 548 if (this.itemMargin != that.itemMargin) { 549 return false; 550 } 551 return super.equals(obj); 552 } 553 554 /** 555 * Returns an independent copy of the renderer. 556 * 557 * @return A clone. 558 * 559 * @throws CloneNotSupportedException should not happen. 560 */ 561 public Object clone() throws CloneNotSupportedException { 562 ScatterRenderer clone = (ScatterRenderer) super.clone(); 563 clone.seriesShapesFilled 564 = (BooleanList) this.seriesShapesFilled.clone(); 565 return clone; 566 } 567 568 /** 569 * Provides serialization support. 570 * 571 * @param stream the output stream. 572 * @throws java.io.IOException if there is an I/O error. 573 */ 574 private void writeObject(ObjectOutputStream stream) throws IOException { 575 stream.defaultWriteObject(); 576 577 } 578 579 /** 580 * Provides serialization support. 581 * 582 * @param stream the input stream. 583 * @throws java.io.IOException if there is an I/O error. 584 * @throws ClassNotFoundException if there is a classpath problem. 585 */ 586 private void readObject(ObjectInputStream stream) 587 throws IOException, ClassNotFoundException { 588 stream.defaultReadObject(); 589 590 } 591 592 }