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