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