using ScottPlot.Drawing;
using ScottPlot.Renderable;
using ScottPlot.Ticks.DateTimeTickUnits;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace ScottPlot.Ticks
{
public enum TickLabelFormat { Numeric, DateTime }; // TODO: add hex, binary, scientific notation, etc?
public enum AxisOrientation { Vertical, Horizontal };
public enum MinorTickDistribution { even, log,EvenAndLog };
public class TickCollection
{
// This class creates pretty tick labels (with offset and exponent) uses graph settings
// to inspect the tick font and ensure tick labels will not overlap.
// It also respects manually defined tick spacing settings set via plt.Grid().
// TODO: store these in a class
public double[] tickPositionsMajor;
public double[] tickPositionsMinor;
public string[] tickLabels;
public double[] manualTickPositions;
public string[] manualTickLabels;
///
/// Label to show in the corner when using multiplier or offset notation
///
public string CornerLabel { get; private set; }
///
/// Measured size of the largest tick label
///
public float LargestLabelWidth { get; private set; } = 15;
///
/// Measured size of the largest tick label
///
public float LargestLabelHeight { get; private set; } = 12;
///
/// Controls how to translate positions to strings
///
public TickLabelFormat LabelFormat = TickLabelFormat.Numeric;
///
/// If True, these ticks are placed along a vertical (Y) axis.
/// This is used to determine whether tick density should be based on tick label width or height.
///
public AxisOrientation Orientation;
///
/// If True, the sign of numeric tick labels will be inverted.
/// This is used to give the appearance of descending ticks.
///
public bool LabelUsingInvertedSign;
///
/// Define how minor ticks are distributed (evenly vs. log scale)
///
public MinorTickDistribution MinorTickDistribution;
public string numericFormatString;
public string dateTimeFormatString;
///
/// If defined, this function will be used to generate tick labels from positions
///
public Func ManualTickFormatter = null;
public int radix = 10;
public string prefix = null;
public double manualSpacingX = 0;
public double manualSpacingY = 0;
public Ticks.DateTimeUnit? manualDateTimeSpacingUnitX = null;
public Ticks.DateTimeUnit? manualDateTimeSpacingUnitY = null;
public CultureInfo Culture = CultureInfo.DefaultThreadCurrentCulture;
public bool useMultiplierNotation = false;
public bool useOffsetNotation = false;
public bool useExponentialNotation = true;
///
/// Optimally packed tick labels have a density 1.0 and lower densities space ticks farther apart.
///
public float TickDensity = 1.0f;
///
/// Defines the minimum distance (in coordinate units) for major ticks.
///
public double MinimumTickSpacing = 0;
///
/// 计算标记、刻度位置
///
///
///
///
public void Recalculate(PlotDimensions dims, Drawing.Font tickFont, AxisDimensions dimensions)
{
if (manualTickPositions is null)
{
if (Orientation==AxisOrientation.Vertical)
{
RecalculateChannelPositionsAutomaticNumeric(dims, 15, 12, (int)(10 * TickDensity), dimensions);
(LargestLabelWidth, LargestLabelHeight) = MaxLabelSize(tickFont);
RecalculateChannelPositionsAutomaticNumeric(dims, LargestLabelWidth, LargestLabelHeight, null, dimensions);
}
else
{
RecalculateTimebasePositionsAutomaticNumeric(dims, 15, 12, (int)(10 * TickDensity), dimensions);
(LargestLabelWidth, LargestLabelHeight) = MaxLabelSize(tickFont);
RecalculateTimebasePositionsAutomaticNumeric(dims, LargestLabelWidth, LargestLabelHeight, null, dimensions);
}
}
else
{ //手动标记
double min = Orientation == AxisOrientation.Vertical ? dims.YMin : dims.XMin;
double max = Orientation == AxisOrientation.Vertical ? dims.YMax : dims.XMax;
var visibleIndexes = Enumerable.Range(0, manualTickPositions.Count())
.Where(i => manualTickPositions[i] >= min)
.Where(i => manualTickPositions[i] <= max);
tickPositionsMajor = visibleIndexes.Select(x => manualTickPositions[x]).ToArray();
tickPositionsMinor = null;
tickLabels = visibleIndexes.Select(x => manualTickLabels[x]).ToArray();
CornerLabel = null;
(LargestLabelWidth, LargestLabelHeight) = MaxLabelSize(tickFont);
}
}
public void SetCulture(
string shortDatePattern = null,
string decimalSeparator = null,
string numberGroupSeparator = null,
int? decimalDigits = null,
int? numberNegativePattern = null,
int[] numberGroupSizes = null
)
{
// Culture may be null if the thread culture is the same is the system culture.
// If it is null, assigning it to a clone of the current culture solves this and also makes it mutable.
Culture = Culture ?? (CultureInfo)CultureInfo.CurrentCulture.Clone();
Culture.DateTimeFormat.ShortDatePattern = shortDatePattern ?? Culture.DateTimeFormat.ShortDatePattern;
Culture.NumberFormat.NumberDecimalDigits = decimalDigits ?? Culture.NumberFormat.NumberDecimalDigits;
Culture.NumberFormat.NumberDecimalSeparator = decimalSeparator ?? Culture.NumberFormat.NumberDecimalSeparator;
Culture.NumberFormat.NumberGroupSeparator = numberGroupSeparator ?? Culture.NumberFormat.NumberGroupSeparator;
Culture.NumberFormat.NumberGroupSizes = numberGroupSizes ?? Culture.NumberFormat.NumberGroupSizes;
Culture.NumberFormat.NumberNegativePattern = numberNegativePattern ?? Culture.NumberFormat.NumberNegativePattern;
}
///
/// 获取指定字体下刻度字符串的Size
///
///
///
private (float width, float height) MaxLabelSize(Drawing.Font tickFont)
{
if (tickLabels is null || tickLabels.Length == 0)
return (0, 0);
string largestString = "";
foreach (string s in tickLabels.Where(x => string.IsNullOrEmpty(x) == false))
if (s.Length > largestString.Length)
largestString = s;
if (LabelFormat == TickLabelFormat.DateTime)
{
// widen largest string based on the longest month name
foreach (string s in new DateTimeFormatInfo().MonthGenitiveNames)
{
string s2 = s + "\n" + "1985";
if (s2.Length > largestString.Length)
largestString = s2;
}
}
var maxLabelSize = GDI.MeasureString(largestString.Trim(), tickFont);
return (maxLabelSize.Width, maxLabelSize.Height);
}
///
/// 计算垂直Position决定的AxisLabel
///
///
///
///
///
///
private void RecalculateChannelPositionsAutomaticNumeric(PlotDimensions dims, float labelWidth, float labelHeight, int? forcedTickCount, AxisDimensions axisDimensions)
{
double low, high, tickSpacing;
int maxTickCount;
int interval = axisDimensions.Intreval;
low = dims.YMin;// - 5000;
high = dims.YMax;// 5000;
maxTickCount = (int)(dims.DataHeight / labelHeight * TickDensity);
maxTickCount = forcedTickCount ?? maxTickCount;
tickSpacing = (manualSpacingY != 0) ? manualSpacingY : GetIdealTickSpacing(low, high, maxTickCount, radix);//刻度间距
tickSpacing = Math.Max(tickSpacing, MinimumTickSpacing);
double eachTickOffsetOfOneThousand = (axisDimensions.Position % interval == 0) ? interval : axisDimensions.Position % interval;
eachTickOffsetOfOneThousand = axisDimensions.Position < interval ? axisDimensions.Position : eachTickOffsetOfOneThousand;
tickSpacing = interval;
int tickCount = (int)(axisDimensions.Position % interval) == 0 ? 9 : 10;
tickCount = axisDimensions.Position < interval ? 9 : 10;
tickCount = tickCount > interval ? interval : tickCount;
tickCount = tickCount < 1 ? 1 : tickCount;
tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
.Select(x => low + eachTickOffsetOfOneThousand + tickSpacing * x)
.Where(x => low < x && x < high)
.ToArray();
if (LabelFormat == TickLabelFormat.DateTime)
{
tickLabels = GetDateLabels(tickPositionsMajor, Culture);
tickPositionsMinor = null;
}
else
{
if (MinorTickDistribution == MinorTickDistribution.log)
{
double scale = 0;
if (axisDimensions.Position != axisDimensions.RePosition && axisDimensions.RePosition != 0 && axisDimensions.Position != 0)
{
double logposition = Math.Log10(axisDimensions.Position);
scale = Math.Log10(axisDimensions.Position) - Math.Log10(axisDimensions.RePosition);
}
if (axisDimensions.RePosition == 0 && axisDimensions.Position != 0)
{
axisDimensions.RePosition = axisDimensions.Position;
}
tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
.Select(x => low * Math.Pow(10, x+ scale))
.Where(x => dims.GetPixelY(x) > dims.DataOffsetY && dims.GetPixelY(x) < dims.DataOffsetY + dims.DataHeight)
.ToArray();
string[] labels = new string[tickPositionsMajor.Length];
for (int i = 0; i < tickPositionsMajor.Length; i++)
{
double labelvalue = Math.Pow(10, Math.Log10(tickPositionsMajor[i]) - scale);
labels[i] = new Quantity(labelvalue, axisDimensions.UnitPrefix, axisDimensions.ScaleUnit).ToString();// tickPositionsMajor[i].ToString();
}
tickLabels = labels;
tickPositionsMinor = MinorFromMajorLog(tickPositionsMajor, low, high, dims,true);
//axisDimensions.SetAxis(,);
}
else if(MinorTickDistribution == MinorTickDistribution.even)
{
(tickLabels, CornerLabel) = GetVerticalTickLabels(
tickPositionsMajor,
useMultiplierNotation,
useOffsetNotation,
useExponentialNotation,
invertSign: LabelUsingInvertedSign,
culture: Culture,
axisDimensions
);//CornerLabel为上标,e的多少次方
tickPositionsMinor = MinorFromMajor(tickPositionsMajor, 5, low, high);//每大格中有五个小格子
}
else if(MinorTickDistribution == MinorTickDistribution.EvenAndLog)
{
(tickLabels, CornerLabel) = GetVerticalTickLabels(
tickPositionsMajor,
useMultiplierNotation,
useOffsetNotation,
useExponentialNotation,
invertSign: LabelUsingInvertedSign,
culture: Culture,
axisDimensions,
true
);//CornerLabel为上标,e的多少次方
tickPositionsMinor = MinorFromMajor(tickPositionsMajor, 9, low, high,true);//每大格中有10个小格子
}
}
}
///
/// 计算水平Position决定的AxisLabel
///
///
///
///
///
///
private void RecalculateTimebasePositionsAutomaticNumeric(PlotDimensions dims, float labelWidth, float labelHeight, int? forcedTickCount, AxisDimensions dimensions)
{
double low, high, tickSpacing;
int maxTickCount;
Int32 interval = 1000;
low = dims.XMin;// 0; // add an extra pixel to capture the edge tick
high = dims.XMax;// 10000; // add an extra pixel to capture the edge tick
maxTickCount = (int)(dims.DataWidth / labelWidth * TickDensity);
maxTickCount = forcedTickCount ?? maxTickCount;
tickSpacing = (manualSpacingX != 0) ? manualSpacingX : GetIdealTickSpacing(low, high, maxTickCount, radix);
tickSpacing = Math.Max(tickSpacing, MinimumTickSpacing);
double eachTickOffsetOfOneThousand = (dimensions.Position % interval == 0) ? interval : dimensions.Position % interval;
eachTickOffsetOfOneThousand = dimensions.Position < interval ? dimensions.Position : eachTickOffsetOfOneThousand;
tickSpacing = interval;
int tickCount = (int)(dimensions.Position % interval) == 0 ? 9 : 10;
tickCount = dimensions.Position < interval ? 9 : 10;
tickCount = tickCount > interval ? interval : tickCount;
tickCount = tickCount < 1 ? 1 : tickCount;
if (LabelFormat == TickLabelFormat.DateTime)
{
tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
.Select(x => low + eachTickOffsetOfOneThousand + tickSpacing * x)
.Where(x => low < x && x < high)
.ToArray();
tickLabels = GetDateLabels(tickPositionsMajor, Culture);
tickPositionsMinor = null;
}
else
{
if (MinorTickDistribution == MinorTickDistribution.log)
{
double scale = 0;
if (dimensions.Position != dimensions.RePosition && dimensions.RePosition != 0)
{
double logposition = Math.Log10(dimensions.Position);
scale = Math.Log10(dimensions.Position) - Math.Log10(dimensions.RePosition);
}
if (dimensions.RePosition == 0 && dimensions.Position != 0)
{
dimensions.RePosition = dimensions.Position;
}
tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
.Select(x => low * Math.Pow(10, x + scale))
.Where(x => dims.GetPixelX(x) > dims.DataOffsetX && dims.GetPixelX(x) < dims.DataOffsetX + dims.DataWidth)
.ToArray();
string[] labels = new string[tickPositionsMajor.Length];
for (int i = 0; i < tickPositionsMajor.Length; i++)
{
double labelvalue = Math.Pow(10, Math.Log10(tickPositionsMajor[i]) - scale);
labels[i] = new Quantity(labelvalue, dimensions.UnitPrefix, dimensions.ScaleUnit) .ToString();//tickPositionsMajor[i].ToString();
}
tickLabels = labels;
tickPositionsMinor = MinorFromMajorLog(tickPositionsMajor, low, high,dims,false);
}
else if(MinorTickDistribution == MinorTickDistribution.even)
{
tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
.Select(x => low + eachTickOffsetOfOneThousand + tickSpacing * x)
.Where(x => low < x && x < high)
.ToArray();
(tickLabels, CornerLabel) = GetHorizontalTickLabels(
tickPositionsMajor,
useMultiplierNotation,
useOffsetNotation,
useExponentialNotation,
invertSign: LabelUsingInvertedSign,
culture: Culture,
dimensions
);//CornerLabel为上标,e的多少次方
tickPositionsMinor = MinorFromMajor(tickPositionsMajor, 5, low, high);//每大格中有五个小格子
}
else if(MinorTickDistribution == MinorTickDistribution.EvenAndLog)
{
tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
.Select(x => low + eachTickOffsetOfOneThousand + tickSpacing * x)
.Where(x => low < x && x < high)
.ToArray();
(tickLabels, CornerLabel) = GetHorizontalTickLabels(
tickPositionsMajor,
useMultiplierNotation,
useOffsetNotation,
useExponentialNotation,
invertSign: LabelUsingInvertedSign,
culture: Culture,
dimensions,
true
);//CornerLabel为上标,e的多少次方
tickPositionsMinor = MinorFromMajor(tickPositionsMajor, 9, low, high,true);//每大格中有九个小格子
}
}
}
///
/// 获取垂直标记的label
///
///
///
///
///
///
///
///
///
public (string[], string) GetVerticalTickLabels(
double[] positions,
bool useMultiplierNotation,
bool useOffsetNotation,
bool useExponentialNotation,
bool invertSign,
CultureInfo culture,
AxisDimensions axisDimensions,
bool isEnevAndLog = false
)
{
// given positions returns nicely-formatted labels (with offset and multiplier)
string[] labels = new string[positions.Length];
string cornerLabel = "";
if (positions.Length == 0)
return (labels, cornerLabel);
double range = positions.Last() - positions.First();
double exponent = (int)(Math.Log10(range));
double multiplier = 1;
if (useMultiplierNotation)
{
if (Math.Abs(exponent) > 2)
multiplier = Math.Pow(10, exponent);
}
double offset = 0;
if (useOffsetNotation)
{
offset = positions.First();
if (Math.Abs(offset / range) < 10)
offset = 0;
}
for (int i = 0; i < positions.Length; i++)
{
double adjustedPosition = (positions[i] - offset) / multiplier;
if (invertSign)
adjustedPosition *= -1;
labels[i] = ManualTickFormatter is null ? FormatLocal(adjustedPosition, culture) : ManualTickFormatter(adjustedPosition);
if (labels[i] == "-0")
labels[i] = "0";
}
if (useExponentialNotation)
{
if (multiplier != 1)
cornerLabel += $"e{exponent} ";
if (offset != 0)
cornerLabel += Tools.ScientificNotation(offset);
}
else
{
if (multiplier != 1)
cornerLabel += FormatLocal(multiplier, culture);
if (offset != 0)
cornerLabel += " +" + FormatLocal(offset, culture);
cornerLabel = cornerLabel.Replace("+-", "-");
}
if (isEnevAndLog == true)
{
for (int i = 0; i < positions.Length; i++)
{
double adjustedPosition = -(axisDimensions.Position - positions[i]) / axisDimensions.Intreval;
if (adjustedPosition == 0)
{
adjustedPosition = 0;
}
else
{
adjustedPosition = Math.Pow(axisDimensions.Scale, Math.Abs(adjustedPosition)) * (adjustedPosition < 0 ? -1 : 1);
}
if (labels[i] == "-0")
labels[i] = "0";
labels[i] = new Quantity(adjustedPosition, axisDimensions.UnitPrefix, axisDimensions.ScaleUnit)
.ToString(); ;
}
}
else
{
for (int i = 0; i < positions.Length; i++)
{
double adjustedPosition = -(axisDimensions.Position - positions[i]) / axisDimensions.Intreval * axisDimensions.Scale;
if (labels[i] == "-0")
labels[i] = "0";
labels[i] = new Quantity(adjustedPosition, axisDimensions.UnitPrefix, axisDimensions.ScaleUnit)
.ToString(); ;
}
}
return (labels, cornerLabel);
}
///
/// 获取水平标记的label
///
///
///
///
///
///
///
///
///
public (string[], string) GetHorizontalTickLabels(
double[] positions,
bool useMultiplierNotation,
bool useOffsetNotation,
bool useExponentialNotation,
bool invertSign,
CultureInfo culture,
AxisDimensions axisDimensions,
bool isEvenAndLog = false
)
{
// given positions returns nicely-formatted labels (with offset and multiplier)
string[] labels = new string[positions.Length];
string cornerLabel = "";
if (positions.Length == 0)
return (labels, cornerLabel);
double range = positions.Last() - positions.First();
double exponent = (int)(Math.Log10(range));
double multiplier = 1;
if (useMultiplierNotation)
{
if (Math.Abs(exponent) > 2)
multiplier = Math.Pow(10, exponent);
}
double offset = 0;
if (useOffsetNotation)
{
offset = positions.First();
if (Math.Abs(offset / range) < 10)
offset = 0;
}
for (int i = 0; i < positions.Length; i++)
{
double adjustedPosition = (positions[i] - offset) / multiplier;
if (invertSign)
adjustedPosition *= -1;
labels[i] = ManualTickFormatter is null ? FormatLocal(adjustedPosition, culture) : ManualTickFormatter(adjustedPosition);
if (labels[i] == "-0")
labels[i] = "0";
}
if (useExponentialNotation)
{
if (multiplier != 1)
cornerLabel += $"e{exponent} ";
if (offset != 0)
cornerLabel += Tools.ScientificNotation(offset);
}
else
{
if (multiplier != 1)
cornerLabel += FormatLocal(multiplier, culture);
if (offset != 0)
cornerLabel += " +" + FormatLocal(offset, culture);
cornerLabel = cornerLabel.Replace("+-", "-");
}
if (isEvenAndLog == true)
{
for (int i = 0; i < positions.Length; i++)
{
double adjustedPosition = -(axisDimensions.Position + 5 * axisDimensions.Intreval - positions[i]) / axisDimensions.Intreval;
if (adjustedPosition == 0)
{
adjustedPosition = 0;
}
else
{
adjustedPosition = Math.Pow(axisDimensions.Scale, Math.Abs(adjustedPosition)) * (adjustedPosition < 0 ? -1 : 1);
}
labels[i] = ManualTickFormatter is null ? FormatLocal(adjustedPosition, culture) : ManualTickFormatter(adjustedPosition);
if (labels[i] == "-0")
labels[i] = "0";
labels[i] = new Quantity(adjustedPosition, axisDimensions.UnitPrefix, axisDimensions.ScaleUnit)
.ToString();
}
}
else
{
for (int i = 0; i < positions.Length; i++)
{
double adjustedPosition = -(axisDimensions.Position + 5 * axisDimensions.Intreval - positions[i]) / axisDimensions.Intreval * axisDimensions.Scale;
labels[i] = ManualTickFormatter is null ? FormatLocal(adjustedPosition, culture) : ManualTickFormatter(adjustedPosition);
if (labels[i] == "-0")
labels[i] = "0";
labels[i] = new Quantity(adjustedPosition, axisDimensions.UnitPrefix, axisDimensions.ScaleUnit)
.ToString();
}
}
return (labels, cornerLabel);
}
private void RecalculatePositionsAutomaticDatetime(PlotDimensions dims, float labelWidth, float labelHeight, int? forcedTickCount)
{
double low, high;
int tickCount;
if (MinimumTickSpacing > 0)
throw new InvalidOperationException("minimum tick spacing does not support DateTime ticks");
if (Orientation == AxisOrientation.Vertical)
{
low = dims.YMin - dims.UnitsPerPxY; // add an extra pixel to capture the edge tick
high = dims.YMax + dims.UnitsPerPxY; // add an extra pixel to capture the edge tick
tickCount = (int)(dims.DataHeight / labelHeight * TickDensity);
tickCount = forcedTickCount ?? tickCount;
}
else
{
low = dims.XMin - dims.UnitsPerPxX; // add an extra pixel to capture the edge tick
high = dims.XMax + dims.UnitsPerPxX; // add an extra pixel to capture the edge tick
tickCount = (int)(dims.DataWidth / labelWidth * TickDensity);
tickCount = forcedTickCount ?? tickCount;
}
if (low < high)
{
low = Math.Max(low, new DateTime(0100, 1, 1, 0, 0, 0).ToOADate()); // minimum OADate value
high = Math.Min(high, DateTime.MaxValue.ToOADate());
var dtManualUnits = (Orientation == AxisOrientation.Vertical) ? manualDateTimeSpacingUnitY : manualDateTimeSpacingUnitX;
var dtManualSpacing = (Orientation == AxisOrientation.Vertical) ? manualSpacingY : manualSpacingX;
try
{
DateTime from = DateTime.FromOADate(low);
DateTime to = DateTime.FromOADate(high);
var unitFactory = new DateTimeUnitFactory();
IDateTimeUnit tickUnit = unitFactory.CreateUnit(from, to, Culture, tickCount, dtManualUnits, (int)dtManualSpacing);
(tickPositionsMajor, tickLabels) = tickUnit.GetTicksAndLabels(from, to, dateTimeFormatString);
tickLabels = tickLabels.Select(x => x.Trim()).ToArray();
}
catch
{
tickPositionsMajor = new double[] { }; // far zoom out can produce FromOADate() exception
}
}
else
{
tickPositionsMajor = new double[] { };
}
// dont forget to set all the things
tickPositionsMinor = null;
CornerLabel = null;
}
private void RecalculatePositionsAutomaticNumeric(PlotDimensions dims, float labelWidth, float labelHeight, int? forcedTickCount)
{
double low, high, tickSpacing;
int maxTickCount;
if (Orientation == AxisOrientation.Vertical)
{
low = dims.YMin - dims.UnitsPerPxY; // add an extra pixel to capture the edge tick
high = dims.YMax + dims.UnitsPerPxY; // add an extra pixel to capture the edge tick
maxTickCount = (int)(dims.DataHeight / labelHeight * TickDensity);
maxTickCount = forcedTickCount ?? maxTickCount;
tickSpacing = (manualSpacingY != 0) ? manualSpacingY : GetIdealTickSpacing(low, high, maxTickCount, radix);//刻度间距
tickSpacing = Math.Max(tickSpacing, MinimumTickSpacing);
}
else
{
low = dims.XMin - dims.UnitsPerPxX; // add an extra pixel to capture the edge tick
high = dims.XMax + dims.UnitsPerPxX; // add an extra pixel to capture the edge tick
maxTickCount = (int)(dims.DataWidth / labelWidth * TickDensity);
maxTickCount = forcedTickCount ?? maxTickCount;
tickSpacing = (manualSpacingX != 0) ? manualSpacingX : GetIdealTickSpacing(low, high, maxTickCount, radix);
tickSpacing = Math.Max(tickSpacing, MinimumTickSpacing);
}
// now that tick spacing is known, populate the list of ticks and labels
double firstTickOffset = low % tickSpacing;
int tickCount = (int)((high - low) / tickSpacing) + 2;
tickCount = tickCount > 1000 ? 1000 : tickCount;
tickCount = tickCount < 1 ? 1 : tickCount;
tickPositionsMajor = Enumerable.Range(0, tickCount)//大格子
.Select(x => low - firstTickOffset + tickSpacing * x)
.Where(x => low <= x && x <= high)
.ToArray();
if (LabelFormat == TickLabelFormat.DateTime)
{
tickLabels = GetDateLabels(tickPositionsMajor, Culture);
tickPositionsMinor = null;
}
else
{
(tickLabels, CornerLabel) = GetPrettyTickLabels(
tickPositionsMajor,
useMultiplierNotation,
useOffsetNotation,
useExponentialNotation,
invertSign: LabelUsingInvertedSign,
culture: Culture
);//CornerLabel为上标,e的多少次方
if (MinorTickDistribution == MinorTickDistribution.log)
tickPositionsMinor = MinorFromMajorLog(tickPositionsMajor, low, high,dims, Orientation == AxisOrientation.Vertical);
else
tickPositionsMinor = MinorFromMajor(tickPositionsMajor, 5, low, high);//每大格中有五个小格子
}
}
public (string[], string) GetPrettyTickLabels(
double[] positions,
bool useMultiplierNotation,
bool useOffsetNotation,
bool useExponentialNotation,
bool invertSign,
CultureInfo culture
)
{
// given positions returns nicely-formatted labels (with offset and multiplier)
string[] labels = new string[positions.Length];
string cornerLabel = "";
if (positions.Length == 0)
return (labels, cornerLabel);
double range = positions.Last() - positions.First();
double exponent = (int)(Math.Log10(range));
double multiplier = 1;
if (useMultiplierNotation)
{
if (Math.Abs(exponent) > 2)
multiplier = Math.Pow(10, exponent);
}
double offset = 0;
if (useOffsetNotation)
{
offset = positions.First();
if (Math.Abs(offset / range) < 10)
offset = 0;
}
for (int i = 0; i < positions.Length; i++)
{
double adjustedPosition = (positions[i] - offset) / multiplier;
if (invertSign)
adjustedPosition *= -1;
labels[i] = ManualTickFormatter is null ? FormatLocal(adjustedPosition, culture) : ManualTickFormatter(adjustedPosition);
if (labels[i] == "-0")
labels[i] = "0";
}
if (useExponentialNotation)
{
if (multiplier != 1)
cornerLabel += $"e{exponent} ";
if (offset != 0)
cornerLabel += Tools.ScientificNotation(offset);
}
else
{
if (multiplier != 1)
cornerLabel += FormatLocal(multiplier, culture);
if (offset != 0)
cornerLabel += " +" + FormatLocal(offset, culture);
cornerLabel = cornerLabel.Replace("+-", "-");
}
return (labels, cornerLabel);
}
public override string ToString()
{
string allTickLabels = string.Join(", ", tickLabels);
return $"Tick Collection: [{allTickLabels}] {CornerLabel}";
}
private static double GetIdealTickSpacing(double low, double high, int maxTickCount, int radix = 10)
{
double range = high - low;
int exponent = (int)Math.Log(range, radix);
List tickSpacings = new List() { Math.Pow(radix, exponent) };
tickSpacings.Add(tickSpacings.Last());
tickSpacings.Add(tickSpacings.Last());
double[] divBy;
if (radix == 10)
divBy = new double[] { 2, 2, 2.5 }; // 10, 5, 2.5, 1
else if (radix == 16)
divBy = new double[] { 2, 2, 2, 2 }; // 16, 8, 4, 2, 1
else
throw new ArgumentException($"radix {radix} is not supported");
int divisions = 0;
int tickCount = 0;
while ((tickCount < maxTickCount) && (tickSpacings.Count < 1000))
{
tickSpacings.Add(tickSpacings.Last() / divBy[divisions++ % divBy.Length]);
tickCount = (int)(range / tickSpacings.Last());
}
return tickSpacings[tickSpacings.Count - 3];
}
private string FormatLocal(double value, CultureInfo culture)
{
// if a custom format string exists use it
if (numericFormatString != null)
return value.ToString(numericFormatString, culture);
// if the number is round or large, use the numeric format
// https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#the-numeric-n-format-specifier
bool isRoundNumber = ((int)value == value);
bool isLargeNumber = (Math.Abs(value) > 1000);
if (isRoundNumber || isLargeNumber)
return value.ToString("N0", culture);
// otherwise the number is probably small or very precise to use the general format (with slight rounding)
// https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#the-general-g-format-specifier
return Math.Round(value, 10).ToString("G", culture);
}
public double[] MinorFromMajor(double[] majorTicks, double minorTicksPerMajorTick, double lowerLimit, double upperLimit,bool isEnevAndLog =false)
{
if ((majorTicks == null) || (majorTicks.Length < 2))
return null;
double majorTickSpacing = majorTicks[1] - majorTicks[0];
double minorTickSpacing = majorTickSpacing / minorTicksPerMajorTick;
List majorTicksWithPadding = new List();
majorTicksWithPadding.Add(majorTicks[0] - majorTickSpacing);//未满一格的长度
majorTicksWithPadding.AddRange(majorTicks);//各个格子的长度
List minorTicks = new List();
if (isEnevAndLog == true)
{
foreach (var majorTickPosition in majorTicksWithPadding)
{
for (int i = 1; i < minorTicksPerMajorTick; i++)
{
double minorTickPosition = majorTickPosition + majorTickSpacing * Math.Log10(i+1);
if ((minorTickPosition > lowerLimit) && (minorTickPosition < upperLimit))
minorTicks.Add(minorTickPosition);
}
}
}
else
{
foreach (var majorTickPosition in majorTicksWithPadding)
{
for (int i = 1; i < minorTicksPerMajorTick; i++)
{
double minorTickPosition = majorTickPosition + minorTickSpacing * i;
if ((minorTickPosition > lowerLimit) && (minorTickPosition < upperLimit))
minorTicks.Add(minorTickPosition);
}
}
}
return minorTicks.ToArray();//小刻度
}
public double[] MinorFromMajorLog(double[] majorTicks, double lowerLimit, double upperLimit, PlotDimensions dims,Boolean isInverted)
{
if ((majorTicks == null) || (majorTicks.Length < 2))
return null;
List minorTicks = new List();
for (int i = 0; i < majorTicks.Length; i++)
{
var yValueMax = (float)Math.Log10(majorTicks[i]);
var yValueMin = (float)Math.Log10(majorTicks[i]/10);
for (int j = 1; j < 9; j++)
{
minorTicks.Add(majorTicks[i] / 10 + (majorTicks[i] / 10 * j));
}
}
for (int j = 1; j < 9; j++)
{
minorTicks.Add(majorTicks[majorTicks.Length-1] + (majorTicks[majorTicks.Length-1] * j));
}
if(isInverted)
return minorTicks.Where(x => dims.GetPixelY(x) > dims.DataOffsetY && dims.GetPixelY(x) < dims.DataOffsetY + dims.DataHeight).ToArray();
else
return minorTicks.Where(x => dims.GetPixelX(x) > dims.DataOffsetX && dims.GetPixelX(x) < dims.DataOffsetX + dims.DataWidth).ToArray();
}
//public double[] MinorFromMajorLog(double[] majorTicks, double lowerLimit, double upperLimit)
//{
// if ((majorTicks == null) || (majorTicks.Length < 2))
// return null;
// double majorTickSpacing = majorTicks[1] - majorTicks[0];
// double lowerBound = majorTicks.First() - majorTickSpacing;
// double upperBound = majorTicks.Last() + majorTickSpacing;
// List minorTicks = new List();
// for (double majorTick = lowerBound; majorTick <= upperBound; majorTick += majorTickSpacing)
// {
// minorTicks.Add(majorTick + majorTickSpacing * (.5));
// minorTicks.Add(majorTick + majorTickSpacing * (.5 + .25));
// minorTicks.Add(majorTick + majorTickSpacing * (.5 + .25 + .125));
// minorTicks.Add(majorTick + majorTickSpacing * (.5 + .25 + .125 + .0625));
// }
// return minorTicks.Where(x => x >= lowerLimit && x <= upperLimit).ToArray();
//}
public static string[] GetDateLabels(double[] ticksOADate, CultureInfo culture)
{
TimeSpan dtTickSep;
string dtFmt = null;
try
{
// TODO: replace this with culture-aware format
dtTickSep = DateTime.FromOADate(ticksOADate[1]) - DateTime.FromOADate(ticksOADate[0]);
if (dtTickSep.TotalDays > 365 * 5)
dtFmt = "{0:yyyy}";
else if (dtTickSep.TotalDays > 365)
dtFmt = "{0:yyyy-MM}";
else if (dtTickSep.TotalDays > .5)
dtFmt = "{0:yyyy-MM-dd}";
else if (dtTickSep.TotalMinutes > .5)
dtFmt = "{0:yyyy-MM-dd\nH:mm}";
else
dtFmt = "{0:yyyy-MM-dd\nH:mm:ss}";
}
catch
{
}
string[] labels = new string[ticksOADate.Length];
for (int i = 0; i < ticksOADate.Length; i++)
{
try
{
DateTime dt = DateTime.FromOADate(ticksOADate[i]);
string lbl = string.Format(culture, dtFmt, dt);
labels[i] = lbl;
}
catch
{
labels[i] = "?";
}
}
return labels;
}
}
}