// (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.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Shapes; namespace System.Windows.Controls.DataVisualization.Charting { /// /// An axis that displays numeric values. /// [StyleTypedProperty(Property = "GridLineStyle", StyleTargetType = typeof(Line))] [StyleTypedProperty(Property = "MajorTickMarkStyle", StyleTargetType = typeof(Line))] [StyleTypedProperty(Property = "MinorTickMarkStyle", StyleTargetType = typeof(Line))] [StyleTypedProperty(Property = "AxisLabelStyle", StyleTargetType = typeof(NumericAxisLabel))] [StyleTypedProperty(Property = "TitleStyle", StyleTargetType = typeof(Title))] [TemplatePart(Name = AxisGridName, Type = typeof(Grid))] [TemplatePart(Name = AxisTitleName, Type = typeof(Title))] public class LinearAxis : NumericAxis { #region public double? Interval /// /// Gets or sets the axis interval. /// [TypeConverter(typeof(NullableConverter))] public double? Interval { get { return (double?)GetValue(IntervalProperty); } set { SetValue(IntervalProperty, value); } } /// /// Identifies the Interval dependency property. /// public static readonly DependencyProperty IntervalProperty = DependencyProperty.Register( "Interval", typeof(double?), typeof(LinearAxis), new PropertyMetadata(null, OnIntervalPropertyChanged)); /// /// IntervalProperty property changed handler. /// /// LinearAxis that changed its Interval. /// Event arguments. private static void OnIntervalPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { LinearAxis source = (LinearAxis)d; source.OnIntervalPropertyChanged(); } /// /// IntervalProperty property changed handler. /// private void OnIntervalPropertyChanged() { OnInvalidated(new RoutedEventArgs()); } #endregion public double? Interval #region public double ActualInterval /// /// Gets the actual interval of the axis. /// public double ActualInterval { get { return (double)GetValue(ActualIntervalProperty); } private set { SetValue(ActualIntervalProperty, value); } } /// /// Identifies the ActualInterval dependency property. /// public static readonly DependencyProperty ActualIntervalProperty = DependencyProperty.Register( "ActualInterval", typeof(double), typeof(LinearAxis), new PropertyMetadata(double.NaN)); #endregion public double ActualInterval /// /// Instantiates a new instance of the LinearAxis class. /// public LinearAxis() { this.ActualRange = new Range(0.0, 1.0); } /// /// Gets the actual range of double values. /// protected Range ActualDoubleRange { get; private set; } /// /// Updates ActualDoubleRange when ActualRange changes. /// /// New ActualRange value. protected override void OnActualRangeChanged(Range range) { ActualDoubleRange = range.ToDoubleRange(); base.OnActualRangeChanged(range); } /// /// Returns the plot area coordinate of a value. /// /// The value to plot. /// The length of axis. /// The plot area coordinate of a value. protected override UnitValue GetPlotAreaCoordinate(object value, double length) { return GetPlotAreaCoordinate(value, ActualDoubleRange, length); } /// /// Returns the plot area coordinate of a value. /// /// The value to plot. /// The range of values. /// The length of axis. /// The plot area coordinate of a value. protected override UnitValue GetPlotAreaCoordinate(object value, Range currentRange, double length) { return GetPlotAreaCoordinate(value, currentRange.ToDoubleRange(), length); } /// /// Returns the plot area coordinate of a value. /// /// The value to plot. /// The range of values. /// The length of axis. /// The plot area coordinate of a value. private static UnitValue GetPlotAreaCoordinate(object value, Range currentRange, double length) { if (currentRange.HasData) { double doubleValue = ValueHelper.ToDouble(value); double pixelLength = Math.Max(length - 1, 0); double rangelength = currentRange.Maximum - currentRange.Minimum; return new UnitValue((doubleValue - currentRange.Minimum) * (pixelLength / rangelength), Unit.Pixels); } return UnitValue.NaN(); } /// /// Returns the actual interval to use to determine which values are /// displayed in the axis. /// /// The available size. /// Actual interval to use to determine which values are /// displayed in the axis. /// protected virtual double CalculateActualInterval(Size availableSize) { if (Interval != null) { return Interval.Value; } // Adjust maximum interval count adjusted for current axis double adjustedMaximumIntervalsPer200Pixels = (Orientation == AxisOrientation.X ? 0.8 : 1.0) * MaximumAxisIntervalsPer200Pixels; // Calculate maximum interval count for current space double maximumIntervalCount = Math.Max(GetLength(availableSize) * adjustedMaximumIntervalsPer200Pixels / 200.0, 1.0); // Calculate range double range = ActualDoubleRange.Maximum - ActualDoubleRange.Minimum; // Calculate largest acceptable interval double bestInterval = range / maximumIntervalCount; // Calculate mimimum ideal interval (ideal => something that gives nice axis values) double minimumIdealInterval = Math.Pow(10, Math.Floor(Math.Log10(bestInterval))); // Walk the list of ideal multipliers foreach (int idealMultiplier in new int[] { 10, 5, 2, 1 }) { // Check the current ideal multiplier against the maximum count double currentIdealInterval = minimumIdealInterval * idealMultiplier; if (maximumIntervalCount < (range / currentIdealInterval)) { // Went too far, break out break; } // Update the best interval bestInterval = currentIdealInterval; } // Return best interval return bestInterval; } /// /// Returns a sequence of values to create major tick marks for. /// /// The available size. /// A sequence of values to create major tick marks for. /// protected override IEnumerable GetMajorTickMarkValues(Size availableSize) { return GetMajorValues(availableSize).CastWrapper(); } /// /// Returns a sequence of major axis values. /// /// The available size. /// A sequence of major axis values. /// private IEnumerable GetMajorValues(Size availableSize) { if (!ActualRange.HasData || ValueHelper.Compare(ActualRange.Minimum, ActualRange.Maximum) == 0 || GetLength(availableSize) == 0.0) { yield break; } this.ActualInterval = CalculateActualInterval(availableSize); double startValue = AlignToInterval(ActualDoubleRange.Minimum, this.ActualInterval); if (startValue < ActualDoubleRange.Minimum) { startValue = AlignToInterval(ActualDoubleRange.Minimum + this.ActualInterval, this.ActualInterval); } double nextValue = startValue; for (int counter = 1; nextValue <= ActualDoubleRange.Maximum; counter++) { yield return nextValue; nextValue = startValue + (counter * this.ActualInterval); } } /// /// Returns a sequence of values to plot on the axis. /// /// The available size. /// A sequence of values to plot on the axis. protected override IEnumerable GetLabelValues(Size availableSize) { return GetMajorValues(availableSize).CastWrapper(); } /// /// Aligns a value to the provided interval value. The aligned value /// should always be smaller than or equal to than the provided value. /// /// The value to align to the interval. /// The interval to align to. /// The aligned value. private static double AlignToInterval(double value, double interval) { double typedInterval = (double)interval; double typedValue = (double)value; return ValueHelper.RemoveNoiseFromDoubleMath(ValueHelper.RemoveNoiseFromDoubleMath(Math.Floor(typedValue / typedInterval)) * typedInterval); } /// /// Returns the value range given a plot area coordinate. /// /// The plot area position. /// The value at that plot area coordinate. protected override IComparable GetValueAtPosition(UnitValue value) { if (ActualRange.HasData && ActualLength != 0.0) { if (value.Unit == Unit.Pixels) { double coordinate = value.Value; double rangelength = ActualDoubleRange.Maximum - ActualDoubleRange.Minimum; double output = ((coordinate * (rangelength / ActualLength)) + ActualDoubleRange.Minimum); return output; } else { throw new NotImplementedException(); } } return null; } /// /// Function that uses the mid point of all the data values /// in the value margins to convert a length into a range /// of data with the mid point as the center of that range. /// /// The mid point of the range. /// The length of the range. /// The range object. private static Range LengthToRange(double midPoint, double length) { double halfLength = length / 2.0; return new Range(midPoint - halfLength, midPoint + halfLength); } /// /// Overrides the actual range to ensure that it is never set to an /// empty range. /// /// The range to override. /// Returns the overridden range. protected override Range OverrideDataRange(Range range) { range = base.OverrideDataRange(range); if (!range.HasData) { return new Range(0.0, 1.0); } else if (ValueHelper.Compare(range.Minimum, range.Maximum) == 0) { Range outputRange = new Range((ValueHelper.ToDouble(range.Minimum)) - 1, (ValueHelper.ToDouble(range.Maximum)) + 1); return outputRange; } // ActualLength of 1.0 or less maps all points to the same coordinate if (range.HasData && this.ActualLength > 1.0) { bool isDataAnchoredToOrigin = false; IList valueMargins = new List(); foreach (IValueMarginProvider valueMarginProvider in this.RegisteredListeners.OfType()) { foreach (ValueMargin valueMargin in valueMarginProvider.GetValueMargins(this)) { IAnchoredToOrigin dataAnchoredToOrigin = valueMarginProvider as IAnchoredToOrigin; isDataAnchoredToOrigin = (dataAnchoredToOrigin != null && dataAnchoredToOrigin.AnchoredAxis == this); valueMargins.Add( new ValueMarginCoordinateAndOverlap { ValueMargin = valueMargin, }); } } if (valueMargins.Count > 0) { double maximumPixelMarginLength = valueMargins .Select(valueMargin => valueMargin.ValueMargin.LowMargin + valueMargin.ValueMargin.HighMargin) .MaxOrNullable().Value; // Requested margin is larger than the axis so give up // trying to find a range that will fit it. if (maximumPixelMarginLength > this.ActualLength) { return range; } Range originalRange = range.ToDoubleRange(); Range currentRange = range.ToDoubleRange(); // Ensure range is not empty. if (currentRange.Minimum == currentRange.Maximum) { currentRange = new Range(currentRange.Maximum - 1, currentRange.Maximum + 1); } // priming the loop double actualLength = this.ActualLength; ValueMarginCoordinateAndOverlap maxLeftOverlapValueMargin; ValueMarginCoordinateAndOverlap maxRightOverlapValueMargin; UpdateValueMargins(valueMargins, currentRange.ToComparableRange()); GetMaxLeftAndRightOverlap(valueMargins, out maxLeftOverlapValueMargin, out maxRightOverlapValueMargin); while (maxLeftOverlapValueMargin.LeftOverlap > 0 || maxRightOverlapValueMargin.RightOverlap > 0) { double unitOverPixels = currentRange.GetLength().Value / actualLength; double newMinimum = currentRange.Minimum - ((maxLeftOverlapValueMargin.LeftOverlap + 0.5) * unitOverPixels); double newMaximum = currentRange.Maximum + ((maxRightOverlapValueMargin.RightOverlap + 0.5) * unitOverPixels); currentRange = new Range(newMinimum, newMaximum); UpdateValueMargins(valueMargins, currentRange.ToComparableRange()); GetMaxLeftAndRightOverlap(valueMargins, out maxLeftOverlapValueMargin, out maxRightOverlapValueMargin); } if (isDataAnchoredToOrigin) { if (originalRange.Minimum >= 0 && currentRange.Minimum < 0) { currentRange = new Range(0, currentRange.Maximum); } else if (originalRange.Maximum <= 0 && currentRange.Maximum > 0) { currentRange = new Range(currentRange.Minimum, 0); } } return currentRange.ToComparableRange(); } } return range; } } }