All the controls missing in WPF. Over 1 million downloads.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

622 lines
26 KiB

// (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
{
/// <summary>
/// An axis that has a range.
/// </summary>
public abstract class RangeAxis : DisplayAxis, IRangeAxis, IValueMarginConsumer
{
/// <summary>
/// A pool of major tick marks.
/// </summary>
private ObjectPool<Line> _majorTickMarkPool;
/// <summary>
/// A pool of major tick marks.
/// </summary>
private ObjectPool<Line> _minorTickMarkPool;
/// <summary>
/// A pool of labels.
/// </summary>
private ObjectPool<Control> _labelPool;
#region public Style MinorTickMarkStyle
/// <summary>
/// Gets or sets the minor tick mark style.
/// </summary>
public Style MinorTickMarkStyle
{
get { return GetValue(MinorTickMarkStyleProperty) as Style; }
set { SetValue(MinorTickMarkStyleProperty, value); }
}
/// <summary>
/// Identifies the MinorTickMarkStyle dependency property.
/// </summary>
public static readonly DependencyProperty MinorTickMarkStyleProperty =
DependencyProperty.Register(
"MinorTickMarkStyle",
typeof(Style),
typeof(RangeAxis),
new PropertyMetadata(null));
#endregion public Style MinorTickMarkStyle
/// <summary>
/// The actual range of values.
/// </summary>
private Range<IComparable> _actualRange;
/// <summary>
/// Gets or sets the actual range of values.
/// </summary>
protected Range<IComparable> ActualRange
{
get
{
return _actualRange;
}
set
{
Range<IComparable> oldValue = _actualRange;
Range<IComparable> minMaxEnforcedValue = EnforceMaximumAndMinimum(value);
if (!oldValue.Equals(minMaxEnforcedValue))
{
_actualRange = minMaxEnforcedValue;
OnActualRangeChanged(minMaxEnforcedValue);
}
}
}
/// <summary>
/// The maximum value displayed in the range axis.
/// </summary>
private IComparable _protectedMaximum;
/// <summary>
/// Gets or sets the maximum value displayed in the range axis.
/// </summary>
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();
}
}
}
/// <summary>
/// The minimum value displayed in the range axis.
/// </summary>
private IComparable _protectedMinimum;
/// <summary>
/// Gets or sets the minimum value displayed in the range axis.
/// </summary>
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
/// <summary>
/// Initializes the static members of the RangeAxis class.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Dependency properties are initialized in-line.")]
static RangeAxis()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(RangeAxis), new FrameworkPropertyMetadata(typeof(RangeAxis)));
}
#endif
/// <summary>
/// Instantiates a new instance of the RangeAxis class.
/// </summary>
protected RangeAxis()
{
#if SILVERLIGHT
this.DefaultStyleKey = typeof(RangeAxis);
#endif
this._labelPool = new ObjectPool<Control>(() => CreateAxisLabel());
this._majorTickMarkPool = new ObjectPool<Line>(() => CreateMajorTickMark());
this._minorTickMarkPool = new ObjectPool<Line>(() => 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;
}
/// <summary>
/// Creates a minor axis tick mark.
/// </summary>
/// <returns>A line to used to render a tick mark.</returns>
protected Line CreateMinorTickMark()
{
return CreateTickMark(MinorTickMarkStyle);
}
/// <summary>
/// Invalidates axis when the actual range changes.
/// </summary>
/// <param name="range">The new actual range.</param>
protected virtual void OnActualRangeChanged(Range<IComparable> range)
{
Invalidate();
}
/// <summary>
/// Returns the plot area coordinate of a given value.
/// </summary>
/// <param name="value">The value to return the plot area coordinate for.</param>
/// <returns>The plot area coordinate of the given value.</returns>
public override UnitValue GetPlotAreaCoordinate(object value)
{
if (value == null)
{
throw new ArgumentNullException("value");
}
return GetPlotAreaCoordinate(value, ActualLength);
}
/// <summary>
/// Returns the plot area coordinate of a given value.
/// </summary>
/// <param name="value">The value to return the plot area coordinate for.</param>
/// <param name="length">The length of the axis.</param>
/// <returns>The plot area coordinate of the given value.</returns>
protected abstract UnitValue GetPlotAreaCoordinate(object value, double length);
/// <summary>
/// Returns the plot area coordinate of a given value.
/// </summary>
/// <param name="value">The value to return the plot area coordinate for.</param>
/// <param name="currentRange">The value range to use when calculating the plot area coordinate.</param>
/// <param name="length">The length of the axis.</param>
/// <returns>The plot area coordinate of the given value.</returns>
protected abstract UnitValue GetPlotAreaCoordinate(object value, Range<IComparable> currentRange, double length);
/// <summary>
/// Overrides the data range.
/// </summary>
/// <param name="range">The range to potentially override.</param>
/// <returns>The overridden range.</returns>
protected virtual Range<IComparable> OverrideDataRange(Range<IComparable> range)
{
return range;
}
/// <summary>
/// Modifies a range to respect the minimum and maximum axis values.
/// </summary>
/// <param name="range">The range of data.</param>
/// <returns>A range modified to respect the minimum and maximum axis
/// values.</returns>
private Range<IComparable> EnforceMaximumAndMinimum(Range<IComparable> 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<IComparable>(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<IComparable>(minimum, maximum);
}
}
/// <summary>
/// Updates the actual range displayed on the axis.
/// </summary>
private void UpdateActualRange()
{
Action action = () =>
{
Range<IComparable> dataRange;
if (ProtectedMaximum == null || ProtectedMinimum == null)
{
if (Orientation == AxisOrientation.None)
{
if (ProtectedMinimum != null)
{
this.ActualRange = OverrideDataRange(new Range<IComparable>(ProtectedMinimum, ProtectedMinimum));
}
else
{
this.ActualRange = OverrideDataRange(new Range<IComparable>(ProtectedMaximum, ProtectedMaximum));
}
}
else
{
dataRange =
this.RegisteredListeners
.OfType<IRangeProvider>()
.Select(rangeProvider => rangeProvider.GetRange(this))
.Sum();
this.ActualRange = OverrideDataRange(dataRange);
}
}
else
{
this.ActualRange = new Range<IComparable>(ProtectedMinimum, ProtectedMaximum);
}
};
// Repeat this after layout pass.
if (this.ActualLength == 0.0)
{
this.Dispatcher.BeginInvoke(action);
}
action();
}
/// <summary>
/// Renders the axis as an oriented axis.
/// </summary>
/// <param name="availableSize">The available size.</param>
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();
}
}
/// <summary>
/// Renders the axis labels, tick marks, and other visual elements.
/// </summary>
/// <param name="availableSize">The available size.</param>
protected override void Render(Size availableSize)
{
RenderOriented(availableSize);
}
/// <summary>
/// Returns a sequence of the major grid line coordinates.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>A sequence of the major grid line coordinates.</returns>
protected override IEnumerable<UnitValue> GetMajorGridLineCoordinates(Size availableSize)
{
return GetMajorTickMarkValues(availableSize).Select(value => GetPlotAreaCoordinate(value)).Where(value => ValueHelper.CanGraph(value.Value));
}
/// <summary>
/// Returns a sequence of the values at which to plot major grid lines.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>A sequence of the values at which to plot major grid lines.
/// </returns>
[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<IComparable> GetMajorGridLineValues(Size availableSize)
{
return GetMajorTickMarkValues(availableSize);
}
/// <summary>
/// Returns a sequence of values to plot on the axis.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>A sequence of values to plot on the axis.</returns>
[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<IComparable> GetMajorTickMarkValues(Size availableSize);
/// <summary>
/// Returns a sequence of values to plot on the axis.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>A sequence of values to plot on the axis.</returns>
[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<IComparable> GetMinorTickMarkValues(Size availableSize)
{
yield break;
}
/// <summary>
/// Returns a sequence of values to plot on the axis.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>A sequence of values to plot on the axis.</returns>
[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<IComparable> GetLabelValues(Size availableSize);
/// <summary>
/// Returns the value range given a plot area coordinate.
/// </summary>
/// <param name="value">The plot area coordinate.</param>
/// <returns>A range of values at that plot area coordinate.</returns>
protected abstract IComparable GetValueAtPosition(UnitValue value);
/// <summary>
/// Gets the actual maximum value.
/// </summary>
Range<IComparable> IRangeAxis.Range
{
get { return ActualRange; }
}
/// <summary>
/// Returns the value range given a plot area coordinate.
/// </summary>
/// <param name="value">The plot area coordinate.</param>
/// <returns>A range of values at that plot area coordinate.</returns>
IComparable IRangeAxis.GetValueAtPosition(UnitValue value)
{
return GetValueAtPosition(value);
}
/// <summary>
/// Updates the axis with information about a provider's data range.
/// </summary>
/// <param name="usesRangeAxis">The information provider.</param>
/// <param name="range">The range of data in the information provider.
/// </param>
void IRangeConsumer.RangeChanged(IRangeProvider usesRangeAxis, Range<IComparable> range)
{
UpdateActualRange();
}
/// <summary>
/// Updates the layout of the axis to accommodate a sequence of value
/// margins.
/// </summary>
/// <param name="provider">A value margin provider.</param>
/// <param name="valueMargins">A sequence of value margins.</param>
void IValueMarginConsumer.ValueMarginsChanged(IValueMarginProvider provider, IEnumerable<ValueMargin> 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<double>(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();
}
}
/// <summary>
/// If a new range provider is registered, update actual range.
/// </summary>
/// <param name="series">The axis listener being registered.</param>
protected override void OnObjectRegistered(IAxisListener series)
{
base.OnObjectRegistered(series);
if (series is IRangeProvider || series is IValueMarginProvider)
{
UpdateActualRange();
}
}
/// <summary>
/// If a range provider is unregistered, update actual range.
/// </summary>
/// <param name="series">The axis listener being unregistered.</param>
protected override void OnObjectUnregistered(IAxisListener series)
{
base.OnObjectUnregistered(series);
if (series is IRangeProvider || series is IValueMarginProvider)
{
UpdateActualRange();
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="valueMargins">The list of value margins, coordinates, and overlaps.</param>
/// <param name="comparableRange">The new range to use to calculate coordinates.</param>
internal void UpdateValueMargins(IList<ValueMarginCoordinateAndOverlap> valueMargins, Range<IComparable> 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;
}
}
/// <summary>
/// Returns the value margin, coordinate, and overlap triples that have the largest left and right overlap.
/// </summary>
/// <param name="valueMargins">The list of value margin, coordinate, and
/// overlap triples.</param>
/// <param name="maxLeftOverlapValueMargin">The value margin,
/// coordinate, and overlap triple that has the largest left overlap.
/// </param>
/// <param name="maxRightOverlapValueMargin">The value margin,
/// coordinate, and overlap triple that has the largest right overlap.
/// </param>
internal static void GetMaxLeftAndRightOverlap(IList<ValueMarginCoordinateAndOverlap> 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;
}
}
}
/// <summary>
/// Gets the origin value on the axis.
/// </summary>
IComparable IRangeAxis.Origin { get { return this.Origin; } }
/// <summary>
/// Gets the origin value on the axis.
/// </summary>
protected abstract IComparable Origin { get; }
}
}