Download and graph COVID-19 case data in C#


This example shows how to download the most recent COVID-19 data and graph it. It demonstrates the following useful techniques.

  • Downloading a file once per day
  • Reading data from a CSV file
  • Creating country data
  • Using different comparers to sort in multiple ways
  • Drawing the graph
  • Displaying data tooltips

This is just a basic example to get things going. Later examples will try to display different data in various ways to get a better understanding of what’s going on in the world.

(I apologize now that this is a fairly long post. I want to include a lot of the code even though some of it has been presented in previous posts.)

Downloading Data

There are two main issues here. First, how do you download the data. Second, how do you download the data only once per day so you don’t bomb the site providing it.

For this example, I decided to download the data into a file named after today’s date. When the program starts, it looks to see if that file is present. If the file is already there, the program uses it. If the file is missing, the program downloads the data.

The data used by this program is from The Humanitarian Data Exchange (HDX). For information about this particular data set, go to Novel Coronavirus (COVID-19) Cases Data. That data is updated daily, so this program only loads the data once per day.

Depending on your timezone and the time when they update the data, you might download yesterday’s data today. If that happens, you can simply delete the data file and try again later. In a production application, you might want to compare the files to see if the data has changed. Depending on how the server makes the data files available, you might also be able to query the site to get the data’s date and then download it if it has changed.

Anyway, the following LoadData method controls the data loading process.

// Load and prepare the data.
private void LoadData()
    // Compose the local data file name.
    string filename = "data" + DateTime.Now.ToString("yyyy_MM_dd") + ".csv";

    // Download today's data.

    // Read the file.
    object[,] fields = LoadCsv(filename);

    // Create the country data.

This method just calls DownloadData, LoadCsv, and CreateCountryData to do all of the work.

The following code shows the DownloadFile method downloads today’s data.

// Download today's data.
private void DownloadFile(string filename)
    // See if we have today's file.
    if (!File.Exists(filename))
        // Download the file.
        this.Cursor = Cursors.WaitCursor;

            // Make a WebClient.
            WebClient web_client = new WebClient();

            // Download the file.
            const string url = "";
            web_client.DownloadFile(url, filename);
        catch (Exception ex)
            MessageBox.Show(ex.Message, "Download Error",
                MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            this.Cursor = Cursors.Default;

The method takes as a parameter the name of today’s file, which has the format data2020_02_01.csv. It uses File.Exists to see if the file already exists. If the file is missing, the program creates a WebClient and uses its DownloadFile method to download the data. The following shows the full path to the data.

The data looks like this:


The program loops through the first row starting at the fifth column to get the dates.

It then loops through the rows starting at the second row to get the country data. For each row, it reads the country’s name and loops through the columns (starting with the fifth column) to get the numbers of cases.

As it loops through the rows, the program combines entries for the same country. For example, it adds up the values for China and places the result in one object.

Reading CSV Data

Usually it’s not too hard to load a CSV (Comma-Separated Value) file as a string and then parse the string. Unfortunately one value in the file (Korea, South) contain a comma. To indicate that the comma is part of the field and not a delimiter, the file encloses that value in double quotes.

You could modify the program to deal with that, but (a) that would be more work, and (b) it wouldn’t handle any other weird situation that might pop up. To avoid that, I decided to use an Excel server to load the file.

To do that, you need to add a reference to Excel to the program. In Solution Explorer, right-click References and select Add Reference. On the COM tab, add a reference to Microsoft Excel 14.0 Object Library (or whatever version you have).

Now the program can use the following method to load the file into a two-dimensional array.

// Load a CSV file into a 1-based array.
private object[,] LoadCsv(string filename)
    // Get the Excel application object.
    Excel.Application excel_app = new Excel.ApplicationClass();

    // Make Excel visible (optional).
    //excel_app.Visible = true;

    // Open the workbook read-only.
    filename = Application.StartupPath + "\\" + filename;
    Excel.Workbook workbook = excel_app.Workbooks.Open(
        Type.Missing, true, Type.Missing, Type.Missing,
        Type.Missing, Type.Missing, Type.Missing, Type.Missing,
        Type.Missing, Type.Missing, Type.Missing, Type.Missing,
        Type.Missing, Type.Missing);

    // Get the first worksheet.
    Excel.Worksheet sheet = (Excel.Worksheet)workbook.Sheets[1];

    // Get the used range.
    Excel.Range used_range = sheet.UsedRange;

    // Get the sheet's values.
    object[,] values = (object[,])used_range.Value2;

    // Close the workbook without saving changes.
    workbook.Close(false, Type.Missing, Type.Missing);

    // Close the Excel server.

    return values;

This method creates an Excel application object. It composes the file’s full path and uses the Excel object’s Workbooks.Open method to open the file.

The code then gets the workbook’s first worksheet. It uses the UsedRange value to get a Range representing the cells in the worksheet that are used. It then calls the range’s Value2 method to return an array containing the values.

Note that Excel starts indexing the array at index 1 so the first item in the array is at position values[1, 1].

Creating Country Data

The program stores data in the CountryData class. The following code shows the pieces of the class that store the data. I’ll show the rest of the class later.

public class CountryData
    static public DateTime[] Dates = null;
    public string Name = null;
    public int[] Cases = null;
    public int MaxCases = 0;
    public int CountryNumber = -1;

    public PointF[] DeviceCoords = null;

    public override string ToString()
        return Name;

    public void SetMax()
        MaxCases = Cases.Max();

The following list summarizes the class’s data fields.

  • Dates – The dates for the data points
  • Name – The country’s name
  • Cases – The number of COVID-19 cases for the corresponding date
  • MaxCases – The maximum number of cases for this country on any date
  • CountryNumber – Just a number
  • DeviceCoords – The country’s data points transformed into bitmap coordinates

The Dates array is declared static so it is shared by all of the CountryData objects.

The program only uses the countries’ numbers to calculate a color for each country. (You’ll see that later.) The number doesn’t really matter as long as it doesn’t change for each country.

The DeviceCoords array holds the country’s data values converted into bitmap coordinates. The program uses those values to tell if the mouse (which has location in bitmap coordinates) is over the country’s data.

The ToString method returns the country’s name. The program’s CheckedListBox automatically uses this to display each CountryData object. (Comment out this method to see what happens if you don’t override ToString).

The program calls the SetMax method after a country’s data has been loaded. That method uses the Max LINQ extension method to find the largest value in the Cases array.

The following CreateCountryData method uses the CSV data to create CountryData objects.

// Create the country data.
private void CreateCountryData(object[,] fields)
    // Load the dates.
    Dictionary<string, CountryData< country_dict =
        new Dictionary<string, CountryData<();
    const int first_date_col = 5;
    int max_row = fields.GetUpperBound(0);
    int max_col = fields.GetUpperBound(1);
    int num_dates = max_col - first_date_col + 1;
    CountryData.Dates = new DateTime[num_dates];
    for (int col = 1; col <= num_dates; col++)
        // Convert the date into a double and then into a date.
        double double_value = (double)fields[1, col + first_date_col - 1];
        CountryData.Dates[col - 1] =

    // Load the country data.
    const int country_col = 2;
    for (int country_num = 2; country_num <= max_row; country_num++)
        // Get the country's name.
        string country_name = fields[country_num, country_col].ToString();

        // Get or create the country's CountryData object.
        CountryData country_data;
        if (country_dict.ContainsKey(country_name))
            country_data = country_dict[country_name];
            country_data = new CountryData();
            country_data.Name = country_name;
            country_data.Cases = new int[num_dates];
            country_dict.Add(country_name, country_data);

        // Add to the country's data.
        for (int col = 1; col <= num_dates; col++)
            // Add the value to the country's total.
            country_data.Cases[col - 1] +=
                (int)(double)fields[country_num, col + first_date_col - 1];

    // Convert CountryDict into CountryList.
    CountryList = country_dict.Values.ToList();

    // Set MaxCases values.
    foreach (CountryData country in CountryList)

    // Sort.

    // Number the countries and set MaxCases values.
    for (int i = 0; i < CountryList.Count; i++)
        CountryList[i].CountryNumber = i;

The method first creates a dictionary to hold the data. It allocates the Dates array and then loops through the data’s first row to read the dates.

Excel represents dates as a double-precision offset from the base date, which is midnight, 30 December 1899. (Because why not use that as the base date?) The program casts the value into an object and then uses DateTime.FromOADate to convert the double into a date.

After reading the dates, the program loops through the country data rows. For each row it reads the country name and looks up the country in the dictionary. If the country is not present, the program creates a new CountryData object and adds it to the dictionary.

The program then loops through the row’s case data, adding the values to the CountryData object’s Cases array. Excel stores numeric values a doubles, so the program converts the generic object into a double. Because the case counts are in fact integers, the code converts the double result into an integer and then adds it to the country’s total for that date.

After it finishes loading the country data, the program pulls the dictionary’s values out into a list. It then loops through the countries and calls their SetMax values so we can later use the maximum number of cases for each country without needing to calculate them again.

The program them sorts the countries using a Comparer object to determine the sort order. I’ll explain that object next.

Finally, the program loops through the countries and sets their CountryNumber values. Those values to not change later even if the list is resorted in a different order.


When you call a list’s Sort method, you can pass it a comparer object to tell the method how to sort items.

The comparer class must implement the IComparer interface so the Sort method can use it. The following code shows the CountryDataComparer class that this program uses.

public class CountryDataComparer : IComparer<CountryData>
    public enum CompareTypes
    private CompareTypes CompareType = CompareTypes.ByName;

    public CountryDataComparer(CompareTypes type)
        CompareType = type;

    public int Compare(CountryData x, CountryData y)
        switch (CompareType)
            case CompareTypes.ByName:
                return x.Name.CompareTo(y.Name);
            case CompareTypes.ByMaxCases:
                return -x.MaxCases.CompareTo(y.MaxCases);

This class defines an enumeration that the program can use to specify the sort type. These objects can sort by country name or maximum number of cases.

The variable CompareType indicates which comparison type a particular comparer object should use. The class’s constructor sets the object’s comparison type.

The Compare method required by the IComparer interface checks the object’s CompareType and then compares the two objects either by name or MaxCases value.

The program declares its comparer object like this.

private CountryDataComparer Comparer =
    new CountryDataComparer(CountryDataComparer.CompareTypes.ByMaxCases);

This means the program initially sorts countries by maximum number of cases.

When you click one of the radio buttons, an event handler changes the comparer. The following code shows how the program creates the new comparer when you click the By Name button.

private void radSortByName_Click(object sender, EventArgs e)
    if (CountryList == null) return;
    Comparer = new CountryDataComparer(CountryDataComparer.CompareTypes.ByName);
    clbCountries.DataSource = null;
    clbCountries.DataSource = CountryList;

This code sets the Comparer variable equal to a new comparer that sorts by name. It then sets the CheckedListBox control’s data source to null, re-sorts the list of data, sets the CheckedListBox control’s data source back to the list, and then redraws the graph. (I’ll get to how that works that eventually.)

Drawing the Graph

The following method draws the graph for the countries selected in the CheckedListBox.

// Draw the graph.
private void GraphCountries()
    ClosePoint = new PointF(-1, -1);
    if (SelectedCountries.Count == 0)
        picGraph.Image = null;

    // Get the maximum value.
    float y_max = SelectedCountries.Max(country => country.Cases.Max());

    // Create a transformation to make the data fit the PictureBox.
    DefineTransform(SelectedCountries, y_max);

    // Create a bitmap.
    Bitmap bm = new Bitmap(
    using (Graphics gr = Graphics.FromImage(bm))
        gr.SmoothingMode = SmoothingMode.AntiAlias;
        gr.TextRenderingHint = TextRenderingHint.AntiAlias;
        gr.Transform = Transform;

        // Draw the axes.

        // Draw the curves.
        Color[] colors =
            Color.Red, Color.Green, Color.Blue, Color.Black,
            Color.Cyan, Color.Orange,
        int num_colors = colors.Length;
        using (Pen pen = new Pen(Color.Black, 0))
            foreach (CountryData country in SelectedCountries)
                pen.Color = colors[country.CountryNumber % num_colors];
                country.Draw(gr, pen, Transform);

    // Display the result.
    picGraph.Image = bm;

The method first sets ClosePoint to (-1, -1). That point indicates the point on the graph where the mouse is an initially the program assumes the mouse is not on the graph.

Next if no countries are selected, the method sets the picGraph PictureBox control’s Image to null and returns.

The code then uses the LINQ Max extension method to get the largest maximum number of cases from the objects in the SelectedCountries list and calls the DefineTransform method (shown shortly) to make a transformation so it can draw using the data’s values and the result will be mapped onto the drawing surface.

The method then creates a Bitmap large enough to fill the PictureBox and makes an associated Graphics object. The program sets a few Graphics object properties. In particular it sets the object’s Transform property to a matrix named Transform. You’ll see how that is defined next.

The method then calls the DrawAxes method to draw the X and Y axes and defines the colors that it will use to draw the countries’ data curves.

It then creates a pen with thickness 0. The value 0 tells the drawing methods to draw the line as thinly as possible (one pixel wide on a monitor) no matter what transformation might be in place that would otherwise scale the pen.

The program loops through the selected countries and sets the pen’s color to a value taken from the colors array. The index in the array is given by the country’s taken modulus the number of colors. This is why we set the country number earlier. That number doesn’t change so the country’s color won’t change even if you resort the countries or change the countries that are selected.

The code calls each country’s Draw method to do the actually drawing. After the bitmap is complete, the program displays it.


The following code shows the DefineTransform method.

private void DefineTransform(List<CountryData> country_list, float y_max)
    int num_cases = country_list[0].Cases.Length;
    WorldBounds = new RectangleF(0, 0, num_cases, y_max);
    int wid = picGraph.ClientSize.Width;
    int hgt = picGraph.ClientSize.Height - 1;
    const int margin = 4;
    PointF[] dest_points =
        new PointF(margin, hgt - margin),
        new PointF(wid - margin, hgt - margin),
        new PointF(margin, margin),
    Transform = new Matrix(WorldBounds, dest_points);
    InverseTransform = Transform.Clone();

This method defines a rectangle that bounds the data values. It then creates an array of points that defines corners of the bitmap. The code then uses the rectangle and array to define a matrix named Transform that maps the data coordinates onto the bitmap.

That transformation allows the program to draw in data coordinates and have the result appear in bitmap coordinates. Sometimes the program needs to do the reverse, so the program creates a new matrix named InverseTransform that is a copy of the first one and inverts the copy.


The DrawAxes method is mostly straightforward so I won’t show most of it here, but it does two interesting things in addition to drawing a bunch of lines.

First, it must decide how tall to make the graph and how frequently to draw horizontal lines. The following code snippet does that.

// Calculate the Y step value.
// Find the largest power of 10 less than y_max.
float y_max = WorldBounds.Bottom;
int power = (int)Math.Log10(y_max);
// If this is more than 1/2 of y_max, use the next smaller power.
if (Math.Pow(10, power) > y_max / 2) power--;
int y_step = (int)Math.Pow(10, power);

This code first gets the data’s largest Y value, takes the logarithm base 10, and truncates the result. For example, if the largest value is 120,000, the logarithm is 5.08, and truncating that gives 5.

Next the program raises ten to this power, in this example giving 100,000. If that value is greater than half of the maximum Y value, the program subtracts one from the power. In this example 100,000 is more than half of 120,000, so we subtract one.

The code then raises ten to the final power to get the distance between the horizontal lines, 10,000 in this example.

The result is that the y_step value provides at least two divisions that are powers of ten below the maximum value. In this example the horizontal lines are 10,000 apart.

The second interesting thing that the DrawAxes method does is it draws the tick marks on the X axis in pixels. If you just drew the tick marks in data coordinates, then the Graphics object’s transformation would scale the marks and produce unpredictable results.

The following snippet ensures that the the tick marks are ten pixels tall.

// Calculate the tick mark size.
const int tick_y_pixels = 5;
PointF[] tick_points = { new PointF(0, tick_y_pixels) };
float tick_y = -tick_points[0].Y;

// Draw tick marks.
for (int i = 0; i < num_cases; i++)
    gr.DrawLine(pen, i, -tick_y, i, tick_y);

This code creates an array holding a single PointF with Y coordinate equal to half of the desired tick mark length. It the calls the InverseTransform matrix’s TransformVectors method to transform the PointF back from bitmap coordinates to data coordinates.

A matrix has two methods that can apply a transformation to an array of PointF. The TransformPoints method treats the values as points. It will scale, rotate, and translate the values as if they were points.

The TransformVectors method treats the values as vectors. Vectors have a direction and length but not a location. For example, the vector pointing from point (2, 3) to point (4, 6) has components <4 – 2, 6 – 3> = <2, 3>. When the TransformVectors method acts on a vector, it rotates and scales the vector, but does not translate it. That is useful in this example because all I really want is the length of the result. If we used TransformPoints, then the point would be translated so it’s Y value would be modified and we could not use it to find the length of a tick mark. Basically we just want to scale the Y value 5 to see how long the tick mark should be.

(Another approach would be to give the array two points with Y coordinates that differ by 5, use TransformPoints, and then subtract their resulting Y coordinates to see how far apart they are in the data coordinate system.)

After figuring out how far five pixels in bitmap coordinates is in data coordinates, the method simply loops through the X values (the dates) and draws their tick marks.

Download the example to see the rest of the DrawAxes method.


The CountryData class’s Draw method shown in the following code draws a country’s data.

// Draw this country's data.
public void Draw(Graphics gr, Pen pen, Matrix transform)
    // Make the array of points.
    int num_cases = Cases.Length;
    PointF[] points = new PointF[num_cases];
    for (int i = 0; i < num_cases; i++)
        points[i] = new PointF(i, Cases[i]);

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

    // Find device coordinates for tooltips.
    DeviceCoords = points;

This code loops through the country’s data and creates a point representing each value. It then calls DrawLines to connect the points. The Graphics object’s transformation automatically transforms the values to the bitmap’s coordinate system.

The method then transforms the points into bitmap coordinates and saves them in the country’s DeviceCoords variable. (Recall that the program uses those coordinates to determine whether the mouse is over the curve.)

Displaying Data Tooltips

The last piece of the program that I want to describe determines when the mouse is over a curve. The following code starts the process when the mouse moves over the graph.

private void picGraph_MouseMove(object sender, MouseEventArgs e)

private void SetTooltip(PointF point)
    if (picGraph.Image == null) return;
    if (SelectedCountries == null) return;

    string new_tip = "";
    int day_num;
    int num_cases;
    foreach (CountryData country in SelectedCountries)
        if (country.PointIsAt(point, out day_num,
            out num_cases, out ClosePoint))
            new_tip = country.Name + "\n" +
                CountryData.Dates[day_num].ToShortDateString() + "\n" +
                num_cases.ToString("n0") + " cases";

    if (tipGraph.GetToolTip(picGraph) != new_tip)
        tipGraph.SetToolTip(picGraph, new_tip);

When the mouse moves, the MouseMove event handler simply calls the SetTooltip method.

The SetTooltip method returns if there is no graph yet or if you have not selected any countries.

If the method doesn’t immediately exit, it sets new_tip equal to a blank string. It then loops through the selected countries and calls their PointIsAt methods (described shortly) to see if the mouse is over the country’s data. If PointIsAt returns true, the code sets new_tip to an appropriate tooltip value and breaks out of the loop.

After the loop ends, the program compares the PictureBox control’s current tooltip to the one stored in new_tip. If the two are different, the program updates the displayed tooltip.

The method finishes by refreshing the PictureBox to circle the point under the mouse. I’ll describe that shortly, but first here’s the CountryData class’s PointIsAt method.

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

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

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

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

This method loops through adjacent pairs of the country’s data points. For each pair it calls the FindDistanceToSegment method to see how far the point is from the line segment connecting the points. See the post Find the shortest distance between a point and a line segment in C# to learn how that method works.

If the point is within four pixels of the segment, the mouse is close to the current segment. The program copmares the distances between the mouse’s location and the segment’s two end points and sets day_num to the index of the closer end point. It then saves the corresponding number of cases in the num_cases output parameter.

The code also saves the point that the program should circle in the output parameter close_point. Depending on which line of code you comment out, that might be the point on the segment or the closer end point.

The method then returns true to let the calling method know that the point is near the data.


The last piece of code that I want to discuss is the PictureBox control’s Paint event handler. The program’s MouseMove event handler refreshes the PictureBox to circle the point on the graph that is close to the mouse’s position.

The following code shows the Paint event handler.

private void picGraph_Paint(object sender, PaintEventArgs e)
    if (ClosePoint.X < 0) return;

    const int radius = 3;
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    float x = ClosePoint.X - radius;
    float y = ClosePoint.Y - radius;
    e.Graphics.FillEllipse(Brushes.White, x, y, 2 * radius, 2 * radius);
    e.Graphics.DrawEllipse(Pens.Red, x, y, 2 * radius, 2 * radius);

Note that this method does not call the Graphics object’s Clear method. The PictureBox control’s Image property is displaying the graph. If the program called Clear, it would erase that image.

If the variable ClosePoint has a negative X coordinate, then the Paint event handler simply exits. Otherwise it draws a circle centered at the point.

The result is that you should see a small circle at the data point and a tool tip indicating the country, date, and number of cases under the mouse. For example, the tooltip in the picture at the top of this post indicates that the United States had 188,172 COVID-19 cases on March 31, 2020.


Again I apologize for this post’s length. The program’s pieces are not too complicated, but there are a lot of them.

Please download the example and experiment with it to understand more about how different countries have handles the COVID-19 pandemic so far. The graph makes some things immediately obvious. For example, China and South Korea have both flattened their case curves dramatically. Other countries that acted less quickly or less strongly, such as Italy, Spain, and the United Kingdom, still have increasing numbers of cases. And some countries such as the United States and the United Kingdom have curves that are still upward curving, indicating that they are still experiencing exponential growth in their numbers of cases.

For now, look the program over, see how it works, and study the data. If you find any interesting patterns, please note them in the comments below.

Later posts will add other ways to study the data such as looking at different values like deaths, mortality ratios, and cases or deaths per million population. If you like, you can try to make some of those modifications yourself before I have a chance to post my versions.

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

5 Responses to Download and graph COVID-19 case data in C#

  1. Edmundo Ralickas says:

    Hi Rod,
    This is a great application! Thank you for sharing it. I am looking forward to the next post.

    When using it, the application freezes once one presses All to get all curves for all Countries and then None. The graph shows a step ladder with two steps.
    I compiled your source code with VS2019 Community.

    • RodStephens says:

      Nice catch! I’ll look into it.

      • RodStephens says:

        I have fixed this. Thanks again for pointing it out.

        For anyone who cares, I’ll explain what was happening. There is actually a useful lesson to be learned.

        There were two bad things happening. First, when you clicked All, the program unchecked all of the countries. Each time it unchecked a country, that fired the CheckedListBox control’s ItemCheck event so the program redraw the graph. That made it flicker wildly as it drew the new graph again and again. Not the end of the world, but annoying and inefficient. A similar thing happened when you clicked None.

        To prevent this, I added a variable named IgnoreItemCheck. The All and None buttons set that value to true, check or uncheck all of the countries, and the set the value back to false before redrawing the graph. Now the clbCountries_ItemCheck event handler checks that value and returns without doing anything if it is true. Now the program only redraws the graph once when it is done checking or unchecking all of the countries.

        The second issue occurred when you clicked None and the program was repeatedly redrawing the graph. Eventually only a few countries were selected and they had very few cases. When that happened, the y_step value used to draw tick marks became zero and the program basically got stuck in a loop where it added zero to its looping variable. I just added a test that ensures that y_step is at least 1.

        Now this issue at least seems to be resolved.

  2. Pingback: Align graphs of COVID-19 case data in C# - C# HelperC# Helper

Comments are closed.