// (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.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Shapes;
namespace System.Windows.Controls.DataVisualization.Charting
{
///
/// Control base class for displaying values as a stacked area/line chart visualization.
///
/// Preview
public abstract class StackedAreaLineSeries : DefinitionSeries
{
///
/// Gets the Shapes corresponding to each SeriesDefinition.
///
protected Dictionary SeriesDefinitionShapes { get; private set; }
///
/// Initializes a new instance of the StackedAreaLineSeries class.
///
protected StackedAreaLineSeries()
{
SeriesDefinitionShapes = new Dictionary();
}
///
/// Builds the visual tree for the control when a new template is applied.
///
public override void OnApplyTemplate()
{
SynchronizeSeriesDefinitionShapes(SeriesDefinitions, null);
base.OnApplyTemplate();
SynchronizeSeriesDefinitionShapes(null, SeriesDefinitions);
}
///
/// Called when the SeriesDefinitions collection changes.
///
/// Type of change.
/// Sequence of old items.
/// Starting index of old items.
/// Sequence of new items.
/// Starting index of new items.
protected override void SeriesDefinitionsCollectionChanged(NotifyCollectionChangedAction action, IList oldItems, int oldStartingIndex, IList newItems, int newStartingIndex)
{
base.SeriesDefinitionsCollectionChanged(action, oldItems, oldStartingIndex, newItems, newStartingIndex);
if (null != oldItems)
{
SynchronizeSeriesDefinitionShapes(oldItems.CastWrapper(), null);
foreach (SeriesDefinition oldDefinition in oldItems.CastWrapper())
{
SeriesDefinitionShapes.Remove(oldDefinition);
}
}
if (null != newItems)
{
foreach (SeriesDefinition newDefinition in newItems.CastWrapper())
{
Shape dataShape = CreateDataShape();
dataShape.SetBinding(Shape.StyleProperty, new Binding("ActualDataShapeStyle") { Source = newDefinition });
SeriesDefinitionShapes[newDefinition] = dataShape;
}
SynchronizeSeriesDefinitionShapes(null, newItems.CastWrapper());
}
}
///
/// Acquires a dependent axis suitable for use with the data values of the series.
///
/// Axis instance.
protected override IAxis AcquireDependentAxis()
{
IAxis dependentAxis = SeriesHost.Axes
.Where(a => (a.Orientation == AxisOrientation.Y) && (a is IRangeAxis) && DataItems.Any() && (a.CanPlot(DataItems.First().ActualDependentValue)))
.FirstOrDefault();
if (null == dependentAxis)
{
LinearAxis linearAxis = new LinearAxis { Orientation = AxisOrientation.Y, ShowGridLines = true };
if (IsStacked100)
{
Style style = new Style(typeof(AxisLabel));
style.Setters.Add(new Setter(AxisLabel.StringFormatProperty, "{0}%"));
linearAxis.AxisLabelStyle = style;
}
dependentAxis = linearAxis;
}
return dependentAxis;
}
///
/// Acquires an independent axis suitable for use with the data values of the series.
///
/// Axis instance.
protected override IAxis AcquireIndependentAxis()
{
IAxis independentAxis = SeriesHost.Axes
.Where(a => (a.Orientation == AxisOrientation.X) && ((a is IRangeAxis) || (a is ICategoryAxis)) && DataItems.Any() && (a.CanPlot(DataItems.First().ActualIndependentValue)))
.FirstOrDefault();
if (null == independentAxis)
{
object probeValue = DataItems.Any() ? DataItems.First().ActualIndependentValue : null;
double convertedDouble;
DateTime convertedDateTime;
if ((null != probeValue) && ValueHelper.TryConvert(probeValue, out convertedDouble))
{
independentAxis = new LinearAxis();
}
else if ((null != probeValue) && ValueHelper.TryConvert(probeValue, out convertedDateTime))
{
independentAxis = new DateTimeAxis();
}
else
{
independentAxis = new CategoryAxis();
}
independentAxis.Orientation = AxisOrientation.X;
}
return independentAxis;
}
///
/// Prepares a DataPoint for use.
///
/// DataPoint instance.
protected override void PrepareDataPoint(DataPoint dataPoint)
{
base.PrepareDataPoint(dataPoint);
dataPoint.SizeChanged += new SizeChangedEventHandler(DataPointSizeChanged);
}
///
/// Handles the SizeChanged event of a DataPoint to update the value margins for the series.
///
/// Event source.
/// Event arguments.
private void DataPointSizeChanged(object sender, SizeChangedEventArgs e)
{
DataPoint dataPoint = (DataPoint)sender;
DataItem dataItem = DataItemFromDataPoint(dataPoint);
// Update placement
double newWidth = e.NewSize.Width;
double newHeight = e.NewSize.Height;
Canvas.SetLeft(dataItem.Container, Math.Round(dataItem.CenterPoint.X - (newWidth / 2)));
Canvas.SetTop(dataItem.Container, Math.Round(dataItem.CenterPoint.Y - (newHeight / 2)));
// Update value margins
double heightMargin = newHeight * (3.0 / 4.0);
NotifyValueMarginsChanged(ActualDependentAxis, new ValueMargin[] { new ValueMargin(dataItem.ActualStackedDependentValue, heightMargin, heightMargin) });
double widthMargin = newWidth * (3.0 / 4.0);
NotifyValueMarginsChanged(ActualIndependentAxis, new ValueMargin[] { new ValueMargin(dataPoint.ActualIndependentValue, widthMargin, widthMargin) });
}
///
/// Creates a series-appropriate Shape for connecting the points of the series.
///
/// Shape instance.
protected abstract Shape CreateDataShape();
///
/// Synchronizes the SeriesDefinitionShapes dictionary with the contents of the SeriesArea Panel.
///
/// SeriesDefinition being removed.
/// SeriesDefinition being added.
private void SynchronizeSeriesDefinitionShapes(IEnumerable oldDefinitions, IEnumerable newDefinitions)
{
if (null != SeriesArea)
{
if (null != oldDefinitions)
{
foreach (SeriesDefinition oldDefinition in oldDefinitions)
{
SeriesArea.Children.Remove(SeriesDefinitionShapes[oldDefinition]);
}
}
if (null != newDefinitions)
{
foreach (SeriesDefinition newDefinition in newDefinitions.OrderBy(sd => sd.Index))
{
SeriesArea.Children.Insert(newDefinition.Index, SeriesDefinitionShapes[newDefinition]);
}
}
}
}
///
/// Returns the range for the data points of the series.
///
/// Consumer of the range.
/// Range of values.
protected override Range IRangeProviderGetRange(IRangeConsumer rangeConsumer)
{
if (rangeConsumer == ActualDependentAxis)
{
IEnumerable> dependentValueRangesByIndependentValue = IndependentValueDependentValues
.Select(g => g.Where(d => ValueHelper.CanGraph(d)))
.Select(g => g.Scan(0.0, (s, t) => s + t).Skip(1).GetRange())
.DefaultIfEmpty(new Range(0, 0))
.ToArray();
double minimum = dependentValueRangesByIndependentValue.Min(r => r.Minimum);
double maximum = dependentValueRangesByIndependentValue.Max(r => r.Maximum);
if (IsStacked100)
{
minimum = Math.Min(minimum, 0);
maximum = Math.Max(maximum, 0);
}
return new Range(minimum, maximum);
}
else
{
return base.IRangeProviderGetRange(rangeConsumer);
}
}
///
/// Returns the value margins for the data points of the series.
///
/// Consumer of the value margins.
/// Sequence of value margins.
protected override IEnumerable IValueMarginProviderGetValueMargins(IValueMarginConsumer valueMarginConsumer)
{
if (IsStacked100 && (valueMarginConsumer == ActualDependentAxis))
{
return Enumerable.Empty();
}
else if ((valueMarginConsumer == ActualDependentAxis) || (valueMarginConsumer == ActualIndependentAxis))
{
Range range = IRangeProviderGetRange((IRangeConsumer)valueMarginConsumer);
double margin = DataItems
.Select(di =>
{
return (null != di.DataPoint) ?
(valueMarginConsumer == ActualDependentAxis) ? di.DataPoint.ActualHeight : di.DataPoint.ActualWidth :
0;
})
.Average() * (3.0 / 4.0);
return new ValueMargin[]
{
new ValueMargin(range.Minimum, margin, margin),
new ValueMargin(range.Maximum, margin, margin),
};
}
else
{
return base.IValueMarginProviderGetValueMargins(valueMarginConsumer);
}
}
///
/// Updates the placement of the DataItems (data points) of the series.
///
/// DataItems in need of an update.
protected override void UpdateDataItemPlacement(IEnumerable dataItems)
{
if ((null != ActualDependentAxis) && (null != ActualIndependentAxis))
{
double plotAreaMaximumDependentCoordinate = ActualDependentAxis.GetPlotAreaCoordinate(ActualDependentRangeAxis.Range.Maximum).Value;
double lineTopBuffer = 1;
List[] points = new List[SeriesDefinitions.Count];
for (int i = 0; i < points.Length; i++)
{
points[i] = new List();
}
foreach (IndependentValueGroup group in IndependentValueGroupsOrderedByIndependentValue)
{
double sum = IsStacked100 ?
group.DataItems.Sum(di => Math.Abs(ValueHelper.ToDouble(di.DataPoint.ActualDependentValue))) :
1;
if (0 == sum)
{
sum = 1;
}
double x = ActualIndependentAxis.GetPlotAreaCoordinate(group.IndependentValue).Value;
if (ValueHelper.CanGraph(x))
{
double lastValue = 0;
Point lastPoint = new Point(x, Math.Max(plotAreaMaximumDependentCoordinate - ActualDependentRangeAxis.GetPlotAreaCoordinate(lastValue).Value, lineTopBuffer));
int i = -1;
SeriesDefinition lastDefinition = null;
foreach (DataItem dataItem in group.DataItems)
{
if (lastDefinition != dataItem.SeriesDefinition)
{
i++;
}
while (dataItem.SeriesDefinition != SeriesDefinitions[i])
{
points[i].Add(lastPoint);
i++;
}
DataPoint dataPoint = dataItem.DataPoint;
double value = IsStacked100 ?
(ValueHelper.ToDouble(dataItem.DataPoint.ActualDependentValue) * (100 / sum)) :
ValueHelper.ToDouble(dataItem.DataPoint.ActualDependentValue);
if (ValueHelper.CanGraph(value))
{
value += lastValue;
dataItem.ActualStackedDependentValue = value;
double y = ActualDependentRangeAxis.GetPlotAreaCoordinate(value).Value;
lastValue = value;
lastPoint.Y = Math.Max(plotAreaMaximumDependentCoordinate - y, lineTopBuffer);
points[i].Add(lastPoint);
dataItem.CenterPoint = new Point(x, plotAreaMaximumDependentCoordinate - y);
double left = dataItem.CenterPoint.X - (dataPoint.ActualWidth / 2);
double top = dataItem.CenterPoint.Y - (dataPoint.ActualHeight / 2);
Canvas.SetLeft(dataItem.Container, Math.Round(left));
Canvas.SetTop(dataItem.Container, Math.Round(top));
dataPoint.Visibility = Visibility.Visible;
}
else
{
points[i].Add(lastPoint);
dataPoint.Visibility = Visibility.Collapsed;
}
lastDefinition = dataItem.SeriesDefinition;
}
}
else
{
foreach (DataPoint dataPoint in group.DataItems.Select(di => di.DataPoint))
{
dataPoint.Visibility = Visibility.Collapsed;
}
}
}
UpdateShape(points);
}
}
///
/// Updates the Shape for the series.
///
/// Locations of the points of each SeriesDefinition in the series.
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Nesting is convenient way to represent data.")]
protected abstract void UpdateShape(IList> definitionPoints);
}
}