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