// (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; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Windows.Controls.DataVisualization.Charting.Primitives; using System.Windows.Markup; namespace System.Windows.Controls.DataVisualization.Charting { /// /// Represents a control that displays a Chart. /// /// Preview [TemplatePart(Name = Chart.ChartAreaName, Type = typeof(EdgePanel))] [TemplatePart(Name = Chart.LegendName, Type = typeof(Legend))] [StyleTypedProperty(Property = "TitleStyle", StyleTargetType = typeof(Title))] [StyleTypedProperty(Property = "LegendStyle", StyleTargetType = typeof(Legend))] [StyleTypedProperty(Property = "ChartAreaStyle", StyleTargetType = typeof(EdgePanel))] [StyleTypedProperty(Property = "PlotAreaStyle", StyleTargetType = typeof(Grid))] [ContentProperty("Series")] public partial class Chart : Control, ISeriesHost { /// /// Specifies the name of the ChartArea TemplatePart. /// private const string ChartAreaName = "ChartArea"; /// /// Specifies the name of the legend TemplatePart. /// private const string LegendName = "Legend"; /// /// Gets or sets the chart area children collection. /// private AggregatedObservableCollection ChartAreaChildren { get; set; } /// /// An adapter that synchronizes changes to the ChartAreaChildren /// property to the ChartArea panel's children collection. /// private ObservableCollectionListAdapter _chartAreaChildrenListAdapter = new ObservableCollectionListAdapter(); /// /// Gets or sets a collection of Axes in the Chart. /// [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Setter is public to work around a limitation with the XAML editing tools.")] [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "value", Justification = "Setter is public to work around a limitation with the XAML editing tools.")] public Collection Axes { get { return _axes; } set { throw new NotSupportedException(Properties.Resources.Chart_Axes_SetterNotSupported); } } /// /// Stores the collection of Axes in the Chart. /// private Collection _axes; /// /// The collection of foreground elements. /// private ObservableCollection _foregroundElements = new NoResetObservableCollection(); /// /// The collection of background elements. /// private ObservableCollection _backgroundElements = new NoResetObservableCollection(); /// /// Gets the collection of foreground elements. /// ObservableCollection ISeriesHost.ForegroundElements { get { return ForegroundElements; } } /// /// Gets the collection of foreground elements. /// protected ObservableCollection ForegroundElements { get { return _foregroundElements; } } /// /// Gets the collection of background elements. /// ObservableCollection ISeriesHost.BackgroundElements { get { return BackgroundElements; } } /// /// Gets the collection of background elements. /// protected ObservableCollection BackgroundElements { get { return _backgroundElements; } } /// /// Axes arranged along the edges. /// private ObservableCollection _edgeAxes = new NoResetObservableCollection(); /// /// Gets or sets the axes that are currently in the chart. /// private IList InternalActualAxes { get; set; } /// /// Gets the actual axes displayed in the chart. /// public ReadOnlyCollection ActualAxes { get; private set; } /// /// Gets or sets the reference to the template's ChartArea. /// private EdgePanel ChartArea { get; set; } /// /// Gets or sets the reference to the Chart's Legend. /// private Legend Legend { get; set; } /// /// Gets or sets the collection of Series displayed by the Chart. /// [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Setter is public to work around a limitation with the XAML editing tools.")] [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "value", Justification = "Setter is public to work around a limitation with the XAML editing tools.")] public Collection Series { get { return _series; } set { throw new NotSupportedException(Properties.Resources.Chart_Series_SetterNotSupported); } } /// /// Stores the collection of Series displayed by the Chart. /// private Collection _series; #region public Style ChartAreaStyle /// /// Gets or sets the Style of the ISeriesHost's ChartArea. /// public Style ChartAreaStyle { get { return GetValue(ChartAreaStyleProperty) as Style; } set { SetValue(ChartAreaStyleProperty, value); } } /// /// Identifies the ChartAreaStyle dependency property. /// public static readonly DependencyProperty ChartAreaStyleProperty = DependencyProperty.Register( "ChartAreaStyle", typeof(Style), typeof(Chart), null); #endregion public Style ChartAreaStyle /// /// Gets the collection of legend items. /// public Collection LegendItems { get; private set; } #region public Style LegendStyle /// /// Gets or sets the Style of the ISeriesHost's Legend. /// public Style LegendStyle { get { return GetValue(LegendStyleProperty) as Style; } set { SetValue(LegendStyleProperty, value); } } /// /// Identifies the LegendStyle dependency property. /// public static readonly DependencyProperty LegendStyleProperty = DependencyProperty.Register( "LegendStyle", typeof(Style), typeof(Chart), null); #endregion public Style LegendStyle #region public object LegendTitle /// /// Gets or sets the Title content of the Legend. /// public object LegendTitle { get { return GetValue(LegendTitleProperty); } set { SetValue(LegendTitleProperty, value); } } /// /// Identifies the LegendTitle dependency property. /// public static readonly DependencyProperty LegendTitleProperty = DependencyProperty.Register( "LegendTitle", typeof(object), typeof(Chart), null); #endregion public object LegendTitle #region public Style PlotAreaStyle /// /// Gets or sets the Style of the ISeriesHost's PlotArea. /// public Style PlotAreaStyle { get { return GetValue(PlotAreaStyleProperty) as Style; } set { SetValue(PlotAreaStyleProperty, value); } } /// /// Identifies the PlotAreaStyle dependency property. /// public static readonly DependencyProperty PlotAreaStyleProperty = DependencyProperty.Register( "PlotAreaStyle", typeof(Style), typeof(Chart), null); #endregion public Style PlotAreaStyle #region public Collection Palette /// /// Gets or sets a palette of ResourceDictionaries used by the children of the Chart. /// [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(Chart), new PropertyMetadata(OnPalettePropertyChanged)); /// /// Called when the value of the Palette property is changed. /// /// Chart that contains the changed Palette. /// /// Event arguments. private static void OnPalettePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { Chart source = (Chart) d; Collection newValue = (Collection)e.NewValue; source.OnPalettePropertyChanged(newValue); } /// /// Called when the value of the Palette property is changed. /// /// The new value for the Palette. private void OnPalettePropertyChanged(Collection newValue) { ResourceDictionaryDispenser.ResourceDictionaries = newValue; } #endregion public Collection Palette /// /// Gets or sets an object that rotates through the palette. /// private ResourceDictionaryDispenser ResourceDictionaryDispenser { get; set; } /// /// Event that is invoked when the ResourceDictionaryDispenser's collection has changed. /// public event EventHandler ResourceDictionariesChanged; #region public object Title /// /// Gets or sets the title displayed for the Chart. /// public object Title { get { return GetValue(TitleProperty); } set { SetValue(TitleProperty, value); } } /// /// Identifies the Title dependency property. /// public static readonly DependencyProperty TitleProperty = DependencyProperty.Register( "Title", typeof(object), typeof(Chart), null); #endregion #region public Style TitleStyle /// /// Gets or sets the Style of the ISeriesHost's Title. /// public Style TitleStyle { get { return GetValue(TitleStyleProperty) as Style; } set { SetValue(TitleStyleProperty, value); } } /// /// Identifies the TitleStyle dependency property. /// public static readonly DependencyProperty TitleStyleProperty = DependencyProperty.Register( "TitleStyle", typeof(Style), typeof(Chart), null); #endregion public Style TitleStyle #if !SILVERLIGHT /// /// Initializes the static members of the Chart class. /// [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Dependency properties are initialized in-line.")] static Chart() { DefaultStyleKeyProperty.OverrideMetadata(typeof(Chart), new FrameworkPropertyMetadata(typeof(Chart))); } #endif /// /// Initializes a new instance of the Chart class. /// public Chart() { #if SILVERLIGHT DefaultStyleKey = typeof(Chart); #endif // Create the backing collection for Series UniqueObservableCollection series = new UniqueObservableCollection(); series.CollectionChanged += new NotifyCollectionChangedEventHandler(SeriesCollectionChanged); _series = series; // Create the backing collection for Axes UniqueObservableCollection axes = new UniqueObservableCollection(); _axes = axes; ObservableCollection actualAxes = new SeriesHostAxesCollection(this, axes); actualAxes.CollectionChanged += ActualAxesCollectionChanged; this.InternalActualAxes = actualAxes; this.ActualAxes = new ReadOnlyCollection(InternalActualAxes); // Create collection for LegendItems LegendItems = new AggregatedObservableCollection(); ChartAreaChildren = new AggregatedObservableCollection(); ChartAreaChildren.ChildCollections.Add(_edgeAxes); ChartAreaChildren.ChildCollections.Add(_backgroundElements); ChartAreaChildren.ChildCollections.Add(Series); ChartAreaChildren.ChildCollections.Add(_foregroundElements); _chartAreaChildrenListAdapter.Collection = ChartAreaChildren; // Create a dispenser ResourceDictionaryDispenser = new ResourceDictionaryDispenser(); ResourceDictionaryDispenser.ResourceDictionariesChanged += delegate { OnResourceDictionariesChanged(EventArgs.Empty); }; } /// /// Invokes the ResourceDictionariesChanged event. /// /// Event arguments. private void OnResourceDictionariesChanged(EventArgs e) { // Forward event on to listeners EventHandler handler = ResourceDictionariesChanged; if (null != handler) { handler.Invoke(this, e); } } /// /// Determines the location of an axis based on the existing axes in /// the chart. /// /// The axis to determine the location of. /// The location of the axis. private AxisLocation GetAutoAxisLocation(Axis axis) { if (axis.Orientation == AxisOrientation.X) { int numberOfTopAxes = InternalActualAxes.OfType().Where(currentAxis => currentAxis.Location == AxisLocation.Top).Count(); int numberOfBottomAxes = InternalActualAxes.OfType().Where(currentAxis => currentAxis.Location == AxisLocation.Bottom).Count(); return (numberOfBottomAxes > numberOfTopAxes) ? AxisLocation.Top : AxisLocation.Bottom; } else if (axis.Orientation == AxisOrientation.Y) { int numberOfLeftAxes = InternalActualAxes.OfType().Where(currentAxis => currentAxis.Location == AxisLocation.Left).Count(); int numberOfRightAxes = InternalActualAxes.OfType().Where(currentAxis => currentAxis.Location == AxisLocation.Right).Count(); return (numberOfLeftAxes > numberOfRightAxes) ? AxisLocation.Right : AxisLocation.Left; } else { return AxisLocation.Auto; } } /// /// Adds an axis to the ISeriesHost area. /// /// The axis to add to the ISeriesHost area. private void AddAxisToChartArea(Axis axis) { IRequireSeriesHost requiresSeriesHost = axis as IRequireSeriesHost; if (requiresSeriesHost != null) { requiresSeriesHost.SeriesHost = this; } if (axis.Location == AxisLocation.Auto) { axis.Location = GetAutoAxisLocation(axis); } SetEdge(axis); axis.LocationChanged += AxisLocationChanged; axis.OrientationChanged += AxisOrientationChanged; if (axis.Location != AxisLocation.Auto) { _edgeAxes.Add(axis); } } /// /// Rebuilds the chart area if an axis orientation is changed. /// /// The source of the event. /// Information about the event. private void AxisOrientationChanged(object sender, RoutedPropertyChangedEventArgs args) { Axis axis = (Axis)sender; axis.Location = GetAutoAxisLocation(axis); } /// /// Sets the Edge property of an axis based on its location and /// orientation. /// /// The axis to set the edge property of. private static void SetEdge(Axis axis) { switch (axis.Location) { case AxisLocation.Bottom: EdgePanel.SetEdge(axis, Edge.Bottom); break; case AxisLocation.Top: EdgePanel.SetEdge(axis, Edge.Top); break; case AxisLocation.Left: EdgePanel.SetEdge(axis, Edge.Left); break; case AxisLocation.Right: EdgePanel.SetEdge(axis, Edge.Right); break; } } /// /// Rebuild the chart area if an axis location is changed. /// /// The source of the event. /// Information about the event. private void AxisLocationChanged(object sender, RoutedPropertyChangedEventArgs args) { Axis axis = (Axis)sender; if (args.NewValue == AxisLocation.Auto) { throw new InvalidOperationException(Properties.Resources.Chart_AxisLocationChanged_CantBeChangedToAutoWhenHostedInsideOfASeriesHost); } SetEdge(axis); _edgeAxes.Remove(axis); _edgeAxes.Add(axis); } /// /// Adds a series to the plot area and injects chart services. /// /// The series to add to the plot area. private void AddSeriesToPlotArea(ISeries series) { series.SeriesHost = this; AggregatedObservableCollection chartLegendItems = this.LegendItems as AggregatedObservableCollection; int indexOfSeries = this.Series.IndexOf(series); chartLegendItems.ChildCollections.Insert(indexOfSeries, series.LegendItems); } /// /// Builds the visual tree for the Chart control when a new template /// is applied. /// public override void OnApplyTemplate() { // Call base implementation base.OnApplyTemplate(); // Unhook events from former template parts if (null != ChartArea) { ChartArea.Children.Clear(); } if (null != Legend) { Legend.ItemsSource = null; } // Access new template parts ChartArea = GetTemplateChild(ChartAreaName) as EdgePanel; Legend = GetTemplateChild(LegendName) as Legend; if (ChartArea != null) { _chartAreaChildrenListAdapter.TargetList = ChartArea.Children; _chartAreaChildrenListAdapter.Populate(); } if (Legend != null) { Legend.ItemsSource = this.LegendItems; } } /// /// Ensures that ISeriesHost is in a consistent state when axes collection is /// changed. /// /// Event source. /// Event arguments. private void ActualAxesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.NewItems != null) { foreach (Axis axis in e.NewItems.OfType()) { AddAxisToChartArea(axis); } } if (e.OldItems != null) { foreach (Axis axis in e.OldItems.OfType()) { RemoveAxisFromChartArea(axis); } } } /// /// Removes an axis from the Chart area. /// /// The axis to remove from the ISeriesHost area. private void RemoveAxisFromChartArea(Axis axis) { axis.LocationChanged -= AxisLocationChanged; axis.OrientationChanged -= AxisOrientationChanged; IRequireSeriesHost requiresSeriesHost = axis as IRequireSeriesHost; if (requiresSeriesHost != null) { requiresSeriesHost.SeriesHost = null; } _edgeAxes.Remove(axis); } /// /// Removes a series from the plot area. /// /// The series to remove from the plot area. /// private void RemoveSeriesFromPlotArea(ISeries series) { AggregatedObservableCollection legendItemsList = LegendItems as AggregatedObservableCollection; legendItemsList.ChildCollections.Remove(series.LegendItems); series.SeriesHost = null; } /// /// Called when the ObservableCollection.CollectionChanged property /// changes. /// /// The object that raised the event. /// The event data. private void SeriesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { // Clear ISeriesHost property of old Series if (null != e.OldItems) { foreach (ISeries series in e.OldItems) { ISeriesHost host = series as ISeriesHost; if (host != null) { foreach (IRequireGlobalSeriesIndex tracksGlobalIndex in host.GetDescendentSeries().OfType()) { tracksGlobalIndex.GlobalSeriesIndexChanged(null); } host.Series.CollectionChanged -= new NotifyCollectionChangedEventHandler(ChildSeriesCollectionChanged); } IRequireGlobalSeriesIndex require = series as IRequireGlobalSeriesIndex; if (require != null) { require.GlobalSeriesIndexChanged(null); } RemoveSeriesFromPlotArea(series); } } // Set ISeriesHost property of new Series if (null != e.NewItems) { foreach (ISeries series in e.NewItems) { ISeriesHost host = series as ISeriesHost; if (null != host) { host.Series.CollectionChanged += new NotifyCollectionChangedEventHandler(ChildSeriesCollectionChanged); } AddSeriesToPlotArea(series); } } if (e.Action != NotifyCollectionChangedAction.Replace) { OnGlobalSeriesIndexesInvalidated(this, new RoutedEventArgs()); } } /// /// Handles changes to the collections of child ISeries implementing ISeriesHost. /// /// Event source. /// Event arguments. private void ChildSeriesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnGlobalSeriesIndexesInvalidated(this, new RoutedEventArgs()); } /// /// 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); } /// /// Updates the global indexes of all descendents that require a global /// index. /// /// The source of the event. /// The event data. private void OnGlobalSeriesIndexesInvalidated(object sender, RoutedEventArgs args) { UpdateGlobalIndexes(); } /// /// Updates the global index property of all Series that track their /// global index. /// private void UpdateGlobalIndexes() { (this as ISeriesHost).GetDescendentSeries().OfType().ForEachWithIndex( (seriesThatTracksGlobalIndex, index) => { seriesThatTracksGlobalIndex.GlobalSeriesIndexChanged(index); }); } /// /// Gets or sets the Series host of the chart. /// /// This will always return null. ISeriesHost IRequireSeriesHost.SeriesHost { get { return SeriesHost; } set { SeriesHost = value; } } /// /// Gets or sets the Series host of the chart. /// /// This will always return null. protected ISeriesHost SeriesHost { get; set; } /// /// Gets the axes collection of the chart. /// ObservableCollection ISeriesHost.Axes { get { return InternalActualAxes as ObservableCollection; } } /// /// Gets the Series collection of the chart. /// ObservableCollection ISeriesHost.Series { get { return (ObservableCollection)Series; } } } }