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