001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2013, 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 046package org.jfree.chart.renderer.category; 047 048import java.awt.Graphics2D; 049import java.awt.Paint; 050import java.awt.Shape; 051import java.awt.Stroke; 052import java.awt.geom.Line2D; 053import java.awt.geom.Rectangle2D; 054import java.io.IOException; 055import java.io.ObjectInputStream; 056import java.io.ObjectOutputStream; 057import java.io.Serializable; 058import java.util.List; 059 060import org.jfree.chart.LegendItem; 061import org.jfree.chart.axis.CategoryAxis; 062import org.jfree.chart.axis.ValueAxis; 063import org.jfree.chart.event.RendererChangeEvent; 064import org.jfree.chart.plot.CategoryPlot; 065import org.jfree.chart.plot.PlotOrientation; 066import org.jfree.data.Range; 067import org.jfree.data.category.CategoryDataset; 068import org.jfree.data.statistics.MultiValueCategoryDataset; 069import org.jfree.util.BooleanList; 070import org.jfree.util.BooleanUtilities; 071import org.jfree.util.ObjectUtilities; 072import org.jfree.util.PublicCloneable; 073import 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 */ 087public 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 @Override 370 public Range findRangeBounds(CategoryDataset dataset) { 371 return findRangeBounds(dataset, true); 372 } 373 374 /** 375 * Draw a single data item. 376 * 377 * @param g2 the graphics device. 378 * @param state the renderer state. 379 * @param dataArea the area in which the data is drawn. 380 * @param plot the plot. 381 * @param domainAxis the domain axis. 382 * @param rangeAxis the range axis. 383 * @param dataset the dataset. 384 * @param row the row index (zero-based). 385 * @param column the column index (zero-based). 386 * @param pass the pass index. 387 */ 388 @Override 389 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 390 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 391 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 392 int pass) { 393 394 // do nothing if item is not visible 395 if (!getItemVisible(row, column)) { 396 return; 397 } 398 int visibleRow = state.getVisibleSeriesIndex(row); 399 if (visibleRow < 0) { 400 return; 401 } 402 int visibleRowCount = state.getVisibleSeriesCount(); 403 404 PlotOrientation orientation = plot.getOrientation(); 405 406 MultiValueCategoryDataset d = (MultiValueCategoryDataset) dataset; 407 List values = d.getValues(row, column); 408 if (values == null) { 409 return; 410 } 411 int valueCount = values.size(); 412 for (int i = 0; i < valueCount; i++) { 413 // current data point... 414 double x1; 415 if (this.useSeriesOffset) { 416 x1 = domainAxis.getCategorySeriesMiddle(column, 417 dataset.getColumnCount(), visibleRow, visibleRowCount, 418 this.itemMargin, dataArea, plot.getDomainAxisEdge()); 419 } 420 else { 421 x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 422 dataArea, plot.getDomainAxisEdge()); 423 } 424 Number n = (Number) values.get(i); 425 double value = n.doubleValue(); 426 double y1 = rangeAxis.valueToJava2D(value, dataArea, 427 plot.getRangeAxisEdge()); 428 429 Shape shape = getItemShape(row, column); 430 if (orientation == PlotOrientation.HORIZONTAL) { 431 shape = ShapeUtilities.createTranslatedShape(shape, y1, x1); 432 } 433 else if (orientation == PlotOrientation.VERTICAL) { 434 shape = ShapeUtilities.createTranslatedShape(shape, x1, y1); 435 } 436 if (getItemShapeFilled(row, column)) { 437 if (this.useFillPaint) { 438 g2.setPaint(getItemFillPaint(row, column)); 439 } 440 else { 441 g2.setPaint(getItemPaint(row, column)); 442 } 443 g2.fill(shape); 444 } 445 if (this.drawOutlines) { 446 if (this.useOutlinePaint) { 447 g2.setPaint(getItemOutlinePaint(row, column)); 448 } 449 else { 450 g2.setPaint(getItemPaint(row, column)); 451 } 452 g2.setStroke(getItemOutlineStroke(row, column)); 453 g2.draw(shape); 454 } 455 } 456 457 } 458 459 /** 460 * Returns a legend item for a series. 461 * 462 * @param datasetIndex the dataset index (zero-based). 463 * @param series the series index (zero-based). 464 * 465 * @return The legend item. 466 */ 467 @Override 468 public LegendItem getLegendItem(int datasetIndex, int series) { 469 470 CategoryPlot cp = getPlot(); 471 if (cp == null) { 472 return null; 473 } 474 475 if (isSeriesVisible(series) && isSeriesVisibleInLegend(series)) { 476 CategoryDataset dataset = cp.getDataset(datasetIndex); 477 String label = getLegendItemLabelGenerator().generateLabel( 478 dataset, series); 479 String description = label; 480 String toolTipText = null; 481 if (getLegendItemToolTipGenerator() != null) { 482 toolTipText = getLegendItemToolTipGenerator().generateLabel( 483 dataset, series); 484 } 485 String urlText = null; 486 if (getLegendItemURLGenerator() != null) { 487 urlText = getLegendItemURLGenerator().generateLabel( 488 dataset, series); 489 } 490 Shape shape = lookupLegendShape(series); 491 Paint paint = lookupSeriesPaint(series); 492 Paint fillPaint = (this.useFillPaint 493 ? getItemFillPaint(series, 0) : paint); 494 boolean shapeOutlineVisible = this.drawOutlines; 495 Paint outlinePaint = (this.useOutlinePaint 496 ? getItemOutlinePaint(series, 0) : paint); 497 Stroke outlineStroke = lookupSeriesOutlineStroke(series); 498 LegendItem result = new LegendItem(label, description, toolTipText, 499 urlText, true, shape, getItemShapeFilled(series, 0), 500 fillPaint, shapeOutlineVisible, outlinePaint, outlineStroke, 501 false, new Line2D.Double(-7.0, 0.0, 7.0, 0.0), 502 getItemStroke(series, 0), getItemPaint(series, 0)); 503 result.setLabelFont(lookupLegendTextFont(series)); 504 Paint labelPaint = lookupLegendTextPaint(series); 505 if (labelPaint != null) { 506 result.setLabelPaint(labelPaint); 507 } 508 result.setDataset(dataset); 509 result.setDatasetIndex(datasetIndex); 510 result.setSeriesKey(dataset.getRowKey(series)); 511 result.setSeriesIndex(series); 512 return result; 513 } 514 return null; 515 516 } 517 518 /** 519 * Tests this renderer for equality with an arbitrary object. 520 * 521 * @param obj the object (<code>null</code> permitted). 522 * @return A boolean. 523 */ 524 @Override 525 public boolean equals(Object obj) { 526 if (obj == this) { 527 return true; 528 } 529 if (!(obj instanceof ScatterRenderer)) { 530 return false; 531 } 532 ScatterRenderer that = (ScatterRenderer) obj; 533 if (!ObjectUtilities.equal(this.seriesShapesFilled, 534 that.seriesShapesFilled)) { 535 return false; 536 } 537 if (this.baseShapesFilled != that.baseShapesFilled) { 538 return false; 539 } 540 if (this.useFillPaint != that.useFillPaint) { 541 return false; 542 } 543 if (this.drawOutlines != that.drawOutlines) { 544 return false; 545 } 546 if (this.useOutlinePaint != that.useOutlinePaint) { 547 return false; 548 } 549 if (this.useSeriesOffset != that.useSeriesOffset) { 550 return false; 551 } 552 if (this.itemMargin != that.itemMargin) { 553 return false; 554 } 555 return super.equals(obj); 556 } 557 558 /** 559 * Returns an independent copy of the renderer. 560 * 561 * @return A clone. 562 * 563 * @throws CloneNotSupportedException should not happen. 564 */ 565 @Override 566 public Object clone() throws CloneNotSupportedException { 567 ScatterRenderer clone = (ScatterRenderer) super.clone(); 568 clone.seriesShapesFilled 569 = (BooleanList) this.seriesShapesFilled.clone(); 570 return clone; 571 } 572 573 /** 574 * Provides serialization support. 575 * 576 * @param stream the output stream. 577 * @throws java.io.IOException if there is an I/O error. 578 */ 579 private void writeObject(ObjectOutputStream stream) throws IOException { 580 stream.defaultWriteObject(); 581 582 } 583 584 /** 585 * Provides serialization support. 586 * 587 * @param stream the input stream. 588 * @throws java.io.IOException if there is an I/O error. 589 * @throws ClassNotFoundException if there is a classpath problem. 590 */ 591 private void readObject(ObjectInputStream stream) 592 throws IOException, ClassNotFoundException { 593 stream.defaultReadObject(); 594 595 } 596 597}