GDI+ curve "overflowing"

941 views Asked by At

I'm currently using GDI+ to draw a line graph, and using Graphics.DrawCurve to smooth out the line. The problem is that the curve doesn't always match the points I feed it, and that makes the curve grow out of the graph frame in some points, as seen below(red is Graphics.DrawLines, green is Graphics.DrawCurve).

graph

How would I go about solving this?

2

There are 2 answers

3
TaW On BEST ANSWER

The simplest solution is to set a tension:

enter image description here

The green curve is drawn with the default tension, the blue one set a tension of 0.1f:

private void panel1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.DrawLines(Pens.Red, points.ToArray());
    e.Graphics.DrawCurve(Pens.Green, points.ToArray());
    e.Graphics.DrawCurve(Pens.Blue, points.ToArray(), 0.1f);

}

You will need to test what is the best compromise, 0.2f is still ok, 0.3f is already overdrawing quite a bit..

For a really good solution you will need to use DrawBeziers. This will let you draw curves that can go through the points without any overdrawing and with full control of the radius of the curves; but to to so you will need to 'find', i.e. calculate good control points, which is anything but trivial..:

enter image description here

This result is by no means perfect but already complicated enough.. I have displayed the curve points and their respective control points in the same color. For each point there is an incoming and an outgoing control point. For a smooth curve they need to have the same tangents/gradients in their curve points.

I use a few helper functions to calculate a few things about the segments:

  • A list of gradients
  • A list of signs of the gradients
  • A list of segment lengths
  • Lists of horizontal and of vertical gaps between points

The main function calculates the array of bezier points, that is the curve points and between each pair the previous left and the next right control points.

In the Paint event it is used like this:

List<PointF> bezz = getBezz(points);

using (Pen pen = new Pen(Color.Black, 2f))
       e.Graphics.DrawBeziers(pen, bezz.ToArray());

Here are the functions I used:

List<float> getGradients(List<PointF> p)
{
    List<float> grads = new List<float>();
    for (int i = 0; i < p.Count - 1; i++)
    {
        float dx = p[i + 1].X - p[i].X;
        float dy = p[i + 1].Y - p[i].Y;
        if (dx == 0) grads.Add(dy == 0 ? 0 : dy > 0 ? 
            float.PositiveInfinity : float.NegativeInfinity);
        else grads.Add(dy / dx);
    }
    return grads;
}

List<float> getLengths(List<PointF> p)
{
    List<float> lengs = new List<float>();
    for (int i = 0; i < p.Count - 1; i++)
    {
        float dx = p[i + 1].X - p[i].X;
        float dy = p[i + 1].Y - p[i].Y;
        lengs.Add((float)Math.Sqrt(dy * dy + dx * dx));
    }
    return lengs;
}

List<float> getGaps(List<PointF> p, bool horizontal)
{
    List<float> gaps = new List<float>();
    for (int i = 0; i < p.Count - 1; i++)
    {
        float dx = p[i + 1].X - p[i].X;
        float dy = p[i + 1].Y - p[i].Y;
        gaps.Add(horizontal ? dx : dy);
    }
    return gaps;
}

List<int> getSigns(List<float> g)
{  
    return g.Select(x => x > 0 ? 1 : x == 0 ? 0 : -1).ToList();  
}

And finally the main function; here I make a distinction: Extreme points ( minima & maxima) should have their control points on the same height as the points themselves. This will prevent vertical overflowing. They are easy to find: The signs of their gradients will always altenate.

Other points need to have the same gradient for incoming and outcoming control points. I use the average between the segments' gradients. (Maybe a weighed average would be better..) And I weigh their distance according to the segment lengths..

List<PointF> getBezz(List<PointF> points)
{
    List<PointF> bezz = new List<PointF>();
    int pMax = points.Count;

    List<float> hGaps = getGaps(points, true);
    List<float> vGaps = getGaps(points, false);
    List<float> grads = getGradients(points);
    List<float> lengs = getLengths(points);
    List<int> signs = getSigns(grads);

    PointF[] bezzA = new PointF[pMax * 3 - 2];

    // curve points
    for (int i = 0; i < pMax; i++) bezzA[i * 3] = points[i];

    // left control points
    for (int i = 1; i < pMax; i++)
    {
        float x = points[i].X - hGaps[i - 1] / 2f;
        float y = points[i].Y;
        if (i < pMax - 1 && signs[i - 1] == signs[i])
        {
            float m = (grads[i-1] + grads[i]) / 2f;
            y = points[i].Y - hGaps[i-1] / 2f * m * vGaps[i-1] / lengs[i-1];
        }
        bezzA[i * 3 - 1] = new PointF(x, y);
    }

    // right control points
    for (int i = 0; i < pMax - 1; i++)
    {
        float x = points[i].X + hGaps[i] / 2f;
        float y = points[i].Y;
        if (i > 0 && signs[i-1] == signs[i])
        {
            float m = (grads[i-1] + grads[i]) / 2f;
            y = points[i].Y + hGaps[i] / 2f * m  * vGaps[i] / lengs[i];
        }
        bezzA[i * 3 + 1] = new PointF(x, y);
    }
    return bezzA.ToList();
}

Note that I didn't code for the case of points with the same x-coordinate. So this is ok for 'functional graphs' but not for, say figures, like e.g. stars..

0
Kenzi On

Maybe you just want to look at the "overshooting the bounds" problem as not a problem with the overshoot, but with the bounds. In which case, you can determine the actual bounds of a curve using the System.Drawing.Drawing2D.GraphicsPath object:

GraphicsPath gp = new GraphicsPath();
gp.AddCurve(listOfPoints);
RectangleF bounds = gp.GetBounds();

You can draw that GraphicsPath directly:

graphics.DrawPath(Pens.Black, gp);

As far as solving the bounds problem, the line necessarily overshoots the vertex on some axis. It's easier to see this fact when the lines are aligned to the bounds.

Given these points:

In order for them to be curved, they must exceed their bounds in some way:

If you never want to exceed their vertical bounds, you could simply ensure that the bezier handles have the same Y value as the vertex, but they will overshoot on the X:

Or vice-versa:

You could deliberately undershoot just enough to avoid the way curves can overshoot. This can be done by swapping the bezier handles, which would maybe be at the line-centers, with the vertices: