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