diff --git a/RealTimeGraphX.UWP.Demo/RealTimeGraphX.UWP.Demo.csproj b/RealTimeGraphX.UWP.Demo/RealTimeGraphX.UWP.Demo.csproj index 35a4a01..a51fdb6 100644 --- a/RealTimeGraphX.UWP.Demo/RealTimeGraphX.UWP.Demo.csproj +++ b/RealTimeGraphX.UWP.Demo/RealTimeGraphX.UWP.Demo.csproj @@ -11,7 +11,7 @@ RealTimeGraphX.UWP.Demo en-US UAP - 10.0.17134.0 + 10.0.17763.0 10.0.17134.0 14 512 diff --git a/RealTimeGraphX.UWP/RealTimeGraphX.UWP.csproj b/RealTimeGraphX.UWP/RealTimeGraphX.UWP.csproj index 65d5daa..52802ad 100644 --- a/RealTimeGraphX.UWP/RealTimeGraphX.UWP.csproj +++ b/RealTimeGraphX.UWP/RealTimeGraphX.UWP.csproj @@ -11,7 +11,7 @@ RealTimeGraphX.UWP en-US UAP - 10.0.17134.0 + 10.0.17763.0 10.0.17134.0 14 512 diff --git a/RealTimeGraphX.WPF.Demo/MainWindow.xaml b/RealTimeGraphX.WPF.Demo/MainWindow.xaml index b194b28..eb5849a 100644 --- a/RealTimeGraphX.WPF.Demo/MainWindow.xaml +++ b/RealTimeGraphX.WPF.Demo/MainWindow.xaml @@ -10,194 +10,209 @@ mc:Ignorable="d" Title="RealtimeGraphX WPF Demo" Height="587" Width="1168.5" d:DataContext="{d:DesignData Type=local:MainWindowVM, IsDesignTimeCreatable=False}"> - - - - - - DodgerBlue - Red - Green - - - - - - - - - - - - - - - - - - - - - - - - - Duration - - - Refresh Rate - - - Minimum Y - - - Maximum Y - - - Auto Range (Y) - - Stroke - - - Thickness - - - Fill - - - Display ToolTip - - Paused - - - - - - - - - - - - - - - - - Duration - - - Refresh Rate - - - Minimum Y - - - Maximum Y - - - Auto Range (Y) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + DodgerBlue + Red + Green + + + + + + + + + + + + + + + + + + + + + + + + + Duration + + + Refresh Rate + + + Minimum Y + + + Maximum Y + + + Auto Range (Y) + + Stroke + + + Thickness + + + Fill + + + Display ToolTip + + Paused + + + + + + + + + + + + + + + + + + Duration + + + Refresh Rate + + + Minimum Y + + + Maximum Y + + + Auto Range (Y) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Format: - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + Format: - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RealTimeGraphX.WPF.Demo/MainWindowVM.cs b/RealTimeGraphX.WPF.Demo/MainWindowVM.cs index 5a8a373..7c73570 100644 --- a/RealTimeGraphX.WPF.Demo/MainWindowVM.cs +++ b/RealTimeGraphX.WPF.Demo/MainWindowVM.cs @@ -32,11 +32,12 @@ public MainWindowVM() Controller.Range.MaximumX = TimeSpan.FromSeconds(10); Controller.Range.AutoY = true; Controller.Range.AutoYFallbackMode = GraphRangeAutoYFallBackMode.MinMax; + Controller.Range.XStartBehavior = XStartBehavior.slideInFromRight ; Controller.DataSeriesCollection.Add(new WpfGraphDataSeries() { Name = "Series", - Stroke = Colors.DodgerBlue, + Stroke = Colors.Red, }); MultiController = new WpfGraphController(); @@ -44,6 +45,7 @@ public MainWindowVM() MultiController.Range.MaximumY = 1080; MultiController.Range.MaximumX = TimeSpan.FromSeconds(10); MultiController.Range.AutoY = true; + MultiController.Range.XStartBehavior = XStartBehavior.fillFromLeft ; MultiController.DataSeriesCollection.Add(new WpfGraphDataSeries() { diff --git a/RealTimeGraphX.WPF.Demo/Properties/AssemblyInfo.cs b/RealTimeGraphX.WPF.Demo/Properties/AssemblyInfo.cs index 7e50401..9910ab1 100644 --- a/RealTimeGraphX.WPF.Demo/Properties/AssemblyInfo.cs +++ b/RealTimeGraphX.WPF.Demo/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.0.0.3")] +[assembly: AssemblyFileVersion("1.0.0.3")] diff --git a/RealTimeGraphX.WPF.Demo/RealTimeGraphX.WPF.Demo.csproj b/RealTimeGraphX.WPF.Demo/RealTimeGraphX.WPF.Demo.csproj index 352ea02..684db45 100644 --- a/RealTimeGraphX.WPF.Demo/RealTimeGraphX.WPF.Demo.csproj +++ b/RealTimeGraphX.WPF.Demo/RealTimeGraphX.WPF.Demo.csproj @@ -58,7 +58,6 @@ - MSBuild:Compile Designer @@ -71,10 +70,6 @@ MainWindow.xaml Code - - MSBuild:Compile - Designer - diff --git a/RealTimeGraphX.WPF.Demo/Themes/Generic.xaml b/RealTimeGraphX.WPF.Demo/Themes/Generic.xaml deleted file mode 100644 index 1174e5b..0000000 --- a/RealTimeGraphX.WPF.Demo/Themes/Generic.xaml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - diff --git a/RealTimeGraphX.WPF.Demo/WpfGraphControl.cs b/RealTimeGraphX.WPF.Demo/WpfGraphControl.cs deleted file mode 100644 index cbace56..0000000 --- a/RealTimeGraphX.WPF.Demo/WpfGraphControl.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; - -namespace RealTimeGraphX.WPF.Demo -{ - public class WpfGraphControl : Control - { - /// - /// Gets or sets the graph controller. - /// - public IGraphController Controller - { - get { return (IGraphController)GetValue(ControllerProperty); } - set { SetValue(ControllerProperty, value); } - } - public static readonly DependencyProperty ControllerProperty = - DependencyProperty.Register("Controller", typeof(IGraphController), typeof(WpfGraphControl), new PropertyMetadata(null)); - - /// - /// Gets or sets a value indicating whether to display a tool tip with the current cursor value. - /// - public bool DisplayToolTip - { - get { return (bool)GetValue(DisplayToolTipProperty); } - set { SetValue(DisplayToolTipProperty, value); } - } - public static readonly DependencyProperty DisplayToolTipProperty = - DependencyProperty.Register("DisplayToolTip", typeof(bool), typeof(WpfGraphControl), new PropertyMetadata(false)); - - /// - /// Gets or sets the string format for the X Axis. - /// - public String StringFormatX - { - get { return (String)GetValue(StringFormatXProperty); } - set { SetValue(StringFormatXProperty, value); } - } - public static readonly DependencyProperty StringFormatXProperty = - DependencyProperty.Register("StringFormatX", typeof(String), typeof(WpfGraphControl), new PropertyMetadata("0.0")); - - /// - /// Gets or sets the string format for the Y Axis. - /// - public String StringFormatY - { - get { return (String)GetValue(StringFormatYProperty); } - set { SetValue(StringFormatYProperty, value); } - } - public static readonly DependencyProperty StringFormatYProperty = - DependencyProperty.Register("StringFormatY", typeof(String), typeof(WpfGraphControl), new PropertyMetadata("hh\\:mm\\:ss")); - - - /// - /// Initializes the class. - /// - static WpfGraphControl() - { - DefaultStyleKeyProperty.OverrideMetadata(typeof(WpfGraphControl), new FrameworkPropertyMetadata(typeof(WpfGraphControl))); - } - } -} diff --git a/RealTimeGraphX.WPF/Properties/AssemblyInfo.cs b/RealTimeGraphX.WPF/Properties/AssemblyInfo.cs index a08459d..9b0cc8e 100644 --- a/RealTimeGraphX.WPF/Properties/AssemblyInfo.cs +++ b/RealTimeGraphX.WPF/Properties/AssemblyInfo.cs @@ -16,8 +16,8 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.0.0.3")] +[assembly: AssemblyFileVersion("1.0.0.3")] diff --git a/RealTimeGraphX.WPF/RealTimeGraphX.WPF.csproj b/RealTimeGraphX.WPF/RealTimeGraphX.WPF.csproj index 6287cae..6127f07 100644 --- a/RealTimeGraphX.WPF/RealTimeGraphX.WPF.csproj +++ b/RealTimeGraphX.WPF/RealTimeGraphX.WPF.csproj @@ -57,6 +57,7 @@ + diff --git a/RealTimeGraphX.WPF/Themes/Generic.xaml b/RealTimeGraphX.WPF/Themes/Generic.xaml index a309fc0..3b8e94c 100644 --- a/RealTimeGraphX.WPF/Themes/Generic.xaml +++ b/RealTimeGraphX.WPF/Themes/Generic.xaml @@ -1,15 +1,35 @@ - + + + + + + + + diff --git a/RealTimeGraphX.WPF/WpfGraphAxisControl.cs b/RealTimeGraphX.WPF/WpfGraphAxisControl.cs index aa738b2..fe3f7a2 100644 --- a/RealTimeGraphX.WPF/WpfGraphAxisControl.cs +++ b/RealTimeGraphX.WPF/WpfGraphAxisControl.cs @@ -68,16 +68,18 @@ internal ObservableCollection Items public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register("Items", typeof(ObservableCollection), typeof(WpfGraphAxisControl), new PropertyMetadata(null)); + internal int Ticks => Divisions + 1 ; + /// - /// Gets or sets the number of ticks to display on the control. + /// Gets or sets the number of divisions to display on the control. /// - public int Ticks + public int Divisions { - get { return (int)GetValue(TicksProperty); } - set { SetValue(TicksProperty, value); } + get { return (int)GetValue(DivisionsProperty); } + set { SetValue(DivisionsProperty, value); } } - public static readonly DependencyProperty TicksProperty = - DependencyProperty.Register("Ticks", typeof(int), typeof(WpfGraphAxisControl), new PropertyMetadata(9, (d, e) => (d as WpfGraphAxisControl).OnTicksChanged())); + public static readonly DependencyProperty DivisionsProperty = + DependencyProperty.Register("Divisions", typeof(int), typeof(WpfGraphAxisControl), new PropertyMetadata(8, (d, e) => (d as WpfGraphAxisControl).OnDivisionsChanged())); /// /// Gets or sets the string format which is used to format the ticks value. @@ -99,13 +101,13 @@ public override void OnApplyTemplate() _items_control = GetTemplateChild("PART_ItemsControl") as ItemsControl; - _items_control.Loaded += (x, e) => + _items_control.Loaded += (x, e) => { ItemsPresenter itemsPresenter = GetVisualChild(_items_control); _axisPanel = VisualTreeHelper.GetChild(itemsPresenter, 0) as WpfGraphAxisPanel; }; - OnTicksChanged(); + OnDivisionsChanged(); } private static T GetVisualChild(DependencyObject parent) where T : Visual @@ -129,6 +131,7 @@ private static T GetVisualChild(DependencyObject parent) where T : Visual return child; } +#if false /// /// Called when the property has changed. /// @@ -140,6 +143,19 @@ protected virtual void OnTicksChanged() _axisPanel?.UpdatePanel(); } +#endif + + /// + /// Called when the property has changed. + /// + protected virtual void OnDivisionsChanged() + { + Items = new ObservableCollection(Enumerable.Range(0, Ticks).Select(x => new WpfGraphAxisTickData())); + + Controller?.RequestVirtualRangeChange(); + + _axisPanel?.UpdatePanel(); + } protected override void OnControllerChanged(IGraphController oldController, IGraphController newController) { diff --git a/RealTimeGraphX.WPF/WpfGraphControl.cs b/RealTimeGraphX.WPF/WpfGraphControl.cs new file mode 100644 index 0000000..83713df --- /dev/null +++ b/RealTimeGraphX.WPF/WpfGraphControl.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace RealTimeGraphX.WPF +{ + public class WpfGraphControl : Control + { + /// + /// Gets or sets the graph controller. + /// + public IGraphController Controller + { + get { return (IGraphController)GetValue(ControllerProperty); } + set { SetValue(ControllerProperty, value); } + } + public static readonly DependencyProperty ControllerProperty = + DependencyProperty.Register("Controller", typeof(IGraphController), typeof(WpfGraphControl), new PropertyMetadata(null)); + + /// + /// Gets or sets a value indicating whether to display a tool tip with the current cursor value. + /// + public bool DisplayToolTip + { + get { return (bool)GetValue(DisplayToolTipProperty); } + set { SetValue(DisplayToolTipProperty, value); } + } + public static readonly DependencyProperty DisplayToolTipProperty = + DependencyProperty.Register("DisplayToolTip", typeof(bool), typeof(WpfGraphControl), new PropertyMetadata(false)); + + /// + /// Gets or sets the string format for the X Axis. + /// + public String StringFormatX + { + get { return (String)GetValue(StringFormatXProperty); } + set { SetValue(StringFormatXProperty, value); } + } + public static readonly DependencyProperty StringFormatXProperty = + DependencyProperty.Register("StringFormatX", typeof(String), typeof(WpfGraphControl), new PropertyMetadata("hh\\:mm\\:ss")); + + /// + /// Gets or sets the string format for the Y Axis. + /// + public String StringFormatY + { + get { return (String)GetValue(StringFormatYProperty); } + set { SetValue(StringFormatYProperty, value); } + } + public static readonly DependencyProperty StringFormatYProperty = + DependencyProperty.Register("StringFormatY", typeof(String), typeof(WpfGraphControl), new PropertyMetadata("0.0")); + + + /// + /// Gets or sets the caption of the Y axis + /// + public String AxisCaptionY + { + get { return (String)GetValue(AxisCaptionYProperty); } + set { SetValue(AxisCaptionYProperty, value); } + } + public static readonly DependencyProperty AxisCaptionYProperty = + DependencyProperty.Register("AxisCaptionY", typeof(String), typeof(WpfGraphControl), new PropertyMetadata(null)); + + /// + /// Gets or sets the caption of the X axis + /// + public String AxisCaptionX + { + get { return (String)GetValue(AxisCaptionXProperty); } + set { SetValue(AxisCaptionXProperty, value); } + } + public static readonly DependencyProperty AxisCaptionXProperty = + DependencyProperty.Register("AxisCaptionX", typeof(String), typeof(WpfGraphControl), new PropertyMetadata(null)); + + /// + /// Gets or sets the brush used for the grid lines + /// + public Brush GridLinesBrush + { + get { return (Brush)GetValue(GridLinesBrushProperty); } + set { SetValue(GridLinesBrushProperty, value); } + } + public static readonly DependencyProperty GridLinesBrushProperty = + DependencyProperty.Register( "GridLinesBrush", typeof(Brush), typeof(WpfGraphControl), new PropertyMetadata((SolidColorBrush)new BrushConverter().ConvertFrom("#FF2E2E2E")) ) ; + + /// + /// Gets or sets the corner radius for the border + /// + public CornerRadius CornerRadius + { + get { return (CornerRadius)GetValue(CornerRadiusProperty); } + set { SetValue(CornerRadiusProperty, value); } + } + public static readonly DependencyProperty CornerRadiusProperty = + DependencyProperty.Register( "CornerRadius", typeof(CornerRadius), typeof(WpfGraphControl), new PropertyMetadata(new CornerRadius(5)) ) ; + + /// + /// Boolean property so show or hide the X-Axis + /// + public bool ShowAxisX + { + get { return (bool)GetValue(ShowAxisXProperty); } + set { SetValue(ShowAxisXProperty, value); } + } + public static readonly DependencyProperty ShowAxisXProperty = + DependencyProperty.Register("ShowAxisX", typeof(bool), typeof(WpfGraphControl), new PropertyMetadata(true)); + + /// + /// Boolean property so show or hide the Y-Axis + /// + public bool ShowAxisY + { + get { return (bool)GetValue(ShowAxisYProperty); } + set { SetValue(ShowAxisYProperty, value); } + } + public static readonly DependencyProperty ShowAxisYProperty = + DependencyProperty.Register("ShowAxisY", typeof(bool), typeof(WpfGraphControl), new PropertyMetadata(true)); + + /// + /// Width of the Y axis, default 70. + /// + public int WidthAxisY + { + get { return (int)GetValue(WidthAxisYProperty); } + set { SetValue(WidthAxisYProperty, value); } + } + public static readonly DependencyProperty WidthAxisYProperty = + DependencyProperty.Register("WidthAxisY", typeof(int), typeof(WpfGraphControl), new PropertyMetadata(70)); + + /// + /// Number of divisions in the Y axis + /// + public int DivisionsY + { + get { return (int)GetValue(DivisionsYProperty); } + set { SetValue(DivisionsYProperty, value); } + } + public static readonly DependencyProperty DivisionsYProperty = + DependencyProperty.Register("DivisionsY", typeof(int), typeof(WpfGraphControl), new PropertyMetadata(8)); + + /// + /// Number of divisions in the X axis + /// + public int DivisionsX + { + get { return (int)GetValue(DivisionsXProperty); } + set { SetValue(DivisionsXProperty, value); } + } + public static readonly DependencyProperty DivisionsXProperty = + DependencyProperty.Register("DivisionsX", typeof(int), typeof(WpfGraphControl), new PropertyMetadata(8)); + + /// + /// Initializes the class. + /// + static WpfGraphControl() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(WpfGraphControl), new FrameworkPropertyMetadata(typeof(WpfGraphControl))); + } + } +} diff --git a/RealTimeGraphX/DataPoints/DateTimeDataPoint.cs b/RealTimeGraphX/DataPoints/DateTimeDataPoint.cs index 86f8099..eb39470 100644 --- a/RealTimeGraphX/DataPoints/DateTimeDataPoint.cs +++ b/RealTimeGraphX/DataPoints/DateTimeDataPoint.cs @@ -184,5 +184,21 @@ protected override DateTimeDataPoint OnGetDefaultMargins() { return new DateTimeDataPoint(new DateTime(1, 1, 1, 1, 1, 1)); } + + /// + /// Expands the range by adding factor * ( max - min ) to the max and subtracting the same value from the min. + /// + /// + /// + /// + public override void ExpandRange ( ref GraphDataPoint min, ref GraphDataPoint max, double factor ) + { + DateTime dMin = min as DateTimeDataPoint; + DateTime dMax = max as DateTimeDataPoint; + long delta = dMax.Ticks - dMin.Ticks ; + long margin = (long)(factor * delta) ; + max = new DateTimeDataPoint ( new DateTime ( dMax.Ticks + margin ) ) ; + min = new DateTimeDataPoint ( new DateTime ( dMin.Ticks - margin ) ) ; + } } } diff --git a/RealTimeGraphX/DataPoints/DoubleDataPoint.cs b/RealTimeGraphX/DataPoints/DoubleDataPoint.cs index 14f4a9b..0455a38 100644 --- a/RealTimeGraphX/DataPoints/DoubleDataPoint.cs +++ b/RealTimeGraphX/DataPoints/DoubleDataPoint.cs @@ -184,5 +184,21 @@ protected override DoubleDataPoint OnGetDefaultMargins() { return new DoubleDataPoint(0.5); } + + /// + /// Expands the range by adding factor * ( max - min ) to the max and subtracting the same value from the min. + /// + /// + /// + /// + public override void ExpandRange ( ref GraphDataPoint min, ref GraphDataPoint max, double factor ) + { + double minimum = (double)min.GetValue(); + double maximum = (double)max.GetValue(); + double delta = maximum - minimum ; + double margin = factor * delta ; + max = new DoubleDataPoint ( (max as DoubleDataPoint).Value + margin ) ; + min = new DoubleDataPoint ( (min as DoubleDataPoint).Value - margin ) ; + } } } diff --git a/RealTimeGraphX/DataPoints/FloatDataPoint.cs b/RealTimeGraphX/DataPoints/FloatDataPoint.cs index 146a700..09923fe 100644 --- a/RealTimeGraphX/DataPoints/FloatDataPoint.cs +++ b/RealTimeGraphX/DataPoints/FloatDataPoint.cs @@ -184,5 +184,21 @@ protected override FloatDataPoint OnGetDefaultMargins() { return new FloatDataPoint(0.5f); } + + /// + /// Expands the range by adding factor * ( max - min ) to the max and subtracting the same value from the min. + /// + /// + /// + /// + public override void ExpandRange ( ref GraphDataPoint min, ref GraphDataPoint max, double factor ) + { + float minimum = (float)min.GetValue(); + float maximum = (float)max.GetValue(); + float delta = maximum - minimum ; + float margin = (float)(factor * delta) ; + max = new FloatDataPoint ( (max as FloatDataPoint).Value + margin ) ; + min = new FloatDataPoint ( (min as FloatDataPoint).Value - margin ) ; + } } } diff --git a/RealTimeGraphX/DataPoints/Int32DataPoint.cs b/RealTimeGraphX/DataPoints/Int32DataPoint.cs index 9e9ad26..90f89b7 100644 --- a/RealTimeGraphX/DataPoints/Int32DataPoint.cs +++ b/RealTimeGraphX/DataPoints/Int32DataPoint.cs @@ -189,5 +189,21 @@ protected override Int32DataPoint OnGetDefaultMargins() { return new Int32DataPoint(1); } + + /// + /// Expands the range by adding factor * ( max - min ) to the max and subtracting the same value from the min. + /// + /// + /// + /// + public override void ExpandRange ( ref GraphDataPoint min, ref GraphDataPoint max, double factor ) + { + int minimum = (int)min.GetValue(); + int maximum = (int)max.GetValue(); + int delta = maximum - minimum ; + int margin = (int)(factor * delta) ; + max = new Int32DataPoint ( (max as Int32DataPoint).Value + margin ) ; + min = new Int32DataPoint ( (min as Int32DataPoint).Value - margin ) ; + } } } diff --git a/RealTimeGraphX/DataPoints/TimeSpanDataPoint.cs b/RealTimeGraphX/DataPoints/TimeSpanDataPoint.cs index e026c7b..5a62547 100644 --- a/RealTimeGraphX/DataPoints/TimeSpanDataPoint.cs +++ b/RealTimeGraphX/DataPoints/TimeSpanDataPoint.cs @@ -186,5 +186,21 @@ protected override TimeSpanDataPoint OnGetDefaultMargins() { return new TimeSpanDataPoint(TimeSpan.FromSeconds(1)); } + + /// + /// Expands the range by adding factor * ( max - min ) to the max and subtracting the same value from the min. + /// + /// + /// + /// + public override void ExpandRange ( ref GraphDataPoint min, ref GraphDataPoint max, double factor ) + { + double minimum = ((TimeSpan)min.GetValue()).TotalMilliseconds; + double maximum = ((TimeSpan)max.GetValue()).TotalMilliseconds; + double delta = maximum - minimum ; + double margin = factor * delta ; + max = new TimeSpanDataPoint ( TimeSpan.FromMilliseconds ( maximum + margin ) ) ; + min = new TimeSpanDataPoint ( TimeSpan.FromMilliseconds ( minimum - margin ) ) ; + } } } diff --git a/RealTimeGraphX/GraphController.cs b/RealTimeGraphX/GraphController.cs index 19dfd4c..4a3e94e 100644 --- a/RealTimeGraphX/GraphController.cs +++ b/RealTimeGraphX/GraphController.cs @@ -1,237 +1,237 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Text; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Drawing; -using RealTimeGraphX.EventArguments; -using RealTimeGraphX.Renderers; -using System.Diagnostics; - -namespace RealTimeGraphX -{ - /// - /// Represents an base class. - /// - /// The type of the data series. - /// The type of the x data point. - /// The type of the y data point. - /// - /// - public abstract class GraphController : GraphObject, IGraphController - where TXDataPoint : GraphDataPoint - where TYDataPoint : GraphDataPoint - where TDataSeries : IGraphDataSeries - { - private GraphDataQueue> _pending_series_collection; - private Dictionary _to_render; - private DateTime _last_render_time; - private object _render_lock = new object(); - private Thread _render_thread; - private bool _clear; - - #region Pending Series Class - - protected class PendingSeries - { - public TDataSeries Series { get; set; } - public List XX { get; set; } - public List YY { get; set; } - - public int NewItemsCount - { - get { return XX.Count - RenderedItems; } - } - - public int RenderedItems { get; set; } - - public bool IsClearSeries { get; set; } - - public bool IsUpdateSeries { get; set; } - } - - #endregion - - #region Events - - /// - /// Occurs when the current effective minimum/maximum has changed. - /// - public event EventHandler EffectiveRangeChanged; - - /// - /// Occurs when the current virtual (effective minimum/maximum after transformation) minimum/maximum has changed. - /// - public event EventHandler VirtualRangeChanged; - - #endregion - - #region Properties - - /// - /// Gets or sets the controller refresh rate. - /// Higher rate requires more CPU time. - /// - public TimeSpan RefreshRate { get; set; } - - /// - /// Gets or sets a value indicating whether to pause rendering. - /// - public bool IsPaused { get; set; } - - /// - /// Gets or sets a value indicating whether to disable the rendering of data. - /// - public bool DisableRendering { get; set; } - - /// - /// Gets the data series collection. - /// - public ObservableCollection DataSeriesCollection { get; } - - private IGraphRenderer _renderer; - /// - /// Gets or sets the graph renderer. - /// - public IGraphRenderer Renderer - { - get - { - return _renderer; - } - set - { - _renderer = value; RaisePropertyChangedAuto(); - } - } - - private IGraphSurface _surface; - /// - /// Gets or sets the rendering surface. - /// - public IGraphSurface Surface - { - get { return _surface; } - set - { - var previous = _surface; - _surface = value; - RequestVirtualRangeChange(); - OnSurfaceChanged(previous, _surface); - } - } - - private GraphRange _range; - /// - /// Gets or sets the graph range (data point boundaries). - /// - public GraphRange Range - { - get - { - return _range; - } - set - { - _range = value; RaisePropertyChangedAuto(); - } - } - - /// - /// Gets the current effective x-axis minimum. - /// - public GraphDataPoint EffectiveMinimumX { get; private set; } - - /// - /// Gets the current effective x-axis maximum. - /// - public GraphDataPoint EffectiveMaximumX { get; private set; } - - /// - /// Gets the current effective y-axis minimum. - /// - public GraphDataPoint EffectiveMinimumY { get; private set; } - - /// - /// Gets the current effective y-axis maximum. - /// - public GraphDataPoint EffectiveMaximumY { get; private set; } - - /// - /// Gets the current virtual (effective minimum/maximum after transformation) x-axis minimum. - /// - public GraphDataPoint VirtualMinimumX { get; private set; } - - /// - /// Gets the current virtual (effective minimum/maximum after transformation) x-axis maximum. - /// - public GraphDataPoint VirtualMaximumX { get; private set; } - - /// - /// Gets the current virtual (effective minimum/maximum after transformation) y-axis minimum. - /// - public GraphDataPoint VirtualMinimumY { get; private set; } - - /// - /// Gets the current virtual (effective minimum/maximum after transformation) y-axis maximum. - /// - public GraphDataPoint VirtualMaximumY { get; private set; } - - #endregion - - #region Commands - - /// - /// Gets the clear command. - /// - public GraphCommand ClearCommand { get; private set; } - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - public GraphController() - { - Renderer = new ScrollingLineRenderer(); - - DataSeriesCollection = new ObservableCollection(); - Range = new GraphRange(); - - _last_render_time = DateTime.Now; - _to_render = new Dictionary(); - _pending_series_collection = new GraphDataQueue>(); - RefreshRate = TimeSpan.FromMilliseconds(50); - - ClearCommand = new GraphCommand(Clear); - - _render_thread = new Thread(RenderThreadMethod); - _render_thread.IsBackground = true; - _render_thread.Priority = ThreadPriority.Highest; - _render_thread.Start(); - } - - #endregion - - #region Render Thread - - /// - /// The rendering thread method. - /// - private void RenderThreadMethod() - { - while (true) - { - if ((!IsPaused && !DisableRendering) || _clear) - { - try - { - List> pending_lists = new List>(); - +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Drawing; +using RealTimeGraphX.EventArguments; +using RealTimeGraphX.Renderers; +using System.Diagnostics; + +namespace RealTimeGraphX +{ + /// + /// Represents an base class. + /// + /// The type of the data series. + /// The type of the x data point. + /// The type of the y data point. + /// + /// + public abstract class GraphController : GraphObject, IGraphController + where TXDataPoint : GraphDataPoint + where TYDataPoint : GraphDataPoint + where TDataSeries : IGraphDataSeries + { + private GraphDataQueue> _pending_series_collection; + private Dictionary _to_render; + private DateTime _last_render_time; + private object _render_lock = new object(); + private Thread _render_thread; + private bool _clear; + + #region Pending Series Class + + protected class PendingSeries + { + public TDataSeries Series { get; set; } + public List XX { get; set; } + public List YY { get; set; } + + public int NewItemsCount + { + get { return XX.Count - RenderedItems; } + } + + public int RenderedItems { get; set; } + + public bool IsClearSeries { get; set; } + + public bool IsUpdateSeries { get; set; } + } + + #endregion + + #region Events + + /// + /// Occurs when the current effective minimum/maximum has changed. + /// + public event EventHandler EffectiveRangeChanged; + + /// + /// Occurs when the current virtual (effective minimum/maximum after transformation) minimum/maximum has changed. + /// + public event EventHandler VirtualRangeChanged; + + #endregion + + #region Properties + + /// + /// Gets or sets the controller refresh rate. + /// Higher rate requires more CPU time. + /// + public TimeSpan RefreshRate { get; set; } + + /// + /// Gets or sets a value indicating whether to pause rendering. + /// + public bool IsPaused { get; set; } + + /// + /// Gets or sets a value indicating whether to disable the rendering of data. + /// + public bool DisableRendering { get; set; } + + /// + /// Gets the data series collection. + /// + public ObservableCollection DataSeriesCollection { get; } + + private IGraphRenderer _renderer; + /// + /// Gets or sets the graph renderer. + /// + public IGraphRenderer Renderer + { + get + { + return _renderer; + } + set + { + _renderer = value; RaisePropertyChangedAuto(); + } + } + + private IGraphSurface _surface; + /// + /// Gets or sets the rendering surface. + /// + public IGraphSurface Surface + { + get { return _surface; } + set + { + var previous = _surface; + _surface = value; + RequestVirtualRangeChange(); + OnSurfaceChanged(previous, _surface); + } + } + + private GraphRange _range; + /// + /// Gets or sets the graph range (data point boundaries). + /// + public GraphRange Range + { + get + { + return _range; + } + set + { + _range = value; RaisePropertyChangedAuto(); + } + } + + /// + /// Gets the current effective x-axis minimum. + /// + public GraphDataPoint EffectiveMinimumX { get; private set; } + + /// + /// Gets the current effective x-axis maximum. + /// + public GraphDataPoint EffectiveMaximumX { get; private set; } + + /// + /// Gets the current effective y-axis minimum. + /// + public GraphDataPoint EffectiveMinimumY { get; private set; } + + /// + /// Gets the current effective y-axis maximum. + /// + public GraphDataPoint EffectiveMaximumY { get; private set; } + + /// + /// Gets the current virtual (effective minimum/maximum after transformation) x-axis minimum. + /// + public GraphDataPoint VirtualMinimumX { get; private set; } + + /// + /// Gets the current virtual (effective minimum/maximum after transformation) x-axis maximum. + /// + public GraphDataPoint VirtualMaximumX { get; private set; } + + /// + /// Gets the current virtual (effective minimum/maximum after transformation) y-axis minimum. + /// + public GraphDataPoint VirtualMinimumY { get; private set; } + + /// + /// Gets the current virtual (effective minimum/maximum after transformation) y-axis maximum. + /// + public GraphDataPoint VirtualMaximumY { get; private set; } + + #endregion + + #region Commands + + /// + /// Gets the clear command. + /// + public GraphCommand ClearCommand { get; private set; } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public GraphController() + { + Renderer = new ScrollingLineRenderer(); + + DataSeriesCollection = new ObservableCollection(); + Range = new GraphRange(); + + _last_render_time = DateTime.Now; + _to_render = new Dictionary(); + _pending_series_collection = new GraphDataQueue>(); + RefreshRate = TimeSpan.FromMilliseconds(50); + + ClearCommand = new GraphCommand(Clear); + + _render_thread = new Thread(RenderThreadMethod); + _render_thread.IsBackground = true; + _render_thread.Priority = ThreadPriority.Highest; + _render_thread.Start(); + } + + #endregion + + #region Render Thread + + /// + /// The rendering thread method. + /// + private void RenderThreadMethod() + { + while (true) + { + if ((!IsPaused && !DisableRendering) || _clear) + { + try + { + List> pending_lists = new List>(); + var pending_list_first = _pending_series_collection.BlockDequeue(); if (_clear) @@ -246,455 +246,476 @@ private void RenderThreadMethod() _pending_series_collection.BlockDequeue(); } continue; - } - - pending_lists.Add(pending_list_first); - - while (_pending_series_collection.Count > 0) - { - var pending_list = _pending_series_collection.BlockDequeue(); - pending_lists.Add(pending_list); - } - - foreach (var pending_list in pending_lists) - { - foreach (var pending_series in pending_list) - { - if (!pending_series.IsUpdateSeries) - { - if (_to_render.ContainsKey(pending_series.Series)) - { - var s = _to_render[pending_series.Series]; - s.XX.AddRange(pending_series.XX); - s.YY.AddRange(pending_series.YY); - } - else - { - _to_render[pending_series.Series] = pending_series; - } - } - } - } - + } + + pending_lists.Add(pending_list_first); + + while (_pending_series_collection.Count > 0) + { + var pending_list = _pending_series_collection.BlockDequeue(); + pending_lists.Add(pending_list); + } + + foreach (var pending_list in pending_lists) + { + foreach (var pending_series in pending_list) + { + if (!pending_series.IsUpdateSeries) + { + if (_to_render.ContainsKey(pending_series.Series)) + { + var s = _to_render[pending_series.Series]; + s.XX.AddRange(pending_series.XX); + s.YY.AddRange(pending_series.YY); + } + else + { + _to_render[pending_series.Series] = pending_series; + } + } + } + } + if (Surface != null) { Render(); - } - } - catch (Exception ex) - { - Debug.WriteLine($"Error in RealTimeGraphX:\n{ex.ToString()}"); - } - } - else if (IsPaused && !DisableRendering) + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error in RealTimeGraphX:\n{ex.ToString()}"); + } + } + else if (IsPaused && !DisableRendering) { if (Surface != null) { Render(); - } - Thread.Sleep(RefreshRate); - } - else - { - Thread.Sleep(RefreshRate); - } - } - } - - private void Render() - { - if (_to_render.Count > 0) - { - GraphDataPoint min_x = _range.MaximumX - _range.MaximumX; - GraphDataPoint max_x = _range.MaximumX; - GraphDataPoint min_y = _range.MinimumY; - GraphDataPoint max_y = _range.MaximumY; - - if (_to_render.Count > 0 && _to_render.First().Value.XX.Count > 0) - { - min_x = _to_render.First().Value.XX.First(); - max_x = _to_render.First().Value.XX.Last(); - } - else - { - return; - } - - if (_range.AutoY) - { - min_y = _to_render.Select(x => x.Value).SelectMany(x => x.YY).Min(); - max_y = _to_render.Select(x => x.Value).SelectMany(x => x.YY).Max(); - } - - if (min_y == max_y) - { - if (_range.AutoYFallbackMode == GraphRangeAutoYFallBackMode.MinMax) - { - min_y = _range.MinimumY; - max_y = _range.MaximumY; - } - else if (_range.AutoYFallbackMode == GraphRangeAutoYFallBackMode.Margins) - { - min_y -= _range.AutoYFallbackMargins; - max_y += _range.AutoYFallbackMargins; - } - } - - EffectiveMinimumX = min_x; - EffectiveMaximumX = max_x; - EffectiveMinimumY = min_y; - EffectiveMaximumY = max_y; - - VirtualMinimumX = EffectiveMinimumX; - VirtualMaximumX = EffectiveMaximumX; - VirtualMinimumY = EffectiveMinimumY; - VirtualMaximumY = EffectiveMaximumY; - - _last_render_time = DateTime.Now; - - if (Surface != null) - { - var surface_size = Surface.GetSize(); - var zoom_rect = Surface.GetZoomRect(); - - if (surface_size.Width > 0 && surface_size.Height > 0) - { - Surface.BeginDraw(); - - if (zoom_rect.Width > 0 && zoom_rect.Height > 0) - { - var zoom_rect_top_percentage = zoom_rect.Top / surface_size.Height; - var zoom_rect_bottom_percentage = zoom_rect.Bottom / surface_size.Height; - var zoom_rect_left_percentage = zoom_rect.Left / surface_size.Width; - var zoom_rect_right_percentage = zoom_rect.Right / surface_size.Width; - - VirtualMinimumY = EffectiveMaximumY - GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumY, EffectiveMaximumY, zoom_rect_bottom_percentage); - VirtualMaximumY = EffectiveMaximumY - GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumY, EffectiveMaximumY, zoom_rect_top_percentage); - - VirtualMinimumX = GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumX, EffectiveMaximumX, zoom_rect_left_percentage); - VirtualMaximumX = GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumX, EffectiveMaximumX, zoom_rect_right_percentage); - - GraphTransform transform = new GraphTransform(); - var scale_x = (float)(surface_size.Width / zoom_rect.Width); - var scale_y = (float)(surface_size.Height / zoom_rect.Height); - var translate_x = (float)-zoom_rect.Left * scale_x; - var translate_y = (float)-zoom_rect.Top * scale_y; - - transform = new GraphTransform(); - transform.TranslateX = translate_x; - transform.TranslateY = translate_y; - transform.ScaleX = scale_x; - transform.ScaleY = scale_y; - - Surface.SetTransform(transform); - } - - List>> to_draw = new List>>(); - - var to_render = _to_render.Select(x => x.Value).ToList(); - - foreach (var item in to_render) - { - if (item.YY.Count > 0) - { - item.Series.CurrentValue = item.YY.Last().GetValue(); - } - - var points = Renderer.Render(Surface, item.Series, _range, item.XX, item.YY, min_x, max_x, min_y, max_y); - to_draw.Add(new Tuple>(item.Series, points)); - } - - for (int i = 0; i < to_draw.Count; i++) - { - if (to_draw[i].Item2.Count() > 2) - { - if (to_draw[i].Item1.IsVisible) - { - Renderer.Draw(Surface, to_draw[i].Item1, to_draw[i].Item2, i, to_draw.Count); - } - } - } - - Surface?.EndDraw(); - } - } - - OnEffectiveRangeChanged(EffectiveMinimumX, EffectiveMaximumX, EffectiveMinimumY, EffectiveMaximumY); - OnVirtualRangeChanged(VirtualMinimumX, VirtualMaximumX, VirtualMinimumY, VirtualMaximumY); - } - } - - #endregion - - #region Protected Methods - - /// - /// Called when the surface has changed. - /// - /// The previous. - /// The surface. - protected virtual void OnSurfaceChanged(IGraphSurface previous, IGraphSurface surface) - { - if (previous != null) - { - previous.SurfaceSizeChanged -= Surface_SurfaceSizeChanged; - previous.ZoomRectChanged -= Surface_ZoomRectChanged; - } - - if (surface != null) - { - surface.SurfaceSizeChanged += Surface_SurfaceSizeChanged; - surface.ZoomRectChanged += Surface_ZoomRectChanged; - } - } - - /// - /// Raises the event. - /// - /// The minimum x. - /// The maximum x. - /// The minimum y. - /// The maximum y. - protected virtual void OnEffectiveRangeChanged(GraphDataPoint minimumX, GraphDataPoint maximumX, GraphDataPoint minimumY, GraphDataPoint maximumY) - { - EffectiveRangeChanged?.Invoke(this, new RangeChangedEventArgs(minimumX, maximumX, minimumY, maximumY)); - } - - /// - /// Raises the event. - /// - /// The minimum x. - /// The maximum x. - /// The minimum y. - /// The maximum y. - protected virtual void OnVirtualRangeChanged(GraphDataPoint minimumX, GraphDataPoint maximumX, GraphDataPoint minimumY, GraphDataPoint maximumY) - { - var range = new RangeChangedEventArgs(minimumX, maximumX, minimumY, maximumY); - VirtualRangeChanged?.Invoke(this, range); - } - - /// - /// Converts the specified relative x position to graph absolute position. - /// - /// The relative x position. - /// - protected virtual float ConvertXValueToRendererValue(double x) - { - return (float)(x * Surface.GetSize().Width / 100); - } - - /// - /// Converts the specified relative y position to graph absolute position. - /// - /// The relative y position. - /// - protected virtual float ConvertYValueToRendererValue(double y) - { - return (float)(Surface.GetSize().Height - (y * Surface.GetSize().Height / 100)); - } - - #endregion - - #region Surface Event Handlers - - /// - /// Handles the ZoomRectChanged event of the Surface control. - /// - /// The source of the event. - /// The instance containing the event data. - private void Surface_ZoomRectChanged(object sender, EventArgs e) - { - if (!_pending_series_collection.ToList().SelectMany(x => x).ToList().Exists(x => x.IsUpdateSeries)) - { - List updateSeries = new List(); - - foreach (var pending_Series in _to_render) - { - updateSeries.Add(new PendingSeries() - { - IsUpdateSeries = true, - Series = pending_Series.Value.Series, - XX = new List(), - YY = new List(), - }); - } - - _pending_series_collection.BlockEnqueue(updateSeries); - } - } - - /// - /// Handles the SurfaceSizeChanged event of the Surface control. - /// - /// The source of the event. - /// The instance containing the event data. - private void Surface_SurfaceSizeChanged(object sender, EventArgs e) - { - if (!_pending_series_collection.ToList().SelectMany(x => x).ToList().Exists(x => x.IsUpdateSeries)) - { - List updateSeries = new List(); - - foreach (var pending_Series in _to_render) - { - updateSeries.Add(new PendingSeries() - { - IsUpdateSeries = true, - Series = pending_Series.Value.Series, - XX = new List(), - YY = new List(), - }); - } - - _pending_series_collection.BlockEnqueue(updateSeries); - } - } - - #endregion - - #region Public Methods - - /// - /// Submits the specified x and y data points. - /// If the controller has more than one data series the data points will be duplicated. - /// - /// X data point. - /// Y data point. - public void PushData(TXDataPoint x, TYDataPoint y) - { - if (DataSeriesCollection.Count == 0) return; - - List> xxxx = new List>(); - List> yyyy = new List>(); - - foreach (var series in DataSeriesCollection.ToList()) - { - xxxx.Add(new List() { x }); - yyyy.Add(new List() { y }); - } - - PushData(xxxx, yyyy); - } - - /// - /// Submits the specified collections of x and y data points. - /// If the controller has more than one data series the data points will be distributed evenly. - /// - /// X data point collection. - /// Y data point collection. - public void PushData(IEnumerable xx, IEnumerable yy) - { - if (DataSeriesCollection.Count == 0) return; - - var xList = xx.ToList(); - var yList = yy.ToList(); - - List> xxxx = new List>(); - List> yyyy = new List>(); - - foreach (var series in DataSeriesCollection.ToList()) - { - xxxx.Add(new List()); - yyyy.Add(new List()); - } - - int counter = 0; - - for (int i = 0; i < xList.Count; i++) - { - xxxx[counter].Add(xList[i]); - yyyy[counter].Add(yList[i]); - - counter++; - - if (counter >= xxxx.Count) - { - counter = 0; - } - } - - PushData(xxxx, yyyy); - } - - /// - /// Submits a matrix of x and y data points. Meaning each data series should process a single collection of x/y data points. - /// - /// X matrix. - /// Y matrix. - public void PushData(IEnumerable> xxxx, IEnumerable> yyyy) - { - if (DataSeriesCollection.Count == 0) return; - - IEnumerable> xxxxI = xxxx.Select(x => x.ToList()).ToList(); - IEnumerable> yyyyI = yyyy.Select(x => x.ToList()).ToList(); - - List> xxxxList = xxxxI.Select(x => x.ToList()).ToList(); - List> yyyyList = yyyyI.Select(x => x.ToList()).ToList(); - - int first_count_x = xxxxList[0].Count; - int first_count_y = yyyyList[0].Count; - - - bool is_data_valid = true; - - for (int i = 0; i < xxxxList.Count; i++) - { - if (xxxxList[0].Count != first_count_x) - { - is_data_valid = false; - break; - } - - if (xxxxList[0].Count != yyyyList[0].Count) - { - is_data_valid = false; - break; - } - } - - if (!is_data_valid) - { - throw new ArgumentOutOfRangeException("When pushing data to a multi series renderer, each series must contain the same amount of data."); - } - - var list = DataSeriesCollection.ToList(); - - var pending_list = new List(); - - for (int i = 0; i < list.Count; i++) - { - pending_list.Add(new PendingSeries() - { - Series = list[i], - XX = xxxxList[i].ToList(), - YY = yyyyList[i].ToList(), - }); - } - - _pending_series_collection.BlockEnqueue(pending_list); - } - - /// - /// Clears all data points from this controller. - /// - public void Clear() - { + } + Thread.Sleep(RefreshRate); + } + else + { + Thread.Sleep(RefreshRate); + } + } + } + + private void Render() + { + if (_to_render.Count > 0) + { + GraphDataPoint min_x = _range.MaximumX - _range.MaximumX; + GraphDataPoint max_x = _range.MaximumX; + GraphDataPoint min_y = _range.MinimumY; + GraphDataPoint max_y = _range.MaximumY; + + if (_to_render.Count > 0 && _to_render.First().Value.XX.Count > 0) + { + min_x = _to_render.First().Value.XX.First(); + max_x = _to_render.First().Value.XX.Last(); + + if ( ( max_x - min_x ) < _range.MaximumX ) + { + switch ( _range.XStartBehavior ) + { + case XStartBehavior.stretchData: + // Original behavior. No action necessary. + break ; + case XStartBehavior.slideInFromRight: + min_x = max_x - _range.MaximumX ; + break ; + case XStartBehavior.fillFromLeft: + max_x = min_x + _range.MaximumX ; + break ; + } + } + } + else + { + return; + } + + if (_range.AutoY) + { + min_y = _to_render.Select(x => x.Value).SelectMany(x => x.YY).Min(); + max_y = _to_render.Select(x => x.Value).SelectMany(x => x.YY).Max(); + + // Note: The "this" object to which this method is applied is not used, + // making it like a static method (but abstract). I'm not really happy + // with this structure. + min_y.ExpandRange ( ref min_y, ref max_y, 0.05 ) ; + } + + if (min_y == max_y) + { + if (_range.AutoYFallbackMode == GraphRangeAutoYFallBackMode.MinMax) + { + min_y = _range.MinimumY; + max_y = _range.MaximumY; + } + else if (_range.AutoYFallbackMode == GraphRangeAutoYFallBackMode.Margins) + { + min_y -= _range.AutoYFallbackMargins; + max_y += _range.AutoYFallbackMargins; + } + } + + EffectiveMinimumX = min_x; + EffectiveMaximumX = max_x; + EffectiveMinimumY = min_y; + EffectiveMaximumY = max_y; + + VirtualMinimumX = EffectiveMinimumX; + VirtualMaximumX = EffectiveMaximumX; + VirtualMinimumY = EffectiveMinimumY; + VirtualMaximumY = EffectiveMaximumY; + + _last_render_time = DateTime.Now; + + if (Surface != null) + { + var surface_size = Surface.GetSize(); + var zoom_rect = Surface.GetZoomRect(); + + if (surface_size.Width > 0 && surface_size.Height > 0) + { + Surface.BeginDraw(); + + if (zoom_rect.Width > 0 && zoom_rect.Height > 0) + { + var zoom_rect_top_percentage = zoom_rect.Top / surface_size.Height; + var zoom_rect_bottom_percentage = zoom_rect.Bottom / surface_size.Height; + var zoom_rect_left_percentage = zoom_rect.Left / surface_size.Width; + var zoom_rect_right_percentage = zoom_rect.Right / surface_size.Width; + + VirtualMinimumY = EffectiveMaximumY - GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumY, EffectiveMaximumY, zoom_rect_bottom_percentage); + VirtualMaximumY = EffectiveMaximumY - GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumY, EffectiveMaximumY, zoom_rect_top_percentage); + + VirtualMinimumX = GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumX, EffectiveMaximumX, zoom_rect_left_percentage); + VirtualMaximumX = GraphDataPointHelper.ComputeAbsolutePosition(EffectiveMinimumX, EffectiveMaximumX, zoom_rect_right_percentage); + + GraphTransform transform = new GraphTransform(); + var scale_x = (float)(surface_size.Width / zoom_rect.Width); + var scale_y = (float)(surface_size.Height / zoom_rect.Height); + var translate_x = (float)-zoom_rect.Left * scale_x; + var translate_y = (float)-zoom_rect.Top * scale_y; + + transform = new GraphTransform(); + transform.TranslateX = translate_x; + transform.TranslateY = translate_y; + transform.ScaleX = scale_x; + transform.ScaleY = scale_y; + + Surface.SetTransform(transform); + } + + List>> to_draw = new List>>(); + + var to_render = _to_render.Select(x => x.Value).ToList(); + + foreach (var item in to_render) + { + if (item.YY.Count > 0) + { + item.Series.CurrentValue = item.YY.Last().GetValue(); + } + + var points = Renderer.Render(Surface, item.Series, _range, item.XX, item.YY, min_x, max_x, min_y, max_y); + to_draw.Add(new Tuple>(item.Series, points)); + } + + for (int i = 0; i < to_draw.Count; i++) + { + if (to_draw[i].Item2.Count() > 2) + { + if (to_draw[i].Item1.IsVisible) + { + Renderer.Draw(Surface, to_draw[i].Item1, to_draw[i].Item2, i, to_draw.Count); + } + } + } + + Surface?.EndDraw(); + } + } + + OnEffectiveRangeChanged(EffectiveMinimumX, EffectiveMaximumX, EffectiveMinimumY, EffectiveMaximumY); + OnVirtualRangeChanged(VirtualMinimumX, VirtualMaximumX, VirtualMinimumY, VirtualMaximumY); + } + } + + #endregion + + #region Protected Methods + + /// + /// Called when the surface has changed. + /// + /// The previous. + /// The surface. + protected virtual void OnSurfaceChanged(IGraphSurface previous, IGraphSurface surface) + { + if (previous != null) + { + previous.SurfaceSizeChanged -= Surface_SurfaceSizeChanged; + previous.ZoomRectChanged -= Surface_ZoomRectChanged; + } + + if (surface != null) + { + surface.SurfaceSizeChanged += Surface_SurfaceSizeChanged; + surface.ZoomRectChanged += Surface_ZoomRectChanged; + } + } + + /// + /// Raises the event. + /// + /// The minimum x. + /// The maximum x. + /// The minimum y. + /// The maximum y. + protected virtual void OnEffectiveRangeChanged(GraphDataPoint minimumX, GraphDataPoint maximumX, GraphDataPoint minimumY, GraphDataPoint maximumY) + { + EffectiveRangeChanged?.Invoke(this, new RangeChangedEventArgs(minimumX, maximumX, minimumY, maximumY)); + } + + /// + /// Raises the event. + /// + /// The minimum x. + /// The maximum x. + /// The minimum y. + /// The maximum y. + protected virtual void OnVirtualRangeChanged(GraphDataPoint minimumX, GraphDataPoint maximumX, GraphDataPoint minimumY, GraphDataPoint maximumY) + { + var range = new RangeChangedEventArgs(minimumX, maximumX, minimumY, maximumY); + VirtualRangeChanged?.Invoke(this, range); + } + + /// + /// Converts the specified relative x position to graph absolute position. + /// + /// The relative x position. + /// + protected virtual float ConvertXValueToRendererValue(double x) + { + return (float)(x * Surface.GetSize().Width / 100); + } + + /// + /// Converts the specified relative y position to graph absolute position. + /// + /// The relative y position. + /// + protected virtual float ConvertYValueToRendererValue(double y) + { + return (float)(Surface.GetSize().Height - (y * Surface.GetSize().Height / 100)); + } + + #endregion + + #region Surface Event Handlers + + /// + /// Handles the ZoomRectChanged event of the Surface control. + /// + /// The source of the event. + /// The instance containing the event data. + private void Surface_ZoomRectChanged(object sender, EventArgs e) + { + if (!_pending_series_collection.ToList().SelectMany(x => x).ToList().Exists(x => x.IsUpdateSeries)) + { + List updateSeries = new List(); + + foreach (var pending_Series in _to_render) + { + updateSeries.Add(new PendingSeries() + { + IsUpdateSeries = true, + Series = pending_Series.Value.Series, + XX = new List(), + YY = new List(), + }); + } + + _pending_series_collection.BlockEnqueue(updateSeries); + } + } + + /// + /// Handles the SurfaceSizeChanged event of the Surface control. + /// + /// The source of the event. + /// The instance containing the event data. + private void Surface_SurfaceSizeChanged(object sender, EventArgs e) + { + if (!_pending_series_collection.ToList().SelectMany(x => x).ToList().Exists(x => x.IsUpdateSeries)) + { + List updateSeries = new List(); + + foreach (var pending_Series in _to_render) + { + updateSeries.Add(new PendingSeries() + { + IsUpdateSeries = true, + Series = pending_Series.Value.Series, + XX = new List(), + YY = new List(), + }); + } + + _pending_series_collection.BlockEnqueue(updateSeries); + } + } + + #endregion + + #region Public Methods + + /// + /// Submits the specified x and y data points. + /// If the controller has more than one data series the data points will be duplicated. + /// + /// X data point. + /// Y data point. + public void PushData(TXDataPoint x, TYDataPoint y) + { + if (DataSeriesCollection.Count == 0) return; + + List> xxxx = new List>(); + List> yyyy = new List>(); + + foreach (var series in DataSeriesCollection.ToList()) + { + xxxx.Add(new List() { x }); + yyyy.Add(new List() { y }); + } + + PushData(xxxx, yyyy); + } + + /// + /// Submits the specified collections of x and y data points. + /// If the controller has more than one data series the data points will be distributed evenly. + /// + /// X data point collection. + /// Y data point collection. + public void PushData(IEnumerable xx, IEnumerable yy) + { + if (DataSeriesCollection.Count == 0) return; + + var xList = xx.ToList(); + var yList = yy.ToList(); + + List> xxxx = new List>(); + List> yyyy = new List>(); + + foreach (var series in DataSeriesCollection.ToList()) + { + xxxx.Add(new List()); + yyyy.Add(new List()); + } + + int counter = 0; + + for (int i = 0; i < xList.Count; i++) + { + xxxx[counter].Add(xList[i]); + yyyy[counter].Add(yList[i]); + + counter++; + + if (counter >= xxxx.Count) + { + counter = 0; + } + } + + PushData(xxxx, yyyy); + } + + /// + /// Submits a matrix of x and y data points. Meaning each data series should process a single collection of x/y data points. + /// + /// X matrix. + /// Y matrix. + public void PushData(IEnumerable> xxxx, IEnumerable> yyyy) + { + if (DataSeriesCollection.Count == 0) return; + + IEnumerable> xxxxI = xxxx.Select(x => x.ToList()).ToList(); + IEnumerable> yyyyI = yyyy.Select(x => x.ToList()).ToList(); + + List> xxxxList = xxxxI.Select(x => x.ToList()).ToList(); + List> yyyyList = yyyyI.Select(x => x.ToList()).ToList(); + + int first_count_x = xxxxList[0].Count; + int first_count_y = yyyyList[0].Count; + + + bool is_data_valid = true; + + for (int i = 0; i < xxxxList.Count; i++) + { + if (xxxxList[0].Count != first_count_x) + { + is_data_valid = false; + break; + } + + if (xxxxList[0].Count != yyyyList[0].Count) + { + is_data_valid = false; + break; + } + } + + if (!is_data_valid) + { + throw new ArgumentOutOfRangeException("When pushing data to a multi series renderer, each series must contain the same amount of data."); + } + + var list = DataSeriesCollection.ToList(); + + var pending_list = new List(); + + for (int i = 0; i < list.Count; i++) + { + pending_list.Add(new PendingSeries() + { + Series = list[i], + XX = xxxxList[i].ToList(), + YY = yyyyList[i].ToList(), + }); + } + + _pending_series_collection.BlockEnqueue(pending_list); + } + + /// + /// Clears all data points from this controller. + /// + public void Clear() + { _clear = true; - _pending_series_collection.BlockEnqueue(new List() - { - new PendingSeries() - { - IsClearSeries = true - }, - }); - } - - /// - /// Requests the controller to invoke a virtual range change event. - /// - public void RequestVirtualRangeChange() - { - OnVirtualRangeChanged(Range.MaximumX, Range.MaximumX, Range.MinimumY, Range.MaximumY); + _pending_series_collection.BlockEnqueue(new List() + { + new PendingSeries() + { + IsClearSeries = true + }, + }); + } + + /// + /// Requests the controller to invoke a virtual range change event. + /// + public void RequestVirtualRangeChange() + { + OnVirtualRangeChanged(Range.MaximumX, Range.MaximumX, Range.MinimumY, Range.MaximumY); } /// @@ -731,8 +752,8 @@ public IGraphDataPoint TranslateSurfaceY(double surfaceYPosition) { return default(IGraphDataPoint); } - } - - #endregion - } -} + } + + #endregion + } +} diff --git a/RealTimeGraphX/GraphDataPoint.cs b/RealTimeGraphX/GraphDataPoint.cs index f1bbcfc..99f63fb 100644 --- a/RealTimeGraphX/GraphDataPoint.cs +++ b/RealTimeGraphX/GraphDataPoint.cs @@ -256,6 +256,14 @@ public Type Type /// public abstract IGraphDataPoint GetDefaultMargins(); + /// + /// Expands the range by adding factor * ( max - min ) to the max and subtracting the same value from the min. + /// + /// + /// + /// + public abstract void ExpandRange ( ref GraphDataPoint min, ref GraphDataPoint max, double factor ) ; + #endregion } diff --git a/RealTimeGraphX/GraphRange.cs b/RealTimeGraphX/GraphRange.cs index fa2b850..3490186 100644 --- a/RealTimeGraphX/GraphRange.cs +++ b/RealTimeGraphX/GraphRange.cs @@ -6,6 +6,31 @@ namespace RealTimeGraphX { + /// + /// This enum defines the initial behaviour, when there is not enough new data to fill the X-Axis. + /// + public enum XStartBehavior + { + /// + /// The option strechData means that the initial data will be stretched to fill the + /// X-Axis. As new data arrives, the data will be squeezed until the specified range + /// for the X-Axis has been reached. After that the data will scroll to the left. + /// + stretchData, + /// + /// The option slideInFromRight means that the range of the X-Axis is fixed. New + /// data will appear at the right of the diagram and scroll to the left. The data + /// scrolls from very beginning. + /// + slideInFromRight, + /// + /// The option fillFromLeft means that the range of the X-Axis is fixed. New Data + /// is drawn relative to the left hand side and does not scroll at all until the + /// specified range of the X-Axis is full. After that the data will scroll to the left. + /// + fillFromLeft + } + /// /// Represents a graph x/y data points boundaries. /// @@ -40,6 +65,12 @@ public interface IGraphRange : IGraphComponent /// Gets or sets the AutoY fallback margins when is set to Margins. /// GraphDataPoint AutoYFallBackMargins { get; set; } + + /// + /// This property defines how data is displayed in the initial period, before the MaximumX has been reached. + /// The options are streching the data, sliding the data in from the right, or filling the data from the left. + /// + XStartBehavior XStartBehavior { get; set; } } /// @@ -181,5 +212,16 @@ GraphDataPoint IGraphRange.AutoYFallBackMargins AutoYFallbackMargins = (YDataPoint)value; } } + + private XStartBehavior _xStartBehavior = XStartBehavior.stretchData ; + /// + /// This property defines how data is displayed in the initial period, before the MaximumX has been reached. + /// The options are streching the data, sliding the data in from the right, or filling the data from the left. + /// + public XStartBehavior XStartBehavior + { + get { return _xStartBehavior; } + set { _xStartBehavior = value; RaisePropertyChangedAuto(); } + } } } diff --git a/RealTimeGraphX/RealTimeGraphX.csproj b/RealTimeGraphX/RealTimeGraphX.csproj index 3dc4555..c8e847f 100644 --- a/RealTimeGraphX/RealTimeGraphX.csproj +++ b/RealTimeGraphX/RealTimeGraphX.csproj @@ -3,7 +3,7 @@ netstandard2.0 false - 1.0.0 + 1.0.0.3 RealTimeGraphX is a data type agnostic, high performance plotting library for WPF and UWP. LICENSE https://github.com/royben/RealTimeGraphX @@ -12,7 +12,7 @@ Chart;Graph;Plotting;RealTime;WPF;UWP Roy Ben Shabat Roy Ben Shabat - Release + Release;Debug