Skip to main content Skip to footer

How to Build a Time-Lapse Chart Using FlexChart

Visualizing trends in data are one of the most important reasons to add charts in an application. In this blog, we'll discuss how you can create a time-lapse chart that allows viewing the progression of trends over time. Using FlexChart for WinForms, we'll show how the current COVID-19 pandemic has affected various countries across the globe. The following shows a glimpse of the chart in action.

 

In this blog, we'll discuss how to:

  • Add a time-lapse effect to FlexChart
  • Add a highlight and fade effect to the chart series
  • Render custom content on the chart
  • Customize the chart LineMarker

Sourcing Data for a Time-Lapse Chart

The data we use in this blog are sourced from Our World In Data. The CSV data fetched will be parsed as the following POCO entity:

public class CoronaStat
{
    public string Country { get; set; }
    public DateTime Date { get; set; }
    public double TotalCases { get; set; }
}

Each country represents an individual series on the chart, so we'll aggregate the CSV dataset into collections of CoronaStat objects as shown below:

Dictionary<string, List<CoronaStat>> completeData = new Dictionary<string, List<CoronaStat>>();
private void InitCumulativeData()
{
    start = DataService.CoronaStats.Min(x => x.Date);
    end = DataService.CoronaStats.Max(x => x.Date);
    completeData = DataService.CoronaStats.GroupBy(x => x.Country).ToDictionary(
        x => x.Key,
        x => x.ToList()
    );
}

How to Create the Time-Lapse Effect

Before we start with the time-lapse effect, let us first initialize the chart object, which we'll operate upon. The following snippet shows the chart initialization code:

flexChart = new FlexChart()
{
    Dock = DockStyle.Fill,
    BindingX = "Date",
    Binding = "TotalCases",
    ChartType = C1.Chart.ChartType.SplineSymbols,
    Palette = Palette.Darkly,
    BackColor = Color.White,
};
flexChart.Options.InterpolateNulls = true;
flexChart.AnimationSettings = C1.Chart.AnimationSettings.Axes;
flexChart.AnimationUpdate.Duration = 50;

flexChart.AxisX.MajorGrid = true;
flexChart.AxisX.Max = start.AddDays(30).ToOADate();
flexChart.AxisY.Min = 0;
flexChart.AxisY.Max = DataService.CoronaStats.Max(x => x.TotalCases);
flexChart.AxisY.LogBase = 10;

Next, we see how to add a series to each country's chart to be plotted on the chart. Add this series using the following code snippet:

private void AddSeries(string name, DateTime? startDate = null, DateTime? endDate = null)
{
    currentData[name] = new ObservableCollection<CoronaStat>(completeData[name].Where(x=> x.Date>=startDate && x.Date<=endDate));
    var ser = new Series()
    {
        Name = name,
        SymbolSize = 3,
        DataSource = currentData[name]
    };
    flexChart.Series.Add(ser);
}

It is important to note here that we've used an ObservableCollection as the series DataSource. FlexChart tracks ObservableCollection notifications and updates itself to display the changes to the collection. To add the time-lapse effect, we will modify these ObservableCollections on a regular interval of time using a Timer and a RangeSlider.

_timer = new Timer() { Interval = 50 };
_timer.Tick += _timer_Tick;

private void _timer_Tick(object sender, EventArgs e)
{
    if (rangeSlider.UpperValue < end.Subtract(start).TotalDays)
    {
        rangeSlider.UpperValue++;
        UpdateDisplayedSeries();
    }
}
Dictionary<string, ObservableCollection<CoronaStat>> currentData = new Dictionary<string, ObservableCollection<CoronaStat>>();

private void UpdateDisplayedSeries(bool selectionChanged = false)
{
    var startDate = start.AddDays(rangeSlider.LowerValue);
    var endDate = start.AddDays(rangeSlider.UpperValue);

  // Other Code to Add or Remove series as per the selected countries.

  // Add or remove data from Series source as per selected range.
    foreach(var key in currentData.Keys)
    {
        var toRemove = currentData[key].Where(x => x.Date < startDate || x.Date > endDate).ToList();
        toRemove.ForEach(x => currentData[key].Remove(x));

        var toAdd = completeData[key].Where(x => !currentData[key].Contains(x) && x.Date >= startDate && x.Date <= endDate).ToList();

        var toPrepend = toAdd.Where(x => x.Date < currentData[key].FirstOrDefault()?.Date).ToList();
        toPrepend.Reverse();
        toPrepend.ForEach(x => currentData[key].Insert(0, x));

        var toAppend = toAdd.Where(x => currentData[key].LastOrDefault() == null || x.Date > currentData[key].LastOrDefault().Date).ToList();
        toAppend.ForEach(x => currentData[key].Add(x));
    }

    flexChart.AxisX.Min = start.AddDays(rangeSlider.LowerValue).ToOADate();
    flexChart.AxisX.Max = start.AddDays(rangeSlider.UpperValue).ToOADate() + 30;
}

And that's all that is necessary for the time-lapse effect, showing the series moving against time.

 

Add Highlight and Fade Effect to Chart Series

FlexChart offers built-in selection methods that let you select individual series or points on the chart when clicked. We can, however, also create our selection mechanisms. This section will create a Highlight & Fade effect for the chart series using the chart's HitTest and Styling API. Primarily we will use:

The HitTestInfo object returned by the chart's HitTest method gives complete details about the part of a FlexChart control at a specified coordinate. We'll use this info to determine the series that the user hovers over with the mouse. Once we identify the series, use the Series.Style to customize its StrokeColor to add the highlight/fade effect. The following code shows the details:

public static void AddSeriesHighlightEffect(this FlexChart chart)
{
    chart.MouseMove += (s, e) =>
    {
        var hitInfo = chart.HitTest(e.Location);
        var palette = (chart as IPalette);
        var fadeColor = Color.FromArgb(100, Color.LightGray);
        if (hitInfo != null && hitInfo.ChartElement == ChartElement.Legend && hitInfo.Series != null)
        {
            chart.BeginUpdate();
            foreach (var ser in chart.Series)
            {
                ser.Style.StrokeColor = ser.Style.FillColor = hitInfo.Series.Name == ser.Name ?
                Color.FromArgb((int)palette.GetColor(chart.Series.IndexOf(ser))) :
                fadeColor;
            }
            chart.EndUpdate();
        }
        else if(chart.Series.Any(ser=>ser.Style.StrokeColor == fadeColor))
        {
            chart.BeginUpdate();
            foreach (var ser in chart.Series)
            {
                ser.Style.StrokeColor = ser.Style.FillColor = Color.FromArgb((int)palette.GetColor(chart.Series.IndexOf(ser)));
            }
            chart.EndUpdate();
        }
    };
}
 

How to Render Custom Content

The highlight/fade effect is an excellent option to identify a series on the chart plot quickly. However, it does require user interaction. Another opportunity to identify series on the plot without the user interaction would be to add a label on the line head that shows the series Name. The DrawString() method of FlexChart's RenderingEngine can be used to add this label. With the help of FlexChart's RenderingEngine, custom content such as text, images, and shapes can be drawn on the chart plot area. The following snippet shows how to use the SymbolRendering event to draw such content:

public static void AddEndMarker(this Series series, IList source)
{
    series.SymbolRendering += (s, e) =>
    {
        if (e.Index == source.Count - 1)
        {
            e.Engine.DrawPolygon(new double[] { e.Point.X, e.Point.X, e.Point.X + 8 }, new double[] { e.Point.Y - 4, e.Point.Y + 4, e.Point.Y});
            e.Engine.SetFont(new Font("Segoe-UI", 8, FontStyle.Bold));
            e.Engine.DrawString(series.Name, new _Point(e.Point.X + 10, e.Point.Y));
        }
    };
}
 

Customize the Chart LineMarker

LineMarkers are horizontal/vertical lines drawn on the chart plot area and are useful to get the values of one or more data points at a given coordinate. The following shows the code snippet to add a LineMarker to a given chart:

var marker = new LineMarker(chart)
{
    Lines = LineMarkerLines.Vertical,
    LineColor = Color.DarkSlateBlue,
    Interaction = LineMarkerInteraction.Move,
    Content = null,
};

The LineMarker added is usually sufficient in most of the cases and can be customized if needed using its paint event. The following shows combining FlexChart's hit-testing capabilities and the WinForms native graphics with the LineMarker's paint event to customize the marker:

<a name="_bookmark1"></a>marker.Paint += (s, e) =>  
{  
PaintMarkerContent(chart, marker, e.Graphics);  
};  
private static void PaintMarkerContent(FlexChart chart, LineMarker marker, Graphics graphics)  
{  
if (marker.Visible)  
{  
var pointsToDraw = new List<MarkerPoint>();  
var hitTest = chart.HitTest(new Point(marker.X, marker.Y), MeasureOption.X); string maxLengthName = string.Empty;  
string xString = string.Empty;  
if (hitTest != null && hitTest.X != null && hitTest.Distance < 3)  
//Logic for collecting points details for the X value  
}  
pointsToDraw = pointsToDraw.OrderByDescending(x => x.Value).ToList(); if (pointsToDraw.Count > 0)  
{  
var font = new Font("Segoe-UI", 10);  
var itemSize = graphics.MeasureString(maxLengthName, font);  
var size = new SizeF(itemSize.Width + 100, (itemSize.Height + 2) * (pointsToDraw.Count + 1) + 15); var location = new PointF(marker.X + 5, marker.Y - size.Height / 2);  
graphics.FillRectangle(new SolidBrush(Color.White), new RectangleF(location, size));  
graphics.FillRectangle(new SolidBrush(Color.FromArgb(50, Color.DarkSlateBlue)), new RectangleF (location, size));

location.Y += 2;  
graphics.FillRectangle(Brushes.DarkSlateBlue, new RectangleF(location, new SizeF(size.Width, itemSize.Height + 10)));  
graphics.DrawString(xString, new Font("Segoe-UI", 12), Brushes.White, location.X + 5, location.Y +

</div>

<div>

5);

location.Y += itemSize.Height + 12;  
for (int idx = 0; idx < pointsToDraw.Count; idx++)  
{

</div>

<div>

var markerPoint = pointsToDraw[idx];  
graphics.FillEllipse(new SolidBrush(markerPoint.Color), location.X + 5, location.Y + 2, 10, 10); graphics.DrawString($"{markerPoint.Name} + {markerPoint.Value}", font, new SolidBrush  
(markerPoint.Color), location.X + 25, location.Y);  
graphics.FillEllipse(new SolidBrush(markerPoint.Color), markerPoint.Location.X - 5, markerPoint.  
Location.Y - 5, 10, 10);  
location.Y += itemSize.Height + 2;  
}  
}  
}

How to Use a Time Lapse Chart Using FlexChart

Moving Forward with Time-Lapse Charts with FlexChart

FlexChart is a powerful data visualization control that provides many useful features such as DataLabels, LineMarkers, Annotations, etc., out of the box. While these features are sufficient to meet most of the everyday needs of any charting application, FlexChart offers a rich API that lets you achieve any other requirements that you might have, as seen in this blog. This blog's customizations have been done as extension methods to use them right away in your applications.

To read more about FlexChart, visit the GrapeCity website.

Basant Khatiyan

Associate Software Engineer
comments powered by Disqus