TheBeardedQuack
TheBeardedQuack

Reputation: 449

Chart bind to non-primitive type

I have a C#.NET Windows Forms application using a chart with a number of line series. I have a physical device that stores readings on it, and as I'm downloading the readings I want the graph to fill in the detail.

I have created types in the software for quantifiable values (such as temperature) that may use different units depending on location, allowing me to have a user setting for display units, and just deal with everything in the background without caring, and when it's displayed to the user it's in the correct unit.

The C#.NET Charting.Series class is very strict and when attempting to draw using the bound array it throws an ArgumentExeption with the statement: "Series data points do not support values of type Units.Temperature only values of these types can be used: Double, Decimal, Single, int, long, uint, ulong, String, DateTime, short, ushort".

The Temperature type has a property called Value which is a double, and also is implicitly convertible to double, but this doesn't seem to matter. I'm hoping there's a way to bind this so that it converts whenever it requests the value from the results array so it can draw the graph correctly, anybody know how this might be done?

I'm hoping to avoid the need to the LogItem[] and then a secondary double[] just for the conversion.

private LogItem[] results;

private async void GraphPage_Load(object sender, EventArgs e) {
    var count = await device.LogCount.Read(CancelToken);
    results = new LogItem[count];
    graph.DataSource = results;
    graph.Series[0].XValueMembers = nameof(LogItem.DateTime); //Type is System.DateTime
    graph.Series[0].YValueMembers = nameof(LogItem.Temperature); //Type my library, Units.Temperature

    var prog = new Progress<double>();
    prog.ProgressChanged += (sender2, e2) => {
        BeginInvoke(new Action(() => {
            progBar.Value = results.Count(log => !Equals(log, LogItem.Empty));
            CheckEmptyPoints(); //Sets or clears Points[x].IsEmpty;
            graph.Invalidate(); //Force graph to draw new results, maybe a better way of doing this?
        }));
    };
    await device.DownloadLogs(CancelToken, results, prog); //Takes output buffer and IProgress to report more data has been downloaded.
}

Upvotes: 2

Views: 462

Answers (2)

TheBeardedQuack
TheBeardedQuack

Reputation: 449

Many thanks to Reza Aghaei for the help.

The Linq statements do hold the answer to providing a conversion function, but just with a little tweaking so it's not creating a new array and rebinding with every refresh.

The solution is to use Charting.DataPointsCollection.DataBind(IEnumerable dataSource, string xField, string yField, string otherFields) to bind to the data. But to use Linq statements to create an IEnumerable object to be used in the databind, and on every call of MoveNext, this IEnumerable will fetch the data from the array and convert for the chart.

// IEnumerable<LogItem>.Select(...) creates an IEnumerable which references the array,
//   and performs the selection and conversion on each call to MoveNext().

graph.Series[0].Points.DataBind(
    results.Select(l => new { X = l.DateTime, Y = l.OilCondition.Value }),
    "X",
    "Y",
    string.Empty);

EDIT: The above does work correctly, but according to the documentation on Points.DataBind(,,,), the evaluation is only performed once, so the DataBind(,,,) method does need to be called each time.

Upvotes: 1

Reza Aghaei
Reza Aghaei

Reputation: 125197

You can shape the result to a chart-friendly shape using linq. For example:

var list = results.Select(x => new { X = x.DateTime, Y = x.Temprature.Value }).ToList();
this.chart1.DataSource = list;
this.chart1.Series[0].XValueMember = "X";
this.chart1.Series[0].YValueMembers = "Y";

X and Y are just place holders and you can use DateTime and Tempratue or any suitable label.

Upvotes: 3

Related Questions