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