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