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