Draw a graph in WPF and C#

Graphing Difficulties

Draw a graph in WPF and C#

This example shows how to draw a graph in WPF and C#. Drawing graphics is always tricky because you typically need to work in at least two different coordinate systems. First, there’s the world coordinates that you want to use for the graph. For example, you might want X values to range over the years 2000 to 2020 and Y values to range over sales figure values between $10,000 and $100,000.

The second coordinate system is the device coordinate system measured in pixels on the screen.

Obviously you need to work with the world coordinate system when you’re drawing things like the graph itself. The trickiest part occurs when you need to position something in world coordinates but draw in device coordinates. For example, suppose you want to draw X and Y axes with tic marks that are 5 pixels wide. You use World coordinates to figure out where the tic marks should be placed, but then you need to calculate the lengths of the tic marks in pixels.

Similarly suppose you want to draw some text on the graph to label something. You position the text in device coordinates, but you probably want to draw the text in device coordinates. Otherwise it’s hard to center and otherwise align the text.

The final weird problem I’ll mention is making the graph’s line have a consistent thickness. Suppose you draw the graph in some normalized space and then scale it to fit the device area. For example, you draw in the world coordinate space 2000 <= x <= 2020, $10,000 <= y <= $100,000 and then you use a LayoutTransform to make the graph fit on a Canvas control. When the transform stretches the graph. It will also stretch the lines you drew for the graph. Unless the vertical and horizontal scale factors are the same, lines will be stretched by different amounts vertically and horizontally. Text is also stretched, giving some really annoying results.

Anyway, to really place everything exactly where you want it, you need to be able to work freely in both world and device coordinates. This post begins a short series of articles examining techniques you can use to draw graphs in C# and WPF.

A Simple Graph

This example draws a simple graph using only device coordinates. In other words, all positions are measured in pixels with (0, 0) in the upper left corner. The following posts will show how to work in more convenient world coordinates.

The following code shows the XAML that builds this program.

<Window x:Class="howto_wpf_graph.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="howto_wpf_graph"
  Height="250" Width="335" Loaded="Window_Loaded">
  <Grid Background="LightGreen">
    <Canvas Name="canGraph" Background="White"
      Width="300" Height="200"
      VerticalAlignment="Center" HorizontalAlignment="Center"/>
  </Grid>
</Window>

The window’s main child is a Grid that contains a Canvas named canGraph.

In WPF, you don’t normally draw directly on a drawing surface. You can if you really have to, but normally you use Line, Ellipse, Rectangle, and other shape controls to draw. You can include those objects in the XAML code if you like, but if you’re going to draw a non-trivial graph, you’re going to need to use code to do it.

When this example starts, the following event handler builds the graph. (Notice the Loaded="Window_Loaded" part of the window’s XAML declaration. That tells the program that the Window_Loaded method is the event handler for the window’s Loaded event.)

// Draw a simple graph.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
    const double margin = 10;
    double xmin = margin;
    double xmax = canGraph.Width - margin;
    double ymin = margin;
    double ymax = canGraph.Height - margin;
    const double step = 10;

    // Make the X axis.
    GeometryGroup xaxis_geom = new GeometryGroup();
    xaxis_geom.Children.Add(new LineGeometry(
        new Point(0, ymax), new Point(canGraph.Width, ymax)));
    for (double x = xmin + step;
        x <= canGraph.Width - step; x += step)
    {
        xaxis_geom.Children.Add(new LineGeometry(
            new Point(x, ymax - margin / 2),
            new Point(x, ymax + margin / 2)));
    }

    Path xaxis_path = new Path();
    xaxis_path.StrokeThickness = 1;
    xaxis_path.Stroke = Brushes.Black;
    xaxis_path.Data = xaxis_geom;
    
    canGraph.Children.Add(xaxis_path);

    // Make the Y ayis.
    GeometryGroup yaxis_geom = new GeometryGroup();
    yaxis_geom.Children.Add(new LineGeometry(
        new Point(xmin, 0), new Point(xmin, canGraph.Height)));
    for (double y = step; y <= canGraph.Height - step; y += step)
    {
        yaxis_geom.Children.Add(new LineGeometry(
            new Point(xmin - margin / 2, y),
            new Point(xmin + margin / 2, y)));
    }

    Path yaxis_path = new Path();
    yaxis_path.StrokeThickness = 1;
    yaxis_path.Stroke = Brushes.Black;
    yaxis_path.Data = yaxis_geom;

    canGraph.Children.Add(yaxis_path);

    // Make some data sets.
    Brush[] brushes = { Brushes.Red, Brushes.Green, Brushes.Blue };
    Random rand = new Random();
    for (int data_set = 0; data_set < 3; data_set++)
    {
        int last_y = rand.Next((int)ymin, (int)ymax);

        PointCollection points = new PointCollection();
        for (double x = xmin; x <= xmax; x += step)
        {
            last_y = rand.Next(last_y - 10, last_y + 10);
            if (last_y < ymin) last_y = (int)ymin;
            if (last_y > ymax) last_y = (int)ymax;
            points.Add(new Point(x, last_y));
        }

        Polyline polyline = new Polyline();
        polyline.StrokeThickness = 1;
        polyline.Stroke = brushes[data_set];
        polyline.Points = points;

        canGraph.Children.Add(polyline);
    }
}

The code first defines some boundaries for the graph.

Next the program creates a GeometryGroup object to represent the X axis. A GeometryGroup can hold other geometry objects such as lines. The code creates a Line to represent the axis’s baseline and adds it to the group. It then uses a loop to create a bunch of Line objects to represent tic marks and adds them to the group.

After it finishes creating all of the axis’s Line objects and adding them to the GeometryGroup, the program creates a Path object and sets its StrokeThickness and Stroke properties. It then sets the path’s Data property equal to the GeometryGroup.

Finally the code adds the path to the canGraph object’s Children collection.

The code then repeats those steps to create the Y axis.

Next the code makes some graph data. For each data set, the code creates a PointCollection object. It generates a bunch of random points and adds them to the collection. When it’s finished making data, the program creates a Polyline, sets its drawing properties, and sets its Points property to the point collection. Finally the code adds the Polyline to the canGraph object’s Children collection.

That’s all the program needs to do. When the window appears, the Line and Polyline objects draw themselves as necessary.

What’s Next?

Unfortunately this method makes you work in device coordinates. My next post will show how you can work more naturally in device coordinates.


Download Example   Follow me on Twitter   RSS feed




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, geometry, graphics, mathematics and tagged , , , , , , , , , , , , , , , , , . Bookmark the permalink.

9 Responses to Draw a graph in WPF and C#

  1. Pingback: Use transformations to draw a graph in WPF and C# -

  2. Bibaswann Bandyopadhyay says:

    This is excellent. I think this one is the only solution for those who want to do it without 3rd party library

  3. VR Karthikeyan says:

    This is amazing. It is a real excellent starting point of graphics in wpf in a very simple and understandable manner.
    Thanks a lot for sharing.

  4. Chi says:

    Nice work. Better than my use of pictureBox for graphing.

  5. Roger Breton says:

    Thank you so much for taking the time to share the wealth of your technical knowledge, Mr. Stephens. When I needed to program Charts in VisualBasic, I picked up one of your Wrox book, if memory serves — I’m sure I still have it on my shelves, somewhere. But moving to C#/WPF, with only WPF Toolkit as a free solution to program charts, I felt under the weather. I didn’t want to pay $$$$ for pro library as I only have very modest chart graphing needs. But of ALL the books I could find that dealt with WPF graphics, they only cover it through XAML? XAML is powerful but it’s a “static” solution, as far as feeding my data into. I needed to be shown, as you did in this blog, how to use WPF graphics capabilities “programmatically”, from code-behind. Thank’s to your generous contribution, I was able to go about my little project satisfactorily — and more.

  6. Roger Breton says:

    Any idea how to “reinitialize” the graph?

    • RodStephens says:

      To clear everything, you would empty the canGraph.Children collection. To remove the data and not the axes, you would remove the children after the axes’ paths. You should be able to remove all but the first two children. Or you could remove the children that are Polylines and not Paths.

  7. Tom Austin says:

    How could I modify this to use a collection of ints instead of using random data?

    • RodStephens says:

      The code loops through X coordinates and generates random Y coordinates. Instead of doing that, just use the Y values in your collection. Perhaps something like this:

      int step = width / values.Count;
      for (int i = 0; i < values.Count; i++)
      {
          points.Add(new Point(x * step, values[i]));
      }

Leave a Reply

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