12,821,494 members (25,153 online)
alternative version

#### Stats

83.6K views
123 bookmarked
Posted 19 Sep 2007

# Expression Plotter Control

, 28 May 2008 CPOL
 Rate this:
A control that plots any number and any combination of mathematical expressions in rectangular or polar mode

## Introduction

This article describes a graphing control that can be dropped into any Windows application to create scientific and technical graphs in a huge range of styles. The control plots any number of mathematical expressions in rectangular as well as polar mode. By "expression," I mean any combination of algebraic, trigonometric, exponential, logarithmic, hyperbolic or custom user-defined functions. There are various graphing controls at CodeProject (and outside), but almost all of them require us to provide a list of values (points). This control is unique in the way that it expects the user to provide an expression, e.g. 6*sin(5*x)*cos(8*x).

## Features

The most important issue for every custom control is how extensible and customizable it is, and how easy it is to use in other Windows applications. Some salient features of this control are:

• Multiple expressions with different colors
• Rectangular as well as polar mode
• High extensibility due to the `IEvaluatable` interface
• Capability of zooming and scrolling the graph
• Ability to save the current graph as an image
• Capability to reverse axes

## Public Interface

The public interface of the control is very handy. Various methods and properties are provided to provide a high degree of customization. Let us have a quick look at some selected methods and properties.

### Methods

 `void AddExpression(IEvaluatable expression, Color color, bool visible)` This function adds an expression to the graph. We will be looking at `IEvaluatable` later. `color` is the color of expression to be plotted while `visible` controls the visibility of the expression. `void SetRangeX(double StartX, double EndX)` This sets the range for the X-axis. For example, use SetRangeX(-5,15) to construct a graph having x-axis starting from -5 and ending at 15 `void SetRangeY(double StartY, double EndY)` This sets the range for the Y-axis. For example, use SetRangeY(15,25) to construct a graph having y-axis starting from 15 and ending at 25 `void ZoomIn()` Zooms-in the graph. Similarly, we have functions `ZoomInX() `(for zooming X-axis only), `ZoomInY `(for zooming Y-axis only) and the corresponding functions for zoom-out (`ZoomOut`, `ZoomOutX` and `ZoomOutY`). Note that these functions automatically adjust their increasing/decreasing ratio, i.e., if we try to zoom-in/out while viewing a large scale graph, the zooming ratio is also large; and if we zoom a graph displayed for a short range, the zooming ratio is small. `void MoveLeft(int division)` Scrolls the graph to left the number of divisions specified. In a similar fashion, we have functions like `MoveRight()`, `MoveUp()` and `MoveDown()`. `Bitmap GetGraphBitmap()` Returns a bitmap object for the current graph. `void CopyToClipboard()` Copies the current graph to clipboard. `double[] GetValues(double point)` Evaluates all the expressions at a given point and returns the result as an array.

### Properties

 ScaleX `double` Base scale for the X-axis, e.g. if `ScaleX` is 10 then the graph will be created from -10 to 10. If we supply a -ve value, the axis is reversed, i.e. supplying `ScaleX`=-10 draws graph from 10 to -10. ForwardX `double` Controls navigation within the X-axis. Note that the graph will be drawn from `-ScaleX+ForwardX` to `ScaleX+ForwardX`, so if we have specified `ScaleX`=20 and `ForwardX`=0 then the graph will draw X-axis from -20 to +20. Similarly, if we have set `ScaleX`=20 with `ForwardX`=-10 then the graph will have X-axis from -30 to +10. DivisionX `int` No of grid divisions for the X-axis. PrintStepX `int` Step (increment) for printing the X-axis labels. Its range is from 0 to `DivisionX`. If set to 0, the graph does not display any labels; if set to `DivisionX`, the graph displays labels with each grid division. GraphMode `Enum` Switches between rectangular and polar modes. PolarSensitivity `double` Adjusts the sensitivity for polar graphs. The higher the value, the more accurate the polar graph. DisplayText `bool` Sets whether to display expression text inside the graph. Grids `bool` Turns on/off the grids. PenWidth `int` Adjusts the pen width for drawing graphs.

Almost all these methods and properties are demonstrated in the application and the user interface of the demo project simply calls the respective methods of the control.

## Using the Code

Before getting into implementation details of the control, we will have a look at its usage. The range for a graph can be controlled using properties (`ScaleX`, `ForwardX`, etc.) as well as functions (`SetRange()`, `ZoomIn()`, `Move()`, etc). Constructing a graph using methods is easy while that using properties is recommended for advanced users that want to have greater control.

Let's construct a simple graph using methods. After we have created all the necessary references and placed the control (named "`expPlotter`") on a Windows form, here's some code to experiment:

```expPlotter.SetRangeX(-6, 14); //set the x-axis range from -6 to 14
expPlotter.SetRangeY(-5, 5); //set the y-axis scale from -5 to +5

expPlotter.DivisionsX = 5; //set no. of grid divisions
expPlotter.DivisionsY = 5;
expPlotter.PenWidth = 2; //set pen width for graph

Color.Green, true);
Color.Blue, true);
Color.Brown, true);

//we need to manually refresh our graph for the changes to take effect
expPlotter.Refresh();```

The above code will produce the following output:

Let's reconstruct the same graph using properties, the more flexible approach.

```expPlotter.ScaleX = 10; //set the base scale to -10 to +10

expPlotter.ForwardX = 4;
//since the base scale was -10 to +10 and our ForwardX value is 4
// so now our graph will have x-axis range: -6 to 14

expPlotter.ScaleY = 5; //set the y-axis scale from -5 to 5
expPlotter.ForwardY = 0; //Y-axis origin at the center

expPlotter.DivisionsX = 5; //set no. of grid divisions
expPlotter.DivisionsY = 5;
expPlotter.PenWidth = 2; //set pen width for graph

Color.Green, true);
Color.Blue, true);
Color.Brown, true);

expPlotter.Refresh(); //refresh the graph```

The above code will produce the same output as the previous one. Similarly, other methods and properties can be used for further customization. Now we will have a look at the implementation of the control.

## Implementation Details

### ExpressionPlotterControl

Plotting in rectangular mode: The control internally stores all expressions in a `List` of `IEvaluatable` interface. We iterate through all these expressions (`List<Evaluatable>`) and start a loop from `-ScaleX+ForwardX` to `ScaleX+ForwardX`. For each value of the loop variable, we find the value of expression using `IEvaluatable.Evaluate(loop variable)` and plot it. For continuity in our graph, we join the previously evaluated value to the newly evaluated value.

Plotting in polar mode: Nothing special is done for handling polar modes since we can transform polar coordinates to rectangular coordinates by using the famous formulae: `x=r*cos(theta)` and `y=r*sin(theta)`. We evaluate the expressions from `-PI` to `+PI` and plot the equivalent rectangular coordinates.

Here's the code for `PlotGraph()` method:

```void PlotGraph(Graphics g)
{
DisplayScale(g);
if (this.bDisplayText)
DisplayExpressionsText(g);

double X, Y;
double dPointX, dPointY;
double dLeastStepX, dLeastStepY;
double dMin, dMax, dStep;
int i;

//All the time, (X1,Y1) will be the previous plotted point,
//while (X2,Y2) will be the current point to plot.
//We will join both to have our graph continuous.
float X1 = 0, Y1 = 0, X2 = 0, Y2 = 0;

//This variable controls whether our graph should be continuous or not
bool bContinuity = false;

//divide scale with its length(pixels) to get increment per pixel
dLeastStepX = dScaleX / iLengthScale;
dLeastStepY = dScaleY / iLengthScale;

//prepare variables for loop
if (graphMode == GraphMode.Polar)
{
dMin = -Math.PI;
dMax = Math.PI;
dStep = dScaleX / iPolarSensitivity;
}
else //if (Rectangular Mode)
{
dStep = dLeastStepX;
dMin = -dScaleX + dForwardX;
dMax = dScaleX + dForwardX;
}

for (i = 0; i < this.expressions.Count; i++)
{
//check if expression needs to be drawn and is valid
if (expVisible[i] == true && expressions[i].IsValid == true)
{
bContinuity = false;
for (X = dMin; X != dMax; X += dStep)
{
if (dScaleX < 0 && X < dMax)
break;
if (dScaleX > 0 && X > dMax)
break;
try
{
//evaluate expression[i] at point: X
Y = expressions[i].Evaluate(X);

if (double.IsNaN(Y))
{
//break continuity in graph if expression returned a NaN
bContinuity = false;
continue;
}

//get points to plot
if (graphMode == GraphMode.Polar)
{
dPointX = Y * Math.Cos(X) / dLeastStepX;
dPointY = Y * Math.Sin(X) / dLeastStepY;
}
else // if (Rectangular mode;
{
dPointX = X / dLeastStepX;
dPointY = Y / dLeastStepY;
}

//check if the point to be plotted lies
//inside our visible area(i.e. inside our
//current axes ranges)
if ((iOriginY - dPointY + dForwardY /
dLeastStepY) < iOriginY - iLengthScale
|| (iOriginY - dPointY + dForwardY /
dLeastStepY) > iOriginY + iLengthScale
|| (iOriginX + dPointX - dForwardX /
dLeastStepX) < iOriginX - iLengthScale
|| (iOriginX + dPointX - dForwardX /
dLeastStepX) > iOriginX + iLengthScale)
{
//the point lies outside our current scale so
//break continuity
bContinuity = false;
continue;
}

//get coordinates for currently evaluated point
X2 = (float)(iOriginX + dPointX - dForwardX /
dLeastStepX);
Y2 = (float)(iOriginY - dPointY + dForwardY /
dLeastStepY);

//if graph should not be continuous
if (bContinuity == false)
{
X1 = X2;
Y1 = Y2;

// the graph should be continuous afterwards
// since the current evaluated value is valid
// and can be plotted within our axes range
bContinuity = true;
}

//join points (X1,Y1) and (X2,Y2)
g.DrawLine(new Pen(expColors[i], iPenWidth),
new PointF(X1, Y1), new PointF(X2, Y2));

//get current values into X1,Y1
X1 = X2;
Y1 = Y2;
}
catch
{
bContinuity = false;
continue;
}
}
}
}
}```

### IEvaluatable

The expression plotter control expects the expressions to implement `IEvaluatable`. This way, we can increase the extensibility of our control since we can write any class with custom evaluation behavior. We just need to provide a definition for the following:

• `string ExpressionText`

Get/Set the text of expression

• `bool IsValid`

Should return `true` if the expression can be evaluated without any exception

• `double Evaluate(double dvalueX)`

This function should evaluate the expression at `dvalueX` and return the result as `double`. If the result cannot be calculated (e.g. log for a -ve number) then it should return `double.NaN`.

### Expression: IEvaluatable

The control contains a sample implementation of `IEvaluatable`, the `Expression` class. Let me briefly describe this implementation. The following pseudo-code can be used to evaluate a simple expression:

```int runningTotal = 0;
Operator lastOperator = "+";
While ( Expression is not scanned )
{
if Expression.Encountered( operand )
runningTotal = runningTotal <lastOperator> operand;
else if Expression.Encountered( operator )
lastOperator = operator;
}```

Let's see how the above code works on a sample expression `2*5+6-9`:

However, the problem with this code is that it "always" evaluates left to right, "ignoring" operator precedences. Thus, `4+3*5` is evaluated as `35` instead of `19` because first `4+3=7` is evaluated and then multiplied by `5` to get `7*5=35`. I solved this problem by inserting parenthesis at appropriate positions in the expression and evaluating parenthesis first, i.e., I converted `4+3*5` to `4+(3*5)`. `InsertPrecedenceBrackets()` is the function that does this stuff while the main function: `EvaluateInternal()`, calls itself recursively whenever it encounters a parenthesis. The `DoAngleOperation()` contains the definition of functions that the class supports.

Here's the code for `EvaluateInternal()`:

```public double EvaluateInternal(double dvalueX,
int startIndex, out int endIndex)
{
double dAnswer = 0, dOperand = 0;
char chCurrentChar, chOperator = '+';
string strAngleOperator;

for (int i = startIndex + 1; i < textInternal.Length; i++)
{
startIndex = i;
chCurrentChar = textInternal[startIndex];

// if found a number, update dOperand
if (char.IsDigit(chCurrentChar))
{
while (char.IsDigit(textInternal[i]) || textInternal[i] == '.')
i++;
dOperand =
Convert.ToDouble(textInternal.Substring(startIndex,
i - startIndex));
i--;
}

//if found an operator
else if (IsOperator(chCurrentChar))
{
chOperator = chCurrentChar;
}

//if found independent variable
else if (char.ToLower(chCurrentChar) == charX)
{
dOperand = dvalueX;
}

//if found a bracket, solve it first
else if (chCurrentChar == '(')
{
dOperand = EvaluateInternal(dvalueX, i, out endIndex);
i = endIndex;
}

//if found closing bracket, return result
else if (chCurrentChar == ')')
{
endIndex = i;
}

else //could be any function e.g. "sin" or any constant e.g "pi"
{
while (char.IsLetter(textInternal[i]))
i++;

//if we got letters followed by "(", we've got a
//function else a constant
if (textInternal[i] == '(')
{
strAngleOperator = textInternal.Substring(startIndex,
i - startIndex).ToLower();
dOperand = EvaluateInternal(dvalueX, i, out endIndex);
i = endIndex;
dOperand = DoAngleOperation(dOperand, strAngleOperator);
}
else //constant
{
dOperand = this.constants[textInternal.Substring(startIndex,
i - startIndex).ToLower()];
i--;
}
}

//return if we got a NaN
{
endIndex = i;
return double.NaN;
}

}
endIndex = textInternal.Length;
return 0;
}```

Also, here are few lines from `DoAngleOperation()`:

```//this function contains definitions for supported functions,
//Ofcourse, we can add more here.
static double DoAngleOperation(double dOperand, string strOperator)
{
strOperator = strOperator.ToLower();
switch (strOperator)
{
case "abs":
return Math.Abs(dOperand);
case "sin":
return Math.Sin(dOperand);
case "arctan":
return Math.Atan(dOperand);
case "arcsinh":
return Math.Log(dOperand + Math.Sqrt(dOperand * dOperand + 1));
case "arccosh":
return Math.Log(dOperand + Math.Sqrt(dOperand * dOperand - 1));
case "MyCustomFunction":
return MyFunctionsClass.MyCustomFunction(dOperand));
:
:
}
}```

### Supported Functions

The current implementation of the `Expression` class contains definitions for `abs` (absolute), `sin` (trigonometric sine), `cos` (trigonometric cosine), `tan` (trigonometric tangent), `sec` (trigonometric secant), `cosec` (trigonometric cosecant), `cot` (trigonometric cotangent), `arcsin` (trigonometric sine inverse), `arccos` (trigonometric cosine inverse), `arctan` (trigonometric tangent inverse), `exp` (exponent), `ln` (natural logarithm), `log` (logarithm in base 10), `antilog` (antilog in base 10), `sqrt` (square root), `sinh` (hyperbolic sine), `cosh` (hyperbolic cosine), `tanh` (hyperbolic tangent), `arcsinh` (hyperbolic sine inverse), `arccosh` (hyperbolic cosine inverse) and `arctanh` (hyperbolic tangent inverse). As mentioned previously, this list can be extended by adding custom user-defined functions.

## Point of Interest

Please note that the control does not redraw itself unless asked to do so. So, we manually need to call `expPlotter.Refresh()` to reflect our changes on the graph. This is done because redrawing the control is a time-consuming task; it involves re-evaluation of all the expressions and re-plotting of all the points. I first thought of an AutoRefresh property for controlling this, but later dropped this idea because most of the time we will be performing more than one operation before refreshing the graph. Please tell me your thoughts on this.

## About the Demo Application: Graph Plotter

The application fairly demonstrates the usage of the control. Since the control provides a very handy interface, we just need to call the respective functions of the control to make a cool application. A quick look at the event handlers of the program can give us a nice idea of how to use the control. Resize the graph window to observe how the control adjusts itself for every size. Enter expressions and notice how the application provides assistance (e.g. text coloring, inserting `*` and parenthesis at appropriate places, etc) while entering expressions. I hope you will like this control. Happy graphing..

## History

• Version 1.0: Initial version
• Version 1.1: The control can now have non-rectangular size (snapshot below)

## Share

 Software Developer Pakistan

Syed Mehroz Alam, living in Karachi, Pakistan, is a developer focusing Microsoft technologies. He has completed his bachelors as a Computer Systems Engineer in 2006 and is currently pursuing a Masters degree in Computer Science. He loves to learn, discover and master all aspects of .NET and SQL Server. Mehroz has developed rich internet enterprise applications using Silverlight in addition to the traditional ASP.NET and Windows Forms applications. He has worked with all three components of SQL Business Intelligence Studio: SSIS, SSRS and SSAS for developing BI Solutions and Data warehouse. He loves to write complex TSQL queries and evaluate his skills by participating in various TSQL Challenges. His blog can be viewed at http://smehrozalam.wordpress.com.

## You may also be interested in...

 Pro

 First PrevNext
 help Tien-Sang12-Jan-14 4:26 Tien-Sang 12-Jan-14 4:26
 Discrete Points Archimedes2414-Mar-11 0:25 Archimedes24 14-Mar-11 0:25
 Re: Discrete Points cdafg29-Aug-12 16:38 cdafg 29-Aug-12 16:38
 Re: Discrete Points Archimedes2429-Aug-12 22:54 Archimedes24 29-Aug-12 22:54
 Modification to expression evaluation Manfred R. Bihy4-Jan-11 10:41 Manfred R. Bihy 4-Jan-11 10:41
 Re: Modification to expression evaluation Syed Mehroz Alam31-Jan-11 23:52 Syed Mehroz Alam 31-Jan-11 23:52
 My vote of 5 Manfred R. Bihy4-Jan-11 10:35 Manfred R. Bihy 4-Jan-11 10:35
 Re: My vote of 5 Syed Mehroz Alam31-Jan-11 23:49 Syed Mehroz Alam 31-Jan-11 23:49
 Background Expression Plotter Control anh2toan27-Jun-10 23:38 anh2toan 27-Jun-10 23:38
 Re: Background Expression Plotter Control Syed Mehroz Alam6-Jul-10 3:11 Syed Mehroz Alam 6-Jul-10 3:11
 Shade tovanvu24-Jun-10 20:15 tovanvu 24-Jun-10 20:15
 Re: Shade Syed Mehroz Alam6-Jul-10 3:12 Syed Mehroz Alam 6-Jul-10 3:12
 Sorry! x = f(y). not y = x * x ... tovanvu22-Jun-10 19:47 tovanvu 22-Jun-10 19:47
 Re: Sorry! x = f(y). not y = x * x ... Syed Mehroz Alam23-Jun-10 5:53 Syed Mehroz Alam 23-Jun-10 5:53
 My vote of 1 tovanvu121-Jun-10 2:44 tovanvu1 21-Jun-10 2:44
 Expression Plotter Control with Expression y = x * x tovanvu20-Jun-10 19:30 tovanvu 20-Jun-10 19:30
 Re: Expression Plotter Control with Expression y = x * x Syed Mehroz Alam21-Jun-10 23:42 Syed Mehroz Alam 21-Jun-10 23:42
 Thanks sabitzhabit22-Dec-09 4:46 sabitzhabit 22-Dec-09 4:46
 Re: Thanks Syed Mehroz Alam21-Jun-10 23:42 Syed Mehroz Alam 21-Jun-10 23:42
 As regards efficiency of (re-)draws xian1233-Dec-09 2:04 xian123 3-Dec-09 2:04
 Re: As regards efficiency of (re-)draws Syed Mehroz Alam15-Dec-09 3:39 Syed Mehroz Alam 15-Dec-09 3:39
 Re: As regards efficiency of (re-)draws xian12318-Dec-09 21:21 xian123 18-Dec-09 21:21
 InsertPrecedenceBrackets ncnlam22-Nov-09 18:19 ncnlam 22-Nov-09 18:19
 Re: InsertPrecedenceBrackets Syed Mehroz Alam22-Nov-09 23:37 Syed Mehroz Alam 22-Nov-09 23:37
 Re: InsertPrecedenceBrackets ncnlam23-Nov-09 0:45 ncnlam 23-Nov-09 0:45
 Last Visit: 31-Dec-99 19:00     Last Update: 26-Mar-17 17:04 Refresh 12 Next »