aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/SideChains/RealTimeGraphX/GraphRendererBase.cs
blob: fb4824837446a822dd1cf6d573de101ead84d8a6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tango.BL;
using Tango.BL.ValueObjects;

namespace Tango.UnitTesting.BL
{
    [TestClass]
    [TestCategory("BL - Hardware Configuration")]
    public class HardwareConfiguration_TST
    {
        [TestMethod]
        public void Merge_HardwareConfiguration_With_HardwareVersion()
        {
            using (ObservablesContext db = ObservablesContext.CreateDefault())
            {
                var hw = new Tango.BL.Builders.HardwareVersionBuilder(db).SetFirst().WithHardwareComponents().Build();

                HardwareConfiguration config = new HardwareConfiguration();

                config.Parameters.Add(new HardwareConfiguration.HardwareConfigurationParameter()
                {
                    ComponentName = Tango.BL.Enumerations.HardwareMotorTypes.MOTO_DISPENSER_1.ToString(),
                    ParameterName = "MinFrequency",
                    Value = (int)100,
                });

                var cloned = config.Merge(hw);

                Assert.IsTrue(cloned.HardwareMotors.SingleOrDefault(x => x.HardwareMotorType.Name == Tango.BL.Enumerations.HardwareMotorTypes.MOTO_DISPENSER_1.ToString()).MinFrequency == 100);
            }
        }
    }
}
a id='n405' href='#n405'>405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
using RealTimeGraphX.EventArguments;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace RealTimeGraphX
{
    /// <summary>
    /// Represents an <see cref="IGraphRenderer{XDataPoint, YDataPoint}"/> base class.
    /// </summary>
    /// <typeparam name="TDataSeries">The type of the graph data series.</typeparam>
    /// <typeparam name="XDataPoint">The type of the x-axis data point.</typeparam>
    /// <typeparam name="YDataPoint">The type of the y-axis data point.</typeparam>
    /// <seealso cref="RealTimeGraphX.GraphInputOutputComponentBase{RealTimeGraphX.IGraphController{XDataPoint, YDataPoint}, RealTimeGraphX.IGraphPainter}" />
    /// <seealso cref="RealTimeGraphX.IGraphRenderer{XDataPoint, YDataPoint}" />
    /// <seealso cref="RealTimeGraphX.IGraphRenderer" />
    public abstract class GraphRendererBase<TDataSeries, XDataPoint, YDataPoint> :
        GraphInputOutputComponentBase<IGraphController<TDataSeries, XDataPoint, YDataPoint>, IGraphPainter<TDataSeries>>,
        IGraphRenderer<TDataSeries, XDataPoint, YDataPoint>,
        IGraphRenderer<TDataSeries>
        where XDataPoint : GraphDataPointBase
        where YDataPoint : GraphDataPointBase
        where TDataSeries : IGraphDataSeries
    {
        private GraphDataQueue<PendingSeries> _pending_series_collection;
        private Dictionary<TDataSeries, PendingSeries> _to_render;
        private DateTime _last_render_time;
        private object _render_lock = new object();
        private Thread _render_thread;

        protected YDataPoint _current_min_y;
        protected YDataPoint _current_max_y;

        private PidController _pid_controller;
        private DateTime _last_pid_sample_time = DateTime.Now;

        #region Pending Series Class

        protected class PendingSeries
        {
            public TDataSeries Series { get; set; }
            public List<XDataPoint> XX { get; set; }
            public List<YDataPoint> YY { get; set; }

            public int NewItemsCount
            {
                get { return XX.Count - RenderedItems; }
            }

            public int RenderedItems { get; set; }
            public bool IsInvalidationSeries { get; set; }
        }

        #endregion

        #region Events

        /// <summary>
        /// Occurs when the current effective x-axis minimum/maximum has changed.
        /// </summary>
        public event EventHandler<RangeChangedEventArgs> EffectiveRangeXChanged;

        /// <summary>
        /// Occurs when the current effective y-axis minimum/maximum has changed.
        /// </summary>
        public event EventHandler<RangeChangedEventArgs> EffectiveRangeYChanged;

        /// <summary>
        /// Occurs when the connected input controller has changed.
        /// </summary>
        event EventHandler<InputChangedEventArgs<IGraphController<TDataSeries>>> IGraphInputComponent<IGraphController<TDataSeries>>.InputChanged
        {
            add
            {
                InputChanged += new EventHandler<InputChangedEventArgs<IGraphController<TDataSeries, XDataPoint, YDataPoint>>>((sender, e) => { value.Invoke(sender, new InputChangedEventArgs<IGraphController<TDataSeries>>(e.Input)); });
            }

            remove
            {
                InputChanged -= new EventHandler<InputChangedEventArgs<IGraphController<TDataSeries, XDataPoint, YDataPoint>>>((sender, e) => { value.Invoke(sender, new InputChangedEventArgs<IGraphController<TDataSeries>>(e.Input)); });
            }
        }

        #endregion

        #region Properties

        private TimeSpan _refreshRate;
        /// <summary>
        /// Gets or sets the renderer refresh rate.
        /// Higher rate requires more CPU time.
        /// </summary>
        public TimeSpan RefreshRate
        {
            get
            {
                return _refreshRate;
            }
            set
            {
                _refreshRate = value;
                RaisePropertyChangedAuto();
            }
        }

        private GraphDataPointBase _effectiveMinimumX;
        /// <summary>
        /// Gets the current effective x-axis minimum.
        /// </summary>
        public GraphDataPointBase EffectiveMinimumX
        {
            get
            {
                return _effectiveMinimumX;
            }
            private set
            {
                _effectiveMinimumX = value;
                RaisePropertyChangedAuto();
            }
        }

        private GraphDataPointBase _effectiveMaximumX;
        /// <summary>
        /// Gets the current effective x-axis maximum.
        /// </summary>
        public GraphDataPointBase EffectiveMaximumX
        {
            get
            {
                return _effectiveMaximumX;
            }
            private set
            {
                _effectiveMaximumX = value;
                RaisePropertyChangedAuto();
            }
        }

        private GraphDataPointBase _effectiveMinimumY;
        /// <summary>
        /// Gets the current effective y-axis minimum.
        /// </summary>
        public GraphDataPointBase EffectiveMinimumY
        {
            get
            {
                return _effectiveMinimumY;
            }
            private set
            {
                _effectiveMinimumY = value;
                RaisePropertyChangedAuto();
            }
        }

        private GraphDataPointBase _effectiveMaximumY;
        /// <summary>
        /// Gets the current effective y-axis maximum.
        /// </summary>
        public GraphDataPointBase EffectiveMaximumY
        {
            get
            {
                return _effectiveMaximumY;
            }
            private set
            {
                _effectiveMaximumY = value;
                RaisePropertyChangedAuto();
            }
        }

        /// <summary>
        /// Gets the current data point x position.
        /// </summary>
        public double CurrentXPosition { get; protected set; }

        private bool _isPaused;
        /// <summary>
        /// Gets or sets a value indicating whether to pause the graph movement.
        /// </summary>
        public bool IsPaused
        {
            get { return _isPaused; }
            set { _isPaused = value; RaisePropertyChangedAuto(); }
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="GraphRendererBase{XDataPoint, YDataPoint}"/> class.
        /// </summary>
        public GraphRendererBase()
        {
            EffectiveMinimumX = GraphDataPointHelper.Init<XDataPoint>();
            EffectiveMinimumY = GraphDataPointHelper.Init<YDataPoint>();
            EffectiveMaximumX = GraphDataPointHelper.Init<XDataPoint>();
            EffectiveMaximumY = GraphDataPointHelper.Init<YDataPoint>();
            _last_render_time = DateTime.Now;
            _to_render = new Dictionary<TDataSeries, PendingSeries>();
            _pending_series_collection = new GraphDataQueue<PendingSeries>();
            RefreshRate = TimeSpan.FromMilliseconds(50);

            _pid_controller = new PidController(10, 10, -10);
        }

        #endregion

        #region Render Thread

        /// <summary>
        /// The rendering thread method.
        /// </summary>
        private void RenderThreadMethod()
        {
            while (Output != null)
            {
                if (!IsPaused)
                {
                    for (int i = 0; i < Input.DataSeriesCollection.Count; i++)
                    {
                        var series = _pending_series_collection.BlockDequeue();

                        if (!series.IsInvalidationSeries)
                        {
                            if (_to_render.ContainsKey(series.Series))
                            {
                                var s = _to_render[series.Series];
                                s.XX.AddRange(series.XX);
                                s.YY.AddRange(series.YY);
                            }
                            else
                            {
                                _to_render[series.Series] = series;
                            }

                            if (i == Input.DataSeriesCollection.Count - 1)
                            {
                                if (DateTime.Now > _last_render_time.AddMilliseconds(RefreshRate.TotalMilliseconds))
                                {
                                    if (Input.Range.AutoY)
                                    {
                                        _current_min_y = _to_render.Select(x => x.Value).SelectMany(x => x.YY).Min();
                                        _current_max_y = _to_render.Select(x => x.Value).SelectMany(x => x.YY).Max();
                                    }

                                    _last_render_time = DateTime.Now;
                                    RenderAndDraw(_to_render.ToList());
                                }
                            }
                        }
                        else
                        {
                            RenderAndDraw(_to_render.ToList());
                        }
                    }
                }
                else
                {
                    RenderAndDraw(_to_render.ToList());
                    Thread.Sleep(50);
                }
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Renders the specified data points collection.
        /// • The number of data series of x and y must match.
        /// • The number of x and y collections must match.
        /// • The number of x and y data points must match.
        /// </summary>
        /// <param name="xxxx">The x-axis series collection.</param>
        /// <param name="yyyy">The y-axis series collection.</param>
        /// <exception cref="ArgumentOutOfRangeException">When pushing data to a multi series renderer, each series must contain the same amount of data.</exception>
        public void Render(IEnumerable<TDataSeries> seriesCollection, IEnumerable<IEnumerable<XDataPoint>> xxxx, IEnumerable<IEnumerable<YDataPoint>> yyyy)
        {
            List<List<XDataPoint>> xxxxList = xxxx.Select(x => x.ToList()).ToList();
            List<List<YDataPoint>> yyyyList = yyyy.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 = seriesCollection.ToList();

            for (int i = 0; i < list.Count; i++)
            {
                _pending_series_collection.BlockEnqueue(new PendingSeries()
                {
                    Series = list[i],
                    XX = xxxxList[i].ToList(),
                    YY = yyyyList[i].ToList(),
                });
            }
        }

        /// <summary>
        /// Invalidates the graph.
        /// </summary>
        public virtual void Invalidate()
        {
            _pending_series_collection.BlockEnqueue(new PendingSeries() { IsInvalidationSeries = true });
        }

        /// <summary>
        /// Connects this renderer to an <see cref="IGraphPainter"/>.
        /// </summary>
        /// <param name="painter">The graph painter.</param>
        /// <param name="fromOutput">Specifies whether this call was made from an <see cref="IGraphPainter"/> component.</param>
        public override void ConnectOutput(IGraphPainter<TDataSeries> painter, bool fromOutput = false)
        {
            painter.ThrowIfNull("Cannot connect a null painter.");

            DisconnectOutput();

            Output = painter;

            if (!fromOutput)
            {
                Output.ConnectInput(this, true);
            }

            _render_thread = new Thread(RenderThreadMethod);
            _render_thread.IsBackground = true;
            _render_thread.Name = "IGraphRenderer Render Thread";
            _render_thread.Start();

            OnEffectiveRangeYChanged(Input.Range.MinimumY, Input.Range.MaximumY);
        }

        /// <summary>
        /// Disconnects this renderer from the connected <see cref="IGraphPainter"/>.
        /// </summary>
        /// <param name="fromOutput">Specifies whether this call was made from an <see cref="IGraphPainter"/> component.</param>
        public override void DisconnectOutput(bool fromOutput = false)
        {
            if (Output != null)
            {
                if (!fromOutput)
                {
                    Output.DisconnectInput(true);
                }

                Output = null;
            }
        }

        /// <summary>
        /// Connects this renderer to the specified <see cref="IGraphController{XDataPoint, YDataPoint}"/>.
        /// </summary>
        /// <param name="controller">The controller.</param>
        /// <param name="fromInput">Specifies whether this call was made from an <see cref="IGraphController{XDataPoint, YDataPoint}"/> component.</param>
        public override void ConnectInput(IGraphController<TDataSeries, XDataPoint, YDataPoint> controller, bool fromInput = false)
        {
            controller.ThrowIfNull("Cannot connect a null controller.");

            DisconnectInput();

            if (!fromInput)
            {
                controller.ConnectOutput(this, true);
            }

            Input = controller;

            Input.RangeChanged += Input_RangeChanged;

            OnEffectiveRangeYChanged(Input.Range.MinimumY, Input.Range.MaximumY);
        }

        /// <summary>
        /// Disconnects this renderer from the current connected <see cref="IGraphController{XDataPoint, YDataPoint}"/>.
        /// </summary>
        /// <param name="fromInput">Specifies whether this call was made from an <see cref="IGraphController{XDataPoint, YDataPoint}"/> component.</param>
        public override void DisconnectInput(bool fromInput = false)
        {
            if (Input != null)
            {
                if (!fromInput)
                {
                    (Input as IGraphOutputComponent<IGraphRenderer<TDataSeries, XDataPoint, YDataPoint>>).DisconnectOutput(true);
                }

                Input.RangeChanged -= Input_RangeChanged;

                Input = null;
            }
        }

        /// <summary>
        /// Gets the connected input controller.
        /// </summary>
        IGraphController<TDataSeries> IGraphInputComponent<IGraphController<TDataSeries>>.Input
        {
            get
            {
                return Input;
            }
        }

        /// <summary>
        /// Connected this renderer to the specified graph controller.
        /// </summary>
        /// <param name="controller">The controller.</param>
        /// <param name="fromInput">Specifies whether this call was made from an <see cref="IGraphController"/> component.</param>
        void IGraphInputComponent<IGraphController<TDataSeries>>.ConnectInput(IGraphController<TDataSeries> controller, bool fromInput)
        {
            ConnectInput(controller as IGraphController<TDataSeries, XDataPoint, YDataPoint>, fromInput);
        }

        /// <summary>
        /// Clears the graph data.
        /// </summary>
        public void Clear()
        {
            while (_pending_series_collection.Count > 0)
            {
                _pending_series_collection.BlockDequeue();
            }

            _to_render.Clear();
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Returns an array of absolute graph data points to render.
        /// This method is called per data series.
        /// </summary>
        /// <param name="seriesCollection">A collection of all data series that is currently in the rendering pass.</param>
        /// <param name="series">The current data series to render.</param>
        /// <param name="toRender">Pending data series object to render.</param>
        /// <returns></returns>
        protected abstract List<GraphPoint> OnRender(IEnumerable<PendingSeries> seriesCollection, TDataSeries series, PendingSeries toRender);

        /// <summary>
        /// Converts the specified relative x position to graph absolute position.
        /// </summary>
        /// <param name="x">The relative x position.</param>
        /// <returns></returns>
        protected virtual float ConvertXValueToRendererValue(double x)
        {
            return (float)(x * Output.SurfaceWidth / 100);
        }

        /// <summary>
        /// Converts the specified relative y position to graph absolute position.
        /// </summary>
        /// <param name="y">The relative y position.</param>
        /// <returns></returns>
        protected virtual float ConvertYValueToRendererValue(double y)
        {
            return (float)(Output.SurfaceHeight - (y * Output.SurfaceHeight / 100));
        }

        /// <summary>
        /// Raises the <see cref="EffectiveRangeXChanged"/> event.
        /// </summary>
        /// <param name="minimumX">The minimum x.</param>
        /// <param name="maximumX">The maximum x.</param>
        protected virtual void OnEffectiveRangeXChanged(GraphDataPointBase minimumX, GraphDataPointBase maximumX)
        {
            EffectiveMinimumX = minimumX;
            EffectiveMaximumX = maximumX;

            EffectiveRangeXChanged?.Invoke(this, new RangeChangedEventArgs()
            {
                Maximum = minimumX,
                Minimum = maximumX,
            });
        }

        /// <summary>
        /// Raises the <see cref="EffectiveRangeYChanged"/> event.
        /// </summary>
        /// <param name="minimumY">The minimum y.</param>
        /// <param name="maximumY">The maximum y.</param>
        protected virtual void OnEffectiveRangeYChanged(GraphDataPointBase minimumY, GraphDataPointBase maximumY)
        {
            EffectiveMinimumY = minimumY;
            EffectiveMaximumY = maximumY;

            EffectiveRangeYChanged?.Invoke(this, new RangeChangedEventArgs()
            {
                Maximum = minimumY,
                Minimum = maximumY,
            });
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Renders the and draws a complete set of data series and their data points.
        /// </summary>
        /// <param name="toRender">To render.</param>
        private void RenderAndDraw(List<KeyValuePair<TDataSeries, PendingSeries>> toRender)
        {
            lock (_render_lock)
            {
                var all_series = toRender.Select(x => x.Value);

                foreach (var series in toRender)
                {
                    var points = OnRender(all_series, series.Key, series.Value);

                    series.Value.RenderedItems = series.Value.XX.Count;

                    if (points.Count > 1)
                    {
                        Output.Draw(all_series.Select(x => x.Series), series.Value.Series, points);
                    }
                }
            }
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Handles the controller range changed event.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The event arguments.</param>
        private void Input_RangeChanged(object sender, GraphRange<XDataPoint, YDataPoint> e)
        {
            OnEffectiveRangeYChanged(Input.Range.MinimumY, Input.Range.MaximumY);
            OnEffectiveRangeXChanged(EffectiveMinimumX, Input.Range.MaximumX);
        }

        #endregion
    }
}