Draw a labeled line graph that displays value tooltips in C#


labeled line graph

This (fairly complicated) example maps between several different coordinate systems:

  • The main graph is mapped from a coordinate space where 1900 ≤ x ≤ 2010 and 0 ≤ y ≤ 8000 to an area on the screen.
  • Each data point is drawn as a circle located at the point’s data coordinates but with size and shape given in form coordinates. (If it were drawn in graph coordinates, it would not be round because the ellipse’s width and height would be specified in year/dollars.)
  • Tick marks are drawn inside the edge of the graph area. The tick marks all have the same length even though the horizontal and vertical units are different.
  • The axes are labeled with text that is aligned with the tick marks in graph coordinates but they are drawn in screen coordinates.
  • The graph title is centered on the form.
  • The axis labels are centered on the axes.
  • The program uses an inverse transformation to see whether the mouse (in form coordinates) is over a data point (in graph coordinates) so it can display a tooltip.

The program breaks drawing tasks into several methods to make things a little simpler. The following sections describe those pieces.


Paint


The following code shows the main Paint event handler.

// Draw the graph.
private void picGraph_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

    // Draw with the identity transformation.
    DrawWithoutTransformation(e.Graphics);

    // Define the graph area.
    GraphXmin = 70;
    GraphXmax = picGraph.ClientSize.Width - 10;
    GraphYmin = 40;
    GraphYmax = picGraph.ClientSize.Height - 70;
    Rectangle graph_area = new Rectangle(
        GraphXmin, GraphYmin,
       GraphXmax - GraphXmin, GraphYmax - GraphYmin);
    e.Graphics.FillRectangle(Brushes.White, graph_area);

    // Draw things in the graph's world coordinate space.
    DrawInGraphCoordinates(e.Graphics,
        GraphXmin, GraphXmax, GraphYmin, GraphYmax);

    // Save the graph's coordinate transformation.
    Matrix graph_transformation = e.Graphics.Transform;

    // Draw things that are positioned using the graph's
    // transformation but that are drawn in pixels.
    DrawWithGraphTransformation(e.Graphics, graph_transformation);
}

The Paint event handler calls DrawWithoutTransformation to draw the main title without using any transformations. It then calculates the area where the graph will be and calls DrawInGraphCoordinates to draw things that use the graph’s coordinate system (such as the graph).

It saves the transformation used to draw in the graph coordinates and then calls DrawWithGraphTransformation. That method draws objects that should be drawn in the normal pixel coordinate system but positioned by the graph’s transformation. For example, the data point circle must be drawn in the normal pixel coordinates so they are round, but they are positioned over the points on the graph.


DrawWithoutTransformation


The following code shows how the DrawWithoutTransformation method draws the graph’s title.

// Draw things that use the identity transformation.
private void DrawWithoutTransformation(Graphics gr)
{
    // Draw the main title centered on the top.
    using (Font title_font = new Font("Times New Roman", 20))
    {
        using (StringFormat string_format = new StringFormat())
        {
            string_format.Alignment = StringAlignment.Center;
            string_format.LineAlignment = StringAlignment.Center;
            Point title_center = new Point(
                picGraph.ClientSize.Width / 2, 20);
            gr.DrawString("U.S. Gross National Debt",
                title_font, Brushes.Blue,
                title_center, string_format);
        }
    }
}

This code makes a big font. It then uses a StringFormat object to draw the text centered on the form.


DrawInGraphCoordinates


The following code shows how the DrawInGraphCoordinates method draws the graph.

// Draw things in the graph's world coordinate.
private void DrawInGraphCoordinates(Graphics gr,
    int xmin, int xmax, int ymin, int ymax)
{
    // Define the world coordinate rectangle.
    RectangleF world_rect =
        new RectangleF(Wxmin, Wymin, Wxmax - Wxmin, Wymax - Wymin);

    // Define the points to which the rectangle's upper left,
    // upper right, and lower right corners should map.
    // Note the vertical flip so large Y values are at the top.
    PointF[] window_points =
    {
        new PointF(xmin, ymax),
        new PointF(xmax, ymax),
        new PointF(xmin, ymin),
    };

    // Define the transformation.
    Matrix graph_transformation =
        new Matrix(world_rect, window_points);

    // Apply the transformation.
    gr.Transform = graph_transformation;

    // Plot the data lines.
    using (Pen green_pen = new Pen(Color.Green, 0))
    {
        for (int i = 1; i < Values.Length; i++)
        {
            gr.DrawLine(green_pen, Values[i - 1], Values[i]);
        }
    }
}

The DrawInGraphCoordinates method maps the graph coordinates (in years and $ billions) to the form’s coordinates. See the example Map points between coordinate systems in C# for information on how that works.

It then applies a transformation to make the mapping and draws the lines that make up the graph. The transformation maps the lines from world coordinates in year/billions to pixels.


DrawWithGraphTransformation


The following DrawWithGraphTransformation method holds the program’s most complicated code.

// Draw things that are positioned using the graph's
// transformation but that are drawn in pixels.
private void DrawWithGraphTransformation(
    Graphics gr, Matrix graph_matrix)
{
    // Reset to the identity transformation.
    gr.ResetTransform();

    // Plot the data points.
    // Copy the points so we don't mess up the original values.
    TransformedValues = (PointF[])Values.Clone();

    // Transform the points to see where they are on the PictureBox.
    graph_matrix.TransformPoints(TransformedValues);

    // Draw the points.
    foreach (PointF pt in TransformedValues)
    {
        gr.FillEllipse(Brushes.Lime,
            pt.X - Radius, pt.Y - Radius, 2 * Radius, 2 * Radius);
        gr.DrawEllipse(Pens.Black,
            pt.X - Radius, pt.Y - Radius, 2 * Radius, 2 * Radius);
    }

    // Draw the axes.
    using (Font label_font = new Font("Times New Roman", 8))
    {
        // Draw the Y axis.
        using (StringFormat label_format = new StringFormat())
        {
            label_format.Alignment = StringAlignment.Far;
            label_format.LineAlignment = StringAlignment.Center;

            // Draw the axis.
            PointF[] y_points = 
            {
                new PointF(Wxmin, Wymin),
                new PointF(Wxmin, Wymax),
            };
            graph_matrix.TransformPoints(y_points);
            gr.DrawLine(Pens.Black, y_points[0], y_points[1]);

            // Draw the tick marks and labels.
            for (int y = Wymin; y <= Wymax; y += 1000)
            {
                // Tick mark.
                PointF[] tick_point = { new PointF(Wxmin, y) };
                graph_matrix.TransformPoints(tick_point);
                gr.DrawLine(Pens.Black,
                    tick_point[0].X, tick_point[0].Y,
                    tick_point[0].X + 10, tick_point[0].Y);

                // Label.
                PointF[] label_point = { new PointF(0, y) };
                graph_matrix.TransformPoints(label_point);
                gr.DrawString(y.ToString("0"), label_font,
                    Brushes.Black, GraphXmin - 10, label_point[0].Y,
                    label_format);
            }
        }

        // Draw the X axis.
        // Draw the axis.
        PointF[] x_points = 
        {
            new PointF(Wxmin, Wymin),
            new PointF(Wxmax, Wymin),
        };
        graph_matrix.TransformPoints(x_points);
        gr.DrawLine(Pens.Black, x_points[0], x_points[1]);

        // Draw the tick marks and labels.
        for (int x = Wxmin; x <= Wxmax; x += 10)
        {
            // Tick mark.
            PointF[] tick_point = { new PointF(x, Wymin) };
            graph_matrix.TransformPoints(tick_point);
            gr.DrawLine(Pens.Black,
                tick_point[0].X, tick_point[0].Y,
                tick_point[0].X, tick_point[0].Y - 10);

            // Label.
            DrawXLabel(gr, x.ToString("0"), label_font,
                Brushes.Black, tick_point[0].X, GraphYmax + 10);
        }
    }

    // Label the axes.
    using (Font axis_font = new Font("Times New Roman", 14))
    {
        // Label the Y axis.
        using (StringFormat ylabel_format = new StringFormat())
        {
            ylabel_format.Alignment = StringAlignment.Center;
            ylabel_format.LineAlignment = StringAlignment.Near;
            gr.ResetTransform();
            gr.RotateTransform(-90);
            float cx = 0;
            float cy = (GraphYmin + GraphYmax) / 2;
            gr.TranslateTransform(cx, cy, MatrixOrder.Append);
            gr.DrawString("Debt ($ billions)", axis_font,
                Brushes.Green, 0, 0, ylabel_format);
            gr.ResetTransform();
        }

        // Label the X axis.
        using (StringFormat xlabel_format = new StringFormat())
        {
            xlabel_format.Alignment = StringAlignment.Center;
            xlabel_format.LineAlignment = StringAlignment.Far;
            RectangleF xlabel_rect = new RectangleF(
                GraphXmin, GraphYmax,
                GraphXmax - GraphXmin,
                picGraph.ClientSize.Height - GraphYmax);
            gr.DrawString("Year", axis_font,
                Brushes.Green, xlabel_rect, xlabel_format);
        }
    }
}

The method first resets the Graphics object’s transformation so no transformation is in use. It transforms the data points to get their locations in screen coordinates and then draws each point at its form location. This lets the code make each point round with width and height given in pixels rather than trying to figure out how wide/tall it would need to be in the graph’s years/dollars coordinate system.

Next the code draws the X and Y axes. It makes an array holding the minimum and maximum graph points, transforms them to see where they are in form coordinates, and uses the transformed values to draw the Y axis. It then loops through the points at which it should draw Y tick marks, transforms the points, and draws a 10 pixel wide tick mark at the transformed locations. Similarly it determines where to draw each tick mark’s label, transforms that location, and draws the label.

The code repeats those steps to draw the X axis. One difference is that it uses the DrawXLabel method to draw labels rotated 90 degrees at a given position in form coordinates.

The method finishes by drawing labels for the axes.


DrawXLabel


The following code shows the DrawXLabel method. (Being able to draw transformed text at an arbitrary location is pretty useful so you may want to add this method to your library of tools.)

// Draw a string rotated 90 degrees at the given position.
private void DrawXLabel(Graphics gr, string txt, Font label_font,
    Brush label_brush, float x, float y)
{
    // Transform to center the label's right edge
    // at the origin when we draw at the origin.
    gr.ResetTransform();

    // Rotate the translated text.
    gr.RotateTransform(90, MatrixOrder.Append);

    // Translate to the final destination.
    gr.TranslateTransform(x, y, MatrixOrder.Append);

    // Draw the label.
    using (StringFormat label_format = new StringFormat())
    {
        // Draw so the text is centered vertically and
        // left aligned at the origin.
        label_format.Alignment = StringAlignment.Near;
        label_format.LineAlignment = StringAlignment.Center;

        // Draw the text at the origin.
        gr.DrawString(txt, label_font, label_brush, 0, 0,
            label_format);
    }

    gr.ResetTransform();
}

The code removes any previous transformation, adds a rotation by 90 degrees, and finishes by translating the origin to the target location. The code then draws the text centered vertically and left aligned at the origin. The result is that the left end of the text is positioned at the target location. (For other programs, you could use a similar technique with different alignment. For example, you might want the center of the transformed text at the target location.)


Tooltips


The final piece of interesting code in this program tracks mouse movement. If you move the mouse over a data point, the program displays a tooltip showing the point’s value.

// If the mouse is hovering over a data point,
// set the PictureBox's tooltip.
private void picGraph_MouseMove(object sender, MouseEventArgs e)
{
    if (TransformedValues == null) return;

    // See what tool tip to display.
    string tip = "";
    for (int i = 0; i < TransformedValues.Length; i++)
    {
        if ((Math.Abs(e.X - TransformedValues[i].X) < Radius) &&
            (Math.Abs(e.Y - TransformedValues[i].Y) < Radius))
        {
            tip = "$" + Values[i].Y.ToString() + "B";
            break;
        }
    }

    // Set the new tool tip.
    if (tipData.GetToolTip(picGraph) != tip)
    {
        tipData.SetToolTip(picGraph, tip);
    }
}

This code simply loops through the transformed data values. (Recall that DrawWithGraphTransformation transforms the values so it can draw the points at pixel positions. The points are still transformed, so the TransformedValues array holds the data points’ values in pixels.) If the mouse is over a data point, the code displays an appropriate tooltip.

Notice that the code first checks whether the new tooltip is the same as the old one to avoid repeatedly setting the tooltip to the same value, which may cause flicker.


That’s all there is to the program. It’s a lot of code, but modifying it to display some other set of data shouldn’t be too hard.


Download Example   Follow me on Twitter   RSS feed   Donate




This entry was posted in drawing, graphics, mathematics, transformations and tagged , , , , , , , , , , , , , . Bookmark the permalink.

One Response to Draw a labeled line graph that displays value tooltips in C#

  1. Pingback: Graph stock prices downloaded from the internet in C#C# Helper

Leave a Reply

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