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 * FastScatterPlot.java 029 * -------------------- 030 * (C) Copyright 2002-2022, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Arnaud Lelievre; 034 * Ulrich Voigt (patch #307); 035 * 036 */ 037 038package org.jfree.chart.plot; 039 040import java.awt.AlphaComposite; 041import java.awt.BasicStroke; 042import java.awt.Color; 043import java.awt.Composite; 044import java.awt.Graphics2D; 045import java.awt.Paint; 046import java.awt.RenderingHints; 047import java.awt.Shape; 048import java.awt.Stroke; 049import java.awt.geom.Line2D; 050import java.awt.geom.Point2D; 051import java.awt.geom.Rectangle2D; 052import java.io.IOException; 053import java.io.ObjectInputStream; 054import java.io.ObjectOutputStream; 055import java.io.Serializable; 056import java.util.List; 057import java.util.Objects; 058import java.util.ResourceBundle; 059import org.jfree.chart.ChartElementVisitor; 060 061import org.jfree.chart.axis.AxisSpace; 062import org.jfree.chart.axis.AxisState; 063import org.jfree.chart.axis.NumberAxis; 064import org.jfree.chart.axis.ValueAxis; 065import org.jfree.chart.axis.ValueTick; 066import org.jfree.chart.event.PlotChangeEvent; 067import org.jfree.chart.api.RectangleEdge; 068import org.jfree.chart.api.RectangleInsets; 069import org.jfree.chart.internal.ArrayUtils; 070import org.jfree.chart.internal.PaintUtils; 071import org.jfree.chart.internal.Args; 072import org.jfree.chart.internal.SerialUtils; 073import org.jfree.data.Range; 074 075/** 076 * A fast scatter plot. 077 */ 078public class FastScatterPlot extends Plot implements ValueAxisPlot, Pannable, 079 Zoomable, Cloneable, Serializable { 080 081 /** For serialization. */ 082 private static final long serialVersionUID = 7871545897358563521L; 083 084 /** The default grid line stroke. */ 085 public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(0.5f, 086 BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0.0f, new float[] 087 {2.0f, 2.0f}, 0.0f); 088 089 /** The default grid line paint. */ 090 public static final Paint DEFAULT_GRIDLINE_PAINT = Color.lightGray; 091 092 /** The data. */ 093 private float[][] data; 094 095 /** The x data range. */ 096 private final Range xDataRange; 097 098 /** The y data range. */ 099 private final Range yDataRange; 100 101 /** The domain axis (used for the x-values). */ 102 private ValueAxis domainAxis; 103 104 /** The range axis (used for the y-values). */ 105 private ValueAxis rangeAxis; 106 107 /** The paint used to plot data points. */ 108 private transient Paint paint; 109 110 /** A flag that controls whether the domain grid-lines are visible. */ 111 private boolean domainGridlinesVisible; 112 113 /** The stroke used to draw the domain grid-lines. */ 114 private transient Stroke domainGridlineStroke; 115 116 /** The paint used to draw the domain grid-lines. */ 117 private transient Paint domainGridlinePaint; 118 119 /** A flag that controls whether the range grid-lines are visible. */ 120 private boolean rangeGridlinesVisible; 121 122 /** The stroke used to draw the range grid-lines. */ 123 private transient Stroke rangeGridlineStroke; 124 125 /** The paint used to draw the range grid-lines. */ 126 private transient Paint rangeGridlinePaint; 127 128 /** 129 * A flag that controls whether or not panning is enabled for the domain 130 * axis. 131 */ 132 private boolean domainPannable; 133 134 /** 135 * A flag that controls whether or not panning is enabled for the range 136 * axis. 137 */ 138 private boolean rangePannable; 139 140 /** The resourceBundle for the localization. */ 141 protected static ResourceBundle localizationResources 142 = ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle"); 143 144 /** 145 * Creates a new instance of {@code FastScatterPlot} with default 146 * axes. 147 */ 148 public FastScatterPlot() { 149 this(null, new NumberAxis("X"), new NumberAxis("Y")); 150 } 151 152 /** 153 * Creates a new fast scatter plot. 154 * <p> 155 * The data is an array of x, y values: data[0][i] = x, data[1][i] = y. 156 * 157 * @param data the data ({@code null} permitted). 158 * @param domainAxis the domain (x) axis ({@code null} not permitted). 159 * @param rangeAxis the range (y) axis ({@code null} not permitted). 160 */ 161 public FastScatterPlot(float[][] data, 162 ValueAxis domainAxis, ValueAxis rangeAxis) { 163 164 super(); 165 Args.nullNotPermitted(domainAxis, "domainAxis"); 166 Args.nullNotPermitted(rangeAxis, "rangeAxis"); 167 168 this.data = data; 169 this.xDataRange = calculateXDataRange(data); 170 this.yDataRange = calculateYDataRange(data); 171 this.domainAxis = domainAxis; 172 this.domainAxis.setPlot(this); 173 this.domainAxis.addChangeListener(this); 174 this.rangeAxis = rangeAxis; 175 this.rangeAxis.setPlot(this); 176 this.rangeAxis.addChangeListener(this); 177 178 this.paint = Color.RED; 179 180 this.domainGridlinesVisible = true; 181 this.domainGridlinePaint = FastScatterPlot.DEFAULT_GRIDLINE_PAINT; 182 this.domainGridlineStroke = FastScatterPlot.DEFAULT_GRIDLINE_STROKE; 183 184 this.rangeGridlinesVisible = true; 185 this.rangeGridlinePaint = FastScatterPlot.DEFAULT_GRIDLINE_PAINT; 186 this.rangeGridlineStroke = FastScatterPlot.DEFAULT_GRIDLINE_STROKE; 187 } 188 189 /** 190 * Returns a short string describing the plot type. 191 * 192 * @return A short string describing the plot type. 193 */ 194 @Override 195 public String getPlotType() { 196 return localizationResources.getString("Fast_Scatter_Plot"); 197 } 198 199 /** 200 * Returns the data array used by the plot. 201 * 202 * @return The data array (possibly {@code null}). 203 * 204 * @see #setData(float[][]) 205 */ 206 public float[][] getData() { 207 return this.data; 208 } 209 210 /** 211 * Sets the data array used by the plot and sends a {@link PlotChangeEvent} 212 * to all registered listeners. 213 * 214 * @param data the data array ({@code null} permitted). 215 * 216 * @see #getData() 217 */ 218 public void setData(float[][] data) { 219 this.data = data; 220 fireChangeEvent(); 221 } 222 223 /** 224 * Returns the orientation of the plot. 225 * 226 * @return The orientation (always {@link PlotOrientation#VERTICAL}). 227 */ 228 @Override 229 public PlotOrientation getOrientation() { 230 return PlotOrientation.VERTICAL; 231 } 232 233 /** 234 * Returns the domain axis for the plot. 235 * 236 * @return The domain axis (never {@code null}). 237 * 238 * @see #setDomainAxis(ValueAxis) 239 */ 240 public ValueAxis getDomainAxis() { 241 return this.domainAxis; 242 } 243 244 /** 245 * Sets the domain axis and sends a {@link PlotChangeEvent} to all 246 * registered listeners. 247 * 248 * @param axis the axis ({@code null} not permitted). 249 * 250 * @see #getDomainAxis() 251 */ 252 public void setDomainAxis(ValueAxis axis) { 253 Args.nullNotPermitted(axis, "axis"); 254 this.domainAxis = axis; 255 fireChangeEvent(); 256 } 257 258 /** 259 * Returns the range axis for the plot. 260 * 261 * @return The range axis (never {@code null}). 262 * 263 * @see #setRangeAxis(ValueAxis) 264 */ 265 public ValueAxis getRangeAxis() { 266 return this.rangeAxis; 267 } 268 269 /** 270 * Sets the range axis and sends a {@link PlotChangeEvent} to all 271 * registered listeners. 272 * 273 * @param axis the axis ({@code null} not permitted). 274 * 275 * @see #getRangeAxis() 276 */ 277 public void setRangeAxis(ValueAxis axis) { 278 Args.nullNotPermitted(axis, "axis"); 279 this.rangeAxis = axis; 280 fireChangeEvent(); 281 } 282 283 /** 284 * Returns the paint used to plot data points. The default is 285 * {@code Color.RED}. 286 * 287 * @return The paint. 288 * 289 * @see #setPaint(Paint) 290 */ 291 public Paint getPaint() { 292 return this.paint; 293 } 294 295 /** 296 * Sets the color for the data points and sends a {@link PlotChangeEvent} 297 * to all registered listeners. 298 * 299 * @param paint the paint ({@code null} not permitted). 300 * 301 * @see #getPaint() 302 */ 303 public void setPaint(Paint paint) { 304 Args.nullNotPermitted(paint, "paint"); 305 this.paint = paint; 306 fireChangeEvent(); 307 } 308 309 /** 310 * Returns {@code true} if the domain gridlines are visible, and 311 * {@code false} otherwise. 312 * 313 * @return {@code true} or {@code false}. 314 * 315 * @see #setDomainGridlinesVisible(boolean) 316 * @see #setDomainGridlinePaint(Paint) 317 */ 318 public boolean isDomainGridlinesVisible() { 319 return this.domainGridlinesVisible; 320 } 321 322 /** 323 * Sets the flag that controls whether or not the domain grid-lines are 324 * visible. If the flag value is changed, a {@link PlotChangeEvent} is 325 * sent to all registered listeners. 326 * 327 * @param visible the new value of the flag. 328 * 329 * @see #getDomainGridlinePaint() 330 */ 331 public void setDomainGridlinesVisible(boolean visible) { 332 if (this.domainGridlinesVisible != visible) { 333 this.domainGridlinesVisible = visible; 334 fireChangeEvent(); 335 } 336 } 337 338 /** 339 * Returns the stroke for the grid-lines (if any) plotted against the 340 * domain axis. 341 * 342 * @return The stroke (never {@code null}). 343 * 344 * @see #setDomainGridlineStroke(Stroke) 345 */ 346 public Stroke getDomainGridlineStroke() { 347 return this.domainGridlineStroke; 348 } 349 350 /** 351 * Sets the stroke for the grid lines plotted against the domain axis and 352 * sends a {@link PlotChangeEvent} to all registered listeners. 353 * 354 * @param stroke the stroke ({@code null} not permitted). 355 * 356 * @see #getDomainGridlineStroke() 357 */ 358 public void setDomainGridlineStroke(Stroke stroke) { 359 Args.nullNotPermitted(stroke, "stroke"); 360 this.domainGridlineStroke = stroke; 361 fireChangeEvent(); 362 } 363 364 /** 365 * Returns the paint for the grid lines (if any) plotted against the domain 366 * axis. 367 * 368 * @return The paint (never {@code null}). 369 * 370 * @see #setDomainGridlinePaint(Paint) 371 */ 372 public Paint getDomainGridlinePaint() { 373 return this.domainGridlinePaint; 374 } 375 376 /** 377 * Sets the paint for the grid lines plotted against the domain axis and 378 * sends a {@link PlotChangeEvent} to all registered listeners. 379 * 380 * @param paint the paint ({@code null} not permitted). 381 * 382 * @see #getDomainGridlinePaint() 383 */ 384 public void setDomainGridlinePaint(Paint paint) { 385 Args.nullNotPermitted(paint, "paint"); 386 this.domainGridlinePaint = paint; 387 fireChangeEvent(); 388 } 389 390 /** 391 * Returns {@code true} if the range axis grid is visible, and 392 * {@code false} otherwise. 393 * 394 * @return {@code true} or {@code false}. 395 * 396 * @see #setRangeGridlinesVisible(boolean) 397 */ 398 public boolean isRangeGridlinesVisible() { 399 return this.rangeGridlinesVisible; 400 } 401 402 /** 403 * Sets the flag that controls whether or not the range axis grid lines are 404 * visible. If the flag value is changed, a {@link PlotChangeEvent} is 405 * sent to all registered listeners. 406 * 407 * @param visible the new value of the flag. 408 * 409 * @see #isRangeGridlinesVisible() 410 */ 411 public void setRangeGridlinesVisible(boolean visible) { 412 if (this.rangeGridlinesVisible != visible) { 413 this.rangeGridlinesVisible = visible; 414 fireChangeEvent(); 415 } 416 } 417 418 /** 419 * Returns the stroke for the grid lines (if any) plotted against the range 420 * axis. 421 * 422 * @return The stroke (never {@code null}). 423 * 424 * @see #setRangeGridlineStroke(Stroke) 425 */ 426 public Stroke getRangeGridlineStroke() { 427 return this.rangeGridlineStroke; 428 } 429 430 /** 431 * Sets the stroke for the grid lines plotted against the range axis and 432 * sends a {@link PlotChangeEvent} to all registered listeners. 433 * 434 * @param stroke the stroke ({@code null} permitted). 435 * 436 * @see #getRangeGridlineStroke() 437 */ 438 public void setRangeGridlineStroke(Stroke stroke) { 439 Args.nullNotPermitted(stroke, "stroke"); 440 this.rangeGridlineStroke = stroke; 441 fireChangeEvent(); 442 } 443 444 /** 445 * Returns the paint for the grid lines (if any) plotted against the range 446 * axis. 447 * 448 * @return The paint (never {@code null}). 449 * 450 * @see #setRangeGridlinePaint(Paint) 451 */ 452 public Paint getRangeGridlinePaint() { 453 return this.rangeGridlinePaint; 454 } 455 456 /** 457 * Sets the paint for the grid lines plotted against the range axis and 458 * sends a {@link PlotChangeEvent} to all registered listeners. 459 * 460 * @param paint the paint ({@code null} not permitted). 461 * 462 * @see #getRangeGridlinePaint() 463 */ 464 public void setRangeGridlinePaint(Paint paint) { 465 Args.nullNotPermitted(paint, "paint"); 466 this.rangeGridlinePaint = paint; 467 fireChangeEvent(); 468 } 469 470 /** 471 * Receives a chart element visitor. 472 * 473 * @param visitor the visitor ({@code null} not permitted). 474 */ 475 @Override 476 public void receive(ChartElementVisitor visitor) { 477 this.domainAxis.receive(visitor); 478 this.rangeAxis.receive(visitor); 479 super.receive(visitor); 480 } 481 482 /** 483 * Draws the fast scatter plot on a Java 2D graphics device (such as the 484 * screen or a printer). 485 * 486 * @param g2 the graphics device. 487 * @param area the area within which the plot (including axis labels) 488 * should be drawn. 489 * @param anchor the anchor point ({@code null} permitted). 490 * @param parentState the state from the parent plot (ignored). 491 * @param info collects chart drawing information ({@code null} 492 * permitted). 493 */ 494 @Override 495 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 496 PlotState parentState, PlotRenderingInfo info) { 497 498 // set up info collection... 499 if (info != null) { 500 info.setPlotArea(area); 501 } 502 503 // adjust the drawing area for plot insets (if any)... 504 RectangleInsets insets = getInsets(); 505 insets.trim(area); 506 507 AxisSpace space = new AxisSpace(); 508 space = this.domainAxis.reserveSpace(g2, this, area, 509 RectangleEdge.BOTTOM, space); 510 space = this.rangeAxis.reserveSpace(g2, this, area, RectangleEdge.LEFT, 511 space); 512 Rectangle2D dataArea = space.shrink(area, null); 513 514 if (info != null) { 515 info.setDataArea(dataArea); 516 } 517 518 // draw the plot background and axes... 519 drawBackground(g2, dataArea); 520 521 AxisState domainAxisState = this.domainAxis.draw(g2, 522 dataArea.getMaxY(), area, dataArea, RectangleEdge.BOTTOM, info); 523 AxisState rangeAxisState = this.rangeAxis.draw(g2, dataArea.getMinX(), 524 area, dataArea, RectangleEdge.LEFT, info); 525 drawDomainGridlines(g2, dataArea, domainAxisState.getTicks()); 526 drawRangeGridlines(g2, dataArea, rangeAxisState.getTicks()); 527 528 Shape originalClip = g2.getClip(); 529 Composite originalComposite = g2.getComposite(); 530 531 g2.clip(dataArea); 532 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 533 getForegroundAlpha())); 534 535 render(g2, dataArea, info, null); 536 537 g2.setClip(originalClip); 538 g2.setComposite(originalComposite); 539 drawOutline(g2, dataArea); 540 541 } 542 543 /** 544 * Draws a representation of the data within the dataArea region. The 545 * {@code info} and {@code crosshairState} arguments may be 546 * {@code null}. 547 * 548 * @param g2 the graphics device. 549 * @param dataArea the region in which the data is to be drawn. 550 * @param info an optional object for collection dimension information. 551 * @param crosshairState collects crosshair information ({@code null} 552 * permitted). 553 */ 554 public void render(Graphics2D g2, Rectangle2D dataArea, 555 PlotRenderingInfo info, CrosshairState crosshairState) { 556 g2.setPaint(this.paint); 557 558 // if the axes use a linear scale, you can uncomment the code below and 559 // switch to the alternative transX/transY calculation inside the loop 560 // that follows - it is a little bit faster then. 561 // 562 // int xx = (int) dataArea.getMinX(); 563 // int ww = (int) dataArea.getWidth(); 564 // int yy = (int) dataArea.getMaxY(); 565 // int hh = (int) dataArea.getHeight(); 566 // double domainMin = this.domainAxis.getLowerBound(); 567 // double domainLength = this.domainAxis.getUpperBound() - domainMin; 568 // double rangeMin = this.rangeAxis.getLowerBound(); 569 // double rangeLength = this.rangeAxis.getUpperBound() - rangeMin; 570 571 if (this.data != null) { 572 for (int i = 0; i < this.data[0].length; i++) { 573 float x = this.data[0][i]; 574 float y = this.data[1][i]; 575 576 //int transX = (int) (xx + ww * (x - domainMin) / domainLength); 577 //int transY = (int) (yy - hh * (y - rangeMin) / rangeLength); 578 int transX = (int) this.domainAxis.valueToJava2D(x, dataArea, 579 RectangleEdge.BOTTOM); 580 int transY = (int) this.rangeAxis.valueToJava2D(y, dataArea, 581 RectangleEdge.LEFT); 582 g2.fillRect(transX, transY, 1, 1); 583 } 584 } 585 } 586 587 /** 588 * Draws the gridlines for the plot, if they are visible. 589 * 590 * @param g2 the graphics device. 591 * @param dataArea the data area. 592 * @param ticks the ticks. 593 */ 594 protected void drawDomainGridlines(Graphics2D g2, Rectangle2D dataArea, 595 List ticks) { 596 if (!isDomainGridlinesVisible()) { 597 return; 598 } 599 Object saved = g2.getRenderingHint(RenderingHints.KEY_STROKE_CONTROL); 600 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 601 RenderingHints.VALUE_STROKE_NORMALIZE); 602 for (Object o : ticks) { 603 ValueTick tick = (ValueTick) o; 604 double v = this.domainAxis.valueToJava2D(tick.getValue(), 605 dataArea, RectangleEdge.BOTTOM); 606 Line2D line = new Line2D.Double(v, dataArea.getMinY(), v, 607 dataArea.getMaxY()); 608 g2.setPaint(getDomainGridlinePaint()); 609 g2.setStroke(getDomainGridlineStroke()); 610 g2.draw(line); 611 } 612 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, saved); 613 } 614 615 /** 616 * Draws the gridlines for the plot, if they are visible. 617 * 618 * @param g2 the graphics device. 619 * @param dataArea the data area. 620 * @param ticks the ticks. 621 */ 622 protected void drawRangeGridlines(Graphics2D g2, Rectangle2D dataArea, 623 List ticks) { 624 625 if (!isRangeGridlinesVisible()) { 626 return; 627 } 628 Object saved = g2.getRenderingHint(RenderingHints.KEY_STROKE_CONTROL); 629 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 630 RenderingHints.VALUE_STROKE_NORMALIZE); 631 632 for (Object o : ticks) { 633 ValueTick tick = (ValueTick) o; 634 double v = this.rangeAxis.valueToJava2D(tick.getValue(), 635 dataArea, RectangleEdge.LEFT); 636 Line2D line = new Line2D.Double(dataArea.getMinX(), v, 637 dataArea.getMaxX(), v); 638 g2.setPaint(getRangeGridlinePaint()); 639 g2.setStroke(getRangeGridlineStroke()); 640 g2.draw(line); 641 } 642 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, saved); 643 } 644 645 /** 646 * Returns the range of data values to be plotted along the axis, or 647 * {@code null} if the specified axis isn't the domain axis or the 648 * range axis for the plot. 649 * 650 * @param axis the axis ({@code null} permitted). 651 * 652 * @return The range (possibly {@code null}). 653 */ 654 @Override 655 public Range getDataRange(ValueAxis axis) { 656 Range result = null; 657 if (axis == this.domainAxis) { 658 result = this.xDataRange; 659 } 660 else if (axis == this.rangeAxis) { 661 result = this.yDataRange; 662 } 663 return result; 664 } 665 666 /** 667 * Calculates the X data range. 668 * 669 * @param data the data ({@code null} permitted). 670 * 671 * @return The range. 672 */ 673 private Range calculateXDataRange(float[][] data) { 674 675 Range result = null; 676 677 if (data != null) { 678 float lowest = Float.POSITIVE_INFINITY; 679 float highest = Float.NEGATIVE_INFINITY; 680 for (int i = 0; i < data[0].length; i++) { 681 float v = data[0][i]; 682 if (v < lowest) { 683 lowest = v; 684 } 685 if (v > highest) { 686 highest = v; 687 } 688 } 689 if (lowest <= highest) { 690 result = new Range(lowest, highest); 691 } 692 } 693 694 return result; 695 696 } 697 698 /** 699 * Calculates the Y data range. 700 * 701 * @param data the data ({@code null} permitted). 702 * 703 * @return The range. 704 */ 705 private Range calculateYDataRange(float[][] data) { 706 707 Range result = null; 708 if (data != null) { 709 float lowest = Float.POSITIVE_INFINITY; 710 float highest = Float.NEGATIVE_INFINITY; 711 for (int i = 0; i < data[0].length; i++) { 712 float v = data[1][i]; 713 if (v < lowest) { 714 lowest = v; 715 } 716 if (v > highest) { 717 highest = v; 718 } 719 } 720 if (lowest <= highest) { 721 result = new Range(lowest, highest); 722 } 723 } 724 return result; 725 726 } 727 728 /** 729 * Multiplies the range on the domain axis by the specified factor. 730 * 731 * @param factor the zoom factor. 732 * @param info the plot rendering info. 733 * @param source the source point. 734 */ 735 @Override 736 public void zoomDomainAxes(double factor, PlotRenderingInfo info, 737 Point2D source) { 738 this.domainAxis.resizeRange(factor); 739 } 740 741 /** 742 * Multiplies the range on the domain axis by the specified factor. 743 * 744 * @param factor the zoom factor. 745 * @param info the plot rendering info. 746 * @param source the source point (in Java2D space). 747 * @param useAnchor use source point as zoom anchor? 748 * 749 * @see #zoomRangeAxes(double, PlotRenderingInfo, Point2D, boolean) 750 */ 751 @Override 752 public void zoomDomainAxes(double factor, PlotRenderingInfo info, 753 Point2D source, boolean useAnchor) { 754 755 if (useAnchor) { 756 // get the source coordinate - this plot has always a VERTICAL 757 // orientation 758 double sourceX = source.getX(); 759 double anchorX = this.domainAxis.java2DToValue(sourceX, 760 info.getDataArea(), RectangleEdge.BOTTOM); 761 this.domainAxis.resizeRange2(factor, anchorX); 762 } 763 else { 764 this.domainAxis.resizeRange(factor); 765 } 766 767 } 768 769 /** 770 * Zooms in on the domain axes. 771 * 772 * @param lowerPercent the new lower bound as a percentage of the current 773 * range. 774 * @param upperPercent the new upper bound as a percentage of the current 775 * range. 776 * @param info the plot rendering info. 777 * @param source the source point. 778 */ 779 @Override 780 public void zoomDomainAxes(double lowerPercent, double upperPercent, 781 PlotRenderingInfo info, Point2D source) { 782 this.domainAxis.zoomRange(lowerPercent, upperPercent); 783 } 784 785 /** 786 * Multiplies the range on the range axis/axes by the specified factor. 787 * 788 * @param factor the zoom factor. 789 * @param info the plot rendering info. 790 * @param source the source point. 791 */ 792 @Override 793 public void zoomRangeAxes(double factor, PlotRenderingInfo info, 794 Point2D source) { 795 this.rangeAxis.resizeRange(factor); 796 } 797 798 /** 799 * Multiplies the range on the range axis by the specified factor. 800 * 801 * @param factor the zoom factor. 802 * @param info the plot rendering info. 803 * @param source the source point (in Java2D space). 804 * @param useAnchor use source point as zoom anchor? 805 * 806 * @see #zoomDomainAxes(double, PlotRenderingInfo, Point2D, boolean) 807 */ 808 @Override 809 public void zoomRangeAxes(double factor, PlotRenderingInfo info, 810 Point2D source, boolean useAnchor) { 811 812 if (useAnchor) { 813 // get the source coordinate - this plot has always a VERTICAL 814 // orientation 815 double sourceY = source.getY(); 816 double anchorY = this.rangeAxis.java2DToValue(sourceY, 817 info.getDataArea(), RectangleEdge.LEFT); 818 this.rangeAxis.resizeRange2(factor, anchorY); 819 } 820 else { 821 this.rangeAxis.resizeRange(factor); 822 } 823 824 } 825 826 /** 827 * Zooms in on the range axes. 828 * 829 * @param lowerPercent the new lower bound as a percentage of the current 830 * range. 831 * @param upperPercent the new upper bound as a percentage of the current 832 * range. 833 * @param info the plot rendering info. 834 * @param source the source point. 835 */ 836 @Override 837 public void zoomRangeAxes(double lowerPercent, double upperPercent, 838 PlotRenderingInfo info, Point2D source) { 839 this.rangeAxis.zoomRange(lowerPercent, upperPercent); 840 } 841 842 /** 843 * Returns {@code true}. 844 * 845 * @return A boolean. 846 */ 847 @Override 848 public boolean isDomainZoomable() { 849 return true; 850 } 851 852 /** 853 * Returns {@code true}. 854 * 855 * @return A boolean. 856 */ 857 @Override 858 public boolean isRangeZoomable() { 859 return true; 860 } 861 862 /** 863 * Returns {@code true} if panning is enabled for the domain axes, 864 * and {@code false} otherwise. 865 * 866 * @return A boolean. 867 */ 868 @Override 869 public boolean isDomainPannable() { 870 return this.domainPannable; 871 } 872 873 /** 874 * Sets the flag that enables or disables panning of the plot along the 875 * domain axes. 876 * 877 * @param pannable the new flag value. 878 */ 879 public void setDomainPannable(boolean pannable) { 880 this.domainPannable = pannable; 881 } 882 883 /** 884 * Returns {@code true} if panning is enabled for the range axes, 885 * and {@code false} otherwise. 886 * 887 * @return A boolean. 888 */ 889 @Override 890 public boolean isRangePannable() { 891 return this.rangePannable; 892 } 893 894 /** 895 * Sets the flag that enables or disables panning of the plot along 896 * the range axes. 897 * 898 * @param pannable the new flag value. 899 */ 900 public void setRangePannable(boolean pannable) { 901 this.rangePannable = pannable; 902 } 903 904 /** 905 * Pans the domain axes by the specified percentage. 906 * 907 * @param percent the distance to pan (as a percentage of the axis length). 908 * @param info the plot info 909 * @param source the source point where the pan action started. 910 */ 911 @Override 912 public void panDomainAxes(double percent, PlotRenderingInfo info, 913 Point2D source) { 914 if (!isDomainPannable() || this.domainAxis == null) { 915 return; 916 } 917 double length = this.domainAxis.getRange().getLength(); 918 double adj = percent * length; 919 if (this.domainAxis.isInverted()) { 920 adj = -adj; 921 } 922 this.domainAxis.setRange(this.domainAxis.getLowerBound() + adj, 923 this.domainAxis.getUpperBound() + adj); 924 } 925 926 /** 927 * Pans the range axes by the specified percentage. 928 * 929 * @param percent the distance to pan (as a percentage of the axis length). 930 * @param info the plot info 931 * @param source the source point where the pan action started. 932 */ 933 @Override 934 public void panRangeAxes(double percent, PlotRenderingInfo info, 935 Point2D source) { 936 if (!isRangePannable() || this.rangeAxis == null) { 937 return; 938 } 939 double length = this.rangeAxis.getRange().getLength(); 940 double adj = percent * length; 941 if (this.rangeAxis.isInverted()) { 942 adj = -adj; 943 } 944 this.rangeAxis.setRange(this.rangeAxis.getLowerBound() + adj, 945 this.rangeAxis.getUpperBound() + adj); 946 } 947 948 /** 949 * Tests an arbitrary object for equality with this plot. Note that 950 * {@code FastScatterPlot} carries its data around with it (rather 951 * than referencing a dataset), and the data is included in the 952 * equality test. 953 * 954 * @param obj the object ({@code null} permitted). 955 * 956 * @return A boolean. 957 */ 958 @Override 959 public boolean equals(Object obj) { 960 if (obj == this) { 961 return true; 962 } 963 if (!super.equals(obj)) { 964 return false; 965 } 966 if (!(obj instanceof FastScatterPlot)) { 967 return false; 968 } 969 FastScatterPlot that = (FastScatterPlot) obj; 970 if (this.domainPannable != that.domainPannable) { 971 return false; 972 } 973 if (this.rangePannable != that.rangePannable) { 974 return false; 975 } 976 if (!ArrayUtils.equal(this.data, that.data)) { 977 return false; 978 } 979 if (!Objects.equals(this.domainAxis, that.domainAxis)) { 980 return false; 981 } 982 if (!Objects.equals(this.rangeAxis, that.rangeAxis)) { 983 return false; 984 } 985 if (!PaintUtils.equal(this.paint, that.paint)) { 986 return false; 987 } 988 if (this.domainGridlinesVisible != that.domainGridlinesVisible) { 989 return false; 990 } 991 if (!PaintUtils.equal(this.domainGridlinePaint, 992 that.domainGridlinePaint)) { 993 return false; 994 } 995 if (!Objects.equals(this.domainGridlineStroke, that.domainGridlineStroke)) { 996 return false; 997 } 998 if (!this.rangeGridlinesVisible == that.rangeGridlinesVisible) { 999 return false; 1000 } 1001 if (!PaintUtils.equal(this.rangeGridlinePaint, 1002 that.rangeGridlinePaint)) { 1003 return false; 1004 } 1005 if (!Objects.equals(this.rangeGridlineStroke, that.rangeGridlineStroke)) { 1006 return false; 1007 } 1008 return true; 1009 } 1010 1011 /** 1012 * Returns a clone of the plot. 1013 * 1014 * @return A clone. 1015 * 1016 * @throws CloneNotSupportedException if some component of the plot does 1017 * not support cloning. 1018 */ 1019 @Override 1020 public Object clone() throws CloneNotSupportedException { 1021 1022 FastScatterPlot clone = (FastScatterPlot) super.clone(); 1023 if (this.data != null) { 1024 clone.data = ArrayUtils.clone(this.data); 1025 } 1026 if (this.domainAxis != null) { 1027 clone.domainAxis = (ValueAxis) this.domainAxis.clone(); 1028 clone.domainAxis.setPlot(clone); 1029 clone.domainAxis.addChangeListener(clone); 1030 } 1031 if (this.rangeAxis != null) { 1032 clone.rangeAxis = (ValueAxis) this.rangeAxis.clone(); 1033 clone.rangeAxis.setPlot(clone); 1034 clone.rangeAxis.addChangeListener(clone); 1035 } 1036 return clone; 1037 1038 } 1039 1040 /** 1041 * Provides serialization support. 1042 * 1043 * @param stream the output stream. 1044 * 1045 * @throws IOException if there is an I/O error. 1046 */ 1047 private void writeObject(ObjectOutputStream stream) throws IOException { 1048 stream.defaultWriteObject(); 1049 SerialUtils.writePaint(this.paint, stream); 1050 SerialUtils.writeStroke(this.domainGridlineStroke, stream); 1051 SerialUtils.writePaint(this.domainGridlinePaint, stream); 1052 SerialUtils.writeStroke(this.rangeGridlineStroke, stream); 1053 SerialUtils.writePaint(this.rangeGridlinePaint, stream); 1054 } 1055 1056 /** 1057 * Provides serialization support. 1058 * 1059 * @param stream the input stream. 1060 * 1061 * @throws IOException if there is an I/O error. 1062 * @throws ClassNotFoundException if there is a classpath problem. 1063 */ 1064 private void readObject(ObjectInputStream stream) 1065 throws IOException, ClassNotFoundException { 1066 stream.defaultReadObject(); 1067 1068 this.paint = SerialUtils.readPaint(stream); 1069 this.domainGridlineStroke = SerialUtils.readStroke(stream); 1070 this.domainGridlinePaint = SerialUtils.readPaint(stream); 1071 1072 this.rangeGridlineStroke = SerialUtils.readStroke(stream); 1073 this.rangeGridlinePaint = SerialUtils.readPaint(stream); 1074 1075 if (this.domainAxis != null) { 1076 this.domainAxis.addChangeListener(this); 1077 } 1078 1079 if (this.rangeAxis != null) { 1080 this.rangeAxis.addChangeListener(this); 1081 } 1082 } 1083 1084}