Graph equations entered by the user in WPF and C#


[graph]

This example uses key techniques described in several examples, mostly the following two:

  • Use transformations to draw a graph in WPF and C#
  • Graph equations entered by the user in C#
      The following two sections describe the key techniques taken from those examples. The section after that explains how the program uses polylines to draw the curve.

      Transformations

      The example Use transformations to draw a graph in WPF and C# shows how you can use transformations to convert points from a world coordinate system that is convenient to work with into pixels on the screen. The following code shows the first of two key transformation methods.

      // Prepare values for perform transformations.
      private Matrix WtoDMatrix, DtoWMatrix;
      private void PrepareTransformations(
          double wxmin, double wxmax, double wymin, double wymax,
          double dxmin, double dxmax, double dymin, double dymax)
      {
          // Make WtoD.
          WtoDMatrix = Matrix.Identity;
          WtoDMatrix.Translate(-wxmin, -wymin);
      
          double xscale = (dxmax - dxmin) / (wxmax - wxmin);
          double yscale = (dymax - dymin) / (wymax - wymin);
          WtoDMatrix.Scale(xscale, yscale);
      
          WtoDMatrix.Translate(dxmin, dymin);
      
          // Make DtoW.
          DtoWMatrix = WtoDMatrix;
          DtoWMatrix.Invert();
      }

      This method prepares the transformation for later use. It takes as inputs the world coordinate bounds that you want to use to define points. In this example, those bounds are entered by the user. (In the picture, they are -20 ≤ X ≤ 20, -5 ≤ Y ≤ 20.

      The method also takes the bounds of the part of the drawing object where you to draw. This example uses the Canvas control’s area measured in pixels. Because the Canvas control’s coordinate system places the point (0, 0) in the upper left corner and then makes coordinates increase downward and to the right, the program switches the control’s Y bounds when it calls PrepareTransformations. The following code shows that call. Notice that the last two parameters use dymax as the minimum Y coordinates and 0 as the larger Y coordinate.

      // Prepare the transformation.
      double dxmax = canGraph.ActualWidth;
      double dymax = canGraph.ActualHeight;
      PrepareTransformations(wxmin, wxmax, wymin, wymax,
          0, dxmax, dymax, 0);

      To build the transformation matrices, the method creates an identity matrix. It then adds a translation to move the upper left coordinates of the world coordinate area to the origin. It scales the result to map the world bounds to the device bounds. It finishes by translating to move the origin to the upper left corner of the destination device area.

      The method finishes by building the inverse transformation matrix in case you need to map from device coordinates back to world coordinates.

      The following methods uses the two transformation matrices to map points between the two coordinate systems.

      // Transform a point from world to device coordinates.
      private Point WtoD(Point point)
      {
          return WtoDMatrix.Transform(point);
      }
      
      // Transform a point from device to world coordinates.
      private Point DtoW(Point point)
      {
          return DtoWMatrix.Transform(point);
      }

      These methods simply call the transformation matrices’ Transform methods to map points from one coordinate system to the other.

      Compiling Equations

      The following DrawCurve method graphs the equation entered by the user.

      private void DrawCurve(Canvas canvas,
          double xmin, double xmax,
          double ymin, double ymax, string equation)
      {
          // Turn the equation into a function.
          string function_text =
              "using System;" +
              "public static class Evaluator" +
              "{" +
              "    public static double Evaluate(double x)" +
              "    {" +
              "        return " + equation + ";" +
              "    }" +
              "}";
      
          // Compile the function.
          CodeDomProvider code_provider =
              CodeDomProvider.CreateProvider("C#");
      
          // Generate a non-executable assembly in memory.
          CompilerParameters parameters = new CompilerParameters();
          parameters.GenerateInMemory = true;
          parameters.GenerateExecutable = false;
      
          // Compile the code.
          CompilerResults results =
              code_provider.CompileAssemblyFromSource(parameters,
                  function_text);
      
          // If there are errors, display them.
          if (results.Errors.Count > 0)
          {
              string msg = "Error compiling the expression.";
              foreach (CompilerError compiler_error in results.Errors)
              {
                  msg += "\n" + compiler_error.ErrorText;
              }
              MessageBox.Show(msg, "Expression Error",
                  MessageBoxButton.OK, MessageBoxImage.Error);
          }
          else
          {
              // Get the Evaluator class type.
              Type evaluator_type =
                  results.CompiledAssembly.GetType("Evaluator");
      
              // Get a MethodInfo object describing the Evaluate method.
              MethodInfo method_info =
                  evaluator_type.GetMethod("Evaluate");
      
              // See how big 1 pixel is in world coordinates.
              Point p0 = DtoW(new Point(0, 0));
              Point p1 = DtoW(new Point(1, 1));
              double dx = p1.X - p0.X;
      
              // Loop over x values to generate points.
              List points = new List();
              bool last_point_in_bounds = false;
              for (double x = xmin; x <= xmax; x += dx)
              {
                  bool point_in_bounds = false;
                  try
                  {
                      // Get the next point.
                      object[] method_params =
                          new object[] { x };
                      double y =
                          (double)method_info.Invoke(null, method_params);
      
                      // See if the point lies within the drawing area.
                      if (double.IsNaN(y))
                      {
                          // The value is undefined. Don't save it.
                      }
                      else
                      {
                          if (y < ymin)
                              y = ymin;
                          else if (y > ymax)
                              y = ymax;
                          else
                              point_in_bounds = true;
      
                          // Save the point.
                          points.Add(new Point(x, y));
                      }
      
                      // Draw the polyline if we should.
                      if (!point_in_bounds)
                      {
                          if (last_point_in_bounds)
                          {
                              // Draw whatever we have saved.
                              MakePolyline(canGraph, points);
                              points.Clear();
                          }
                          else
                          {
                              // Delete the previous invalid first point.
                              if (points.Count > 1) points.RemoveAt(0);
                          }
                      }
      
                      last_point_in_bounds = point_in_bounds;
                  }
                  catch (Exception ex)
                  {
                      MessageBox.Show(ex.Message);
                      return;
                  }
              }
      
              // Draw any remaining points.
              if (points.Count > 1) MakePolyline(canvas, points);
          }
      }

      This method composes a string that defines a function that evaluates the equation and returns the result. The function looks something like this:

      using System;
      public static class Evaluator{
          public static double Evaluate(double x)
          {
              return 10 * Math.Sin(x) / x;
          }
      }

      The program compiles this code and gets a MethodInfo object representing the Evaluator class’s Evaluate method.

      The method then creates two points in device coordinates that are one pixel apart horizontally and vertically. It uses the DtoW method to map those points into world coordinates and uses the results to see how far apart the points are horizontally in world coordinates.

      Next, the code loops from xmin to xmax generating points on the equation. To make a smooth curve, the program makes the X coordinate increase by the value dx that it calculated so that each point’s X value in device coordinates is one pixel larger than the previous points.

      Notice that the program compiles the function once and then calls it many times for different X values. That is much more efficient than compiling a new function for each point.

      Notice also that you must capitalize Math library methods and other keywords that should be capitalized. The evaluator’s code must have correct C# syntax.

      For some equations, the resulting Y value may lie outside of the world coordinate bounds. For example, for the equation shown in the picture, if x = 1.1, then y ≈ 5.76, which is outside of the bounds -3 ≤ y ≤ 5. If the equation is Math.Abs(x) / x and x is 0, then the y value is undefined.

      To handle those situations, the code uses double.IsNaN to see if y is undefined. In that case, the code ignores the new value. The variable point_in_bounds remains false, so the code that follows will draw any points that it has saved so far.

      If the value y is a number (although note that it might be infinity or -infinity), the code determines whether it lies within the world coordinate bounds and adjusts it if it does not. It then adds the (possibly adjusted) point to the points list.

      Next, the program determines whether the curve has left the world coordinate bounds. If the current point is outside of the world coordinate bounds but the previous point is inside, then the program has saved one or more valid points and is now leaving the world coordinate bounds. In that case, the code calls the MakePolyline method to draw whatever points it has saved before this point.

      If the current point does not lie within the world coordinate bounds and the previous point did not either, then the curve currently lies outside of the world coordinate bounds. In that case, we don’t need to save the previous invalid point, so the code removes it from the points list. (The program only removes the previous point if there actually is a previous point so the points list contains more than just the current point. The list will contain only the current point if it has just been created or cleared.)

      After it has finished looping through all of the points, the code calls MakePolyline a final time to draw any points that have not yet been drawn.

      Making Polylines

      The following code shows the MakePolyline method.

      // Make a polyline connecting the points.
      private void MakePolyline(Canvas canvas, List<Point> points)
      {
          // Convert the points into device coordinates
          // and add them to a PointCollection.
          PointCollection point_collection =
              new PointCollection(points.Count);
          foreach (Point point in points)
              point_collection.Add(WtoD(point));
      
          // Make a Polyline that uses the PointCollection.
          Polyline polyline = new Polyline();
          polyline.Points = point_collection;
          polyline.StrokeThickness = 1;
          polyline.Stroke = Brushes.Red;
          canvas.Children.Add(polyline);
      }

      This method’s second parameter is a list of points in world coordinates. To connect them with a polyline, it must convert the points into device coordinates that the Canvas control can understand.

      The code first creates a PointCollection to hold the converted points. It sets the collection’s initial capacity to the number of points in the list so the collection won’t need to resize itself later. The code then loops through the points, uses the WtoD method to convert them into device coordinates, and saves the results into the collection.

      Next, the method creates a Polyline object and saves the points in the object’s Points property. It sets the polyline’s thickness and color, and then adds it to the Canvas control’s Children collection.

      Summary

      This example seems to handle special cases such a vertical asymptotes, situations where the Y values leave the world coordinate bounds, and undefined Y values, but I can’t guarantee that it handles every possible situation. For example, if you graph the equation Math.Abs(x) / x and x increments so it skips over the value x = 0, then the program will not hit the undefined Y value so the curve will incorrectly jump from y = -1 to y = 1. Still, the program should do a reasonably good job in may situations.

      See the previous examples and download this example’s code to see additional details.


      Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in algorithms, drawing, graphics, mathematics, reflection, wpf and tagged , , , , , , , , , , , , , . Bookmark the permalink.

1 Response to Graph equations entered by the user in WPF and C#

  1. Pingback: Graph equations while preserving aspect ratio in WPF and C# - C# HelperC# Helper

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.