// (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.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Windows.Data; using System.Windows.Media; namespace System.Windows.Controls.DataVisualization.Charting { /// /// Represents a control that contains a data series to be rendered in pie /// format. /// /// Preview [StyleTypedProperty(Property = DataPointStyleName, StyleTargetType = typeof(PieDataPoint))] [StyleTypedProperty(Property = "LegendItemStyle", StyleTargetType = typeof(LegendItem))] [TemplatePart(Name = DataPointSeries.PlotAreaName, Type = typeof(Canvas))] public partial class PieSeries : DataPointSeries, IResourceDictionaryDispenser, IRequireGlobalSeriesIndex { #region public Collection Palette /// /// Gets or sets a palette of ResourceDictionaries used by the series. /// [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Want to allow this to be set from XAML.")] public Collection Palette { get { return GetValue(PaletteProperty) as Collection; } set { SetValue(PaletteProperty, value); } } /// /// Identifies the Palette dependency property. /// public static readonly DependencyProperty PaletteProperty = DependencyProperty.Register( "Palette", typeof(Collection), typeof(Series), new PropertyMetadata(OnPalettePropertyChanged)); /// /// PaletteProperty property changed handler. /// /// Parent that changed its Palette. /// Event arguments. private static void OnPalettePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { PieSeries source = d as PieSeries; Collection newValue = e.NewValue as Collection; source.OnPalettePropertyChanged(newValue); } /// /// PaletteProperty property changed handler. /// /// New value. private void OnPalettePropertyChanged(Collection newValue) { ResourceDictionaryDispenser.ResourceDictionaries = newValue; } #endregion public Collection Palette /// /// The pie data point style enumerator. /// private IEnumerator _resourceDictionaryEnumerator; #if !SILVERLIGHT /// /// Initializes the static members of the PieSeries class. /// static PieSeries() { DefaultStyleKeyProperty.OverrideMetadata(typeof(PieSeries), new FrameworkPropertyMetadata(typeof(PieSeries))); } #endif /// /// Initializes a new instance of the PieSeries class. /// public PieSeries() { #if SILVERLIGHT this.DefaultStyleKey = typeof(PieSeries); #endif this.ResourceDictionaryDispenser = new ResourceDictionaryDispenser(); ResourceDictionaryDispenser.ResourceDictionariesChanged += delegate { OnResourceDictionariesChanged(EventArgs.Empty); }; } /// /// Invokes the ResourceDictionariesChanged event. /// /// Event arguments. protected virtual void OnResourceDictionariesChanged(EventArgs e) { // Update with new styles Refresh(); // Forward event on to listeners EventHandler handler = ResourceDictionariesChanged; if (null != handler) { handler.Invoke(this, e); } } /// /// A dictionary that links data points to their legend items. /// private Dictionary _dataPointLegendItems = new Dictionary(); /// /// Accepts a ratio of a full rotation, the x and y length and returns /// the 2D point using trigonometric functions. /// /// The ratio of a full rotation [0..1]. /// The x radius. /// The y radius. /// The corresponding 2D point. private static Point ConvertRatioOfRotationToPoint(double ratio, double radiusX, double radiusY) { double radians = (((ratio * 360) - 90) * (Math.PI / 180)); return new Point(radiusX * Math.Cos(radians), radiusY * Math.Sin(radians)); } /// /// Creates a legend item for each data point. /// /// The data point added. protected override void AddDataPoint(DataPoint dataPoint) { base.AddDataPoint(dataPoint); PieDataPoint pieDataPoint = (PieDataPoint)dataPoint; int index = ActiveDataPoints.IndexOf(dataPoint) + 1; LegendItem legendItem = CreatePieLegendItem(dataPoint, index); // Grab a style enumerator if we don't have one already. if (_resourceDictionaryEnumerator == null) { _resourceDictionaryEnumerator = GetResourceDictionaryWithTargetType(this, typeof(PieDataPoint), true); } if (_resourceDictionaryEnumerator.MoveNext()) { ResourceDictionary paletteResources = #if SILVERLIGHT _resourceDictionaryEnumerator.Current.ShallowCopy(); #else _resourceDictionaryEnumerator.Current; #endif pieDataPoint.PaletteResources = paletteResources; pieDataPoint.Resources.MergedDictionaries.Add(paletteResources); } else { pieDataPoint.PaletteResources = null; } pieDataPoint.ActualDataPointStyle = DataPointStyle ?? pieDataPoint.Resources[DataPointStyleName] as Style; pieDataPoint.SetBinding(PieDataPoint.StyleProperty, new Binding(PieDataPoint.ActualDataPointStyleName) { Source = pieDataPoint }); pieDataPoint.ActualLegendItemStyle = LegendItemStyle ?? (pieDataPoint.Resources[LegendItemStyleName] as Style); legendItem.SetBinding(LegendItem.StyleProperty, new Binding(ActualLegendItemStyleName) { Source = pieDataPoint }); _dataPointLegendItems[dataPoint] = legendItem; LegendItems.Add(legendItem); UpdateLegendItemIndexes(); } /// /// Removes data point's legend item when the data point is removed. /// /// The data point to remove. protected override void RemoveDataPoint(DataPoint dataPoint) { base.RemoveDataPoint(dataPoint); if (dataPoint != null) { LegendItem legendItem = _dataPointLegendItems[dataPoint]; _dataPointLegendItems.Remove(dataPoint); LegendItems.Remove(legendItem); UpdateLegendItemIndexes(); } } /// /// Creates a data point. /// /// A data point. protected override DataPoint CreateDataPoint() { return new PieDataPoint(); } /// /// Gets the active pie data points. /// private IEnumerable ActivePieDataPoints { get { return ActiveDataPoints.OfType(); } } /// /// Updates all ratios before data points are updated. /// protected override void OnBeforeUpdateDataPoints() { UpdateRatios(); base.OnBeforeUpdateDataPoints(); } /// /// Called after data points have been loaded from the items source. /// /// New active data points. /// Old inactive data points. protected override void OnDataPointsChanged(IList newDataPoints, IList oldDataPoints) { UpdateDataPoints(newDataPoints); base.OnDataPointsChanged(newDataPoints, oldDataPoints); } /// /// Updates the indexes of all legend items when a change is made to the collection. /// private void UpdateLegendItemIndexes() { int index = 0; foreach (DataPoint dataPoint in ActiveDataPoints) { LegendItem legendItem = _dataPointLegendItems[dataPoint]; legendItem.Content = dataPoint.IndependentValue ?? (index + 1); index++; } } /// /// Updates the ratios of each data point. /// private void UpdateRatios() { double sum = ActivePieDataPoints.Select(pieDataPoint => Math.Abs(ValueHelper.ToDouble(pieDataPoint.DependentValue))).Sum(); // Priming the loop by calculating initial value of // offset ratio and its corresponding points. double offsetRatio = 0; foreach (PieDataPoint dataPoint in ActivePieDataPoints) { double dependentValue = Math.Abs(ValueHelper.ToDouble(dataPoint.DependentValue)); double ratio = dependentValue / sum; if (!ValueHelper.CanGraph(ratio)) { ratio = 0.0; } dataPoint.Ratio = ratio; dataPoint.OffsetRatio = offsetRatio; offsetRatio += ratio; } } /// /// Updates a data point. /// /// The data point to update. protected override void UpdateDataPoint(DataPoint dataPoint) { PieDataPoint pieDataPoint = (PieDataPoint) dataPoint; pieDataPoint.Width = ActualWidth; pieDataPoint.Height = ActualHeight; UpdatePieDataPointGeometry(pieDataPoint, ActualWidth, ActualHeight); Canvas.SetLeft(pieDataPoint, 0); Canvas.SetTop(pieDataPoint, 0); } /// /// Updates the PieDataPoint's Geometry property. /// /// PieDataPoint instance. /// PlotArea width. /// PlotArea height. internal static void UpdatePieDataPointGeometry(PieDataPoint pieDataPoint, double plotAreaWidth, double plotAreaHeight) { double diameter = (plotAreaWidth < plotAreaHeight) ? plotAreaWidth : plotAreaHeight; diameter *= 0.95; double plotAreaRadius = diameter / 2; double maxDistanceFromCenter = 0.0; double sliceRadius = plotAreaRadius - maxDistanceFromCenter; Point translatePoint = new Point(plotAreaWidth / 2, plotAreaHeight / 2); if (pieDataPoint.ActualRatio == 1) { foreach (DependencyProperty dependencyProperty in new DependencyProperty[] { PieDataPoint.GeometryProperty, PieDataPoint.GeometrySelectionProperty, PieDataPoint.GeometryHighlightProperty }) { Geometry geometry = new EllipseGeometry { Center = translatePoint, RadiusX = sliceRadius, RadiusY = sliceRadius }; pieDataPoint.SetValue(dependencyProperty, geometry); } } else { if (pieDataPoint.ActualRatio == 0.0) { pieDataPoint.Geometry = null; pieDataPoint.GeometryHighlight = null; pieDataPoint.GeometrySelection = null; } else { double ratio = pieDataPoint.ActualRatio; double offsetRatio = pieDataPoint.ActualOffsetRatio; double currentRatio = offsetRatio + ratio; Point offsetRatioPoint = ConvertRatioOfRotationToPoint(offsetRatio, sliceRadius, sliceRadius); Point adjustedOffsetRatioPoint = offsetRatioPoint.Translate(translatePoint); // Calculate the last clockwise point in the pie slice Point currentRatioPoint = ConvertRatioOfRotationToPoint(currentRatio, sliceRadius, sliceRadius); // Adjust point using center of plot area as origin // instead of 0,0 Point adjustedCurrentRatioPoint = currentRatioPoint.Translate(translatePoint); foreach (DependencyProperty dependencyProperty in new DependencyProperty[] { PieDataPoint.GeometryProperty, PieDataPoint.GeometrySelectionProperty, PieDataPoint.GeometryHighlightProperty }) { // Creating the pie slice geometry object PathFigure pathFigure = new PathFigure { IsClosed = true }; pathFigure.StartPoint = translatePoint; pathFigure.Segments.Add(new LineSegment { Point = adjustedOffsetRatioPoint }); bool isLargeArc = (currentRatio - offsetRatio) > 0.5; pathFigure.Segments.Add( new ArcSegment { Point = adjustedCurrentRatioPoint, IsLargeArc = isLargeArc, Size = new Size(sliceRadius, sliceRadius), SweepDirection = SweepDirection.Clockwise }); PathGeometry pathGeometry = new PathGeometry(); pathGeometry.Figures.Add(pathFigure); pieDataPoint.SetValue(dependencyProperty, pathGeometry); } } } } /// /// Creates a legend item from a data point. /// /// The data point to use to create the legend item. /// The 1-based index of the Control. /// The series host legend item. protected virtual LegendItem CreatePieLegendItem(DataPoint dataPoint, int index) { LegendItem legendItem = CreateLegendItem(this); // Set the Content of the LegendItem legendItem.Content = dataPoint.IndependentValue ?? index; // Create a representative DataPoint for access to styled properties DataPoint legendDataPoint = CreateDataPoint(); legendDataPoint.DataContext = dataPoint.DataContext; if (null != PlotArea) { // Bounce into the visual tree to get default Style applied PlotArea.Children.Add(legendDataPoint); PlotArea.Children.Remove(legendDataPoint); } legendDataPoint.SetBinding(DataPoint.StyleProperty, new Binding(PieDataPoint.ActualDataPointStyleName) { Source = dataPoint }); legendItem.DataContext = legendDataPoint; return legendItem; } /// /// Attach event handlers to a data point. /// /// The data point. protected override void AttachEventHandlersToDataPoint(DataPoint dataPoint) { PieDataPoint pieDataPoint = dataPoint as PieDataPoint; pieDataPoint.ActualRatioChanged += OnPieDataPointActualRatioChanged; pieDataPoint.ActualOffsetRatioChanged += OnPieDataPointActualOffsetRatioChanged; pieDataPoint.RatioChanged += OnPieDataPointRatioChanged; pieDataPoint.OffsetRatioChanged += OnPieDataPointOffsetRatioChanged; base.AttachEventHandlersToDataPoint(dataPoint); } /// /// Detaches event handlers from a data point. /// /// The data point. protected override void DetachEventHandlersFromDataPoint(DataPoint dataPoint) { PieDataPoint pieDataPoint = dataPoint as PieDataPoint; pieDataPoint.ActualRatioChanged -= OnPieDataPointActualRatioChanged; pieDataPoint.ActualOffsetRatioChanged -= OnPieDataPointActualOffsetRatioChanged; pieDataPoint.RatioChanged -= OnPieDataPointRatioChanged; pieDataPoint.OffsetRatioChanged -= OnPieDataPointOffsetRatioChanged; base.DetachEventHandlersFromDataPoint(dataPoint); } /// /// This method updates the global series index property. /// /// The global index of the series. public void GlobalSeriesIndexChanged(int? globalIndex) { // Do nothing because we want to use up an index but do nothing // with it. } /// /// Updates the data point when the dependent value is changed. /// /// The data point. /// The old value. /// The new value. protected override void OnDataPointDependentValueChanged(DataPoint dataPoint, IComparable oldValue, IComparable newValue) { UpdateRatios(); base.OnDataPointDependentValueChanged(dataPoint, oldValue, newValue); } /// /// Updates the data point when the independent value is changed. /// /// The data point. /// The old value. /// The new value. protected override void OnDataPointIndependentValueChanged(DataPoint dataPoint, object oldValue, object newValue) { _dataPointLegendItems[dataPoint].Content = newValue; base.OnDataPointIndependentValueChanged(dataPoint, oldValue, newValue); } /// /// Updates the data point when the pie data point's actual ratio is /// changed. /// /// The source of the event. /// Information about the event. private void OnPieDataPointActualRatioChanged(object sender, RoutedPropertyChangedEventArgs args) { UpdateDataPoint(sender as DataPoint); } /// /// Updates the data point when the pie data point's actual offset ratio /// is changed. /// /// The source of the event. /// Information about the event. private void OnPieDataPointActualOffsetRatioChanged(object sender, RoutedPropertyChangedEventArgs args) { UpdateDataPoint(sender as DataPoint); } /// /// Updates the data point when the pie data point's ratio is changed. /// /// The source of the event. /// Information about the event. private void OnPieDataPointRatioChanged(object sender, RoutedPropertyChangedEventArgs args) { DataPoint dataPoint = sender as DataPoint; dataPoint.BeginAnimation(PieDataPoint.ActualRatioProperty, "ActualRatio", args.NewValue, TransitionDuration, this.TransitionEasingFunction); } /// /// Updates the data point when the pie data point's offset ratio is /// changed. /// /// The source of the event. /// Information about the event. private void OnPieDataPointOffsetRatioChanged(object sender, RoutedPropertyChangedEventArgs args) { DataPoint dataPoint = sender as DataPoint; dataPoint.BeginAnimation(PieDataPoint.ActualOffsetRatioProperty, "ActualOffsetRatio", args.NewValue, TransitionDuration, this.TransitionEasingFunction); } /// /// Gets or sets an object used to dispense styles from the style /// palette. /// private ResourceDictionaryDispenser ResourceDictionaryDispenser { get; set; } /// /// Event that is invoked when the ResourceDictionaryDispenser's collection has changed. /// public event EventHandler ResourceDictionariesChanged; /// /// Returns a rotating enumerator of ResourceDictionary objects that coordinates /// with the dispenser object to ensure that no two enumerators are on the same /// item. If the dispenser is reset or its collection is changed then the /// enumerators are also reset. /// /// A predicate that returns a value indicating /// whether to return an item. /// An enumerator of ResourceDictionaries. public IEnumerator GetResourceDictionariesWhere(Func predicate) { return ResourceDictionaryDispenser.GetResourceDictionariesWhere(predicate); } /// /// Called when the value of the SeriesHost property changes. /// /// The value to be replaced. /// The new series host value. protected override void OnSeriesHostPropertyChanged(ISeriesHost oldValue, ISeriesHost newValue) { base.OnSeriesHostPropertyChanged(oldValue, newValue); if (null != oldValue) { oldValue.ResourceDictionariesChanged -= new EventHandler(SeriesHostResourceDictionariesChanged); } if (null != newValue) { newValue.ResourceDictionariesChanged += new EventHandler(SeriesHostResourceDictionariesChanged); } else { // Dispose of the enumerator. if (null != _resourceDictionaryEnumerator) { _resourceDictionaryEnumerator.Dispose(); _resourceDictionaryEnumerator = null; } } this.ResourceDictionaryDispenser.Parent = newValue; } /// /// Handles the SeriesHost's ResourceDictionariesChanged event. /// /// ISeriesHost instance. /// Event args. private void SeriesHostResourceDictionariesChanged(object sender, EventArgs e) { Refresh(); } /// /// DataPointStyleProperty property changed handler. /// /// Old value. /// New value. protected override void OnDataPointStylePropertyChanged(Style oldValue, Style newValue) { // Propagate change foreach (PieDataPoint pieDataPoint in ActiveDataPoints) { pieDataPoint.ActualDataPointStyle = newValue ?? (pieDataPoint.Resources[DataPointStyleName] as Style); } base.OnDataPointStylePropertyChanged(oldValue, newValue); } /// /// Called when the value of the LegendItemStyle property changes. /// /// Old value. /// New value. protected override void OnLegendItemStylePropertyChanged(Style oldValue, Style newValue) { // Propagate change foreach (PieDataPoint pieDataPoint in ActiveDataPoints) { pieDataPoint.ActualLegendItemStyle = newValue ?? (pieDataPoint.Resources[LegendItemStyleName] as Style); } base.OnLegendItemStylePropertyChanged(oldValue, newValue); } } }