Click here to Skip to main content
15,885,366 members
Articles / Desktop Programming / WPF

How to create stock charts using the Silverlight Toolkit

Rate me:
Please Sign up or sign in to vote.
4.70/5 (15 votes)
16 Feb 2009CPOL2 min read 142K   2.7K   65  
An article on how to create a Candlestick stock chart using the Silverlight Toolkit.
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Shapes;

namespace Microsoft.Windows.Controls.DataVisualization
{
    /// <summary>
    /// A panel that plots elements on a one dimensional plane.  In order to 
    /// minimize collisions it moves elements further and further from the edge 
    /// of the plane based on their priority.  Elements that have the same
    /// priority level are always the same distance from the edge.
    /// </summary>
    internal class OrientedPanel : Panel
    {
        #region public double ActualMinimumDistanceBetweenChildren
        /// <summary>
        /// Gets the actual minimum distance between children.
        /// </summary>
        public double ActualMinimumDistanceBetweenChildren
        {
            get { return (double)GetValue(ActualMinimumDistanceBetweenChildrenProperty); }
            private set { SetValue(ActualMinimumDistanceBetweenChildrenProperty, value); }
        }

        /// <summary>
        /// Identifies the ActualMinimumDistanceBetweenChildren dependency property.
        /// </summary>
        public static readonly DependencyProperty ActualMinimumDistanceBetweenChildrenProperty =
            DependencyProperty.Register(
                "ActualMinimumDistanceBetweenChildren",
                typeof(double),
                typeof(OrientedPanel),
                new PropertyMetadata(0.0));

        #endregion public double ActualMinimumDistanceBetweenChildren

        #region public double MinimumDistanceBetweenChildren
        /// <summary>
        /// Gets or sets the minimum distance between children.
        /// </summary>
        public double MinimumDistanceBetweenChildren
        {
            get { return (double)GetValue(MinimumDistanceBetweenChildrenProperty); }
            set { SetValue(MinimumDistanceBetweenChildrenProperty, value); }
        }

        /// <summary>
        /// Identifies the MinimumDistanceBetweenChildren dependency property.
        /// </summary>
        public static readonly DependencyProperty MinimumDistanceBetweenChildrenProperty =
            DependencyProperty.Register(
                "MinimumDistanceBetweenChildren",
                typeof(double),
                typeof(OrientedPanel),
                new PropertyMetadata(0.0, OnMinimumDistanceBetweenChildrenPropertyChanged));

        /// <summary>
        /// MinimumDistanceBetweenChildrenProperty property changed handler.
        /// </summary>
        /// <param name="d">OrientedPanel that changed its MinimumDistanceBetweenChildren.</param>
        /// <param name="e">Event arguments.</param>
        private static void OnMinimumDistanceBetweenChildrenPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            OrientedPanel source = (OrientedPanel)d;
            double oldValue = (double)e.OldValue;
            double newValue = (double)e.NewValue;
            source.OnMinimumDistanceBetweenChildrenPropertyChanged(oldValue, newValue);
        }

        /// <summary>
        /// MinimumDistanceBetweenChildrenProperty property changed handler.
        /// </summary>
        /// <param name="oldValue">Old value.</param>
        /// <param name="newValue">New value.</param>        
        protected virtual void OnMinimumDistanceBetweenChildrenPropertyChanged(double oldValue, double newValue)
        {
            InvalidateMeasure();
        }
        #endregion public double MinimumDistanceBetweenChildren

        #region public double ActualLength
        /// <summary>
        /// Gets the actual length of the panel.
        /// </summary>
        public double ActualLength
        {
            get { return (double)GetValue(ActualLengthProperty); }
        }

        /// <summary>
        /// Identifies the ActualLength dependency property.
        /// </summary>
        public static readonly DependencyProperty ActualLengthProperty =
            DependencyProperty.Register(
                "ActualLength",
                typeof(double),
                typeof(OrientedPanel),
                new PropertyMetadata(0.0));
        #endregion public double ActualLength

        #region public attached double CenterCoordinate
        /// <summary>
        /// Gets the value of the CenterCoordinate attached property for a specified UIElement.
        /// </summary>
        /// <param name="element">The UIElement from which the property value is read.</param>
        /// <returns>The CenterCoordinate property value for the UIElement.</returns>
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This is an attached property and is only intended to be set on UIElement's")]
        public static double GetCenterCoordinate(UIElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return (double) element.GetValue(CenterCoordinateProperty);
        }

        /// <summary>
        /// Sets the value of the CenterCoordinate attached property to a specified UIElement.
        /// </summary>
        /// <param name="element">The UIElement to which the attached property is written.</param>
        /// <param name="value">The needed CenterCoordinate value.</param>
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This is an attached property and is only intended to be set on UIElement's")]
        public static void SetCenterCoordinate(UIElement element, double value)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            element.SetValue(CenterCoordinateProperty, value);
        }

        /// <summary>
        /// Identifies the CenterCoordinate dependency property.
        /// </summary>
        public static readonly DependencyProperty CenterCoordinateProperty =
            DependencyProperty.RegisterAttached(
                "CenterCoordinate",
                typeof(double),
                typeof(OrientedPanel),
                new PropertyMetadata(OnCenterCoordinatePropertyChanged));

        /// <summary>
        /// CenterCoordinateProperty property changed handler.
        /// </summary>
        /// <param name="dependencyObject">UIElement that changed its CenterCoordinate.</param>
        /// <param name="eventArgs">Event arguments.</param>
        public static void OnCenterCoordinatePropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
        {
            UIElement source = dependencyObject as UIElement;
            if (source == null)
            {
                throw new ArgumentNullException("dependencyObject");
            }
            OrientedPanel parent = VisualTreeHelper.GetParent(source) as OrientedPanel;
            if (parent != null)
            {
                parent.InvalidateMeasure();
            }
        }
        #endregion public attached double CenterCoordinate

        #region public double OffsetPadding
        /// <summary>
        /// Gets or sets the amount of offset padding to add between items.
        /// </summary>
        public double OffsetPadding
        {
            get { return (double)GetValue(OffsetPaddingProperty); }
            set { SetValue(OffsetPaddingProperty, value); }
        }

        /// <summary>
        /// Identifies the OffsetPadding dependency property.
        /// </summary>
        public static readonly DependencyProperty OffsetPaddingProperty =
            DependencyProperty.Register(
                "OffsetPadding",
                typeof(double),
                typeof(OrientedPanel),
                new PropertyMetadata(0.0, OnOffsetPaddingPropertyChanged));

        /// <summary>
        /// OffsetPaddingProperty property changed handler.
        /// </summary>
        /// <param name="d">OrientedPanel that changed its OffsetPadding.</param>
        /// <param name="e">Event arguments.</param>
        private static void OnOffsetPaddingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            OrientedPanel source = (OrientedPanel)d;
            double oldValue = (double)e.OldValue;
            double newValue = (double)e.NewValue;
            source.OnOffsetPaddingPropertyChanged(oldValue, newValue);
        }

        /// <summary>
        /// OffsetPaddingProperty property changed handler.
        /// </summary>
        /// <param name="oldValue">Old value.</param>
        /// <param name="newValue">New value.</param>        
        protected virtual void OnOffsetPaddingPropertyChanged(double oldValue, double newValue)
        {
            this.InvalidateMeasure();
        }
        #endregion public double OffsetPadding

        #region public attached int Priority
        /// <summary>
        /// Gets the value of the Priority attached property for a specified UIElement.
        /// </summary>
        /// <param name="element">The UIElement from which the property value is read.</param>
        /// <returns>The Priority property value for the UIElement.</returns>
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This is an attached property and is only intended to be set on UIElement's")]
        public static int GetPriority(UIElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return (int) element.GetValue(PriorityProperty);
        }

        /// <summary>
        /// Sets the value of the Priority attached property to a specified UIElement.
        /// </summary>
        /// <param name="element">The UIElement to which the attached property is written.</param>
        /// <param name="value">The needed Priority value.</param>
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This is an attached property and is only intended to be set on UIElement's")]
        public static void SetPriority(UIElement element, int value)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            element.SetValue(PriorityProperty, value);
        }

        /// <summary>
        /// Identifies the Priority dependency property.
        /// </summary>
        public static readonly DependencyProperty PriorityProperty =
            DependencyProperty.RegisterAttached(
                "Priority",
                typeof(int),
                typeof(OrientedPanel),
                new PropertyMetadata(OnPriorityPropertyChanged));

        /// <summary>
        /// PriorityProperty property changed handler.
        /// </summary>
        /// <param name="dependencyObject">UIElement that changed its Priority.</param>
        /// <param name="eventArgs">Event arguments.</param>
        public static void OnPriorityPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
        {
            UIElement source = dependencyObject as UIElement;
            if (source == null)
            {
                throw new ArgumentNullException("dependencyObject");
            }
            OrientedPanel parent = VisualTreeHelper.GetParent(source) as OrientedPanel;
            if (parent != null)
            {
                parent.InvalidateMeasure();
            }
        }
        #endregion public attached int Priority

        #region public bool IsInverted
        /// <summary>
        /// Gets or sets a value indicating whether the panel is inverted.
        /// </summary>
        public bool IsInverted
        {
            get { return (bool)GetValue(IsInvertedProperty); }
            set { SetValue(IsInvertedProperty, value); }
        }

        /// <summary>
        /// Identifies the IsInverted dependency property.
        /// </summary>
        public static readonly DependencyProperty IsInvertedProperty =
            DependencyProperty.Register(
                "IsInverted",
                typeof(bool),
                typeof(OrientedPanel),
                new PropertyMetadata(false, OnIsInvertedPropertyChanged));

        /// <summary>
        /// IsInvertedProperty property changed handler.
        /// </summary>
        /// <param name="d">OrientedPanel that changed its IsInverted.</param>
        /// <param name="e">Event arguments.</param>
        private static void OnIsInvertedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            OrientedPanel source = (OrientedPanel)d;
            bool oldValue = (bool)e.OldValue;
            bool newValue = (bool)e.NewValue;
            source.OnIsInvertedPropertyChanged(oldValue, newValue);
        }

        /// <summary>
        /// IsInvertedProperty property changed handler.
        /// </summary>
        /// <param name="oldValue">Old value.</param>
        /// <param name="newValue">New value.</param>        
        protected virtual void OnIsInvertedPropertyChanged(bool oldValue, bool newValue)
        {
            InvalidateMeasure();
        }
        #endregion public bool IsInverted

        #region public bool IsReversed
        /// <summary>
        /// Gets or sets a value indicating whether the direction is reversed. 
        /// </summary>
        public bool IsReversed
        {
            get { return (bool)GetValue(IsReversedProperty); }
            set { SetValue(IsReversedProperty, value); }
        }

        /// <summary>
        /// Identifies the IsReversed dependency property.
        /// </summary>
        public static readonly DependencyProperty IsReversedProperty =
            DependencyProperty.Register(
                "IsReversed",
                typeof(bool),
                typeof(OrientedPanel),
                new PropertyMetadata(false, OnIsReversedPropertyChanged));

        /// <summary>
        /// IsReversedProperty property changed handler.
        /// </summary>
        /// <param name="d">OrientedPanel that changed its IsReversed.</param>
        /// <param name="e">Event arguments.</param>
        private static void OnIsReversedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            OrientedPanel source = (OrientedPanel)d;
            bool oldValue = (bool)e.OldValue;
            bool newValue = (bool)e.NewValue;
            source.OnIsReversedPropertyChanged(oldValue, newValue);
        }

        /// <summary>
        /// IsReversedProperty property changed handler.
        /// </summary>
        /// <param name="oldValue">Old value.</param>
        /// <param name="newValue">New value.</param>        
        protected virtual void OnIsReversedPropertyChanged(bool oldValue, bool newValue)
        {
            InvalidateMeasure();
        }
        #endregion public bool IsReversed

        #region public Orientation Orientation
        /// <summary>
        /// Gets or sets the orientation of the panel.
        /// </summary>
        public Orientation Orientation
        {
            get { return (Orientation)GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }

        /// <summary>
        /// Identifies the Orientation dependency property.
        /// </summary>
        public static readonly DependencyProperty OrientationProperty =
            DependencyProperty.Register(
                "Orientation",
                typeof(Orientation),
                typeof(OrientedPanel),
                new PropertyMetadata(Orientation.Horizontal, OnOrientationPropertyChanged));

        /// <summary>
        /// OrientationProperty property changed handler.
        /// </summary>
        /// <param name="d">OrientedPanel that changed its Orientation.</param>
        /// <param name="e">Event arguments.</param>
        private static void OnOrientationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            OrientedPanel source = (OrientedPanel)d;
            Orientation newValue = (Orientation)e.NewValue;
            source.OnOrientationPropertyChanged(newValue);
        }

        /// <summary>
        /// OrientationProperty property changed handler.
        /// </summary>
        /// <param name="newValue">New value.</param>        
        protected virtual void OnOrientationPropertyChanged(Orientation newValue)
        {
            UpdateActualLength();
            InvalidateMeasure();
        }
        #endregion public Orientation Orientation

        /// <summary>
        /// Gets or sets the offset of the edge to use for each priority group.
        /// </summary>
        private IDictionary<int, double> PriorityOffsets { get; set; }

        /// <summary>
        /// Instantiates a new instance of the OrientedPanel class.
        /// </summary>
        public OrientedPanel()
        {
            UpdateActualLength();
        }

        /// <summary>
        /// Updates the actual length property.
        /// </summary>
        private void UpdateActualLength()
        {
            this.SetBinding(ActualLengthProperty, new Binding((Orientation == Orientation.Horizontal) ? "ActualWidth" : "ActualHeight") { Source = this });
        }

        /// <summary>
        /// Returns a sequence of ranges for a given sequence of children and a
        /// length selector.
        /// </summary>
        /// <param name="children">A sequence of children.</param>
        /// <param name="lengthSelector">A function that returns a length given
        /// a UIElement.</param>
        /// <returns>A sequence of ranges.</returns>
        private static IEnumerable<Range<double>> GetRanges(IEnumerable<UIElement> children, Func<UIElement, double> lengthSelector)
        {
            return 
                children
                    .Select(child =>
                    {
                        double centerCoordinate = GetCenterCoordinate(child);
                        double halfLength = lengthSelector(child) / 2;
                        return new Range<double>(centerCoordinate - halfLength, centerCoordinate + halfLength);
                    });
        }

        /// <summary>
        /// Measures children and determines necessary size.
        /// </summary>
        /// <param name="availableSize">The available size.</param>
        /// <returns>The necessary size.</returns>
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Linq use artificially increases cyclomatic complexity.  Linq functions are well-understood.")]
        protected override Size MeasureOverride(Size availableSize)
        {
            double offset = 0.0;
            if (Children.Count > 0)
            {
                Size totalSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
                foreach (UIElement child in this.Children)
                {
                    child.Measure(totalSize);
                }

                Func<UIElement, double> lengthSelector = null;
                Func<UIElement, double> offsetSelector = null;

                if (Orientation == Orientation.Horizontal)
                {
                    lengthSelector = child => GetCorrectedDesiredSize(child).Width;
                    offsetSelector = child => GetCorrectedDesiredSize(child).Height;
                }
                else
                {
                    lengthSelector = child => GetCorrectedDesiredSize(child).Height;
                    offsetSelector = child => GetCorrectedDesiredSize(child).Width;
                }

                IEnumerable<IGrouping<int, UIElement>> priorityGroups =
                    from child in Children
                    group child by GetPriority(child) into priorityGroup
                    select priorityGroup;

                ActualMinimumDistanceBetweenChildren =
                    (from priorityGroup in priorityGroups
                     let orderedElements =
                         (from element in priorityGroup
                          orderby GetCenterCoordinate(element) ascending
                          select element).ToList()
                     where orderedElements.Count >= 2
                     select
                         (EnumerableFunctions.Zip(
                             orderedElements,
                             orderedElements.Skip(1),
                             (leftElement, rightElement) =>
                             {
                                 double halfLeftLength = lengthSelector(leftElement) / 2;
                                 double leftCenterCoordinate = GetCenterCoordinate(leftElement);

                                 double halfRightLength = lengthSelector(rightElement) / 2;
                                 double rightCenterCoordinate = GetCenterCoordinate(rightElement);

                                 return (rightCenterCoordinate - halfRightLength) - (leftCenterCoordinate + halfLeftLength);
                             }))
                             .Min())
                        .MinOrNullable() ?? MinimumDistanceBetweenChildren;

                IEnumerable<int> priorities =
                    Children
                        .Select(child => GetPriority(child)).Distinct().OrderBy(priority => priority).ToList();

                PriorityOffsets = new Dictionary<int, double>();
                foreach (int priority in priorities)
                {
                    PriorityOffsets[priority] = 0.0;
                }

                IEnumerable<Tuple<int, int>> priorityPairs =
                    EnumerableFunctions.Zip(priorities, priorities.Skip(1), (previous, next) => new Tuple<int, int>(previous, next));

                foreach (Tuple<int, int> priorityPair in priorityPairs)
                {
                    IEnumerable<UIElement> currentPriorityChildren = Children.Where(child => GetPriority(child) == priorityPair.First).ToList();

                    IEnumerable<Range<double>> currentPriorityRanges =
                        GetRanges(currentPriorityChildren, lengthSelector);
                    
                    IEnumerable<UIElement> nextPriorityChildren = Children.Where(child => GetPriority(child) == priorityPair.Second).ToList();

                    IEnumerable<Range<double>> nextPriorityRanges =
                        GetRanges(nextPriorityChildren, lengthSelector);

                    bool intersects =
                        (from currentPriorityRange in currentPriorityRanges
                         from nextPriorityRange in nextPriorityRanges
                         select currentPriorityRange.IntersectsWith(nextPriorityRange))
                            .Any(value => value);

                    if (intersects)
                    {
                        double maxCurrentPriorityChildOffset =
                            currentPriorityChildren
                                .Select(child => offsetSelector(child))
                                .MaxOrNullable() ?? 0.0;

                        offset += maxCurrentPriorityChildOffset + OffsetPadding;
                    }
                    PriorityOffsets[priorityPair.Second] = offset;
                }

                offset =
                    (Children
                        .GroupBy(child => GetPriority(child))
                        .Select(
                            group =>
                                group
                                    .Select(child => PriorityOffsets[group.Key] + offsetSelector(child))
                                    .MaxOrNullable()))
                    .Where(num => num.HasValue)
                    .Select(num => num.Value)
                    .MaxOrNullable() ?? 0.0;
            }

            if (Orientation == Orientation.Horizontal)
            {
                return new Size(0, offset);
            }
            else
            {
                return new Size(offset, 0);
            }
        }

        /// <summary>
        /// Arranges items according to position and priority.
        /// </summary>
        /// <param name="finalSize">The final size of the panel.</param>
        /// <returns>The final size of the control.</returns>
        protected override Size ArrangeOverride(Size finalSize)
        {
            foreach (UIElement child in Children)
            {
                double x = 0.0;
                double y = 0.0;

                x = GetCenterCoordinate(child);
                y = PriorityOffsets[GetPriority(child)];

                double totalLength = 0.0;
                double totalOffsetLength = 0.0;
                double length = 0.0;
                double offsetLength = 0.0;
                Size childCorrectedDesiredSize = GetCorrectedDesiredSize(child);
                if (Orientation == Orientation.Horizontal)
                {
                    totalLength = finalSize.Width;
                    length = childCorrectedDesiredSize.Width;
                    offsetLength = childCorrectedDesiredSize.Height;
                    totalOffsetLength = finalSize.Height;
                }
                else if (Orientation == Orientation.Vertical)
                {
                    totalLength = finalSize.Height;
                    length = childCorrectedDesiredSize.Height;
                    offsetLength = childCorrectedDesiredSize.Width;
                    totalOffsetLength = finalSize.Width;
                }

                double halfLength = length / 2;

                double left = 0.0;
                double top = 0.0;
                if (!IsReversed)
                {
                    left = x - halfLength;
                }
                else
                {
                    left = totalLength - Math.Round(x + halfLength);
                }
                if (!IsInverted)
                {
                    top = y;
                }
                else
                {
                    top = totalOffsetLength - Math.Round(y + offsetLength);
                }

                left = Math.Min(Math.Round(left), totalLength - 1);
                top = Math.Round(top);
                if (Orientation == Orientation.Horizontal)
                {
                    child.Arrange(new Rect(left, top, length, offsetLength));
                }
                else if (Orientation == Orientation.Vertical)
                {
                    child.Arrange(new Rect(top, left, offsetLength, length));
                }
            }

            return finalSize;
        }

        /// <summary>
        /// Gets the "corrected" DesiredSize (for Line instances); one that is
        /// more consistent with how the elements actually render.
        /// </summary>
        /// <param name="element">UIElement to get the size for.</param>
        /// <returns>Corrected size.</returns>
        private static Size GetCorrectedDesiredSize(UIElement element)
        {
            Line elementAsLine = element as Line;
            if (null != elementAsLine)
            {
                return new Size(
                    Math.Max(elementAsLine.StrokeThickness, elementAsLine.X2 - elementAsLine.X1),
                    Math.Max(elementAsLine.StrokeThickness, elementAsLine.Y2 - elementAsLine.Y1));
            }
            else
            {
                return element.DesiredSize;
            }
        }
    }
}

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 Code Project Open License (CPOL)


Written By
South Africa South Africa
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions