3

In WinForms, I'm trying to create an interactive chart but am having difficulties with one aspect. I am using System.Windows.Forms.DataVisualization.Charting.Chart for the chart, and using a FastLine series to render the data.

In this chart, you can zoom/pan/change channels, and new points are dynamically added / removed as needed. The reason for this is that the source of these points is a large binary file that can be well over a gigabyte, and I am using a MemoryMappedFile to pull as little data as possible to increase performance.

When panning to the right, everything works fine - new points are added to the end of the list and you can't even tell that they were not there a second ago.

However, when I pan to the left, the graph displays in a very odd way. There are lines running all over the place. I know what is happening here, I just can't figure out how to fix it.

The cause is that when a point is added, a line is drawn from the last point added to the new one. So if the last point was added to the right side, and the new point is added to the left side, a line will shoot across the graph.

Here is the code that adds points to the graph:

for (var i = scanStart; i + _step <= scanEnd; i += _step)
{
   var sample = _expeData.ReadSample(channel, i);
   Series[0].Points.AddXY(i, sample);
}

Is there any way to prevent this from happening? Here is a screenshot showing what it looks like after panning to the left:

enter image description here

Zong
  • 6,160
  • 5
  • 32
  • 46
kingcoyote
  • 1,145
  • 8
  • 21
  • this may be stupid cuz i have no idea of how you draw this graph, but when theres written `AddXY` maybe you need an `InsertAtZeroXY` function – efkah Dec 13 '13 at 23:38
  • There is an InsertXY function on that object, but it doesn't seem to do anything different than AddXY does, in regards to this display issue. – kingcoyote Dec 13 '13 at 23:40
  • Could this be as simple as calling `graph.Invalidate();` after the data changes? –  Dec 16 '13 at 18:18
  • Unfortunately, that does not help. I've tried both graph.Invalidate() an graph.Series[0].Invalidate() and neither one has any impact on this bug. – kingcoyote Dec 16 '13 at 18:20
  • How about wiping out the points that are in the series and giving the graph a whole new set of points. – Sam Axe Dec 16 '13 at 18:59
  • The Series[0].Points.Clear() command is kinda heavy. I tried that and it worked, but at a fairly significant performance hit. And this piece of software is meant to replace an older program whose biggest complaint is speed. So I would like to do whatever I can to keep performance up. – kingcoyote Dec 16 '13 at 19:02

4 Answers4

0

I decided to experiment with using LINQ to handle sorting.

        // Suspend updates prior to insertion to speed everything up
        chart1.Series[0].Points.SuspendUpdates();

        // Do a quicky population for testing purposes
        for (int i = 0; i < 100; i++)
        {
            chart1.Series[0].Points.AddXY(i, i);
        }
        for (int i = -100; i < 0; i++)
        {
            chart1.Series[0].Points.AddXY(i, -i * 2);
        }

        // Sort the data points
        var newDataPoints =
            (from d in chart1.Series[0].Points
             orderby d.XValue
             select new { XValue = d.XValue, YValue = d.YValues[0] }).ToList();

        // Replace the datapoints
        chart1.Series[0].Points.DataBind(newDataPoints, "XValue", "YValue", "");

        // Turn updates back on
        chart1.Series[0].Points.ResumeUpdates();
RomSteady
  • 398
  • 2
  • 13
  • I tried this, and it definitely fixed the display bug, but at a significant performance hit. Previously, it required under a millisecond to update the points. With this, it takes 8-12ms, causing a visible lag when panning. – kingcoyote Dec 16 '13 at 19:15
  • Have you considered adding your points to a SortedList outside of the chart, then just databinding? And a secondary question...how often do your users actually pan left past the start value? – RomSteady Dec 16 '13 at 19:36
  • They pan left and right constantly. The data is a multi day stream of analyzer recordings - oxygen levels, co2 levels, movement sensors, etc. They zoom in to view sections, and pan around to get an idea of what was going on during that time. – kingcoyote Dec 16 '13 at 19:41
  • Then the question becomes where is delay acceptable to your users? I question if 12ms is truly unacceptable, given that 60fps video games are handling their frame updates in ~16ms. If it's panning close to where they are, consider doing chunked updates from the file. If it's overall panning, consider a level-of-detail model during panning. – RomSteady Dec 16 '13 at 19:45
  • Even though your answer didn't strictly work for me, the idea of sorting the points led me to the answer that did work. Once the bounty timer expires, I'll give it to you. – kingcoyote Dec 16 '13 at 23:38
0

That's a cool control that I've never noticed before. Please do not mark my post as an answer. It is just talking out loud.

Could you clear the chart and reassign the data on a LEFT PAN?

You could declare some variables global to your class:

private int x_index;
private DataTable m_dataTable;

In your Chart's constructor, wire up the AxisViewChanging and AxisViewChanged event handlers:

public Form1() {
  InitializeComponent();
  x_index = -1;
  if (graph1.Series == null) {
    graph1.Series = new SeriesCollection();
  }
  graph1.AxisViewChanging += new EventHandler<ViewEventArgs>(graph1_AxisViewChanging);
  graph1.AxisViewChanged += new EventHandler<ViewEventArgs>(graph1_AxisViewChanged);
}

Now, modify your Get Data routine to collect the data into your new DataTable:

private void GetData(string channel, int scanStart, int step, int scanEnd) {
  m_dataTable = new DataTable();
  var col1 = m_dataTable.Columns.Add("Index", typeof(int));
  var col2 = m_dataTable.Columns.Add("Data", typeof(Series));
  for (var i = scanStart; i + step <= scanEnd; i += step) {
    var sample = _expeData.ReadSample(channel, i);
    var row = m_dataTable.NewRow();
    row[col1] = i;
    row[col2] = new Series(sample);
    m_dataTable.Rows.Add(row);
  }
}

The Changing handler below just records what your current index is:

private void graph1_AxisViewChanging(object sender, ViewEventArgs e) {
  // below is probably wrong, but basically, get the x_axis before the data changes
  x_index = Convert.ToInt32(graph1.Series[0].XValueMember);
}

The Changed handler below is used to display data before x_index that you collected above:

private void graph1_AxisViewChanged(object sender, ViewEventArgs e) {
  var index = Convert.ToInt32(graph1.Series[0].XValueMember);
  if (index < x_index) {
    graph1.Series.Clear();
    for (int i = index; i < index + 50; i++) {
      var pt = (Series)m_dataTable.Rows[i]["Data"];
      graph1.Series.Add(pt);
    }
  }
}

In the above, the value 50 would be whatever you were wanting to count to. It should really be specified as a variable or constant somewhere in your code so that it does not look like a Magic Number.

I'm not sure if this works how you want.

If it does, it would need to be tweaked to account for your values when someone pans to the RIGHT - because my code likely breaks that functionality. ;)

  • References: [DotNetPerls](http://www.dotnetperls.com/chart), [SO Class](http://stackoverflow.com/a/10624227/153923p), and [MSDN](http://archive.msdn.microsoft.com/mschart/Release/ProjectReleases.aspx?ReleaseId=4418) –  Dec 16 '13 at 19:15
  • There are two problems with this - one is that the Series.Clear() command is too heavy. It introduces lag since I have to update hundreds of points each frame, rather than 2 or 3. The other is that the DataTable object would get ridiculous. The data I'm pulling is a binary file full of IEEE 754 4 byte floats. The data can be up to a gig in size, and populating a datatable with this is impractically large. – kingcoyote Dec 16 '13 at 19:20
0

Did you tried to use the InsetXY method?

chart.Series[0].Points.InsertXY(0, i, sample)

With that you will always add to the first position.

Don't know if that fits your problem.

Markus Fantone
  • 341
  • 1
  • 8
0

I found the solution was to make use of the Series[0].Sort() function. I modified the original code as follows:

for (var i = scanStart; i + _step <= scanEnd; i += _step)
{
    var sample = _expeData.ReadSample(_channel, i);
    Series[0].Points.AddXY(i, sample);
}

Series[0].Sort(new PointComparer());

And I added a private class to handle the sorting:

private class PointComparer : IComparer<DataPoint>
{
    public int Compare(DataPoint x, DataPoint y)
    {
        return (int)(x.XValue - y.XValue);
    }
}

This sorts the points based on XValue. There is a slight performance hit when the number of points starts to grow above roughly 40,000. However, I can mitigate that by only loading as many points as are visible on screen (max of roughly 2,000 on a modern wide screen monitor) and then trimming once I exceed a certain amount (I'm using 20,000). This causes an occasional hiccup after a relatively large amount of panning, but it isn't very noticable.

kingcoyote
  • 1,145
  • 8
  • 21