using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media.Animation; using System.Windows.Threading; using Infragistics.Controls.Charts; namespace Infragistics.Samples.Common.Frameworks { /// <summary> /// Provides animation of data points over time in xamDataChart control /// </summary> public class MotionFrameworkManger : ObservableModel { public MotionFrameworkManger() { _transitionDuration = TimeSpan.FromMilliseconds(1000); _transitionFunction = new ExponentialEase { EasingMode = EasingMode.EaseInOut }; this.AutosetAxisMargin = new Thickness(0, 0.5, 0, 0.5); this.AutosetAxisRange = true; this.DataUpdateTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1000) }; this.DataUpdateTimer.Tick += OnDataUpdateTimerTick; } #region Fields protected DispatcherTimer DataUpdateTimer; protected int ElementsMaxCount; #endregion #region Events public delegate void CurrentDateChangedEventHandler(object sender, DateTimeChangedEventArgs e); public event CurrentDateChangedEventHandler CurrentDateChanged; protected virtual void OnCurrentDateChanged(DateTimeChangedEventArgs e) { if (CurrentDateChanged != null) CurrentDateChanged(this, e); } public delegate void PlaybackStoppedEventHandler(object sender, EventArgs e); public event PlaybackStoppedEventHandler PlaybackStopped; protected virtual void OnPlaybackStopped(EventArgs e) { if (PlaybackStopped != null) PlaybackStopped(this, e); } public delegate void PlaybackStartedEventHandler(object sender, EventArgs e); public event PlaybackStartedEventHandler PlaybackStarted; protected virtual void OnPlaybackStarted(EventArgs e) { if (PlaybackStarted != null) PlaybackStarted(this, e); } public class DateTimeChangedEventArgs : EventArgs { public DateTime CurrentDate { get; set; } } #endregion #region Event Handlers private void OnDateTimeSliderValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { if (!this.IsPlaying) { if (!this.IsInitialized && this.RequiredConditionsCheck()) { this.InitializeDataChart(); this.IsInitialized = true; } this.CurrentElementShown = (int)e.NewValue; if (this.CurrentElementShown < this.ElementsMaxCount) this.RefreshSeries(); } } private void OnDataUpdateTimerTick(object sender, EventArgs e) { if (this.CurrentElementShown < this.ElementsMaxCount) { this.RefreshSeries(); this.CurrentElementShown++; if (this.DateTimeSlider != null) { this.DateTimeSlider.Value = this.CurrentElementShown; this.DateTimeSlider.Tag = this.CurrentDateTime.ToShortDateString(); } } else { this.Pause(); } } #endregion #region Private Methods private bool RequiredConditionsCheck() { bool ret = this.Chart != null; ret = (ret && this.DataSources != null); ret = (ret && this.XAxisName != null); ret = (ret && this.YAxisName != null); ret = (ret && this.SeriesXMemberPath != null); ret = (ret && this.SeriesYMemberPath != null); ret = (ret && this.SeriesRadiusMemberPath != null); return ret; } private string GetSeriesName(int key, bool isUnique = false) { if (isUnique) return string.Format("{0}|Series {0}", key); return string.Format("Series{0}", key); } private int GetNumDigits(double max, double min) { double error = (max - min) / 100; int numDigits = 0; while (error < 1) { error *= 10; ++numDigits; } return numDigits; } private void InitializeDataChart() { if (this.RequiredConditionsCheck()) { this.Chart.Series.Clear(); this.ElementsMaxCount = this.DataSources[0].Count; this.CurrentElementShown = 0; double minX = Double.PositiveInfinity; double minY = Double.PositiveInfinity; double maxX = Double.NegativeInfinity; double maxY = Double.NegativeInfinity; DateTime minDateTime = DateTime.MaxValue; DateTime maxDateTime = DateTime.MinValue; foreach (var item in this.DataSources) { this.InitializeSeries(this.GetSeriesName(item.Key), item.Key); //if autoset enabled, traverse all data points for this series //looking for the smallest/largest values along X and Y #region Find Min/Max Values if (this.AutosetAxisRange) { //make sure the item.value list is sorted along its datetime property foreach (var seriesPoint in item.Value) { double xValue = (double)seriesPoint.GetType().GetProperty(this.SeriesXMemberPath).GetValue(seriesPoint, null); double yValue = (double)seriesPoint.GetType().GetProperty(this.SeriesYMemberPath).GetValue(seriesPoint, null); DateTime dateTimeValue = (DateTime)seriesPoint.GetType().GetProperty(this.SeriesTimeMemberPath).GetValue(seriesPoint, null); if (xValue < minX) minX = xValue; if (xValue > maxX) maxX = xValue; if (yValue < minY) minY = yValue; if (yValue > maxY) maxY = yValue; if (dateTimeValue < minDateTime) minDateTime = dateTimeValue; if (dateTimeValue > maxDateTime) maxDateTime = dateTimeValue; } } #endregion } #region Set range of axes based on min/max values if (this.AutosetAxisRange) { NumericXAxis xAxis = this.Chart.FindName(XAxisName) as NumericXAxis; NumericYAxis yAxis = this.Chart.FindName(YAxisName) as NumericYAxis; double xAxisMarginLeft = ((maxX - minX) * this.AutosetAxisMargin.Left) / 2; double xAxisMarginRight = ((maxX - minX) * this.AutosetAxisMargin.Right) / 2; if (xAxis != null) { xAxis.MinimumValue = System.Math.Round(((float)minX - (float)xAxisMarginLeft), GetNumDigits(maxX, minX)); xAxis.MaximumValue = System.Math.Round(((float)maxX + (float)xAxisMarginRight), GetNumDigits(maxX, minX)); } double yAxisMarginTop = ((maxY - minY) * this.AutosetAxisMargin.Top) / 2; double yAxisMarginBottom = ((maxY - minY) * this.AutosetAxisMargin.Bottom) / 2; if (yAxis != null) { yAxis.MinimumValue = System.Math.Round(((float)minY - (float)yAxisMarginBottom), GetNumDigits(maxY, minY)); yAxis.MaximumValue = System.Math.Round(((float)maxY + (float)yAxisMarginTop), GetNumDigits(maxY, minY)); } } #endregion if (this.DateTimeSlider != null) { this.MinDateTime = minDateTime; this.MaxDateTime = maxDateTime; this.DateTimeSlider.Minimum = 0; this.DateTimeSlider.Maximum = this.ElementsMaxCount - 1; } } } private void InitializeSeries(string name, int dataOrder, IEnumerable dataSource = null) { BubbleSeries motionSeries = new BubbleSeries(); motionSeries.Name = this.GetSeriesName(dataOrder); motionSeries.YMemberPath = this.SeriesYMemberPath; motionSeries.XMemberPath = this.SeriesXMemberPath; motionSeries.RadiusMemberPath = this.SeriesRadiusMemberPath; motionSeries.TransitionDuration = this.TransitionDuration; motionSeries.TransitionEasingFunction = this.TransitionFunction; motionSeries.Title = name; motionSeries.XAxis = this.Chart.FindName(XAxisName) as NumericXAxis; motionSeries.YAxis = this.Chart.FindName(YAxisName) as NumericYAxis; motionSeries.MarkerTemplate = this.MarkerTemplate; if (this.ChartLegend != null) motionSeries.Legend = this.ChartLegend; if (this.LegendItemTemplate != null) motionSeries.LegendItemTemplate = this.LegendItemTemplate; // adding the single data point that will be changed in order to be animated over time List<MotionDataPoint> newDataSource = new List<MotionDataPoint>(); newDataSource.Add(new MotionDataPoint { ValueX = 0, ValueY = 0, ValueR = 0 }); motionSeries.ItemsSource = newDataSource; if (dataSource != null) motionSeries.ItemsSource = dataSource; ScatterSplineSeries trailSeries = CopySeries(motionSeries); trailSeries.Thickness = 2; motionSeries.Tag = trailSeries; this.Chart.Series.Add(trailSeries); this.Chart.Series.Add(motionSeries); } private ScatterSplineSeries CopySeries(BubbleSeries series) { ScatterSplineSeries trailSeries = new ScatterSplineSeries(); trailSeries.MarkerType = MarkerType.None; trailSeries.YMemberPath = series.YMemberPath; trailSeries.XMemberPath = series.XMemberPath; trailSeries.TransitionDuration = series.TransitionDuration; trailSeries.Title = series.Title; trailSeries.XAxis = series.XAxis; trailSeries.YAxis = series.YAxis; MotionDataSource<DataPoint> ds = new MotionDataSource<DataPoint>(); trailSeries.ItemsSource = ds; return trailSeries; } private void UpdateMotionSeries(Series series, DataPoint dataPoint) { ((IList<MotionDataPoint>)series.ItemsSource)[0].ValueX = dataPoint.ValueX; ((IList<MotionDataPoint>)series.ItemsSource)[0].ValueY = dataPoint.ValueY; ((IList<MotionDataPoint>)series.ItemsSource)[0].ValueR = dataPoint.ValueR; ((IList<MotionDataPoint>)series.ItemsSource)[0].ToolTip = dataPoint.ToolTip; } private void UpdateTrailSeries(Series series) { ScatterLineSeries trailSeries = series.Tag as ScatterLineSeries; if (trailSeries != null) { trailSeries.Brush = series.ActualBrush; trailSeries.MarkerBrush = series.ActualBrush; MotionDataSource<DataPoint> dm = trailSeries.ItemsSource as MotionDataSource<DataPoint>; if (dm != null) { dm.Add(new DataPoint { ValueX = ((IList<MotionDataPoint>)series.ItemsSource)[0].ValueX, ValueY = ((IList<MotionDataPoint>)series.ItemsSource)[0].ValueY, ToolTip = ((IList<MotionDataPoint>)series.ItemsSource)[0].ToolTip }); } } } private void UpdateTrailSeries(Series series, IEnumerable<DataPoint> dataSource) { ScatterSplineSeries trailSeries = series.Tag as ScatterSplineSeries; if (trailSeries != null) { trailSeries.Brush = series.ActualBrush; trailSeries.MarkerBrush = series.ActualBrush; trailSeries.ItemsSource = dataSource; } } private void UpdateSeriesTransitions() { if (this.IsInitialized) { this.Chart.Series.Clear(); // update all series with the current Transition parameters foreach (var item in this.DataSources) { string seriesName = this.GetSeriesName(item.Key); this.InitializeSeries(seriesName, item.Key); DataPoint dataPoint; List<DataPoint> trailsDataSource; if (this.CurrentElementShown < this.ElementsMaxCount) { dataPoint = (DataPoint)item.Value[this.CurrentElementShown]; trailsDataSource = ((List<DataPoint>)item.Value).GetRange(0, this.CurrentElementShown + 1); } else { dataPoint = (DataPoint)item.Value[this.ElementsMaxCount - 1]; trailsDataSource = ((List<DataPoint>)item.Value).GetRange(0, this.CurrentElementShown); } this.UpdateMotionSeries(this.Chart.Series[seriesName], dataPoint); if (this.ShowTrails) { // update trails series with all data points up to the current element shown this.UpdateTrailSeries(this.Chart.Series[seriesName], trailsDataSource); } } } } private void RefreshSeries() { foreach (var item in this.DataSources) { // update the motion series with the current element shown string seriesName = this.GetSeriesName(item.Key); DataPoint dataPoint = (DataPoint)item.Value[this.CurrentElementShown]; this.UpdateMotionSeries(this.Chart.Series[seriesName], dataPoint); if (this.ShowTrails) { // update trails series with all data points up to the current element shown List<DataPoint> trailsDataSource = ((List<DataPoint>)item.Value).GetRange(0, this.CurrentElementShown + 1); this.UpdateTrailSeries(this.Chart.Series[seriesName], trailsDataSource); } } } #endregion #region Public Methods /// <summary> /// Initializes elements of MotionFramework /// </summary> public void Initialize() { if (this.RequiredConditionsCheck()) { this.InitializeDataChart(); this.DataUpdateTimer.Interval = this.DataUpdateInterval; this.CurrentElementShown = 0; this.IsInitialized = true; } } /// <summary> /// Starts animation of data points over time in xamDataChart control /// </summary> public void Play() { if (!this.IsInitialized) this.Initialize(); //starting play when playback has finished resets the current element shown to 0 if (this.CurrentElementShown >= this.ElementsMaxCount - 1) { this.CurrentElementShown = 0; if (this.DateTimeSlider != null) this.DateTimeSlider.Value = 0; } if (this.IsInitialized && this.CurrentElementShown == 0) { //clear scatter series when starting playback from the beginning foreach (Series series in this.Chart.Series) { ScatterLineSeries trailSeries = series as ScatterLineSeries; if (trailSeries != null) { if (trailSeries.ItemsSource != null) ((IList)trailSeries.ItemsSource).Clear(); } } } if (this.DateTimeSlider != null) this.DateTimeSlider.IsEnabled = false; this.DataUpdateTimer.Start(); OnPlaybackStarted(new EventArgs()); } /// <summary> /// Stops animation of data points in xamDataChart control /// </summary> public void Pause() { if (this.DataUpdateTimer.IsEnabled) { if (this.DateTimeSlider != null) this.DateTimeSlider.IsEnabled = true; this.DataUpdateTimer.Stop(); OnPlaybackStopped(new EventArgs()); } } #endregion #region Properties public bool IsInitialized { get; private set; } /// <summary> /// Checks if MotionFramework is playing animation of data points over time /// </summary> public bool IsPlaying { get { return this.DataUpdateTimer.IsEnabled; } } /// <summary> /// Represents the Timer Interval between update in animation of data points /// </summary> public TimeSpan DataUpdateInterval { get { return this.DataUpdateTimer.Interval; } set { this.DataUpdateTimer.Interval = value; } } private TimeSpan _transitionDuration; public TimeSpan TransitionDuration { get { return _transitionDuration; } set { if (_transitionDuration == value) return; _transitionDuration = value; this.UpdateSeriesTransitions(); this.OnPropertyChanged("TransitionDuration"); } } private EasingFunctionBase _transitionFunction; public EasingFunctionBase TransitionFunction { get { return _transitionFunction; } set { if (_transitionFunction == value) return; _transitionFunction = value; this.UpdateSeriesTransitions(); this.OnPropertyChanged("TransitionFunction"); } } private int _currentElementShown; /// <summary> /// Represents the current index of animation of data points /// </summary> public int CurrentElementShown { get { return _currentElementShown; } set { _currentElementShown = value; this.CurrentDateTime = this.CurrentDataPoint.ValueDateTime; this.CurrentDateTimeString = this.CurrentDateTime.ToShortDateString(); } } private string _currentDateTimeString; /// <summary> /// Represents the current Date Time of animation of data points as string /// </summary> public string CurrentDateTimeString { get { return _currentDateTimeString; } set { if (_currentDateTimeString == value) return; _currentDateTimeString = value; this.OnPropertyChanged("CurrentDateTimeString"); } } private DateTime _currentDateTime; /// <summary> /// Represents the current Date Time of animation of data points /// </summary> public DateTime CurrentDateTime { get { return _currentDateTime; } set { if (_currentDateTime == value) return; _currentDateTime = value; this.OnPropertyChanged("CurrentDateTime"); OnCurrentDateChanged(new DateTimeChangedEventArgs { CurrentDate = this.CurrentDateTime }); } } protected DataPoint CurrentDataPoint { get { if (this.CurrentElementShown < this.ElementsMaxCount) return ((DataPoint)this.DataSources[0][this.CurrentElementShown]); return ((DataPoint)this.DataSources[0][this.ElementsMaxCount - 1]); } } public TimeSpan CurrentTimeSpan { get { return this.CurrentDateTime.Subtract(this.MinDateTime); } } public DateTime MinDateTime { get; private set; } public DateTime MaxDateTime { get; private set; } public Thickness AutosetAxisMargin { get; set; } public bool AutosetAxisRange { get; set; } public bool ShowTrails { get; set; } private Slider _dateTimeSlider; /// <summary> /// Represents the Slider that controls animation of data points over time /// </summary> public Slider DateTimeSlider { get { return _dateTimeSlider; } set { if (_dateTimeSlider != null) _dateTimeSlider.ValueChanged -= OnDateTimeSliderValueChanged; _dateTimeSlider = value; if (_dateTimeSlider != null) _dateTimeSlider.ValueChanged += OnDateTimeSliderValueChanged; } } public Legend ChartLegend { get; set; } public XamDataChart Chart { get; set; } private Dictionary<int, IList> _dataSource; /// <summary> /// Represents Data Sources for all series in the xamDataChart /// </summary> public Dictionary<int, IList> DataSources { get { return _dataSource; } set { if (_dateTimeSlider == null) return; _dataSource = value; } } public string XAxisName { get; set; } public string YAxisName { get; set; } public string SeriesXMemberPath { get; set; } public string SeriesYMemberPath { get; set; } public string SeriesRadiusMemberPath { get; set; } public string SeriesTimeMemberPath { get; set; } public DataTemplate MarkerTemplate { get; set; } public DataTemplate LegendItemTemplate { get; set; } #endregion } public class DataSourceGenerator { protected static Random Random; static DataSourceGenerator() { Random = new Random(); } public static IList<DataPoint> GetRandomData(int maxValues, int order) { List<DataPoint> result = new List<DataPoint>(maxValues); DateTime valueDateTime = DateTime.Now.AddDays(maxValues * -1); double valueModifier = (order % 2 == 0) ? 1 : -1; double valueMultiplier = (order % 2 == 0) ? order : order - 1; for (int i = 0; i < maxValues; i++) { double x = Convert.ToDouble(i) / 100f; double y = valueModifier * System.Math.Round(System.Math.Sin(x * (valueMultiplier + 1)), 2); double r = 10 + (Convert.ToDouble(i) / 25f) * (order + 1); result.Add(new DataPoint { ValueX = x, ValueY = y, ValueR = r, ValueDateTime = valueDateTime }); valueDateTime = valueDateTime.AddDays(1); } return result; } public static Dictionary<int, IList> GetDataSources(int maxSeries, int maxDataPerSeries) { Dictionary<int, IList> dict = new Dictionary<int, IList>(); for (int i = 0; i < maxSeries; i++) { dict.Add(i, (IList)GetRandomData(maxDataPerSeries, i)); } return dict; } } public class DataPoint { public DateTime ValueDateTime { get; set; } public string ToolTip { get; set; } public double ValueX { get; set; } public double ValueY { get; set; } public double ValueR { get; set; } } public class MotionDataSource<T> : ObservableCollection<T> { } public class MotionDataPoint : ObservableModel { private double _valueX; private double _valueY; private double _valueR; public DateTime ValueDateTime { get; set; } public string ToolTip { get; set; } public static string PropertyNameValueX = "ValueX"; public static string PropertyNameValueY = "ValueY"; public static string PropertyNameValueR = "ValueR"; public static string PropertyNameValueDateTime = "ValueDateTime"; public double ValueX { get { return _valueX; } set { if (_valueX == value) return; _valueX = value; this.OnPropertyChanged(PropertyNameValueX); } } public double ValueY { get { return _valueY; } set { if (_valueY == value) return; _valueY = value; this.OnPropertyChanged(PropertyNameValueY); } } public double ValueR { get { return _valueR; } set { if (_valueR == value) return; _valueR = value; this.OnPropertyChanged(PropertyNameValueR); } } } public abstract class ObservableModel : INotifyPropertyChanged { #region Event Handlers public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); } protected void OnPropertyChanged(PropertyChangedEventArgs propertyChangedEventArgs) { PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) handler(this, propertyChangedEventArgs); } #endregion } }