Files
2026-04-17 14:55:32 -04:00

736 lines
31 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace DTS.Slice.PedestrianAndHeadReports
{
/// <summary>
/// Generic class for handling graphs in reports
/// </summary>
public class ReportGraph : INotifyPropertyChanged
{
/*private string GetEngineeringUnits(DTS.Slice.Control.Event.Module.Channel channel)
{
if (channel is DTS.DAS.Concepts.DAS.Channel.IEngineeringUnitAware)
{
return (channel as DTS.DAS.Concepts.DAS.Channel.IEngineeringUnitAware).EngineeringUnits;
}
else { return "EU"; }
}*/
/// <summary>
/// colors for lines, start with blue, etc.
/// </summary>
private System.Drawing.Color[] COLORS = new System.Drawing.Color[] { System.Drawing.Color.Blue, System.Drawing.Color.Red, System.Drawing.Color.Green, System.Drawing.Color.Orange };
/// <summary>
/// we hold onto an actual chart object if we know what it is,
/// which we will once we've drawn it atleast once.
/// this lets us redraw if something changes
/// </summary>
private C1.Win.C1Chart.C1Chart _chart = null;
public C1.Win.C1Chart.C1Chart Chart { get { return _chart; } }
public void Draw() { Draw(_chart); }
public void Draw(C1.Win.C1Chart.C1Chart chart)
{
if (null == chart) { return; }
if (null == _chart) { _chart = chart; }
chart.ChartGroups[0].ChartData.SeriesList.Clear();
for (int i = 0; i < _channelIds.Count; i++)
{
chart.ChartGroups[0].ChartData.SeriesList.AddNewSeries();
GraphChannel gc = _channels[_channelIds[i]];
if (gc.Channel == null) { continue; }
chart.ChartGroups[0].ChartData.SeriesList[i].Label = gc.DisplayName;
chart.ChartGroups[0].ChartData.SeriesList[i].LineStyle.Color = COLORS[i];
chart.ChartGroups[0].ChartData.SeriesList[i].LineStyle.Thickness = 2;
chart.ChartGroups[0].ChartData.SeriesList[i].SymbolStyle.Shape = C1.Win.C1Chart.SymbolShapeEnum.None;
chart.ChartGroups[0].ChartData.SeriesList[i].PlotFilterMethod = C1.Win.C1Chart.PlotFilterMethodEnum.Alternative;
chart.ChartGroups[0].ChartData.SeriesList[i].PlotFilter = 5;
DTS.Slice.Control.Event.Module.Channel aic = gc.Channel.Channel as DTS.Slice.Control.Event.Module.Channel;
if (null == aic) { continue; }
double dCurrentTime = ((double)aic.ParentModule.StartRecordSampleNumber - (double)aic.ParentModule.TriggerSampleNumbers[0]) / (double)aic.ParentModule.SampleRateHz;
double dIncrement = 1/(double)aic.ParentModule.SampleRateHz;
if (TimeUnits == "ms") { dCurrentTime *= 1000; dIncrement *=1000;}
List<double> xAxis = new List<double>(500000);
List<double> yAxis = new List<double>(500000);
double dMin = double.MaxValue;
double dMax = double.MinValue;
double dTimeOfMax = double.NaN;
double dTimeOfMin = double.NaN;
double eu;
MeasurementUnit channelUnit = GetChannelUnit();
string actualUnit = GetEngineeringUnits(aic);
double scaleFactor = 1D;
try
{
scaleFactor = channelUnit.GetScalerConversionFrom(actualUnit);//GetScaleFactor(channelUnit, actualUnit);
System.Diagnostics.Trace.WriteLine("Scaling " + gc.Id + " by " + scaleFactor.ToString());
}
catch (System.Exception) { }
for (ulong z = 0; z < aic.ParentModule.NumberOfSamples; z++)
{
try
{
if (dCurrentTime >= DomainMin && dCurrentTime <= DomainMax)
{
eu = aic.DataEu[Convert.ToInt32(z)];
eu *= scaleFactor;
xAxis.Add(dCurrentTime);
yAxis.Add(eu);
if (dMax < eu) { dMax = eu; dTimeOfMax = dCurrentTime; }
if (dMin > eu) { dMin = eu; dTimeOfMin = dCurrentTime; }
}
}
catch (System.Exception ex)
{
DTS.Utilities.Logging.APILogger.Log("Failed to access sample at index: ", z, " of ", aic.ChannelDescriptionString, " which reports: ",
aic.ParentModule.NumberOfSamples, " samples.", ex);
if (z < (aic.ParentModule.NumberOfSamples - 1))
{
System.Windows.Forms.MessageBox.Show("Incomplete data for " + aic.ChannelDescriptionString + " please check data file: ", ex.Message);
}
}
dCurrentTime += dIncrement;
}
chart.ChartGroups[0].ChartData.SeriesList[i].X.CopyDataIn(xAxis.ToArray());
chart.ChartGroups[0].ChartData.SeriesList[i].Y.CopyDataIn(yAxis.ToArray());
if (double.MinValue != dMax)
{
if (aic is DTS.Slice.Control.Event.Module.AnalogInputChannel)
{
System.Diagnostics.Trace.WriteLine("Max for " + gc.Id + " is " + dMax.ToString() + " scale is " + aic.ScaleFactorMv.ToString());
}
gc.DataMax = dMax; gc.TimeOfMax = dTimeOfMax;
}
if (double.MaxValue != dMin) { gc.DataMin = dMin; gc.TimeOfMin = dTimeOfMin; }
}
int startIndex = chart.ChartGroups[0].ChartData.SeriesList.Count;
if (ThresholdsActive)
{
for (int i = 0; i < _thresholds.Count; i++)
{
chart.ChartGroups[0].ChartData.SeriesList.AddNewSeries();
chart.ChartGroups[0].ChartData.SeriesList[startIndex].LineStyle.Color = System.Drawing.Color.Black;
chart.ChartGroups[0].ChartData.SeriesList[startIndex].LineStyle.Thickness = 2;
chart.ChartGroups[0].ChartData.SeriesList[startIndex].SymbolStyle.Shape = C1.Win.C1Chart.SymbolShapeEnum.None;
chart.ChartGroups[0].ChartData.SeriesList[startIndex].X.CopyDataIn(new double[] { DomainMin, DomainMax });
chart.ChartGroups[0].ChartData.SeriesList[startIndex].Y.CopyDataIn(new double[] { _thresholds[i], _thresholds[i] });
startIndex++;
}
}
if (UseRangeMin) { chart.ChartArea.AxisY.AutoMin = false; chart.ChartArea.AxisY.Min = RangeMin; }
else { chart.ChartArea.AxisY.AutoMin = true; RangeMin = chart.ChartGroups[0].ChartData.MinY; }
if (UseRangeMax) { chart.ChartArea.AxisY.AutoMax = false; chart.ChartArea.AxisY.Max = RangeMax; }
else { chart.ChartArea.AxisY.AutoMax = true; RangeMax = chart.ChartGroups[0].ChartData.MaxY; }
chart.ChartArea.AxisX.Text = string.Format("Time ({0})", TimeUnits);
}
/// <summary>
/// since the CFC changes the channel data, we need to know when it changes and redraw accordingly
/// </summary>
/// <param name="cfc"></param>
public void SetCFC(string cfc, ReportBase report)
{
SensorDB.FilterClass fc = new DTS.SensorDB.FilterClass(cfc);
var e = _channels.GetEnumerator();
while (e.MoveNext())
{
if (null != e.Current.Value.Channel)
{
DTS.Slice.Control.Event.Module.Channel aic = e.Current.Value.Channel.Channel as DTS.Slice.Control.Event.Module.Channel;
if (null == aic) { return; }
switch (fc.FClass)
{
case DTS.SensorDB.FilterClass.FilterClassType.AdHoc:
throw new NotSupportedException();
case DTS.SensorDB.FilterClass.FilterClassType.CFC10:
aic.CurrentFilter = new DTS.Slice.Control.Event.Module.Channel.SaeJ211Filter(DTS.Utilities.ChannelFilter.Class10);
break;
case DTS.SensorDB.FilterClass.FilterClassType.CFC1000:
aic.CurrentFilter = new DTS.Slice.Control.Event.Module.Channel.SaeJ211Filter(DTS.Utilities.ChannelFilter.Class1000);
break;
case DTS.SensorDB.FilterClass.FilterClassType.CFC180:
aic.CurrentFilter = new DTS.Slice.Control.Event.Module.Channel.SaeJ211Filter(DTS.Utilities.ChannelFilter.Class180);
break;
case DTS.SensorDB.FilterClass.FilterClassType.CFC60:
aic.CurrentFilter = new DTS.Slice.Control.Event.Module.Channel.SaeJ211Filter(DTS.Utilities.ChannelFilter.Class60);
break;
case DTS.SensorDB.FilterClass.FilterClassType.CFC600:
aic.CurrentFilter = new DTS.Slice.Control.Event.Module.Channel.SaeJ211Filter(DTS.Utilities.ChannelFilter.Class600);
break;
case DTS.SensorDB.FilterClass.FilterClassType.None:
aic.CurrentFilter = new DTS.Slice.Control.Event.Module.Channel.SaeJ211Filter(DTS.Utilities.ChannelFilter.Unfiltered);
break;
}
}
}
report.DrawGraph(Id, _chart);
//Draw(_chart);
}
/// <summary>
/// gets the scale factor from two different units
/// assumes units are linearly scaled (example G's to m/sec^2)
/// </summary>
/// <param name="desiredUnits"></param>
/// <param name="actualUnits"></param>
/// <returns></returns>
public static double GetScaleFactor(string desiredUnits, string actualUnits)
{
actualUnits = actualUnits.Trim();
//1 g = 9.80665 m / s2
desiredUnits = desiredUnits.Trim().ToLower();
actualUnits = actualUnits.Trim().ToLower();
if ( desiredUnits == actualUnits)
{
return 1D;
}
else if (actualUnits == "g" && desiredUnits.Contains("m/sec") )
{
return 9.80665D;
}
else if (actualUnits.Contains("m/sec") && desiredUnits == "g")
{
return 1D / 9.80665D;
}//"N", "kN"
else if (actualUnits == "n" && desiredUnits == "kn")
{
return .001D;
}
else if (actualUnits == "kn" && desiredUnits == "n")
{
return 1000D;
}
else { throw new NotSupportedException("unknown conversion"); }
}
/// <summary>
/// gets a value of a channel field in the graph
/// (properties of channels)
/// </summary>
/// <param name="id">channel id</param>
/// <param name="field">field</param>
/// <returns>value</returns>
public string GetValue(string id, GraphChannel.Fields field)
{
return _channels[id].GetValue(field);
}
public string GetValueTrunc2Places(string id, GraphChannel.Fields field)
{
return _channels[id].GetValueTrunc2Places(field);
}
/// <summary>
/// gets a value of a graph field
/// (properties of the graph)
/// </summary>
/// <param name="field"></param>
/// <returns></returns>
public string GetValue(Fields field)
{
switch (field)
{
case Fields.DomainMax:
return DomainMax.ToString();
case Fields.DomainMin:
return DomainMin.ToString();
case Fields.RangeMax:
return RangeMax.ToString(Properties.Settings.Default.PROTECTIONREPORT_NumberFormat);
case Fields.RangeMin:
return RangeMin.ToString(Properties.Settings.Default.PROTECTIONREPORT_NumberFormat);
case Fields.ThresholdInUse:
return ThresholdsActive.ToString();
case Fields.Thresholds:
return Thresholds;
case Fields.UseRangeMax:
return UseRangeMax.ToString();
case Fields.UseRangeMin:
return UseRangeMin.ToString();
default:
throw new NotImplementedException();
}
}
/// <summary>
/// sets the value for a field of the graph
/// (properties of graph)
/// </summary>
/// <param name="field"></param>
/// <param name="value"></param>
public void SetValue(Fields field, string value)
{
switch (field)
{
case Fields.DomainMax:
{
double d;
if (double.TryParse(value.Trim(), out d))
{
DomainMax = d;
}
}
break;
case Fields.DomainMin:
{
double d;
if (double.TryParse(value.Trim(), out d))
{
DomainMin = d;
}
}
break;
case Fields.RangeMax:
{
double d;
if (double.TryParse(value.Trim(), out d))
{
RangeMax = d;
}
}
break;
case Fields.RangeMin:
{
double d;
if (double.TryParse(value.Trim(), out d))
{
RangeMin = d;
}
}
break;
case Fields.ThresholdInUse:
{
bool b;
if (Boolean.TryParse(value.Trim(), out b))
{
ThresholdsActive = b;
}
}
break;
case Fields.Thresholds:
{
Thresholds = value;
}
break;
case Fields.UseRangeMax:
{
UseRangeMax = Convert.ToBoolean(value.Trim());
}
break;
case Fields.UseRangeMin:
{
UseRangeMin = Convert.ToBoolean(value.Trim());
}
break;
}
}
/// <summary>
/// whether graph minimum range is constrained
/// </summary>
private bool _useRangeMin = false;
public bool UseRangeMin
{
get { return _useRangeMin; }
set
{
SetProperty(ref _useRangeMin, value, "UseRangeMin");
Draw(_chart);
}
}
/// <summary>
/// whether graph maximum range is constrained
/// </summary>
private bool _useRangeMax = false;
public bool UseRangeMax
{
get { return _useRangeMax; }
set
{
SetProperty(ref _useRangeMax, value, "UseRangeMax");
Draw(_chart);
}
}
/// <summary>
/// fields/properties of a graph
/// </summary>
public enum Fields
{
RangeMin,
UseRangeMin,
RangeMax,
UseRangeMax,
DomainMin,
DomainMax,
Thresholds,
ThresholdInUse
}
/// <summary>
/// time units for the graph (seconds or ms)
/// </summary>
private string _timeUnits = "s";
public string TimeUnits
{
get { return _timeUnits; }
set
{
if (_timeUnits != value)
{
if (value == "ms") { DomainMin = _domainMin * 1000D; DomainMax = _domainMax * 1000D; }
else { DomainMin = _domainMin / 1000D; DomainMax = _domainMax / 1000D; }
SetProperty(ref _timeUnits, value, "TimeUnits");
//convert domain min/max to new units..
Draw(_chart);
}
}
}
/// <summary>
/// these functions make us WPF/dependency property friendly, and also
/// adds a convenient way for us to notify consumers when a property has changed
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value, String propertyName)
{
if (object.Equals(storage, value)) return false;
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged(string propertyName)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// graph id
/// </summary>
private string _id;
public string Id { get { return _id; } }
/// <summary>
/// display name for graph
/// </summary>
private string _displayName;
public string DisplayName { get { return _displayName; } set { SetProperty(ref _displayName, value, "DisplayName"); } }
/// <summary>
/// the minimum range of the graph (use UseRangeMin) to see if the graph is constrained to min or not
/// </summary>
private double _rangeMin;
public double RangeMin
{
get { return _rangeMin; }
set
{
if (value != _rangeMin)
{
SetProperty(ref _rangeMin, value, "RangeMin");
if (UseRangeMin) { Draw(_chart); }
}
}
}
/// <summary>
/// the maximum range of the graph (use UseRangeMax to determine if graph is constrained to max or not)
/// </summary>
private double _rangeMax;
public double RangeMax
{
get { return _rangeMax; }
set
{
if (value != _rangeMax)
{
SetProperty(ref _rangeMax, value, "RangeMax");
if (UseRangeMax) { Draw(_chart); }
}
}
}
/// <summary>
/// graph is always domain constrained, this is the min x axis value
/// default is -.5s
/// </summary>
private double _domainMin = -.5;
public double DomainMin
{
get { return _domainMin; }
set
{
SetProperty(ref _domainMin, value, "DomainMin");
Draw(_chart);
}
}
/// <summary>
/// max x axis value (default is .5s)
/// </summary>
private double _domainMax = .5;
public double DomainMax
{
get { return _domainMax; }
set
{
if (value != _domainMax)
{
SetProperty(ref _domainMax, value, "DomainMax");
Draw(_chart);
}
}
}
/// <summary>
/// collection of thresholds (just horizontal lines for now)
/// </summary>
private List<double> _thresholds = new List<double>();
public string Thresholds
{
get
{
List<string> thresholds = new List<string>();
foreach (var d in _thresholds)
{
if (!thresholds.Contains(d.ToString())) { thresholds.Add(d.ToString()); }
}
return string.Join(",", thresholds.ToArray());
}
set
{
string[] tokens = value.Split(new char[] { ',' });
List<double> thresholds = new List<double>();
foreach (string token in tokens)
{
double d;
if (double.TryParse(token.Trim(), out d))
{
if (!thresholds.Contains(d)) { thresholds.Add(d); }
}
}
SetProperty(ref _thresholds, thresholds, "Thresholds");
if (ThresholdsActive) { Draw(_chart); }
}
}
/// <summary>
/// draw thresholds or not
/// </summary>
private bool _bThresholdsActive = false;
public bool ThresholdsActive
{
get { return _bThresholdsActive; }
set { SetProperty(ref _bThresholdsActive, value, "ThresholdsActive"); Draw(_chart); }
}
/// <summary>
/// list of channel ids for channels in the graph
/// </summary>
private List<string> _channelIds = new List<string>();
private Dictionary<string, GraphChannel> _channels = new Dictionary<string, GraphChannel>();
private MeasurementUnit[] _channelUnits;
public MeasurementUnit[] GetChannelUnits() { return _channelUnits; }
private MeasurementUnit _channelUnit;
public MeasurementUnit GetChannelUnit() { return _channelUnit; }
public void SetChannelUnit(MeasurementUnit mu)
{
SetProperty(ref _channelUnit, mu, "ChannelUnit");
var e = _channels.GetEnumerator();
while (e.MoveNext())
{
string eu = mu.MainDisplayUnit;
if (null == e.Current.Value) { continue; }
if (null == e.Current.Value.Channel) { continue; }
if (null == e.Current.Value.Channel.Channel) { continue; }
if (e.Current.Value.Channel.Channel is DTS.DAS.Concepts.DAS.Channel.IEngineeringUnitAware)
{
if (!(e.Current.Value.Channel.Channel is DTS.Slice.Control.Event.Module.AnalogInputChannel))
{
continue;
}
eu = ((DTS.DAS.Concepts.DAS.Channel.IEngineeringUnitAware)e.Current.Value.Channel.Channel).EngineeringUnits;
if (!mu.UnitMatches(eu))
{
double scaler = mu.GetScalerConversionFrom(eu);
System.Diagnostics.Trace.WriteLine("Changed scale factor for " + e.Current.Value.Channel.Channel.ChannelDescriptionString + " from "
+ e.Current.Value.Channel.Channel.ScaleFactorMv.ToString() + " to " + (e.Current.Value.Channel.Channel.ScaleFactorMv * scaler).ToString());
e.Current.Value.Channel.Channel.ScaleFactorMv *= scaler;
((DTS.DAS.Concepts.DAS.Channel.IEngineeringUnitAware)e.Current.Value.Channel.Channel).EngineeringUnits = mu.ToString();
}
}
}
}
public ReportGraph(string id, string displayname, MeasurementUnit [] possibleUnits, GraphChannel[] channels, string thresholds)
{
_id = id;
DisplayName = displayname;
_channelUnits = possibleUnits;
if (null != possibleUnits && possibleUnits.Length >= 1)
{
_channelUnit = possibleUnits[0];
}
foreach (var channel in channels) { _channelIds.Add(channel.Id); _channels.Add(channel.Id, channel); channel.PropertyChanged += new PropertyChangedEventHandler(channel_PropertyChanged); }
Thresholds = thresholds;
}
/// <summary>
/// pass on when a channel or graph property changes
/// this lets UI code react when a property changes
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void channel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Channel")
{
OnPropertyChanged("Channel");
}
else
{
GraphChannel gc = sender as GraphChannel;
OnPropertyChanged("graph-x-" + Id + "-x-" + gc.Id + "-x-" + e.PropertyName);
}
}
/// <summary>
/// the reviewtestchannel is the actual analoginputdaschannel, or the actual binary data for the file
/// </summary>
/// <param name="graphchannel"></param>
/// <returns></returns>
public ReviewTestChannel GetReviewTestChannel(string graphchannel)
{
return _channels[graphchannel].Channel;
}
public void SetReviewTestChannel(string graphchannel, ReviewTestChannel channel)
{
if (null != channel)
{
if (channel.Channel is DTS.Slice.Control.Event.Module.AnalogInputChannel)
{
MeasurementUnit unit = GetChannelUnit();
if (null != channel && null != channel.Channel)
{
var channelEU = (channel.Channel as DTS.DAS.Concepts.DAS.Channel.IEngineeringUnitAware).EngineeringUnits;
if (unit.UnitMatches(channelEU))
{
DTS.Slice.Control.Event.Module.AnalogInputChannel analog = channel.Channel as DTS.Slice.Control.Event.Module.AnalogInputChannel;
double scaler = unit.GetScalerConversionFrom(channelEU);
System.Diagnostics.Trace.WriteLine("Changing " + analog.ChannelDescriptionString + " from " + analog.ScaleFactorMv.ToString() + " to " + (analog.ScaleFactorMv * scaler).ToString());
analog.ScaleFactorMv *= scaler;
analog.EngineeringUnits = unit.ToString();
}
var graphChannel = GetGraphChannel(graphchannel);
if (graphChannel.UseOffset)
{
channel.Channel.UserOffsetEU = graphChannel.Offset;
}
else
{
channel.Channel.UserOffsetEU = 0D;
}
}
}
}
_channels[graphchannel].Channel = channel;
}
public GraphChannel GetGraphChannel(string graphChannel)
{
return _channels[graphChannel];
}
/// <summary>
/// returns all viable channels to fullfill the graph channel
/// </summary>
/// <param name="target"></param>
/// <param name="test"></param>
/// <returns></returns>
public ReviewTestChannel[] GetAvailableChannels(string target, ReviewTest test)
{
List<ReviewTestChannel> channels = new List<ReviewTestChannel>();
if (null != test)
{
try
{
foreach (var channel in test.Channels)
{
bool bAdded = false;
string eu = GetEngineeringUnits(channel.Channel);
foreach (var mu in GetChannelUnits())
{
if (bAdded) { continue; }
if (mu.UnitMatches(eu.Trim()))
{
if (!channels.Contains(channel)) { channels.Add(channel); bAdded = true; }
}
}
}
}
catch (System.Exception ex)
{
System.Windows.Forms.MessageBox.Show(string.Format("Test missing data\r\n[{0}]", ex.Message), "Warning", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
}
}
return channels.ToArray();
}
public void Clear(ReviewTest test)
{
var e = _channels.GetEnumerator();
while (e.MoveNext())
{
if (null != e.Current.Value.Channel) { e.Current.Value.Channel.Cleanup(); }
e.Current.Value.Channel = null;
}
}
public void SetDefaults(ReviewTest test)
{
var e = _channels.GetEnumerator();
Dictionary<string, ReviewTestChannel> lookup = new Dictionary<string, ReviewTestChannel>();
while (e.MoveNext())
{
var bestchoices = from channel in GetAvailableChannels(e.Current.Key, test) where (channel.Channel as Slice.Control.Event.Module.AnalogInputChannel).Description.ToLower().Contains(e.Current.Value.ChannelNameHint.ToLower()) select channel;
if (null != bestchoices && bestchoices.Count() > 0 && null == e.Current.Value.Channel)
{
lookup.Add(e.Current.Key, bestchoices.First());
}
}
try
{
var e2 = lookup.GetEnumerator();
while (e2.MoveNext())
{
SetReviewTestChannel(e2.Current.Key, e2.Current.Value);
}
}
catch (System.Exception) { }
}
/// <summary>
/// returns the x plot for a given channel
/// </summary>
/// <param name="id">id of channel to get</param>
/// <returns></returns>
public double[] GetXPlot(string id)
{
int index = _channelIds.IndexOf(id);
if (null != _chart) { return _chart.ChartGroups[0].ChartData.SeriesList[index].X.Cast<double>().ToArray(); }
else return new double[0];
}
/// <summary>
/// returns the y plot for a given channel
/// </summary>
/// <param name="id">channel id</param>
/// <returns></returns>
public double[] GetYPlot(string id)
{
int index = _channelIds.IndexOf(id);
if (null != _chart) { return _chart.ChartGroups[0].ChartData.SeriesList[index].Y.Cast<double>().ToArray(); }
else return new double[0];
}
}
}