mirror of
https://gitee.com/akwkevin/aistudio.-wpf.-diagram
synced 2026-03-03 00:00:57 +08:00
540 lines
18 KiB
C#
540 lines
18 KiB
C#
//The MIT License(MIT)
|
|
|
|
//Copyright(c) 2016 Alberto Rodriguez & LiveCharts Contributors
|
|
|
|
//Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
//of this software and associated documentation files (the "Software"), to deal
|
|
//in the Software without restriction, including without limitation the rights
|
|
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
//copies of the Software, and to permit persons to whom the Software is
|
|
//furnished to do so, subject to the following conditions:
|
|
|
|
//The above copyright notice and this permission notice shall be included in all
|
|
//copies or substantial portions of the Software.
|
|
|
|
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
//SOFTWARE.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using LiveCharts.Charts;
|
|
using LiveCharts.Definitions.Charts;
|
|
using LiveCharts.Dtos;
|
|
using LiveCharts.Helpers;
|
|
|
|
namespace LiveCharts
|
|
{
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
public class AxisCore
|
|
{
|
|
private double _topLimit;
|
|
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="AxisCore"/> class.
|
|
/// </summary>
|
|
/// <param name="view">The view.</param>
|
|
public AxisCore(IAxisView view)
|
|
{
|
|
View = view;
|
|
CleanFactor = 3;
|
|
Cache = new Dictionary<double, SeparatorElementCore>();
|
|
LastAxisMax = null;
|
|
LastAxisMin = null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public Properties
|
|
|
|
/// <summary>
|
|
/// Gets or sets the chart.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The chart.
|
|
/// </value>
|
|
public ChartCore Chart { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets the view.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The view.
|
|
/// </value>
|
|
public IAxisView View { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets the labels.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The labels.
|
|
/// </value>
|
|
public IList<string> Labels { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets the sections.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The sections.
|
|
/// </value>
|
|
public List<AxisSectionCore> Sections { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets the label formatter.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The label formatter.
|
|
/// </value>
|
|
public Func<double, string> LabelFormatter { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets the stroke thickness.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The stroke thickness.
|
|
/// </value>
|
|
public double StrokeThickness { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether [show labels].
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if [show labels]; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public bool ShowLabels { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets the maximum value.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The maximum value.
|
|
/// </value>
|
|
public double MaxValue { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets the minimum value.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The minimum value.
|
|
/// </value>
|
|
public double MinValue { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets the title.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The title.
|
|
/// </value>
|
|
public string Title { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether [disable animations].
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if [disable animations]; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public bool DisableAnimations { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets the position.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The position.
|
|
/// </value>
|
|
public AxisPosition Position { get; set; }
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this instance is merged.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if this instance is merged; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public bool IsMerged { get; set; }
|
|
/// <summary>
|
|
/// Gets a value indicating whether [evaluates unit width].
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if [evaluates unit width]; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public bool EvaluatesUnitWidth { get; internal set; }
|
|
/// <summary>
|
|
/// Gets the current separators.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The current separators.
|
|
/// </value>
|
|
public IEnumerable<SeparatorElementCore> CurrentSeparators
|
|
{
|
|
get { return Cache.Values; }
|
|
}
|
|
/// <summary>
|
|
/// Gets or sets the separator.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The separator.
|
|
/// </value>
|
|
public SeparatorConfigurationCore Separator { get; set; }
|
|
/// <summary>
|
|
/// Gets the s.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The s.
|
|
/// </value>
|
|
public double S { get; internal set; }
|
|
|
|
#endregion
|
|
|
|
#region Internal Properties
|
|
|
|
internal double Tab { get; set; }
|
|
|
|
internal double TopLimit
|
|
{
|
|
get { return _topLimit; }
|
|
set
|
|
{
|
|
_topLimit = value;
|
|
}
|
|
}
|
|
|
|
internal double BotLimit { get; set; }
|
|
internal double TopSeriesLimit { get; set; }
|
|
internal double BotSeriesLimit { get; set; }
|
|
internal double MaxPointRadius { get; set; }
|
|
internal double Magnitude { get; set; }
|
|
internal double CleanFactor { get; set; }
|
|
internal Dictionary<double, SeparatorElementCore> Cache { get; set; }
|
|
internal double? LastAxisMax { get; set; }
|
|
internal double? LastAxisMin { get; set; }
|
|
internal CoreRectangle LastPlotArea { get; set; }
|
|
internal int GarbageCollectorIndex { get; set; }
|
|
internal double PreviousTop { get; set; }
|
|
internal double PreviousBot { get; set; }
|
|
protected internal double FirstSeparator { get; protected set; }
|
|
protected internal double LastSeparator { get; protected set; }
|
|
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
/// <summary>
|
|
/// Gets the formatter.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public virtual Func<double, string> GetFormatter()
|
|
{
|
|
return Formatter;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the separators.
|
|
/// </summary>
|
|
public void ClearSeparators()
|
|
{
|
|
foreach (var separator in Cache)
|
|
{
|
|
separator.Value.View.Clear(Chart.View);
|
|
}
|
|
Cache = new Dictionary<double, SeparatorElementCore>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal Methods
|
|
|
|
internal virtual void CalculateSeparator(ChartCore chart, AxisOrientation source)
|
|
{
|
|
var range = TopLimit - BotLimit;
|
|
range = range <= 0 ? 1 : range;
|
|
|
|
//ToDO: Improve this according to current labels!
|
|
var separations = source == AxisOrientation.Y
|
|
? Math.Round(chart.ControlSize.Height / ((12) * CleanFactor), 0) // at least 3 font 12 labels per separator.
|
|
: Math.Round(chart.ControlSize.Width / (50 * CleanFactor), 0); // at least 150 pixels per separator.
|
|
|
|
separations = separations < 2 ? 2 : separations;
|
|
|
|
var minimum = range / separations;
|
|
Magnitude = Math.Pow(10, Math.Floor(Math.Log(minimum) / Math.Log(10)));
|
|
|
|
if (!double.IsNaN(Separator.Step))
|
|
{
|
|
S = Separator.Step;
|
|
return;
|
|
}
|
|
|
|
var residual = minimum / Magnitude;
|
|
double tick;
|
|
if (residual > 5)
|
|
tick = 10 * Magnitude;
|
|
else if (residual > 2)
|
|
tick = 5 * Magnitude;
|
|
else if (residual > 1)
|
|
tick = 2 * Magnitude;
|
|
else
|
|
tick = Magnitude;
|
|
|
|
S = tick;
|
|
|
|
if (Labels != null) S = S < 1 ? 1 : S;
|
|
}
|
|
|
|
internal virtual CoreMargin PrepareChart(AxisOrientation source, ChartCore chart)
|
|
{
|
|
if (!(Math.Abs(TopLimit - BotLimit) > S * .01) || !ShowLabels) return new CoreMargin();
|
|
|
|
CalculateSeparator(chart, source);
|
|
|
|
var f = GetFormatter();
|
|
|
|
var currentMargin = new CoreMargin();
|
|
var tolerance = S / 10;
|
|
|
|
InitializeGarbageCollector();
|
|
|
|
var m = !double.IsNaN(View.BarUnit)
|
|
? View.BarUnit
|
|
: (!double.IsNaN(View.Unit)
|
|
? View.Unit
|
|
: Magnitude);
|
|
|
|
var u = !double.IsNaN(View.BarUnit)
|
|
? View.BarUnit
|
|
: (!double.IsNaN(View.Unit)
|
|
? View.Unit
|
|
: 1);
|
|
|
|
if (TopLimit <= 0 && BotLimit < 0)
|
|
{
|
|
var l = TopLimit - (EvaluatesUnitWidth ? u : 0);
|
|
LastSeparator = l;
|
|
for (var i = l; i >= Math.Truncate(BotLimit / m) * m; i -= S)
|
|
{
|
|
FirstSeparator = i;
|
|
DrawSeparator(i, tolerance, currentMargin, f, source);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var l = Math.Truncate(BotLimit / m) * m;
|
|
FirstSeparator = l;
|
|
for (var i = l; i <= TopLimit - (EvaluatesUnitWidth ? u : 0); i += S)
|
|
{
|
|
LastSeparator = i;
|
|
DrawSeparator(i, tolerance, currentMargin, f, source);
|
|
}
|
|
}
|
|
|
|
return currentMargin;
|
|
}
|
|
|
|
internal void UpdateSeparators(AxisOrientation source, ChartCore chart, int axisIndex)
|
|
{
|
|
foreach (var element in Cache.Values.ToArray())
|
|
{
|
|
if (element.GarbageCollectorIndex < GarbageCollectorIndex)
|
|
{
|
|
element.State = SeparationState.Remove;
|
|
Cache.Remove(element.Key);
|
|
}
|
|
|
|
var toLine = ChartFunctions.ToPlotArea(element.Value, source, chart, axisIndex);
|
|
|
|
var direction = source == AxisOrientation.X ? 1 : -1;
|
|
|
|
toLine += EvaluatesUnitWidth ? direction * ChartFunctions.GetUnitWidth(source, chart, this) / 2 : 0;
|
|
var toLabel = toLine + element.View.LabelModel.GetOffsetBySource(source);
|
|
|
|
if (IsMerged)
|
|
{
|
|
const double padding = 4;
|
|
|
|
if (source == AxisOrientation.Y)
|
|
{
|
|
if (toLabel + element.View.LabelModel.ActualHeight >
|
|
chart.DrawMargin.Top + chart.DrawMargin.Height)
|
|
toLabel -= element.View.LabelModel.ActualHeight + padding;
|
|
}
|
|
else
|
|
{
|
|
if (toLabel + element.View.LabelModel.ActualWidth >
|
|
chart.DrawMargin.Left + chart.DrawMargin.Width)
|
|
toLabel -= element.View.LabelModel.ActualWidth + padding;
|
|
}
|
|
}
|
|
|
|
var labelTab = Tab;
|
|
labelTab += element.View.LabelModel.GetOffsetBySource(source.Invert());
|
|
|
|
switch (element.State)
|
|
{
|
|
case SeparationState.Remove:
|
|
if (!chart.View.DisableAnimations && !View.DisableAnimations)
|
|
{
|
|
element.View.Move(chart, this, source, axisIndex, toLabel, toLine, labelTab);
|
|
element.View.FadeOutAndRemove(chart);
|
|
}
|
|
else
|
|
{
|
|
element.View.Remove(chart);
|
|
}
|
|
break;
|
|
case SeparationState.Keep:
|
|
if (!chart.View.DisableAnimations && !View.DisableAnimations)
|
|
{
|
|
if (element.IsNew)
|
|
{
|
|
var toLinePrevious = FromPreviousState(element.Value, source, chart);
|
|
toLinePrevious += EvaluatesUnitWidth ? ChartFunctions.GetUnitWidth(source, chart, this) / 2 : 0;
|
|
var toLabelPrevious = toLinePrevious + element.View.LabelModel.GetOffsetBySource(source);
|
|
element.View.Place(chart, this, source, axisIndex, toLabelPrevious,
|
|
toLinePrevious, labelTab);
|
|
element.View.FadeIn(this, chart);
|
|
}
|
|
element.View.Move(chart, this, source, axisIndex, toLabel, toLine, labelTab);
|
|
}
|
|
else
|
|
{
|
|
element.View.Place(chart, this, source, axisIndex, toLabel, toLine, labelTab);
|
|
}
|
|
break;
|
|
case SeparationState.InitialAdd:
|
|
element.View.Place(chart, this, source, axisIndex, toLabel, toLine, labelTab);
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
|
|
LastAxisMax = TopLimit;
|
|
LastAxisMin = BotLimit;
|
|
LastPlotArea = new CoreRectangle(chart.DrawMargin.Left, chart.DrawMargin.Top,
|
|
chart.DrawMargin.Width, chart.DrawMargin.Height);
|
|
}
|
|
|
|
internal double FromPreviousState(double value, AxisOrientation source, ChartCore chart)
|
|
{
|
|
if (LastAxisMax == null) return 0;
|
|
|
|
var p1 = new CorePoint();
|
|
var p2 = new CorePoint();
|
|
|
|
if (source == AxisOrientation.Y)
|
|
{
|
|
p1.X = LastAxisMax ?? 0;
|
|
p1.Y = LastPlotArea.Top;
|
|
|
|
p2.X = LastAxisMin ?? 0;
|
|
p2.Y = LastPlotArea.Top + LastPlotArea.Height;
|
|
}
|
|
else
|
|
{
|
|
p1.X = LastAxisMax ?? 0;
|
|
p1.Y = LastPlotArea.Width + LastPlotArea.Left;
|
|
|
|
p2.X = LastAxisMin ?? 0;
|
|
p2.Y = LastPlotArea.Left;
|
|
}
|
|
|
|
var deltaX = p2.X - p1.X;
|
|
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
|
var m = (p2.Y - p1.Y) / (deltaX == 0 ? double.MinValue : deltaX);
|
|
var d = m * (value - p1.X) + p1.Y;
|
|
|
|
return d;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Protected Methods
|
|
|
|
/// <summary>
|
|
/// Initializes the garbage collector.
|
|
/// </summary>
|
|
protected void InitializeGarbageCollector()
|
|
{
|
|
if (GarbageCollectorIndex == int.MaxValue)
|
|
{
|
|
foreach (var value in Cache.Values)
|
|
{
|
|
value.GarbageCollectorIndex = 0;
|
|
}
|
|
GarbageCollectorIndex = 0;
|
|
}
|
|
|
|
GarbageCollectorIndex++;
|
|
}
|
|
#endregion
|
|
|
|
#region Private Methods
|
|
|
|
private void DrawSeparator(double i, double tolerance, CoreMargin currentMargin, Func<double, string> f, AxisOrientation source)
|
|
{
|
|
if (i < BotLimit) return;
|
|
|
|
SeparatorElementCore asc;
|
|
|
|
var key = Math.Round(i / tolerance) * tolerance;
|
|
if (!Cache.TryGetValue(key, out asc))
|
|
{
|
|
asc = new SeparatorElementCore { IsNew = true };
|
|
Cache[key] = asc;
|
|
}
|
|
else
|
|
{
|
|
asc.IsNew = false;
|
|
}
|
|
|
|
View.RenderSeparator(asc, Chart);
|
|
|
|
asc.Key = key;
|
|
asc.Value = i;
|
|
asc.GarbageCollectorIndex = GarbageCollectorIndex;
|
|
|
|
var labelsMargin = asc.View.UpdateLabel(f(i), this, source);
|
|
|
|
currentMargin.Width = labelsMargin.TakenWidth > currentMargin.Width
|
|
? labelsMargin.TakenWidth
|
|
: currentMargin.Width;
|
|
currentMargin.Height = labelsMargin.TakenHeight > currentMargin.Height
|
|
? labelsMargin.TakenHeight
|
|
: currentMargin.Height;
|
|
|
|
currentMargin.Left = labelsMargin.Left > currentMargin.Left
|
|
? labelsMargin.Left
|
|
: currentMargin.Left;
|
|
currentMargin.Right = labelsMargin.Right > currentMargin.Right
|
|
? labelsMargin.Right
|
|
: currentMargin.Right;
|
|
|
|
currentMargin.Top = labelsMargin.Top > currentMargin.Top
|
|
? labelsMargin.Top
|
|
: currentMargin.Top;
|
|
currentMargin.Bottom = labelsMargin.Bottom > currentMargin.Bottom
|
|
? labelsMargin.Bottom
|
|
: currentMargin.Bottom;
|
|
|
|
if (LastAxisMax == null)
|
|
{
|
|
asc.State = SeparationState.InitialAdd;
|
|
return;
|
|
}
|
|
|
|
asc.State = SeparationState.Keep;
|
|
}
|
|
|
|
private string Formatter(double x)
|
|
{
|
|
if (Labels == null)
|
|
{
|
|
return LabelFormatter == null
|
|
? x.ToString(CultureInfo.InvariantCulture)
|
|
: LabelFormatter(x);
|
|
}
|
|
|
|
if (x < 0) x *= -1;
|
|
return Labels.Count > x && x >= 0
|
|
? Labels[(int)x]
|
|
: "";
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
} |