123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979 |
- 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;
- /// <summary>
- /// Label to show in the corner when using multiplier or offset notation
- /// </summary>
- public string CornerLabel { get; private set; }
- /// <summary>
- /// Measured size of the largest tick label
- /// </summary>
- public float LargestLabelWidth { get; private set; } = 15;
- /// <summary>
- /// Measured size of the largest tick label
- /// </summary>
- public float LargestLabelHeight { get; private set; } = 12;
- /// <summary>
- /// Controls how to translate positions to strings
- /// </summary>
- public TickLabelFormat LabelFormat = TickLabelFormat.Numeric;
- /// <summary>
- /// 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.
- /// </summary>
- public AxisOrientation Orientation;
- /// <summary>
- /// If True, the sign of numeric tick labels will be inverted.
- /// This is used to give the appearance of descending ticks.
- /// </summary>
- public bool LabelUsingInvertedSign;
- /// <summary>
- /// Define how minor ticks are distributed (evenly vs. log scale)
- /// </summary>
- public MinorTickDistribution MinorTickDistribution;
- public string numericFormatString;
- public string dateTimeFormatString;
- /// <summary>
- /// If defined, this function will be used to generate tick labels from positions
- /// </summary>
- public Func<double, string> 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;
- /// <summary>
- /// Optimally packed tick labels have a density 1.0 and lower densities space ticks farther apart.
- /// </summary>
- public float TickDensity = 1.0f;
- /// <summary>
- /// Defines the minimum distance (in coordinate units) for major ticks.
- /// </summary>
- public double MinimumTickSpacing = 0;
- /// <summary>
- /// 计算标记、刻度位置
- /// </summary>
- /// <param name="dims"></param>
- /// <param name="tickFont"></param>
- /// <param name="dimensions"></param>
- 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;
- }
- /// <summary>
- /// 获取指定字体下刻度字符串的Size
- /// </summary>
- /// <param name="tickFont"></param>
- /// <returns></returns>
- 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);
- }
- /// <summary>
- /// 计算垂直Position决定的AxisLabel
- /// </summary>
- /// <param name="dims"></param>
- /// <param name="labelWidth"></param>
- /// <param name="labelHeight"></param>
- /// <param name="forcedTickCount"></param>
- /// <param name="axisDimensions"></param>
- 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个小格子
- }
- }
- }
- /// <summary>
- /// 计算水平Position决定的AxisLabel
- /// </summary>
- /// <param name="dims"></param>
- /// <param name="labelWidth"></param>
- /// <param name="labelHeight"></param>
- /// <param name="forcedTickCount"></param>
- /// <param name="dimensions"></param>
- 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);//每大格中有九个小格子
- }
- }
- }
- /// <summary>
- /// 获取垂直标记的label
- /// </summary>
- /// <param name="positions"></param>
- /// <param name="useMultiplierNotation"></param>
- /// <param name="useOffsetNotation"></param>
- /// <param name="useExponentialNotation"></param>
- /// <param name="invertSign"></param>
- /// <param name="culture"></param>
- /// <param name="axisDimensions"></param>
- /// <returns></returns>
- 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);
- }
- /// <summary>
- /// 获取水平标记的label
- /// </summary>
- /// <param name="positions"></param>
- /// <param name="useMultiplierNotation"></param>
- /// <param name="useOffsetNotation"></param>
- /// <param name="useExponentialNotation"></param>
- /// <param name="invertSign"></param>
- /// <param name="culture"></param>
- /// <param name="axisDimensions"></param>
- /// <returns></returns>
- 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<double> tickSpacings = new List<double>() { 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<double> majorTicksWithPadding = new List<double>();
- majorTicksWithPadding.Add(majorTicks[0] - majorTickSpacing);//未满一格的长度
- majorTicksWithPadding.AddRange(majorTicks);//各个格子的长度
- List<double> minorTicks = new List<double>();
- 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<double> minorTicks = new List<double>();
- 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<double> minorTicks = new List<double>();
- // 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;
- }
- }
- }
|