// (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.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Windows; using System.Windows.Media; using System.Windows.Controls; using System.Windows.Shapes; namespace System.Windows.Controls.DataVisualization.Charting { /// /// An axis that has a range. /// public abstract class RangeAxis : DisplayAxis, IRangeAxis, IValueMarginConsumer { /// /// A pool of major tick marks. /// private ObjectPool _majorTickMarkPool; /// /// A pool of major tick marks. /// private ObjectPool _minorTickMarkPool; /// /// A pool of labels. /// private ObjectPool _labelPool; #region public Style MinorTickMarkStyle /// /// Gets or sets the minor tick mark style. /// public Style MinorTickMarkStyle { get { return GetValue(MinorTickMarkStyleProperty) as Style; } set { SetValue(MinorTickMarkStyleProperty, value); } } /// /// Identifies the MinorTickMarkStyle dependency property. /// public static readonly DependencyProperty MinorTickMarkStyleProperty = DependencyProperty.Register( "MinorTickMarkStyle", typeof(Style), typeof(RangeAxis), new PropertyMetadata(null)); #endregion public Style MinorTickMarkStyle /// /// The actual range of values. /// private Range _actualRange; /// /// Gets or sets the actual range of values. /// protected Range ActualRange { get { return _actualRange; } set { Range oldValue = _actualRange; Range minMaxEnforcedValue = EnforceMaximumAndMinimum(value); if (!oldValue.Equals(minMaxEnforcedValue)) { _actualRange = minMaxEnforcedValue; OnActualRangeChanged(minMaxEnforcedValue); } } } /// /// The maximum value displayed in the range axis. /// private IComparable _protectedMaximum; /// /// Gets or sets the maximum value displayed in the range axis. /// protected IComparable ProtectedMaximum { get { return _protectedMaximum; } set { if (value != null && ProtectedMinimum != null && ValueHelper.Compare(ProtectedMinimum, value) > 0) { throw new InvalidOperationException(Properties.Resources.RangeAxis_MaximumValueMustBeLargerThanOrEqualToMinimumValue); } if (!object.ReferenceEquals(_protectedMaximum, value) && !object.Equals(_protectedMaximum, value)) { _protectedMaximum = value; UpdateActualRange(); } } } /// /// The minimum value displayed in the range axis. /// private IComparable _protectedMinimum; /// /// Gets or sets the minimum value displayed in the range axis. /// protected IComparable ProtectedMinimum { get { return _protectedMinimum; } set { if (value != null && ProtectedMaximum != null && ValueHelper.Compare(value, ProtectedMaximum) > 0) { throw new InvalidOperationException(Properties.Resources.RangeAxis_MinimumValueMustBeLargerThanOrEqualToMaximumValue); } if (!object.ReferenceEquals(_protectedMinimum, value) && !object.Equals(_protectedMinimum, value)) { _protectedMinimum = value; UpdateActualRange(); } } } #if !SILVERLIGHT /// /// Initializes the static members of the RangeAxis class. /// [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Dependency properties are initialized in-line.")] static RangeAxis() { DefaultStyleKeyProperty.OverrideMetadata(typeof(RangeAxis), new FrameworkPropertyMetadata(typeof(RangeAxis))); } #endif /// /// Instantiates a new instance of the RangeAxis class. /// protected RangeAxis() { #if SILVERLIGHT this.DefaultStyleKey = typeof(RangeAxis); #endif this._labelPool = new ObjectPool(() => CreateAxisLabel()); this._majorTickMarkPool = new ObjectPool(() => CreateMajorTickMark()); this._minorTickMarkPool = new ObjectPool(() => CreateMinorTickMark()); // Update actual range when size changes for the first time. This // is necessary because the value margins may have changed after // the first layout pass. SizeChangedEventHandler handler = null; handler = delegate { SizeChanged -= handler; UpdateActualRange(); }; SizeChanged += handler; } /// /// Creates a minor axis tick mark. /// /// A line to used to render a tick mark. protected Line CreateMinorTickMark() { return CreateTickMark(MinorTickMarkStyle); } /// /// Invalidates axis when the actual range changes. /// /// The new actual range. protected virtual void OnActualRangeChanged(Range range) { Invalidate(); } /// /// Returns the plot area coordinate of a given value. /// /// The value to return the plot area coordinate for. /// The plot area coordinate of the given value. public override UnitValue GetPlotAreaCoordinate(object value) { if (value == null) { throw new ArgumentNullException("value"); } return GetPlotAreaCoordinate(value, ActualLength); } /// /// Returns the plot area coordinate of a given value. /// /// The value to return the plot area coordinate for. /// The length of the axis. /// The plot area coordinate of the given value. protected abstract UnitValue GetPlotAreaCoordinate(object value, double length); /// /// Returns the plot area coordinate of a given value. /// /// The value to return the plot area coordinate for. /// The value range to use when calculating the plot area coordinate. /// The length of the axis. /// The plot area coordinate of the given value. protected abstract UnitValue GetPlotAreaCoordinate(object value, Range currentRange, double length); /// /// Overrides the data range. /// /// The range to potentially override. /// The overridden range. protected virtual Range OverrideDataRange(Range range) { return range; } /// /// Modifies a range to respect the minimum and maximum axis values. /// /// The range of data. /// A range modified to respect the minimum and maximum axis /// values. private Range EnforceMaximumAndMinimum(Range range) { if (range.HasData) { IComparable minimum = ProtectedMinimum ?? range.Minimum; IComparable maximum = ProtectedMaximum ?? range.Maximum; if (ValueHelper.Compare(minimum, maximum) > 0) { IComparable temp = maximum; maximum = minimum; minimum = temp; } return new Range(minimum, maximum); } else { IComparable minimum = ProtectedMinimum; IComparable maximum = ProtectedMaximum; if (ProtectedMinimum != null && ProtectedMaximum == null) { maximum = minimum; } else if (ProtectedMaximum != null && ProtectedMinimum == null) { minimum = maximum; } else { return range; } return new Range(minimum, maximum); } } /// /// Updates the actual range displayed on the axis. /// private void UpdateActualRange() { Action action = () => { Range dataRange; if (ProtectedMaximum == null || ProtectedMinimum == null) { if (Orientation == AxisOrientation.None) { if (ProtectedMinimum != null) { this.ActualRange = OverrideDataRange(new Range(ProtectedMinimum, ProtectedMinimum)); } else { this.ActualRange = OverrideDataRange(new Range(ProtectedMaximum, ProtectedMaximum)); } } else { dataRange = this.RegisteredListeners .OfType() .Select(rangeProvider => rangeProvider.GetRange(this)) .Sum(); this.ActualRange = OverrideDataRange(dataRange); } } else { this.ActualRange = new Range(ProtectedMinimum, ProtectedMaximum); } }; // Repeat this after layout pass. if (this.ActualLength == 0.0) { this.Dispatcher.BeginInvoke(action); } action(); } /// /// Renders the axis as an oriented axis. /// /// The available size. private void RenderOriented(Size availableSize) { _minorTickMarkPool.Reset(); _majorTickMarkPool.Reset(); _labelPool.Reset(); double length = GetLength(availableSize); try { OrientedPanel.Children.Clear(); if (ActualRange.HasData && !Object.Equals(ActualRange.Minimum, ActualRange.Maximum)) { foreach (IComparable axisValue in GetMajorTickMarkValues(availableSize)) { UnitValue coordinate = GetPlotAreaCoordinate(axisValue, length); if (ValueHelper.CanGraph(coordinate.Value)) { Line line = _majorTickMarkPool.Next(); OrientedPanel.SetCenterCoordinate(line, coordinate.Value); OrientedPanel.SetPriority(line, 0); OrientedPanel.Children.Add(line); } } foreach (IComparable axisValue in GetMinorTickMarkValues(availableSize)) { UnitValue coordinate = GetPlotAreaCoordinate(axisValue, length); if (ValueHelper.CanGraph(coordinate.Value)) { Line line = _minorTickMarkPool.Next(); OrientedPanel.SetCenterCoordinate(line, coordinate.Value); OrientedPanel.SetPriority(line, 0); OrientedPanel.Children.Add(line); } } int count = 0; foreach (IComparable axisValue in GetLabelValues(availableSize)) { UnitValue coordinate = GetPlotAreaCoordinate(axisValue, length); if (ValueHelper.CanGraph(coordinate.Value)) { Control axisLabel = _labelPool.Next(); PrepareAxisLabel(axisLabel, axisValue); OrientedPanel.SetCenterCoordinate(axisLabel, coordinate.Value); OrientedPanel.SetPriority(axisLabel, count + 1); OrientedPanel.Children.Add(axisLabel); count = (count + 1) % 2; } } } } finally { _minorTickMarkPool.Done(); _majorTickMarkPool.Done(); _labelPool.Done(); } } /// /// Renders the axis labels, tick marks, and other visual elements. /// /// The available size. protected override void Render(Size availableSize) { RenderOriented(availableSize); } /// /// Returns a sequence of the major grid line coordinates. /// /// The available size. /// A sequence of the major grid line coordinates. protected override IEnumerable GetMajorGridLineCoordinates(Size availableSize) { return GetMajorTickMarkValues(availableSize).Select(value => GetPlotAreaCoordinate(value)).Where(value => ValueHelper.CanGraph(value.Value)); } /// /// Returns a sequence of the values at which to plot major grid lines. /// /// The available size. /// A sequence of the values at which to plot major grid lines. /// [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "GridLine", Justification = "This is the expected capitalization.")] [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method may do a lot of work and is therefore not a suitable candidate for a property.")] protected virtual IEnumerable GetMajorGridLineValues(Size availableSize) { return GetMajorTickMarkValues(availableSize); } /// /// Returns a sequence of values to plot on the axis. /// /// The available size. /// A sequence of values to plot on the axis. [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method may do a lot of work and is therefore not a suitable candidate for a property.")] protected abstract IEnumerable GetMajorTickMarkValues(Size availableSize); /// /// Returns a sequence of values to plot on the axis. /// /// The available size. /// A sequence of values to plot on the axis. [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method may do a lot of work and is therefore not a suitable candidate for a property.")] protected virtual IEnumerable GetMinorTickMarkValues(Size availableSize) { yield break; } /// /// Returns a sequence of values to plot on the axis. /// /// The available size. /// A sequence of values to plot on the axis. [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method may do a lot of work and is therefore not a suitable candidate for a property.")] protected abstract IEnumerable GetLabelValues(Size availableSize); /// /// Returns the value range given a plot area coordinate. /// /// The plot area coordinate. /// A range of values at that plot area coordinate. protected abstract IComparable GetValueAtPosition(UnitValue value); /// /// Gets the actual maximum value. /// Range IRangeAxis.Range { get { return ActualRange; } } /// /// Returns the value range given a plot area coordinate. /// /// The plot area coordinate. /// A range of values at that plot area coordinate. IComparable IRangeAxis.GetValueAtPosition(UnitValue value) { return GetValueAtPosition(value); } /// /// Updates the axis with information about a provider's data range. /// /// The information provider. /// The range of data in the information provider. /// void IRangeConsumer.RangeChanged(IRangeProvider usesRangeAxis, Range range) { UpdateActualRange(); } /// /// Updates the layout of the axis to accommodate a sequence of value /// margins. /// /// A value margin provider. /// A sequence of value margins. void IValueMarginConsumer.ValueMarginsChanged(IValueMarginProvider provider, IEnumerable valueMargins) { Action action = () => { if (this.Orientation != AxisOrientation.None) { // Determine if any of the value margins are outside the axis // area. If so update range. bool updateRange = valueMargins .Select( valueMargin => { double coordinate = GetPlotAreaCoordinate(valueMargin.Value).Value; return new Range(coordinate - valueMargin.LowMargin, coordinate + valueMargin.HighMargin); }) .Where(range => range.Minimum < 0 || range.Maximum > this.ActualLength) .Any(); if (updateRange) { UpdateActualRange(); } } }; // Repeat this after layout pass. if (this.ActualLength == 0) { this.Dispatcher.BeginInvoke(action); } else { action(); } } /// /// If a new range provider is registered, update actual range. /// /// The axis listener being registered. protected override void OnObjectRegistered(IAxisListener series) { base.OnObjectRegistered(series); if (series is IRangeProvider || series is IValueMarginProvider) { UpdateActualRange(); } } /// /// If a range provider is unregistered, update actual range. /// /// The axis listener being unregistered. protected override void OnObjectUnregistered(IAxisListener series) { base.OnObjectUnregistered(series); if (series is IRangeProvider || series is IValueMarginProvider) { UpdateActualRange(); } } /// /// Create function that when given a range will return the /// amount in pixels by which the value margin range /// overlaps. Positive numbers represent values outside the /// range. /// /// The list of value margins, coordinates, and overlaps. /// The new range to use to calculate coordinates. internal void UpdateValueMargins(IList valueMargins, Range comparableRange) { double actualLength = this.ActualLength; int valueMarginsCount = valueMargins.Count; for (int count = 0; count < valueMarginsCount; count++) { ValueMarginCoordinateAndOverlap item = valueMargins[count]; item.Coordinate = GetPlotAreaCoordinate(item.ValueMargin.Value, comparableRange, actualLength).Value; item.LeftOverlap = -(item.Coordinate - item.ValueMargin.LowMargin); item.RightOverlap = (item.Coordinate + item.ValueMargin.HighMargin) - actualLength; } } /// /// Returns the value margin, coordinate, and overlap triples that have the largest left and right overlap. /// /// The list of value margin, coordinate, and /// overlap triples. /// The value margin, /// coordinate, and overlap triple that has the largest left overlap. /// /// The value margin, /// coordinate, and overlap triple that has the largest right overlap. /// internal static void GetMaxLeftAndRightOverlap(IList valueMargins, out ValueMarginCoordinateAndOverlap maxLeftOverlapValueMargin, out ValueMarginCoordinateAndOverlap maxRightOverlapValueMargin) { maxLeftOverlapValueMargin = new ValueMarginCoordinateAndOverlap(); maxRightOverlapValueMargin = new ValueMarginCoordinateAndOverlap(); double maxLeftOverlap = double.MinValue; double maxRightOverlap = double.MinValue; int valueMarginsCount = valueMargins.Count; for (int cnt = 0; cnt < valueMarginsCount; cnt++) { ValueMarginCoordinateAndOverlap valueMargin = valueMargins[cnt]; double leftOverlap = valueMargin.LeftOverlap; if (leftOverlap > maxLeftOverlap) { maxLeftOverlap = leftOverlap; maxLeftOverlapValueMargin = valueMargin; } double rightOverlap = valueMargin.RightOverlap; if (rightOverlap > maxRightOverlap) { maxRightOverlap = rightOverlap; maxRightOverlapValueMargin = valueMargin; } } } /// /// Gets the origin value on the axis. /// IComparable IRangeAxis.Origin { get { return this.Origin; } } /// /// Gets the origin value on the axis. /// protected abstract IComparable Origin { get; } } }