Reputation: 10393
I'm trying to write a WPF application to plot line charts for series of numbers I have. These numbers are listed in a .CSV
file which I will read at run time. Thus, I do not know the number of series I will have, nor the max/min values of each.
To demonstrate, and for the sake of brevity, take a look at the following example. Think of these series values as what I will read from my .CSV
file in my actual application.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SeriesCollection = new SeriesCollection
{
new LineSeries
{
Title = "Series 1",
Values = new ChartValues<double> { 4, 66, 5, 2, 4 },
},
new LineSeries
{
Title = "Series 2",
Values = new ChartValues<double> { 6, 7, 3, 4, 6 },
},
new LineSeries
{
Title = "Series 3",
Values = new ChartValues<double> { 4, 2, 7, 2, 7 },
}
};
DataContext = this;
}
public SeriesCollection SeriesCollection { get; set; }
}
My XAML
looks very simple, like so:
<Window x:Class="WPFCharts.MainWindow"
...
Title="MainWindow" Height="450" Width="800">
<Grid>
<lvc:CartesianChart Series="{Binding SeriesCollection}"/>
</Grid>
</Window>
As you can see, one of the values in one series is off the charts compared to the rest if I were to plot this in a line graph with default LiveCharts
settings:
So I want to give the user the opportunity to put such line graphs in their own axis. Reading through LiveCharts
documentation, I found, as shown here, that you can put different line series in different axis by using the ScaleXAt
and ScaleYAt
properties.
However that example sets axis in XAML
whereas I want to do this dynamically. So I tried setting the said property in code behind like so:
SeriesCollection = new SeriesCollection
{
new LineSeries
{
Title = "Series 1",
Values = new ChartValues<double> { 4, 66, 5, 2, 4 },
ScalesYAt = 0
},
new LineSeries
{
Title = "Series 2",
Values = new ChartValues<double> { 6, 7, 3, 4, 6 },
ScalesYAt = 1
},
new LineSeries
{
Title = "Series 3",
Values = new ChartValues<double> { 4, 2, 7, 2, 7 },
ScalesYAt = 2
}
};
But when I do that, and run the application, I get an exception saying:
System.ArgumentOutOfRangeException: 'Index was out of range. Must be non-negative and less than the size of the collection.'
What am I doing wrong here? How can I set this using code, not XAML
?
Upvotes: 0
Views: 7236
Reputation: 149
I'm working on something similar to this; its a mess right now, I'm using it as a learning experience.
using CsvHelper;
using LiveCharts.Geared;
using LiveCharts.Wpf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using System.Windows.Media;
using System.ComponentModel;
using System.Data;
namespace CSVtoCHART
{
public partial class FrmCSVtoCHART : Form
{
public FrmCSVtoCHART()
{
InitializeComponent();
LoadSettings();
InitializeDataGridSettings();
cartesianChart1.AxisX.Add(new Axis
{
Title = "Time",
Labels = new string[] { "0", "1", "2", "3", "4" },
MinValue = 0,
MaxValue = 4,
LabelsRotation = -45,
Foreground = Brushes.Black
});
cartesianChart1.AxisY.Add(new Axis
{
Foreground = Brushes.Black,
Title = "Chart",
MinValue = 0,
MaxValue = 100
});
cartesianChart1.Zoom = LiveCharts.ZoomingOptions.X;
}
public DataTable dt = new DataTable();
private void BtnOpenCSV_Click(object sender, EventArgs e)
{
string FilePath;
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
cartesianChart1.AxisX.Clear();
cartesianChart1.AxisY.Clear();
cartesianChart1.Series.Clear();
FilePath = openFileDialog1.FileName;
using (var streamReader = new StreamReader(@FilePath))
using (var csv = new CsvReader(streamReader, CultureInfo.InvariantCulture))
{
csv.Read();
csv.ReadHeader();
csv.Read();
int num = 0;
foreach (var header in csv.HeaderRecord)
{
if (double.TryParse(csv.GetField(num), out double value))
{
dt.Columns.Add(header, typeof(double));
}
else if (TryConvertStringToDateTime(csv.GetField(num), out DateTime ts))
{
dt.Columns.Add(header, typeof(string));
}
num++;
}
while (csv.Read())
{
num = 0;
var row = dt.NewRow();
foreach (DataColumn column in dt.Columns)
{
if (double.TryParse(csv.GetField(num), out double value))
{
row[column.ColumnName] = value;
}
else if (TryConvertStringToDateTime(csv.GetField(num), out DateTime ts))
{
row[column.ColumnName] = csv.GetField(num);
}
else if (column.DataType == typeof(double))
{
row[column.ColumnName] = 0; //double.NaN;
}
num++;
}
dt.Rows.Add(row);
}
var dgRow = new List<RowSettings>();
num = 0;
foreach (DataColumn column in dt.Columns)
{
if (column.DataType == typeof(double))
{
var brushes = typeof(Brushes).GetProperties()
.Where(pi => pi.PropertyType == typeof(SolidColorBrush))
.Select(pi => pi.Name)
.ToList();
var random = new Random();
int index = random.Next(brushes.Count);
var brush = typeof(Brushes).GetProperty(brushes[index])
.GetValue(null)
as SolidColorBrush;
List<double> vals = dt.AsEnumerable().Select(v => v.Field<double>(column.ColumnName)).Distinct().ToList();
double minVal = vals.Min();
double maxVal = vals.Max();
dgRow.Add(new RowSettings
(
column.ColumnName,
brushes[index],
true,
minVal,
maxVal
));
AddYSeries(num, brushes[index], column, minVal, maxVal);
num++;
}
if (column.DataType == typeof(string))
{
var col = dt.AsEnumerable().Select(c => c.Field<string>(column.ColumnName)).ToList();
cartesianChart1.AxisX.Add(new Axis
{
Title = "Time",
Labels = col,
LabelsRotation = -45,
Foreground = Brushes.Black,
MinValue = 0,
MaxValue = 10000
});
}
}
dataGridView1.DataSource = dgRow;
}
}
}
private void AddYSeries(int num, string lineColor, DataColumn column, double minVal, double maxVal)
{
var brush = typeof(Brushes).GetProperty(lineColor)
.GetValue(null)
as SolidColorBrush;
var col = dt.AsEnumerable().Select(c => c.Field<double>(column.ColumnName)).ToList();
cartesianChart1.Series.Add(new LineSeries
{
Title = column.ColumnName,
Values = col.AsGearedValues().WithQuality(Quality.Low),
ScalesYAt = num,
LineSmoothness = 0,
StrokeThickness = 1,
Fill = Brushes.Transparent,
PointGeometry = null,
Stroke = brush
});
cartesianChart1.AxisY.Add(new Axis
{
Foreground = brush,
Title = column.ColumnName,
MinValue = minVal,
MaxValue = maxVal,
DisableAnimations = true
});
}
private void InitializeDataGridSettings()
{
var brushes = typeof(Brushes).GetProperties()
.Where(pi => pi.PropertyType == typeof(SolidColorBrush))
.Select(pi => pi.Name)
.ToList();
DataGridViewColumn[] dgCol =
{
new DataGridViewTextBoxColumn()
{
HeaderText = "Plot Name",
SortMode = DataGridViewColumnSortMode.NotSortable,
DataPropertyName = "PlotName",
ValueType = typeof(string),
ReadOnly = true
},
new DataGridViewCheckBoxColumn()
{
HeaderText = "Display on chart?",
SortMode = DataGridViewColumnSortMode.NotSortable,
DataPropertyName = "Display",
ValueType = typeof(bool),
ReadOnly = false
},
new DataGridViewComboBoxColumn()
{
HeaderText = "Line Color",
DataSource = brushes,
SortMode = DataGridViewColumnSortMode.NotSortable,
DataPropertyName = "LineColor",
ValueType = typeof(string),
ReadOnly = false
},
new DataGridViewComboBoxColumn()
{
HeaderText = "Line Style",
DataSource = new List<string>
{
"Solid",
"Dash Dash",
"Dash Dot",
"Dot Dot",
},
SortMode = DataGridViewColumnSortMode.NotSortable,
DataPropertyName = "LineStyle",
MaxDropDownItems = 4,
ValueType = typeof(List<string>),
ReadOnly = false
},
new DataGridViewTextBoxColumn()
{
HeaderText = "Chart Min",
SortMode = DataGridViewColumnSortMode.NotSortable,
DataPropertyName = "ChartMin",
ValueType = typeof(double),
ReadOnly = false
},
new DataGridViewTextBoxColumn()
{
HeaderText = "Chart Max",
SortMode = DataGridViewColumnSortMode.NotSortable,
DataPropertyName = "ChartMax",
ValueType = typeof(double),
ReadOnly = false
}
};
dataGridView1.Columns.AddRange(dgCol);
dataGridView1.RowHeadersVisible = false;
dataGridView1.RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.DisableResizing;
}
public bool TryConvertStringToDateTime(string s, out DateTime result) // function for converting string to datetime
{
return ((DateTime.TryParse(s, out result)) ||
(DateTime.TryParseExact(s, "yyyy-MM-dd H:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) ||
(DateTime.TryParseExact(s, "yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) ||
(DateTime.TryParseExact(s, "yyyy-MM-dd h:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) ||
(DateTime.TryParseExact(s, "yyyy-MM-dd hh:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) ||
(DateTime.TryParseExact(s, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) ||
(DateTime.TryParseExact(s, "yyyy/MM/dd H:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) ||
(DateTime.TryParseExact(s, "yyyy/MM/dd HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) ||
(DateTime.TryParseExact(s, "yyyy/MM/dd h:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) ||
(DateTime.TryParseExact(s, "yyyy/MM/dd hh:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) ||
(DateTime.TryParseExact(s, "yyyy/MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) ||
(DateTime.TryParseExact(s, "MM.dd.yy HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out result))
);
}
private void LoadSettings()
{
this.Height = Properties.Settings.Default.FormHeight;
this.Width = Properties.Settings.Default.FormWidth;
this.Location = Properties.Settings.Default.FormLocation;
}
private void FrmCSVtoCHART_FormClosing(object sender, FormClosingEventArgs e)
{
Properties.Settings.Default.FormHeight = this.Height;
Properties.Settings.Default.FormWidth = this.Width;
Properties.Settings.Default.FormLocation = this.Location;
Properties.Settings.Default.Save();
}
private void btnApply_Click(object sender, EventArgs e)
{
cartesianChart1.AxisY.Clear();
cartesianChart1.Series.Clear();
int num = 0;
List<string> showSeries = new List<string>();
List<string> seriesColor = new List<string>();
List<double> seriesMin = new List<double>();
List<double> seriesMax = new List<double>();
foreach (DataGridViewRow row in dataGridView1.Rows)
{
if (Convert.ToBoolean(row.Cells[1].Value))
{
showSeries.Add(row.Cells[0].Value.ToString());
seriesColor.Add(row.Cells[2].Value.ToString());
seriesMin.Add(Convert.ToDouble(row.Cells[4].Value));
seriesMax.Add(Convert.ToDouble(row.Cells[5].Value));
}
}
foreach (DataColumn column in dt.Columns)
{
if (showSeries.Contains(column.ColumnName))
{
AddYSeries(num, seriesColor[num], column, seriesMin[num], seriesMax[num]);
num++;
}
}
}
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CSVtoCHART
{
class RowSettings
{
private string _PlotName, _LineColor, _LineStyle;
private bool _Display;
private double _ChartMin, _ChartMax;
public RowSettings(string _PlotName, string _LineColor, bool _Display, double _ChartMin, double _ChartMax)
{
this._PlotName = _PlotName;
this._LineColor = _LineColor;
this._Display = _Display;
this._ChartMin = _ChartMin;
this._ChartMax = _ChartMax;
}
public string PlotName
{
get
{
return _PlotName;
}
set
{
_PlotName = value;
}
}
public bool Display
{
get
{
return _Display;
}
set
{
_Display = value;
}
}
public string LineColor
{
get
{
return _LineColor;
}
set
{
_LineColor = value;
}
}
public string LineStyle
{
get
{
return _LineStyle;
}
set
{
_LineStyle = value;
}
}
public double ChartMin
{
get
{
return _ChartMin;
}
set
{
_ChartMin = value;
}
}
public double ChartMax
{
get
{
return _ChartMax;
}
set
{
_ChartMax = value;
}
}
}
}
Upvotes: 0
Reputation: 5666
If you want to use different Y Axis, then you need to declare them, maybe you missed it. So your model will become something like:
public class ViewModel
{
public ViewModel()
{
SeriesCollection = new SeriesCollection
{
new LineSeries
{
Title = "Series 1",
Values = new ChartValues<double> { 4, 66, 5, 2, 4 },
ScalesYAt = 0
},
new LineSeries
{
Title = "Series 2",
Values = new ChartValues<double> { 6, 7, 3, 4, 6 },
ScalesYAt = 1
},
new LineSeries
{
Title = "Series 3",
Values = new ChartValues<double> { 4, 2, 7, 2, 7 },
ScalesYAt = 2
}
};
AxisYCollection = new AxesCollection
{
new Axis { Title = "Y Axis 1", Foreground = Brushes.Gray },
new Axis { Title = "Y Axis 2", Foreground = Brushes.Red },
new Axis { Title = "Y Axis 3", Foreground = Brushes.Brown }
};
}
public AxesCollection AxisYCollection { get; set; }
public SeriesCollection SeriesCollection { get; set; }
}
while the XAML will be:
<Grid>
<lvc:CartesianChart Series="{Binding SeriesCollection}" AxisY="{Binding AxisYCollection}" />
</Grid>
Of course you need to set an instance of ViewModel
class as the DataContext
of your Window:
public MainWindow()
{
vm = new ViewModel();
InitializeComponent();
DataContext = vm;
}
If you do not declare "enough" Axis in the binded AxesCollection
, its element at n index won't be found and you will slip into an ArgumentOutOfRangeException. I hope it can help you.
Upvotes: 6