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: fireChangeEvent(); 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: fireChangeEvent(); 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: fireChangeEvent(); 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, and sends a {@link RendererChangeEvent} to all registered 236: * listeners. 237: * 238: * @param use the flag. 239: * 240: * @see #getUseOutlinePaint() 241: */ 242: public void setUseOutlinePaint(boolean use) { 243: this.useOutlinePaint = use; 244: fireChangeEvent(); 245: } 246: 247: // SHAPES FILLED 248: 249: /** 250: * Returns the flag used to control whether or not the shape for an item 251: * is filled. The default implementation passes control to the 252: * <code>getSeriesShapesFilled</code> method. You can override this method 253: * if you require different behaviour. 254: * 255: * @param series the series index (zero-based). 256: * @param item the item index (zero-based). 257: * @return A boolean. 258: */ 259: public boolean getItemShapeFilled(int series, int item) { 260: return getSeriesShapesFilled(series); 261: } 262: 263: /** 264: * Returns the flag used to control whether or not the shapes for a series 265: * are filled. 266: * 267: * @param series the series index (zero-based). 268: * @return A boolean. 269: */ 270: public boolean getSeriesShapesFilled(int series) { 271: Boolean flag = this.seriesShapesFilled.getBoolean(series); 272: if (flag != null) { 273: return flag.booleanValue(); 274: } 275: else { 276: return this.baseShapesFilled; 277: } 278: 279: } 280: 281: /** 282: * Sets the 'shapes filled' flag for a series and sends a 283: * {@link RendererChangeEvent} to all registered listeners. 284: * 285: * @param series the series index (zero-based). 286: * @param filled the flag. 287: */ 288: public void setSeriesShapesFilled(int series, Boolean filled) { 289: this.seriesShapesFilled.setBoolean(series, filled); 290: fireChangeEvent(); 291: } 292: 293: /** 294: * Sets the 'shapes filled' flag for a series and sends a 295: * {@link RendererChangeEvent} to all registered listeners. 296: * 297: * @param series the series index (zero-based). 298: * @param filled the flag. 299: */ 300: public void setSeriesShapesFilled(int series, boolean filled) { 301: this.seriesShapesFilled.setBoolean(series, 302: BooleanUtilities.valueOf(filled)); 303: fireChangeEvent(); 304: } 305: 306: /** 307: * Returns the base 'shape filled' attribute. 308: * 309: * @return The base flag. 310: */ 311: public boolean getBaseShapesFilled() { 312: return this.baseShapesFilled; 313: } 314: 315: /** 316: * Sets the base 'shapes filled' flag and sends a 317: * {@link RendererChangeEvent} to all registered listeners. 318: * 319: * @param flag the flag. 320: */ 321: public void setBaseShapesFilled(boolean flag) { 322: this.baseShapesFilled = flag; 323: fireChangeEvent(); 324: } 325: 326: /** 327: * Returns <code>true</code> if the renderer should use the fill paint 328: * setting to fill shapes, and <code>false</code> if it should just 329: * use the regular paint. 330: * 331: * @return A boolean. 332: */ 333: public boolean getUseFillPaint() { 334: return this.useFillPaint; 335: } 336: 337: /** 338: * Sets the flag that controls whether the fill paint is used to fill 339: * shapes, and sends a {@link RendererChangeEvent} to all 340: * registered listeners. 341: * 342: * @param flag the flag. 343: */ 344: public void setUseFillPaint(boolean flag) { 345: this.useFillPaint = flag; 346: fireChangeEvent(); 347: } 348: 349: /** 350: * Draw a single data item. 351: * 352: * @param g2 the graphics device. 353: * @param state the renderer state. 354: * @param dataArea the area in which the data is drawn. 355: * @param plot the plot. 356: * @param domainAxis the domain axis. 357: * @param rangeAxis the range axis. 358: * @param dataset the dataset. 359: * @param row the row index (zero-based). 360: * @param column the column index (zero-based). 361: * @param pass the pass index. 362: */ 363: public void drawItem(Graphics2D g2, CategoryItemRendererState state, 364: Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 365: ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 366: int pass) { 367: 368: // do nothing if item is not visible 369: if (!getItemVisible(row, column)) { 370: return; 371: } 372: 373: PlotOrientation orientation = plot.getOrientation(); 374: 375: MultiValueCategoryDataset d = (MultiValueCategoryDataset) dataset; 376: List values = d.getValues(row, column); 377: if (values == null) { 378: return; 379: } 380: int valueCount = values.size(); 381: for (int i = 0; i < valueCount; i++) { 382: // current data point... 383: double x1; 384: if (this.useSeriesOffset) { 385: x1 = domainAxis.getCategorySeriesMiddle(dataset.getColumnKey( 386: column), dataset.getRowKey(row), dataset, 387: this.itemMargin, dataArea, plot.getDomainAxisEdge()); 388: } 389: else { 390: x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 391: dataArea, plot.getDomainAxisEdge()); 392: } 393: Number n = (Number) values.get(i); 394: double value = n.doubleValue(); 395: double y1 = rangeAxis.valueToJava2D(value, dataArea, 396: plot.getRangeAxisEdge()); 397: 398: Shape shape = getItemShape(row, column); 399: if (orientation == PlotOrientation.HORIZONTAL) { 400: shape = ShapeUtilities.createTranslatedShape(shape, y1, x1); 401: } 402: else if (orientation == PlotOrientation.VERTICAL) { 403: shape = ShapeUtilities.createTranslatedShape(shape, x1, y1); 404: } 405: if (getItemShapeFilled(row, column)) { 406: if (this.useFillPaint) { 407: g2.setPaint(getItemFillPaint(row, column)); 408: } 409: else { 410: g2.setPaint(getItemPaint(row, column)); 411: } 412: g2.fill(shape); 413: } 414: if (this.drawOutlines) { 415: if (this.useOutlinePaint) { 416: g2.setPaint(getItemOutlinePaint(row, column)); 417: } 418: else { 419: g2.setPaint(getItemPaint(row, column)); 420: } 421: g2.setStroke(getItemOutlineStroke(row, column)); 422: g2.draw(shape); 423: } 424: } 425: 426: } 427: 428: /** 429: * Returns a legend item for a series. 430: * 431: * @param datasetIndex the dataset index (zero-based). 432: * @param series the series index (zero-based). 433: * 434: * @return The legend item. 435: */ 436: public LegendItem getLegendItem(int datasetIndex, int series) { 437: 438: CategoryPlot cp = getPlot(); 439: if (cp == null) { 440: return null; 441: } 442: 443: if (isSeriesVisible(series) && isSeriesVisibleInLegend(series)) { 444: CategoryDataset dataset = cp.getDataset(datasetIndex); 445: String label = getLegendItemLabelGenerator().generateLabel( 446: dataset, series); 447: String description = label; 448: String toolTipText = null; 449: if (getLegendItemToolTipGenerator() != null) { 450: toolTipText = getLegendItemToolTipGenerator().generateLabel( 451: dataset, series); 452: } 453: String urlText = null; 454: if (getLegendItemURLGenerator() != null) { 455: urlText = getLegendItemURLGenerator().generateLabel( 456: dataset, series); 457: } 458: Shape shape = lookupSeriesShape(series); 459: Paint paint = lookupSeriesPaint(series); 460: Paint fillPaint = (this.useFillPaint 461: ? getItemFillPaint(series, 0) : paint); 462: boolean shapeOutlineVisible = this.drawOutlines; 463: Paint outlinePaint = (this.useOutlinePaint 464: ? getItemOutlinePaint(series, 0) : paint); 465: Stroke outlineStroke = lookupSeriesOutlineStroke(series); 466: LegendItem result = new LegendItem(label, description, toolTipText, 467: urlText, true, shape, getItemShapeFilled(series, 0), 468: fillPaint, shapeOutlineVisible, outlinePaint, outlineStroke, 469: false, new Line2D.Double(-7.0, 0.0, 7.0, 0.0), 470: getItemStroke(series, 0), getItemPaint(series, 0)); 471: result.setDataset(dataset); 472: result.setDatasetIndex(datasetIndex); 473: result.setSeriesKey(dataset.getRowKey(series)); 474: result.setSeriesIndex(series); 475: return result; 476: } 477: return null; 478: 479: } 480: 481: /** 482: * Tests this renderer for equality with an arbitrary object. 483: * 484: * @param obj the object (<code>null</code> permitted). 485: * @return A boolean. 486: */ 487: public boolean equals(Object obj) { 488: if (obj == this) { 489: return true; 490: } 491: if (!(obj instanceof ScatterRenderer)) { 492: return false; 493: } 494: ScatterRenderer that = (ScatterRenderer) obj; 495: if (!ObjectUtilities.equal(this.seriesShapesFilled, 496: that.seriesShapesFilled)) { 497: return false; 498: } 499: if (this.baseShapesFilled != that.baseShapesFilled) { 500: return false; 501: } 502: if (this.useFillPaint != that.useFillPaint) { 503: return false; 504: } 505: if (this.drawOutlines != that.drawOutlines) { 506: return false; 507: } 508: if (this.useOutlinePaint != that.useOutlinePaint) { 509: return false; 510: } 511: if (this.useSeriesOffset != that.useSeriesOffset) { 512: return false; 513: } 514: if (this.itemMargin != that.itemMargin) { 515: return false; 516: } 517: return super.equals(obj); 518: } 519: 520: /** 521: * Returns an independent copy of the renderer. 522: * 523: * @return A clone. 524: * 525: * @throws CloneNotSupportedException should not happen. 526: */ 527: public Object clone() throws CloneNotSupportedException { 528: ScatterRenderer clone = (ScatterRenderer) super.clone(); 529: clone.seriesShapesFilled 530: = (BooleanList) this.seriesShapesFilled.clone(); 531: return clone; 532: } 533: 534: /** 535: * Provides serialization support. 536: * 537: * @param stream the output stream. 538: * @throws java.io.IOException if there is an I/O error. 539: */ 540: private void writeObject(ObjectOutputStream stream) throws IOException { 541: stream.defaultWriteObject(); 542: 543: } 544: 545: /** 546: * Provides serialization support. 547: * 548: * @param stream the input stream. 549: * @throws java.io.IOException if there is an I/O error. 550: * @throws ClassNotFoundException if there is a classpath problem. 551: */ 552: private void readObject(ObjectInputStream stream) 553: throws IOException, ClassNotFoundException { 554: stream.defaultReadObject(); 555: 556: } 557: 558: }