TickCollection.cs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979
  1. using ScottPlot.Drawing;
  2. using ScottPlot.Renderable;
  3. using ScottPlot.Ticks.DateTimeTickUnits;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Globalization;
  7. using System.Linq;
  8. namespace ScottPlot.Ticks
  9. {
  10. public enum TickLabelFormat { Numeric, DateTime }; // TODO: add hex, binary, scientific notation, etc?
  11. public enum AxisOrientation { Vertical, Horizontal };
  12. public enum MinorTickDistribution { even, log,EvenAndLog };
  13. public class TickCollection
  14. {
  15. // This class creates pretty tick labels (with offset and exponent) uses graph settings
  16. // to inspect the tick font and ensure tick labels will not overlap.
  17. // It also respects manually defined tick spacing settings set via plt.Grid().
  18. // TODO: store these in a class
  19. public double[] tickPositionsMajor;
  20. public double[] tickPositionsMinor;
  21. public string[] tickLabels;
  22. public double[] manualTickPositions;
  23. public string[] manualTickLabels;
  24. /// <summary>
  25. /// Label to show in the corner when using multiplier or offset notation
  26. /// </summary>
  27. public string CornerLabel { get; private set; }
  28. /// <summary>
  29. /// Measured size of the largest tick label
  30. /// </summary>
  31. public float LargestLabelWidth { get; private set; } = 15;
  32. /// <summary>
  33. /// Measured size of the largest tick label
  34. /// </summary>
  35. public float LargestLabelHeight { get; private set; } = 12;
  36. /// <summary>
  37. /// Controls how to translate positions to strings
  38. /// </summary>
  39. public TickLabelFormat LabelFormat = TickLabelFormat.Numeric;
  40. /// <summary>
  41. /// If True, these ticks are placed along a vertical (Y) axis.
  42. /// This is used to determine whether tick density should be based on tick label width or height.
  43. /// </summary>
  44. public AxisOrientation Orientation;
  45. /// <summary>
  46. /// If True, the sign of numeric tick labels will be inverted.
  47. /// This is used to give the appearance of descending ticks.
  48. /// </summary>
  49. public bool LabelUsingInvertedSign;
  50. /// <summary>
  51. /// Define how minor ticks are distributed (evenly vs. log scale)
  52. /// </summary>
  53. public MinorTickDistribution MinorTickDistribution;
  54. public string numericFormatString;
  55. public string dateTimeFormatString;
  56. /// <summary>
  57. /// If defined, this function will be used to generate tick labels from positions
  58. /// </summary>
  59. public Func<double, string> ManualTickFormatter = null;
  60. public int radix = 10;
  61. public string prefix = null;
  62. public double manualSpacingX = 0;
  63. public double manualSpacingY = 0;
  64. public Ticks.DateTimeUnit? manualDateTimeSpacingUnitX = null;
  65. public Ticks.DateTimeUnit? manualDateTimeSpacingUnitY = null;
  66. public CultureInfo Culture = CultureInfo.DefaultThreadCurrentCulture;
  67. public bool useMultiplierNotation = false;
  68. public bool useOffsetNotation = false;
  69. public bool useExponentialNotation = true;
  70. /// <summary>
  71. /// Optimally packed tick labels have a density 1.0 and lower densities space ticks farther apart.
  72. /// </summary>
  73. public float TickDensity = 1.0f;
  74. /// <summary>
  75. /// Defines the minimum distance (in coordinate units) for major ticks.
  76. /// </summary>
  77. public double MinimumTickSpacing = 0;
  78. /// <summary>
  79. /// 计算标记、刻度位置
  80. /// </summary>
  81. /// <param name="dims"></param>
  82. /// <param name="tickFont"></param>
  83. /// <param name="dimensions"></param>
  84. public void Recalculate(PlotDimensions dims, Drawing.Font tickFont, AxisDimensions dimensions)
  85. {
  86. if (manualTickPositions is null)
  87. {
  88. if (Orientation==AxisOrientation.Vertical)
  89. {
  90. RecalculateChannelPositionsAutomaticNumeric(dims, 15, 12, (int)(10 * TickDensity), dimensions);
  91. (LargestLabelWidth, LargestLabelHeight) = MaxLabelSize(tickFont);
  92. RecalculateChannelPositionsAutomaticNumeric(dims, LargestLabelWidth, LargestLabelHeight, null, dimensions);
  93. }
  94. else
  95. {
  96. RecalculateTimebasePositionsAutomaticNumeric(dims, 15, 12, (int)(10 * TickDensity), dimensions);
  97. (LargestLabelWidth, LargestLabelHeight) = MaxLabelSize(tickFont);
  98. RecalculateTimebasePositionsAutomaticNumeric(dims, LargestLabelWidth, LargestLabelHeight, null, dimensions);
  99. }
  100. }
  101. else
  102. { //手动标记
  103. double min = Orientation == AxisOrientation.Vertical ? dims.YMin : dims.XMin;
  104. double max = Orientation == AxisOrientation.Vertical ? dims.YMax : dims.XMax;
  105. var visibleIndexes = Enumerable.Range(0, manualTickPositions.Count())
  106. .Where(i => manualTickPositions[i] >= min)
  107. .Where(i => manualTickPositions[i] <= max);
  108. tickPositionsMajor = visibleIndexes.Select(x => manualTickPositions[x]).ToArray();
  109. tickPositionsMinor = null;
  110. tickLabels = visibleIndexes.Select(x => manualTickLabels[x]).ToArray();
  111. CornerLabel = null;
  112. (LargestLabelWidth, LargestLabelHeight) = MaxLabelSize(tickFont);
  113. }
  114. }
  115. public void SetCulture(
  116. string shortDatePattern = null,
  117. string decimalSeparator = null,
  118. string numberGroupSeparator = null,
  119. int? decimalDigits = null,
  120. int? numberNegativePattern = null,
  121. int[] numberGroupSizes = null
  122. )
  123. {
  124. // Culture may be null if the thread culture is the same is the system culture.
  125. // If it is null, assigning it to a clone of the current culture solves this and also makes it mutable.
  126. Culture = Culture ?? (CultureInfo)CultureInfo.CurrentCulture.Clone();
  127. Culture.DateTimeFormat.ShortDatePattern = shortDatePattern ?? Culture.DateTimeFormat.ShortDatePattern;
  128. Culture.NumberFormat.NumberDecimalDigits = decimalDigits ?? Culture.NumberFormat.NumberDecimalDigits;
  129. Culture.NumberFormat.NumberDecimalSeparator = decimalSeparator ?? Culture.NumberFormat.NumberDecimalSeparator;
  130. Culture.NumberFormat.NumberGroupSeparator = numberGroupSeparator ?? Culture.NumberFormat.NumberGroupSeparator;
  131. Culture.NumberFormat.NumberGroupSizes = numberGroupSizes ?? Culture.NumberFormat.NumberGroupSizes;
  132. Culture.NumberFormat.NumberNegativePattern = numberNegativePattern ?? Culture.NumberFormat.NumberNegativePattern;
  133. }
  134. /// <summary>
  135. /// 获取指定字体下刻度字符串的Size
  136. /// </summary>
  137. /// <param name="tickFont"></param>
  138. /// <returns></returns>
  139. private (float width, float height) MaxLabelSize(Drawing.Font tickFont)
  140. {
  141. if (tickLabels is null || tickLabels.Length == 0)
  142. return (0, 0);
  143. string largestString = "";
  144. foreach (string s in tickLabels.Where(x => string.IsNullOrEmpty(x) == false))
  145. if (s.Length > largestString.Length)
  146. largestString = s;
  147. if (LabelFormat == TickLabelFormat.DateTime)
  148. {
  149. // widen largest string based on the longest month name
  150. foreach (string s in new DateTimeFormatInfo().MonthGenitiveNames)
  151. {
  152. string s2 = s + "\n" + "1985";
  153. if (s2.Length > largestString.Length)
  154. largestString = s2;
  155. }
  156. }
  157. var maxLabelSize = GDI.MeasureString(largestString.Trim(), tickFont);
  158. return (maxLabelSize.Width, maxLabelSize.Height);
  159. }
  160. /// <summary>
  161. /// 计算垂直Position决定的AxisLabel
  162. /// </summary>
  163. /// <param name="dims"></param>
  164. /// <param name="labelWidth"></param>
  165. /// <param name="labelHeight"></param>
  166. /// <param name="forcedTickCount"></param>
  167. /// <param name="axisDimensions"></param>
  168. private void RecalculateChannelPositionsAutomaticNumeric(PlotDimensions dims, float labelWidth, float labelHeight, int? forcedTickCount, AxisDimensions axisDimensions)
  169. {
  170. double low, high, tickSpacing;
  171. int maxTickCount;
  172. int interval = axisDimensions.Intreval;
  173. low = dims.YMin;// - 5000;
  174. high = dims.YMax;// 5000;
  175. maxTickCount = (int)(dims.DataHeight / labelHeight * TickDensity);
  176. maxTickCount = forcedTickCount ?? maxTickCount;
  177. tickSpacing = (manualSpacingY != 0) ? manualSpacingY : GetIdealTickSpacing(low, high, maxTickCount, radix);//刻度间距
  178. tickSpacing = Math.Max(tickSpacing, MinimumTickSpacing);
  179. double eachTickOffsetOfOneThousand = (axisDimensions.Position % interval == 0) ? interval : axisDimensions.Position % interval;
  180. eachTickOffsetOfOneThousand = axisDimensions.Position < interval ? axisDimensions.Position : eachTickOffsetOfOneThousand;
  181. tickSpacing = interval;
  182. int tickCount = (int)(axisDimensions.Position % interval) == 0 ? 9 : 10;
  183. tickCount = axisDimensions.Position < interval ? 9 : 10;
  184. tickCount = tickCount > interval ? interval : tickCount;
  185. tickCount = tickCount < 1 ? 1 : tickCount;
  186. tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
  187. .Select(x => low + eachTickOffsetOfOneThousand + tickSpacing * x)
  188. .Where(x => low < x && x < high)
  189. .ToArray();
  190. if (LabelFormat == TickLabelFormat.DateTime)
  191. {
  192. tickLabels = GetDateLabels(tickPositionsMajor, Culture);
  193. tickPositionsMinor = null;
  194. }
  195. else
  196. {
  197. if (MinorTickDistribution == MinorTickDistribution.log)
  198. {
  199. double scale = 0;
  200. if (axisDimensions.Position != axisDimensions.RePosition && axisDimensions.RePosition != 0 && axisDimensions.Position != 0)
  201. {
  202. double logposition = Math.Log10(axisDimensions.Position);
  203. scale = Math.Log10(axisDimensions.Position) - Math.Log10(axisDimensions.RePosition);
  204. }
  205. if (axisDimensions.RePosition == 0 && axisDimensions.Position != 0)
  206. {
  207. axisDimensions.RePosition = axisDimensions.Position;
  208. }
  209. tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
  210. .Select(x => low * Math.Pow(10, x+ scale))
  211. .Where(x => dims.GetPixelY(x) > dims.DataOffsetY && dims.GetPixelY(x) < dims.DataOffsetY + dims.DataHeight)
  212. .ToArray();
  213. string[] labels = new string[tickPositionsMajor.Length];
  214. for (int i = 0; i < tickPositionsMajor.Length; i++)
  215. {
  216. double labelvalue = Math.Pow(10, Math.Log10(tickPositionsMajor[i]) - scale);
  217. labels[i] = new Quantity(labelvalue, axisDimensions.UnitPrefix, axisDimensions.ScaleUnit).ToString();// tickPositionsMajor[i].ToString();
  218. }
  219. tickLabels = labels;
  220. tickPositionsMinor = MinorFromMajorLog(tickPositionsMajor, low, high, dims,true);
  221. //axisDimensions.SetAxis(,);
  222. }
  223. else if(MinorTickDistribution == MinorTickDistribution.even)
  224. {
  225. (tickLabels, CornerLabel) = GetVerticalTickLabels(
  226. tickPositionsMajor,
  227. useMultiplierNotation,
  228. useOffsetNotation,
  229. useExponentialNotation,
  230. invertSign: LabelUsingInvertedSign,
  231. culture: Culture,
  232. axisDimensions
  233. );//CornerLabel为上标,e的多少次方
  234. tickPositionsMinor = MinorFromMajor(tickPositionsMajor, 5, low, high);//每大格中有五个小格子
  235. }
  236. else if(MinorTickDistribution == MinorTickDistribution.EvenAndLog)
  237. {
  238. (tickLabels, CornerLabel) = GetVerticalTickLabels(
  239. tickPositionsMajor,
  240. useMultiplierNotation,
  241. useOffsetNotation,
  242. useExponentialNotation,
  243. invertSign: LabelUsingInvertedSign,
  244. culture: Culture,
  245. axisDimensions,
  246. true
  247. );//CornerLabel为上标,e的多少次方
  248. tickPositionsMinor = MinorFromMajor(tickPositionsMajor, 9, low, high,true);//每大格中有10个小格子
  249. }
  250. }
  251. }
  252. /// <summary>
  253. /// 计算水平Position决定的AxisLabel
  254. /// </summary>
  255. /// <param name="dims"></param>
  256. /// <param name="labelWidth"></param>
  257. /// <param name="labelHeight"></param>
  258. /// <param name="forcedTickCount"></param>
  259. /// <param name="dimensions"></param>
  260. private void RecalculateTimebasePositionsAutomaticNumeric(PlotDimensions dims, float labelWidth, float labelHeight, int? forcedTickCount, AxisDimensions dimensions)
  261. {
  262. double low, high, tickSpacing;
  263. int maxTickCount;
  264. Int32 interval = 1000;
  265. low = dims.XMin;// 0; // add an extra pixel to capture the edge tick
  266. high = dims.XMax;// 10000; // add an extra pixel to capture the edge tick
  267. maxTickCount = (int)(dims.DataWidth / labelWidth * TickDensity);
  268. maxTickCount = forcedTickCount ?? maxTickCount;
  269. tickSpacing = (manualSpacingX != 0) ? manualSpacingX : GetIdealTickSpacing(low, high, maxTickCount, radix);
  270. tickSpacing = Math.Max(tickSpacing, MinimumTickSpacing);
  271. double eachTickOffsetOfOneThousand = (dimensions.Position % interval == 0) ? interval : dimensions.Position % interval;
  272. eachTickOffsetOfOneThousand = dimensions.Position < interval ? dimensions.Position : eachTickOffsetOfOneThousand;
  273. tickSpacing = interval;
  274. int tickCount = (int)(dimensions.Position % interval) == 0 ? 9 : 10;
  275. tickCount = dimensions.Position < interval ? 9 : 10;
  276. tickCount = tickCount > interval ? interval : tickCount;
  277. tickCount = tickCount < 1 ? 1 : tickCount;
  278. if (LabelFormat == TickLabelFormat.DateTime)
  279. {
  280. tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
  281. .Select(x => low + eachTickOffsetOfOneThousand + tickSpacing * x)
  282. .Where(x => low < x && x < high)
  283. .ToArray();
  284. tickLabels = GetDateLabels(tickPositionsMajor, Culture);
  285. tickPositionsMinor = null;
  286. }
  287. else
  288. {
  289. if (MinorTickDistribution == MinorTickDistribution.log)
  290. {
  291. double scale = 0;
  292. if (dimensions.Position != dimensions.RePosition && dimensions.RePosition != 0)
  293. {
  294. double logposition = Math.Log10(dimensions.Position);
  295. scale = Math.Log10(dimensions.Position) - Math.Log10(dimensions.RePosition);
  296. }
  297. if (dimensions.RePosition == 0 && dimensions.Position != 0)
  298. {
  299. dimensions.RePosition = dimensions.Position;
  300. }
  301. tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
  302. .Select(x => low * Math.Pow(10, x + scale))
  303. .Where(x => dims.GetPixelX(x) > dims.DataOffsetX && dims.GetPixelX(x) < dims.DataOffsetX + dims.DataWidth)
  304. .ToArray();
  305. string[] labels = new string[tickPositionsMajor.Length];
  306. for (int i = 0; i < tickPositionsMajor.Length; i++)
  307. {
  308. double labelvalue = Math.Pow(10, Math.Log10(tickPositionsMajor[i]) - scale);
  309. labels[i] = new Quantity(labelvalue, dimensions.UnitPrefix, dimensions.ScaleUnit) .ToString();//tickPositionsMajor[i].ToString();
  310. }
  311. tickLabels = labels;
  312. tickPositionsMinor = MinorFromMajorLog(tickPositionsMajor, low, high,dims,false);
  313. }
  314. else if(MinorTickDistribution == MinorTickDistribution.even)
  315. {
  316. tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
  317. .Select(x => low + eachTickOffsetOfOneThousand + tickSpacing * x)
  318. .Where(x => low < x && x < high)
  319. .ToArray();
  320. (tickLabels, CornerLabel) = GetHorizontalTickLabels(
  321. tickPositionsMajor,
  322. useMultiplierNotation,
  323. useOffsetNotation,
  324. useExponentialNotation,
  325. invertSign: LabelUsingInvertedSign,
  326. culture: Culture,
  327. dimensions
  328. );//CornerLabel为上标,e的多少次方
  329. tickPositionsMinor = MinorFromMajor(tickPositionsMajor, 5, low, high);//每大格中有五个小格子
  330. }
  331. else if(MinorTickDistribution == MinorTickDistribution.EvenAndLog)
  332. {
  333. tickPositionsMajor = Enumerable.Range(-22, tickCount + 124)//大格子
  334. .Select(x => low + eachTickOffsetOfOneThousand + tickSpacing * x)
  335. .Where(x => low < x && x < high)
  336. .ToArray();
  337. (tickLabels, CornerLabel) = GetHorizontalTickLabels(
  338. tickPositionsMajor,
  339. useMultiplierNotation,
  340. useOffsetNotation,
  341. useExponentialNotation,
  342. invertSign: LabelUsingInvertedSign,
  343. culture: Culture,
  344. dimensions,
  345. true
  346. );//CornerLabel为上标,e的多少次方
  347. tickPositionsMinor = MinorFromMajor(tickPositionsMajor, 9, low, high,true);//每大格中有九个小格子
  348. }
  349. }
  350. }
  351. /// <summary>
  352. /// 获取垂直标记的label
  353. /// </summary>
  354. /// <param name="positions"></param>
  355. /// <param name="useMultiplierNotation"></param>
  356. /// <param name="useOffsetNotation"></param>
  357. /// <param name="useExponentialNotation"></param>
  358. /// <param name="invertSign"></param>
  359. /// <param name="culture"></param>
  360. /// <param name="axisDimensions"></param>
  361. /// <returns></returns>
  362. public (string[], string) GetVerticalTickLabels(
  363. double[] positions,
  364. bool useMultiplierNotation,
  365. bool useOffsetNotation,
  366. bool useExponentialNotation,
  367. bool invertSign,
  368. CultureInfo culture,
  369. AxisDimensions axisDimensions,
  370. bool isEnevAndLog = false
  371. )
  372. {
  373. // given positions returns nicely-formatted labels (with offset and multiplier)
  374. string[] labels = new string[positions.Length];
  375. string cornerLabel = "";
  376. if (positions.Length == 0)
  377. return (labels, cornerLabel);
  378. double range = positions.Last() - positions.First();
  379. double exponent = (int)(Math.Log10(range));
  380. double multiplier = 1;
  381. if (useMultiplierNotation)
  382. {
  383. if (Math.Abs(exponent) > 2)
  384. multiplier = Math.Pow(10, exponent);
  385. }
  386. double offset = 0;
  387. if (useOffsetNotation)
  388. {
  389. offset = positions.First();
  390. if (Math.Abs(offset / range) < 10)
  391. offset = 0;
  392. }
  393. for (int i = 0; i < positions.Length; i++)
  394. {
  395. double adjustedPosition = (positions[i] - offset) / multiplier;
  396. if (invertSign)
  397. adjustedPosition *= -1;
  398. labels[i] = ManualTickFormatter is null ? FormatLocal(adjustedPosition, culture) : ManualTickFormatter(adjustedPosition);
  399. if (labels[i] == "-0")
  400. labels[i] = "0";
  401. }
  402. if (useExponentialNotation)
  403. {
  404. if (multiplier != 1)
  405. cornerLabel += $"e{exponent} ";
  406. if (offset != 0)
  407. cornerLabel += Tools.ScientificNotation(offset);
  408. }
  409. else
  410. {
  411. if (multiplier != 1)
  412. cornerLabel += FormatLocal(multiplier, culture);
  413. if (offset != 0)
  414. cornerLabel += " +" + FormatLocal(offset, culture);
  415. cornerLabel = cornerLabel.Replace("+-", "-");
  416. }
  417. if (isEnevAndLog == true)
  418. {
  419. for (int i = 0; i < positions.Length; i++)
  420. {
  421. double adjustedPosition = -(axisDimensions.Position - positions[i]) / axisDimensions.Intreval;
  422. if (adjustedPosition == 0)
  423. {
  424. adjustedPosition = 0;
  425. }
  426. else
  427. {
  428. adjustedPosition = Math.Pow(axisDimensions.Scale, Math.Abs(adjustedPosition)) * (adjustedPosition < 0 ? -1 : 1);
  429. }
  430. if (labels[i] == "-0")
  431. labels[i] = "0";
  432. labels[i] = new Quantity(adjustedPosition, axisDimensions.UnitPrefix, axisDimensions.ScaleUnit)
  433. .ToString(); ;
  434. }
  435. }
  436. else
  437. {
  438. for (int i = 0; i < positions.Length; i++)
  439. {
  440. double adjustedPosition = -(axisDimensions.Position - positions[i]) / axisDimensions.Intreval * axisDimensions.Scale;
  441. if (labels[i] == "-0")
  442. labels[i] = "0";
  443. labels[i] = new Quantity(adjustedPosition, axisDimensions.UnitPrefix, axisDimensions.ScaleUnit)
  444. .ToString(); ;
  445. }
  446. }
  447. return (labels, cornerLabel);
  448. }
  449. /// <summary>
  450. /// 获取水平标记的label
  451. /// </summary>
  452. /// <param name="positions"></param>
  453. /// <param name="useMultiplierNotation"></param>
  454. /// <param name="useOffsetNotation"></param>
  455. /// <param name="useExponentialNotation"></param>
  456. /// <param name="invertSign"></param>
  457. /// <param name="culture"></param>
  458. /// <param name="axisDimensions"></param>
  459. /// <returns></returns>
  460. public (string[], string) GetHorizontalTickLabels(
  461. double[] positions,
  462. bool useMultiplierNotation,
  463. bool useOffsetNotation,
  464. bool useExponentialNotation,
  465. bool invertSign,
  466. CultureInfo culture,
  467. AxisDimensions axisDimensions,
  468. bool isEvenAndLog = false
  469. )
  470. {
  471. // given positions returns nicely-formatted labels (with offset and multiplier)
  472. string[] labels = new string[positions.Length];
  473. string cornerLabel = "";
  474. if (positions.Length == 0)
  475. return (labels, cornerLabel);
  476. double range = positions.Last() - positions.First();
  477. double exponent = (int)(Math.Log10(range));
  478. double multiplier = 1;
  479. if (useMultiplierNotation)
  480. {
  481. if (Math.Abs(exponent) > 2)
  482. multiplier = Math.Pow(10, exponent);
  483. }
  484. double offset = 0;
  485. if (useOffsetNotation)
  486. {
  487. offset = positions.First();
  488. if (Math.Abs(offset / range) < 10)
  489. offset = 0;
  490. }
  491. for (int i = 0; i < positions.Length; i++)
  492. {
  493. double adjustedPosition = (positions[i] - offset) / multiplier;
  494. if (invertSign)
  495. adjustedPosition *= -1;
  496. labels[i] = ManualTickFormatter is null ? FormatLocal(adjustedPosition, culture) : ManualTickFormatter(adjustedPosition);
  497. if (labels[i] == "-0")
  498. labels[i] = "0";
  499. }
  500. if (useExponentialNotation)
  501. {
  502. if (multiplier != 1)
  503. cornerLabel += $"e{exponent} ";
  504. if (offset != 0)
  505. cornerLabel += Tools.ScientificNotation(offset);
  506. }
  507. else
  508. {
  509. if (multiplier != 1)
  510. cornerLabel += FormatLocal(multiplier, culture);
  511. if (offset != 0)
  512. cornerLabel += " +" + FormatLocal(offset, culture);
  513. cornerLabel = cornerLabel.Replace("+-", "-");
  514. }
  515. if (isEvenAndLog == true)
  516. {
  517. for (int i = 0; i < positions.Length; i++)
  518. {
  519. double adjustedPosition = -(axisDimensions.Position + 5 * axisDimensions.Intreval - positions[i]) / axisDimensions.Intreval;
  520. if (adjustedPosition == 0)
  521. {
  522. adjustedPosition = 0;
  523. }
  524. else
  525. {
  526. adjustedPosition = Math.Pow(axisDimensions.Scale, Math.Abs(adjustedPosition)) * (adjustedPosition < 0 ? -1 : 1);
  527. }
  528. labels[i] = ManualTickFormatter is null ? FormatLocal(adjustedPosition, culture) : ManualTickFormatter(adjustedPosition);
  529. if (labels[i] == "-0")
  530. labels[i] = "0";
  531. labels[i] = new Quantity(adjustedPosition, axisDimensions.UnitPrefix, axisDimensions.ScaleUnit)
  532. .ToString();
  533. }
  534. }
  535. else
  536. {
  537. for (int i = 0; i < positions.Length; i++)
  538. {
  539. double adjustedPosition = -(axisDimensions.Position + 5 * axisDimensions.Intreval - positions[i]) / axisDimensions.Intreval * axisDimensions.Scale;
  540. labels[i] = ManualTickFormatter is null ? FormatLocal(adjustedPosition, culture) : ManualTickFormatter(adjustedPosition);
  541. if (labels[i] == "-0")
  542. labels[i] = "0";
  543. labels[i] = new Quantity(adjustedPosition, axisDimensions.UnitPrefix, axisDimensions.ScaleUnit)
  544. .ToString();
  545. }
  546. }
  547. return (labels, cornerLabel);
  548. }
  549. private void RecalculatePositionsAutomaticDatetime(PlotDimensions dims, float labelWidth, float labelHeight, int? forcedTickCount)
  550. {
  551. double low, high;
  552. int tickCount;
  553. if (MinimumTickSpacing > 0)
  554. throw new InvalidOperationException("minimum tick spacing does not support DateTime ticks");
  555. if (Orientation == AxisOrientation.Vertical)
  556. {
  557. low = dims.YMin - dims.UnitsPerPxY; // add an extra pixel to capture the edge tick
  558. high = dims.YMax + dims.UnitsPerPxY; // add an extra pixel to capture the edge tick
  559. tickCount = (int)(dims.DataHeight / labelHeight * TickDensity);
  560. tickCount = forcedTickCount ?? tickCount;
  561. }
  562. else
  563. {
  564. low = dims.XMin - dims.UnitsPerPxX; // add an extra pixel to capture the edge tick
  565. high = dims.XMax + dims.UnitsPerPxX; // add an extra pixel to capture the edge tick
  566. tickCount = (int)(dims.DataWidth / labelWidth * TickDensity);
  567. tickCount = forcedTickCount ?? tickCount;
  568. }
  569. if (low < high)
  570. {
  571. low = Math.Max(low, new DateTime(0100, 1, 1, 0, 0, 0).ToOADate()); // minimum OADate value
  572. high = Math.Min(high, DateTime.MaxValue.ToOADate());
  573. var dtManualUnits = (Orientation == AxisOrientation.Vertical) ? manualDateTimeSpacingUnitY : manualDateTimeSpacingUnitX;
  574. var dtManualSpacing = (Orientation == AxisOrientation.Vertical) ? manualSpacingY : manualSpacingX;
  575. try
  576. {
  577. DateTime from = DateTime.FromOADate(low);
  578. DateTime to = DateTime.FromOADate(high);
  579. var unitFactory = new DateTimeUnitFactory();
  580. IDateTimeUnit tickUnit = unitFactory.CreateUnit(from, to, Culture, tickCount, dtManualUnits, (int)dtManualSpacing);
  581. (tickPositionsMajor, tickLabels) = tickUnit.GetTicksAndLabels(from, to, dateTimeFormatString);
  582. tickLabels = tickLabels.Select(x => x.Trim()).ToArray();
  583. }
  584. catch
  585. {
  586. tickPositionsMajor = new double[] { }; // far zoom out can produce FromOADate() exception
  587. }
  588. }
  589. else
  590. {
  591. tickPositionsMajor = new double[] { };
  592. }
  593. // dont forget to set all the things
  594. tickPositionsMinor = null;
  595. CornerLabel = null;
  596. }
  597. private void RecalculatePositionsAutomaticNumeric(PlotDimensions dims, float labelWidth, float labelHeight, int? forcedTickCount)
  598. {
  599. double low, high, tickSpacing;
  600. int maxTickCount;
  601. if (Orientation == AxisOrientation.Vertical)
  602. {
  603. low = dims.YMin - dims.UnitsPerPxY; // add an extra pixel to capture the edge tick
  604. high = dims.YMax + dims.UnitsPerPxY; // add an extra pixel to capture the edge tick
  605. maxTickCount = (int)(dims.DataHeight / labelHeight * TickDensity);
  606. maxTickCount = forcedTickCount ?? maxTickCount;
  607. tickSpacing = (manualSpacingY != 0) ? manualSpacingY : GetIdealTickSpacing(low, high, maxTickCount, radix);//刻度间距
  608. tickSpacing = Math.Max(tickSpacing, MinimumTickSpacing);
  609. }
  610. else
  611. {
  612. low = dims.XMin - dims.UnitsPerPxX; // add an extra pixel to capture the edge tick
  613. high = dims.XMax + dims.UnitsPerPxX; // add an extra pixel to capture the edge tick
  614. maxTickCount = (int)(dims.DataWidth / labelWidth * TickDensity);
  615. maxTickCount = forcedTickCount ?? maxTickCount;
  616. tickSpacing = (manualSpacingX != 0) ? manualSpacingX : GetIdealTickSpacing(low, high, maxTickCount, radix);
  617. tickSpacing = Math.Max(tickSpacing, MinimumTickSpacing);
  618. }
  619. // now that tick spacing is known, populate the list of ticks and labels
  620. double firstTickOffset = low % tickSpacing;
  621. int tickCount = (int)((high - low) / tickSpacing) + 2;
  622. tickCount = tickCount > 1000 ? 1000 : tickCount;
  623. tickCount = tickCount < 1 ? 1 : tickCount;
  624. tickPositionsMajor = Enumerable.Range(0, tickCount)//大格子
  625. .Select(x => low - firstTickOffset + tickSpacing * x)
  626. .Where(x => low <= x && x <= high)
  627. .ToArray();
  628. if (LabelFormat == TickLabelFormat.DateTime)
  629. {
  630. tickLabels = GetDateLabels(tickPositionsMajor, Culture);
  631. tickPositionsMinor = null;
  632. }
  633. else
  634. {
  635. (tickLabels, CornerLabel) = GetPrettyTickLabels(
  636. tickPositionsMajor,
  637. useMultiplierNotation,
  638. useOffsetNotation,
  639. useExponentialNotation,
  640. invertSign: LabelUsingInvertedSign,
  641. culture: Culture
  642. );//CornerLabel为上标,e的多少次方
  643. if (MinorTickDistribution == MinorTickDistribution.log)
  644. tickPositionsMinor = MinorFromMajorLog(tickPositionsMajor, low, high,dims, Orientation == AxisOrientation.Vertical);
  645. else
  646. tickPositionsMinor = MinorFromMajor(tickPositionsMajor, 5, low, high);//每大格中有五个小格子
  647. }
  648. }
  649. public (string[], string) GetPrettyTickLabels(
  650. double[] positions,
  651. bool useMultiplierNotation,
  652. bool useOffsetNotation,
  653. bool useExponentialNotation,
  654. bool invertSign,
  655. CultureInfo culture
  656. )
  657. {
  658. // given positions returns nicely-formatted labels (with offset and multiplier)
  659. string[] labels = new string[positions.Length];
  660. string cornerLabel = "";
  661. if (positions.Length == 0)
  662. return (labels, cornerLabel);
  663. double range = positions.Last() - positions.First();
  664. double exponent = (int)(Math.Log10(range));
  665. double multiplier = 1;
  666. if (useMultiplierNotation)
  667. {
  668. if (Math.Abs(exponent) > 2)
  669. multiplier = Math.Pow(10, exponent);
  670. }
  671. double offset = 0;
  672. if (useOffsetNotation)
  673. {
  674. offset = positions.First();
  675. if (Math.Abs(offset / range) < 10)
  676. offset = 0;
  677. }
  678. for (int i = 0; i < positions.Length; i++)
  679. {
  680. double adjustedPosition = (positions[i] - offset) / multiplier;
  681. if (invertSign)
  682. adjustedPosition *= -1;
  683. labels[i] = ManualTickFormatter is null ? FormatLocal(adjustedPosition, culture) : ManualTickFormatter(adjustedPosition);
  684. if (labels[i] == "-0")
  685. labels[i] = "0";
  686. }
  687. if (useExponentialNotation)
  688. {
  689. if (multiplier != 1)
  690. cornerLabel += $"e{exponent} ";
  691. if (offset != 0)
  692. cornerLabel += Tools.ScientificNotation(offset);
  693. }
  694. else
  695. {
  696. if (multiplier != 1)
  697. cornerLabel += FormatLocal(multiplier, culture);
  698. if (offset != 0)
  699. cornerLabel += " +" + FormatLocal(offset, culture);
  700. cornerLabel = cornerLabel.Replace("+-", "-");
  701. }
  702. return (labels, cornerLabel);
  703. }
  704. public override string ToString()
  705. {
  706. string allTickLabels = string.Join(", ", tickLabels);
  707. return $"Tick Collection: [{allTickLabels}] {CornerLabel}";
  708. }
  709. private static double GetIdealTickSpacing(double low, double high, int maxTickCount, int radix = 10)
  710. {
  711. double range = high - low;
  712. int exponent = (int)Math.Log(range, radix);
  713. List<double> tickSpacings = new List<double>() { Math.Pow(radix, exponent) };
  714. tickSpacings.Add(tickSpacings.Last());
  715. tickSpacings.Add(tickSpacings.Last());
  716. double[] divBy;
  717. if (radix == 10)
  718. divBy = new double[] { 2, 2, 2.5 }; // 10, 5, 2.5, 1
  719. else if (radix == 16)
  720. divBy = new double[] { 2, 2, 2, 2 }; // 16, 8, 4, 2, 1
  721. else
  722. throw new ArgumentException($"radix {radix} is not supported");
  723. int divisions = 0;
  724. int tickCount = 0;
  725. while ((tickCount < maxTickCount) && (tickSpacings.Count < 1000))
  726. {
  727. tickSpacings.Add(tickSpacings.Last() / divBy[divisions++ % divBy.Length]);
  728. tickCount = (int)(range / tickSpacings.Last());
  729. }
  730. return tickSpacings[tickSpacings.Count - 3];
  731. }
  732. private string FormatLocal(double value, CultureInfo culture)
  733. {
  734. // if a custom format string exists use it
  735. if (numericFormatString != null)
  736. return value.ToString(numericFormatString, culture);
  737. // if the number is round or large, use the numeric format
  738. // https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#the-numeric-n-format-specifier
  739. bool isRoundNumber = ((int)value == value);
  740. bool isLargeNumber = (Math.Abs(value) > 1000);
  741. if (isRoundNumber || isLargeNumber)
  742. return value.ToString("N0", culture);
  743. // otherwise the number is probably small or very precise to use the general format (with slight rounding)
  744. // https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#the-general-g-format-specifier
  745. return Math.Round(value, 10).ToString("G", culture);
  746. }
  747. public double[] MinorFromMajor(double[] majorTicks, double minorTicksPerMajorTick, double lowerLimit, double upperLimit,bool isEnevAndLog =false)
  748. {
  749. if ((majorTicks == null) || (majorTicks.Length < 2))
  750. return null;
  751. double majorTickSpacing = majorTicks[1] - majorTicks[0];
  752. double minorTickSpacing = majorTickSpacing / minorTicksPerMajorTick;
  753. List<double> majorTicksWithPadding = new List<double>();
  754. majorTicksWithPadding.Add(majorTicks[0] - majorTickSpacing);//未满一格的长度
  755. majorTicksWithPadding.AddRange(majorTicks);//各个格子的长度
  756. List<double> minorTicks = new List<double>();
  757. if (isEnevAndLog == true)
  758. {
  759. foreach (var majorTickPosition in majorTicksWithPadding)
  760. {
  761. for (int i = 1; i < minorTicksPerMajorTick; i++)
  762. {
  763. double minorTickPosition = majorTickPosition + majorTickSpacing * Math.Log10(i+1);
  764. if ((minorTickPosition > lowerLimit) && (minorTickPosition < upperLimit))
  765. minorTicks.Add(minorTickPosition);
  766. }
  767. }
  768. }
  769. else
  770. {
  771. foreach (var majorTickPosition in majorTicksWithPadding)
  772. {
  773. for (int i = 1; i < minorTicksPerMajorTick; i++)
  774. {
  775. double minorTickPosition = majorTickPosition + minorTickSpacing * i;
  776. if ((minorTickPosition > lowerLimit) && (minorTickPosition < upperLimit))
  777. minorTicks.Add(minorTickPosition);
  778. }
  779. }
  780. }
  781. return minorTicks.ToArray();//小刻度
  782. }
  783. public double[] MinorFromMajorLog(double[] majorTicks, double lowerLimit, double upperLimit, PlotDimensions dims,Boolean isInverted)
  784. {
  785. if ((majorTicks == null) || (majorTicks.Length < 2))
  786. return null;
  787. List<double> minorTicks = new List<double>();
  788. for (int i = 0; i < majorTicks.Length; i++)
  789. {
  790. var yValueMax = (float)Math.Log10(majorTicks[i]);
  791. var yValueMin = (float)Math.Log10(majorTicks[i]/10);
  792. for (int j = 1; j < 9; j++)
  793. {
  794. minorTicks.Add(majorTicks[i] / 10 + (majorTicks[i] / 10 * j));
  795. }
  796. }
  797. for (int j = 1; j < 9; j++)
  798. {
  799. minorTicks.Add(majorTicks[majorTicks.Length-1] + (majorTicks[majorTicks.Length-1] * j));
  800. }
  801. if(isInverted)
  802. return minorTicks.Where(x => dims.GetPixelY(x) > dims.DataOffsetY && dims.GetPixelY(x) < dims.DataOffsetY + dims.DataHeight).ToArray();
  803. else
  804. return minorTicks.Where(x => dims.GetPixelX(x) > dims.DataOffsetX && dims.GetPixelX(x) < dims.DataOffsetX + dims.DataWidth).ToArray();
  805. }
  806. //public double[] MinorFromMajorLog(double[] majorTicks, double lowerLimit, double upperLimit)
  807. //{
  808. // if ((majorTicks == null) || (majorTicks.Length < 2))
  809. // return null;
  810. // double majorTickSpacing = majorTicks[1] - majorTicks[0];
  811. // double lowerBound = majorTicks.First() - majorTickSpacing;
  812. // double upperBound = majorTicks.Last() + majorTickSpacing;
  813. // List<double> minorTicks = new List<double>();
  814. // for (double majorTick = lowerBound; majorTick <= upperBound; majorTick += majorTickSpacing)
  815. // {
  816. // minorTicks.Add(majorTick + majorTickSpacing * (.5));
  817. // minorTicks.Add(majorTick + majorTickSpacing * (.5 + .25));
  818. // minorTicks.Add(majorTick + majorTickSpacing * (.5 + .25 + .125));
  819. // minorTicks.Add(majorTick + majorTickSpacing * (.5 + .25 + .125 + .0625));
  820. // }
  821. // return minorTicks.Where(x => x >= lowerLimit && x <= upperLimit).ToArray();
  822. //}
  823. public static string[] GetDateLabels(double[] ticksOADate, CultureInfo culture)
  824. {
  825. TimeSpan dtTickSep;
  826. string dtFmt = null;
  827. try
  828. {
  829. // TODO: replace this with culture-aware format
  830. dtTickSep = DateTime.FromOADate(ticksOADate[1]) - DateTime.FromOADate(ticksOADate[0]);
  831. if (dtTickSep.TotalDays > 365 * 5)
  832. dtFmt = "{0:yyyy}";
  833. else if (dtTickSep.TotalDays > 365)
  834. dtFmt = "{0:yyyy-MM}";
  835. else if (dtTickSep.TotalDays > .5)
  836. dtFmt = "{0:yyyy-MM-dd}";
  837. else if (dtTickSep.TotalMinutes > .5)
  838. dtFmt = "{0:yyyy-MM-dd\nH:mm}";
  839. else
  840. dtFmt = "{0:yyyy-MM-dd\nH:mm:ss}";
  841. }
  842. catch
  843. {
  844. }
  845. string[] labels = new string[ticksOADate.Length];
  846. for (int i = 0; i < ticksOADate.Length; i++)
  847. {
  848. try
  849. {
  850. DateTime dt = DateTime.FromOADate(ticksOADate[i]);
  851. string lbl = string.Format(culture, dtFmt, dt);
  852. labels[i] = lbl;
  853. }
  854. catch
  855. {
  856. labels[i] = "?";
  857. }
  858. }
  859. return labels;
  860. }
  861. }
  862. }