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 * CrosshairOverlay.java 029 * --------------------- 030 * (C) Copyright 2011-2020, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): John Matthews, Michal Wozniak; 034 * 035 */ 036 037package org.jfree.chart.panel; 038 039import java.awt.Font; 040import java.awt.Graphics2D; 041import java.awt.Paint; 042import java.awt.Rectangle; 043import java.awt.Shape; 044import java.awt.Stroke; 045import java.awt.geom.Line2D; 046import java.awt.geom.Point2D; 047import java.awt.geom.Rectangle2D; 048import java.beans.PropertyChangeEvent; 049import java.beans.PropertyChangeListener; 050import java.io.Serializable; 051import java.util.ArrayList; 052import java.util.Iterator; 053import java.util.List; 054import org.jfree.chart.ChartPanel; 055import org.jfree.chart.JFreeChart; 056import org.jfree.chart.axis.ValueAxis; 057import org.jfree.chart.event.OverlayChangeEvent; 058import org.jfree.chart.plot.Crosshair; 059import org.jfree.chart.plot.PlotOrientation; 060import org.jfree.chart.plot.XYPlot; 061import org.jfree.chart.text.TextUtils; 062import org.jfree.chart.ui.RectangleAnchor; 063import org.jfree.chart.ui.RectangleEdge; 064import org.jfree.chart.ui.TextAnchor; 065import org.jfree.chart.util.ObjectUtils; 066import org.jfree.chart.util.Args; 067import org.jfree.chart.util.PublicCloneable; 068 069/** 070 * An overlay for a {@link ChartPanel} that draws crosshairs on a chart. If 071 * you are using the JavaFX extensions for JFreeChart, then you should use 072 * the {@code CrosshairOverlayFX} class. 073 * 074 * @since 1.0.13 075 */ 076public class CrosshairOverlay extends AbstractOverlay implements Overlay, 077 PropertyChangeListener, PublicCloneable, Cloneable, Serializable { 078 079 /** Storage for the crosshairs along the x-axis. */ 080 private List<Crosshair> xCrosshairs; 081 082 /** Storage for the crosshairs along the y-axis. */ 083 private List<Crosshair> yCrosshairs; 084 085 /** 086 * Creates a new overlay that initially contains no crosshairs. 087 */ 088 public CrosshairOverlay() { 089 super(); 090 this.xCrosshairs = new ArrayList<>(); 091 this.yCrosshairs = new ArrayList<>(); 092 } 093 094 /** 095 * Adds a crosshair against the domain axis (x-axis) and sends an 096 * {@link OverlayChangeEvent} to all registered listeners. 097 * 098 * @param crosshair the crosshair ({@code null} not permitted). 099 * 100 * @see #removeDomainCrosshair(org.jfree.chart.plot.Crosshair) 101 * @see #addRangeCrosshair(org.jfree.chart.plot.Crosshair) 102 */ 103 public void addDomainCrosshair(Crosshair crosshair) { 104 Args.nullNotPermitted(crosshair, "crosshair"); 105 this.xCrosshairs.add(crosshair); 106 crosshair.addPropertyChangeListener(this); 107 fireOverlayChanged(); 108 } 109 110 /** 111 * Removes a domain axis crosshair and sends an {@link OverlayChangeEvent} 112 * to all registered listeners. 113 * 114 * @param crosshair the crosshair ({@code null} not permitted). 115 * 116 * @see #addDomainCrosshair(org.jfree.chart.plot.Crosshair) 117 */ 118 public void removeDomainCrosshair(Crosshair crosshair) { 119 Args.nullNotPermitted(crosshair, "crosshair"); 120 if (this.xCrosshairs.remove(crosshair)) { 121 crosshair.removePropertyChangeListener(this); 122 fireOverlayChanged(); 123 } 124 } 125 126 /** 127 * Clears all the domain crosshairs from the overlay and sends an 128 * {@link OverlayChangeEvent} to all registered listeners (unless there 129 * were no crosshairs to begin with). 130 */ 131 public void clearDomainCrosshairs() { 132 if (this.xCrosshairs.isEmpty()) { 133 return; // nothing to do - avoids firing change event 134 } 135 for (Crosshair c : getDomainCrosshairs()) { 136 this.xCrosshairs.remove(c); 137 c.removePropertyChangeListener(this); 138 } 139 fireOverlayChanged(); 140 } 141 142 /** 143 * Returns a new list containing the domain crosshairs for this overlay. 144 * 145 * @return A list of crosshairs. 146 */ 147 public List<Crosshair> getDomainCrosshairs() { 148 return new ArrayList<>(this.xCrosshairs); 149 } 150 151 /** 152 * Adds a crosshair against the range axis and sends an 153 * {@link OverlayChangeEvent} to all registered listeners. 154 * 155 * @param crosshair the crosshair ({@code null} not permitted). 156 */ 157 public void addRangeCrosshair(Crosshair crosshair) { 158 Args.nullNotPermitted(crosshair, "crosshair"); 159 this.yCrosshairs.add(crosshair); 160 crosshair.addPropertyChangeListener(this); 161 fireOverlayChanged(); 162 } 163 164 /** 165 * Removes a range axis crosshair and sends an {@link OverlayChangeEvent} 166 * to all registered listeners. 167 * 168 * @param crosshair the crosshair ({@code null} not permitted). 169 * 170 * @see #addRangeCrosshair(org.jfree.chart.plot.Crosshair) 171 */ 172 public void removeRangeCrosshair(Crosshair crosshair) { 173 Args.nullNotPermitted(crosshair, "crosshair"); 174 if (this.yCrosshairs.remove(crosshair)) { 175 crosshair.removePropertyChangeListener(this); 176 fireOverlayChanged(); 177 } 178 } 179 180 /** 181 * Clears all the range crosshairs from the overlay and sends an 182 * {@link OverlayChangeEvent} to all registered listeners (unless there 183 * were no crosshairs to begin with). 184 */ 185 public void clearRangeCrosshairs() { 186 if (this.yCrosshairs.isEmpty()) { 187 return; // nothing to do - avoids change notification 188 } 189 for (Crosshair c : getRangeCrosshairs()) { 190 this.yCrosshairs.remove(c); 191 c.removePropertyChangeListener(this); 192 } 193 fireOverlayChanged(); 194 } 195 196 /** 197 * Returns a new list containing the range crosshairs for this overlay. 198 * 199 * @return A list of crosshairs. 200 */ 201 public List<Crosshair> getRangeCrosshairs() { 202 return new ArrayList<>(this.yCrosshairs); 203 } 204 205 /** 206 * Receives a property change event (typically a change in one of the 207 * crosshairs). 208 * 209 * @param e the event. 210 */ 211 @Override 212 public void propertyChange(PropertyChangeEvent e) { 213 fireOverlayChanged(); 214 } 215 216 /** 217 * Renders the crosshairs in the overlay on top of the chart that has just 218 * been rendered in the specified {@code chartPanel}. This method is 219 * called by the JFreeChart framework, you won't normally call it from 220 * user code. 221 * 222 * @param g2 the graphics target. 223 * @param chartPanel the chart panel. 224 */ 225 @Override 226 public void paintOverlay(Graphics2D g2, ChartPanel chartPanel) { 227 Shape savedClip = g2.getClip(); 228 Rectangle2D dataArea = chartPanel.getScreenDataArea(); 229 g2.clip(dataArea); 230 JFreeChart chart = chartPanel.getChart(); 231 XYPlot plot = (XYPlot) chart.getPlot(); 232 ValueAxis xAxis = plot.getDomainAxis(); 233 RectangleEdge xAxisEdge = plot.getDomainAxisEdge(); 234 for (Crosshair ch : this.xCrosshairs) { 235 if (ch.isVisible()) { 236 double x = ch.getValue(); 237 double xx = xAxis.valueToJava2D(x, dataArea, xAxisEdge); 238 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 239 drawVerticalCrosshair(g2, dataArea, xx, ch); 240 } else { 241 drawHorizontalCrosshair(g2, dataArea, xx, ch); 242 } 243 } 244 } 245 ValueAxis yAxis = plot.getRangeAxis(); 246 RectangleEdge yAxisEdge = plot.getRangeAxisEdge(); 247 for (Crosshair ch : this.yCrosshairs) { 248 if (ch.isVisible()) { 249 double y = ch.getValue(); 250 double yy = yAxis.valueToJava2D(y, dataArea, yAxisEdge); 251 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 252 drawHorizontalCrosshair(g2, dataArea, yy, ch); 253 } else { 254 drawVerticalCrosshair(g2, dataArea, yy, ch); 255 } 256 } 257 } 258 g2.setClip(savedClip); 259 } 260 261 /** 262 * Draws a crosshair horizontally across the plot. 263 * 264 * @param g2 the graphics target. 265 * @param dataArea the data area. 266 * @param y the y-value in Java2D space. 267 * @param crosshair the crosshair. 268 */ 269 protected void drawHorizontalCrosshair(Graphics2D g2, Rectangle2D dataArea, 270 double y, Crosshair crosshair) { 271 272 if (y >= dataArea.getMinY() && y <= dataArea.getMaxY()) { 273 Line2D line = new Line2D.Double(dataArea.getMinX(), y, 274 dataArea.getMaxX(), y); 275 Paint savedPaint = g2.getPaint(); 276 Stroke savedStroke = g2.getStroke(); 277 g2.setPaint(crosshair.getPaint()); 278 g2.setStroke(crosshair.getStroke()); 279 g2.draw(line); 280 if (crosshair.isLabelVisible()) { 281 String label = crosshair.getLabelGenerator().generateLabel( 282 crosshair); 283 if (label != null && !label.isEmpty()) { 284 Font savedFont = g2.getFont(); 285 g2.setFont(crosshair.getLabelFont()); 286 RectangleAnchor anchor = crosshair.getLabelAnchor(); 287 Point2D pt = calculateLabelPoint(line, anchor, crosshair.getLabelXOffset(), crosshair.getLabelYOffset()); 288 float xx = (float) pt.getX(); 289 float yy = (float) pt.getY(); 290 TextAnchor alignPt = textAlignPtForLabelAnchorH(anchor); 291 Shape hotspot = TextUtils.calculateRotatedStringBounds( 292 label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER); 293 if (!dataArea.contains(hotspot.getBounds2D())) { 294 anchor = flipAnchorV(anchor); 295 pt = calculateLabelPoint(line, anchor, crosshair.getLabelXOffset(), crosshair.getLabelYOffset()); 296 xx = (float) pt.getX(); 297 yy = (float) pt.getY(); 298 alignPt = textAlignPtForLabelAnchorH(anchor); 299 hotspot = TextUtils.calculateRotatedStringBounds( 300 label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER); 301 } 302 303 g2.setPaint(crosshair.getLabelBackgroundPaint()); 304 g2.fill(hotspot); 305 if (crosshair.isLabelOutlineVisible()) { 306 g2.setPaint(crosshair.getLabelOutlinePaint()); 307 g2.setStroke(crosshair.getLabelOutlineStroke()); 308 g2.draw(hotspot); 309 } 310 g2.setPaint(crosshair.getLabelPaint()); 311 TextUtils.drawAlignedString(label, g2, xx, yy, alignPt); 312 g2.setFont(savedFont); 313 } 314 } 315 g2.setPaint(savedPaint); 316 g2.setStroke(savedStroke); 317 } 318 } 319 320 /** 321 * Draws a crosshair vertically on the plot. 322 * 323 * @param g2 the graphics target. 324 * @param dataArea the data area. 325 * @param x the x-value in Java2D space. 326 * @param crosshair the crosshair. 327 */ 328 protected void drawVerticalCrosshair(Graphics2D g2, Rectangle2D dataArea, 329 double x, Crosshair crosshair) { 330 331 if (x >= dataArea.getMinX() && x <= dataArea.getMaxX()) { 332 Line2D line = new Line2D.Double(x, dataArea.getMinY(), x, 333 dataArea.getMaxY()); 334 Paint savedPaint = g2.getPaint(); 335 Stroke savedStroke = g2.getStroke(); 336 g2.setPaint(crosshair.getPaint()); 337 g2.setStroke(crosshair.getStroke()); 338 g2.draw(line); 339 if (crosshair.isLabelVisible()) { 340 String label = crosshair.getLabelGenerator().generateLabel( 341 crosshair); 342 if (label != null && !label.isEmpty()) { 343 Font savedFont = g2.getFont(); 344 g2.setFont(crosshair.getLabelFont()); 345 RectangleAnchor anchor = crosshair.getLabelAnchor(); 346 Point2D pt = calculateLabelPoint(line, anchor, crosshair.getLabelXOffset(), crosshair.getLabelYOffset()); 347 float xx = (float) pt.getX(); 348 float yy = (float) pt.getY(); 349 TextAnchor alignPt = textAlignPtForLabelAnchorV(anchor); 350 Shape hotspot = TextUtils.calculateRotatedStringBounds( 351 label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER); 352 if (!dataArea.contains(hotspot.getBounds2D())) { 353 anchor = flipAnchorH(anchor); 354 pt = calculateLabelPoint(line, anchor, crosshair.getLabelXOffset(), crosshair.getLabelYOffset()); 355 xx = (float) pt.getX(); 356 yy = (float) pt.getY(); 357 alignPt = textAlignPtForLabelAnchorV(anchor); 358 hotspot = TextUtils.calculateRotatedStringBounds( 359 label, g2, xx, yy, alignPt, 0.0, TextAnchor.CENTER); 360 } 361 g2.setPaint(crosshair.getLabelBackgroundPaint()); 362 g2.fill(hotspot); 363 if (crosshair.isLabelOutlineVisible()) { 364 g2.setPaint(crosshair.getLabelOutlinePaint()); 365 g2.setStroke(crosshair.getLabelOutlineStroke()); 366 g2.draw(hotspot); 367 } 368 g2.setPaint(crosshair.getLabelPaint()); 369 TextUtils.drawAlignedString(label, g2, xx, yy, alignPt); 370 g2.setFont(savedFont); 371 } 372 } 373 g2.setPaint(savedPaint); 374 g2.setStroke(savedStroke); 375 } 376 } 377 378 /** 379 * Calculates the anchor point for a label. 380 * 381 * @param line the line for the crosshair. 382 * @param anchor the anchor point. 383 * @param deltaX the x-offset. 384 * @param deltaY the y-offset. 385 * 386 * @return The anchor point. 387 */ 388 private Point2D calculateLabelPoint(Line2D line, RectangleAnchor anchor, 389 double deltaX, double deltaY) { 390 double x, y; 391 boolean left = (anchor == RectangleAnchor.BOTTOM_LEFT 392 || anchor == RectangleAnchor.LEFT 393 || anchor == RectangleAnchor.TOP_LEFT); 394 boolean right = (anchor == RectangleAnchor.BOTTOM_RIGHT 395 || anchor == RectangleAnchor.RIGHT 396 || anchor == RectangleAnchor.TOP_RIGHT); 397 boolean top = (anchor == RectangleAnchor.TOP_LEFT 398 || anchor == RectangleAnchor.TOP 399 || anchor == RectangleAnchor.TOP_RIGHT); 400 boolean bottom = (anchor == RectangleAnchor.BOTTOM_LEFT 401 || anchor == RectangleAnchor.BOTTOM 402 || anchor == RectangleAnchor.BOTTOM_RIGHT); 403 Rectangle rect = line.getBounds(); 404 405 // we expect the line to be vertical or horizontal 406 if (line.getX1() == line.getX2()) { // vertical 407 x = line.getX1(); 408 y = (line.getY1() + line.getY2()) / 2.0; 409 if (left) { 410 x = x - deltaX; 411 } 412 if (right) { 413 x = x + deltaX; 414 } 415 if (top) { 416 y = Math.min(line.getY1(), line.getY2()) + deltaY; 417 } 418 if (bottom) { 419 y = Math.max(line.getY1(), line.getY2()) - deltaY; 420 } 421 } 422 else { // horizontal 423 x = (line.getX1() + line.getX2()) / 2.0; 424 y = line.getY1(); 425 if (left) { 426 x = Math.min(line.getX1(), line.getX2()) + deltaX; 427 } 428 if (right) { 429 x = Math.max(line.getX1(), line.getX2()) - deltaX; 430 } 431 if (top) { 432 y = y - deltaY; 433 } 434 if (bottom) { 435 y = y + deltaY; 436 } 437 } 438 return new Point2D.Double(x, y); 439 } 440 441 /** 442 * Returns the text anchor that is used to align a label to its anchor 443 * point. 444 * 445 * @param anchor the anchor. 446 * 447 * @return The text alignment point. 448 */ 449 private TextAnchor textAlignPtForLabelAnchorV(RectangleAnchor anchor) { 450 TextAnchor result = TextAnchor.CENTER; 451 if (anchor.equals(RectangleAnchor.TOP_LEFT)) { 452 result = TextAnchor.TOP_RIGHT; 453 } 454 else if (anchor.equals(RectangleAnchor.TOP)) { 455 result = TextAnchor.TOP_CENTER; 456 } 457 else if (anchor.equals(RectangleAnchor.TOP_RIGHT)) { 458 result = TextAnchor.TOP_LEFT; 459 } 460 else if (anchor.equals(RectangleAnchor.LEFT)) { 461 result = TextAnchor.HALF_ASCENT_RIGHT; 462 } 463 else if (anchor.equals(RectangleAnchor.RIGHT)) { 464 result = TextAnchor.HALF_ASCENT_LEFT; 465 } 466 else if (anchor.equals(RectangleAnchor.BOTTOM_LEFT)) { 467 result = TextAnchor.BOTTOM_RIGHT; 468 } 469 else if (anchor.equals(RectangleAnchor.BOTTOM)) { 470 result = TextAnchor.BOTTOM_CENTER; 471 } 472 else if (anchor.equals(RectangleAnchor.BOTTOM_RIGHT)) { 473 result = TextAnchor.BOTTOM_LEFT; 474 } 475 return result; 476 } 477 478 /** 479 * Returns the text anchor that is used to align a label to its anchor 480 * point. 481 * 482 * @param anchor the anchor. 483 * 484 * @return The text alignment point. 485 */ 486 private TextAnchor textAlignPtForLabelAnchorH(RectangleAnchor anchor) { 487 TextAnchor result = TextAnchor.CENTER; 488 if (anchor.equals(RectangleAnchor.TOP_LEFT)) { 489 result = TextAnchor.BOTTOM_LEFT; 490 } 491 else if (anchor.equals(RectangleAnchor.TOP)) { 492 result = TextAnchor.BOTTOM_CENTER; 493 } 494 else if (anchor.equals(RectangleAnchor.TOP_RIGHT)) { 495 result = TextAnchor.BOTTOM_RIGHT; 496 } 497 else if (anchor.equals(RectangleAnchor.LEFT)) { 498 result = TextAnchor.HALF_ASCENT_LEFT; 499 } 500 else if (anchor.equals(RectangleAnchor.RIGHT)) { 501 result = TextAnchor.HALF_ASCENT_RIGHT; 502 } 503 else if (anchor.equals(RectangleAnchor.BOTTOM_LEFT)) { 504 result = TextAnchor.TOP_LEFT; 505 } 506 else if (anchor.equals(RectangleAnchor.BOTTOM)) { 507 result = TextAnchor.TOP_CENTER; 508 } 509 else if (anchor.equals(RectangleAnchor.BOTTOM_RIGHT)) { 510 result = TextAnchor.TOP_RIGHT; 511 } 512 return result; 513 } 514 515 private RectangleAnchor flipAnchorH(RectangleAnchor anchor) { 516 RectangleAnchor result = anchor; 517 if (anchor.equals(RectangleAnchor.TOP_LEFT)) { 518 result = RectangleAnchor.TOP_RIGHT; 519 } 520 else if (anchor.equals(RectangleAnchor.TOP_RIGHT)) { 521 result = RectangleAnchor.TOP_LEFT; 522 } 523 else if (anchor.equals(RectangleAnchor.LEFT)) { 524 result = RectangleAnchor.RIGHT; 525 } 526 else if (anchor.equals(RectangleAnchor.RIGHT)) { 527 result = RectangleAnchor.LEFT; 528 } 529 else if (anchor.equals(RectangleAnchor.BOTTOM_LEFT)) { 530 result = RectangleAnchor.BOTTOM_RIGHT; 531 } 532 else if (anchor.equals(RectangleAnchor.BOTTOM_RIGHT)) { 533 result = RectangleAnchor.BOTTOM_LEFT; 534 } 535 return result; 536 } 537 538 private RectangleAnchor flipAnchorV(RectangleAnchor anchor) { 539 RectangleAnchor result = anchor; 540 if (anchor.equals(RectangleAnchor.TOP_LEFT)) { 541 result = RectangleAnchor.BOTTOM_LEFT; 542 } 543 else if (anchor.equals(RectangleAnchor.TOP_RIGHT)) { 544 result = RectangleAnchor.BOTTOM_RIGHT; 545 } 546 else if (anchor.equals(RectangleAnchor.TOP)) { 547 result = RectangleAnchor.BOTTOM; 548 } 549 else if (anchor.equals(RectangleAnchor.BOTTOM)) { 550 result = RectangleAnchor.TOP; 551 } 552 else if (anchor.equals(RectangleAnchor.BOTTOM_LEFT)) { 553 result = RectangleAnchor.TOP_LEFT; 554 } 555 else if (anchor.equals(RectangleAnchor.BOTTOM_RIGHT)) { 556 result = RectangleAnchor.TOP_RIGHT; 557 } 558 return result; 559 } 560 561 /** 562 * Tests this overlay for equality with an arbitrary object. 563 * 564 * @param obj the object ({@code null} permitted). 565 * 566 * @return A boolean. 567 */ 568 @Override 569 public boolean equals(Object obj) { 570 if (obj == this) { 571 return true; 572 } 573 if (!(obj instanceof CrosshairOverlay)) { 574 return false; 575 } 576 CrosshairOverlay that = (CrosshairOverlay) obj; 577 if (!this.xCrosshairs.equals(that.xCrosshairs)) { 578 return false; 579 } 580 if (!this.yCrosshairs.equals(that.yCrosshairs)) { 581 return false; 582 } 583 return true; 584 } 585 586 /** 587 * Returns a clone of this instance. 588 * 589 * @return A clone of this instance. 590 * 591 * @throws java.lang.CloneNotSupportedException if there is some problem 592 * with the cloning. 593 */ 594 @Override 595 public Object clone() throws CloneNotSupportedException { 596 CrosshairOverlay clone = (CrosshairOverlay) super.clone(); 597 clone.xCrosshairs = (List) ObjectUtils.deepClone(this.xCrosshairs); 598 clone.yCrosshairs = (List) ObjectUtils.deepClone(this.yCrosshairs); 599 return clone; 600 } 601 602}