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 * StatisticalLineAndShapeRenderer.java 029 * ------------------------------------ 030 * (C) Copyright 2005-present, by David Gilbert and Contributors. 031 * 032 * Original Author: Mofeed Shahin; 033 * Contributor(s): David Gilbert; 034 * Peter Kolb (patch 2497611); 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.Objects; 051 052import org.jfree.chart.HashUtils; 053import org.jfree.chart.axis.CategoryAxis; 054import org.jfree.chart.axis.ValueAxis; 055import org.jfree.chart.entity.EntityCollection; 056import org.jfree.chart.event.RendererChangeEvent; 057import org.jfree.chart.plot.CategoryPlot; 058import org.jfree.chart.plot.PlotOrientation; 059import org.jfree.chart.ui.RectangleEdge; 060import org.jfree.chart.util.PaintUtils; 061import org.jfree.chart.util.PublicCloneable; 062import org.jfree.chart.util.SerialUtils; 063import org.jfree.chart.util.ShapeUtils; 064import org.jfree.data.Range; 065import org.jfree.data.category.CategoryDataset; 066import org.jfree.data.statistics.StatisticalCategoryDataset; 067 068/** 069 * A renderer that draws shapes for each data item, and lines between data 070 * items. Each point has a mean value and a standard deviation line. For use 071 * with the {@link CategoryPlot} class. The example shown 072 * here is generated by the {@code StatisticalLineChartDemo1.java} program 073 * included in the JFreeChart Demo Collection: 074 * <br><br> 075 * <img src="doc-files/StatisticalLineRendererSample.png" 076 * alt="StatisticalLineRendererSample.png"> 077 */ 078public class StatisticalLineAndShapeRenderer extends LineAndShapeRenderer 079 implements Cloneable, PublicCloneable, Serializable { 080 081 /** For serialization. */ 082 private static final long serialVersionUID = -3557517173697777579L; 083 084 /** The paint used to show the error indicator. */ 085 private transient Paint errorIndicatorPaint; 086 087 /** 088 * The stroke used to draw the error indicators. If null, the renderer 089 * will use the itemOutlineStroke. 090 */ 091 private transient Stroke errorIndicatorStroke; 092 093 /** 094 * Constructs a default renderer (draws shapes and lines). 095 */ 096 public StatisticalLineAndShapeRenderer() { 097 this(true, true); 098 } 099 100 /** 101 * Constructs a new renderer. 102 * 103 * @param linesVisible draw lines? 104 * @param shapesVisible draw shapes? 105 */ 106 public StatisticalLineAndShapeRenderer(boolean linesVisible, 107 boolean shapesVisible) { 108 super(linesVisible, shapesVisible); 109 this.errorIndicatorPaint = null; 110 this.errorIndicatorStroke = null; 111 } 112 113 /** 114 * Returns the paint used for the error indicators. 115 * 116 * @return The paint used for the error indicators (possibly 117 * {@code null}). 118 * 119 * @see #setErrorIndicatorPaint(Paint) 120 */ 121 public Paint getErrorIndicatorPaint() { 122 return this.errorIndicatorPaint; 123 } 124 125 /** 126 * Sets the paint used for the error indicators (if {@code null}, 127 * the item paint is used instead) and sends a 128 * {@link RendererChangeEvent} to all registered listeners. 129 * 130 * @param paint the paint ({@code null} permitted). 131 * 132 * @see #getErrorIndicatorPaint() 133 */ 134 public void setErrorIndicatorPaint(Paint paint) { 135 this.errorIndicatorPaint = paint; 136 fireChangeEvent(); 137 } 138 139 /** 140 * Returns the stroke used for the error indicators. 141 * 142 * @return The stroke used for the error indicators (possibly 143 * {@code null}). 144 * 145 * @see #setErrorIndicatorStroke(Stroke) 146 */ 147 public Stroke getErrorIndicatorStroke() { 148 return this.errorIndicatorStroke; 149 } 150 151 /** 152 * Sets the stroke used for the error indicators (if {@code null}, 153 * the item outline stroke is used instead) and sends a 154 * {@link RendererChangeEvent} to all registered listeners. 155 * 156 * @param stroke the stroke ({@code null} permitted). 157 * 158 * @see #getErrorIndicatorStroke() 159 */ 160 public void setErrorIndicatorStroke(Stroke stroke) { 161 this.errorIndicatorStroke = stroke; 162 fireChangeEvent(); 163 } 164 165 /** 166 * Returns the range of values the renderer requires to display all the 167 * items from the specified dataset. 168 * 169 * @param dataset the dataset ({@code null} permitted). 170 * 171 * @return The range (or {@code null} if the dataset is 172 * {@code null} or empty). 173 */ 174 @Override 175 public Range findRangeBounds(CategoryDataset dataset) { 176 return findRangeBounds(dataset, true); 177 } 178 179 /** 180 * Draw a single data item. 181 * 182 * @param g2 the graphics device. 183 * @param state the renderer state. 184 * @param dataArea the area in which the data is drawn. 185 * @param plot the plot. 186 * @param domainAxis the domain axis. 187 * @param rangeAxis the range axis. 188 * @param dataset the dataset (a {@link StatisticalCategoryDataset} is 189 * required). 190 * @param row the row index (zero-based). 191 * @param column the column index (zero-based). 192 * @param pass the pass. 193 */ 194 @Override 195 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 196 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 197 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 198 int pass) { 199 200 // do nothing if item is not visible 201 if (!getItemVisible(row, column)) { 202 return; 203 } 204 205 // if the dataset is not a StatisticalCategoryDataset then just revert 206 // to the superclass (LineAndShapeRenderer) behaviour... 207 if (!(dataset instanceof StatisticalCategoryDataset)) { 208 super.drawItem(g2, state, dataArea, plot, domainAxis, rangeAxis, 209 dataset, row, column, pass); 210 return; 211 } 212 213 int visibleRow = state.getVisibleSeriesIndex(row); 214 if (visibleRow < 0) { 215 return; 216 } 217 int visibleRowCount = state.getVisibleSeriesCount(); 218 219 StatisticalCategoryDataset statDataset 220 = (StatisticalCategoryDataset) dataset; 221 Number meanValue = statDataset.getMeanValue(row, column); 222 if (meanValue == null) { 223 return; 224 } 225 PlotOrientation orientation = plot.getOrientation(); 226 227 // current data point... 228 double x1; 229 if (getUseSeriesOffset()) { 230 x1 = domainAxis.getCategorySeriesMiddle(column, 231 dataset.getColumnCount(), 232 visibleRow, visibleRowCount, 233 getItemMargin(), dataArea, plot.getDomainAxisEdge()); 234 } 235 else { 236 x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 237 dataArea, plot.getDomainAxisEdge()); 238 } 239 double y1 = rangeAxis.valueToJava2D(meanValue.doubleValue(), dataArea, 240 plot.getRangeAxisEdge()); 241 242 // draw the standard deviation lines *before* the shapes (if they're 243 // visible) - it looks better if the shape fill colour is different to 244 // the line colour 245 Number sdv = statDataset.getStdDevValue(row, column); 246 if (pass == 1 && sdv != null) { 247 //standard deviation lines 248 RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); 249 double valueDelta = sdv.doubleValue(); 250 double highVal, lowVal; 251 if ((meanValue.doubleValue() + valueDelta) 252 > rangeAxis.getRange().getUpperBound()) { 253 highVal = rangeAxis.valueToJava2D( 254 rangeAxis.getRange().getUpperBound(), dataArea, 255 yAxisLocation); 256 } 257 else { 258 highVal = rangeAxis.valueToJava2D(meanValue.doubleValue() 259 + valueDelta, dataArea, yAxisLocation); 260 } 261 262 if ((meanValue.doubleValue() + valueDelta) 263 < rangeAxis.getRange().getLowerBound()) { 264 lowVal = rangeAxis.valueToJava2D( 265 rangeAxis.getRange().getLowerBound(), dataArea, 266 yAxisLocation); 267 } 268 else { 269 lowVal = rangeAxis.valueToJava2D(meanValue.doubleValue() 270 - valueDelta, dataArea, yAxisLocation); 271 } 272 273 if (this.errorIndicatorPaint != null) { 274 g2.setPaint(this.errorIndicatorPaint); 275 } 276 else { 277 g2.setPaint(getItemPaint(row, column)); 278 } 279 if (this.errorIndicatorStroke != null) { 280 g2.setStroke(this.errorIndicatorStroke); 281 } 282 else { 283 g2.setStroke(getItemOutlineStroke(row, column)); 284 } 285 Line2D line = new Line2D.Double(); 286 if (orientation == PlotOrientation.HORIZONTAL) { 287 line.setLine(lowVal, x1, highVal, x1); 288 g2.draw(line); 289 line.setLine(lowVal, x1 - 5.0d, lowVal, x1 + 5.0d); 290 g2.draw(line); 291 line.setLine(highVal, x1 - 5.0d, highVal, x1 + 5.0d); 292 g2.draw(line); 293 } 294 else { // PlotOrientation.VERTICAL 295 line.setLine(x1, lowVal, x1, highVal); 296 g2.draw(line); 297 line.setLine(x1 - 5.0d, highVal, x1 + 5.0d, highVal); 298 g2.draw(line); 299 line.setLine(x1 - 5.0d, lowVal, x1 + 5.0d, lowVal); 300 g2.draw(line); 301 } 302 303 } 304 305 Shape hotspot = null; 306 if (pass == 1 && getItemShapeVisible(row, column)) { 307 Shape shape = getItemShape(row, column); 308 if (orientation == PlotOrientation.HORIZONTAL) { 309 shape = ShapeUtils.createTranslatedShape(shape, y1, x1); 310 } 311 else if (orientation == PlotOrientation.VERTICAL) { 312 shape = ShapeUtils.createTranslatedShape(shape, x1, y1); 313 } 314 hotspot = shape; 315 316 if (getItemShapeFilled(row, column)) { 317 if (getUseFillPaint()) { 318 g2.setPaint(getItemFillPaint(row, column)); 319 } 320 else { 321 g2.setPaint(getItemPaint(row, column)); 322 } 323 g2.fill(shape); 324 } 325 if (getDrawOutlines()) { 326 if (getUseOutlinePaint()) { 327 g2.setPaint(getItemOutlinePaint(row, column)); 328 } 329 else { 330 g2.setPaint(getItemPaint(row, column)); 331 } 332 g2.setStroke(getItemOutlineStroke(row, column)); 333 g2.draw(shape); 334 } 335 // draw the item label if there is one... 336 if (isItemLabelVisible(row, column)) { 337 if (orientation == PlotOrientation.HORIZONTAL) { 338 drawItemLabel(g2, orientation, dataset, row, column, 339 y1, x1, (meanValue.doubleValue() < 0.0)); 340 } 341 else if (orientation == PlotOrientation.VERTICAL) { 342 drawItemLabel(g2, orientation, dataset, row, column, 343 x1, y1, (meanValue.doubleValue() < 0.0)); 344 } 345 } 346 } 347 348 if (pass == 0 && getItemLineVisible(row, column)) { 349 if (column != 0) { 350 351 Number previousValue = statDataset.getValue(row, column - 1); 352 if (previousValue != null) { 353 354 // previous data point... 355 double previous = previousValue.doubleValue(); 356 double x0; 357 if (getUseSeriesOffset()) { 358 x0 = domainAxis.getCategorySeriesMiddle( 359 column - 1, dataset.getColumnCount(), 360 visibleRow, visibleRowCount, 361 getItemMargin(), dataArea, 362 plot.getDomainAxisEdge()); 363 } 364 else { 365 x0 = domainAxis.getCategoryMiddle(column - 1, 366 getColumnCount(), dataArea, 367 plot.getDomainAxisEdge()); 368 } 369 double y0 = rangeAxis.valueToJava2D(previous, dataArea, 370 plot.getRangeAxisEdge()); 371 372 Line2D line = null; 373 if (orientation == PlotOrientation.HORIZONTAL) { 374 line = new Line2D.Double(y0, x0, y1, x1); 375 } 376 else if (orientation == PlotOrientation.VERTICAL) { 377 line = new Line2D.Double(x0, y0, x1, y1); 378 } 379 g2.setPaint(getItemPaint(row, column)); 380 g2.setStroke(getItemStroke(row, column)); 381 g2.draw(line); 382 } 383 } 384 } 385 386 if (pass == 1) { 387 // add an item entity, if this information is being collected 388 EntityCollection entities = state.getEntityCollection(); 389 if (entities != null) { 390 addEntity(entities, hotspot, dataset, row, column, x1, y1); 391 } 392 } 393 394 } 395 396 /** 397 * Tests this renderer for equality with an arbitrary object. 398 * 399 * @param obj the object ({@code null} permitted). 400 * 401 * @return A boolean. 402 */ 403 @Override 404 public boolean equals(Object obj) { 405 if (obj == this) { 406 return true; 407 } 408 if (!(obj instanceof StatisticalLineAndShapeRenderer)) { 409 return false; 410 } 411 StatisticalLineAndShapeRenderer that 412 = (StatisticalLineAndShapeRenderer) obj; 413 if (!PaintUtils.equal(this.errorIndicatorPaint, 414 that.errorIndicatorPaint)) { 415 return false; 416 } 417 if (!Objects.equals(this.errorIndicatorStroke, 418 that.errorIndicatorStroke)) { 419 return false; 420 } 421 return super.equals(obj); 422 } 423 424 /** 425 * Returns a hash code for this instance. 426 * 427 * @return A hash code. 428 */ 429 @Override 430 public int hashCode() { 431 int hash = super.hashCode(); 432 hash = HashUtils.hashCode(hash, this.errorIndicatorPaint); 433 return hash; 434 } 435 436 /** 437 * Provides serialization support. 438 * 439 * @param stream the output stream. 440 * 441 * @throws IOException if there is an I/O error. 442 */ 443 private void writeObject(ObjectOutputStream stream) throws IOException { 444 stream.defaultWriteObject(); 445 SerialUtils.writePaint(this.errorIndicatorPaint, stream); 446 SerialUtils.writeStroke(this.errorIndicatorStroke, stream); 447 } 448 449 /** 450 * Provides serialization support. 451 * 452 * @param stream the input stream. 453 * 454 * @throws IOException if there is an I/O error. 455 * @throws ClassNotFoundException if there is a classpath problem. 456 */ 457 private void readObject(ObjectInputStream stream) 458 throws IOException, ClassNotFoundException { 459 stream.defaultReadObject(); 460 this.errorIndicatorPaint = SerialUtils.readPaint(stream); 461 this.errorIndicatorStroke = SerialUtils.readStroke(stream); 462 } 463 464}