// 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
}
}