// (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; }
}
}