using System;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.Research.DynamicDataDisplay.Charts.Axes;
using System.Windows.Data;
using Microsoft.Research.DynamicDataDisplay.Common;
using System.Windows.Threading;
namespace Microsoft.Research.DynamicDataDisplay.Charts
{
///
/// Defines a base class for axis UI representation.
/// Contains a number of properties that can be used to adjust ticks set and their look.
///
///
[TemplatePart(Name = "PART_AdditionalLabelsCanvas", Type = typeof(StackCanvas))]
[TemplatePart(Name = "PART_CommonLabelsCanvas", Type = typeof(StackCanvas))]
[TemplatePart(Name = "PART_TicksPath", Type = typeof(Path))]
[TemplatePart(Name = "PART_ContentsGrid", Type = typeof(Panel))]
public abstract class AxisControl : AxisControlBase
{
private const string templateKey = "axisControlTemplate";
private const string smoothTemplateKey = "smoothAxisControlTemplate";
private const string additionalLabelTransformKey = "additionalLabelsTransform";
private const string PART_AdditionalLabelsCanvas = "PART_AdditionalLabelsCanvas";
private const string PART_CommonLabelsCanvas = "PART_CommonLabelsCanvas";
private const string PART_TicksPath = "PART_TicksPath";
private const string PART_ContentsGrid = "PART_ContentsGrid";
///
/// Initializes a new instance of the class.
///
protected AxisControl()
{
HorizontalContentAlignment = HorizontalAlignment.Stretch;
VerticalContentAlignment = VerticalAlignment.Stretch;
Background = Brushes.Transparent;
Focusable = false;
UpdateUIResources();
UpdateSizeGetters();
}
internal void MakeDependent()
{
independent = false;
}
///
/// This conversion is performed to make horizontal one-string and two-string labels
/// stay at one height.
///
///
///
private static AxisPlacement GetBetterPlacement(AxisPlacement placement)
{
switch (placement)
{
case AxisPlacement.Left:
return AxisPlacement.Left;
case AxisPlacement.Right:
return AxisPlacement.Right;
case AxisPlacement.Top:
return AxisPlacement.Top;
case AxisPlacement.Bottom:
return AxisPlacement.Bottom;
default:
throw new NotSupportedException();
}
}
#region Properties
private AxisPlacement placement = AxisPlacement.Bottom;
///
/// Gets or sets the placement of axis control.
/// Relative positioning of parts of axis depends on this value.
///
/// The placement.
public AxisPlacement Placement
{
get { return placement; }
set
{
if (placement != value)
{
placement = value;
UpdateUIResources();
UpdateSizeGetters();
}
}
}
private void UpdateSizeGetters()
{
switch (placement)
{
case AxisPlacement.Left:
case AxisPlacement.Right:
getSize = size => size.Height;
getCoordinate = p => p.Y;
createScreenPoint1 = d => new Point(scrCoord1, d);
createScreenPoint2 = (d, size) => new Point(scrCoord2 * size, d);
break;
case AxisPlacement.Top:
case AxisPlacement.Bottom:
getSize = size => size.Width;
getCoordinate = p => p.X;
createScreenPoint1 = d => new Point(d, scrCoord1);
createScreenPoint2 = (d, size) => new Point(d, scrCoord2 * size);
break;
default:
break;
}
switch (placement)
{
case AxisPlacement.Left:
createDataPoint = d => new Point(0, d);
break;
case AxisPlacement.Right:
createDataPoint = d => new Point(1, d);
break;
case AxisPlacement.Top:
createDataPoint = d => new Point(d, 1);
break;
case AxisPlacement.Bottom:
createDataPoint = d => new Point(d, 0);
break;
default:
break;
}
}
private void UpdateUIResources()
{
ResourceDictionary resources = new ResourceDictionary
{
Source = new Uri("/DynamicDataDisplay;component/Charts/Axes/AxisControlStyle.xaml", UriKind.Relative)
};
string templateKey;
if (useSmoothPanning)
templateKey = smoothTemplateKey;
else
templateKey = AxisControl.templateKey;
AxisPlacement placement = GetBetterPlacement(this.placement);
ControlTemplate template = (ControlTemplate)resources[templateKey + placement.ToString()];
Verify.AssertNotNull(template);
var content = (FrameworkElement)template.LoadContent();
if (ticksPath != null && ticksPath.Data != null)
{
GeometryGroup group = (GeometryGroup)ticksPath.Data;
foreach (var child in group.Children)
{
LineGeometry geometry = (LineGeometry)child;
lineGeomPool.Put(geometry);
}
group.Children.Clear();
}
ticksPath = (Path)content.FindName(PART_TicksPath);
ticksPath.SnapsToDevicePixels = true;
Verify.AssertNotNull(ticksPath);
// as this method can be called not only on loading of axisControl, but when its placement changes, internal panels
// can be not empty and their contents should be released
if (commonLabelsCanvas != null && labelProvider != null)
{
foreach (UIElement child in commonLabelsCanvas.Children)
{
if (child != null)
{
labelProvider.ReleaseLabel(child);
}
}
labels = null;
commonLabelsCanvas.Children.Clear();
}
commonLabelsCanvas = (StackCanvas)content.FindName(PART_CommonLabelsCanvas);
Verify.AssertNotNull(commonLabelsCanvas);
commonLabelsCanvas.Placement = placement;
if (additionalLabelsCanvas != null && majorLabelProvider != null)
{
foreach (UIElement child in additionalLabelsCanvas.Children)
{
if (child != null)
{
majorLabelProvider.ReleaseLabel(child);
}
}
}
additionalLabelsCanvas = (StackCanvas)content.FindName(PART_AdditionalLabelsCanvas);
Verify.AssertNotNull(additionalLabelsCanvas);
additionalLabelsCanvas.Placement = placement;
mainGrid = (Panel)content.FindName(PART_ContentsGrid);
Verify.AssertNotNull(mainGrid);
mainGrid.SetBinding(Control.BackgroundProperty, new Binding { Path = new PropertyPath("Background"), Source = this });
mainGrid.SizeChanged += new SizeChangedEventHandler(mainGrid_SizeChanged);
Content = mainGrid;
string transformKey = additionalLabelTransformKey + placement.ToString();
if (resources.Contains(transformKey))
{
additionalLabelTransform = (Transform)resources[transformKey];
}
UpdateUI();
}
private void mainGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (placement.IsBottomOrTop() && e.WidthChanged ||
e.HeightChanged)
{
// this is performed because if not, whole axisControl's size was measured wrongly.
InvalidateMeasure();
UpdateUI();
}
}
private bool updateOnCommonChange = true;
internal IDisposable OpenUpdateRegion(bool forceUpdate)
{
return new UpdateRegionHolder(this, forceUpdate);
}
private sealed class UpdateRegionHolder : IDisposable
{
private Range prevRange;
private CoordinateTransform prevTransform;
private AxisControl owner;
private bool forceUpdate = false;
public UpdateRegionHolder(AxisControl owner) : this(owner, false) { }
public UpdateRegionHolder(AxisControl owner, bool forceUpdate)
{
this.owner = owner;
owner.updateOnCommonChange = false;
prevTransform = owner.transform;
prevRange = owner.range;
this.forceUpdate = forceUpdate;
}
#region IDisposable Members
public void Dispose()
{
owner.updateOnCommonChange = true;
bool shouldUpdate = owner.range != prevRange;
var screenRect = owner.Transform.ScreenRect;
var prevScreenRect = prevTransform.ScreenRect;
if (owner.placement.IsBottomOrTop())
{
shouldUpdate |= prevScreenRect.Width != screenRect.Width;
}
else
{
shouldUpdate |= prevScreenRect.Height != screenRect.Height;
}
shouldUpdate |= owner.transform.DataTransform != prevTransform.DataTransform;
shouldUpdate |= forceUpdate;
if (shouldUpdate)
{
owner.UpdateUI();
}
owner = null;
}
#endregion
}
private Range range;
///
/// Gets or sets the range, which ticks are generated for.
///
/// The range.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Range Range
{
get { return range; }
set
{
range = value;
if (updateOnCommonChange)
{
UpdateUI();
}
}
}
private bool drawMinorTicks = true;
///
/// Gets or sets a value indicating whether to show minor ticks.
///
/// true if show minor ticks; otherwise, false.
public bool DrawMinorTicks
{
get { return drawMinorTicks; }
set
{
if (drawMinorTicks != value)
{
drawMinorTicks = value;
UpdateUI();
}
}
}
private bool drawMajorLabels = true;
///
/// Gets or sets a value indicating whether to show major labels.
///
/// true if show major labels; otherwise, false.
public bool DrawMajorLabels
{
get { return drawMajorLabels; }
set
{
if (drawMajorLabels != value)
{
drawMajorLabels = value;
UpdateUI();
}
}
}
private bool drawTicks = true;
public bool DrawTicks
{
get { return drawTicks; }
set
{
if (drawTicks != value)
{
drawTicks = value;
UpdateUI();
}
}
}
#region TicksProvider
private ITicksProvider ticksProvider;
///
/// Gets or sets the ticks provider - generator of ticks for given range.
///
/// Should not be null.
///
/// The ticks provider.
public ITicksProvider TicksProvider
{
get { return ticksProvider; }
set
{
if (value == null)
throw new ArgumentNullException("value");
if (ticksProvider != value)
{
DetachTicksProvider();
ticksProvider = value;
AttachTicksProvider();
UpdateUI();
}
}
}
private void AttachTicksProvider()
{
if (ticksProvider != null)
{
ticksProvider.Changed += ticksProvider_Changed;
}
}
private void ticksProvider_Changed(object sender, EventArgs e)
{
UpdateUI();
}
private void DetachTicksProvider()
{
if (ticksProvider != null)
{
ticksProvider.Changed -= ticksProvider_Changed;
}
}
#endregion
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool ShouldSerializeContent()
{
return false;
}
protected override bool ShouldSerializeProperty(DependencyProperty dp)
{
// do not serialize template - for XAML serialization
if (dp == TemplateProperty) return false;
return base.ShouldSerializeProperty(dp);
}
#region MajorLabelProvider
private LabelProviderBase majorLabelProvider;
///
/// Gets or sets the major label provider, which creates labels for major ticks.
/// If null, major labels will not be shown.
///
/// The major label provider.
public LabelProviderBase MajorLabelProvider
{
get { return majorLabelProvider; }
set
{
if (majorLabelProvider != value)
{
DetachMajorLabelProvider();
majorLabelProvider = value;
AttachMajorLabelProvider();
UpdateUI();
}
}
}
private void AttachMajorLabelProvider()
{
if (majorLabelProvider != null)
{
majorLabelProvider.Changed += majorLabelProvider_Changed;
}
}
private void majorLabelProvider_Changed(object sender, EventArgs e)
{
UpdateUI();
}
private void DetachMajorLabelProvider()
{
if (majorLabelProvider != null)
{
majorLabelProvider.Changed -= majorLabelProvider_Changed;
}
}
#endregion
#region LabelProvider
private LabelProviderBase labelProvider;
///
/// Gets or sets the label provider, which generates labels for axis ticks.
/// Should not be null.
///
/// The label provider.
[NotNull]
public LabelProviderBase LabelProvider
{
get { return labelProvider; }
set
{
if (value == null)
throw new ArgumentNullException("value");
if (labelProvider != value)
{
DetachLabelProvider();
labelProvider = value;
AttachLabelProvider();
UpdateUI();
}
}
}
private void AttachLabelProvider()
{
if (labelProvider != null)
{
labelProvider.Changed += labelProvider_Changed;
}
}
private void labelProvider_Changed(object sender, EventArgs e)
{
UpdateUI();
}
private void DetachLabelProvider()
{
if (labelProvider != null)
{
labelProvider.Changed -= labelProvider_Changed;
}
}
#endregion
private CoordinateTransform transform = CoordinateTransform.CreateDefault();
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never)]
public CoordinateTransform Transform
{
get { return transform; }
set
{
transform = value;
if (updateOnCommonChange)
{
UpdateUI();
}
}
}
#endregion
private const double defaultSmallerSize = 1;
private const double defaultBiggerSize = 150;
protected override Size MeasureOverride(Size constraint)
{
var baseSize = base.MeasureOverride(constraint);
mainGrid.Measure(constraint);
Size gridSize = mainGrid.DesiredSize;
Size result = gridSize;
bool isHorizontal = placement == AxisPlacement.Bottom || placement == AxisPlacement.Top;
if (Double.IsInfinity(constraint.Width) && isHorizontal)
{
result = new Size(defaultBiggerSize, gridSize.Height != 0 ? gridSize.Height : defaultSmallerSize);
}
else if (Double.IsInfinity(constraint.Height) && !isHorizontal)
{
result = new Size(gridSize.Width != 0 ? gridSize.Width : defaultSmallerSize, defaultBiggerSize);
}
return result;
}
//protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
//{
// base.OnRenderSizeChanged(sizeInfo);
// bool isHorizontal = placement == AxisPlacement.Top || placement == AxisPlacement.Bottom;
// if (isHorizontal && sizeInfo.WidthChanged || !isHorizontal && sizeInfo.HeightChanged)
// {
// UpdateUIRepresentation();
// }
//}
private void InitTransform(Size newRenderSize)
{
Rect dataRect = CreateDataRect();
transform = transform.WithRects(dataRect, new Rect(newRenderSize));
}
private Rect CreateDataRect()
{
double min = convertToDouble(range.Min);
double max = convertToDouble(range.Max);
Rect dataRect;
switch (placement)
{
case AxisPlacement.Left:
case AxisPlacement.Right:
dataRect = new Rect(new Point(min, min), new Point(max, max));
break;
case AxisPlacement.Top:
case AxisPlacement.Bottom:
dataRect = new Rect(new Point(min, min), new Point(max, max));
break;
default:
throw new NotSupportedException();
}
return dataRect;
}
///
/// Gets the Path with ticks strokes.
///
/// The ticks path.
public override Path TicksPath
{
get { return ticksPath; }
}
private Panel mainGrid;
private StackCanvas additionalLabelsCanvas;
private StackCanvas commonLabelsCanvas;
private Path ticksPath;
private bool rendered = false;
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
if (!rendered)
{
UpdateUI();
}
rendered = true;
}
private bool independent = true;
private double scrCoord1 = 0; // px
private double scrCoord2 = 10; // px
///
/// Gets or sets the size of main axis ticks.
///
/// The size of the tick.
public double TickSize
{
get { return scrCoord2; }
set
{
if (scrCoord2 != value)
{
scrCoord2 = value;
UpdateUI();
}
}
}
private bool useSmoothPanning = false;
///
/// Gets or sets a value indicating whether axis control uses smooth panning.
///
/// true if control uses smooth panning; otherwise, false.
public bool UseSmoothPanning
{
get { return useSmoothPanning; }
set
{
useSmoothPanning = value;
UpdateUIResources();
}
}
double cachedPartLength;
double[] originalScreenTicks;
private Range axisLongRange;
private GeometryGroup geomGroup = new GeometryGroup();
internal void UpdateUI()
{
if (range.IsEmpty)
return;
if (transform == null) return;
if (independent)
{
InitTransform(RenderSize);
}
bool isHorizontal = Placement == AxisPlacement.Bottom || Placement == AxisPlacement.Top;
if (transform.ScreenRect.Width == 0 && isHorizontal
|| transform.ScreenRect.Height == 0 && !isHorizontal)
return;
if (!IsMeasureValid)
{
InvalidateMeasure();
}
Range currentDoubleRange = new Range(convertToDouble(range.Min), convertToDouble(range.Max));
bool sameLength = Math.Abs(cachedPartLength - currentDoubleRange.GetLength()) / cachedPartLength < 0.01 || cachedPartLength == 0;
if (UseSmoothPanning && sameLength)
{
Debug.WriteLine(Placement + " " + range + " " + axisLongRange);
// current range is included into axisLongRange
if (currentDoubleRange < axisLongRange)
{
var axisContent = (FrameworkElement)mainGrid.Children[0];
double leftScreen;
if (placement.IsBottomOrTop())
leftScreen = ((axisLongRange.Min - currentDoubleRange.Min) / currentDoubleRange.GetLength() + 1) * getSize(transform.ScreenRect.Size);
else
leftScreen = -((axisLongRange.Min - currentDoubleRange.Min) / currentDoubleRange.GetLength() + 1) * getSize(transform.ScreenRect.Size);
StackCanvas.SetCoordinate(axisContent, leftScreen);
// this call should be commented
// double rightScreen = ((axisLongRange.Max - currentDoubleRange.Max) / currentDoubleRange.GetLength() + 1) * getSize(transform.ScreenRect.Size);
// StackCanvas.SetEndCoordinate(axisContent, rightScreen);
}
else
{
double length = currentDoubleRange.GetLength();
cachedPartLength = length;
// cached axis part is three times longer
double min = currentDoubleRange.Min - length;
double max = currentDoubleRange.Max + length;
Range widerRange = new Range(convertFromDouble(min), convertFromDouble(max));
axisLongRange = new Range(min, max);
// rebuild entire ticks
FillParts(widerRange);
Debug.WriteLine("не шире " + Placement + " " + widerRange);
originalScreenTicks = screenTicks.ToArray();
}
// updating screen ticks (for axis grid)
// 3 is a ratio of cached area to visible area
double shift = (currentDoubleRange.Min - (axisLongRange.Min + axisLongRange.GetLength() / 3)) * getSize(transform.ScreenRect.Size);
if (!placement.IsBottomOrTop())
shift *= -1;
screenTicks = originalScreenTicks.ToArray();
for (int i = 0; i < originalScreenTicks.Length; i++)
{
screenTicks[i] -= shift;
}
}
else
{
FillParts(range);
}
ScreenTicksChanged.Raise(this);
}
private void FillParts(Range range)
{
CreateTicks(range);
// removing unfinite screen ticks
var tempTicks = new List(ticks);
var tempScreenTicks = new List(ticks.Length);
var tempLabels = new List(labels);
int i = 0;
while (i < tempTicks.Count)
{
T tick = tempTicks[i];
double screenTick = getCoordinate(createDataPoint(convertToDouble(tick)).DataToScreen(transform));
if (screenTick.IsFinite())
{
tempScreenTicks.Add(screenTick);
i++;
}
else
{
tempTicks.RemoveAt(i);
tempLabels.RemoveAt(i);
}
}
ticks = tempTicks.ToArray();
screenTicks = tempScreenTicks.ToArray();
labels = tempLabels.ToArray();
// saving generated lines into pool
for (i = 0; i < geomGroup.Children.Count; i++)
{
var geometry = (LineGeometry)geomGroup.Children[i];
lineGeomPool.Put(geometry);
}
geomGroup = new GeometryGroup();
geomGroup.Children = new GeometryCollection(lineGeomPool.Count);
if (drawTicks)
DoDrawTicks(screenTicks, geomGroup.Children);
if (drawMinorTicks)
DoDrawMinorTicks(geomGroup.Children);
ticksPath.Data = geomGroup;
DoDrawCommonLabels(screenTicks);
if (drawMajorLabels)
DoDrawMajorLabels();
}
bool drawTicksOnEmptyLabel = false;
///
/// Gets or sets a value indicating whether to draw ticks on empty label.
///
///
/// true if draw ticks on empty label; otherwise, false.
///
public bool DrawTicksOnEmptyLabel
{
get { return drawTicksOnEmptyLabel; }
set
{
if (drawTicksOnEmptyLabel != value)
{
drawTicksOnEmptyLabel = value;
UpdateUI();
}
}
}
private readonly ResourcePool lineGeomPool = new ResourcePool();
private void DoDrawTicks(double[] screenTicksX, ICollection lines)
{
for (int i = 0; i < screenTicksX.Length; i++)
{
if (labels[i] == null && !drawTicksOnEmptyLabel)
continue;
Point p1 = createScreenPoint1(screenTicksX[i]);
Point p2 = createScreenPoint2(screenTicksX[i], 1);
LineGeometry line = lineGeomPool.GetOrCreate();
line.StartPoint = p1;
line.EndPoint = p2;
lines.Add(line);
}
}
private double GetRangesRatio(Range nominator, Range denominator)
{
double nomMin = ConvertToDouble(nominator.Min);
double nomMax = ConvertToDouble(nominator.Max);
double denMin = ConvertToDouble(denominator.Min);
double denMax = ConvertToDouble(denominator.Max);
return (nomMax - nomMin) / (denMax - denMin);
}
Transform additionalLabelTransform = null;
private void DoDrawMajorLabels()
{
ITicksProvider majorTicksProvider = ticksProvider.MajorProvider;
additionalLabelsCanvas.Children.Clear();
if (majorTicksProvider != null && majorLabelProvider != null)
{
additionalLabelsCanvas.Visibility = Visibility.Visible;
Size renderSize = RenderSize;
var majorTicks = majorTicksProvider.GetTicks(range, DefaultTicksProvider.DefaultTicksCount);
double[] screenCoords = majorTicks.Ticks.Select(tick => createDataPoint(convertToDouble(tick))).
Select(p => p.DataToScreen(transform)).Select(p => getCoordinate(p)).ToArray();
// todo this is not the best decision - when displaying, for example,
// milliseconds, it causes to create hundreds and thousands of textBlocks.
double rangesRatio = GetRangesRatio(majorTicks.Ticks.GetPairs().ToArray()[0], range);
object info = majorTicks.Info;
MajorLabelsInfo newInfo = new MajorLabelsInfo
{
Info = info,
MajorLabelsCount = (int)Math.Ceiling(rangesRatio)
};
var newMajorTicks = new TicksInfo
{
Info = newInfo,
Ticks = majorTicks.Ticks,
TickSizes = majorTicks.TickSizes
};
UIElement[] additionalLabels = MajorLabelProvider.CreateLabels(newMajorTicks);
for (int i = 0; i < additionalLabels.Length; i++)
{
if (screenCoords[i].IsNaN())
continue;
UIElement tickLabel = additionalLabels[i];
tickLabel.Measure(renderSize);
StackCanvas.SetCoordinate(tickLabel, screenCoords[i]);
StackCanvas.SetEndCoordinate(tickLabel, screenCoords[i + 1]);
if (tickLabel is FrameworkElement)
((FrameworkElement)tickLabel).LayoutTransform = additionalLabelTransform;
additionalLabelsCanvas.Children.Add(tickLabel);
}
}
else
{
additionalLabelsCanvas.Visibility = Visibility.Collapsed;
}
}
private int prevMinorTicksCount = DefaultTicksProvider.DefaultTicksCount;
private const int maxTickArrangeIterations = 12;
private void DoDrawMinorTicks(ICollection lines)
{
ITicksProvider minorTicksProvider = ticksProvider.MinorProvider;
if (minorTicksProvider != null)
{
int minorTicksCount = prevMinorTicksCount;
int prevActualTicksCount = -1;
ITicksInfo minorTicks;
TickCountChange result = TickCountChange.OK;
TickCountChange prevResult;
int iteration = 0;
do
{
Verify.IsTrue(++iteration < maxTickArrangeIterations);
minorTicks = minorTicksProvider.GetTicks(range, minorTicksCount);
prevActualTicksCount = minorTicks.Ticks.Length;
prevResult = result;
result = CheckMinorTicksArrangement(minorTicks);
if (prevResult == TickCountChange.Decrease && result == TickCountChange.Increase)
{
// stop tick number oscillating
result = TickCountChange.OK;
}
if (result == TickCountChange.Decrease)
{
int newMinorTicksCount = minorTicksProvider.DecreaseTickCount(minorTicksCount);
if (newMinorTicksCount == minorTicksCount)
{
result = TickCountChange.OK;
}
minorTicksCount = newMinorTicksCount;
}
else if (result == TickCountChange.Increase)
{
int newCount = minorTicksProvider.IncreaseTickCount(minorTicksCount);
if (newCount == minorTicksCount)
{
result = TickCountChange.OK;
}
minorTicksCount = newCount;
}
} while (result != TickCountChange.OK);
prevMinorTicksCount = minorTicksCount;
double[] sizes = minorTicks.TickSizes;
double[] screenCoords = minorTicks.Ticks.Select(
coord => getCoordinate(createDataPoint(convertToDouble(coord)).
DataToScreen(transform))).ToArray();
minorScreenTicks = new MinorTickInfo[screenCoords.Length];
for (int i = 0; i < screenCoords.Length; i++)
{
minorScreenTicks[i] = new MinorTickInfo(sizes[i], screenCoords[i]);
}
for (int i = 0; i < screenCoords.Length; i++)
{
double screenCoord = screenCoords[i];
Point p1 = createScreenPoint1(screenCoord);
Point p2 = createScreenPoint2(screenCoord, sizes[i]);
LineGeometry line = lineGeomPool.GetOrCreate();
line.StartPoint = p1;
line.EndPoint = p2;
lines.Add(line);
}
}
}
private TickCountChange CheckMinorTicksArrangement(ITicksInfo minorTicks)
{
Size renderSize = RenderSize;
TickCountChange result = TickCountChange.OK;
if (minorTicks.Ticks.Length * 3 > getSize(renderSize))
result = TickCountChange.Decrease;
else if (minorTicks.Ticks.Length * 6 < getSize(renderSize))
result = TickCountChange.Increase;
return result;
}
private bool isStaticAxis = false;
///
/// Gets or sets a value indicating whether this instance is a static axis.
/// If axis is static, its labels from sides are shifted so that they are not clipped by axis bounds.
///
///
/// true if this instance is static axis; otherwise, false.
///
public bool IsStaticAxis
{
get { return isStaticAxis; }
set
{
if (isStaticAxis != value)
{
isStaticAxis = value;
UpdateUI();
}
}
}
private double ToScreen(T value)
{
return getCoordinate(createDataPoint(convertToDouble(value)).DataToScreen(transform));
}
private double staticAxisMargin = 1; // px
private void DoDrawCommonLabels(double[] screenTicksX)
{
Size renderSize = RenderSize;
commonLabelsCanvas.Children.Clear();
#if DEBUG
if (labels != null)
{
foreach (FrameworkElement item in labels)
{
if (item != null)
Debug.Assert(item.Parent == null);
}
}
#endif
double minCoordUnsorted = ToScreen(range.Min);
double maxCoordUnsorted = ToScreen(range.Max);
double minCoord = Math.Min(minCoordUnsorted, maxCoordUnsorted);
double maxCoord = Math.Max(minCoordUnsorted, maxCoordUnsorted);
double maxCoordDiff = (maxCoord - minCoord) / labels.Length / 2.0;
double minCoordToAdd = minCoord - maxCoordDiff;
double maxCoordToAdd = maxCoord + maxCoordDiff;
for (int i = 0; i < ticks.Length; i++)
{
FrameworkElement tickLabel = (FrameworkElement)labels[i];
if (tickLabel == null) continue;
Debug.Assert(((FrameworkElement)tickLabel).Parent == null);
tickLabel.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
double screenX = screenTicksX[i];
double coord = screenX;
tickLabel.HorizontalAlignment = HorizontalAlignment.Center;
tickLabel.VerticalAlignment = VerticalAlignment.Center;
if (isStaticAxis)
{
// getting real size of label
tickLabel.Measure(renderSize);
Size tickLabelSize = tickLabel.DesiredSize;
if (Math.Abs(screenX - minCoord) < maxCoordDiff)
{
coord = minCoord + staticAxisMargin;
if (placement.IsBottomOrTop())
tickLabel.HorizontalAlignment = HorizontalAlignment.Left;
else
tickLabel.VerticalAlignment = VerticalAlignment.Top;
}
else if (Math.Abs(screenX - maxCoord) < maxCoordDiff)
{
coord = maxCoord - getSize(tickLabelSize) / 2 - staticAxisMargin;
if (!placement.IsBottomOrTop())
{
tickLabel.VerticalAlignment = VerticalAlignment.Bottom;
coord = maxCoord - staticAxisMargin;
}
}
}
// label is out of visible area
if (coord < minCoord || coord > maxCoord)
{
// todo investigate
//continue;
}
if (coord.IsNaN())
continue;
StackCanvas.SetCoordinate(tickLabel, coord);
commonLabelsCanvas.Children.Add(tickLabel);
}
}
private double GetCoordinateFromTick(T tick)
{
return getCoordinate(createDataPoint(convertToDouble(tick)).DataToScreen(transform));
}
private Func convertToDouble;
///
/// Gets or sets the convertion of tick to double.
/// Should not be null.
///
/// The convert to double.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Func ConvertToDouble
{
get { return convertToDouble; }
set
{
if (value == null)
throw new ArgumentNullException("value");
convertToDouble = value;
UpdateUI();
}
}
private Func convertFromDouble;
///
/// Convertation function for use in smooth axes panning.
/// Converts from axis value to double.
///
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Func ConvertFromDouble
{
get { return convertFromDouble; }
set
{
if (value == null)
throw new ArgumentNullException("value");
convertFromDouble = value;
UpdateUI();
}
}
internal event EventHandler ScreenTicksChanged;
private double[] screenTicks;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never)]
public double[] ScreenTicks
{
get { return screenTicks; }
}
private MinorTickInfo[] minorScreenTicks;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never)]
public MinorTickInfo[] MinorScreenTicks
{
get { return minorScreenTicks; }
}
ITicksInfo ticksInfo;
private T[] ticks;
private UIElement[] labels;
private const double increaseRatio = 3.0;
private const double decreaseRatio = 1.6;
private Func getSize = size => size.Width;
private Func getCoordinate = p => p.X;
private Func createDataPoint = d => new Point(d, 0);
private Func createScreenPoint1 = d => new Point(d, 0);
private Func createScreenPoint2 = (d, size) => new Point(d, size);
private int previousTickCount = DefaultTicksProvider.DefaultTicksCount;
private void CreateTicks(Range range)
{
TickCountChange result = TickCountChange.OK;
TickCountChange prevResult;
int prevActualTickCount = -1;
int tickCount = previousTickCount;
int iteration = 0;
do
{
Verify.IsTrue(++iteration < maxTickArrangeIterations);
ticksInfo = ticksProvider.GetTicks(range, tickCount);
ticks = ticksInfo.Ticks;
if (ticks.Length == prevActualTickCount)
{
result = TickCountChange.OK;
break;
}
prevActualTickCount = ticks.Length;
if (labels != null)
{
for (int i = 0; i < labels.Length; i++)
{
labelProvider.ReleaseLabel(labels[i]);
}
}
labels = labelProvider.CreateLabels(ticksInfo);
prevResult = result;
result = CheckLabelsArrangement(labels, ticks);
if (prevResult == TickCountChange.Decrease && result == TickCountChange.Increase)
{
// stop tick number oscillating
result = TickCountChange.OK;
}
if (result != TickCountChange.OK)
{
int prevTickCount = tickCount;
if (result == TickCountChange.Decrease)
tickCount = ticksProvider.DecreaseTickCount(tickCount);
else
{
tickCount = ticksProvider.IncreaseTickCount(tickCount);
//DebugVerify.Is(tickCount >= prevTickCount);
}
// ticks provider could not create less ticks or tick number didn't change
if (tickCount == 0 || prevTickCount == tickCount)
{
tickCount = prevTickCount;
result = TickCountChange.OK;
}
}
} while (result != TickCountChange.OK);
previousTickCount = tickCount;
}
private TickCountChange CheckLabelsArrangement(UIElement[] labels, T[] ticks)
{
var actualLabels = labels.Select((label, i) => new { Label = label, Index = i })
.Where(el => el.Label != null)
.Select(el => new { Label = el.Label, Tick = ticks[el.Index] })
.ToList();
actualLabels.ForEach(item => item.Label.Measure(RenderSize));
var sizeInfos = actualLabels.Select(item =>
new { X = GetCoordinateFromTick(item.Tick), Size = getSize(item.Label.DesiredSize) })
.OrderBy(item => item.X).ToArray();
TickCountChange res = TickCountChange.OK;
int increaseCount = 0;
for (int i = 0; i < sizeInfos.Length - 1; i++)
{
if ((sizeInfos[i].X + sizeInfos[i].Size * decreaseRatio) > sizeInfos[i + 1].X)
{
res = TickCountChange.Decrease;
break;
}
if ((sizeInfos[i].X + sizeInfos[i].Size * increaseRatio) < sizeInfos[i + 1].X)
{
increaseCount++;
}
}
if (increaseCount > sizeInfos.Length / 2)
res = TickCountChange.Increase;
return res;
}
}
[DebuggerDisplay("{X} + {Size}")]
internal sealed class SizeInfo : IComparable
{
public double Size { get; set; }
public double X { get; set; }
public int CompareTo(SizeInfo other)
{
return X.CompareTo(other.X);
}
}
internal enum TickCountChange
{
Increase = -1,
OK = 0,
Decrease = 1
}
///
/// Represents an auxiliary structure for storing additional info during major DateTime labels generation.
///
public struct MajorLabelsInfo
{
public object Info { get; set; }
public int MajorLabelsCount { get; set; }
public override string ToString()
{
return String.Format("{0}, Count={1}", Info, MajorLabelsCount);
}
}
}