Developing a package for mathematical illustration


Graph of a cubic
Abstract classes
Packages
Interfaces

Based on notes Developing a package for mathematical illustration

Graph of a cubic

Our goal is to draw this figure again with the aim of making the code more flexible so that we can create other illustrations more easily.

Breaking it into objects

Let's think about what we'll need to draw this figure.

  • A JPanel to contain the figure elements,

  • the mathematical viewing rectangle,

  • a change of coordinates from the mathematical viewing rectangle to the JPanel's coordinate system,

  • a grid behind the graph, and

  • the graph

Moreover, it's easy to imagine that we want to reuse some of these features, the grid and graph in particular, in other illustrations. This thought is a hint to make separate objects out of them. Then we will be able to insert them into other illustrations later on.

In fact, each of the items above will be an object.

BoundingBox

The viewing rectangle will be important for setting up the change of coordinates and for drawing both the grid and the graph. We will create a class BoundingBox to help us pass the information around easily.

public class BoundingBox {
    public double llx, lly, urx, ury;
    public BoundingBox(double lowerLeftX, double lowerLeftY,
                       double upperRightX, double upperRightY) {
        llx = lowerLeftX;  lly = lowerLeftY;
        urx = upperRightX; ury = upperRightY;
    }
}

Notice that all a BoundingBox does is store information.

CubicPanel's fields

We will create an extension of JPanel, called CubicPanel, in a moment. Let's think about what we want out of it.

  • CubicPanel will be an extension of JPanel onto which we draw our figure.

  • It should have fields for the elements--that is, the grid and graph--we are going to draw.

  • Moreover, since it holds the pixels on which we will draw, it makes sense for it to hold the dimensions of the JPanel, the mathematical BoundingBox and the AffineTransform that takes mathematical coordinates into pixel coordinates.

    Grid grid;
    CubicFunction function;
    AffineTransform transform;
    int width = 0, height = 0;
    BoundingBox bbox;

CubicPanel's constructor

CubicPanel's constructor will just instantiate the fields we need.

    public CubicPanel(double llx, double lly, double urx, double ury) {
        setBackground(Color.white);
        bbox = new BoundingBox(llx, lly, urx, ury);
        grid = new Grid(this);
        function = new CubicFunction(this);
    }

Notice that we pass a reference to the CubicPanel to the constructors for the grid and graph. We'll see why this is presently.

The AffineTransform is not instantiated in the constructor for two reasons.

  • CubicPanel does not have a dimension until it is placed in a JFrame and the JFrame is packed.

  • At any time, the JFrame holding CubicPanel may be resized. Therefore, we will set up the AffineTransform inside the paintComponent method when the actual drawing is performed.

CubicPanel's paintComponent method

Here we will simply determine the AffineTransform and ask the grid and graph to draw themselves on the Graphics2D.

    public void paintComponent(Graphics gfx) {
	super.paintComponent(gfx);
        Rectangle size = getBounds();
        Graphics2D g = (Graphics2D) gfx;
        if (size.width != width || size.height != height) {
            width = size.width;  height = size.height;
            float ratioX = (float) (width / (bbox.urx - bbox.llx));
            float ratioY = (float) (-height /(bbox.ury - bbox.lly));
            transform = new AffineTransform();
            transform.scale(ratioX, ratioY);
            transform.translate(-bbox.llx, -bbox.ury);
        }
        grid.plot(g);
        function.plot(g);
    }

Two other methods

When the grid and graph are plotting, they will need to know the BoundingBox and the AffineTransform. We'll include methods for the grid and graph to retrieve these objects from the CubicPanel when they are drawn.

    public BoundingBox getBoundingBox() {
	return bbox;
    }

    public AffineTransform getTransform() {
	return transform;
    }

Grid

Here is Grid.java:

public class Grid  {
    CubicPanel panel;

    public Grid(CubicPanel cp) {
	panel = cp;
    }
    public void plot(Graphics2D g) {
	BoundingBox bbox = panel.getBoundingBox();
        GeneralPath path = new GeneralPath();
        int minX = (int) Math.floor(Math.min(bbox.llx, bbox.urx));
        int maxX = (int) Math.ceil(Math.max(bbox.llx, bbox.urx));
        for (int x = minX; x <= maxX; x++) {
            path.moveTo(x, (float) bbox.lly);
            path.lineTo(x, (float) bbox.ury);
        }
        int minY = (int) Math.floor(Math.min(bbox.lly, bbox.ury));
        int maxY = (int) Math.ceil(Math.max(bbox.lly, bbox.ury));
        for (int y = minY; y <= maxY; y++) {
            path.moveTo((float) bbox.llx, y);
            path.lineTo((float) bbox.urx, y);
        }
	AffineTransform transform = panel.getTransform();
        g.draw(transform.createTransformedShape(path));
    }
}

Notice that this arrangement allows us to concentrate on drawing the grid in this class without worrying about the other details.

CubicFunction.java

Here is CubicFunction.java:

public class CubicFunction {
    CubicPanel panel;

    public CubicFunction(CubicPanel cp) {
	panel = cp;
    }

    public void plot(Graphics2D g) {
	BoundingBox bbox = panel.getBoundingBox();
	AffineTransform transform = panel.getTransform();
        int steps = 50;
        double stepsize = (bbox.urx - bbox.llx)/steps;
        GeneralPath path = new GeneralPath();
        path.moveTo((float) bbox.llx, (float) valueAt(bbox.llx));
        for (int i = 1; i <= steps; i++) {
	    double x = bbox.llx + i*stepsize;
            path.lineTo((float) x, (float) valueAt(x));
	}
        g.setPaint(Color.black);
        g.draw(transform.createTransformedShape(path));
    }

    public double valueAt(double x) {
        return x*x*x - x;
    }
}

And all together


Abstract classes

Now that we've made the grid and graph reusable, we'd like to take this just a bit further.

  • First, we will change our CubicPanel into a FigurePanel to allow for more general illustrations.

  • Second, notice that grid and graph are, in many respects, quite similar: they are both given a CubicPanel in their constructors and they both a method plot(Graphic2D). The only real difference is in the implementation of this method.

    In fact, it's easy to imagine adding other figure elements such as lines and points that would fit into the same basic format. Furthermore, we may wish to add, say, the ability to color our figure elements.

    Java provides a useful construction called an abstract class to help deal with several different classes that are largely similar.

More abstract classes

We will define an abstract class called Plotable that models the behavior of a figure element that can be plotted on a FigurePanel.

Here is Plotable.java

import java.awt.*;

public abstract class Plotable {
    FigurePanel panel;
    Color color = Color.black;

    public void setFigurePanel(FigurePanel fp) {
	panel = fp; 
    }
    public void setColor(Color c) {
	color = c; 
    }

    public abstract void plot(Graphics2D g);
}

This looks something like a typical class: there are instance fields and two methods, setFigurePanel and setColor, that are defined.

Notice that the method plot is not defined and that it is declared to be abstract. This is allowed as long as the class is declared to be abstract. This is exactly how we think about our figure elements: they can be given a color and put in a FigurePanel, but they differ in how they are plotted.

Abstract classes can not be instantiated but they can be subclassed by a class that defined any abstract methods.

Our new Grid class

public class Grid extends Plotable {

    public void plot(Graphics2D g) {
	BoundingBox bbox = panel.getBoundingBox();
        GeneralPath path = new GeneralPath();
        int minX = (int) Math.floor(Math.min(bbox.llx, bbox.urx));
        int maxX = (int) Math.ceil(Math.max(bbox.llx, bbox.urx));
        for (int x = minX; x <= maxX; x++) {
            path.moveTo(x, (float) bbox.lly);
            path.lineTo(x, (float) bbox.ury);
        }
        int minY = (int) Math.floor(Math.min(bbox.lly, bbox.ury));
        int maxY = (int) Math.ceil(Math.max(bbox.lly, bbox.ury));
        for (int y = minY; y <= maxY; y++) {
            path.moveTo((float) bbox.llx, y);
            path.lineTo((float) bbox.urx, y);
        }
        g.setPaint(color);
	AffineTransform transform = panel.getTransform();
        g.draw(transform.createTransformedShape(path));
    }
}

Notice that all we have done to define this class is make explicit how the grid should be plotted.

Axes

Since we may wish to plot axes in some figures, let's build a class Axes that also extends Plotable.

import java.awt.*;
import java.awt.geom.*;

public class Axes extends Plotable {
    public void plot(Graphics2D g) {
	BoundingBox bbox = panel.getBoundingBox();
	GeneralPath path = new GeneralPath();
	path.moveTo((float) bbox.llx, 0);  path.lineTo((float) bbox.urx, 0);
	path.moveTo(0, (float) bbox.lly);  path.lineTo(0, (float) bbox.ury);
	g.setPaint(color);
	AffineTransform transform = panel.getTransform();
	g.draw(transform.createTransformedShape(path));
    }
}

and a class for points

Here is a class GraphicalPoint that allows us to plot points. Besides defining plot, it allows us to specify how large the point should be and a style, either a circle or square, for drawing the point.

public class GraphicalPoint extends Plotable implements Moveable {
    public static final int CIRCLE = 0;
    public static final int SQUARE = 1;
    int style = CIRCLE;
    double size = 2;
    public double x, y;
    Shape shape;
    Mover mover;
    
    public GraphicalPoint(double xp, double yp) {
	x = xp;  y = yp;
    }

    public void setPoint(double xp, double yp) {
	x = xp;  y = yp;
    }

    public void setSize(double s) { size = s; }
    
    public void setStyle(int s) { style = s; }

    public void plot(Graphics2D g) {
	Point2D.Double point = new Point2D.Double(x, y);
	panel.getTransform().transform(point, point);
	if (style == CIRCLE) 
	    shape = 
		new Ellipse2D.Double(point.x - size, point.y - size,
				    2*size, 2*size);
	else shape = 
		new Rectangle2D.Double(point.x - size, point.y - size,
				    2*size, 2*size);
	g.setPaint(color);
	g.fill(shape);
	g.setPaint(Color.black);
	g.draw(shape);
    }
}

FigurePanel.java

Now that we have a reasonable collection of Plotables, let's look at what modifications we need to make in CubicPanel to obtain a more general FigurePanel.

First, we would not like to be able to add an arbitrary number and type of elements into the diagram. Therefore, we will store the Plotables in a Vector and include a method add by which a Plotable may be added to the FigurePanel.

FigurePanel.java

An illustration

SimpleFigure.java


Putting everything into a package

By now, we have quite a few classes that work together to create illustrations. Let's see how to bundle them together into a package.

Our classes are:

    Axes		FigurePanel	Grid		SimpleFigure
    BoundingBox		GraphicalPoint	Plotable

To create a package called figure:

  • Create a directory (or folder) called figure and move all relevant .java files into it. Notice that the directory name has to agree with the name of the package.

  • Add a line to the beginning of each .java file before the class declaration:
    	package figure;
    

  • Set the CLASSPATH environment variable to point to the parent directory of figure.

    In a bash shell, use export CLASSPATH=..

    In a windows terminal window, use set CLASSPATH=..

  • Recompile all classes just as before.

  • To run the application SimpleFigure, use

    	java figure.SimpleFigure
    

Interfaces

When building a general class to graph functions, we need to be able to specify the particular function we are interested in. In other words, we need to pass a method to the graphing class.

We can do this using an interface.

    package figure;

    public interface Function {
        public double valueAt(double x, double[] params);
    }

The point is that if we know a class implements the Function interface, we can be sure that it has a method with the signature:

        public double valueAt(double x, double[] params);

We include double[] params in the argument list so that we can pass parameters to the function along with the argument x.

Implementing an interface

Here is an implementation of Function:

public class Cubic implements Function {
    public double valueAt(double x, double[] params) {
        return x*x*x - x;
    }
}

Building a graphing class

Let's now construct the class in our figure package that will allow us to graph functions. To construct an instance, we will need to give it an implementation of Function to build the graph.

public class GraphicalFunction extends Plotable {
    Function function;
    double[] params;
    Stroke stroke;
    int steps = 50;

    public GraphicalFunction(Function f, double[] p) {
        function = f;  params = p;
        stroke = new BasicStroke(1f);
    }

    public void setSteps(int s) { steps = s; }

    public void setStroke(Stroke s) { stroke = s; }

    public void plot(Graphics2D g) {
        BoundingBox bbox = panel.getBoundingBox();
        AffineTransform transform = panel.getTransform();
        double stepsize = (bbox.urx - bbox.llx)/steps;
        GeneralPath path = new GeneralPath();
        path.moveTo((float) bbox.llx, 
                    (float) function.valueAt(bbox.llx, params));
        for (int i = 1; i <= steps; i++) {
            double x = bbox.llx + i*stepsize;
            path.lineTo((float) x, (float) function.valueAt(x, params));
        }
        g.setPaint(color);
        Stroke savedStroke = g.getStroke();
        g.setStroke(stroke);
        g.draw(transform.createTransformedShape(path));
        g.setStroke(savedStroke);
    }
}

How to use it

FunctionFigure.java

Interactivity

We can set up our figure package to allow for interactive diagrams.

LineMover.java

Get the figure package

Download the figure package and unpack it using the command tar xzvf figure.tgz