Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors. 6: * 7: * Project Info: http://www.jfree.org/jfreechart/index.html 8: * 9: * This library is free software; you can redistribute it and/or modify it 10: * under the terms of the GNU Lesser General Public License as published by 11: * the Free Software Foundation; either version 2.1 of the License, or 12: * (at your option) any later version. 13: * 14: * This library is distributed in the hope that it will be useful, but 15: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17: * License for more details. 18: * 19: * You should have received a copy of the GNU Lesser General Public 20: * License along with this library; if not, write to the Free Software 21: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22: * USA. 23: * 24: * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 25: * in the United States and other countries.] 26: * 27: * -------------------- 28: * ScatterRenderer.java 29: * -------------------- 30: * (C) Copyright 2007, by Object Refinery Limited and Contributors. 31: * 32: * Original Author: David Gilbert (for Object Refinery Limited); 33: * Contributor(s): David Forslund; 34: * 35: * Changes 36: * ------- 37: * 08-Oct-2007 : Version 1, based on patch 1780779 by David Forslund (DG); 38: * 11-Oct-2007 : Renamed ScatterRenderer (DG); 39: * 40: */ 41: 42: package org.jfree.chart.renderer.category; 43: 44: import java.awt.Graphics2D; 45: import java.awt.Paint; 46: import java.awt.Shape; 47: import java.awt.Stroke; 48: import java.awt.geom.Line2D; 49: import java.awt.geom.Rectangle2D; 50: import java.io.IOException; 51: import java.io.ObjectInputStream; 52: import java.io.ObjectOutputStream; 53: import java.io.Serializable; 54: import java.util.List; 55: 56: import org.jfree.chart.LegendItem; 57: import org.jfree.chart.axis.CategoryAxis; 58: import org.jfree.chart.axis.ValueAxis; 59: import org.jfree.chart.event.RendererChangeEvent; 60: import org.jfree.chart.plot.CategoryPlot; 61: import org.jfree.chart.plot.PlotOrientation; 62: import org.jfree.data.category.CategoryDataset; 63: import org.jfree.data.statistics.MultiValueCategoryDataset; 64: import org.jfree.util.BooleanList; 65: import org.jfree.util.BooleanUtilities; 66: import org.jfree.util.ObjectUtilities; 67: import org.jfree.util.PublicCloneable; 68: import org.jfree.util.ShapeUtilities; 69: 70: /** 71: * A renderer that handles the multiple values from a 72: * {@link MultiValueCategoryDataset} by plotting a shape for each value for 73: * each given item in the dataset. 74: * 75: * @since 1.0.7 76: */ 77: public class ScatterRenderer extends AbstractCategoryItemRenderer 78: implements Cloneable, PublicCloneable, Serializable { 79: 80: /** 81: * A table of flags that control (per series) whether or not shapes are 82: * filled. 83: */ 84: private BooleanList seriesShapesFilled; 85: 86: /** 87: * The default value returned by the getShapeFilled() method. 88: */ 89: private boolean baseShapesFilled; 90: 91: /** 92: * A flag that controls whether the fill paint is used for filling 93: * shapes. 94: */ 95: private boolean useFillPaint; 96: 97: /** 98: * A flag that controls whether outlines are drawn for shapes. 99: */ 100: private boolean drawOutlines; 101: 102: /** 103: * A flag that controls whether the outline paint is used for drawing shape 104: * outlines - if not, the regular series paint is used. 105: */ 106: private boolean useOutlinePaint; 107: 108: /** 109: * A flag that controls whether or not the x-position for each item is 110: * offset within the category according to the series. 111: */ 112: private boolean useSeriesOffset; 113: 114: /** 115: * The item margin used for series offsetting - this allows the positioning 116: * to match the bar positions of the {@link BarRenderer} class. 117: */ 118: private double itemMargin; 119: 120: /** 121: * Constructs a new renderer. 122: */ 123: public ScatterRenderer() { 124: this.seriesShapesFilled = new BooleanList(); 125: this.baseShapesFilled = true; 126: this.useFillPaint = false; 127: this.drawOutlines = false; 128: this.useOutlinePaint = false; 129: this.useSeriesOffset = true; 130: this.itemMargin = 0.20; 131: } 132: 133: /** 134: * Returns the flag that controls whether or not the x-position for each 135: * data item is offset within the category according to the series. 136: * 137: * @return A boolean. 138: * 139: * @see #setUseSeriesOffset(boolean) 140: */ 141: public boolean getUseSeriesOffset() { 142: return this.useSeriesOffset; 143: } 144: 145: /** 146: * Sets the flag that controls whether or not the x-position for each 147: * data item is offset within its category according to the series, and 148: * sends a {@link RendererChangeEvent} to all registered listeners. 149: * 150: * @param offset the offset. 151: * 152: * @see #getUseSeriesOffset() 153: */ 154: public void setUseSeriesOffset(boolean offset) { 155: this.useSeriesOffset = offset; 156: notifyListeners(new RendererChangeEvent(this)); 157: } 158: 159: /** 160: * Returns the item margin, which is the gap between items within a 161: * category (expressed as a percentage of the overall category width). 162: * This can be used to match the offset alignment with the bars drawn by 163: * a {@link BarRenderer}). 164: * 165: * @return The item margin. 166: * 167: * @see #setItemMargin(double) 168: * @see #getUseSeriesOffset() 169: */ 170: public double getItemMargin() { 171: return this.itemMargin; 172: } 173: 174: /** 175: * Sets the item margin, which is the gap between items within a category 176: * (expressed as a percentage of the overall category width), and sends 177: * a {@link RendererChangeEvent} to all registered listeners. 178: * 179: * @param margin the margin (0.0 <= margin < 1.0). 180: * 181: * @see #getItemMargin() 182: * @see #getUseSeriesOffset() 183: */ 184: public void setItemMargin(double margin) { 185: if (margin < 0.0 || margin >= 1.0) { 186: throw new IllegalArgumentException("Requires 0.0 <= margin < 1.0."); 187: } 188: this.itemMargin = margin; 189: notifyListeners(new RendererChangeEvent(this)); 190: } 191: 192: /** 193: * Returns <code>true</code> if outlines should be drawn for shapes, and 194: * <code>false</code> otherwise. 195: * 196: * @return A boolean. 197: * 198: * @see #setDrawOutlines(boolean) 199: */ 200: public boolean getDrawOutlines() { 201: return this.drawOutlines; 202: } 203: 204: /** 205: * Sets the flag that controls whether outlines are drawn for 206: * shapes, and sends a {@link RendererChangeEvent} to all registered 207: * listeners. 208: * <p/> 209: * In some cases, shapes look better if they do NOT have an outline, but 210: * this flag allows you to set your own preference. 211: * 212: * @param flag the flag. 213: * 214: * @see #getDrawOutlines() 215: */ 216: public void setDrawOutlines(boolean flag) { 217: this.drawOutlines = flag; 218: notifyListeners(new RendererChangeEvent(this)); 219: } 220: 221: /** 222: * Returns the flag that controls whether the outline paint is used for 223: * shape outlines. If not, the regular series paint is used. 224: * 225: * @return A boolean. 226: * 227: * @see #setUseOutlinePaint(boolean) 228: */ 229: public boolean getUseOutlinePaint() { 230: return this.useOutlinePaint; 231: } 232: 233: /** 234: * Sets the flag that controls whether the outline paint is used for shape 235: * outlines. 236: * 237: * @param use the flag. 238: * 239: * @see #getUseOutlinePaint() 240: */ 241: public void setUseOutlinePaint(boolean use) { 242: this.useOutlinePaint = use; 243: notifyListeners(new RendererChangeEvent(this)); 244: } 245: 246: // SHAPES FILLED 247: 248: /** 249: * Returns the flag used to control whether or not the shape for an item 250: * is filled. The default implementation passes control to the 251: * <code>getSeriesShapesFilled</code> method. You can override this method 252: * if you require different behaviour. 253: * 254: * @param series the series index (zero-based). 255: * @param item the item index (zero-based). 256: * @return A boolean. 257: */ 258: public boolean getItemShapeFilled(int series, int item) { 259: return getSeriesShapesFilled(series); 260: } 261: 262: /** 263: * Returns the flag used to control whether or not the shapes for a series 264: * are filled. 265: * 266: * @param series the series index (zero-based). 267: * @return A boolean. 268: */ 269: public boolean getSeriesShapesFilled(int series) { 270: Boolean flag = this.seriesShapesFilled.getBoolean(series); 271: if (flag != null) { 272: return flag.booleanValue(); 273: } 274: else { 275: return this.baseShapesFilled; 276: } 277: 278: } 279: 280: /** 281: * Sets the 'shapes filled' flag for a series. 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.seriesShapesFilled.setBoolean(series, filled); 288: notifyListeners(new RendererChangeEvent(this)); 289: } 290: 291: /** 292: * Sets the 'shapes filled' flag for a series. 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, 299: BooleanUtilities.valueOf(filled)); 300: notifyListeners(new RendererChangeEvent(this)); 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. 314: * 315: * @param flag the flag. 316: */ 317: public void setBaseShapesFilled(boolean flag) { 318: this.baseShapesFilled = flag; 319: notifyListeners(new RendererChangeEvent(this)); 320: } 321: 322: /** 323: * Returns <code>true</code> if the renderer should use the fill paint 324: * setting to fill shapes, and <code>false</code> 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: notifyListeners(new RendererChangeEvent(this)); 343: } 344: 345: /** 346: * Draw a single data item. 347: * 348: * @param g2 the graphics device. 349: * @param state the renderer state. 350: * @param dataArea the area in which the data is drawn. 351: * @param plot the plot. 352: * @param domainAxis the domain axis. 353: * @param rangeAxis the range axis. 354: * @param dataset the dataset. 355: * @param row the row index (zero-based). 356: * @param column the column index (zero-based). 357: * @param pass the pass index. 358: */ 359: public void drawItem(Graphics2D g2, CategoryItemRendererState state, 360: Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 361: ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 362: int pass) { 363: 364: // do nothing if item is not visible 365: if (!getItemVisible(row, column)) { 366: return; 367: } 368: 369: PlotOrientation orientation = plot.getOrientation(); 370: 371: MultiValueCategoryDataset d = (MultiValueCategoryDataset) dataset; 372: List values = d.getValues(row, column); 373: if (values == null) { 374: return; 375: } 376: int valueCount = values.size(); 377: for (int i = 0; i < valueCount; i++) { 378: // current data point... 379: double x1; 380: if (this.useSeriesOffset) { 381: x1 = domainAxis.getCategorySeriesMiddle(dataset.getColumnKey( 382: column), dataset.getRowKey(row), dataset, 383: this.itemMargin, dataArea, plot.getDomainAxisEdge()); 384: } 385: else { 386: x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 387: dataArea, plot.getDomainAxisEdge()); 388: } 389: Number n = (Number) values.get(i); 390: double value = n.doubleValue(); 391: double y1 = rangeAxis.valueToJava2D(value, dataArea, 392: plot.getRangeAxisEdge()); 393: 394: Shape shape = getItemShape(row, column); 395: if (orientation == PlotOrientation.HORIZONTAL) { 396: shape = ShapeUtilities.createTranslatedShape(shape, y1, x1); 397: } 398: else if (orientation == PlotOrientation.VERTICAL) { 399: shape = ShapeUtilities.createTranslatedShape(shape, x1, y1); 400: } 401: if (getItemShapeFilled(row, column)) { 402: if (this.useFillPaint) { 403: g2.setPaint(getItemFillPaint(row, column)); 404: } 405: else { 406: g2.setPaint(getItemPaint(row, column)); 407: } 408: g2.fill(shape); 409: } 410: if (this.drawOutlines) { 411: if (this.useOutlinePaint) { 412: g2.setPaint(getItemOutlinePaint(row, column)); 413: } 414: else { 415: g2.setPaint(getItemPaint(row, column)); 416: } 417: g2.setStroke(getItemOutlineStroke(row, column)); 418: g2.draw(shape); 419: } 420: } 421: 422: } 423: 424: /** 425: * Returns a legend item for a series. 426: * 427: * @param datasetIndex the dataset index (zero-based). 428: * @param series the series index (zero-based). 429: * 430: * @return The legend item. 431: */ 432: public LegendItem getLegendItem(int datasetIndex, int series) { 433: 434: CategoryPlot cp = getPlot(); 435: if (cp == null) { 436: return null; 437: } 438: 439: if (isSeriesVisible(series) && isSeriesVisibleInLegend(series)) { 440: CategoryDataset dataset = cp.getDataset(datasetIndex); 441: String label = getLegendItemLabelGenerator().generateLabel( 442: dataset, series); 443: String description = label; 444: String toolTipText = null; 445: if (getLegendItemToolTipGenerator() != null) { 446: toolTipText = getLegendItemToolTipGenerator().generateLabel( 447: dataset, series); 448: } 449: String urlText = null; 450: if (getLegendItemURLGenerator() != null) { 451: urlText = getLegendItemURLGenerator().generateLabel( 452: dataset, series); 453: } 454: Shape shape = lookupSeriesShape(series); 455: Paint paint = lookupSeriesPaint(series); 456: Paint fillPaint = (this.useFillPaint 457: ? getItemFillPaint(series, 0) : paint); 458: boolean shapeOutlineVisible = this.drawOutlines; 459: Paint outlinePaint = (this.useOutlinePaint 460: ? getItemOutlinePaint(series, 0) : paint); 461: Stroke outlineStroke = lookupSeriesOutlineStroke(series); 462: LegendItem result = new LegendItem(label, description, toolTipText, 463: urlText, true, shape, getItemShapeFilled(series, 0), 464: fillPaint, shapeOutlineVisible, outlinePaint, outlineStroke, 465: false, new Line2D.Double(-7.0, 0.0, 7.0, 0.0), 466: getItemStroke(series, 0), getItemPaint(series, 0)); 467: result.setDataset(dataset); 468: result.setDatasetIndex(datasetIndex); 469: result.setSeriesKey(dataset.getRowKey(series)); 470: result.setSeriesIndex(series); 471: return result; 472: } 473: return null; 474: 475: } 476: 477: /** 478: * Tests this renderer for equality with an arbitrary object. 479: * 480: * @param obj the object (<code>null</code> permitted). 481: * @return A boolean. 482: */ 483: public boolean equals(Object obj) { 484: if (obj == this) { 485: return true; 486: } 487: if (!(obj instanceof ScatterRenderer)) { 488: return false; 489: } 490: ScatterRenderer that = (ScatterRenderer) obj; 491: if (!ObjectUtilities.equal(this.seriesShapesFilled, 492: that.seriesShapesFilled)) { 493: return false; 494: } 495: if (this.baseShapesFilled != that.baseShapesFilled) { 496: return false; 497: } 498: if (this.useFillPaint != that.useFillPaint) { 499: return false; 500: } 501: if (this.drawOutlines != that.drawOutlines) { 502: return false; 503: } 504: if (this.useOutlinePaint != that.useOutlinePaint) { 505: return false; 506: } 507: if (this.useSeriesOffset != that.useSeriesOffset) { 508: return false; 509: } 510: if (this.itemMargin != that.itemMargin) { 511: return false; 512: } 513: return super.equals(obj); 514: } 515: 516: /** 517: * Returns an independent copy of the renderer. 518: * 519: * @return A clone. 520: * 521: * @throws CloneNotSupportedException should not happen. 522: */ 523: public Object clone() throws CloneNotSupportedException { 524: ScatterRenderer clone = (ScatterRenderer) super.clone(); 525: clone.seriesShapesFilled 526: = (BooleanList) this.seriesShapesFilled.clone(); 527: return clone; 528: } 529: 530: /** 531: * Provides serialization support. 532: * 533: * @param stream the output stream. 534: * @throws java.io.IOException if there is an I/O error. 535: */ 536: private void writeObject(ObjectOutputStream stream) throws IOException { 537: stream.defaultWriteObject(); 538: 539: } 540: 541: /** 542: * Provides serialization support. 543: * 544: * @param stream the input stream. 545: * @throws java.io.IOException if there is an I/O error. 546: * @throws ClassNotFoundException if there is a classpath problem. 547: */ 548: private void readObject(ObjectInputStream stream) 549: throws IOException, ClassNotFoundException { 550: stream.defaultReadObject(); 551: 552: } 553: 554: }