07-25-2016 09:49 AM
Hi all,
I'm relatively new to WPF and trying to plot multiple (x,y)-lines in a ni:Graph control.
As datasource for the graph control I'm using
ObservableCollection<Point[]> plotData = new ObservableCollection<Point[]>();
and assigning this to my graph's datasource
tandGraph.DataSource = plotData;
As plotData changes during runtime, I get new (multiple) line plots (fine so far). I have the legend bound to the plots in xaml:
<ni:Legend ItemsSource="{Binding AllPlots, ElementName=tandGraph}"Now the legend shows something like "Plot1; Plot2; etc." but I would like to specify the plotnames manually. I did plotting/charting in Matlab and Labview, but I don't seem to get how I could specify all the important things like LineStyle, Color, (or even "UserData" for some custom information) etc. in WPF as I would do it in Matlab:
plot(X,Y,LineSpec)
Sorry, I know that maybe I'm talking about two entirely different programming worlds, but I would really like to try this in WPF. I also know, I could define (a fixed number of plots) via xaml, but the number of plots changes during runtime. How would this usually be done? Can anybody help me?
Thanks in advance
Greetings
07-25-2016 10:58 AM
Note that WPF constructs shown in XAML can also be done in code, and so can be done dynamically as your data changes. For example, the XAML in How to: Plot and Chart used to declare the plot style is equivalent to code creating a new Plot, assigning the Renderer to a LinePlotRenderer, and adding it to the Plots collection on the graph:
tandGraph.Plots.Add( new Plot { Renderer = new LinePlotRenderer { ... } } );
If you want to use MVVM and minimize the amount of code-behind logic, you may find it easier to maintain an observable collection of plots (like your observable collection of data), and use the example PlotsSource attached property described in Wpf Graph Binding and defining Axes and Plots only in ViewModel.
You may also find the links on the WPF Controls overview useful.
07-26-2016 01:50 AM
Thanks Paul,
I was also already thinking about implementing a new set of DefaultPlotRenderers to change the style etc. as I want. I also had a look at your code with the PlotSynchronizer, but am still too unfamiliar with ViewModel and the other things you are coding here - in short: I don't understand it 😉
But your suggestion with adding the Plots manually would work for me (if I knew how to "put" the data to the plot - DataContext seems to be the wrong attribute 😐 ) although it means a bit more of code - I am not really satisfied with this solution (not because of your solution but because I don't understand how it should be done the right way...)
I'll do a little more trial and error
07-26-2016 03:38 AM
Sorry, could not find the Edit function... (?)
What I found out now: I can rename each "Plot" directly after editing the DataSource of the graph by
plotData.Add(singlePlot);
NationalInstruments.Controls.Primitives.IPlotObserver plotObs = tandGraph.AllPlots[tandGraph.AllPlots.Count() - 1]; plotObs.Label = ("ID: " + mc.ID.ToString());
I will still try to understand your PlotsSource example...
07-26-2016 10:14 AM
Sorry for any confusion. There is no singular "one right way" — different approaches have different benefits and tradeoffs. I wasn't sure what approach you were trying to take, so I tried to provide several different options.
So stepping back a bit, let me try to explain a little about how the graph is designed.
For the WPF graph, we tried to make it work like a native WPF control, where configuration is based on properties and collections. The key properties on the graph are Data and DataSource, which provide the graph with data (these work the same way as Items and ItemsSource on the WPF ItemsControl).
∴ For your case, setting DataSource to your observable collection sounds good.
To scale and display the data, the graph uses Plots (and if you do not configure a plot for your data, then the graph will automatically create one and add it to AllPlots). There is a one-to-one correspondence between values in the Data collection and plots in the Plots (or AllPlots) collection. (Since the graph focuses on property-based configuration, we do not have a separate data source on each individual plot, or an imperative method to plot data; although that can be emulated, as with this PlotXY extension method from another question.)
∴ So using the auto-generated plots inAllPlotsas you are doing now is okay, but be aware thatAllPlotsis managed by the graph (i.e. if you remove data, the associated plot will be removed as well, and your labels may be lost or become mis-aligned). Unfortunately, I do not have another easy option to offer for configuring labels, besides what you are doing already (accessingAllPlots), or managing thePlotscollection yourself (either directly in code, or through your own collection and bindingPlotsSource).
The display of data for a Plot is controlled by the Renderer. If you don't assign a renderer explicitly, then the plot will fall back to the renderers in the DefaultPlotRenderers collection on the parent graph. The renderers are focused on specific styles (i.e. lines, points), but can be combined for a single plot using a PlotRendererGroup.
∴ Using DefaultPlotRenderers as you mentioned is a good option.
Hopefully I've covered some of your more basic questions about understanding the graph. Please continue to ask about anything that needs clarification or that I didn't address.
07-27-2016 08:09 AM
Thanks again Paul for the extensive answer!
Maybe it's easier, if I just explain, what I would like to have in the end:
I got data from a SQL-database with some columns e.g. [ID], [Voltage], [Current], [ParameterA], [x], [y], [z],...
Let's say, dependent on the same value of [x], [Voltage] and [Current] should be plotted as one line graph (this works so far with the ObservableCollection<Point[]>). As already described, I want the legend to show [ID] for example - this also works with the IPlotObserver (but seems not very sophisticated...).
I thought about a new class where all values (rows) from the database from one [x] are bound together also with the according 'linestyles' (PlotRenderer) and put these in an ObservableCollection<newLinePlotClass> -> I don't have any idea how I can bind the Graph.DataSource to one attribute of a class (e.g. newLinePlotClass.xyValuePairs)
I sometimes also may want to plot [ParameterA] over [Voltage] (I thought this could work by implementing x- and y- attributes of the class returning different columns as x- and y- values - as before e.g. newLinePlotClass.xyValuePairs...).
Last but not least, I would like a reference for each datapoint as described before with all different columns in the database to also show for example [x], [y] and [z] in a tooltip, when one datapoint is selected (I did this in Matlab before and I've seen the "CustomPlotRenderers"-example, which fixed me onto this ;-))
Do you think I am totally off the track and should stay in Matlab? x_x
Thanks for any advice...
07-27-2016 02:24 PM
Our graph is capable of supporting a wide variety of data types and configurations, so I would not say you are totally off track 🙂
I appreciate your more detailed description, though I'm afraid I do not quite understand the various ways you are mapping your SQL columns to collections of points. I could try to take a guess, but I don't know how useful that would be. However, if you could provide a small example project with some placeholder data types (i.e. a small array of struct Row { int ID; double Voltage; double Current; ... } items in memory, with dummy values, and example code that shows how that gets translated), it would be easier for me to help you.
07-28-2016 08:07 AM
Allright, let's try this!
I am using Entity Framework and Linq to SQL to access the database. There are three main databases, that are dependent on each other as the following image shows:
So in the normal use-case, there would be six "MeasurementCycles" for one "TrackMeasurement", and 12-14 "MeasurementValues" per "MeasurementCycle". These "MeasurementValues" are plotted and linked in one linegraph for each MeasurementCycle.
With Entity Framework, the classes for the databases are implemented as follows (I blurred the picture to save some code here):
public partial class TrackMeasurement
{
{
MeasurementCycles = new HashSet<MeasurementCycle>();
MeasurementPDRefs = new HashSet<MeasurementPDRef>();
}
public int ID { get; set; }
[Required]
[StringLength(50)]
public string Strecke { get; set; }
public int Messstelle { get; set; }
public float Außentemperatur { get; set; }
[...]
} public partial class MeasurementCycle
{
{
MeasurementValues = new HashSet<MeasurementValue>();
}
public int ID { get; set; }
public int TMeasID { get; set; }
public int Leiter { get; set; }
public float Frequenz { get; set; }
[...]
}public partial class MeasurementValue
{
public int ID { get; set; }
public int CycleID { get; set; }
public int ParameterID { get; set; }
public double Z_abs { get; set; }
public double Z_arg { get; set; }
public double tan_delta { get; set; }
public double U_rms { get; set; }
public double I_rms { get; set; }
[...]
}I have three ListViews with GridViews to display the three datatables (only one here, the other ones are similar):
<ListView SelectionChanged="lvTrackMeasurement_SelectionChanged" Name="lvTrackMeasurement" Grid.Row="1">
<ListView.View>
<GridView>
<GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}"/>
<GridViewColumn Header="Strecke" DisplayMemberBinding="{Binding Strecke}"/>
<GridViewColumn Header="Messstelle" DisplayMemberBinding="{Binding Messstelle}"/>
<GridViewColumn Header="Außentemperatur" DisplayMemberBinding="{Binding Außentemperatur}"/>
[...]Setting the ItemsSource property of the ListViews makes them show the datatables:
List<TrackMeasurement> allTrackMeasurements = new List<TrackMeasurement>();
ObservableCollection<MeasurementCycle> selectedMeasurementCycles = new ObservableCollection<MeasurementCycle>();
ObservableCollection<MeasurementValue> selectedMeasurementValues = new ObservableCollection<MeasurementValue>();
public MainWindow() { InitializeComponent(); using (var db = new IDSdbModel()) { allTrackMeasurements = db.TrackMeasurements.ToList(); } lvTrackMeasurement.ItemsSource = allTrackMeasurements; lvMeasurementCycles.ItemsSource = availableMeasurementCycles; lvMeasurementValues.ItemsSource = selectedMeasurementValues; tandGraph.DataSource = plotData; }
Everytime I change the selection of the first table ("TrackMeasurement"), the other tables are changed accordingly, and the selected "MeasurementCycles" are plotted automatically:
private void lvTrackMeasurement_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
using (var db = new IDSdbModel())
{
//Completely refill tables
selectedMeasurementCycles.Clear();
availableMeasurementCycles.Clear();
foreach (TrackMeasurement currentTrackMeasurement in lvTrackMeasurement.SelectedItems)
{
foreach (MeasurementCycle mc in db.MeasurementCycles.Where(mc => mc.TMeasID == currentTrackMeasurement.ID).ToList())
{
availableMeasurementCycles.Add(mc);
}
}
lvMeasurementCycles.SelectAll();
}
}
private void lvMeasurementCycles_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
using (var db = new IDSdbModel())
{
tandGraph.Plots.Clear();
plotData.Clear();
selectedMeasurementValues.Clear();
selectedMeasurementCycles.Clear();
//What was chosen?
foreach (MeasurementCycle currentMeasurementCycle in lvMeasurementCycles.SelectedItems)
{
Point[] singlePlot = new Point[currentMeasurementCycle.MeasurementValues.Count()];
int count = 0;
foreach (MeasurementValue mv in currentMeasurementCycle.MeasurementValues)
{
selectedMeasurementValues.Add(mv);
singlePlot[count] = new Point(mv.U_rms, mv.tan_delta);
count++;
}
plotData.Add(singlePlot);
NationalInstruments.Controls.Primitives.IPlotObserver plotObs = tandGraph.AllPlots[tandGraph.AllPlots.Count() - 1];
plotObs.Label = ("ID: " + currentMeasurementCycle.ID.ToString());
}
}
}So the plotted lines change, if either I select other "TrackMeasurements" or change the selected "MeasurementCycles" of the selected "TrackMeasurements". As I did with the ItemsSource of the ListViews and their Bindings on the Attributenames of the classes they are bound to, I thought it might be possible (that was what I meant in the last post) to have some kind of class, which includes all the stuff I would need for one linegraph (Point[], MeasurementValue, LineRenderer, etc...) and bind these (somehow...??!) to the graph.
I hope you can understand what I mean... Do you need more information?
Thanks so much!!
07-28-2016 03:51 PM
Thanks for the added details! I've created the attached example project based on your remarks.
As we've discussed, it is possible to implement the features in several ways. For this example, I've chosen to use some of the native graph data management features to display the additional values using a cursor. I also introduced a dictionary to hold on to plot renderers (so that one MeasurementCycle will get the same trace color every time it is selected), along with the PlotsSource extension.
(Not that this is a proof of concept; I didn't spend a lot of time factoring things between XAML and code behind, but this should give you a working example to start from.)
08-04-2016 02:00 AM
Thank you so much Paul, I will try and see, how far I can get with your examples... Thanks!