Align graphs of COVID-19 case data in C#


[COVID-19]

The example Download and graph COVID-19 case data in C# shows how to graph different countries’ COVID-19 case data. The graphs are informative. For example, they can show you whether a country’s graph is upward bending or whether it has started to level off.

Unfortunately that program makes some comparisons difficult. For example, the following graph shows the case data for Belgium and New Zealand.


[COVID-19]

Belgium’s curve is on the top so it looks like it is doing much worse than New Zealand. However, Belgium recorded its first case of COVID-19 on February 4 but New Zealand didn’t find a case until more than three weeks later on February 28. It could be that Belgium has more cases because the virus has been spreading there longer.

This example lets you align graphs so each one’s left edge shows the day where that country recorded a certain number of cases. You can set the number of cases to one or some larger value. You can also set it to zero to get a result similar to the previous example.

The CountryData Class

The basic idea is that the CountryData class should keep track of the first entry in its case data that is larger than a certain number of cases.

The class stores that value in the new variable FirstDrawnDate.

public int FirstDrawnDate = -1;

For example, if the number of cases that you want to use for alignment is 10, then FirstDrawnDate is the index of the first Cases entry that has a value at least 10.

When the class draws a country’s data, the Draw method sets FirstDrawnDate as shown in the following code.

// Draw this country's data.
public void Draw(int align_cases, Graphics gr, Pen pen, Matrix transform)
{
    // Find the first date with align_cases cases.
    FirstDrawnDate = -1;
    int num_cases = Cases.Length;
    for (int i = 0; i < num_cases; i++)
        if (Cases[i] >= align_cases)
        {
            FirstDrawnDate = i;
            break;
        }

    // Don't draw unless we have at least one day of data left.
    if ((FirstDrawnDate < 0) || (FirstDrawnDate >= num_cases - 1)) return;

    // Make the points.
    List<PointF> point_list = new List<PointF>();
    for (int i = FirstDrawnDate; i < num_cases; i++)
        point_list.Add(new PointF(i - FirstDrawnDate, Cases[i]));
    PointF[] points = point_list.ToArray();

    // Draw the curve.
    gr.DrawLines(pen, points);

    // Find device coordinates for tooltips.
    transform.TransformPoints(points);
    DeviceCoords = points;
}

The method first loops through the case data until it finds the first entry with value greater than the parameter align_cases.

If there are any entries left to draw, the code loops through the cases from that point onward to draw the graph. This part is similar to the previous code except the program subtracts FirstDrawnDate from each of the points’ X coordinates. That moves the graph to the left so the first point is on the left edge of the program’s drawing area.

That’s the only change needed to align the graphs. The class’s PointIsAt method must also be modified so the program can correctly figure out what date is under the mouse. The following code shows the new PointIsAt method.

public bool PointIsAt(PointF device_point, out int day_num,
    out int num_cases, out PointF close_point)
{
    if (FirstDrawnDate >= 0)
    {
        const double close_dist = 4;
        PointF closest;
        for (int i = FirstDrawnDate + 1; i < Cases.Length; i++)
        {
            int coord_num = i - FirstDrawnDate;
            double dist = FindDistanceToSegment(device_point,
                DeviceCoords[coord_num - 1], DeviceCoords[coord_num], out closest);
            if (dist <= close_dist)
            {
                // See whether it is closer to this this
                // segment's left or right point.
                if (DistanceBetweenPoints(DeviceCoords[coord_num - 1], closest) <
                    DistanceBetweenPoints(DeviceCoords[coord_num], closest))
                    coord_num--;
                day_num = coord_num + FirstDrawnDate;
                num_cases = Cases[day_num];

                // Use the point on the segment.
                //close_point = closest;

                // Use the closer segment end point.
                close_point = DeviceCoords[coord_num];
                return true;
            }
        }
    }

    day_num = -1;
    num_cases = -1;
    close_point = new PointF(-1, -1);
    return false;
}

The method first checks the FirstDrawnDate value. If that value is less than zero, then the country’s data does not have any dates with the desired number of cases so the graph is not drawn. In that case, the PointIsAt method does not bother to search for a segment close to the point device_point. Instead it skips to the bottom, sets its return parameters to indicate that it did not find a close point, and returns.

If the graph does have visible points, the code loops variable i over data indices starting at entry number FirstDrawnDate + 1. It examines the segment between that point and the previous point to see if the segment is close to the target point device_point.

Inside the loop, the program sets variable coord_num equal to i minus FirstDrawnDate. That makes coord_num give the entry in the DeviceCoords array corresponding to case entry i. (Recall that DeviceCoords holds the coordinates of the graph’s points on the screen.

If it finds a close point, the program calculates the distance between device_point and the segment’s end point to see which one is closer. If the first point is closer, the program subtracts one from coord_num so it indicates the segment’s left point (in the DeviceCoords array).

To prepare to return, the method sets day_num = coord_num + FirstDrawnDate so day_num indicates the index of the case corresponding to the close point. It also sets num_cases equal to the number of cases on that day and saves the closer segment end point in output parameter close_point.

This may all seem a bit confusing. All it’s really doing is offsetting the array indices by FirstDrawnDate because the graph has been

Main Program

The only changes needed to the main program are in the GraphCountries method. That method uses the following code snippet to get the number of cases that should be used to align the graphs.

// Get the number of cases where we should align countries.
    int align_cases = 0;
    int.TryParse(txtAlignCases.Text, out align_cases);

This code sets align_cases to 0 and the tries to parse the value entered in the txtAlignCases TextBox. If the value doesn’t parse, the value of align_cases remains 0.

Later the method uses the following statement to draw the countries’ graphs.

country.Draw(align_cases, gr, pen, Transform);

The only change here is this statement passes the align_cases value into the Draw method.

Conclusion

The following picture shows the graphs for Belgium and New Zealand when they are aligned at the first days when they saw at least one case. (Recall that Belgium’s graph is the taller one.)


[COVID-19]

In this picture you can see that New Zealand actually has more cases than Belgium had at the same number of day into its epidemic. The shapes of the curves, however, imply that New Zealand may be better off, as long as their curve remains relatively flat.

Download the new example and give it a try. Future COVID-19 posts will make further improvements so we can study the pandemic in other ways.


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

1 Response to Align graphs of COVID-19 case data in C#

  1. Pingback: Graph COVID-19 cases per million in 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.