001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2014, 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-2014, 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} 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} if outlines should be drawn for shapes, and 204 * {@code false} 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>In some cases, shapes look better if they do NOT have an outline, but 219 * this flag allows you to set your own preference.</p> 220 * 221 * @param flag the flag. 222 * 223 * @see #getDrawOutlines() 224 */ 225 public void setDrawOutlines(boolean flag) { 226 this.drawOutlines = flag; 227 fireChangeEvent(); 228 } 229 230 /** 231 * Returns the flag that controls whether the outline paint is used for 232 * shape outlines. If not, the regular series paint is used. 233 * 234 * @return A boolean. 235 * 236 * @see #setUseOutlinePaint(boolean) 237 */ 238 public boolean getUseOutlinePaint() { 239 return this.useOutlinePaint; 240 } 241 242 /** 243 * Sets the flag that controls whether the outline paint is used for shape 244 * outlines, and sends a {@link RendererChangeEvent} to all registered 245 * listeners. 246 * 247 * @param use the flag. 248 * 249 * @see #getUseOutlinePaint() 250 */ 251 public void setUseOutlinePaint(boolean use) { 252 this.useOutlinePaint = use; 253 fireChangeEvent(); 254 } 255 256 // SHAPES FILLED 257 258 /** 259 * Returns the flag used to control whether or not the shape for an item 260 * is filled. The default implementation passes control to the 261 * {@code getSeriesShapesFilled} method. You can override this method 262 * if you require different behaviour. 263 * 264 * @param series the series index (zero-based). 265 * @param item the item index (zero-based). 266 * @return A boolean. 267 */ 268 public boolean getItemShapeFilled(int series, int item) { 269 return getSeriesShapesFilled(series); 270 } 271 272 /** 273 * Returns the flag used to control whether or not the shapes for a series 274 * are filled. 275 * 276 * @param series the series index (zero-based). 277 * @return A boolean. 278 */ 279 public boolean getSeriesShapesFilled(int series) { 280 Boolean flag = this.seriesShapesFilled.getBoolean(series); 281 if (flag != null) { 282 return flag.booleanValue(); 283 } 284 else { 285 return this.baseShapesFilled; 286 } 287 288 } 289 290 /** 291 * Sets the 'shapes filled' flag for a series and sends a 292 * {@link RendererChangeEvent} to all registered listeners. 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, filled); 299 fireChangeEvent(); 300 } 301 302 /** 303 * Sets the 'shapes filled' flag for a series and sends a 304 * {@link RendererChangeEvent} to all registered listeners. 305 * 306 * @param series the series index (zero-based). 307 * @param filled the flag. 308 */ 309 public void setSeriesShapesFilled(int series, boolean filled) { 310 this.seriesShapesFilled.setBoolean(series, 311 BooleanUtilities.valueOf(filled)); 312 fireChangeEvent(); 313 } 314 315 /** 316 * Returns the base 'shape filled' attribute. 317 * 318 * @return The base flag. 319 */ 320 public boolean getBaseShapesFilled() { 321 return this.baseShapesFilled; 322 } 323 324 /** 325 * Sets the base 'shapes filled' flag and sends a 326 * {@link RendererChangeEvent} to all registered listeners. 327 * 328 * @param flag the flag. 329 */ 330 public void setBaseShapesFilled(boolean flag) { 331 this.baseShapesFilled = flag; 332 fireChangeEvent(); 333 } 334 335 /** 336 * Returns {@code true} if the renderer should use the fill paint 337 * setting to fill shapes, and {@code false} if it should just 338 * use the regular paint. 339 * 340 * @return A boolean. 341 */ 342 public boolean getUseFillPaint() { 343 return this.useFillPaint; 344 } 345 346 /** 347 * Sets the flag that controls whether the fill paint is used to fill 348 * shapes, and sends a {@link RendererChangeEvent} to all 349 * registered listeners. 350 * 351 * @param flag the flag. 352 */ 353 public void setUseFillPaint(boolean flag) { 354 this.useFillPaint = flag; 355 fireChangeEvent(); 356 } 357 358 /** 359 * Returns the range of values the renderer requires to display all the 360 * items from the specified dataset. This takes into account the range 361 * between the min/max values, possibly ignoring invisible series. 362 * 363 * @param dataset the dataset ({@code null} permitted). 364 * 365 * @return The range (or {@code null} if the dataset is 366 * {@code null} or empty). 367 */ 368 @Override 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 @Override 388 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 389 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 390 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 391 int pass) { 392 393 // do nothing if item is not visible 394 if (!getItemVisible(row, column)) { 395 return; 396 } 397 int visibleRow = state.getVisibleSeriesIndex(row); 398 if (visibleRow < 0) { 399 return; 400 } 401 int visibleRowCount = state.getVisibleSeriesCount(); 402 403 PlotOrientation orientation = plot.getOrientation(); 404 405 MultiValueCategoryDataset d = (MultiValueCategoryDataset) dataset; 406 List values = d.getValues(row, column); 407 if (values == null) { 408 return; 409 } 410 int valueCount = values.size(); 411 for (int i = 0; i < valueCount; i++) { 412 // current data point... 413 double x1; 414 if (this.useSeriesOffset) { 415 x1 = domainAxis.getCategorySeriesMiddle(column, 416 dataset.getColumnCount(), visibleRow, visibleRowCount, 417 this.itemMargin, dataArea, plot.getDomainAxisEdge()); 418 } 419 else { 420 x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 421 dataArea, plot.getDomainAxisEdge()); 422 } 423 Number n = (Number) values.get(i); 424 double value = n.doubleValue(); 425 double y1 = rangeAxis.valueToJava2D(value, dataArea, 426 plot.getRangeAxisEdge()); 427 428 Shape shape = getItemShape(row, column); 429 if (orientation == PlotOrientation.HORIZONTAL) { 430 shape = ShapeUtilities.createTranslatedShape(shape, y1, x1); 431 } 432 else if (orientation == PlotOrientation.VERTICAL) { 433 shape = ShapeUtilities.createTranslatedShape(shape, x1, y1); 434 } 435 if (getItemShapeFilled(row, column)) { 436 if (this.useFillPaint) { 437 g2.setPaint(getItemFillPaint(row, column)); 438 } 439 else { 440 g2.setPaint(getItemPaint(row, column)); 441 } 442 g2.fill(shape); 443 } 444 if (this.drawOutlines) { 445 if (this.useOutlinePaint) { 446 g2.setPaint(getItemOutlinePaint(row, column)); 447 } 448 else { 449 g2.setPaint(getItemPaint(row, column)); 450 } 451 g2.setStroke(getItemOutlineStroke(row, column)); 452 g2.draw(shape); 453 } 454 } 455 456 } 457 458 /** 459 * Returns a legend item for a series. 460 * 461 * @param datasetIndex the dataset index (zero-based). 462 * @param series the series index (zero-based). 463 * 464 * @return The legend item. 465 */ 466 @Override 467 public LegendItem getLegendItem(int datasetIndex, int series) { 468 469 CategoryPlot cp = getPlot(); 470 if (cp == null) { 471 return null; 472 } 473 474 if (isSeriesVisible(series) && isSeriesVisibleInLegend(series)) { 475 CategoryDataset dataset = cp.getDataset(datasetIndex); 476 String label = getLegendItemLabelGenerator().generateLabel( 477 dataset, series); 478 String description = label; 479 String toolTipText = null; 480 if (getLegendItemToolTipGenerator() != null) { 481 toolTipText = getLegendItemToolTipGenerator().generateLabel( 482 dataset, series); 483 } 484 String urlText = null; 485 if (getLegendItemURLGenerator() != null) { 486 urlText = getLegendItemURLGenerator().generateLabel( 487 dataset, series); 488 } 489 Shape shape = lookupLegendShape(series); 490 Paint paint = lookupSeriesPaint(series); 491 Paint fillPaint = (this.useFillPaint 492 ? getItemFillPaint(series, 0) : paint); 493 boolean shapeOutlineVisible = this.drawOutlines; 494 Paint outlinePaint = (this.useOutlinePaint 495 ? getItemOutlinePaint(series, 0) : paint); 496 Stroke outlineStroke = lookupSeriesOutlineStroke(series); 497 LegendItem result = new LegendItem(label, description, toolTipText, 498 urlText, true, shape, getItemShapeFilled(series, 0), 499 fillPaint, shapeOutlineVisible, outlinePaint, outlineStroke, 500 false, new Line2D.Double(-7.0, 0.0, 7.0, 0.0), 501 getItemStroke(series, 0), getItemPaint(series, 0)); 502 result.setLabelFont(lookupLegendTextFont(series)); 503 Paint labelPaint = lookupLegendTextPaint(series); 504 if (labelPaint != null) { 505 result.setLabelPaint(labelPaint); 506 } 507 result.setDataset(dataset); 508 result.setDatasetIndex(datasetIndex); 509 result.setSeriesKey(dataset.getRowKey(series)); 510 result.setSeriesIndex(series); 511 return result; 512 } 513 return null; 514 515 } 516 517 /** 518 * Tests this renderer for equality with an arbitrary object. 519 * 520 * @param obj the object ({@code null} permitted). 521 * @return A boolean. 522 */ 523 @Override 524 public boolean equals(Object obj) { 525 if (obj == this) { 526 return true; 527 } 528 if (!(obj instanceof ScatterRenderer)) { 529 return false; 530 } 531 ScatterRenderer that = (ScatterRenderer) obj; 532 if (!ObjectUtilities.equal(this.seriesShapesFilled, 533 that.seriesShapesFilled)) { 534 return false; 535 } 536 if (this.baseShapesFilled != that.baseShapesFilled) { 537 return false; 538 } 539 if (this.useFillPaint != that.useFillPaint) { 540 return false; 541 } 542 if (this.drawOutlines != that.drawOutlines) { 543 return false; 544 } 545 if (this.useOutlinePaint != that.useOutlinePaint) { 546 return false; 547 } 548 if (this.useSeriesOffset != that.useSeriesOffset) { 549 return false; 550 } 551 if (this.itemMargin != that.itemMargin) { 552 return false; 553 } 554 return super.equals(obj); 555 } 556 557 /** 558 * Returns an independent copy of the renderer. 559 * 560 * @return A clone. 561 * 562 * @throws CloneNotSupportedException should not happen. 563 */ 564 @Override 565 public Object clone() throws CloneNotSupportedException { 566 ScatterRenderer clone = (ScatterRenderer) super.clone(); 567 clone.seriesShapesFilled 568 = (BooleanList) this.seriesShapesFilled.clone(); 569 return clone; 570 } 571 572 /** 573 * Provides serialization support. 574 * 575 * @param stream the output stream. 576 * @throws java.io.IOException if there is an I/O error. 577 */ 578 private void writeObject(ObjectOutputStream stream) throws IOException { 579 stream.defaultWriteObject(); 580 581 } 582 583 /** 584 * Provides serialization support. 585 * 586 * @param stream the input stream. 587 * @throws java.io.IOException if there is an I/O error. 588 * @throws ClassNotFoundException if there is a classpath problem. 589 */ 590 private void readObject(ObjectInputStream stream) 591 throws IOException, ClassNotFoundException { 592 stream.defaultReadObject(); 593 594 } 595 596}