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 * HighLowRenderer.java 029 * -------------------- 030 * (C) Copyright 2001-2016, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Richard Atkinson; 034 * Christian W. Zuckschwerdt; 035 * 036 */ 037 038package org.jfree.chart.renderer.xy; 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.axis.ValueAxis; 052import org.jfree.chart.entity.EntityCollection; 053import org.jfree.chart.event.RendererChangeEvent; 054import org.jfree.chart.plot.CrosshairState; 055import org.jfree.chart.plot.PlotOrientation; 056import org.jfree.chart.plot.PlotRenderingInfo; 057import org.jfree.chart.plot.XYPlot; 058import org.jfree.chart.ui.RectangleEdge; 059import org.jfree.chart.util.PaintUtils; 060import org.jfree.chart.util.PublicCloneable; 061import org.jfree.chart.util.SerialUtils; 062import org.jfree.data.Range; 063import org.jfree.data.general.DatasetUtils; 064import org.jfree.data.xy.OHLCDataset; 065import org.jfree.data.xy.XYDataset; 066 067/** 068 * A renderer that draws high/low/open/close markers on an {@link XYPlot} 069 * (requires a {@link OHLCDataset}). This renderer does not include code to 070 * calculate the crosshair point for the plot. 071 * 072 * The example shown here is generated by the {@code HighLowChartDemo1.java} 073 * program included in the JFreeChart Demo Collection: 074 * <br><br> 075 * <img src="doc-files/HighLowRendererSample.png" alt="HighLowRendererSample.png"> 076 */ 077public class HighLowRenderer extends AbstractXYItemRenderer 078 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { 079 080 /** For serialization. */ 081 private static final long serialVersionUID = -8135673815876552516L; 082 083 /** A flag that controls whether the open ticks are drawn. */ 084 private boolean drawOpenTicks; 085 086 /** A flag that controls whether the close ticks are drawn. */ 087 private boolean drawCloseTicks; 088 089 /** 090 * The paint used for the open ticks (if {@code null}, the series 091 * paint is used instead). 092 */ 093 private transient Paint openTickPaint; 094 095 /** 096 * The paint used for the close ticks (if {@code null}, the series 097 * paint is used instead). 098 */ 099 private transient Paint closeTickPaint; 100 101 /** 102 * The tick length (in Java2D units). 103 * 104 * @since 1.0.10 105 */ 106 private double tickLength; 107 108 /** 109 * The default constructor. 110 */ 111 public HighLowRenderer() { 112 super(); 113 this.drawOpenTicks = true; 114 this.drawCloseTicks = true; 115 this.tickLength = 2.0; 116 } 117 118 /** 119 * Returns the flag that controls whether open ticks are drawn. 120 * 121 * @return A boolean. 122 * 123 * @see #getDrawCloseTicks() 124 * @see #setDrawOpenTicks(boolean) 125 */ 126 public boolean getDrawOpenTicks() { 127 return this.drawOpenTicks; 128 } 129 130 /** 131 * Sets the flag that controls whether open ticks are drawn, and sends a 132 * {@link RendererChangeEvent} to all registered listeners. 133 * 134 * @param draw the flag. 135 * 136 * @see #getDrawOpenTicks() 137 */ 138 public void setDrawOpenTicks(boolean draw) { 139 this.drawOpenTicks = draw; 140 fireChangeEvent(); 141 } 142 143 /** 144 * Returns the flag that controls whether close ticks are drawn. 145 * 146 * @return A boolean. 147 * 148 * @see #getDrawOpenTicks() 149 * @see #setDrawCloseTicks(boolean) 150 */ 151 public boolean getDrawCloseTicks() { 152 return this.drawCloseTicks; 153 } 154 155 /** 156 * Sets the flag that controls whether close ticks are drawn, and sends a 157 * {@link RendererChangeEvent} to all registered listeners. 158 * 159 * @param draw the flag. 160 * 161 * @see #getDrawCloseTicks() 162 */ 163 public void setDrawCloseTicks(boolean draw) { 164 this.drawCloseTicks = draw; 165 fireChangeEvent(); 166 } 167 168 /** 169 * Returns the paint used to draw the ticks for the open values. 170 * 171 * @return The paint used to draw the ticks for the open values (possibly 172 * {@code null}). 173 * 174 * @see #setOpenTickPaint(Paint) 175 */ 176 public Paint getOpenTickPaint() { 177 return this.openTickPaint; 178 } 179 180 /** 181 * Sets the paint used to draw the ticks for the open values and sends a 182 * {@link RendererChangeEvent} to all registered listeners. If you set 183 * this to {@code null} (the default), the series paint is used 184 * instead. 185 * 186 * @param paint the paint ({@code null} permitted). 187 * 188 * @see #getOpenTickPaint() 189 */ 190 public void setOpenTickPaint(Paint paint) { 191 this.openTickPaint = paint; 192 fireChangeEvent(); 193 } 194 195 /** 196 * Returns the paint used to draw the ticks for the close values. 197 * 198 * @return The paint used to draw the ticks for the close values (possibly 199 * {@code null}). 200 * 201 * @see #setCloseTickPaint(Paint) 202 */ 203 public Paint getCloseTickPaint() { 204 return this.closeTickPaint; 205 } 206 207 /** 208 * Sets the paint used to draw the ticks for the close values and sends a 209 * {@link RendererChangeEvent} to all registered listeners. If you set 210 * this to {@code null} (the default), the series paint is used 211 * instead. 212 * 213 * @param paint the paint ({@code null} permitted). 214 * 215 * @see #getCloseTickPaint() 216 */ 217 public void setCloseTickPaint(Paint paint) { 218 this.closeTickPaint = paint; 219 fireChangeEvent(); 220 } 221 222 /** 223 * Returns the tick length (in Java2D units). 224 * 225 * @return The tick length. 226 * 227 * @since 1.0.10 228 * 229 * @see #setTickLength(double) 230 */ 231 public double getTickLength() { 232 return this.tickLength; 233 } 234 235 /** 236 * Sets the tick length (in Java2D units) and sends a 237 * {@link RendererChangeEvent} to all registered listeners. 238 * 239 * @param length the length. 240 * 241 * @since 1.0.10 242 * 243 * @see #getTickLength() 244 */ 245 public void setTickLength(double length) { 246 this.tickLength = length; 247 fireChangeEvent(); 248 } 249 250 /** 251 * Returns the range of values the renderer requires to display all the 252 * items from the specified dataset. 253 * 254 * @param dataset the dataset ({@code null} permitted). 255 * 256 * @return The range ({@code null} if the dataset is {@code null} 257 * or empty). 258 */ 259 @Override 260 public Range findRangeBounds(XYDataset dataset) { 261 if (dataset != null) { 262 return DatasetUtils.findRangeBounds(dataset, true); 263 } 264 else { 265 return null; 266 } 267 } 268 269 /** 270 * Draws the visual representation of a single data item. 271 * 272 * @param g2 the graphics device. 273 * @param state the renderer state. 274 * @param dataArea the area within which the plot is being drawn. 275 * @param info collects information about the drawing. 276 * @param plot the plot (can be used to obtain standard color 277 * information etc). 278 * @param domainAxis the domain axis. 279 * @param rangeAxis the range axis. 280 * @param dataset the dataset. 281 * @param series the series index (zero-based). 282 * @param item the item index (zero-based). 283 * @param crosshairState crosshair information for the plot 284 * ({@code null} permitted). 285 * @param pass the pass index. 286 */ 287 @Override 288 public void drawItem(Graphics2D g2, XYItemRendererState state, 289 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 290 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 291 int series, int item, CrosshairState crosshairState, int pass) { 292 293 double x = dataset.getXValue(series, item); 294 if (!domainAxis.getRange().contains(x)) { 295 return; // the x value is not within the axis range 296 } 297 double xx = domainAxis.valueToJava2D(x, dataArea, 298 plot.getDomainAxisEdge()); 299 300 // setup for collecting optional entity info... 301 Shape entityArea = null; 302 EntityCollection entities = null; 303 if (info != null) { 304 entities = info.getOwner().getEntityCollection(); 305 } 306 307 PlotOrientation orientation = plot.getOrientation(); 308 RectangleEdge location = plot.getRangeAxisEdge(); 309 310 Paint itemPaint = getItemPaint(series, item); 311 Stroke itemStroke = getItemStroke(series, item); 312 g2.setPaint(itemPaint); 313 g2.setStroke(itemStroke); 314 315 if (dataset instanceof OHLCDataset) { 316 OHLCDataset hld = (OHLCDataset) dataset; 317 318 double yHigh = hld.getHighValue(series, item); 319 double yLow = hld.getLowValue(series, item); 320 if (!Double.isNaN(yHigh) && !Double.isNaN(yLow)) { 321 double yyHigh = rangeAxis.valueToJava2D(yHigh, dataArea, 322 location); 323 double yyLow = rangeAxis.valueToJava2D(yLow, dataArea, 324 location); 325 if (orientation == PlotOrientation.HORIZONTAL) { 326 g2.draw(new Line2D.Double(yyLow, xx, yyHigh, xx)); 327 entityArea = new Rectangle2D.Double(Math.min(yyLow, yyHigh), 328 xx - 1.0, Math.abs(yyHigh - yyLow), 2.0); 329 } 330 else if (orientation == PlotOrientation.VERTICAL) { 331 g2.draw(new Line2D.Double(xx, yyLow, xx, yyHigh)); 332 entityArea = new Rectangle2D.Double(xx - 1.0, 333 Math.min(yyLow, yyHigh), 2.0, 334 Math.abs(yyHigh - yyLow)); 335 } 336 } 337 338 double delta = getTickLength(); 339 if (domainAxis.isInverted()) { 340 delta = -delta; 341 } 342 if (getDrawOpenTicks()) { 343 double yOpen = hld.getOpenValue(series, item); 344 if (!Double.isNaN(yOpen)) { 345 double yyOpen = rangeAxis.valueToJava2D(yOpen, dataArea, 346 location); 347 if (this.openTickPaint != null) { 348 g2.setPaint(this.openTickPaint); 349 } 350 else { 351 g2.setPaint(itemPaint); 352 } 353 if (orientation == PlotOrientation.HORIZONTAL) { 354 g2.draw(new Line2D.Double(yyOpen, xx + delta, yyOpen, 355 xx)); 356 } 357 else if (orientation == PlotOrientation.VERTICAL) { 358 g2.draw(new Line2D.Double(xx - delta, yyOpen, xx, 359 yyOpen)); 360 } 361 } 362 } 363 364 if (getDrawCloseTicks()) { 365 double yClose = hld.getCloseValue(series, item); 366 if (!Double.isNaN(yClose)) { 367 double yyClose = rangeAxis.valueToJava2D( 368 yClose, dataArea, location); 369 if (this.closeTickPaint != null) { 370 g2.setPaint(this.closeTickPaint); 371 } 372 else { 373 g2.setPaint(itemPaint); 374 } 375 if (orientation == PlotOrientation.HORIZONTAL) { 376 g2.draw(new Line2D.Double(yyClose, xx, yyClose, 377 xx - delta)); 378 } 379 else if (orientation == PlotOrientation.VERTICAL) { 380 g2.draw(new Line2D.Double(xx, yyClose, xx + delta, 381 yyClose)); 382 } 383 } 384 } 385 386 } 387 else { 388 // not a HighLowDataset, so just draw a line connecting this point 389 // with the previous point... 390 if (item > 0) { 391 double x0 = dataset.getXValue(series, item - 1); 392 double y0 = dataset.getYValue(series, item - 1); 393 double y = dataset.getYValue(series, item); 394 if (Double.isNaN(x0) || Double.isNaN(y0) || Double.isNaN(y)) { 395 return; 396 } 397 double xx0 = domainAxis.valueToJava2D(x0, dataArea, 398 plot.getDomainAxisEdge()); 399 double yy0 = rangeAxis.valueToJava2D(y0, dataArea, location); 400 double yy = rangeAxis.valueToJava2D(y, dataArea, location); 401 if (orientation == PlotOrientation.HORIZONTAL) { 402 g2.draw(new Line2D.Double(yy0, xx0, yy, xx)); 403 } 404 else if (orientation == PlotOrientation.VERTICAL) { 405 g2.draw(new Line2D.Double(xx0, yy0, xx, yy)); 406 } 407 } 408 } 409 410 if (entities != null) { 411 addEntity(entities, entityArea, dataset, series, item, 0.0, 0.0); 412 } 413 414 } 415 416 /** 417 * Returns a clone of the renderer. 418 * 419 * @return A clone. 420 * 421 * @throws CloneNotSupportedException if the renderer cannot be cloned. 422 */ 423 @Override 424 public Object clone() throws CloneNotSupportedException { 425 return super.clone(); 426 } 427 428 /** 429 * Tests this renderer for equality with an arbitrary object. 430 * 431 * @param obj the object ({@code null} permitted). 432 * 433 * @return A boolean. 434 */ 435 @Override 436 public boolean equals(Object obj) { 437 if (this == obj) { 438 return true; 439 } 440 if (!(obj instanceof HighLowRenderer)) { 441 return false; 442 } 443 HighLowRenderer that = (HighLowRenderer) obj; 444 if (this.drawOpenTicks != that.drawOpenTicks) { 445 return false; 446 } 447 if (this.drawCloseTicks != that.drawCloseTicks) { 448 return false; 449 } 450 if (!PaintUtils.equal(this.openTickPaint, that.openTickPaint)) { 451 return false; 452 } 453 if (!PaintUtils.equal(this.closeTickPaint, that.closeTickPaint)) { 454 return false; 455 } 456 if (this.tickLength != that.tickLength) { 457 return false; 458 } 459 if (!super.equals(obj)) { 460 return false; 461 } 462 return true; 463 } 464 465 /** 466 * Provides serialization support. 467 * 468 * @param stream the input stream. 469 * 470 * @throws IOException if there is an I/O error. 471 * @throws ClassNotFoundException if there is a classpath problem. 472 */ 473 private void readObject(ObjectInputStream stream) 474 throws IOException, ClassNotFoundException { 475 stream.defaultReadObject(); 476 this.openTickPaint = SerialUtils.readPaint(stream); 477 this.closeTickPaint = SerialUtils.readPaint(stream); 478 } 479 480 /** 481 * Provides serialization support. 482 * 483 * @param stream the output stream. 484 * 485 * @throws IOException if there is an I/O error. 486 */ 487 private void writeObject(ObjectOutputStream stream) throws IOException { 488 stream.defaultWriteObject(); 489 SerialUtils.writePaint(this.openTickPaint, stream); 490 SerialUtils.writePaint(this.closeTickPaint, stream); 491 } 492 493}