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