All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jdesktop.swingx.JXGraph Maven / Gradle / Ivy

/*
 * $Id$
 *
 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jdesktop.swingx;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.LinkedList;
import java.util.List;

import org.jdesktop.beans.AbstractBean;
import org.jdesktop.beans.JavaBean;
import org.jdesktop.swingx.painter.Painter;

// TODO: keyboard navigation
// TODO: honor clip rect with text painting
// TODO: let client change zoom multiplier
// TODO: improve text drawing when origin is not on a multiple of majorX/majorY
// TODO: programmatically zoom in and out (or expose ZOOM_MULTIPLIER)

/**
 * 

JXGraph provides a component which can display one or more * plots on top of a graduated background (or grid.)

* *

User input

* *

To help analyze the plots, this component allows the user to pan the * view by left-clicking and dragging the mouse around. Using the mouse wheel, * the user is also able to zoom in and out. Clicking the middle button resets * the view to its original position.

* *

All user input can be disabled by calling * {@link #setInputEnabled(boolean)} and passing false. This does not prevent * subclasses from registering their own event listeners, such as mouse or key * listeners.

* *

Initializing the component and setting the view

* *

Whenever a new instance of this component is created, the grid boundaries, * or view, must be defined. The view is comprised of several elements whose * descriptions are the following:

* *
    *
  • minX: Minimum value initially displayed by the component on the * X axis (horizontally.)
  • *
  • minY: Minimum value initially displayed by the component on the * Y axis (vertically.)
  • *
  • maxX: Maximum value initially displayed by the component on the * X axis (horizontally.)
  • *
  • maxY: Maximum value initially displayed by the component on the * Y axis (vertically.)
  • *
  • originX: Origin on the X axis of the vertical axis.
  • *
  • originY: Origin on the Y axis of the horizontal axis.
  • *
  • majorX: Distance between two major vertical lines of the * grid.
  • *
  • majorY: Distance between two major horizontal lines of the * grid.
  • *
  • minCountX: Number of minor vertical lines between two major * vertical lines in the grid.
  • *
  • minCountY: Number of minor horizontal lines between two major * horizontal lines in the grid.
  • *
* *

View and origin

* *

The default constructor defines a view bounds by -1.0 and * +1.0 on both axis, and centered on an origin at * (0, 0).

* *

To simplify the API, the origin can be read and written with a * Point2D instance (see {@link #getOrigin()} and * {@link #setOrigin(Point2D)}.)

* *

Likewise, the view can be read and written with a * Rectangle2D instance (see {@link #getView()} and * {@link #setView(Rectangle2D)}.) In this case, you need not to define the * maximum boundaries of the view. Instead, you need to set the origin of the * rectangle as the minimum boundaries. The width and the height of the * rectangle define the distance between the minimum and maximum boundaries. For * instance, to set the view to minX=-1.0, maxX=1.0, minY=-1.0 and maxY=1.0 you * can use the following rectangle:

* *
new Rectangle2D.Double(-1.0d, -1.0d, 2.0d, 2.0d);
* *

You can check the boundaries by calling Rectangle2D.getMaxX() * and Rectangle2D.getMaxY() once your rectangle has been * created.

* *

Alternatively, you can set the view and the origin at the same time by * calling the method {@link #setViewAndOrigin(Rectangle2D)}. Calling this * method will set the origin so as to center it in the view defined by the * rectangle.

* *

Grid lines

* *

By default, the component defines a spacing of 0.2 units between two * major grid lines. It also defines 4 minor grid lines between two major * grid lines. The spacing between major grid lines and the number of minor * grid lines can be accessed through the getters {@link #getMajorX()}, * {@link #getMajorY()}, {@link #getMinorCountX()} and * {@link #getMinorCountY()}.

* *

You can change the number of grid lines at runtime by calling the setters * {@link #setMajorX(double)}, {@link #setMajorY(double)}, * {@link #setMinorCountX(int)} and {@link #setMinorCountY(int)}.

* *

Appearance

* *

Although it provides sensible defaults, this component lets you change * its appearance in several ways. It is possible to modify the colors of the * graph by calling the setters {@link #setAxisColor(Color)}, * {@link #setMajorGridColor(Color)} and {@link #setMinorGridColor(Color)}.

* *

You can also enable or disable given parts of the resulting graph by * calling the following setters:

* *
    *
  • {@link #setAxisPainted(boolean)}: Defines whether the main axis (see * {@link #getOrigin()}) is painted.
  • *
  • {@link #setBackgroundPainted(boolean)}: Defines whether the background * is painted (see {@link #setBackground(Color)}.)
  • *
  • {@link #setGridPainted(boolean)}: Defines whether the grid is * painted.
  • *
  • {@link #setTextPainted(boolean)}: Defines whether the axis labels are * painted.
  • *
* *

Usage example

* *

The following code snippet creates a new graph centered on * (0, 0), bound to the view [-1.0 1.0 -1.0 1.0], with * a major grid line every 0.5 units and a minor grid line count of 5:

* *
 * Point2D origin = new Point2D.Double(0.0d, 0.0d);
 * Rectangle2D view = new Rectangle2D.Double(-1.0d, 1.0d, 2.0d, 2.0d);
 * JXGraph graph = new JXGraph(origin, view, 0.5d, 5, 0.5d, 5);
 * 
* *

Plots

* *

Definition

* *

A plot is defined by a mathematical transformation that, given a value on * the graph's X axis, returns a value on the Y axis. The component draws the * result by plotting a spot of color at the coordinates defined by * (X, f(X)) where f() is the aforementionned * mathematical transformation. Given the following transformation:

* *
 * f(X) = X * 2.0
 * 
* *

For X=1.0, the component will show a spot of color at the * coordinates (1.0, 2.0).

* *

Creating a new plot

* *

Every plot drawn by the component must be a subclass of * {@link JXGraph.Plot}. This abstract public class defines a single method to * be implemented by its children:

* *
 * public double compute(double value)
 * 
* *

The previous example can be defined by a concrete * JXGraph.Plot as follow:

* *
 * class TwiceTheValuePlot extends JXGraph.Plot {
 *     public double compute(double value) {
 *         return value * 2.0d;
 *     }
 * }
 * 
* *

Most of the time though, a plot requires supplementary parameters. For * instance, let's define the X axis of your graph as the mass of an object. To * compute the weight of the object given its mass, you need to use the * acceleration of gravity (w=m*g where g is the * acceleration.) To let the user modify this last parameter, to compute his * weight at the surface of the moon for instance, you need to add a parameter * to your plot.

* *

While JXGraph.Plot does not give you an API for such a * purpose, it does define an event dispatching API (see * {@link JXGraph#firePropertyChange(String, double, double)}.) Whenever a * plot is added to the graph, the component registers itself as a property * listener of the plot. If you take care of firing events whenever the user * changes a parameter of your plot, the graph will automatically update its * display. While not mandatory, it is highly recommended to leverage this * API.

* *

Adding and removing plots to and from the graph

* *

To add a plot to the graph, simply call the method * {@link #addPlots(Color, JXGraph.Plot...)}. You can use it to add one or more * plots at the same time and associate them with a color. This color is used * when drawing the plots:

* *
 * JXGraph.Plot plot = new TwiceTheValuePlot();
 * graph.addPlots(Color.BLUE, plot);
 * 
* *

These two lines will display our previously defined plot in blue on * screen. Removing one or several plots is as simple as calling the method * {@link #removePlots(JXGraph.Plot...)}. You can also remove all plots at once * with {@link #removeAllPlots()}.

* *

Painting more information

* *

How to draw on the graph

* *

If you need to add more information on the graph you need to extend * it and override the method {@link #paintExtra(Graphics2D)}. This * method has a default empty implementation and is called after everything * has been drawn. Its sole parameter is a reference to the component's drawing * surface, as configured by {@link #setupGraphics(Graphics2D)}. By default, the * setup method activates antialising but it can be overriden to change the * drawing surface. (Translation, rotation, new rendering hints, etc.)

* *

Getting the right coordinates

* *

To properly draw on the graph you will need to perform a translation * between the graph's coordinates and the screen's coordinates. The component * defines 4 methods to assist you in this task:

* *
    *
  • {@link #xPixelToPosition(double)}: Converts a pixel coordinate on the * X axis into a world coordinate.
  • *
  • {@link #xPositionToPixel(double)}: Converts a world coordinate on the * X axis into a pixel coordinate.
  • *
  • {@link #yPixelToPosition(double)}: Converts a pixel coordinate on the * Y axis into a world coordinate.
  • *
  • {@link #yPositionToPixel(double)}: Converts a world coordinate on the * Y axis into a pixel coordinate.
  • *
* *

If you have defined a graph view centered on the origin * (0, 0), the origin of the graph will be at the exact center of * the screen. That means the world coordinates (0, 0) are * equivalent to the pixel coordinates (width / 2, height / 2). * Thus, calling xPositionToPixel(0.0d) would give you the same * value as the expression getWidth() / 2.0d.

* *

Converting from world coordinates to pixel coordinates is mostly used to * draw the result of a mathematical transformation. Converting from pixel * coordinates to world coordinates is mostly used to get the position in the * world of a mouse event.

* * @see JXGraph.Plot * @author Romain Guy */ @JavaBean public class JXGraph extends JXPanel { // stroke widths used to draw the main axis and the grid // the main axis is slightly thicker private static final float STROKE_AXIS = 1.2f; private static final float STROKE_GRID = 1.0f; // defines by how much the view is shrinked or expanded everytime the // user zooms in or out private static final float ZOOM_MULTIPLIER = 1.1f; //listens to changes to plots and repaints the graph private PropertyChangeListener plotChangeListener; // default color of the graph (does not include plots colors) private Color majorGridColor = Color.GRAY.brighter(); private Color minorGridColor = new Color(220, 220, 220); private Color axisColor = Color.BLACK; // the list of plots currently known and displayed by the graph private List plots; // view boundaries as defined by the user private double minX; private double maxX; private double minY; private double maxY; // the default view is set when the view is manually changed by the client // it is used to reset the view in resetView() private Rectangle2D defaultView; // coordinates of the major axis private double originX; private double originY; // definition of the grid // various default values are used when the view is reset private double majorX; private double defaultMajorX; private int minorCountX; private double majorY; private double defaultMajorY; private int minorCountY; // enables painting layers private boolean textPainted = true; private boolean gridPainted = true; private boolean axisPainted = true; private boolean backPainted = true; // used by the PanHandler to move the view private Point dragStart; // mainFormatter is used for numbers > 0.01 and < 100 // secondFormatter uses scientific notation private NumberFormat mainFormatter; private NumberFormat secondFormatter; // input handlers private boolean inputEnabled = true; private ZoomHandler zoomHandler; private PanMotionHandler panMotionHandler; private PanHandler panHandler; private ResetHandler resetHandler; /** *

Creates a new graph display. The following properties are * automatically set:

*
    *
  • view: -1.0 to +1.0 on both axis
  • *
  • origin: At (0, 0)
  • *
  • grid: Spacing of 0.2 between major lines; minor lines * count is 4
  • *
*/ public JXGraph() { this(0.0, 0.0, -1.0, 1.0, -1.0, 1.0, 0.2, 4, 0.2, 4); } /** *

Creates a new graph display with the specified view. The following * properties are automatically set:

*
    *
  • origin: Center of the specified view
  • *
  • grid: Spacing of 0.2 between major lines; minor lines * count is 4
  • *
* * @param view the rectangle defining the view boundaries */ public JXGraph(Rectangle2D view) { this(new Point2D.Double(view.getCenterX(), view.getCenterY()), view, 0.2, 4, 0.2, 4); } /** *

Creates a new graph display with the specified view and grid lines. * The origin is set at the center of the view.

* * @param view the rectangle defining the view boundaries * @param majorX the spacing between two major grid lines on the X axis * @param minorCountX the number of minor grid lines between two major * grid lines on the X axis * @param majorY the spacing between two major grid lines on the Y axis * @param minorCountY the number of minor grid lines between two major * grid lines on the Y axis * @throws IllegalArgumentException if minX >= maxX or minY >= maxY or * minorCountX < 0 or minorCountY < 0 or * majorX <= 0.0 or majorY <= 0.0 */ public JXGraph(Rectangle2D view, double majorX, int minorCountX, double majorY, int minorCountY) { this(new Point2D.Double(view.getCenterX(), view.getCenterY()), view, majorX, minorCountX, majorY, minorCountY); } /** *

Creates a new graph display with the specified view and origin. * The following properties are automatically set:

*
    *
  • grid: Spacing of 0.2 between major lines; minor lines * count is 4
  • *
* * @param origin the coordinates of the main axis origin * @param view the rectangle defining the view boundaries */ public JXGraph(Point2D origin, Rectangle2D view) { this(origin, view, 0.2, 4, 0.2, 4); } /** *

Creates a new graph display with the specified view, origin and grid * lines.

* * @param origin the coordinates of the main axis origin * @param view the rectangle defining the view boundaries * @param majorX the spacing between two major grid lines on the X axis * @param minorCountX the number of minor grid lines between two major * grid lines on the X axis * @param majorY the spacing between two major grid lines on the Y axis * @param minorCountY the number of minor grid lines between two major * grid lines on the Y axis * @throws IllegalArgumentException if minX >= maxX or minY >= maxY or * minorCountX < 0 or minorCountY < 0 or * majorX <= 0.0 or majorY <= 0.0 */ public JXGraph(Point2D origin, Rectangle2D view, double majorX, int minorCountX, double majorY, int minorCountY) { this(origin.getX(), origin.getY(), view.getMinX(), view.getMaxX(), view.getMinY(), view.getMaxY(), majorX, minorCountX, majorY, minorCountY); } /** *

Creates a new graph display with the specified view, origin and grid * lines.

* * @param originX the coordinate of the major X axis * @param originY the coordinate of the major Y axis * @param minX the minimum coordinate on the X axis for the view * @param maxX the maximum coordinate on the X axis for the view * @param minY the minimum coordinate on the Y axis for the view * @param maxY the maximum coordinate on the Y axis for the view * @param majorX the spacing between two major grid lines on the X axis * @param minorCountX the number of minor grid lines between two major * grid lines on the X axis * @param majorY the spacing between two major grid lines on the Y axis * @param minorCountY the number of minor grid lines between two major * grid lines on the Y axis * @throws IllegalArgumentException if minX >= maxX or minY >= maxY or * minorCountX < 0 or minorCountY < 0 or * majorX <= 0.0 or majorY <= 0.0 */ public JXGraph(double originX, double originY, double minX, double maxX, double minY, double maxY, double majorX, int minorCountX, double majorY, int minorCountY) { if (minX >= maxX) { throw new IllegalArgumentException("minX must be < to maxX"); } if (minY >= maxY) { throw new IllegalArgumentException("minY must be < to maxY"); } if (minorCountX < 0) { throw new IllegalArgumentException("minorCountX must be >= 0"); } if (minorCountY < 0) { throw new IllegalArgumentException("minorCountY must be >= 0"); } if (majorX <= 0.0) { throw new IllegalArgumentException("majorX must be > 0.0"); } if (majorY <= 0.0) { throw new IllegalArgumentException("majorY must be > 0.0"); } this.originX = originX; this.originY = originY; this.minX = minX; this.maxX = maxX; this.minY = minY; this.maxY = maxY; this.defaultView = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); this.setMajorX(this.defaultMajorX = majorX); this.setMinorCountX(minorCountX); this.setMajorY(this.defaultMajorY = majorY); this.setMinorCountY(minorCountY); this.plots = new LinkedList(); this.mainFormatter = NumberFormat.getInstance(); this.mainFormatter.setMaximumFractionDigits(2); this.secondFormatter = new DecimalFormat("0.##E0"); resetHandler = new ResetHandler(); addMouseListener(resetHandler); panHandler = new PanHandler(); addMouseListener(panHandler); panMotionHandler = new PanMotionHandler(); addMouseMotionListener(panMotionHandler); zoomHandler = new ZoomHandler(); addMouseWheelListener(zoomHandler); setBackground(Color.WHITE); setForeground(Color.BLACK); plotChangeListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { repaint(); } }; } /** * {@inheritDoc} */ @Override public boolean isOpaque() { if (!isBackgroundPainted()) { return false; } return super.isOpaque(); } /** * {@inheritDoc} * @see #setInputEnabled(boolean) */ @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); setInputEnabled(enabled); } /** *

Enables or disables user input on the component. When user input is * enabled, panning, zooming and view resetting. Disabling input will * prevent the user from modifying the currently displayed view.

*

Calling {@link #setEnabled(boolean)} disables the component in the * Swing hierarchy and invokes this method.

* * @param enabled true if user input must be enabled, false otherwise * @see #setEnabled(boolean) * @see #isInputEnabled() */ public void setInputEnabled(boolean enabled) { if (inputEnabled != enabled) { boolean old = isInputEnabled(); this.inputEnabled = enabled; if (enabled) { addMouseListener(resetHandler); addMouseListener(panHandler); addMouseMotionListener(panMotionHandler); addMouseWheelListener(zoomHandler); } else { removeMouseListener(resetHandler); removeMouseListener(panHandler); removeMouseMotionListener(panMotionHandler); removeMouseWheelListener(zoomHandler); } firePropertyChange("inputEnabled", old, isInputEnabled()); } } /** *

Defines whether or not user input is accepted and managed by this * component. The component is always created with user input enabled.

* * @return true if user input is enabled, false otherwise * @see #setInputEnabled(boolean) */ public boolean isInputEnabled() { return inputEnabled; } /** *

Defines whether or not axis labels are painted by this component. * The component is always created with text painting enabled.

* * @return true if axis labels are painted, false otherwise * @see #setTextPainted(boolean) * @see #getForeground() */ public boolean isTextPainted() { return textPainted; } /** *

Enables or disables the painting of axis labels depending on the * value of the parameter. Text painting is enabled by default.

* * @param textPainted if true, axis labels are painted * @see #isTextPainted() * @see #setForeground(Color) */ public void setTextPainted(boolean textPainted) { boolean old = isTextPainted(); this.textPainted = textPainted; firePropertyChange("textPainted", old, this.textPainted); } /** *

Defines whether or not grids lines are painted by this component. * The component is always created with grid lines painting enabled.

* * @return true if grid lines are painted, false otherwise * @see #setGridPainted(boolean) * @see #getMajorGridColor() * @see #getMinorGridColor() */ public boolean isGridPainted() { return gridPainted; } /** *

Enables or disables the painting of grid lines depending on the * value of the parameter. Grid painting is enabled by default.

* * @param gridPainted if true, axis labels are painted * @see #isGridPainted() * @see #setMajorGridColor(Color) * @see #setMinorGridColor(Color) */ public void setGridPainted(boolean gridPainted) { boolean old = isGridPainted(); this.gridPainted = gridPainted; firePropertyChange("gridPainted", old, isGridPainted()); } /** *

Defines whether or not the graph main axis is painted by this * component. The component is always created with main axis painting * enabled.

* * @return true if main axis is painted, false otherwise * @see #setTextPainted(boolean) * @see #getAxisColor() */ public boolean isAxisPainted() { return axisPainted; } /** *

Enables or disables the painting of main axis depending on the * value of the parameter. Axis painting is enabled by default.

* * @param axisPainted if true, axis labels are painted * @see #isAxisPainted() * @see #setAxisColor(Color) */ public void setAxisPainted(boolean axisPainted) { boolean old = isAxisPainted(); this.axisPainted = axisPainted; firePropertyChange("axisPainted", old, isAxisPainted()); } /** *

Defines whether or not the background painted by this component. * The component is always created with background painting enabled. * When background painting is disabled, background painting is deferred * to the parent class.

* * @return true if background is painted, false otherwise * @see #setBackgroundPainted(boolean) * @see #getBackground() */ public boolean isBackgroundPainted() { return backPainted; } /** *

Enables or disables the painting of background depending on the * value of the parameter. Background painting is enabled by default.

* * @param backPainted if true, axis labels are painted * @see #isBackgroundPainted() * @see #setBackground(Color) */ public void setBackgroundPainted(boolean backPainted) { boolean old = isBackgroundPainted(); this.backPainted = backPainted; firePropertyChange("backgroundPainted", old, isBackgroundPainted()); } /** *

Gets the major grid lines color of this component.

* * @return this component's major grid lines color * @see #setMajorGridColor(Color) * @see #setGridPainted(boolean) */ public Color getMajorGridColor() { return majorGridColor; } /** *

Sets the color of major grid lines on this component. The color * can be translucent.

* * @param majorGridColor the color to become this component's major grid * lines color * @throws IllegalArgumentException if the specified color is null * @see #getMajorGridColor() * @see #isGridPainted() */ public void setMajorGridColor(Color majorGridColor) { if (majorGridColor == null) { throw new IllegalArgumentException("Color cannot be null."); } Color old = getMajorGridColor(); this.majorGridColor = majorGridColor; firePropertyChange("majorGridColor", old, getMajorGridColor()); } /** *

Gets the minor grid lines color of this component.

* * @return this component's minor grid lines color * @see #setMinorGridColor(Color) * @see #setGridPainted(boolean) */ public Color getMinorGridColor() { return minorGridColor; } /** *

Sets the color of minor grid lines on this component. The color * can be translucent.

* * @param minorGridColor the color to become this component's minor grid * lines color * @throws IllegalArgumentException if the specified color is null * @see #getMinorGridColor() * @see #isGridPainted() */ public void setMinorGridColor(Color minorGridColor) { if (minorGridColor == null) { throw new IllegalArgumentException("Color cannot be null."); } Color old = getMinorGridColor(); this.minorGridColor = minorGridColor; firePropertyChange("minorGridColor", old, getMinorGridColor()); } /** *

Gets the main axis color of this component.

* * @return this component's main axis color * @see #setAxisColor(Color) * @see #setGridPainted(boolean) */ public Color getAxisColor() { return axisColor; } /** *

Sets the color of main axis on this component. The color * can be translucent.

* * @param axisColor the color to become this component's main axis color * @throws IllegalArgumentException if the specified color is null * @see #getAxisColor() * @see #isAxisPainted() */ public void setAxisColor(Color axisColor) { if (axisColor == null) { throw new IllegalArgumentException("Color cannot be null."); } Color old = getAxisColor(); this.axisColor = axisColor; firePropertyChange("axisColor", old, getAxisColor()); } /** *

Gets the distance, in graph units, between two major grid lines on * the X axis.

* * @return the spacing between two major grid lines on the X axis * @see #setMajorX(double) * @see #getMajorY() * @see #setMajorY(double) * @see #getMinorCountX() * @see #setMinorCountX(int) */ public double getMajorX() { return majorX; } /** *

Sets the distance, in graph units, between two major grid lines on * the X axis.

* * @param majorX the requested spacing between two major grid lines on the * X axis * @throws IllegalArgumentException if majorX is <= 0.0d * @see #getMajorX() * @see #getMajorY() * @see #setMajorY(double) * @see #getMinorCountX() * @see #setMinorCountX(int) */ public void setMajorX(double majorX) { if (majorX <= 0.0) { throw new IllegalArgumentException("majorX must be > 0.0"); } double old = getMajorX(); this.majorX = majorX; this.defaultMajorX = majorX; repaint(); firePropertyChange("majorX", old, getMajorX()); } /** *

Gets the number of minor grid lines between two major grid lines * on the X axis.

* * @return the number of minor grid lines between two major grid lines * @see #setMinorCountX(int) * @see #getMinorCountY() * @see #setMinorCountY(int) * @see #getMajorX() * @see #setMajorX(double) */ public int getMinorCountX() { return minorCountX; } /** *

Sets the number of minor grid lines between two major grid lines on * the X axis.

* * @param minorCountX the number of minor grid lines between two major grid * lines on the X axis * @throws IllegalArgumentException if minorCountX is < 0 * @see #getMinorCountX() * @see #getMinorCountY() * @see #setMinorCountY(int) * @see #getMajorX() * @see #setMajorX(double) */ public void setMinorCountX(int minorCountX) { if (minorCountX < 0) { throw new IllegalArgumentException("minorCountX must be >= 0"); } int old = getMinorCountX(); this.minorCountX = minorCountX; repaint(); firePropertyChange("minorCountX", old, getMinorCountX()); } /** *

Gets the distance, in graph units, between two major grid lines on * the Y axis.

* * @return the spacing between two major grid lines on the Y axis * @see #setMajorY(double) * @see #getMajorX() * @see #setMajorX(double) * @see #getMinorCountY() * @see #setMinorCountY(int) */ public double getMajorY() { return majorY; } /** *

Sets the distance, in graph units, between two major grid lines on * the Y axis.

* * @param majorY the requested spacing between two major grid lines on the * Y axis * @throws IllegalArgumentException if majorY is <= 0.0d * @see #getMajorY() * @see #getMajorX() * @see #setMajorX(double) * @see #getMinorCountY() * @see #setMinorCountY(int) */ public void setMajorY(double majorY) { if (majorY <= 0.0) { throw new IllegalArgumentException("majorY must be > 0.0"); } double old = getMajorY(); this.majorY = majorY; this.defaultMajorY = majorY; repaint(); firePropertyChange("majorY", old, getMajorY()); } /** *

Gets the number of minor grid lines between two major grid lines * on the Y axis.

* * @return the number of minor grid lines between two major grid lines * @see #setMinorCountY(int) * @see #getMinorCountX() * @see #setMinorCountX(int) * @see #getMajorY() * @see #setMajorY(double) */ public int getMinorCountY() { return minorCountY; } /** *

Sets the number of minor grid lines between two major grid lines on * the Y axis.

* * @param minorCountY the number of minor grid lines between two major grid * lines on the Y axis * @throws IllegalArgumentException if minorCountY is < 0 * @see #getMinorCountY() * @see #getMinorCountX() * @see #setMinorCountX(int) * @see #getMajorY() * @see #setMajorY(double) */ public void setMinorCountY(int minorCountY) { if (minorCountY < 0) { throw new IllegalArgumentException("minorCountY must be >= 0"); } int old = getMinorCountY(); this.minorCountY = minorCountY; repaint(); firePropertyChange("minorCountY", old, getMinorCountY()); } /** *

Sets the view and the origin of the graph at the same time. The view * minimum boundaries are defined by the location of the rectangle passed * as parameter. The width and height of the rectangle define the distance * between the minimum and maximum boundaries:

* *
    *
  • minX: bounds.getX()
  • *
  • minY: bounds.getY()
  • *
  • maxY: bounds.getMaxX() (minX + bounds.getWidth())
  • *
  • maxX: bounds.getMaxY() (minY + bounds.getHeight())
  • *
* *

The origin is located at the center of the view. Its coordinates are * defined by calling bounds.getCenterX() and bounds.getCenterY().

* * @param bounds the rectangle defining the graph's view and its origin * @see #getView() * @see #setView(Rectangle2D) * @see #getOrigin() * @see #setOrigin(Point2D) */ public void setViewAndOrigin(Rectangle2D bounds) { setView(bounds); setOrigin(new Point2D.Double(bounds.getCenterX(), bounds.getCenterY())); } /** *

Sets the view of the graph. The view minimum boundaries are defined by * the location of the rectangle passed as parameter. The width and height * of the rectangle define the distance between the minimum and maximum * boundaries:

* *
    *
  • minX: bounds.getX()
  • *
  • minY: bounds.getY()
  • *
  • maxY: bounds.getMaxX() (minX + bounds.getWidth())
  • *
  • maxX: bounds.getMaxY() (minY + bounds.getHeight())
  • *
* *

If the specified view is null, nothing happens.

* *

Calling this method leaves the origin intact.

* * @param bounds the rectangle defining the graph's view and its origin * @see #getView() * @see #setViewAndOrigin(Rectangle2D) */ public void setView(Rectangle2D bounds) { if (bounds == null) { return; } Rectangle2D old = getView(); defaultView = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight()); minX = defaultView.getMinX(); maxX = defaultView.getMaxX(); minY = defaultView.getMinY(); maxY = defaultView.getMaxY(); majorX = defaultMajorX; majorY = defaultMajorY; firePropertyChange("view", old, getView()); repaint(); } /** *

Gets the view of the graph. The returned rectangle defines the bounds * of the view as follows:

* *
    *
  • minX: bounds.getX()
  • *
  • minY: bounds.getY()
  • *
  • maxY: bounds.getMaxX() (minX + bounds.getWidth())
  • *
  • maxX: bounds.getMaxY() (minY + bounds.getHeight())
  • *
* * @return the rectangle corresponding to the current view of the graph * @see #setView(Rectangle2D) * @see #setViewAndOrigin(Rectangle2D) */ public Rectangle2D getView() { return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); } /** *

Resets the view to the default view if it has been changed by the user * by panning and zooming. The default view is defined by the view last * specified in a constructor call or a call to the methods * {@link #setView(Rectangle2D)} and * {@link #setViewAndOrigin(Rectangle2D)}.

* * @see #setView(Rectangle2D) * @see #setViewAndOrigin(Rectangle2D) */ public void resetView() { setView(defaultView); } /** *

Sets the origin of the graph. The coordinates of the origin are * defined by the coordinates of the point passed as parameter.

* *

If the specified view is null, nothing happens.

* *

Calling this method leaves the view intact.

* * @param origin the coordinates of the new origin * @see #getOrigin() * @see #setViewAndOrigin(Rectangle2D) */ public void setOrigin(Point2D origin) { if (origin == null) { return; } Point2D old = getOrigin(); originX = origin.getX(); originY = origin.getY(); firePropertyChange("origin", old, getOrigin()); repaint(); } /** *

Gets the origin coordinates of the graph. The coordinates are * represented as an instance of Point2D and stored in * double format.

* @return the origin coordinates in double format * @see #setOrigin(Point2D) * @see #setViewAndOrigin(Rectangle2D) */ public Point2D getOrigin() { return new Point2D.Double(originX, originY); } /** *

Adds one or more plots to the graph. These plots are associated to * a color used to draw them.

* *

If plotList is null or empty, nothing happens.

* *

This method is not thread safe and should be called only from the * EDT.

* * @param color the color to be usd to draw the plots * @param plotList the list of plots to add to the graph * @throws IllegalArgumentException if color is null * @see #removePlots(JXGraph.Plot...) * @see #removeAllPlots() */ public void addPlots(Color color, Plot... plotList) { if (color == null) { throw new IllegalArgumentException("Plots color cannot be null."); } if (plotList == null) { return; } for (Plot plot : plotList) { DrawablePlot drawablePlot = new DrawablePlot(plot, color); if (plot != null && !plots.contains(drawablePlot)) { plot.addPropertyChangeListener(plotChangeListener); plots.add(drawablePlot); } } repaint(); } /** *

Removes the specified plots from the graph. Plots to be removed * are identified by identity. This means you cannot remove a plot by * passing a clone or another instance of the same subclass of * {@link JXGraph.Plot}.

* *

If plotList is null or empty, nothing happens.

* *

This method is not thread safe and should be called only from the * EDT.

* * @param plotList the list of plots to be removed from the graph * @see #removeAllPlots() * @see #addPlots(Color, JXGraph.Plot...) */ public void removePlots(Plot... plotList) { if (plotList == null) { return; } for (Plot plot : plotList) { if (plot != null) { DrawablePlot toRemove = null; for (DrawablePlot drawable: plots) { if (drawable.getEquation() == plot) { toRemove = drawable; break; } } if (toRemove != null) { plot.removePropertyChangeListener(plotChangeListener); plots.remove(toRemove); } } } repaint(); } /** *

Removes all the plots currently associated with this graph.

* *

This method is not thread safe and should be called only from the * EDT.

* * @see #removePlots(JXGraph.Plot...) * @see #addPlots(Color, JXGraph.Plot...) */ public void removeAllPlots() { plots.clear(); repaint(); } /** * {@inheritDoc} */ @Override public Dimension getPreferredSize() { return new Dimension(400, 400); } /** *

Converts a position, in graph units, from the Y axis into a pixel * coordinate. For instance, if you defined the origin so it appears at the * exact center of the view, calling * yPositionToPixel(getOriginY()) will return a value * approximately equal to getHeight() / 2.0.

* * @param position the Y position to be converted into pixels * @return the coordinate in pixels of the specified graph Y position * @see #xPositionToPixel(double) * @see #yPixelToPosition(double) */ protected double yPositionToPixel(double position) { double height = getHeight(); return height - ((position - minY) * height / (maxY - minY)); } /** *

Converts a position, in graph units, from the X axis into a pixel * coordinate. For instance, if you defined the origin so it appears at the * exact center of the view, calling * xPositionToPixel(getOriginX()) will return a value * approximately equal to getWidth() / 2.0.

* * @param position the X position to be converted into pixels * @return the coordinate in pixels of the specified graph X position * @see #yPositionToPixel(double) * @see #xPixelToPosition(double) */ protected double xPositionToPixel(double position) { return (position - minX) * getWidth() / (maxX - minX); } /** *

Converts a pixel coordinate from the X axis into a graph position, in * graph units. For instance, if you defined the origin so it appears at the * exact center of the view, calling * xPixelToPosition(getWidth() / 2.0) will return a value * approximately equal to getOriginX().

* * @param pixel the X pixel coordinate to be converted into a graph position * @return the graph X position of the specified pixel coordinate * @see #yPixelToPosition(double) * @see #xPositionToPixel(double) */ protected double xPixelToPosition(double pixel) { // double axisV = xPositionToPixel(originX); // return (pixel - axisV) * (maxX - minX) / (double) getWidth(); return minX + pixel * (maxX - minX) / getWidth(); } /** *

Converts a pixel coordinate from the Y axis into a graph position, in * graph units. For instance, if you defined the origin so it appears at the * exact center of the view, calling * yPixelToPosition(getHeight() / 2.0) will return a value * approximately equal to getOriginY().

* * @param pixel the Y pixel coordinate to be converted into a graph position * @return the graph Y position of the specified pixel coordinate * @see #xPixelToPosition(double) * @see #yPositionToPixel(double) */ protected double yPixelToPosition(double pixel) { // double axisH = yPositionToPixel(originY); // return (getHeight() - pixel - axisH) * (maxY - minY) / (double) getHeight(); return minY + (getHeight() - pixel) * (maxY - minY) / getHeight(); } /** * {@inheritDoc} */ @Override protected void paintComponent(Graphics g) { if (!isVisible()) { return; } Graphics2D g2 = (Graphics2D) g; setupGraphics(g2); paintBackground(g2); drawGrid(g2); drawAxis(g2); drawPlots(g2); drawLabels(g2); paintExtra(g2); } /** *

This painting method is meant to be overridden by subclasses of * JXGraph. This method is called after all the painting * is done. By overriding this method, a subclass can display extra * information on top of the graph.

*

The graphics surface passed as parameter is configured by * {@link #setupGraphics(Graphics2D)}.

* * @param g2 the graphics surface on which the graph is drawn * @see #setupGraphics(Graphics2D) * @see #xPixelToPosition(double) * @see #yPixelToPosition(double) * @see #xPositionToPixel(double) * @see #yPositionToPixel(double) */ protected void paintExtra(Graphics2D g2) { } // Draw all the registered plots with the appropriate color. private void drawPlots(Graphics2D g2) { for (DrawablePlot drawable: plots) { g2.setColor(drawable.getColor()); drawPlot(g2, drawable.getEquation()); } } // Draw a single plot as a GeneralPath made of straight lines. private void drawPlot(Graphics2D g2, Plot equation) { float x = 0.0f; float y = (float) yPositionToPixel(equation.compute(xPixelToPosition(0.0))); GeneralPath path = new GeneralPath(); path.moveTo(x, y); float width = getWidth(); for (x = 0.0f; x < width; x += 1.0f) { double position = xPixelToPosition(x); y = (float) yPositionToPixel(equation.compute(position)); path.lineTo(x, y); } g2.draw(path); } // Draws the grid. First draw the vertical lines, then the horizontal lines. private void drawGrid(Graphics2D g2) { Stroke stroke = g2.getStroke(); if (isGridPainted()) { drawVerticalGrid(g2); drawHorizontalGrid(g2); } g2.setStroke(stroke); } // Draw all labels. First draws labels on the horizontal axis, then labels // on the vertical axis. If the axis is set not to be painted, this // method draws the origin as a straight cross. private void drawLabels(Graphics2D g2) { if (isTextPainted()) { double axisH = yPositionToPixel(originY); double axisV = xPositionToPixel(originX); if (isAxisPainted()) { Stroke stroke = g2.getStroke(); g2.setStroke(new BasicStroke(STROKE_AXIS)); g2.setColor(getAxisColor()); g2.drawLine((int) axisV - 3, (int) axisH, (int) axisV + 3, (int) axisH); g2.drawLine((int) axisV, (int) axisH - 3, (int) axisV, (int) axisH + 3); g2.setStroke(stroke); } g2.setColor(getForeground()); FontMetrics metrics = g2.getFontMetrics(); g2.drawString(format(originX) + "; " + format(originY), (int) axisV + 5, (int) axisH + metrics.getHeight()); drawHorizontalAxisLabels(g2); drawVerticalAxisLabels(g2); } } // Draws labels on the vertical axis. First draws labels below the origin, // then draw labels on top of the origin. private void drawVerticalAxisLabels(Graphics2D g2) { double axisV = xPositionToPixel(originX); // double startY = Math.floor((minY - originY) / majorY) * majorY; double startY = Math.floor(minY / majorY) * majorY; for (double y = startY; y < maxY + majorY; y += majorY) { if (((y - majorY / 2.0) < originY) && ((y + majorY / 2.0) > originY)) { continue; } int position = (int) yPositionToPixel(y); g2.drawString(format(y), (int) axisV + 5, position); } } // Draws the horizontal lines of the grid. Draws both minor and major // grid lines. private void drawHorizontalGrid(Graphics2D g2) { double minorSpacing = majorY / getMinorCountY(); double axisV = xPositionToPixel(originX); Stroke gridStroke = new BasicStroke(STROKE_GRID); Stroke axisStroke = new BasicStroke(STROKE_AXIS); Rectangle clip = g2.getClipBounds(); int position; if (!isAxisPainted()) { position = (int) xPositionToPixel(originX); if (position >= clip.x && position <= clip.x + clip.width) { g2.setColor(getMajorGridColor()); g2.drawLine(position, clip.y, position, clip.y + clip.height); } } // double startY = Math.floor((minY - originY) / majorY) * majorY; double startY = Math.floor(minY / majorY) * majorY; for (double y = startY; y < maxY + majorY; y += majorY) { g2.setStroke(gridStroke); g2.setColor(getMinorGridColor()); for (int i = 0; i < getMinorCountY(); i++) { position = (int) yPositionToPixel(y - i * minorSpacing); if (position >= clip.y && position <= clip.y + clip.height) { g2.drawLine(clip.x, position, clip.x + clip.width, position); } } position = (int) yPositionToPixel(y); if (position >= clip.y && position <= clip.y + clip.height) { g2.setColor(getMajorGridColor()); g2.drawLine(clip.x, position, clip.x + clip.width, position); if (isAxisPainted()) { g2.setStroke(axisStroke); g2.setColor(getAxisColor()); g2.drawLine((int) axisV - 3, position, (int) axisV + 3, position); } } } } // Draws labels on the horizontal axis. First draws labels on the right of // the origin, then on the left. private void drawHorizontalAxisLabels(Graphics2D g2) { double axisH = yPositionToPixel(originY); FontMetrics metrics = g2.getFontMetrics(); // double startX = Math.floor((minX - originX) / majorX) * majorX; double startX = Math.floor(minX / majorX) * majorX; for (double x = startX; x < maxX + majorX; x += majorX) { if (((x - majorX / 2.0) < originX) && ((x + majorX / 2.0) > originX)) { continue; } int position = (int) xPositionToPixel(x); g2.drawString(format(x), position, (int) axisH + metrics.getHeight()); } } // Draws the vertical lines of the grid. Draws both minor and major // grid lines. private void drawVerticalGrid(Graphics2D g2) { double minorSpacing = majorX / getMinorCountX(); double axisH = yPositionToPixel(originY); Stroke gridStroke = new BasicStroke(STROKE_GRID); Stroke axisStroke = new BasicStroke(STROKE_AXIS); Rectangle clip = g2.getClipBounds(); int position; if (!isAxisPainted()) { position = (int) yPositionToPixel(originY); if (position >= clip.y && position <= clip.y + clip.height) { g2.setColor(getMajorGridColor()); g2.drawLine(clip.x, position, clip.x + clip.width, position); } } // double startX = Math.floor((minX - originX) / majorX) * majorX; double startX = Math.floor(minX / majorX) * majorX; for (double x = startX; x < maxX + majorX; x += majorX) { g2.setStroke(gridStroke); g2.setColor(getMinorGridColor()); for (int i = 0; i < getMinorCountX(); i++) { position = (int) xPositionToPixel(x - i * minorSpacing); if (position >= clip.x && position <= clip.x + clip.width) { g2.drawLine(position, clip.y, position, clip.y + clip.height); } } position = (int) xPositionToPixel(x); if (position >= clip.x && position <= clip.x + clip.width) { g2.setColor(getMajorGridColor()); g2.drawLine(position, clip.y, position, clip.y + clip.height); if (isAxisPainted()) { g2.setStroke(axisStroke); g2.setColor(getAxisColor()); g2.drawLine(position, (int) axisH - 3, position, (int) axisH + 3); } } } } // Drase the main axis. private void drawAxis(Graphics2D g2) { if (!isAxisPainted()) { return; } double axisH = yPositionToPixel(originY); double axisV = xPositionToPixel(originX); Rectangle clip = g2.getClipBounds(); g2.setColor(getAxisColor()); Stroke stroke = g2.getStroke(); g2.setStroke(new BasicStroke(STROKE_AXIS)); if (axisH >= clip.y && axisH <= clip.y + clip.height) { g2.drawLine(clip.x, (int) axisH, clip.x + clip.width, (int) axisH); } if (axisV >= clip.x && axisV <= clip.x + clip.width) { g2.drawLine((int) axisV, clip.y, (int) axisV, clip.y + clip.height); } g2.setStroke(stroke); } /** *

This method is called by the component prior to any drawing operation * to configure the drawing surface. The default implementation enables * antialiasing on the graphics.

*

This method can be overriden by subclasses to modify the drawing * surface before any painting happens.

* * @param g2 the graphics surface to set up * @see #paintExtra(Graphics2D) * @see #paintBackground(Graphics2D) */ protected void setupGraphics(Graphics2D g2) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } /** *

This method is called by the component whenever it needs to paint * its background. The default implementation fills the background with * a solid color as defined by {@link #getBackground()}. Background painting * does not happen when {@link #isBackgroundPainted()} returns false.

*

It is recommended to subclasses to honor the contract defined by * {@link #isBackgroundPainted()} and {@link #setBackgroundPainted(boolean)}. * * @param g2 the graphics surface on which the background must be drawn * @see #setupGraphics(Graphics2D) * @see #paintExtra(Graphics2D) * @see #isBackgroundPainted() * @see #setBackgroundPainted(boolean) */ protected void paintBackground(Graphics2D g2) { if (isBackgroundPainted()) { Painter p = getBackgroundPainter(); if (p != null) { p.paint(g2, this, getWidth(), getHeight()); } else { g2.setColor(getBackground()); g2.fill(g2.getClipBounds()); } } } // Format a number with the appropriate number formatter. Numbers >= 0.01 // and < 100 are formatted with a regular, 2-digits, numbers formatter. // Other numbers use a scientific notation given by a DecimalFormat instance private String format(double number) { boolean farAway = (number != 0.0d && Math.abs(number) < 0.01d) || Math.abs(number) > 99.0d; return (farAway ? secondFormatter : mainFormatter).format(number); } /** *

A plot represents a mathematical transformation used by * {@link JXGraph}. When a plot belongs to a graph, the graph component * asks for the transformation of a value along the X axis. The resulting * value defines the Y coordinates at which the graph must draw a spot of * color.

* *

Here is a sample implementation of this class that draws a straight line * once added to a graph (it follows the well-known equation y=a.x+b):

* *
     * class LinePlot extends JXGraph.Plot {
     *     public double compute(double value) {
     *         return 2.0 * value + 1.0;
     *     }
     * }
     * 
* *

When a plot is added to an instance of * JXGraph, the JXGraph automatically becomes * a new property change listener of the plot. If property change events are * fired, the graph will be updated accordingly.

* *

More information about plots usage can be found in {@link JXGraph} in * the section entitled Plots.

* * @see JXGraph * @see JXGraph#addPlots(Color, JXGraph.Plot...) */ public abstract static class Plot extends AbstractBean { /** *

Creates a new, parameter-less plot.

*/ protected Plot() { } /** *

This method must return the result of a mathematical * transformation of its sole parameter.

* * @param value a value along the X axis of the graph currently * drawing this plot * @return the result of the mathematical transformation of value */ public abstract double compute(double value); } // Encapsulates a plot and its color. Avoids the use of a full-blown Map. private static class DrawablePlot { private final Plot equation; private final Color color; private DrawablePlot(Plot equation, Color color) { this.equation = equation; this.color = color; } private Plot getEquation() { return equation; } private Color getColor() { return color; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final DrawablePlot that = (DrawablePlot) o; if (!color.equals(that.color)) { return false; } return equation.equals(that.equation); } @Override public int hashCode() { int result; result = equation.hashCode(); result = 29 * result + color.hashCode(); return result; } } // Shrinks or expand the view depending on the mouse wheel direction. // When the wheel moves down, the view is expanded. Otherwise it is shrunk. private class ZoomHandler implements MouseWheelListener { @Override public void mouseWheelMoved(MouseWheelEvent e) { double distanceX = maxX - minX; double distanceY = maxY - minY; double cursorX = minX + distanceX / 2.0; double cursorY = minY + distanceY / 2.0; int rotation = e.getWheelRotation(); if (rotation < 0) { distanceX /= ZOOM_MULTIPLIER; distanceY /= ZOOM_MULTIPLIER; majorX /= ZOOM_MULTIPLIER; majorY /= ZOOM_MULTIPLIER; } else { distanceX *= ZOOM_MULTIPLIER; distanceY *= ZOOM_MULTIPLIER; majorX *= ZOOM_MULTIPLIER; majorY *= ZOOM_MULTIPLIER; } minX = cursorX - distanceX / 2.0; maxX = cursorX + distanceX / 2.0; minY = cursorY - distanceY / 2.0; maxY = cursorY + distanceY / 2.0; repaint(); } } // Listens for a click on the middle button of the mouse and resets the view private class ResetHandler extends MouseAdapter { @Override public void mousePressed(MouseEvent e) { if (e.getButton() != MouseEvent.BUTTON2) { return; } resetView(); } } // Starts and ends drag gestures with mouse left button. private class PanHandler extends MouseAdapter { @Override public void mousePressed(MouseEvent e) { if (e.getButton() != MouseEvent.BUTTON1) { return; } dragStart = e.getPoint(); setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); } @Override public void mouseReleased(MouseEvent e) { if (e.getButton() != MouseEvent.BUTTON1) { return; } setCursor(Cursor.getDefaultCursor()); } } // Handles drag gesture with the left mouse button and relocates the view // accordingly. private class PanMotionHandler extends MouseMotionAdapter { @Override public void mouseDragged(MouseEvent e) { Point dragEnd = e.getPoint(); double distance = xPixelToPosition(dragEnd.getX()) - xPixelToPosition(dragStart.getX()); minX = minX - distance; maxX = maxX - distance; distance = yPixelToPosition(dragEnd.getY()) - yPixelToPosition(dragStart.getY()); minY = minY - distance; maxY = maxY - distance; repaint(); dragStart = dragEnd; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy