-1

Using PDFBox, I have created a line chart to plot some data, and it looks much like any general line chart you will see through a google search. It also looks identical to the line chart I've attached to this question. The way the line chart drawing algorithm works is that it looks at the current point, and then the next one, and a line is drawn if a valid point is found there.

My problem is that a client does not like how sharply the lines connect with each other. Instead, they want the joins between the lines to happen in more of a curved fashion. Attached is an image of a rough idea of what the client wants. Note that although the lines look very curvy, the client specifically cares about the line joins themselves being curvy, and not sharp like in a standard line chart.

So far, I have tried using Bézier curves, but I can't seem to find the right values to make it scale right for all the different magnitudes between the points. I first tried changing the line cap and line join styles, but this did not produce the desired "cuvyness" between the line joins. I have also contemplated using paths to achieve this result, but I haven't managed to figure out how to proceed.

Is there something that I'm missing that could make it easier to draw these lines? If not, can anyone help me figure out the right Bézier values/paths to achieve these curves? Thanks in advance for any suggestions/code examples.

Due to an NDA, I cannot give a code example that shows how the chart is drawn and plotted (this would give up our algorithm entirely). All that I can say is that I created an internal representation for how the data should be plotted in the chart, and this system is very roughly translated in the provided image. I can say that the function that plots the data exclusively uses the PDPageContentStream classes's lineTo and strokeTo functions, after an initial moveTo to the position of the starting point based on our internal coordinate representation.

A rough idea of the curves the client wants.

maht33n
  • 35
  • 5
  • 1
    A proper [mre] of a sample chart you've done with dummy data will help us to get started on the code. Please provide one – Frakcool Aug 04 '20 at 20:39
  • I can't do that due to an NDA. There are also way too many moving parts that draw the chart anyway. What I need to know is how to curve the lines, which I think can be done outside the context of the chart the data is plotted on. I think the provided image gives good context of dummy data for the chart. – maht33n Aug 04 '20 at 20:44
  • I'm not asking for your actual code, but for a brand new code that isolates the issue. A program that creates a chart with dummy data (random numbers is fine). Very little people are going to try and test something without code from you – Frakcool Aug 04 '20 at 20:47
  • I updated the question. The plotting is exclusively done using the PDPageContentStream class's lineTo and strokeTo functions. That's why the joins look sharp. I need to know if there is a way to join them in a curvier fashion. – maht33n Aug 04 '20 at 21:01
  • If you use **moveTo** and **lineTo** only, it obviously doesn't become curvy (unless you do it in microsteps). You indeed should use Bezier curves instead, at least near the joins. For more help a helper needs more information. – mkl Aug 05 '20 at 04:56
  • I've realized that my question lacks focus, and that my approach has made it hard for people to help me out. I have three ways in which I will try to generate the curvature on these lines. If one of these ways works, then I will update this question with a solution. Otherwise, I will post a new question (with code examples showing how the lines are drawn) and seek your help in getting a smooth curvature between the points/line joins. I appreciate your feedback, I'm thinking I'll be ready to share my findings in a day or so. – maht33n Aug 06 '20 at 19:01

1 Answers1

2

---A quick "solution" is to use round line joins instead of miter joins (the default) --- it seems that I missed this.

The charts in your sample probably use curve interpolation and this question and answers might help you: How does polyline simplification in Adobe Illustrator work?

The code below shows how to transform a list of lines into Bezier connected lines (it's C# but it can be converted to Java with minimal changes):

/// <summary>
/// Draws the Bezier connected lines on the page.
/// </summary>
/// <param name="page">Page where to draw the lines.</param>
/// <param name="points">List of points representing the connected lines.</param>
/// <param name="pen">Pen to draw the final path.</param>
/// <param name="smoothFactor">Smooth factor for computing the Bezier curve</param>
/// <param name="font"></param>
private static void DrawBezierConnectedLines(PDFPage page, PDFPoint[] points, PDFPen pen, double smoothFactor, PDFFont font)
{

    PDFPath path = new PDFPath();
    path.StartSubpath(points[0].X, points[0].Y);

    for (int i = 0; i < points.Length - 2; i++)
    {
        PDFPoint[] pts = ComputeBezierConnectedLines(points[i], points[i + 1], points[i + 2], smoothFactor, i == 0, i == points.Length - 3);
        switch (pts.Length)
        {
            case 2: // Intermediate/last section - straight lines
                path.AddLineTo(pts[0].X, pts[0].Y);
                path.AddLineTo(pts[1].X, pts[1].Y);
                break;
            case 3: // First section - straight lines
                path.AddLineTo(pts[0].X, pts[0].Y);
                path.AddLineTo(pts[1].X, pts[1].Y);
                path.AddLineTo(pts[2].X, pts[2].Y);
                break;
            case 4: // Intermediate/last section
                path.AddLineTo(pts[0].X, pts[0].Y);
                path.AddBezierTo(pts[1].X, pts[1].Y, pts[1].X, pts[1].Y, pts[2].X, pts[2].Y);
                path.AddLineTo(pts[3].X, pts[3].Y);
                break;
            case 5: // First section
                path.AddLineTo(pts[0].X, pts[0].Y);
                path.AddLineTo(pts[1].X, pts[1].Y);
                path.AddBezierTo(pts[2].X, pts[2].Y, pts[2].X, pts[2].Y, pts[3].X, pts[3].Y);
                path.AddLineTo(pts[4].X, pts[4].Y);
                break;
        }
    }

    page.Canvas.DrawPath(pen, path);

    page.Canvas.DrawString($"Smooth factor = {smoothFactor}", font, new PDFBrush(), points[points.Length - 1].X, points[0].Y);
}

/// <summary>
/// Given a sequence of 3 consecutive points representing 2 connected lines the method computes the points required to display the new lines and the connecting curve.
/// </summary>
/// <param name="pt1">First point</param>
/// <param name="pt2">Second point</param>
/// <param name="pt3">Third point</param>
/// <param name="smoothFactor">Smooth factor for computing the Bezier curve</param>
/// <param name="isFirstSection">True if the points are the first 3 in the list of points</param>
/// <param name="isLastSection">True if the 3 points are last 3 in the list of points.</param>
/// <returns>A list of points representing the new lines and the connecting curve.</returns>
/// <remarks>The method returns 5 points if this is the first section, points that represent the first line, connecting curve and last line.
/// If this is not the first section the method returns 4 points representing the connecting curve and the last line.</remarks>
private static PDFPoint[] ComputeBezierConnectedLines(PDFPoint pt1, PDFPoint pt2, PDFPoint pt3, double smoothFactor, bool isFirstSection, bool isLastSection)
{
    PDFPoint[] outputPoints = null;

    if (smoothFactor > 0.5)
    {
        smoothFactor = 0.5; // Half line maximum
    }
    if (((pt1.X == pt2.X) && (pt2.X == pt3.X)) || // Vertical lines
        ((pt1.Y == pt2.Y) && (pt2.Y == pt3.Y)) || // Horizontal lines
        (smoothFactor == 0))
    {
        if (!isFirstSection)
        {
            pt1 = ComputeIntermediatePoint(pt1, pt2, smoothFactor, false);
        }
        if (!isLastSection)
        {
            pt3 = ComputeIntermediatePoint(pt2, pt3, smoothFactor, true);
        }
        if (isFirstSection)
        {
            outputPoints = new PDFPoint[] { pt1, pt2, pt3 };
        }
        else
        {
            outputPoints = new PDFPoint[] { pt2, pt3 };
        }
    }
    else
    {
        PDFPoint startPoint = new PDFPoint(pt1);
        if (!isFirstSection)
        {
            startPoint = ComputeIntermediatePoint(pt1, pt2, smoothFactor, false);
        }
        PDFPoint firstIntermediaryPoint = ComputeIntermediatePoint(pt1, pt2, smoothFactor, true);
        PDFPoint secondIntermediaryPoint = new PDFPoint(pt2);
        PDFPoint thirdIntermediaryPoint = ComputeIntermediatePoint(pt2, pt3, smoothFactor, false);
        PDFPoint endPoint = new PDFPoint(pt3);
        if (!isLastSection)
        {
            endPoint = ComputeIntermediatePoint(pt2, pt3, smoothFactor, true);
        }

        if (isFirstSection)
        {
            outputPoints = new PDFPoint[] { startPoint, firstIntermediaryPoint, secondIntermediaryPoint, thirdIntermediaryPoint, endPoint };
        }
        else
        {
            outputPoints = new PDFPoint[] { firstIntermediaryPoint, secondIntermediaryPoint, thirdIntermediaryPoint, endPoint };
        }
    }

    return outputPoints;
}

/// <summary>
/// Given the line from pt1 to pt2 the method computes an intermediary point on the line.
/// </summary>
/// <param name="pt1">Start point</param>
/// <param name="pt2">End point</param>
/// <param name="smoothFactor">Smooth factor specifying how from from the line end the intermediary point is located.</param>
/// <param name="isEndLocation">True if the intermediary point should be computed relative to end point, 
/// false if the intermediary point should be computed relative to start point.</param>
/// <returns>A point on the line defined by pt1->pt2</returns>
private static PDFPoint ComputeIntermediatePoint(PDFPoint pt1, PDFPoint pt2, double smoothFactor, bool isEndLocation)
{
    if (isEndLocation)
    {
        smoothFactor = 1 - smoothFactor;
    }

    PDFPoint intermediate = new PDFPoint();
    if (pt1.X == pt2.X)
    {
        intermediate.X = pt1.X;
        intermediate.Y = pt1.Y + (pt2.Y - pt1.Y) * smoothFactor;
    }
    else
    {
        intermediate.X = pt1.X + (pt2.X - pt1.X) * smoothFactor;
        intermediate.Y = (intermediate.X * (pt2.Y - pt1.Y) + (pt2.X * pt1.Y - pt1.X * pt2.Y)) / (pt2.X - pt1.X);
    }

    return intermediate;
}

For this set of points:

PDFPoint[] points = new PDFPoint[] {
    new PDFPoint(50, 150), new PDFPoint(100, 200), new PDFPoint(150, 50), new PDFPoint(200, 150), new PDFPoint(250, 50) };
DrawBezierConnectedLines(page, points, pen, 0, helvetica);

this the result: enter image description here

The corresponding PDF file can be downloaded here: https://github.com/o2solutions/pdf4net/blob/master/GettingStarted/BezierConnectedLines/BezierConnectedLines.pdf

iPDFdev
  • 5,229
  • 2
  • 17
  • 18
  • 1
    In the question the OP says "I first tried changing the line cap and line join styles, but this did not produce the desired "cuvyness" between the line joins." – mkl Aug 06 '20 at 08:20
  • Indeed, I was hoping it would be as simple as using the round joins, but using them still results in too sharp of an edge for the client. The line joins do look round though, but they are not curvy enough. – maht33n Aug 06 '20 at 19:03
  • @iPDFdev: Thank you so much for this algorithm!!!!! I will try it out this weekend and let you know how it goes. – maht33n Aug 07 '20 at 17:18
  • It's been a while but here is an update.Thank you so much iPDFdev. This algorithm worked very well. For curves with very sharp angles, the curves sometimes did not actually cross the required point. To fix this, I used the function of a Bezier curve to correctly predict when this would happen, and add an offset until the curve actually crossed the point. Thanks again!! – maht33n Sep 29 '20 at 18:32