Click here to Skip to main content
11,493,171 members (63,267 online)
Click here to Skip to main content

GraphDisplay: a Bezier based control for graphing functions and curves

, 26 Mar 2010 CPOL 36K 3K 71
Rate this:
Please Sign up or sign in to vote.
A WPF control for graphing functions, parametric curves, and polar curves.

Introduction

This article presents a control for the graphing of functions and parametric curves. The control will take a mathematically defined function and make a smooth representation of its graph using Bezier segments. Originally, this was part of some work on creating geometrical representations of curves using Bezier segments. As it evolved, I felt the need to verify that the geometries represented were the geometries I believed them to be. Grids, rules, ticks, and labels were all added for this purpose. In the end, a graphing control was produced that should prove useful in its own right.

Background

This article uses calculus and some simple linear algebra and a bit of trigonometry. The curves are graphed directly from the information on the function or curve and its derivatives. As I was unable to find derivations or formulas of that nature, I was required to recreate them from scratch. If any kind reader knows of somewhere where this derivation is done, please let me know and I will joyfully update this article.

How to Use

Functions

As this control graphs functions and curves, it would be best to start by defining exactly what definition of function or curve is being used. Put loosely, a function is a pair of methods, one that represents a value and one that represents a slope or derivative. The base class from which all functions derive is:

public abstract class FunctionBase
{
    public abstract double Value(double x);
    public abstract double Derivative(double x);
    
    //... Other stuff
}

Graphing Functions

To graph a function, we call the AddFunction method of GraphDisplay.

public void AddFunction(FunctionBase f, double start, 
            double end, int segments,   GraphStyle style)

For example:

GraphStyle curveStyle = new GraphStyle()
            {Stroke = new SolidColorBrush(Colors.Red ) ,Thickness = 2};
               
Exp ex = new Exp() ;
gDisplay.AddFunction(exp, 1, 2, 5, curveStyle);

would use GraphDisplay gDisplay to graph the exponential function y= ex from x= 1 to x= 2 in the color red with a stroke of width 2 using five Bezier segments. However, we are not quite done. The control needs to know how to relate the values in the abstract mathematical space where the function resides.

gDisplay.YTop = 8 ;
gDisplay.YBottom 2;
gDisplay.XLeft =0;
gDisplay.XRight =3;

This gives a small bit of whitespace around the graphed exponential. We are not restricted in the number of functions that we can add to a GraphDisplay, but all of the functions use the same bounds.

Creating Functions through Inheritance

Functions can be created by directly inheriting from FunctionBase. For example, let's say we want the function corresponding to f(x) = sin(x).

public class Sin : Mathematics.BaseClasses.FunctionBase
{
    public override double Value(double x)
    {
        return  Math.Sin( x );
    }

    public override double Derivative(double x)
    {
        return Math.Cos(x);
    }
}

Please note that this is a minimalist version of the Mathemaics.Functions.Sin class which corresponds to the family of functions of the form f(x) = ASin(nx + d).

Creating Functions Directly

We can also create functions through the Function class. The function class is as follows:

public sealed  class Function:FunctionBase
{
    private Func<double,> mF;
    private Func<double,> mDF;

    public Function( Func<double,double> f, Func<double,double> df)
  {
      mF = f;
      mDF = df;
  }

    public override double Value(double x)
    {
        return mF(x);
    }

    public override double Derivative(double x)
    {
        return mDF(x);
    }
}

We just pass in delegates for the function and its derivative. For example, we could again create f(x) = sin(x) via:

Function f = new Function(x=> Math.Sin(x), x=> Math.Cos(x));

Overriding is probably preferable for creating functions while the Function class works dynamically at runtime.

Combining Functions

Functions can also be combined in various ways. It is important to remember that instances of classes that derive from FunctionBase are Calculus like functions, so the chain rule needs to be obeyed. Fortunately, there are several static methods in FunctionBase for combining functions. To compose y= 2x and y= sin(x) and get y=sin(2x), we could use the following code:

Function twoX = new Function (x=>2*x,x=>2);
Sin sin = new Sin();
Function composition = Function.Compose(sin ,twoX);

All of the complexity is hidden behind the scenes.

public static Function Compose(FunctionBase outerFunction, FunctionBase innerFunction)
{
    //Change of variables for more readable code
    FunctionBase f = outerFunction;
    FunctionBase g = innerFunction;
    return new Function(
        x => f.Value(g.Value(x)),  //Composition f(g(x))
        x => f.Derivative(g.Value(x)) * g.Derivative(x) 
        //Chain Rule  f'(g(x))*g'(x)
        );
}

There are also static methods for the sum, difference, product, and quotient of functions. These functions can both be called directly or used via operators. With the same sin and twoX, f(x) = sin(x) + 2x can be done via:

Function plus = Function.Sum(sin, twoX); 
Function plus =  sin + twoX ;

which calls:

public static Function Sum(FunctionBase f, FunctionBase g)
{
    return new Function(x => f.Value(x) + g.Value(x),
        x => f.Derivative(x) + g.Derivative(x)
        );
}

f(x) = sin(x) - 2x can be made via:

Function minus = Function.Difference(sin, twoX); 
Function minus =  sin - twoX ;

which calls:

public static Function Difference(FunctionBase f, FunctionBase g)
{
    return new Function(x => f.Value(x) - g.Value(x),
        x => f.Derivative(x) - g.Derivative(x)
        );
}

f(x) = 2xsin(x) can be made via:

Function times = Function.Product( twoX ,sin); 
Function  times =  twoX * sin;

which calls:

public static Function Product(FunctionBase f, FunctionBase g)
{
    return new Function(x => f.Value(x) * g.Value(x),
       x => f.Derivative(x) * g.Value(x) + 
           g.Derivative(x) * f.Value(x) //Chain Rule f'(x)g(x) + f(x)g'(x)
       );
}

f(x) = sin(x)/2x can be made via:

Function divides = Function.Product( sin , twoX); 
Function  divides = sin /twoX;

which calls:

public static Function Quotient(FunctionBase numerator, FunctionBase denominator)
{
    //Change of variables for more readable code
    FunctionBase f = numerator;
    FunctionBase g = denominator;
    return new Function(x => f.Value(x) / g.Value(x),
        //Chain rule (f'(x)g(x) - f(x)g'(x))/g(x)^2
       x => (f.Derivative(x) * g.Value(x) - f.Value(x) * 
              g.Derivative(x)) / Math.Pow(g.Value(x), 2)
       );
}

I have belabored these transformations because they are important. The main reason for creating FunctionBase is to have an entity that will naturally obey the chain rule.

Function Sample

As a sample for the graphing of functions, we have the DampedSinusoid.

public class DampedSinusoid : FunctionBase
{
    public double A { get; private set; }
    public double Gamma { get; private set; }
    public double Omega { get; private set; }
    public double Phi { get; private set; }
    public DampedSinusoid(double a, double gamma, double omega, double phi)
    {
        A = a; Gamma = gamma; Omega = omega; Phi = phi;
    }

    public override double Value(double x)
    {
        return A * Math.Pow(Math.E, -Gamma * x) * Math.Cos(Omega * x + Phi);
    }

    public override double Derivative(double x)
    {
        return A * Math.Pow(Math.E, -Gamma * x) * Math.Cos(Omega * x + Phi) * (-Gamma)
               - A * Math.Pow(Math.E, -Gamma * x) * Math.Sin(Omega * x + Phi) * Omega;
    }

}

The sample shows two damped sinusoids and their sum. I might note that all of the values used in this class are set in the constructor and thereafter unalterable. I would strongly recommend following this policy.

Curves

A curve is very similar to a function except that there are four methods instead of two.

public abstract  class CurveBase
{
   public abstract Double X(double t);
   public abstract Double Y(double t);
   public abstract Double Dx(double t);
   public abstract Double Dy(double t);
   
   ... other stuff
}

The is also a CyclicCurveBase. This should be used in cases where we have a closed curve. In general, we should not expect an end user to know the period of a function.

public abstract class CyclicCurveBase:CurveBase 
{
  public abstract double CycleStart {get;}
  public abstract double CycleEnd{ get;}
}

Curves can be added via the AddCurve and AddCyclicCurve methods.

public void AddCurve(CurveBase c, double start, double end, 
                     int segments, GraphStyle style){...}
public void AddCyclicCurve(CyclicCurveBase c, int segments, GraphStyle style){...}

Curve Class Hierarchy

At this point, looking at the class hierarchy for the descendents of CurveBase should be helpful.

In order to create a new curve to graph, we should either inherit from one of the abstract base classes (blue) or compose using the compositional classes (purple). This is one of those times when being able to employ multiple inheritance would be nice. For example, I would like to be able to inherit the cyclic properties from some cyclic base class and the polar properties from a polar base. The way things are implemented involves some regrettable duplication. The classes were designed for expansion without significant changes to the graphing control. For example, if bipolar coordinates were added to the mix, no changes would need to be made to the graphing control. We could reasonably add other types of curves such as bipolar, elliptic, or hyperbolic in the future. In that case, we should create four classes for each coordinate system, just as we have PolarCurveBase, PolarCurve, CyclicPolarCurveBase, and CyclicPolarCurve.

Polar Curves and their Constructor Order

The PolarCurve class can be used to construct a curve if we have a function r= f(Θ)

public  sealed  class PolarCurve:PolarCurveBase 
{
    private Func<double, double> mR;
    private Func<double, double> mDr;

    public PolarCurve(Function r)
    {
        mR = r.Value;
        mDr = r.Derivative;
    }

    public override double R(double theta)
    {
        return mR(theta);
    }

    public override double Dr(double theta)
    {
        return mDr(theta);
    }
}

The thing to note is that the graphing control does not use r or Θ coordinates. This is handled by the base class.

public abstract class PolarCurveBase:CurveBase 
{
      public abstract double R(double theta);
      public abstract double Dr(double theta);
      private Curve mC;
  
     static Cos sCos = new Cos();
     static Sin sSin = new Sin();

     protected PolarCurveBase() {
         Function r = new Function(R,Dr);
         mC = new Curve( r * sCos ,  r * sSin );
     }

     public override double X(double t)
     {
         return mC.X(t);
     }

     public override double Y(double t)
     {
         return mC.Y(t);
     }

     public override double Dx(double t)
     {
         return mC.Dx(t);
     }

     public override double Dy(double t)
     {
         return mC.Dy(t);
     }
}

The interesting thing is the order in which the two constructors are called. PolarCurveBase's constructor is called before PolarCurve's.

This means that the curve transforming the function is created from overridden methods encapsulating the function before the function itself is introduced. The sample for polar curves is the Rose curve.

The Mathematics

Bezier Curves

Bezier curves are extremely useful for the drawing of smooth curves, so a little motivation might be of value. Let's assume we have a curve that we are interested in creating a representation of. It starts at Point a = (ax, ay) and goes to point b = (bx, by). We could represent the curve between them by a pair of functions. Bx(t) where Bx(0)=ax and Bx(1)=bx along with By(t) where By(0)=ay and By(1)=by.

Linear Bezier Curves

The simplest functions we could use for Bx and By (B is for Bezier) would be straight lines of the form f(x) = Ax + B, subject to the initial constraints. (A different equivalent formulation is usually used, but that adds complexity we don't need yet.) The important thing to notice is that we need 4 numbers to uniquely determine this line, two for Bx(t) and two for By(t). Fortunately, these are available in the x and y coordinates of the endpoints. With a little reorganization and notational change, this could be transformed to the customary definition of a Linear Bezier Curve.

B(t) = P0 + t(P1 - P0) where P0 is the starting point and P1 is the end point.

A line is not that good of an approximation to a curve, but it's worth keeping in mind that using it, we can use a lot of Bezier segments next to one another and that in the process of being displayed on the screen, our Bezier curves are converted back into line segments. The Geometry base class even has a GetFlattenedPathGeometry method to do just that. WPF supports this in spirit with the LineSegment class, if not in name.

Quadratic Bezier Curves

We can make a better approximation to our curve using a Quadratic (of the form f(x) = Ax2 + Bx + C) instead of a linear function. The thing to notice is that now we will need 6 numbers to define our segment. The obvious candidate for these numbers is the slope at the initial and final points. Put in vector form, we have:

B(t) = P0 (1-t)<sup>2</sup> + P1 2(1-t)t + P2t<sup>2</sup>

which corresponds to:

B<sub>x</sub>(t) = P0<sub>x</sub>(1-t)<sup>2</sup> + P1<sub>x</sub> 2(1-t)t + P2<sub>x</sub>t<sup>2</sup>
B<sub>y</sub>(t) = P0<sub>y</sub>(1-t)<sup>2</sup> + P1<sub>y</sub> 2(1-t)t + P2<sub>y</sub>t<sup>2</sup>

You might ask why we need to use this format instead of the simpler Ax2 + Bx + C. First, it is obvious that we can do this. Just multiply everything out and collect the terms to get A, B, and C. The benefit of this comes when we evaluate the conditions at the endpoints. If we start at Point a = (ax, ay) and go to point b = (bx, by), then we can immediately get P0x = ax and P0y = ay by evaluating at t=0. We get P2x = bx and P2y = by by evaluating at t=0. We then have the simpler problem of finding P1x and P1y. In WPF, Quadratic Bezier segments are supported by QuadraticBezierSegment. The constructor for QuadraticBezierSegment has a space for a P1 and a P2 but not a P0. At first, this might make it seem as if it only takes 4 rather than 6 numbers to define a Quadratic Bezier segment. This has much more to do with memory management than mathematics. If you have two consecutive segments in a path, P2 for the first acts as P0 for the second. Microsoft both saves memory and removes the chance for us to enter a wrong value by doing this. It does not change the dynamics of finding the values of P1. If we want to think in the Microsoft way, we must lose the two conditions at the initial point along with the requirement to find P0.

Cubic Bezier Curves

These are the type of segments that we are most likely to use. Notably, it is represented by a class named BezierSegment rather than CubicBezierSegment. These are also the segments you use in Adobe Illustrator with the pen tool. In vector form, it looks like:

B(t) = P0 (1-t)<sup>3</sup> + P1 3(1-t)<sup>2</sup> t + P2 3(1-t)t<sup>2</sup> + P3 t<sup>3</sup>

which corresponds to:

B<sub>x</sub>(t) = P0<sub>x</sub> (1-t)<sup>3</sup> + P1<sub>x</sub> 3(1-t)<sup>2</sup> t + P2<sub>x</sub> 3(1-t)t<sup>2</sup> + P3<sub>x</sub> t<sup>3</sup>
B<sub>y</sub>(t) = P0<sub>y</sub> (1-t)<sup>3</sup> + P1<sub>y</sub> 3(1-t)<sup>2</sup> t + P2<sub>y</sub> 3(1-t)t<sup>2</sup> + P3<sub>y</sub> t<sup>3</sup>

One of the nice things about the vector form is that it makes certain common transformations of Bezier segments easy. For instance, if we want to move the Bezier curve by 5 in the positive x direction, all we would need to do is add 5 to the x components of P0, P1, P2, and P3. In fact, any translation, reflection, or other transformation that can be accomplished by matrix multiplication has this property. Also, it means that when drawing pictures of Bezier segments, we lose no generality by rotating them, so they are easier to draw.

There is a very important relationship between the Ps and the derivatives at t =0 and t=1. It is just straightforward algebra, but unpleasant enough that I would advise using Mathematica or something similar rather than pencil and paper.

B'<sub>y</sub>(0) /B'<sub>x</sub>(0) = (P0<sub>y</sub> - P1<sub>y</sub>) /(P0<sub>x</sub> - P1<sub>x</sub>)
B'<sub>y</sub>(1) /B'<sub>x</sub>(1) = (P2<sub>y</sub> - P3<sub>y</sub>) /(P2<sub>x</sub> - P3<sub>x</sub>)

This means that the tangent line to the Bezier curve at Point P0 intersects P1, and that the tangent line at P3 intersects P2. So, if the only conditions that we have are the endpoints and the slopes at the ends, there is a large collection of possible P1s. Similar reasoning applies for P2.

Furthermore, let's say we wanted to choose a P1 such that it has a specified slope at P0. We could pick any point that is both on the tangent line and in the direction of P3, and it would have the correct slope at P0. For example, we could have the following possibilities:

In the one called leaning, the curve extends further to the right than P3, P1 would have needed to be significantly further out for it to be more noticeable. As we can see, we can get significantly different Bezier segments that have the same endpoints and slopes at their endpoints.

By the same reasoning as with Quadratic curves, we need 8 numbers to define the segment, four of which are provided by the initial and final points.

Functions

Now that the preliminaries are out of the way, it's time to graph functions. I am assuming that f(x) and its derivative f'(x) also exist for the range that is being graphed. This type of function was chosen as the graphing sweet spot. Giving up the first derivative makes things significantly harder, and derivatives higher than the first add a lot less than we would like.

B<sub>x</sub>(t) = P0<sub>x</sub> (1-t)<sup>3</sup> + P1<sub>x</sub> 3(1-t)<sup>2</sup> t + P2<sub>x</sub> 3(1-t)t<sup>2</sup> + P3<sub>x</sub> t<sup>3</sup>
B<sub>y</sub>(t) = P0<sub>y</sub> (1-t)<sup>3</sup> + P1<sub>y</sub> 3(1-t)<sup>2</sup> t + P2<sub>y</sub> 3(1-t)t<sup>2</sup> + P3<sub>y</sub> t<sup>3</sup>
a = P0<sub>x</sub>
f(a) = P0<sub>y</sub>
b = P3<sub>x</sub>
f(b) = P3<sub>y</sub>
f'(a) = (P0<sub>y</sub> - P1<sub>y</sub>) /(P0<sub>x</sub> - P1<sub>x</sub>)
f'(b) = (P2<sub>y</sub> - P3<sub>y</sub>) /(P2<sub>x</sub> - P3<sub>x</sub>)

After a bit of simplification, we get:

P1<sub>y</sub> -f'(a) P1<sub>x</sub> = f(a) - af'(a)
<sub>y</sub> -f'(b) P2<sub>x</sub> = f(b) - bf'(b)

It looks a bit better, but we still have two equations and four unknowns, and as we have already seen, this leaves a lot of possible segments. So we need more constraints. We have two, although they are not expressed in the form of equalities. First, we are graphing a function. By definition, that means that for every x, there must be exactly one f(x). That makes Bezier segments that loop and lean unacceptable representations of the function. Also, we expect that as a and b get closer together, the Bezier segment will match its portion of the curve more closely, just as it would if we were approximating the curve with line segments.

To go further, we need more conditions that are in the form of equations. Let's consider the following two:

P1<sub>x</sub> =( 2 a + b)/3
P2<sub>x</sub> =( a + 2 b)/3 

We will later see that these are excellent choices for conditions, but it is worth noting that they are not the only choices we could have made, and this choice puts the curve in the normal category. It also matches with the illustrator pen tool rule of thumb, that control handles should be about 1/3 of the way between nodes. With these conditions, we get:

P1<sub>y</sub> =( 3 f(a) - a f'(a) + b f'(a))/3
P2<sub>y</sub> =( 3 f(b) - a f'(b) + b f'(b))/3 

This is exactly the sort of result we would hope for. P1y and P2y are expressed as linear combinations of the conditions at the endpoints. There is no denominator that could possibly go to zero. One of the major reasons that Quadratic Bezier segments are not used is that there is a denominator that can go to zero, which causes artifacts in the graph. So, in summary, we have the following result:

B<sub>x</sub>(t) = P0<sub>x</sub> (1-t)<sup>3</sup> + P1<sub>x</sub> 3(1-t)<sup>2</sup> t + P2<sub>x</sub> 3(1-t)t<sup>2</sup> + P3<sub>x</sub> t<sup>3</sup>
B<sub>y</sub>(t) = P0<sub>y</sub> (1-t)<sup>3</sup> + P1<sub>y</sub> 3(1-t)<sup>2</sup> t + P2<sub>y</sub> 3(1-t)t<sup>2</sup> + P3<sub>y</sub> t<sup>3</sup>
P0<sub>x</sub> = a , P0<sub>y</sub> = f(a)
P1<sub>x</sub> = ( 2 a + b)/3 , P1<sub>y</sub> =( 3 f(a) - a f'(a) + b f'(a))/3
P2<sub>x</sub> =( a + 2 b)/3 , P2<sub>y</sub> =( 3 f(b) - a f'(b) + b f'(b))/3
P3<sub>x</sub> = b , P3<sub>y</sub> = f(b)

Parametric Curves

For a parametric curve, we have a curve defined by two functions: x = x(t) and y=y(t). By long standing tradition, t is used for the parameter (t is a good letter for time), It should be remembered that this t is not the same t as in Bx(t) and By(t). For a curve going from t = a to t= b, and functions x(t) and y(t), we have:

B<sub>x</sub>(t) = P0<sub>x</sub> (1-t)<sup>3</sup> + P1<sub>x</sub> 3(1-t)<sup>2</sup> t + P2<sub>x</sub> 3(1-t)t<sup>2</sup> + P3<sub>x</sub> t<sup>3</sup>
B<sub>y</sub>(t) = P0<sub>y</sub> (1-t)<sup>3</sup> + P1<sub>y</sub> 3(1-t)<sup>2</sup> t + P2<sub>y</sub> 3(1-t)t<sup>2</sup> + P3<sub>y</sub> t<sup>3</sup>
x(a) = P0<sub>x</sub>
y(a) = P0<sub>y</sub>
x(b) = P3<sub>x</sub>
y(b) = P3<sub>y</sub>
y'(a)/x'(a) = (P0<sub>y</sub> - P1<sub>y</sub>) /(P0<sub>x</sub> - P1<sub>x</sub>)
y'(b)/x'(b) = (P0<sub>y</sub> - P1<sub>y</sub>) /(P0<sub>x</sub> - P1<sub>x</sub>)

Again, we need more conditions. Loops and points can occur in parametric curves such as the Lissajous curve, but should not be added as artifacts of our algorithm. One of the implications of this is that P1 cannot be entirely determined from the behavior at P0. To see this, consider bringing b closer and closer to a. Eventually, we would get a pointed looped or leaning curve for the Bezier segment.

We could consider the same idea we used while graphing functions. Since the argument for P1 is completely analogous to the one for P2, the discussion will focus solely on P1. The difference is that instead of moving horizontally 1/3 of the x distance, we could put P1 on the line that intersects the segment connecting P0 and P3. In doing this, we define a triangle and so are able to find P1.

First, we know the angle α. The slopes of the lines between P0 and P3 is known since P0 and P3 are known. The slope of the line between P0 and P1 is just y'(a)/x'(a), which again is known. Via a well known formula tan(α) = (mb - ms)/ (1+ mb ms), where mb is the bigger slope and ms is the smaller slope. Due to the law of Sines, we know that the length of the segment from P0 to P1 is 1/3 the distance between P0 and P3 divided by the sine of pi/2 - α.

Once we have the length between P0 and P1, call it h, we can get P1.

P1<sub>x</sub> =P0<sub>x</sub> + h cos(μ) where μ is the angle made by the tangent line at P0 with the X axis
P1<sub>y</sub> =P0<sub>y</sub> + h sin(μ)

Since we have dx and dy separately, we can simplify this a bit as cos(μ) = dx/(dx2 + dyx2)1/2 and sin(μ) = dy/(dx2 + dyx2)1/2. This gives us a way to find P1. It is not nearly as good a solution as was found for the functional case, but it provides a procedure that can be used to plot curves.

How it Works

Coordinates

Up until now, coordinates have been taken for granted. Functions and curves have been defined in an abstract space of numbers, but they must be displayed on the screen in terms of screen coordinates. This involves changes in translation, scale, and orientation. On the screen, we need to graph a TransformedFunction rather than the original FunctionBase.

public  class TransformedFunction
{
   private FunctionBase mF;
   private FunctionBase mXTrans;
   private FunctionBase mYTrans;

   public TransformedFunction(FunctionBase f,  
          FunctionBase xTrans,  FunctionBase yTrans)
   {
       mF = f; mXTrans = xTrans; mYTrans = yTrans;
   }

   public double Input(double x)
   {
       return mXTrans.Value(x);
   }

   public double Value(double x)
   {
       return mYTrans.Value(mF.Value(x));
   }

   public double Derivative(double x)
   {
       return (mYTrans.Derivative(mYTrans.Value(x)) / 
               mXTrans.Derivative(x)) * mF.Derivative(x);//Chain Rule
   }
}

The only things to note are that all three items must be transformed, the input (x), the output (y), and the derivative (dy/dx). Also, we should remember that the chain rule is not optional. The interesting bits are the two FunctionBases that transform the x and y coordinates.

mXTransform = new Function(x => (DisplayCanvas.ActualWidth / 
   (XRight - XLeft)) * (x - XLeft), x => DisplayCanvas.ActualWidth / (XRight - XLeft));
mYTransform = new Function(y => (DisplayCanvas.ActualHeight / (YBottom - YTop)) * 
    (y - YTop), y => DisplayCanvas.ActualHeight / (YBottom - YTop));

This takes care of all three aspects including the fact that in our functional space, increasing y goes up, while on the canvas, it goes down. This is not the only function we could use. A slightly different expression would give a logarithmic scaling. There is very little difference in this respect between curves and functions, so I will omit the discussion.

Functions

When graphing a function, we just add it to the GraphDisplay:

public void AddFunction(FunctionBase f, double start, double end, 
                        int segments, GraphStyle style)
{
    Path p = new Path();
    TransformedFunction tf = 
       new TransformedFunction(f, mXTransform, mYTransform);
    p.Data = BezierGeometry(tf, start, end, segments);
    p.Fill = style.Fill;
    p.Stroke = style.Stroke;
    p.StrokeThickness = style.Thickness;
    DisplayCanvas.Children.Add(p);
}

This utilizes the BezierGeometry method to produce the geometry.

private PathGeometry BezierGeometry(TransformedFunction tf, 
        double start, double end, int segments)
{
    PathFigure pF = new PathFigure();
    pF.StartPoint = new Point(tf.Input(start),tf.Value(start));
    pF.IsClosed = false;
    Func<double, double> f = tf.Value;       //for readability
    Func<double, double> df = tf.Derivative; //for readability
    for (int i = 0; i < segments; i++)
    {
        double aOrig = i * (end - start) / segments + start;
        double a = tf.Input(aOrig);
        double bOrig = (i + 1) * (end - start) / segments + start;
        double b = tf.Input(bOrig);
        Point P1 = new Point((2 * a + b) / 3, 
           (3 * f(aOrig) - a * df(aOrig) + b * df(aOrig)) / 3);
        Point P2 = new Point((2 * b + a) / 3, 
           (3 * f(bOrig) + a * df(bOrig) - b * df(bOrig)) / 3);
        Point P3 = new Point(b, f(bOrig));
        BezierSegment bs = new BezierSegment(P1, P2, P3, true);
        pF.Segments.Add(bs);
    }

    PathGeometry pGeo = new PathGeometry();
    pGeo.Figures.Add(pF);

    return pGeo;
}

Curves

The code for adding a curve or cyclic curve is virtually identical.

public void AddCurve(CurveBase c, double start, 
            double end, int segments, GraphStyle style)
{
    Path p = new Path();
    TransformedCurve tf = new TransformedCurve(c, mXTransform, mYTransform);
    p.Data = BezierGeometry(tf, start, end, segments);
    p.Fill = style.Fill;
    p.Stroke = style.Stroke;
    p.StrokeThickness = style.Thickness;
    DisplayCanvas.Children.Add(p);
}

The only difference lies in the BezierGeometry method. The algorithm described above is used for generating the Bezier segments. However, the algorithm for generating the curve's geometry is not as stable as in the functional case. Rapid changes of direction are not handled well. For example, Lissajous figures can have spikes in them. These are handled less than optimally.

For this reason, there are two distinct algorithms being used in calculating the Bezier segments: BezierBuilder and AjustingBezierBuilder.

private PathGeometry BezierGeometry(TransformedCurve tc, 
                     double start, double end, int segments)
{
    PathFigure pF = new PathFigure();
    pF.StartPoint = new Point(tc.X(start), tc.Y(start));
    pF.IsClosed = false;
  
    Stack<double> boundaries = new Stack<double>();
    for (int i = segments; i >= 0; i--)
    {
        boundaries.Push(i * (end - start) / segments + start);
    }

    for (int i = 0; i < segments; i++)
    {
        double a = boundaries.Pop();
        double b =  boundaries.Peek();
        AjustingBezierBuilder abb = new AjustingBezierBuilder(tc, a, b);
        abb.AddTo( pF);
    }

    PathGeometry pGeo = new PathGeometry();
    pGeo.Figures.Add(pF);

    return pGeo;
}

The BezierGeometry method first creates a Stack of boundaries to define the start and end points for calculating the Bezier segments. This replaces the loop used in the functional case. This step of creating the stack may appear odd, but it was chosen to be able to harmonize with several different optimization strategies that in the end did not make it to this edition of the graphing control. Under normal circumstances, the results of AjustingBezierBuilder will be the same as for when BezierBuilder will be used for the segment. Normal circumstances means that the distance between the starting and ending points is less than the distance from the points to their nearby control points. This is just following the procedure described in the mathematical area.

  • The distance between the endpoints is calculated.
  • Check if endpoints are close enough to approximate with a straight line.
  • The slope of the lines to the control points is determined from the derivatives of the curve at the endpoints.
  • The slope of the line connecting the endpoints of the segment is determined.
  • The angles between the slope at the endpoints and the endpoint connecting lines is determined from the slopes.
  • The length of the segments connecting the endpoints to the control points is determined via the law of Sines.
  • The control points are determined by moving along the line connecting the endpoints to the control points, the distance to the control points.
  • BezierSegment is created from the control points and the endpoints.
public BezierBuilder(CurveBase c, double a, double  b)
{
  A = a;
  B = b;
  Point InitialPoint = new Point(c.X(a), c.Y(a));
  FinalPoint = new Point(c.X(b), c.Y(b));
  //The distance between the endpoints is calculated.
  double distance =  Geometry.Distance(InitialPoint, FinalPoint)  ;

  //Check if endpoints are close enough to approximate with a straight line.
  if (distance < tolerance) //optimization 
  {
      mBs = new LineSegment(FinalPoint, true);
      IsStable = true;
  }
  else
  {
      //The slope of the lines to the control points is determined
      //from the derivatives of the curve at the enpoints.
      double curveSlopeA = c.Slope(a);       ///dy(a) / dx(a);
      double curveSlopeB = c.Slope(b);       ///dy(b) / dx(b);

      //The slope of the line connecting
      // the endpoints of the segment is detemined.
      double slope = Geometry.Slope(InitialPoint, FinalPoint);

      //The Angles between the slope at the endpoints 
      //and the endpoint connecting lines is determined from the slopes.
      double angleA = Geometry.AngleBetweenLines(slope, curveSlopeA);
      double angleB = Geometry.AngleBetweenLines(slope, curveSlopeB);

      //The length of the segments connecting the Endpoints 
      //to the control points is determined via the law of sines
      Double distanceIn = distance / 3; //our extra condition
      Double lengthAlongLineA = distanceIn / 
             (Math.Sin(Math.PI / 2 - angleA)); //Law of Sines
      Double lengthAlongLineB = distanceIn / 
             (Math.Sin(Math.PI / 2 - angleB)); //Law of Sines

      //The Control points are determined by moving along the line 
      //conecting the enpoints to the control points 
      //the distance to the control points.
      Point controlPointA = Geometry.AtDistanceFromPointOnLine(InitialPoint, 
                            c.Dx(a), c.Dy(a), lengthAlongLineA);
      Point controlPointB = Geometry.AtDistanceFromPointOnLine(FinalPoint, 
                            -c.Dx(b), -c.Dy(b), lengthAlongLineB);

      //The  BezierSegment is created from the control points and the endpoints.
      mBs = new BezierSegment(controlPointA, controlPointB, FinalPoint, true);
    
      //Mark if Bezier segment is Stable
      IsStable = distance > lengthAlongLineA && 
                 distance > lengthAlongLineB;
      PointDistance = Geometry.Distance(InitialPoint, FinalPoint);
  }
}

This works reasonably well. However, it handled spikes less well than could be hoped for. To handle this situation, AdjustingBezierBuilder is used.

public AjustingBezierBuilder(CurveBase c, double a, double b)
{
   BezierBuilder bb = new BezierBuilder(c,a,b);
   if ( bb.IsStable) // No Need to Adjust
   {
       mSegments = new List<pathsegment>();
        mSegments.Add(bb.Segment);
   }else{
     LinkedList<bezierbuilder> builders = new LinkedList<bezierbuilder>();
     LinkedListNode<bezierbuilder>  current=  
       builders.AddFirst(new BezierBuilder(c, a, (a + b) / 2));
     builders.AddAfter(current, new BezierBuilder(c,  (a + b) / 2,b ));

     while (current != null  )
     {
         if (current.Value.IsStable) //No Problems
         {
             current = current.Next;
         }
         else // There is a problem with the current Bezier builder 
         {
             //Slices BezierBuilder into two samller Bezier builders
             builders.AddAfter(current, new BezierBuilder(c, 
               (current.Value.A + current.Value.B) / 2, current.Value.B));
             LinkedListNode<bezierbuilder> newcurrent = 
                builders.AddAfter(current, 
                new BezierBuilder(c, current.Value.A, 
                (current.Value.A + current.Value.B) / 2)); 
             builders.Remove(current );
             current = newcurrent;
         }
     }
     mSegments = (from bld in builders select bld.Segment).ToList();
   }
}

When BezierBuilder is normal, AdjustingBezierBuilder defaults to the BezierBuilder's segment. Otherwise, AdjustingBezierBuilder splits the segment that the Bezier segment is made from into two. Each of those segments defines a new BezierBuilder which is checked. If that too is unstable, then it is split again. This process continues until all of the BezierBuilder objects are marked as stable. We know that this process will stop since, once the distance between the endpoints goes below the static value of the Tolerance in the BezierBuilder, the process will stop. It should be noted that each distinct BezierBuilder is only created and tested once. If a BezierBuilder is not found to be stable, it is replaced by two new ones, or equivalently, two new ones are added after the original and then the original is removed. A linked-list was employed to make this more efficient.

Conclusion

I hope that this graphing control serves you well. This is something that would have been significantly harder to do just a few years ago. It is also just a preliminary to a new set of functional geometries that I hope to have ready soon.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

KenJohnson
Software Developer (Senior)
United States United States
Written software for what seems like forever. I'm currenly infatuated with WPF. Hopefully my affections are returned.

Comments and Discussions

 
QuestionSecond derivative Pin
Baccalore4-Oct-13 4:46
memberBaccalore4-Oct-13 4:46 
GeneralMy vote of 5 Pin
abat8-Jul-12 4:44
memberabat8-Jul-12 4:44 
QuestionNovel curve generation project 1 Pin
Member 873609322-Mar-12 12:20
memberMember 873609322-Mar-12 12:20 
QuestionSmall Problem Pin
Doug Wyrembek20-Aug-11 7:17
memberDoug Wyrembek20-Aug-11 7:17 
GeneralMy vote of 5 Pin
toantvo15-Aug-11 7:04
membertoantvo15-Aug-11 7:04 
GeneralMy vote of 5 Pin
Filip D'haene23-Jun-11 7:47
memberFilip D'haene23-Jun-11 7:47 
QuestionGreat piece of work beautifully presented Pin
Declan Brennan21-Jun-11 23:26
memberDeclan Brennan21-Jun-11 23:26 
GeneralMy Vote of 5 Pin
Ali BaderEddin6-Apr-10 15:46
memberAli BaderEddin6-Apr-10 15:46 
GeneralExcellent. Pin
Luc Pattyn27-Mar-10 6:07
mvpLuc Pattyn27-Mar-10 6:07 
GeneralRe: Excellent. Pin
KenJohnson27-Mar-10 16:47
memberKenJohnson27-Mar-10 16:47 
GeneralThat's cool Pin
Philip F.27-Mar-10 2:38
memberPhilip F.27-Mar-10 2:38 
GeneralRe: That's cool Pin
KenJohnson27-Mar-10 16:48
memberKenJohnson27-Mar-10 16:48 
GeneralExcellent work! Pin
Tefik Becirovic27-Mar-10 1:14
memberTefik Becirovic27-Mar-10 1:14 
GeneralRe: Excellent work! Pin
KenJohnson27-Mar-10 16:56
memberKenJohnson27-Mar-10 16:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150520.1 | Last Updated 27 Mar 2010
Article Copyright 2010 by KenJohnson
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid