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); } } }