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