001 /* 002 * $Id: JXGraph.java,v 1.3 2006/03/17 22:10:33 gfx Exp $ 003 * 004 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 */ 021 022 package org.jdesktop.swingx; 023 024 import java.awt.BasicStroke; 025 import java.awt.Color; 026 import java.awt.Cursor; 027 import java.awt.Dimension; 028 import java.awt.FontMetrics; 029 import java.awt.Graphics; 030 import java.awt.Graphics2D; 031 import java.awt.Point; 032 import java.awt.Rectangle; 033 import java.awt.RenderingHints; 034 import java.awt.Stroke; 035 import java.awt.event.MouseAdapter; 036 import java.awt.event.MouseEvent; 037 import java.awt.event.MouseMotionAdapter; 038 import java.awt.event.MouseWheelEvent; 039 import java.awt.event.MouseWheelListener; 040 import java.awt.geom.GeneralPath; 041 import java.awt.geom.Point2D; 042 import java.awt.geom.Rectangle2D; 043 import java.beans.PropertyChangeEvent; 044 import java.beans.PropertyChangeListener; 045 import java.beans.PropertyChangeSupport; 046 import java.text.DecimalFormat; 047 import java.text.NumberFormat; 048 import java.util.LinkedList; 049 import java.util.List; 050 051 // TODO: keyboard navigation 052 // TODO: honor clip rect with text painting 053 // TODO: let client change zoom multiplier 054 // TODO: improve text drawing when origin is not on a multiple of majorX/majorY 055 // TODO: programmatically zoom in and out (or expose ZOOM_MULTIPLIER) 056 057 /** 058 * <h2>Presentation</h2> 059 * 060 * <p><code>JXGraph</code> provides a component which can display one or more 061 * plots on top of a graduated background (or grid.)</p> 062 * 063 * <h2>User input</h2> 064 * 065 * <p>To help analyzing the plots, this component allows the user to pan the 066 * view by left-clicking and dragging the mouse around. Using the mouse wheel, 067 * the user is also able to zoom in and out. Clicking the middle button resets 068 * the view to its original position.</p> 069 * 070 * <p>All user input can be disabled by calling 071 * {@link #setInputEnabled(boolean)} and passing false. This does not prevent 072 * subclasses to register their own event listeners, such as mouse or key 073 * listeners.</p> 074 * 075 * <h2>Initializing the component and setting the view</h2> 076 * 077 * <p>Whenever a new instance of this component is created, the grid boundaries, 078 * or view, must be defined. The view is comprised of several elements whose 079 * descriptions are the following:</p> 080 * 081 * <ul> 082 * <li><i>minX</i>: Minimum value initially displayed by the component on the 083 * X axis (horizontally.)</li> 084 * <li><i>minY</i>: Minimum value initially displayed by the component on the 085 * Y axis (vertycally.)</li> 086 * <li><i>maxX</i>: Maximum value initially displayed by the component on the 087 * X axis (horizontally.)</li> 088 * <li><i>maxY</i>: Maximum value initially displayed by the component on the 089 * Y axis (vertycally.)</li> 090 * <li><i>originX</i>: Origin on the X axis of the vertical axis.</li> 091 * <li><i>originY</i>: Origin on the Y axis of the horizontal axis.</li> 092 * <li><i>majorX</i>: Distance between two major vertical lines of the 093 * grid.</li> 094 * <li><i>majorY</i>: Distance between two major horizontal lines of the 095 * grid.</li> 096 * <li><i>minCountX</i>: Number of minor vertical lines between two major 097 * vertical lines in the grid.</li> 098 * <li><i>minCountY</i>: Number of minor horizontal lines between two major 099 * horizontal lines in the grid.</li> 100 * </ul> 101 * 102 * <h3>View and origin</h3> 103 * 104 * <p>The default constructor defines a view bounds by <code>-1.0</code> and 105 * <code>+1.0</code> on both axis, and centered on an origin at 106 * <code>(0, 0)</code>.</p> 107 * 108 * <p>To simplify the API, the origin can be read and written with a 109 * <code>Point2D</code> instance (see {@link #getOrigin()} and 110 * {@link #setOrigin(Point2D)}.)</p> 111 * 112 * <p>Likewise, the view can be read and written with a 113 * <code>Rectangle2D</code> instance (see {@link #getView()} and 114 * {@link #setView(Rectangle2D)}.) In this case, you need not to define the 115 * maximum boundaries of the view. Instead, you need to set the origin of the 116 * rectangle as the minimum boundaries. The width and the height of the 117 * rectangle define the distance between the minimum and maximum boundaries. For 118 * instance, to set the view to minX=-1.0, maxX=1.0, minY=-1.0 and maxY=1.0 you 119 * can use the following rectangle:</p> 120 * 121 * <pre>new Rectangle2D.Double(-1.0d, -1.0d, 2.0d, 2.0d);</pre> 122 * 123 * <p>You can check the boundaries by calling <code>Rectangle2D.getMaxX()</code> 124 * and <code>Rectangle2D.getMaxY()</code> once your rectangle has been 125 * created.</p> 126 * 127 * <p>Alternatively, you can set the view and the origin at the same time by 128 * calling the method {@link #setViewAndOrigin(Rectangle2D)}. Calling this 129 * method will set the origin so as to center it in the view defined by the 130 * rectangle.</p> 131 * 132 * <h3>Grid lines</h3> 133 * 134 * <p>By default, the component defines a spacing of 0.2 unites between two 135 * major grid lines. It also defines 4 minor grid lines between two major 136 * grid lines. The spacing between major grid lines and the number of minor 137 * grid lines can be accessed through the getters {@link #getMajorX()}, 138 * {@link #getMajorY()}, {@link #getMinorCountX()} and 139 * {@link #getMinorCountY()}.</p> 140 * 141 * <p>You can change the number of grid lines at runtime by calling the setters 142 * {@link #setMajorX(double)}, {@link #setMajorY(double)}, 143 * {@link #setMinorCountX(int)} and {@link #setMinorCountY(int)}.</p> 144 * 145 * <h3>Appearance</h3> 146 * 147 * <p>Although it provides sensible defaults, this component lets you change 148 * its appearance in several ways. It is possible to modify the colors of the 149 * graph by calling the setters {@link #setAxisColor(Color)}, 150 * {@link #setMajorGridColor(Color)} and {@link #setMinorGridColor(Color)}.</p> 151 * 152 * <p>You can also enable or disable given parts of the resulting graph by 153 * calling the following setters:</p> 154 * 155 * <ul> 156 * <li>{@link #setAxisPainted(boolean)}: Defines whether the main axis (see 157 * {@link #getOrigin()}) is painted.</li> 158 * <li>{@link #setBackgroundPainted(boolean)}: Defines whether the background 159 * is painted (see {@link #setBackground(Color)}.)</li> 160 * <li>{@link #setGridPainted(boolean)}: Defines whether the grid is 161 * painted.</li> 162 * <li>{@link #setTextPainted(boolean)}: Defines whether the axis labels are 163 * painted.</li> 164 * </ul> 165 * 166 * <h3>Usage example</h3> 167 * 168 * <p>The following code snippet creates a new graph centered on 169 * <code>(0, 0)</code>, bound to the view <code>[-1.0 1.0 -1.0 1.0</code>, with 170 * a major grid line every 0.5 units and a minor grid lines count of 5:</p> 171 * 172 * <pre> 173 * Point2D origin = new Point2D.Double(0.0d, 0.0d); 174 * Rectangle2D view = new Rectangle2D.Double(-1.0d, 1.0d, 2.0d, 2.0d); 175 * JXGraph graph = new JXGraph(origin, view, 0.5d, 5, 0.5d, 5); 176 * </pre> 177 * 178 * <h2>Plots</h2> 179 * 180 * <h3>Definition</h3> 181 * 182 * <p>A plot is defined by a mathematical transformation that, given a value on 183 * the graph's X axis, returns a value on the Y axis. The component draws the 184 * result by plotting a spot of color at the coordinates defined by 185 * <code>(X, f(X))</code> where <code>f()</code> is the aforementionned 186 * mathematical transformation. Given the following transformation:</p> 187 * 188 * <pre> 189 * f(X) = X * 2.0 190 * </pre> 191 * 192 * <p>For <code>X=1.0</code>, the component will show a spot of color at the 193 * coordinates <code>(1.0, 2.0)</code>.</p> 194 * 195 * <h3>Creating a new plot</h3> 196 * 197 * <p>Every plot drawn by the component must be a subclass of 198 * {@link JXGraph.Plot}. This abstract public class defines a single method to 199 * be implemented by its children:</p> 200 * 201 * <pre> 202 * public double compute(double value) 203 * </pre> 204 * 205 * <p>The previous example can be defined by a concrete 206 * <code>JXGraph.Plot</code> as follow:</p> 207 * 208 * <pre> 209 * class TwiceTheValuePlot extends JXGraph.Plot { 210 * public double compute(double value) { 211 * return value * 2.0d; 212 * } 213 * } 214 * </pre> 215 * 216 * <p>Most of the time though, a plot requires supplementary parameters. For 217 * instance, let's define the X axis of your graph as the mass of an object. To 218 * compute the weight of the object given its mass, you need to use the 219 * acceleration of gravity (<code>w=m.g</code> where <code>g</code> is the 220 * acceleration.) To let the user modify this last parameter, to compute his 221 * weight at the surface of the moon for instance, you need to add a parameter 222 * to your plot.</p> 223 * 224 * <p>While <code>JXGraph.Plot</code> does not give you an API for such a 225 * purpose, it does define an event dispatching API (see 226 * {@link JXGraph.Plot#firePropertyChange(String, double, double)}.) Whenever a 227 * plot is added to the graph, the component registers itself as a property 228 * listener of the plot. If you take care of firing events whenever the user 229 * changes a parameter of your plot, the graph will automatically update its 230 * display. While not mandatory, it is highly recommended to leverage this 231 * API.</p> 232 * 233 * <h3>Adding and removing plots to and from the graph</h3> 234 * 235 * <p>To add a plot to the graph, simply call the method 236 * {@link #addPlots(Color, JXGraph.Plot...)}. You can use it to add one or more 237 * plots at the same time and associate them with a color. This color is used 238 * when drawing the plots:</p> 239 * 240 * <pre> 241 * JXGraph.Plot plot = new TwiceTheValuePlot(); 242 * graph.addPlots(Color.BLUE, plot); 243 * </pre> 244 * 245 * <p>These two lines will display our previously defined plot in blue on 246 * screen. Removing one or several plots is as simple as calling the method 247 * {@link #removePlots(JXGraph.Plot...)}. You can also remove all plots at once 248 * with {@link #removeAllPlots()}.</p> 249 * 250 * <h2>Painting more information</h2> 251 * 252 * <h3>How to draw on the graph</h3> 253 * 254 * <p>If you need to add more information on the graph you need to extend 255 * its class and override the method {@link #paintExtra(Graphics2D)}. This 256 * method has a default empty implementation and is called after everything 257 * has been drawn. Its sole parameter is a reference to the component's drawing 258 * surface, as configured by {@link #setupGraphics(Graphics2D)}. By default, the 259 * setup method activates antialising but it can be overriden to change the 260 * drawing surface. (Translation, rotation, new rendering hints, etc.)</p> 261 * 262 * <h3>Getting the right coordinates</h3> 263 * 264 * <p>To properly draw on the graph you will need to perform a translation 265 * between the graph's coordinates and the screen's coordinates. The component 266 * defines 4 methods to assist you in this task:</p> 267 * 268 * <ul> 269 * <li>{@link #xPixelToPosition(double)}: Converts a pixel coordinate on the 270 * X axis into a world coordinate.</li> 271 * <li>{@link #xPositionToPixel(double)}: Converts a world coordinate on the 272 * X axis into a pixel coordinate.</li> 273 * <li>{@link #yPixelToPosition(double)}: Converts a pixel coordinate on the 274 * Y axis into a world coordinate.</li> 275 * <li>{@link #yPositionToPixel(double)}: Converts a world coordinate on the 276 * Y axis into a pixel coordinate.</li> 277 * </ul> 278 * 279 * <p>If you have defined a graph view centered on the origin 280 * <code>(0, 0)</code>, the origin of the graph will be at the exact center of 281 * the screen. That means the world coordinates <code>(0, 0)</code> are 282 * equivalent to the pixel coordinates <code>(width / 2, height / 2)</code>. 283 * Thus, calling <code>xPositionToPixel(0.0d)</code> would give you the same 284 * value as the expression <code>getWidth() / 2.0d</code>.</p> 285 * 286 * <p>Converting from world coordinates to pixel coordinates is mostly used to 287 * draw the result of a mathematical transformation. Converting from pixel 288 * coordinates to world coordinates is mostly used to get the position in the 289 * world of a mouse event.</p> 290 * 291 * @see JXGraph.Plot 292 * @author Romain Guy <romain.guy@mac.com> 293 */ 294 public class JXGraph extends JXPanel implements PropertyChangeListener { 295 // stroke widths used to draw the main axis and the grid 296 // the main axis is slightly thicker 297 private static final float STROKE_AXIS = 1.2f; 298 private static final float STROKE_GRID = 1.0f; 299 300 // defines by how much the view is shrinked or expanded everytime the 301 // user zooms in or out 302 private static final float ZOOM_MULTIPLIER = 1.1f; 303 304 // default color of the graph (does not include plots colors) 305 private Color majorGridColor = Color.GRAY.brighter(); 306 private Color minorGridColor = new Color(220, 220, 220); 307 private Color axisColor = Color.BLACK; 308 309 // the list of plots currently known and displayed by the graph 310 private List<DrawablePlot> plots; 311 312 // view boundaries as defined by the user 313 private double minX; 314 private double maxX; 315 private double minY; 316 private double maxY; 317 318 // the default view is set when the view is manually changed by the client 319 // it is used to reset the view in resetView() 320 private Rectangle2D defaultView; 321 322 // coordinates of the major axis 323 private double originX; 324 private double originY; 325 326 // definition of the grid 327 // various default values are used when the view is reset 328 private double majorX; 329 private double defaultMajorX; 330 private int minorCountX; 331 private double majorY; 332 private double defaultMajorY; 333 private int minorCountY; 334 335 // enables painting layers 336 private boolean textPainted = true; 337 private boolean gridPainted = true; 338 private boolean axisPainted = true; 339 private boolean backPainted = true; 340 341 // used by the PanHandler to move the view 342 private Point dragStart; 343 344 // mainFormatter is used for numbers > 0.01 and < 100 345 // secondFormatter uses scientific notation 346 private NumberFormat mainFormatter; 347 private NumberFormat secondFormatter; 348 349 // input handlers 350 private boolean inputEnabled = true; 351 private ZoomHandler zoomHandler; 352 private PanMotionHandler panMotionHandler; 353 private PanHandler panHandler; 354 private ResetHandler resetHandler; 355 356 /** 357 * <p>Creates a new graph display. The following properties are 358 * automatically set:</p> 359 * <ul> 360 * <li><i>view</i>: -1.0 to +1.0 on both axis</li> 361 * <li><i>origin</i>: At <code>(0, 0)</code></li> 362 * <li><i>grid</i>: Spacing of 0.2 between major lines; minor lines 363 * count is 4</li> 364 * </ul> 365 */ 366 public JXGraph() { 367 this(0.0, 0.0, -1.0, 1.0, -1.0, 1.0, 0.2, 4, 0.2, 4); 368 } 369 370 /** 371 * <p>Creates a new graph display with the specified view. The following 372 * properties are automatically set:</p> 373 * <ul> 374 * <li><i>origin</i>: Center of the specified view</code></li> 375 * <li><i>grid</i>: Spacing of 0.2 between major lines; minor lines 376 * count is 4</li> 377 * </ul> 378 * 379 * @param view the rectangle defining the view boundaries 380 */ 381 public JXGraph(Rectangle2D view) { 382 this(new Point2D.Double(view.getCenterX(), view.getCenterY()), 383 view, 0.2, 4, 0.2, 4); 384 } 385 386 /** 387 * <p>Creates a new graph display with the specified view and grid lines. 388 * The origin is set at the center of the view.</p> 389 * 390 * @param view the rectangle defining the view boundaries 391 * @param majorX the spacing between two major grid lines on the X axis 392 * @param minorCountX the number of minor grid lines betweek two major 393 * grid lines on the X axis 394 * @param majorY the spacing between two major grid lines on the Y axis 395 * @param minorCountY the number of minor grid lines betweek two major 396 * grid lines on the Y axis 397 * @throws IllegalArgumentException if minX >= maxX or minY >= maxY or 398 * minorCountX < 0 or minorCountY < 0 or 399 * majorX <= 0.0 or majorY <= 0.0 400 */ 401 public JXGraph(Rectangle2D view, 402 double majorX, int minorCountX, 403 double majorY, int minorCountY) { 404 this(new Point2D.Double(view.getCenterX(), view.getCenterY()), 405 view, majorX, minorCountX, majorY, minorCountY); 406 } 407 408 /** 409 * <p>Creates a new graph display with the specified view and origin. 410 * The following properties are automatically set:</p> 411 * <ul> 412 * <li><i>grid</i>: Spacing of 0.2 between major lines; minor lines 413 * count is 4</li> 414 * </ul> 415 * 416 * @param origin the coordinates of the main axis origin 417 * @param view the rectangle defining the view boundaries 418 */ 419 public JXGraph(Point2D origin, Rectangle2D view) { 420 this(origin, view, 0.2, 4, 0.2, 4); 421 } 422 423 /** 424 * <p>Creates a new graph display with the specified view, origin and grid 425 * lines.</p> 426 * 427 * @param origin the coordinates of the main axis origin 428 * @param view the rectangle defining the view boundaries 429 * @param majorX the spacing between two major grid lines on the X axis 430 * @param minorCountX the number of minor grid lines betweek two major 431 * grid lines on the X axis 432 * @param majorY the spacing between two major grid lines on the Y axis 433 * @param minorCountY the number of minor grid lines betweek two major 434 * grid lines on the Y axis 435 * @throws IllegalArgumentException if minX >= maxX or minY >= maxY or 436 * minorCountX < 0 or minorCountY < 0 or 437 * majorX <= 0.0 or majorY <= 0.0 438 */ 439 public JXGraph(Point2D origin, Rectangle2D view, 440 double majorX, int minorCountX, 441 double majorY, int minorCountY) { 442 this(origin.getX(), origin.getY(), 443 view.getMinX(), view.getMaxX(), view.getMinY(), view.getMaxY(), 444 majorX, minorCountX, majorY, minorCountY); 445 } 446 447 /** 448 * <p>Creates a new graph display with the specified view, origin and grid 449 * lines.</p> 450 * 451 * @param originX the coordinate of the major X axis 452 * @param originY the coordinate of the major Y axis 453 * @param minX the minimum coordinate on the X axis for the view 454 * @param maxX the maximum coordinate on the X axis for the view 455 * @param minY the minimum coordinate on the Y axis for the view 456 * @param maxY the maximum coordinate on the Y axis for the view 457 * @param majorX the spacing between two major grid lines on the X axis 458 * @param minorCountX the number of minor grid lines betweek two major 459 * grid lines on the X axis 460 * @param majorY the spacing between two major grid lines on the Y axis 461 * @param minorCountY the number of minor grid lines betweek two major 462 * grid lines on the Y axis 463 * @throws IllegalArgumentException if minX >= maxX or minY >= maxY or 464 * minorCountX < 0 or minorCountY < 0 or 465 * majorX <= 0.0 or majorY <= 0.0 466 */ 467 public JXGraph(double originX, double originY, 468 double minX, double maxX, 469 double minY, double maxY, 470 double majorX, int minorCountX, 471 double majorY, int minorCountY) { 472 if (minX >= maxX) { 473 throw new IllegalArgumentException("minX must be < to maxX"); 474 } 475 476 if (minY >= maxY) { 477 throw new IllegalArgumentException("minY must be < to maxY"); 478 } 479 480 if (minorCountX < 0) { 481 throw new IllegalArgumentException("minorCountX must be >= 0"); 482 } 483 484 if (minorCountY < 0) { 485 throw new IllegalArgumentException("minorCountY must be >= 0"); 486 } 487 488 if (majorX <= 0.0) { 489 throw new IllegalArgumentException("majorX must be > 0.0"); 490 } 491 492 if (majorY <= 0.0) { 493 throw new IllegalArgumentException("majorY must be > 0.0"); 494 } 495 496 this.originX = originX; 497 this.originY = originY; 498 499 this.minX = minX; 500 this.maxX = maxX; 501 this.minY = minY; 502 this.maxY = maxY; 503 504 this.defaultView = new Rectangle2D.Double(minX, minY, 505 maxX - minX, maxY - minY); 506 507 this.setMajorX(this.defaultMajorX = majorX); 508 this.setMinorCountX(minorCountX); 509 this.setMajorY(this.defaultMajorY = majorY); 510 this.setMinorCountY(minorCountY); 511 512 this.plots = new LinkedList<DrawablePlot>(); 513 514 this.mainFormatter = NumberFormat.getInstance(); 515 this.mainFormatter.setMaximumFractionDigits(2); 516 517 this.secondFormatter = new DecimalFormat("0.##E0"); 518 519 resetHandler = new ResetHandler(); 520 addMouseListener(resetHandler); 521 panHandler = new PanHandler(); 522 addMouseListener(panHandler); 523 panMotionHandler = new PanMotionHandler(); 524 addMouseMotionListener(panMotionHandler); 525 zoomHandler = new ZoomHandler(); 526 addMouseWheelListener(zoomHandler); 527 528 setBackground(Color.WHITE); 529 setForeground(Color.BLACK); 530 } 531 532 /** 533 * {@inheritDoc} 534 */ 535 @Override 536 public boolean isOpaque() { 537 if (!isBackgroundPainted()) { 538 return false; 539 } 540 return super.isOpaque(); 541 } 542 543 /** 544 * {@inheritDoc} 545 * @see #setInputEnabled(boolean) 546 */ 547 @Override 548 public void setEnabled(boolean enabled) { 549 super.setEnabled(enabled); 550 setInputEnabled(enabled); 551 } 552 553 /** 554 * <p>Enables or disables user input on the component. When user input is 555 * enabled, panning, zooming and view resetting. Disabling input will 556 * prevent the user from modifying the currently displayed view.<p> 557 * <p>Calling {@link #setEnabled(boolean)} disables the component in the 558 * Swing hierarchy and invokes this method.</p> 559 * 560 * @param enabled true if user input must be enabled, false otherwise 561 * @see #setEnabled(boolean) 562 * @see #isInputEnabled() 563 */ 564 public void setInputEnabled(boolean enabled) { 565 if (inputEnabled != enabled) { 566 this.inputEnabled = inputEnabled; 567 568 if (enabled) { 569 addMouseListener(resetHandler); 570 addMouseListener(panHandler); 571 addMouseMotionListener(panMotionHandler); 572 addMouseWheelListener(zoomHandler); 573 } else { 574 removeMouseListener(resetHandler); 575 removeMouseListener(panHandler); 576 removeMouseMotionListener(panMotionHandler); 577 removeMouseWheelListener(zoomHandler); 578 } 579 } 580 } 581 582 /** 583 * <p>Defines whether or not user input is accepted and managed by this 584 * component. The component is always created with user input enabled.</p> 585 * 586 * @return true if user input is enabled, false otherwise 587 * @see #setInputEnabled(boolean) 588 */ 589 public boolean isInputEnabled() { 590 return inputEnabled; 591 } 592 593 /** 594 * <p>Defines whether or not axis labels are painted by this component. 595 * The component is always created with text painting enabled.</p> 596 * 597 * @return true if axis labels are painted, false otherwise 598 * @see #setTextPainted(boolean) 599 * @see #getForeground() 600 */ 601 public boolean isTextPainted() { 602 return textPainted; 603 } 604 605 /** 606 * <p>Enables or disables the painting of axis labels depending on the 607 * value of the parameter. Text painting is enabled by default.</p> 608 * 609 * @param textPainted if true, axis labels are painted 610 * @see #isTextPainted() 611 * @see #setForeground(Color) 612 */ 613 public void setTextPainted(boolean textPainted) { 614 this.textPainted = textPainted; 615 } 616 617 /** 618 * <p>Defines whether or not grids lines are painted by this component. 619 * The component is always created with grid lines painting enabled.</p> 620 * 621 * @return true if grid lines are painted, false otherwise 622 * @see #setGridPainted(boolean) 623 * @see #getMajorGridColor() 624 * @see #getMinorGridColor() 625 */ 626 public boolean isGridPainted() { 627 return gridPainted; 628 } 629 630 /** 631 * <p>Enables or disables the painting of grid lines depending on the 632 * value of the parameter. Grid painting is enabled by default.</p> 633 * 634 * @param gridPainted if true, axis labels are painted 635 * @see #isGridPainted() 636 * @see #setMajorGridColor(Color) 637 * @see #setMinorGridColor(Color) 638 */ 639 public void setGridPainted(boolean gridPainted) { 640 this.gridPainted = gridPainted; 641 } 642 643 /** 644 * <p>Defines whether or not the graph main axis is painted by this 645 * component. The component is always created with main axis painting 646 * enabled.</p> 647 * 648 * @return true if main axis is painted, false otherwise 649 * @see #setTextPainted(boolean) 650 * @see #getAxisColor() 651 */ 652 public boolean isAxisPainted() { 653 return axisPainted; 654 } 655 656 /** 657 * <p>Enables or disables the painting of main axis depending on the 658 * value of the parameter. Axis painting is enabled by default.</p> 659 * 660 * @param axisPainted if true, axis labels are painted 661 * @see #isAxisPainted() 662 * @see #setAxisColor(Color) 663 */ 664 public void setAxisPainted(boolean axisPainted) { 665 this.axisPainted = axisPainted; 666 } 667 668 /** 669 * <p>Defines whether or not the background painted by this component. 670 * The component is always created with background painting enabled. 671 * When background painting is disabled, background painting is deferred 672 * to the parent class.</p> 673 * 674 * @return true if background is painted, false otherwise 675 * @see #setBackgroundPainted(boolean) 676 * @see #getBackground() 677 */ 678 public boolean isBackgroundPainted() { 679 return backPainted; 680 } 681 682 /** 683 * <p>Enables or disables the painting of background depending on the 684 * value of the parameter. Background painting is enabled by default.</p> 685 * 686 * @param backPainted if true, axis labels are painted 687 * @see #isBackgroundPainted() 688 * @see #setBackground(Color) 689 */ 690 public void setBackgroundPainted(boolean backPainted) { 691 this.backPainted = backPainted; 692 } 693 694 /** 695 * <p>Gets the major grid lines color of this component.</p> 696 * 697 * @return this component's major grid lines color 698 * @see #setMajorGridColor(Color) 699 * @see #setGridPainted(boolean) 700 */ 701 public Color getMajorGridColor() { 702 return majorGridColor; 703 } 704 705 /** 706 * <p>Sets the color of major grid lines on this component. The color 707 * can be translucent.</p> 708 * 709 * @param majorGridColor the color to become this component's major grid 710 * lines color 711 * @throws IllegalArgumentException if the specified color is null 712 * @see #getMajorGridColor() 713 * @see #isGridPainted() 714 */ 715 public void setMajorGridColor(Color majorGridColor) { 716 if (majorGridColor == null) { 717 throw new IllegalArgumentException("Color cannot be null."); 718 } 719 720 this.majorGridColor = majorGridColor; 721 } 722 723 /** 724 * <p>Gets the minor grid lines color of this component.</p> 725 * 726 * @return this component's minor grid lines color 727 * @see #setMinorGridColor(Color) 728 * @see #setGridPainted(boolean) 729 */ 730 public Color getMinorGridColor() { 731 return minorGridColor; 732 } 733 734 /** 735 * <p>Sets the color of minor grid lines on this component. The color 736 * can be translucent.</p> 737 * 738 * @param minorGridColor the color to become this component's minor grid 739 * lines color 740 * @throws IllegalArgumentException if the specified color is null 741 * @see #getMinorGridColor() 742 * @see #isGridPainted() 743 */ 744 public void setMinorGridColor(Color minorGridColor) { 745 if (minorGridColor == null) { 746 throw new IllegalArgumentException("Color cannot be null."); 747 } 748 this.minorGridColor = minorGridColor; 749 } 750 751 /** 752 * <p>Gets the main axis color of this component.</p> 753 * 754 * @return this component's main axis color 755 * @see #setAxisColor(Color) 756 * @see #setGridPainted(boolean) 757 */ 758 public Color getAxisColor() { 759 return axisColor; 760 } 761 762 /** 763 * <p>Sets the color of main axis on this component. The color 764 * can be translucent.</p> 765 * 766 * @param axisColor the color to become this component's main axis color 767 * @throws IllegalArgumentException if the specified color is null 768 * @see #getAxisColor() 769 * @see #isAxisPainted() 770 */ 771 public void setAxisColor(Color axisColor) { 772 if (axisColor == null) { 773 throw new IllegalArgumentException("Color cannot be null."); 774 } 775 this.axisColor = axisColor; 776 } 777 778 /** 779 * <p>Gets the distance, in graph units, between two major grid lines on 780 * the X axis.</p> 781 * 782 * @return the spacing between two major grid lines on the X axis 783 * @see #setMajorX(double) 784 * @see #getMajorY() 785 * @see #setMajorY(double) 786 * @see #getMinorCountX() 787 * @see #setMinorCountX(int) 788 */ 789 public double getMajorX() { 790 return majorX; 791 } 792 793 /** 794 * <p>Sets the distance, in graph units, between two major grid lines on 795 * the X axis.</p> 796 * 797 * @param majorX the requested spacing between two major grid lines on the 798 * X axis 799 * @throws IllegalArgumentException if majorX is <= 0.0d 800 * @see #getMajorX() 801 * @see #getMajorY() 802 * @see #setMajorY(double) 803 * @see #getMinorCountX() 804 * @see #setMinorCountX(int) 805 */ 806 public void setMajorX(double majorX) { 807 if (majorX <= 0.0) { 808 throw new IllegalArgumentException("majorX must be > 0.0"); 809 } 810 811 this.majorX = majorX; 812 this.defaultMajorX = majorX; 813 repaint(); 814 } 815 816 /** 817 * <p>Gets the number of minor grid lines between two major grid lines 818 * on the X axis.</p> 819 * 820 * @return the number of minor grid lines between two major grid lines 821 * @see #setMinorCountX(int) 822 * @see #getMinorCountY() 823 * @see #setMinorCountY(int) 824 * @see #getMajorX() 825 * @see #setMajorX(double) 826 */ 827 public int getMinorCountX() { 828 return minorCountX; 829 } 830 831 /** 832 * <p>Sets the number of minor grid lines between two major grid lines on 833 * the X axis.</p> 834 * 835 * @param minorCountX the number of minor grid lines between two major grid 836 * lines on the X axis 837 * @throws IllegalArgumentException if minorCountX is < 0 838 * @see #getMinorCountX() 839 * @see #getMinorCountY() 840 * @see #setMinorCountY(int) 841 * @see #getMajorX() 842 * @see #setMajorX(double) 843 */ 844 public void setMinorCountX(int minorCountX) { 845 if (minorCountX < 0) { 846 throw new IllegalArgumentException("minorCountX must be >= 0"); 847 } 848 849 this.minorCountX = minorCountX; 850 repaint(); 851 } 852 853 /** 854 * <p>Gets the distance, in graph units, between two major grid lines on 855 * the Y axis.</p> 856 * 857 * @return the spacing between two major grid lines on the Y axis 858 * @see #setMajorY(double) 859 * @see #getMajorX() 860 * @see #setMajorX(double) 861 * @see #getMinorCountY() 862 * @see #setMinorCountY(int) 863 */ 864 public double getMajorY() { 865 return majorY; 866 } 867 868 /** 869 * <p>Sets the distance, in graph units, between two major grid lines on 870 * the Y axis.</p> 871 * 872 * @param majorY the requested spacing between two major grid lines on the 873 * Y axis 874 * @throws IllegalArgumentException if majorY is <= 0.0d 875 * @see #getMajorY() 876 * @see #getMajorX() 877 * @see #setMajorX(double) 878 * @see #getMinorCountY() 879 * @see #setMinorCountY(int) 880 */ 881 public void setMajorY(double majorY) { 882 if (majorY <= 0.0) { 883 throw new IllegalArgumentException("majorY must be > 0.0"); 884 } 885 886 this.majorY = majorY; 887 this.defaultMajorY = majorY; 888 repaint(); 889 } 890 891 /** 892 * <p>Gets the number of minor grid lines between two major grid lines 893 * on the Y axis.</p> 894 * 895 * @return the number of minor grid lines between two major grid lines 896 * @see #setMinorCountY(int) 897 * @see #getMinorCountX() 898 * @see #setMinorCountX(int) 899 * @see #getMajorY() 900 * @see #setMajorY(double) 901 */ 902 public int getMinorCountY() { 903 return minorCountY; 904 } 905 906 /** 907 * <p>Sets the number of minor grid lines between two major grid lines on 908 * the Y axis.</p> 909 * 910 * @param minorCountY the number of minor grid lines between two major grid 911 * lines on the Y axis 912 * @throws IllegalArgumentException if minorCountY is < 0 913 * @see #getMinorCountY() 914 * @see #getMinorCountX() 915 * @see #setMinorCountX(int) 916 * @see #getMajorY() 917 * @see #setMajorY(double) 918 */ 919 public void setMinorCountY(int minorCountY) { 920 if (minorCountY < 0) { 921 throw new IllegalArgumentException("minorCountY must be >= 0"); 922 } 923 924 this.minorCountY = minorCountY; 925 repaint(); 926 } 927 928 /** 929 * <p>Sets the view and the origin of the graph at the same time. The view 930 * minimum boundaries are defined by the location of the rectangle passed 931 * as parameter. The width and height of the rectangle define the distance 932 * between the minimum and maximum boundaries:</p> 933 * 934 * <ul> 935 * <li><i>minX</i>: bounds.getX()</li> 936 * <li><i>minY</i>: bounds.getY()</li> 937 * <li><i>maxY</i>: bounds.getMaxX() (minX + bounds.getWidth())</li> 938 * <li><i>maxX</i>: bounds.getMaxY() (minY + bounds.getHeight())</li> 939 * </ul> 940 * 941 * <p>The origin is located at the center of the view. Its coordinates are 942 * defined by calling bounds.getCenterX() and bounds.getCenterY().</p> 943 * 944 * @param bounds the rectangle defining the graph's view and its origin 945 * @see #getView() 946 * @see #setView(Rectangle2D) 947 * @see #getOrigin() 948 * @see #setOrigin(Point2D) 949 */ 950 public void setViewAndOrigin(Rectangle2D bounds) { 951 setView(bounds); 952 setOrigin(new Point2D.Double(bounds.getCenterX(), bounds.getCenterY())); 953 } 954 955 /** 956 * <p>Sets the view of the graph. The view minimum boundaries are defined by 957 * the location of the rectangle passed as parameter. The width and height 958 * of the rectangle define the distance between the minimum and maximum 959 * boundaries:</p> 960 * 961 * <ul> 962 * <li><i>minX</i>: bounds.getX()</li> 963 * <li><i>minY</i>: bounds.getY()</li> 964 * <li><i>maxY</i>: bounds.getMaxX() (minX + bounds.getWidth())</li> 965 * <li><i>maxX</i>: bounds.getMaxY() (minY + bounds.getHeight())</li> 966 * </ul> 967 * 968 * <p>If the specificed view is null, nothing happens.</p> 969 * 970 * <p>Calling this method leaves the origin intact.</p> 971 * 972 * @param bounds the rectangle defining the graph's view and its origin 973 * @see #getView() 974 * @see #setViewAndOrigin(Rectangle2D) 975 */ 976 public void setView(Rectangle2D bounds) { 977 if (bounds == null) { 978 return; 979 } 980 981 defaultView = new Rectangle2D.Double(bounds.getX(), bounds.getY(), 982 bounds.getWidth(), bounds.getHeight()); 983 984 minX = defaultView.getMinX(); 985 maxX = defaultView.getMaxX(); 986 minY = defaultView.getMinY(); 987 maxY = defaultView.getMaxY(); 988 989 majorX = defaultMajorX; 990 majorY = defaultMajorY; 991 992 repaint(); 993 } 994 995 /** 996 * <p>Gets the view of the graph. The returned rectangle defines the bounds 997 * of the view as follows:</p> 998 * 999 * <ul> 1000 * <li><i>minX</i>: bounds.getX()</li> 1001 * <li><i>minY</i>: bounds.getY()</li> 1002 * <li><i>maxY</i>: bounds.getMaxX() (minX + bounds.getWidth())</li> 1003 * <li><i>maxX</i>: bounds.getMaxY() (minY + bounds.getHeight())</li> 1004 * </ul> 1005 * 1006 * @return the rectangle corresponding to the current view of the graph 1007 * @see #setView(Rectangle2D) 1008 * @see #setViewAndOrigin(Rectangle2D) 1009 */ 1010 public Rectangle2D getView() { 1011 return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); 1012 } 1013 1014 /** 1015 * <p>Resets the view to the default view if it has been changed by the user 1016 * by panning and zooming. The default view is defined by the view last 1017 * specified in a constructor call or a call to the methods 1018 * {@link #setView(Rectangle2D)} and 1019 * {@link #setViewAndOrigin(Rectangle2D)}.</p> 1020 * 1021 * @see #setView(Rectangle2D) 1022 * @see #setViewAndOrigin(Rectangle2D) 1023 */ 1024 public void resetView() { 1025 setView(defaultView); 1026 } 1027 1028 /** 1029 * <p>Sets the origin of the graph. The coordinates of the origin are 1030 * defined by the coordinates of the point passed as parameter.</p> 1031 * 1032 * <p>If the specificed view is null, nothing happens.</p> 1033 * 1034 * <p>Calling this method leaves the view intact.</p> 1035 * 1036 * @param origin the coordinates of the new origin 1037 * @see #getOrigin() 1038 * @see #setViewAndOrigin(Rectangle2D) 1039 */ 1040 public void setOrigin(Point2D origin) { 1041 if (origin == null) { 1042 return; 1043 } 1044 1045 originX = origin.getX(); 1046 originY = origin.getY(); 1047 1048 repaint(); 1049 } 1050 1051 /** 1052 * <p>Gets the origin coordinates of the graph. The coordinates are 1053 * represented as an instance of <code>Point2D</code> and stored in 1054 * <code>double</code> format.</p> 1055 1056 * @return the origin coordinates in double format 1057 * @see #setOrigin(Point2D) 1058 * @see #setViewAndOrigin(Rectangle2D) 1059 */ 1060 public Point2D getOrigin() { 1061 return new Point2D.Double(originX, originY); 1062 } 1063 1064 /** 1065 * <p>Adds one or more plots to the graph. These plots are associated to 1066 * a color used to draw them.</p> 1067 * 1068 * <p>If plotList is null or empty, nothing happens.</p> 1069 * 1070 * <p>This method is not thread safe and should be called only from the 1071 * EDT.</p> 1072 * 1073 * @param color the color to be usd to draw the plots 1074 * @param plotList the list of plots to add to the graph 1075 * @throws IllegalArgumentException if color is null 1076 * @see #removePlots(JXGraph.Plot...) 1077 * @see #removeAllPlots() 1078 */ 1079 public void addPlots(Color color, Plot... plotList) { 1080 if (color == null) { 1081 throw new IllegalArgumentException("Plots color cannot be null."); 1082 } 1083 1084 if (plotList == null) { 1085 return; 1086 } 1087 1088 for (Plot plot : plotList) { 1089 if (plot != null && !plots.contains(plot)) { 1090 plot.addPropertyChangeListener(this); 1091 plots.add(new DrawablePlot(plot, color)); 1092 } 1093 } 1094 repaint(); 1095 } 1096 1097 /** 1098 * <p>Removes the specified plots from the graph. Plots to be removed 1099 * are identified by identity. This means you cannot remove a plot by 1100 * passing a clone or another instance of the same subclass of 1101 * {@link JXGraph.Plot}.</p> 1102 * 1103 * <p>If plotList is null or empty, nothing happens.</p> 1104 * 1105 * <p>This method is not thread safe and should be called only from the 1106 * EDT.</p> 1107 * 1108 * @param plotList the list of plots to be removed from the graph 1109 * @see #removeAllPlots() 1110 * @see #addPlots(Color, JXGraph.Plot...) 1111 */ 1112 public void removePlots(Plot... plotList) { 1113 if (plotList == null) { 1114 return; 1115 } 1116 1117 for (Plot plot : plotList) { 1118 if (plot != null) { 1119 DrawablePlot toRemove = null; 1120 for (DrawablePlot drawable: plots) { 1121 if (drawable.getEquation() == plot) { 1122 toRemove = drawable; 1123 break; 1124 } 1125 } 1126 1127 if (toRemove != null) { 1128 plot.removePropertyChangeListener(this); 1129 plots.remove(toRemove); 1130 } 1131 } 1132 } 1133 repaint(); 1134 } 1135 1136 /** 1137 * <p>Removes all the plots currently associated with this graph.</p> 1138 * 1139 * <p>This method is not thread safe and should be called only from the 1140 * EDT.</p> 1141 * 1142 * @see #removePlots(JXGraph.Plot...) 1143 * @see #addPlots(Color, JXGraph.Plot...) 1144 */ 1145 public void removeAllPlots() { 1146 plots.clear(); 1147 repaint(); 1148 } 1149 1150 /** 1151 * {@inheritDoc} 1152 */ 1153 @Override 1154 public Dimension getPreferredSize() { 1155 return new Dimension(400, 400); 1156 } 1157 1158 /** 1159 * {@inheritDoc} 1160 */ 1161 public void propertyChange(PropertyChangeEvent evt) { 1162 repaint(); 1163 } 1164 1165 /** 1166 * <p>Converts a position, in graph units, from the Y axis into a pixel 1167 * coordinate. For instance, if you defined the origin so it appears at the 1168 * exact center of the view, calling 1169 * <code>yPositionToPixel(getOriginY())</code> will return a value 1170 * approximately equal to <code>getHeight() / 2.0</code>.</p> 1171 * 1172 * @param position the Y position to be converted into pixels 1173 * @return the coordinate in pixels of the specified graph Y position 1174 * @see #xPositionToPixel(double) 1175 * @see #yPixelToPosition(double) 1176 */ 1177 protected double yPositionToPixel(double position) { 1178 double height = (double) getHeight(); 1179 return height - ((position - minY) * height / (maxY - minY)); 1180 } 1181 1182 /** 1183 * <p>Converts a position, in graph units, from the X axis into a pixel 1184 * coordinate. For instance, if you defined the origin so it appears at the 1185 * exact center of the view, calling 1186 * <code>xPositionToPixel(getOriginX())</code> will return a value 1187 * approximately equal to <code>getWidth() / 2.0</code>.</p> 1188 * 1189 * @param position the X position to be converted into pixels 1190 * @return the coordinate in pixels of the specified graph X position 1191 * @see #yPositionToPixel(double) 1192 * @see #xPixelToPosition(double) 1193 */ 1194 protected double xPositionToPixel(double position) { 1195 return (position - minX) * (double) getWidth() / (maxX - minX); 1196 } 1197 1198 /** 1199 * <p>Converts a pixel coordinate from the X axis into a graph position, in 1200 * graph units. For instance, if you defined the origin so it appears at the 1201 * exact center of the view, calling 1202 * <code>xPixelToPosition(getWidth() / 2.0)</code> will return a value 1203 * approximately equal to <code>getOriginX()</code>.</p> 1204 * 1205 * @param pixel the X pixel coordinate to be converted into a graph position 1206 * @return the graph X position of the specified pixel coordinate 1207 * @see #yPixelToPosition(double) 1208 * @see #xPositionToPixel(double) 1209 */ 1210 protected double xPixelToPosition(double pixel) { 1211 double axisV = xPositionToPixel(originX); 1212 return (pixel - axisV) * (maxX - minX) / (double) getWidth(); 1213 } 1214 1215 /** 1216 * <p>Converts a pixel coordinate from the Y axis into a graph position, in 1217 * graph units. For instance, if you defined the origin so it appears at the 1218 * exact center of the view, calling 1219 * <code>yPixelToPosition(getHeight() / 2.0)</code> will return a value 1220 * approximately equal to <code>getOriginY()</code>.</p> 1221 * 1222 * @param pixel the Y pixel coordinate to be converted into a graph position 1223 * @return the graph Y position of the specified pixel coordinate 1224 * @see #xPixelToPosition(double) 1225 * @see #yPositionToPixel(double) 1226 */ 1227 protected double yPixelToPosition(double pixel) { 1228 double axisH = yPositionToPixel(originY); 1229 return (getHeight() - pixel - axisH) * (maxY - minY) / (double) getHeight(); 1230 } 1231 1232 /** 1233 * {@inheritDoc} 1234 */ 1235 @Override 1236 protected void paintComponent(Graphics g) { 1237 if (!isVisible()) { 1238 return; 1239 } 1240 1241 Graphics2D g2 = (Graphics2D) g; 1242 setupGraphics(g2); 1243 1244 paintBackground(g2); 1245 drawGrid(g2); 1246 drawAxis(g2); 1247 drawPlots(g2); 1248 drawLabels(g2); 1249 1250 paintExtra(g2); 1251 } 1252 1253 /** 1254 * <p>This painting method is meant to be overriden by subclasses of 1255 * <code>JXGraph</code>. This method is called after all the painting 1256 * is done. By overriding this method, a subclass can display extra 1257 * information on top of the graph.</p> 1258 * <p>The graphics surface passed as parameter is configured by 1259 * {@link #setupGraphics(Graphics2D)}.</p> 1260 * 1261 * @param g2 the graphics surface on which the graph is drawn 1262 * @see #setupGraphics(Graphics2D) 1263 * @see #xPixelToPosition(double) 1264 * @see #yPixelToPosition(double) 1265 * @see #xPositionToPixel(double) 1266 * @see #yPositionToPixel(double) 1267 */ 1268 protected void paintExtra(Graphics2D g2) { 1269 } 1270 1271 // Draw all the registered plots with the appropriate color. 1272 private void drawPlots(Graphics2D g2) { 1273 for (DrawablePlot drawable: plots) { 1274 g2.setColor(drawable.getColor()); 1275 drawPlot(g2, drawable.getEquation()); 1276 } 1277 } 1278 1279 // Draw a single plot as a GeneralPath made of straight lines. 1280 private void drawPlot(Graphics2D g2, Plot equation) { 1281 float x = 0.0f; 1282 float y = (float) yPositionToPixel(equation.compute(xPixelToPosition(0.0))); 1283 1284 GeneralPath path = new GeneralPath(); 1285 path.moveTo(x, y); 1286 1287 float width = (float) getWidth(); 1288 for (x = 0.0f; x < width; x += 1.0f) { 1289 double position = xPixelToPosition(x); 1290 y = (float) yPositionToPixel(equation.compute(position)); 1291 path.lineTo(x, y); 1292 } 1293 1294 g2.draw(path); 1295 } 1296 1297 // Draws the grid. First draw the vertical lines, then the horizontal lines. 1298 private void drawGrid(Graphics2D g2) { 1299 Stroke stroke = g2.getStroke(); 1300 1301 if (isGridPainted()) { 1302 drawVerticalGrid(g2); 1303 drawHorizontalGrid(g2); 1304 } 1305 1306 g2.setStroke(stroke); 1307 } 1308 1309 // Draw all labels. First draws labels on the horizontal axis, then labels 1310 // on the vertical axis. If the axis is set not to be painted, this 1311 // method draws the origin as a straight cross. 1312 private void drawLabels(Graphics2D g2) { 1313 if (isTextPainted()) { 1314 double axisH = yPositionToPixel(originY); 1315 double axisV = xPositionToPixel(originX); 1316 1317 if (!isAxisPainted()) { 1318 Stroke stroke = g2.getStroke(); 1319 g2.setStroke(new BasicStroke(STROKE_AXIS)); 1320 g2.setColor(getAxisColor()); 1321 g2.drawLine((int) axisV - 3, (int) axisH, 1322 (int) axisV + 3, (int) axisH); 1323 g2.drawLine((int) axisV, (int) axisH - 3, 1324 (int) axisV, (int) axisH + 3); 1325 g2.setStroke(stroke); 1326 } 1327 1328 g2.setColor(getForeground()); 1329 FontMetrics metrics = g2.getFontMetrics(); 1330 g2.drawString(format(originX) + "; " + 1331 format(originY), (int) axisV + 5, 1332 (int) axisH + metrics.getHeight()); 1333 1334 drawHorizontalAxisLabels(g2); 1335 drawVerticalAxisLabels(g2); 1336 } 1337 } 1338 1339 // Draws labels on the vertical axis. First draws labels below the origin, 1340 // then draw labels on top of the origin. 1341 private void drawVerticalAxisLabels(Graphics2D g2) { 1342 double axisV = xPositionToPixel(originX); 1343 1344 double startY = Math.floor((minY - originY) / majorY) * majorY; 1345 for (double y = startY; y < maxY + majorY; y += majorY) { 1346 if (((y - majorY / 2.0) < originY) && 1347 ((y + majorY / 2.0) > originY)) { 1348 continue; 1349 } 1350 1351 int position = (int) yPositionToPixel(y); 1352 g2.drawString(format(y), (int) axisV + 5, position); 1353 } 1354 } 1355 1356 // Draws the horizontal lines of the grid. Draws both minor and major 1357 // grid lines. 1358 private void drawHorizontalGrid(Graphics2D g2) { 1359 double minorSpacing = majorY / getMinorCountY(); 1360 double axisV = xPositionToPixel(originX); 1361 1362 Stroke gridStroke = new BasicStroke(STROKE_GRID); 1363 Stroke axisStroke = new BasicStroke(STROKE_AXIS); 1364 1365 Rectangle clip = g2.getClipBounds(); 1366 1367 int position = 0; 1368 1369 if (!isAxisPainted()) { 1370 position = (int) xPositionToPixel(originX); 1371 if (position >= clip.x && position <= clip.x + clip.width) { 1372 g2.setColor(getMajorGridColor()); 1373 g2.drawLine(position, clip.y, position, clip.y + clip.height); 1374 } 1375 } 1376 1377 double startY = Math.floor((minY - originY) / majorY) * majorY; 1378 for (double y = startY; y < maxY + majorY; y += majorY) { 1379 g2.setStroke(gridStroke); 1380 g2.setColor(getMinorGridColor()); 1381 for (int i = 0; i < getMinorCountY(); i++) { 1382 position = (int) yPositionToPixel(y - i * minorSpacing); 1383 if (position >= clip.y && position <= clip.y + clip.height) { 1384 g2.drawLine(clip.x, position, clip.x + clip.width, position); 1385 } 1386 } 1387 1388 position = (int) yPositionToPixel(y); 1389 if (position >= clip.y && position <= clip.y + clip.height) { 1390 g2.setColor(getMajorGridColor()); 1391 g2.drawLine(clip.x, position, clip.x + clip.width, position); 1392 1393 g2.setStroke(axisStroke); 1394 g2.setColor(getAxisColor()); 1395 g2.drawLine((int) axisV - 3, position, (int) axisV + 3, position); 1396 } 1397 } 1398 } 1399 1400 // Draws labels on the horizontal axis. First draws labels on the right of 1401 // the origin, then on the left. 1402 private void drawHorizontalAxisLabels(Graphics2D g2) { 1403 double axisH = yPositionToPixel(originY); 1404 FontMetrics metrics = g2.getFontMetrics(); 1405 1406 double startX = Math.floor((minX - originX) / majorX) * majorX; 1407 for (double x = startX; x < maxX + majorX; x += majorX) { 1408 if (((x - majorX / 2.0) < originX) && 1409 ((x + majorX / 2.0) > originX)) { 1410 continue; 1411 } 1412 1413 int position = (int) xPositionToPixel(x); 1414 g2.drawString(format(x), position, 1415 (int) axisH + metrics.getHeight()); 1416 } 1417 } 1418 1419 // Draws the vertical lines of the grid. Draws both minor and major 1420 // grid lines. 1421 private void drawVerticalGrid(Graphics2D g2) { 1422 double minorSpacing = majorX / getMinorCountX(); 1423 double axisH = yPositionToPixel(originY); 1424 1425 Stroke gridStroke = new BasicStroke(STROKE_GRID); 1426 Stroke axisStroke = new BasicStroke(STROKE_AXIS); 1427 1428 Rectangle clip = g2.getClipBounds(); 1429 1430 int position = 0; 1431 if (!isAxisPainted()) { 1432 position = (int) yPositionToPixel(originY); 1433 if (position >= clip.y && position <= clip.y + clip.height) { 1434 g2.setColor(getMajorGridColor()); 1435 g2.drawLine(clip.x, position, clip.x + clip.width, position); 1436 } 1437 } 1438 1439 double startX = Math.floor((minX - originX) / majorX) * majorX; 1440 for (double x = startX; x < maxX + majorX; x += majorX) { 1441 g2.setStroke(gridStroke); 1442 g2.setColor(getMinorGridColor()); 1443 for (int i = 0; i < getMinorCountX(); i++) { 1444 position = (int) xPositionToPixel(x - i * minorSpacing); 1445 if (position >= clip.x && position <= clip.x + clip.width) { 1446 g2.drawLine(position, clip.y, position, clip.y + clip.height); 1447 } 1448 } 1449 1450 position = (int) xPositionToPixel(x); 1451 if (position >= clip.x && position <= clip.x + clip.width) { 1452 g2.setColor(getMajorGridColor()); 1453 g2.drawLine(position, clip.y, position, clip.y + clip.height); 1454 1455 g2.setStroke(axisStroke); 1456 g2.setColor(getAxisColor()); 1457 g2.drawLine(position, (int) axisH - 3, position, (int) axisH + 3); 1458 } 1459 } 1460 } 1461 1462 // Drase the main axis. 1463 private void drawAxis(Graphics2D g2) { 1464 if (!isAxisPainted()) { 1465 return; 1466 } 1467 1468 double axisH = yPositionToPixel(originY); 1469 double axisV = xPositionToPixel(originX); 1470 1471 Rectangle clip = g2.getClipBounds(); 1472 1473 g2.setColor(getAxisColor()); 1474 Stroke stroke = g2.getStroke(); 1475 g2.setStroke(new BasicStroke(STROKE_AXIS)); 1476 1477 if (axisH >= clip.y && axisH <= clip.y + clip.height) { 1478 g2.drawLine(clip.x, (int) axisH, clip.x + clip.width, (int) axisH); 1479 } 1480 if (axisV >= clip.x && axisV <= clip.x + clip.width) { 1481 g2.drawLine((int) axisV, clip.y, (int) axisV, clip.y + clip.height); 1482 } 1483 1484 g2.setStroke(stroke); 1485 } 1486 1487 /** 1488 * <p>This method is called by the component prior to any drawing operation 1489 * to configure the drawing surface. The default implementation enables 1490 * antialiasing on the graphics.</p> 1491 * <p>This method can be overriden by subclasses to modify the drawig 1492 * surface before any painting happens.</p> 1493 * 1494 * @param g2 the graphics surface to set up 1495 * @see #paintExtra(Graphics2D) 1496 * @see #paintBackground(Graphics2D) 1497 */ 1498 protected void setupGraphics(Graphics2D g2) { 1499 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 1500 RenderingHints.VALUE_ANTIALIAS_ON); 1501 } 1502 1503 /** 1504 * <p>This method is called by the component whenever it needs to paint 1505 * its background. The default implementation fills the background with 1506 * a solid color as defined by {@link #getBackground()}. Background painting 1507 * does not happen when {@link #isBackgroundPainted()} returns false.</p> 1508 * <p>It is recommended to subclasses to honor the contract defined by 1509 * {@link #isBackgroundPainted()} and {@link #setBackgroundPainted(boolean)}. 1510 * 1511 * @param g2 the graphics surface on which the background must be drawn 1512 * @see #setupGraphics(Graphics2D) 1513 * @see #paintExtra(Graphics2D) 1514 * @see #isBackgroundPainted() 1515 * @see #setBackgroundPainted(boolean) 1516 */ 1517 protected void paintBackground(Graphics2D g2) { 1518 if (isBackgroundPainted()) { 1519 g2.setColor(getBackground()); 1520 g2.fill(g2.getClipBounds()); 1521 } 1522 } 1523 1524 // Format a number with the appropriate number formatter. Numbers >= 0.01 1525 // and < 100 are formatted with a regular, 2-digits, numbers formatter. 1526 // Other numbers use a scientific notation given by a DecimalFormat instance 1527 private String format(double number) { 1528 boolean farAway = (number != 0.0d && Math.abs(number) < 0.01d) || 1529 Math.abs(number) > 99.0d; 1530 return (farAway ? secondFormatter : mainFormatter).format(number); 1531 } 1532 1533 /** 1534 * <p>A plot represents a mathematical transformation used by 1535 * {@link JXGraph}. When a plot belongs to a graph, the graph component 1536 * asks for the transformation of a value along the X axis. The resulting 1537 * value defines the Y coordinates at which the graph must draw a spot of 1538 * color.</p> 1539 * 1540 * <p>Here is a sample implemention of this class that draws a straight line 1541 * once added to a graph (it follows the well-known equation y=a.x+b):</p> 1542 * 1543 * <pre> 1544 * class LinePlot extends JXGraph.Plot { 1545 * public double compute(double value) { 1546 * return 2.0 * value + 1.0; 1547 * } 1548 * } 1549 * </pre> 1550 * 1551 * <p>More information about plots usage can be found in {@link JXGraph} in 1552 * the section entitled <i>Plots</i>.</p> 1553 * 1554 * @see JXGraph 1555 * @see JXGraph#addPlots(Color, JXGraph.Plot...) 1556 */ 1557 public abstract static class Plot { 1558 /** 1559 * <p>List of property change listeners listening to this plot's 1560 * instance. When this plot is added to an instance of 1561 * <code>JXGraph</code>, the <code>JXGraph</code> automatically becomes 1562 * a new property change listener of this plot.</p> 1563 */ 1564 protected PropertyChangeSupport support; 1565 1566 /** 1567 * <p>Creates a new, parameter-less plot.</p> 1568 */ 1569 protected Plot() { 1570 this.support = new PropertyChangeSupport(this); 1571 } 1572 1573 /** 1574 * <p>This method must return the result of a mathematical 1575 * transformation of its sole parameter.</p> 1576 * 1577 * @param value a value along the X axis of the graph currently 1578 * drawing this plot 1579 * @return the result of the mathematical transformation of value 1580 */ 1581 public abstract double compute(double value); 1582 1583 /** 1584 * <p>Registers a new property change listener with the current plot.</p> 1585 * 1586 * @param listener the new property change listener to be notified of 1587 * property changes 1588 */ 1589 public void addPropertyChangeListener(PropertyChangeListener listener) { 1590 support.addPropertyChangeListener(listener); 1591 } 1592 1593 /** 1594 * <p>Removes a new property change listener from the current plot.</p> 1595 * 1596 * @param listener the property change listener to be removed 1597 */ 1598 public void removePropertyChangeListener(PropertyChangeListener listener) { 1599 support.removePropertyChangeListener(listener); 1600 } 1601 1602 /** 1603 * <p>Notifies property change listeners that a property has changed. 1604 * This method must be used to notify the graph that a parameter of the 1605 * mathematical transformation of this plot has changed. Failing to do 1606 * so, a plot will be refreshed when its internal parameters change.</p> 1607 * 1608 * @param propertyName the name of the property known to have changed 1609 * @param oldValue the value of the property prior to the change 1610 * @param newValue the value of the property after the change 1611 */ 1612 protected void firePropertyChange(String propertyName, 1613 double oldValue, double newValue) { 1614 PropertyChangeEvent changeEvent = 1615 new PropertyChangeEvent(this, propertyName, oldValue, newValue); 1616 for (PropertyChangeListener listener : support.getPropertyChangeListeners()) { 1617 listener.propertyChange(changeEvent); 1618 } 1619 } 1620 } 1621 1622 // Encapsulates a plot and its color. Avoids the use of a full-blown Map. 1623 private class DrawablePlot { 1624 private final Plot equation; 1625 private final Color color; 1626 1627 private DrawablePlot(Plot equation, Color color) { 1628 this.equation = equation; 1629 this.color = color; 1630 } 1631 1632 private Plot getEquation() { 1633 return equation; 1634 } 1635 1636 private Color getColor() { 1637 return color; 1638 } 1639 } 1640 1641 // Shrinks or expand the view depending on the mouse wheel direction. 1642 // When the wheel moves down, the view is expanded. Otherwise it is shrunk. 1643 private class ZoomHandler implements MouseWheelListener { 1644 public void mouseWheelMoved(MouseWheelEvent e) { 1645 double distanceX = maxX - minX; 1646 double distanceY = maxY - minY; 1647 1648 double cursorX = minX + distanceX / 2.0; 1649 double cursorY = minY + distanceY / 2.0; 1650 1651 int rotation = e.getWheelRotation(); 1652 if (rotation < 0) { 1653 distanceX /= ZOOM_MULTIPLIER; 1654 distanceY /= ZOOM_MULTIPLIER; 1655 1656 majorX /= ZOOM_MULTIPLIER; 1657 majorY /= ZOOM_MULTIPLIER; 1658 } else { 1659 distanceX *= ZOOM_MULTIPLIER; 1660 distanceY *= ZOOM_MULTIPLIER; 1661 1662 majorX *= ZOOM_MULTIPLIER; 1663 majorY *= ZOOM_MULTIPLIER; 1664 } 1665 1666 minX = cursorX - distanceX / 2.0; 1667 maxX = cursorX + distanceX / 2.0; 1668 minY = cursorY - distanceY / 2.0; 1669 maxY = cursorY + distanceY / 2.0; 1670 1671 repaint(); 1672 } 1673 } 1674 1675 // Listens for a click on the middle button of the mouse and resets the view 1676 private class ResetHandler extends MouseAdapter { 1677 @Override 1678 public void mousePressed(MouseEvent e) { 1679 if (e.getButton() != MouseEvent.BUTTON2) { 1680 return; 1681 } 1682 1683 resetView(); 1684 } 1685 } 1686 1687 // Starts and ends drag gestures with mouse left button. 1688 private class PanHandler extends MouseAdapter { 1689 @Override 1690 public void mousePressed(MouseEvent e) { 1691 if (e.getButton() != MouseEvent.BUTTON1) { 1692 return; 1693 } 1694 1695 dragStart = e.getPoint(); 1696 setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); 1697 } 1698 1699 @Override 1700 public void mouseReleased(MouseEvent e) { 1701 if (e.getButton() != MouseEvent.BUTTON1) { 1702 return; 1703 } 1704 1705 setCursor(Cursor.getDefaultCursor()); 1706 } 1707 } 1708 1709 // Handles drag gesture with the left mouse button and relocates the view 1710 // accordingly. 1711 private class PanMotionHandler extends MouseMotionAdapter { 1712 @Override 1713 public void mouseDragged(MouseEvent e) { 1714 Point dragEnd = e.getPoint(); 1715 1716 double distance = xPixelToPosition(dragEnd.getX()) - 1717 xPixelToPosition(dragStart.getX()); 1718 minX = minX - distance; 1719 maxX = maxX - distance; 1720 1721 distance = yPixelToPosition(dragEnd.getY()) - 1722 yPixelToPosition(dragStart.getY()); 1723 minY = minY - distance; 1724 maxY = maxY - distance; 1725 1726 repaint(); 1727 dragStart = dragEnd; 1728 } 1729 } 1730 }