Click here to Skip to main content
15,895,084 members
Articles / Web Development / ASP.NET

Legion: Build your own virtual super computer with Silverlight

Rate me:
Please Sign up or sign in to vote.
4.87/5 (139 votes)
27 Oct 2008LGPL321 min read 423K   1.1K   335  
Legion is a grid computing framework that uses the Silverlight CLR to execute user definable tasks. It provides grid-wide thread-safe operations for web clients. Client performance metrics, such as bandwidth and processor speed, may be used to tailor jobs. Also includes a WPF Manager application.
//  This code file for the WPF Realtime Line Graph control was developed
//  by Andre de Cavaignac and Daniel Simon at Lab49.
//
//  The code in this file can be freely used and redistributed in applications
//  providing that this file header is maintained in files relating to the
//  line graph control.
//
//  2007, Andre de Cavaignac and Daniel Simon
//
//  Lab49 Blog:
//      http://blog.lab49.com
//  Andre de Cavaignac's Blog:
//      http://decav.com
//  Andre de Cavaignac's Blog Article on this Control:
//      http://decav.com/blogs/andre/archive/2007/08/25/live-updating-line-graph-in-wpf.aspx
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Media.Animation;
using System.Diagnostics;

namespace Decav.Windows.Controls.LineGraph
{
    /// <summary>
    /// A line graph that updates in realtime with new data points.
    /// </summary>
    public partial class TickingLineGraph : UserControl
    {
        public TickingLineGraph()
        {
            AutoStart = false;
            InitializeComponent();
            GraphMargin = 0.1M;
            _window = new GraphTickTimeWindow(Duration, _ticks);
            Ticks.CollectionChanged += new NotifyCollectionChangedEventHandler(Ticks_CollectionChanged);
            _window.MaximumValueChanged += new EventHandler(Ticks_MinMaxValueChanged);
            _window.MinimumValueChanged += new EventHandler(Ticks_MinMaxValueChanged);

            this.Loaded += new RoutedEventHandler(TickingLineGraph_Loaded);
        }

        /// <summary>
        /// Starts the graph animation and begins plotting points.
        /// </summary>
        public void Start()
        {
            if (_started)
                return;

            _timeSinceStart.Start();

            _started = true;
        }

#region Protected Overrides
        protected override Size MeasureOverride(Size constraint)
        {
            Size suggest = base.MeasureOverride(constraint);
            return (suggest == Size.Empty) ? constraint : suggest;
        }

        protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            Redraw();
            base.OnRenderSizeChanged(sizeInfo);
        }

        void TickingLineGraph_Loaded(object sender, RoutedEventArgs e)
        {
            if (AutoStart)
                Start();
        }
#endregion

        #region Ticks Window, Collection Handlers
        void Ticks_MinMaxValueChanged(object sender, EventArgs e)
        {
        }

        void Ticks_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    foreach (GraphTick tick in e.NewItems)
                        AddTick(tick);
                    break;

                case NotifyCollectionChangedAction.Remove:
                default:
                    Redraw();
                    break;
            }
        }
        #endregion

        private Stopwatch _timeSinceStart = new Stopwatch();
        private bool _started;
        private TimeSpan _timeAtDrawingZero = TimeSpan.Zero;
        private GraphTickTimeWindow _window;
        private readonly GraphTickCollection _ticks = new GraphTickCollection();

        #region Internal Properties

        /// <summary>
        /// Gets the moving average simulated time that passes per second of real time.
        /// </summary>
        public TimeSpan AverageSimulatedTimePerSecond
        {
            get
            {
                if (Ticks.Count < 2)
                    throw new InvalidOperationException("Cannot calculate the time because there are not enough ticks.");

                int avgWindow = Math.Min(5, Ticks.Count);

                GraphTick lastTick = Ticks[Ticks.Count - 1];
                GraphTick windowTick = Ticks[Ticks.Count - avgWindow];

                TimeSpan realDelta = lastTick.TimeRecieved - windowTick.TimeRecieved;
                TimeSpan simDelta = lastTick.Time - windowTick.Time;

                if (realDelta < TimeSpan.Zero || simDelta < TimeSpan.Zero)
                    throw new InvalidOperationException("The ticks in the ticks collection were not in the correct chronological order.");

                return TimeSpan.FromSeconds(
                    simDelta.TotalSeconds / realDelta.TotalSeconds);
            }
        }

        #region CurrentValueInPixelHeight
        /// <summary>
        /// The dependency property that gets or sets the last value of the control in pixel height.
        /// </summary>
        protected static DependencyProperty CurrentValueInPixelHeightProperty = DependencyProperty.Register(
            "CurrentValueInPixelHeight", typeof(double), typeof(TickingLineGraph));

        /// <summary>
        /// Gets or sets the last value of the control in pixel height.
        /// </summary>
        protected double CurrentValueInPixelHeight
        {
            get { return (double)GetValue(CurrentValueInPixelHeightProperty); }
            set { SetValue(CurrentValueInPixelHeightProperty, value); }
        }
        #endregion

        /// <summary>
        /// Gets the dueation of the graph in points.
        /// </summary>
        protected double DurationInPoints
        {
            get
            {
                return TimeToWidth(Duration);
            }
        }

        /// <summary>
        /// Gers the time at the zero point of the drawing area.
        /// </summary>
        protected TimeSpan TimeAtDrawingZero
        {
            get
            {
                return _timeAtDrawingZero;
            }
            private set
            {
                _timeAtDrawingZero = value;
            }
        }

        /// <summary>
        /// Gets how far the left most end of the graph window is from the 0 point of the
        /// drawing area.
        /// </summary>
        protected double StartXRelativeToDrawingZero
        {
            get
            {
                return (double)GraphLine.GetValue(Canvas.LeftProperty)*-1;
            }
        }

        /// <summary>
        /// Gets how far the right most end of the graph window is from the 0 point of the
        /// drawing area.
        /// </summary>
        protected double EndXRelativeToDrawingZero
        {
            get
            {
                return StartXRelativeToDrawingZero + DurationInPoints;
            }
        }

        /// <summary>
        /// Gets the time represented by the left edge of the view window.
        /// </summary>
        protected TimeSpan StartTime
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// Gets the time represented by the right edge of the view window.
        /// </summary>
        protected TimeSpan EndTime
        {
            get
            {
                throw new NotImplementedException();
            }
        }
        
        /// <summary>
        /// Gets the value that is at the top of the view window, including <see cref="GraphMargin"/>.
        /// </summary>
        protected decimal TopValueInternal
        {
            get
            {
                if (Ticks.Count == 0)
                    return 100;

                return (_window.MaximumValue.Value + GraphMarginValue);
            }
        }

        /// <summary>
        /// Gets the value of the <see cref="GraphMargin"/> after it is calculated
        /// against the <see cref="GraphTickCollection.ValueSpread"/>.
        /// </summary>
        protected decimal GraphMarginValue
        {
            get
            {
                // If theres only one tick, we'll center the point by putting
                // equal margin on both sides.
                return (Ticks.Count == 1) ? 1 : (Ticks.ValueSpread * GraphMargin);
            }
        }

        /// <summary>
        /// Gets the value that is at the bottom of the view window, including <see cref="GraphMargin"/>.
        /// </summary>
        protected decimal BaseValueInternal
        {
            get
            {
                if (Ticks.Count == 0)
                    return 0;

                return (_window.MinimumValue.Value - GraphMarginValue);
            }
        }

        /// <summary>
        /// Gets the spread between the <see cref="TopValue"/> and <see cref="BaseValue"/>
        /// showing the total visible range on the graph.
        /// </summary>
        protected decimal TotalValueSpread
        {
            get
            {
                return TopValueInternal - BaseValueInternal;
            }
        }


        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the collection of <see cref="GraphTick"/> that are represented on the graph.
        /// </summary>
        public GraphTickCollection Ticks
        {
            get
            {
                return _ticks;
            }
        }

        #region Duration
        /// <summary>
        /// The dependency property that gets or sets the duration of time taken to move the graph from its right bound to its left bound.
        /// </summary>
        public static DependencyProperty DurationProperty = DependencyProperty.Register(
            "Duration", typeof(TimeSpan), typeof(TickingLineGraph),
            new PropertyMetadata(new PropertyChangedCallback(DurationChanged)));

        private static void DurationChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            TimeSpan newValue = (TimeSpan)e.NewValue;

            if (newValue <= TimeSpan.Zero)
                throw new ArgumentException("Duration must be a positive, non-zero TimeSpan.");

            TickingLineGraph graph = (TickingLineGraph)obj;
            graph._window.Duration = newValue;
            graph.Redraw();
        }

        /// <summary>
        /// <para>
        /// Gets or sets the duration of time taken to move the
        /// graph from the right bound to the left bound of the view window.
        /// </para>
        /// <para>
        /// This represents the total duration of time that is displayed within the bounds
        /// of the graph.
        /// </para>
        /// </summary>
        public TimeSpan Duration
        {
            get { return (TimeSpan)GetValue(DurationProperty); }
            set { SetValue(DurationProperty, value); }
        }
        #endregion

        #region AutoStart
        /// <summary>
        /// The dependency property that gets or sets whether or not the graph will automatically start ticking.
        /// </summary>
        public static DependencyProperty AutoStartProperty = DependencyProperty.Register(
            "AutoStart", typeof(bool), typeof(TickingLineGraph));

        /// <summary>
        /// Gets or sets whether or not the graph will automatically start ticking.
        /// </summary>
        public bool AutoStart
        {
            get { return (bool)GetValue(AutoStartProperty); }
            set { SetValue(AutoStartProperty, value); }
        }
        #endregion

        #region GraphMargin
        /// <summary>
        /// The dependency property that gets or sets the margin between the most extreme displayed value and the view window border, as a percent where 1.0 is 100%.
        /// </summary>
        public static DependencyProperty GraphMarginProperty = DependencyProperty.Register(
            "GraphMargin", typeof(decimal), typeof(TickingLineGraph));

        /// <summary>
        /// Gets or sets the margin between the most extreme displayed value and the view window border, as a percent where 1.0 is 100%.
        /// </summary>
        public decimal GraphMargin
        {
            get
            {
                return (decimal)GetValue(GraphMarginProperty);
            }
            set { SetValue(GraphMarginProperty, value); }
        }
        #endregion

        #region TopValue
        /// <summary>
        /// The dependency property that gets or sets the maximum value of the graph.
        /// </summary>
        public static DependencyProperty TopValueProperty = DependencyProperty.Register(
            "TopValue", typeof(decimal), typeof(TickingLineGraph));

        /// <summary>
        /// Gets the maximum value of the graph.
        /// </summary>
        public decimal TopValue
        {
            get { return (decimal)GetValue(TopValueProperty); }
            private set { SetValue(TopValueProperty, value); }
        }

        #region BaseValue
        /// <summary>
        /// The dependency property that gets or sets the minimum value of the graph.
        /// </summary>
        public static DependencyProperty BaseValueProperty = DependencyProperty.Register(
            "BaseValue", typeof(decimal), typeof(TickingLineGraph));

        /// <summary>
        /// Gets the minimum value of the graph.
        /// </summary>
        public decimal BaseValue
        {
            get { return (decimal)GetValue(BaseValueProperty); }
            private set { SetValue(BaseValueProperty, value); }
        }
        #endregion
        
        #endregion

        #region CurrentValue
        /// <summary>
        /// The dependency property that gets or sets the value of the last <see cref="GraphTick"/> in the <see cref="Ticks"/>.
        /// </summary>
        public static DependencyProperty CurrentValueProperty = DependencyProperty.Register(
            "CurrentValue", typeof(GraphTick), typeof(TickingLineGraph));

        /// <summary>
        /// Gets the value of the last <see cref="GraphTick"/> in the <see cref="Ticks"/>.
        /// </summary>
        public GraphTick CurrentValue
        {
            get { return (GraphTick)GetValue(CurrentValueProperty); }
            private set { SetValue(CurrentValueProperty, value); }
        }
        #endregion

        #region GraphLineStroke
        /// <summary>
        /// The dependency property that gets or sets the brush that is used to draw the graph line.
        /// </summary>
        public static DependencyProperty GraphLineStrokeProperty = DependencyProperty.Register(
            "GraphLineStroke", typeof(Brush), typeof(TickingLineGraph));

        /// <summary>
        /// Gets or sets the brush that is used to draw the graph line.
        /// </summary>
        public Brush GraphLineStroke
        {
            get { return (Brush)GetValue(GraphLineStrokeProperty); }
            set { SetValue(GraphLineStrokeProperty, value); }
        }
        #endregion

        #region CurrentValueLineStroke
        /// <summary>
        /// The dependency property that gets or sets the brush used to display the horizontal line that represents the current value.
        /// </summary>
        public static DependencyProperty CurrentValueLineStrokeProperty = DependencyProperty.Register(
            "CurrentValueLineStroke", typeof(Brush), typeof(TickingLineGraph));

        /// <summary>
        /// Gets or sets the brush used to display the horizontal line that represents the current value.
        /// </summary>
        public Brush CurrentValueLineStroke
        {
            get { return (Brush)GetValue(CurrentValueLineStrokeProperty); }
            set { SetValue(CurrentValueLineStrokeProperty, value); }
        }
        #endregion

        #region GraphLineThickness
        /// <summary>
        /// The dependency property that gets or sets the thickness of the graph line.
        /// </summary>
        public static DependencyProperty GraphLineThicknessProperty = DependencyProperty.Register(
            "GraphLineThickness", typeof(double), typeof(TickingLineGraph));

        /// <summary>
        /// Gets or sets the thickness of the graph line.
        /// </summary>
        public double GraphLineThickness
        {
            get { return (double)GetValue(GraphLineThicknessProperty); }
            set { SetValue(GraphLineThicknessProperty, value); }
        }
        #endregion

        #region CurrentValueLineThickness
        /// <summary>
        /// The dependency property that gets or sets the thickness of the line representing the current value.
        /// </summary>
        public static DependencyProperty CurrentValueLineThicknessProperty = DependencyProperty.Register(
            "CurrentValueLineThickness", typeof(double), typeof(TickingLineGraph));

        /// <summary>
        /// Gets or sets the thickness of the line representing the current value.
        /// </summary>
        public double CurrentValueLineThickness
        {
            get { return (double)GetValue(CurrentValueLineThicknessProperty); }
            set { SetValue(CurrentValueLineThicknessProperty, value); }
        }
        #endregion
        

        #endregion

        #region Drawing
        private void AddTick(GraphTick tick)
        {
            CurrentValue = tick;
            Point coordinate = TickToPoint(tick);

            // Move the line that shows the current value down to the
            // coordinate that is being added...
            CurrentValueInPixelHeight = coordinate.Y;

            if (NewTickRequiresRedraw(tick, coordinate))
            {
                Redraw();
                return;
            }

            // If we don't need to redraw, then we can just add the tick to the graph...
            GraphLine.Points.Add(coordinate);
        }

        private void Redraw()
        {
            // Console.WriteLine("{0} Requires Redraw", DateTime.Now.ToString());

            GraphLine.Width = ActualWidth * 3;

            // Start from the right most point we're going to add, and then figure out where the
            // new zero point of the graph is.
            TimeSpan newZeroPoint = Ticks.LastTick.Time - Duration; // This is both starttime, and the new (reset) zero

            // Set this early on so that it can be used by the new point algorythms
            TimeAtDrawingZero = newZeroPoint;
            EndGraphAnimation();
            
            // Go through our window of time and put all our points on our new line...
            PointCollection newPoints = new PointCollection();
            foreach (GraphTick tick in _window)
                newPoints.Add(TickToPoint(tick));

            GraphLine.SetValue(Canvas.LeftProperty, 0.0); // Bring the line all the way back to its starting position.
            GraphLine.Points = newPoints;

            // Move the line that shows the current value down to the
            // coordinate that is now considered current...
            if (newPoints.Count > 1)
                CurrentValueInPixelHeight = newPoints[newPoints.Count - 1].Y;

            // Reset the dprops so that they can be used by bindings outside this graph.
            TopValue = TopValueInternal;
            BaseValue = BaseValueInternal;

            // Use at least three ticks to determine average tick speed, therefore,
            // we don't want to animate till we have those...
            if (Ticks.Count > 2  && AverageSimulatedTimePerSecond != TimeSpan.Zero)
                StartGraphAnimation();
        }

        private bool NewTickRequiresRedraw(GraphTick tick, Point coordinate)
        {
            // Check if the Y exceeds the bounds of the current min/max scale
            // NOTE:  TopValue and BaseValue are saved at the end of Redraw, so these are safe to check
            // because they are not recalculated when things are added to the Ticks collection...
            if (TopValueInternal != TopValue || BaseValueInternal != BaseValue) 
                return true;

            
            // Check X margin of error...  All points should draw at the
            // "end time" of the graph...
            // allow points to be 5% off of the entire window.
            double permittedErrorPoints = (DurationInPoints*0.05);
            if (Math.Abs(coordinate.X - EndXRelativeToDrawingZero) > permittedErrorPoints)
                return true;

            return false;
        }

        private void StartGraphAnimation()
        {
            if (_animation != null)
                return;

            //Console.WriteLine(string.Format("{0} Sim Time: {1}", DateTime.Now, AverageSimulatedTimePerSecond));
            _animation = new DoubleAnimation();
            _animation.FillBehavior = FillBehavior.Stop;
            _animation.From = 0.0;
            _animation.To = DurationInPoints * -2;
            _animation.Completed += new EventHandler(Animation_Completed);
            _animation.Duration =  TimeSpan.FromMilliseconds(Duration.TotalMilliseconds * 2);
            _animation.SpeedRatio = AverageSimulatedTimePerSecond.TotalSeconds;

            GraphLine.BeginAnimation(Canvas.LeftProperty, _animation, HandoffBehavior.SnapshotAndReplace);
        }

        private void EndGraphAnimation()
        {
            if (_animation == null)
                return;

            _animation.Completed -= new EventHandler(Animation_Completed);
            _animation = null;

            // Stop the old animation by overriding it with a new animation.
            DoubleAnimation resetAnimation = new DoubleAnimation();
            resetAnimation.To = 0.0;
            resetAnimation.Duration = TimeSpan.Zero;
            GraphLine.BeginAnimation(Canvas.LeftProperty, _animation, HandoffBehavior.SnapshotAndReplace);
        }

        void Animation_Completed(object sender, EventArgs e)
        {
            EndGraphAnimation();
            Redraw();
        }

        private DoubleAnimation _animation;
        #endregion

        #region Tick Conversions (Methods)

        private Point TickToPoint(GraphTick tick)
        {
            // Tick MUST be part of the collection at the time of evaluations
            return new Point(
                TimeToX(tick.Time),
                ValueToHeight(tick.Value));
        }

        private double ValueToHeight(decimal value)
        {
            double heightPerValue = ViewWindow.ActualHeight / (double)TotalValueSpread;

            return (double)(TopValueInternal - value) * heightPerValue;
        }

        /// <summary>
        /// Gets the X coordinate for a <see cref="TimeSpan"/> value, relative to the
        /// beginning of the drawing area.
        /// </summary>
        private double TimeToX(TimeSpan time)
        {
            TimeSpan timeRelToDrawingArea = time - TimeAtDrawingZero;
            return TimeToWidth(timeRelToDrawingArea);
        }

        /// <summary>
        /// Gets the length of a TimeSpan value, as number of points on the screen
        /// </summary>
        private double TimeToWidth(TimeSpan time)
        {
            return time.TotalMilliseconds * (ViewWindow.ActualWidth / Duration.TotalMilliseconds);
        }
        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Engineer
Switzerland Switzerland
Daniel is a former senior engineer in Technology and Research at the Office of the CTO at Microsoft, working on next generation systems.

Previously Daniel was a nine-time Microsoft MVP and co-founder of Outcoder, a Swiss software and consulting company.

Daniel is the author of Windows Phone 8 Unleashed and Windows Phone 7.5 Unleashed, both published by SAMS.

Daniel is the developer behind several acclaimed mobile apps including Surfy Browser for Android and Windows Phone. Daniel is the creator of a number of popular open-source projects, most notably Codon.

Would you like Daniel to bring value to your organisation? Please contact

Blog | Twitter


Xamarin Experts
Windows 10 Experts

Comments and Discussions