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