GraphViewTests.cs 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Terminal.Gui;
  5. using Xunit;
  6. using Terminal.Gui.Graphs;
  7. using Point = Terminal.Gui.Point;
  8. using Attribute = Terminal.Gui.Attribute;
  9. using System.Text;
  10. using System.Text.RegularExpressions;
  11. using Xunit.Abstractions;
  12. namespace Terminal.Gui.Views {
  13. #region Helper Classes
  14. class FakeHAxis : HorizontalAxis {
  15. public List<Point> DrawAxisLinePoints = new List<Point> ();
  16. public List<int> LabelPoints = new List<int> ();
  17. protected override void DrawAxisLine (GraphView graph, int x, int y)
  18. {
  19. base.DrawAxisLine (graph, x, y);
  20. DrawAxisLinePoints.Add (new Point (x, y));
  21. }
  22. public override void DrawAxisLabel (GraphView graph, int screenPosition, string text)
  23. {
  24. base.DrawAxisLabel (graph, screenPosition, text);
  25. LabelPoints.Add (screenPosition);
  26. }
  27. }
  28. class FakeVAxis : VerticalAxis {
  29. public List<Point> DrawAxisLinePoints = new List<Point> ();
  30. public List<int> LabelPoints = new List<int> ();
  31. protected override void DrawAxisLine (GraphView graph, int x, int y)
  32. {
  33. base.DrawAxisLine (graph, x, y);
  34. DrawAxisLinePoints.Add (new Point (x, y));
  35. }
  36. public override void DrawAxisLabel (GraphView graph, int screenPosition, string text)
  37. {
  38. base.DrawAxisLabel (graph, screenPosition, text);
  39. LabelPoints.Add (screenPosition);
  40. }
  41. }
  42. #endregion
  43. public class GraphViewTests {
  44. public static FakeDriver InitFakeDriver ()
  45. {
  46. var driver = new FakeDriver ();
  47. Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
  48. driver.Init (() => { });
  49. return driver;
  50. }
  51. /// <summary>
  52. /// Returns a basic very small graph (10 x 5)
  53. /// </summary>
  54. /// <returns></returns>
  55. public static GraphView GetGraph ()
  56. {
  57. GraphViewTests.InitFakeDriver ();
  58. var gv = new GraphView ();
  59. gv.ColorScheme = new ColorScheme ();
  60. gv.MarginBottom = 1;
  61. gv.MarginLeft = 1;
  62. gv.Bounds = new Rect (0, 0, 10, 5);
  63. return gv;
  64. }
  65. #pragma warning disable xUnit1013 // Public method should be marked as test
  66. public static void AssertDriverContentsAre (string expectedLook, ITestOutputHelper output)
  67. {
  68. #pragma warning restore xUnit1013 // Public method should be marked as test
  69. var sb = new StringBuilder ();
  70. var driver = ((FakeDriver)Application.Driver);
  71. var contents = driver.Contents;
  72. for (int r = 0; r < driver.Rows; r++) {
  73. for (int c = 0; c < driver.Cols; c++) {
  74. sb.Append ((char)contents [r, c, 0]);
  75. }
  76. sb.AppendLine ();
  77. }
  78. var actualLook = sb.ToString ();
  79. if (!string.Equals (expectedLook, actualLook)) {
  80. // ignore trailing whitespace on each line
  81. var trailingWhitespace = new Regex (@"\s+$", RegexOptions.Multiline);
  82. // get rid of trailing whitespace on each line (and leading/trailing whitespace of start/end of full string)
  83. expectedLook = trailingWhitespace.Replace (expectedLook, "").Trim ();
  84. actualLook = trailingWhitespace.Replace (actualLook, "").Trim ();
  85. // standardise line endings for the comparison
  86. expectedLook = expectedLook.Replace ("\r\n", "\n");
  87. actualLook = actualLook.Replace ("\r\n", "\n");
  88. output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
  89. output?.WriteLine ("But Was:" + Environment.NewLine + actualLook);
  90. Assert.Equal (expectedLook, actualLook);
  91. }
  92. }
  93. public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestOutputHelper output)
  94. {
  95. var lines = new List<List<char>> ();
  96. var sb = new StringBuilder ();
  97. var driver = ((FakeDriver)Application.Driver);
  98. var x = -1;
  99. var y = -1;
  100. int w = -1;
  101. int h = -1;
  102. var contents = driver.Contents;
  103. for (int r = 0; r < driver.Rows; r++) {
  104. var runes = new List<char> ();
  105. for (int c = 0; c < driver.Cols; c++) {
  106. var rune = (char)contents [r, c, 0];
  107. if (rune != ' ') {
  108. if (x == -1) {
  109. x = c;
  110. y = r;
  111. for (int i = 0; i < c; i++) {
  112. runes.InsertRange (i, new List<char> () { ' ' });
  113. }
  114. }
  115. if (c > w) {
  116. w = c;
  117. }
  118. h = r - y;
  119. }
  120. if (x > -1) {
  121. runes.Add (rune);
  122. }
  123. }
  124. if (runes.Count > 0) {
  125. lines.Add (runes);
  126. }
  127. }
  128. // Remove unnecessary empty lines
  129. for (int r = lines.Count - 1; r > h; r--) {
  130. lines.RemoveAt (r);
  131. }
  132. // Remove trailing whitespace on each line
  133. for (int r = 0; r < lines.Count; r++) {
  134. List<char> row = lines [r];
  135. for (int c = row.Count - 1; c >= 0; c--) {
  136. if (row [c] != ' ') {
  137. break;
  138. }
  139. row.RemoveAt (c);
  140. }
  141. if (row.Count == 0) {
  142. lines.Remove (row);
  143. }
  144. }
  145. // Convert char list to string
  146. for (int r = 0; r < lines.Count; r++) {
  147. var line = new string (lines [r].ToArray ());
  148. if (r == lines.Count - 1) {
  149. sb.Append (line);
  150. } else {
  151. sb.AppendLine (line);
  152. }
  153. }
  154. var actualLook = sb.ToString ();
  155. if (!string.Equals (expectedLook, actualLook)) {
  156. // Remove the first empty line from the expectedLook
  157. expectedLook = string.Join (Environment.NewLine, expectedLook
  158. .Split (Environment.NewLine.ToCharArray ())
  159. .Skip (1)
  160. .ToArray ()).TrimEnd ();
  161. // standardise line endings for the comparison
  162. expectedLook = expectedLook.Replace ("\r\n", "\n");
  163. actualLook = actualLook.Replace ("\r\n", "\n");
  164. output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
  165. output?.WriteLine ("But Was:" + Environment.NewLine + actualLook);
  166. Assert.Equal (expectedLook, actualLook);
  167. }
  168. return new Rect (x, y, w > -1 ? w + 1 : 0, h > -1 ? h + 1 : 0);
  169. }
  170. #pragma warning disable xUnit1013 // Public method should be marked as test
  171. /// <summary>
  172. /// Verifies the console was rendered using the given <paramref name="expectedColors"/> at the given locations.
  173. /// Pass a bitmap of indexes into <paramref name="expectedColors"/> as <paramref name="expectedLook"/> and the
  174. /// test method will verify those colors were used in the row/col of the console during rendering
  175. /// </summary>
  176. /// <param name="expectedLook">Numbers between 0 and 9 for each row/col of the console. Must be valid indexes of <paramref name="expectedColors"/></param>
  177. /// <param name="expectedColors"></param>
  178. public static void AssertDriverColorsAre (string expectedLook, Attribute [] expectedColors)
  179. {
  180. #pragma warning restore xUnit1013 // Public method should be marked as test
  181. if (expectedColors.Length > 10) {
  182. throw new ArgumentException ("This method only works for UIs that use at most 10 colors");
  183. }
  184. expectedLook = expectedLook.Trim ();
  185. var driver = ((FakeDriver)Application.Driver);
  186. var contents = driver.Contents;
  187. int r = 0;
  188. foreach (var line in expectedLook.Split ('\n').Select (l => l.Trim ())) {
  189. for (int c = 0; c < line.Length; c++) {
  190. int val = contents [r, c, 1];
  191. var match = expectedColors.Where (e => e.Value == val).ToList ();
  192. if (match.Count == 0) {
  193. throw new Exception ($"Unexpected color {val} was used at row {r} and col {c}. Color value was {val} (expected colors were {string.Join (",", expectedColors.Select (c => c.Value))})");
  194. } else if (match.Count > 1) {
  195. throw new ArgumentException ($"Bad value for expectedColors, {match.Count} Attributes had the same Value");
  196. }
  197. var colorUsed = Array.IndexOf (expectedColors, match [0]).ToString () [0];
  198. var userExpected = line [c];
  199. if (colorUsed != userExpected) {
  200. throw new Exception ($"Colors used did not match expected at row {r} and col {c}. Color index used was {colorUsed} but test expected {userExpected} (these are indexes into the expectedColors array)");
  201. }
  202. }
  203. r++;
  204. }
  205. }
  206. #region Screen to Graph Tests
  207. [Fact]
  208. public void ScreenToGraphSpace_DefaultCellSize ()
  209. {
  210. var gv = new GraphView ();
  211. gv.Bounds = new Rect (0, 0, 20, 10);
  212. // origin should be bottom left
  213. var botLeft = gv.ScreenToGraphSpace (0, 9);
  214. Assert.Equal (0, botLeft.X);
  215. Assert.Equal (0, botLeft.Y);
  216. Assert.Equal (1, botLeft.Width);
  217. Assert.Equal (1, botLeft.Height);
  218. // up 2 rows of the console and along 1 col
  219. var up2along1 = gv.ScreenToGraphSpace (1, 7);
  220. Assert.Equal (1, up2along1.X);
  221. Assert.Equal (2, up2along1.Y);
  222. }
  223. [Fact]
  224. public void ScreenToGraphSpace_DefaultCellSize_WithMargin ()
  225. {
  226. var gv = new GraphView ();
  227. gv.Bounds = new Rect (0, 0, 20, 10);
  228. // origin should be bottom left
  229. var botLeft = gv.ScreenToGraphSpace (0, 9);
  230. Assert.Equal (0, botLeft.X);
  231. Assert.Equal (0, botLeft.Y);
  232. Assert.Equal (1, botLeft.Width);
  233. Assert.Equal (1, botLeft.Height);
  234. gv.MarginLeft = 1;
  235. botLeft = gv.ScreenToGraphSpace (0, 9);
  236. // Origin should be at 1,9 now to leave a margin of 1
  237. // so screen position 0,9 would be data space -1,0
  238. Assert.Equal (-1, botLeft.X);
  239. Assert.Equal (0, botLeft.Y);
  240. Assert.Equal (1, botLeft.Width);
  241. Assert.Equal (1, botLeft.Height);
  242. gv.MarginLeft = 1;
  243. gv.MarginBottom = 1;
  244. botLeft = gv.ScreenToGraphSpace (0, 9);
  245. // Origin should be at 1,0 (to leave a margin of 1 in both sides)
  246. // so screen position 0,9 would be data space -1,-1
  247. Assert.Equal (-1, botLeft.X);
  248. Assert.Equal (-1, botLeft.Y);
  249. Assert.Equal (1, botLeft.Width);
  250. Assert.Equal (1, botLeft.Height);
  251. }
  252. [Fact]
  253. public void ScreenToGraphSpace_CustomCellSize ()
  254. {
  255. var gv = new GraphView ();
  256. gv.Bounds = new Rect (0, 0, 20, 10);
  257. // Each cell of screen measures 5 units in graph data model vertically and 1/4 horizontally
  258. gv.CellSize = new PointF (0.25f, 5);
  259. // origin should be bottom left
  260. // (note that y=10 is actually overspilling the control, the last row is 9)
  261. var botLeft = gv.ScreenToGraphSpace (0, 9);
  262. Assert.Equal (0, botLeft.X);
  263. Assert.Equal (0, botLeft.Y);
  264. Assert.Equal (0.25f, botLeft.Width);
  265. Assert.Equal (5, botLeft.Height);
  266. // up 2 rows of the console and along 1 col
  267. var up2along1 = gv.ScreenToGraphSpace (1, 7);
  268. Assert.Equal (0.25f, up2along1.X);
  269. Assert.Equal (10, up2along1.Y);
  270. Assert.Equal (0.25f, botLeft.Width);
  271. Assert.Equal (5, botLeft.Height);
  272. }
  273. #endregion
  274. #region Graph to Screen Tests
  275. [Fact]
  276. public void GraphSpaceToScreen_DefaultCellSize ()
  277. {
  278. var gv = new GraphView ();
  279. gv.Bounds = new Rect (0, 0, 20, 10);
  280. // origin should be bottom left
  281. var botLeft = gv.GraphSpaceToScreen (new PointF (0, 0));
  282. Assert.Equal (0, botLeft.X);
  283. Assert.Equal (9, botLeft.Y); // row 9 of the view is the bottom left
  284. // along 2 and up 1 in graph space
  285. var along2up1 = gv.GraphSpaceToScreen (new PointF (2, 1));
  286. Assert.Equal (2, along2up1.X);
  287. Assert.Equal (8, along2up1.Y);
  288. }
  289. [Fact]
  290. public void GraphSpaceToScreen_DefaultCellSize_WithMargin ()
  291. {
  292. var gv = new GraphView ();
  293. gv.Bounds = new Rect (0, 0, 20, 10);
  294. // origin should be bottom left
  295. var botLeft = gv.GraphSpaceToScreen (new PointF (0, 0));
  296. Assert.Equal (0, botLeft.X);
  297. Assert.Equal (9, botLeft.Y); // row 9 of the view is the bottom left
  298. gv.MarginLeft = 1;
  299. // With a margin of 1 the origin should be at x=1 y= 9
  300. botLeft = gv.GraphSpaceToScreen (new PointF (0, 0));
  301. Assert.Equal (1, botLeft.X);
  302. Assert.Equal (9, botLeft.Y); // row 9 of the view is the bottom left
  303. gv.MarginLeft = 1;
  304. gv.MarginBottom = 1;
  305. // With a margin of 1 in both directions the origin should be at x=1 y= 9
  306. botLeft = gv.GraphSpaceToScreen (new PointF (0, 0));
  307. Assert.Equal (1, botLeft.X);
  308. Assert.Equal (8, botLeft.Y); // row 8 of the view is the bottom left up 1 cell
  309. }
  310. [Fact]
  311. public void GraphSpaceToScreen_ScrollOffset ()
  312. {
  313. var gv = new GraphView ();
  314. gv.Bounds = new Rect (0, 0, 20, 10);
  315. //graph is scrolled to present chart space -5 to 5 in both axes
  316. gv.ScrollOffset = new PointF (-5, -5);
  317. // origin should be right in the middle of the control
  318. var botLeft = gv.GraphSpaceToScreen (new PointF (0, 0));
  319. Assert.Equal (5, botLeft.X);
  320. Assert.Equal (4, botLeft.Y);
  321. // along 2 and up 1 in graph space
  322. var along2up1 = gv.GraphSpaceToScreen (new PointF (2, 1));
  323. Assert.Equal (7, along2up1.X);
  324. Assert.Equal (3, along2up1.Y);
  325. }
  326. [Fact]
  327. public void GraphSpaceToScreen_CustomCellSize ()
  328. {
  329. var gv = new GraphView ();
  330. gv.Bounds = new Rect (0, 0, 20, 10);
  331. // Each cell of screen is responsible for rendering 5 units in graph data model
  332. // vertically and 1/4 horizontally
  333. gv.CellSize = new PointF (0.25f, 5);
  334. // origin should be bottom left
  335. var botLeft = gv.GraphSpaceToScreen (new PointF (0, 0));
  336. Assert.Equal (0, botLeft.X);
  337. // row 9 of the view is the bottom left (height is 10 so 0,1,2,3..9)
  338. Assert.Equal (9, botLeft.Y);
  339. // along 2 and up 1 in graph space
  340. var along2up1 = gv.GraphSpaceToScreen (new PointF (2, 1));
  341. Assert.Equal (8, along2up1.X);
  342. Assert.Equal (9, along2up1.Y);
  343. // Y value 4 should be rendered in bottom most row
  344. Assert.Equal (9, gv.GraphSpaceToScreen (new PointF (2, 4)).Y);
  345. // Cell height is 5 so this is the first point of graph space that should
  346. // be rendered in the graph in next row up (row 9)
  347. Assert.Equal (8, gv.GraphSpaceToScreen (new PointF (2, 5)).Y);
  348. // More boundary testing for this cell size
  349. Assert.Equal (8, gv.GraphSpaceToScreen (new PointF (2, 6)).Y);
  350. Assert.Equal (8, gv.GraphSpaceToScreen (new PointF (2, 7)).Y);
  351. Assert.Equal (8, gv.GraphSpaceToScreen (new PointF (2, 8)).Y);
  352. Assert.Equal (8, gv.GraphSpaceToScreen (new PointF (2, 9)).Y);
  353. Assert.Equal (7, gv.GraphSpaceToScreen (new PointF (2, 10)).Y);
  354. Assert.Equal (7, gv.GraphSpaceToScreen (new PointF (2, 11)).Y);
  355. }
  356. [Fact]
  357. public void GraphSpaceToScreen_CustomCellSize_WithScrollOffset ()
  358. {
  359. var gv = new GraphView ();
  360. gv.Bounds = new Rect (0, 0, 20, 10);
  361. // Each cell of screen is responsible for rendering 5 units in graph data model
  362. // vertically and 1/4 horizontally
  363. gv.CellSize = new PointF (0.25f, 5);
  364. //graph is scrolled to present some negative chart (4 negative cols and 2 negative rows)
  365. gv.ScrollOffset = new PointF (-1, -10);
  366. // origin should be in the lower left (but not right at the bottom)
  367. var botLeft = gv.GraphSpaceToScreen (new PointF (0, 0));
  368. Assert.Equal (4, botLeft.X);
  369. Assert.Equal (7, botLeft.Y);
  370. // along 2 and up 1 in graph space
  371. var along2up1 = gv.GraphSpaceToScreen (new PointF (2, 1));
  372. Assert.Equal (12, along2up1.X);
  373. Assert.Equal (7, along2up1.Y);
  374. // More boundary testing for this cell size/offset
  375. Assert.Equal (6, gv.GraphSpaceToScreen (new PointF (2, 6)).Y);
  376. Assert.Equal (6, gv.GraphSpaceToScreen (new PointF (2, 7)).Y);
  377. Assert.Equal (6, gv.GraphSpaceToScreen (new PointF (2, 8)).Y);
  378. Assert.Equal (6, gv.GraphSpaceToScreen (new PointF (2, 9)).Y);
  379. Assert.Equal (5, gv.GraphSpaceToScreen (new PointF (2, 10)).Y);
  380. Assert.Equal (5, gv.GraphSpaceToScreen (new PointF (2, 11)).Y);
  381. }
  382. #endregion
  383. /// <summary>
  384. /// A cell size of 0 would result in mapping all graph space into the
  385. /// same cell of the console. Since <see cref="GraphView.CellSize"/>
  386. /// is mutable a sensible place to check this is in redraw.
  387. /// </summary>
  388. [Fact]
  389. public void CellSizeZero ()
  390. {
  391. InitFakeDriver ();
  392. var gv = new GraphView ();
  393. gv.ColorScheme = new ColorScheme ();
  394. gv.Bounds = new Rect (0, 0, 50, 30);
  395. gv.Series.Add (new ScatterSeries () { Points = new List<PointF> { new PointF (1, 1) } });
  396. gv.CellSize = new PointF (0, 5);
  397. var ex = Assert.Throws<Exception> (() => gv.Redraw (gv.Bounds));
  398. Assert.Equal ("CellSize cannot be 0", ex.Message);
  399. // Shutdown must be called to safely clean up Application if Init has been called
  400. Application.Shutdown ();
  401. }
  402. /// <summary>
  403. /// Tests that each point in the screen space maps to a rectangle of
  404. /// (float) graph space and that each corner of that rectangle of graph
  405. /// space maps back to the same row/col of the graph that was fed in
  406. /// </summary>
  407. [Fact]
  408. public void TestReversing_ScreenToGraphSpace ()
  409. {
  410. var gv = new GraphView ();
  411. gv.Bounds = new Rect (0, 0, 50, 30);
  412. // How much graph space each cell of the console depicts
  413. gv.CellSize = new PointF (0.1f, 0.25f);
  414. gv.AxisX.Increment = 1;
  415. gv.AxisX.ShowLabelsEvery = 1;
  416. gv.AxisY.Increment = 1;
  417. gv.AxisY.ShowLabelsEvery = 1;
  418. // Start the graph at 80
  419. gv.ScrollOffset = new PointF (0, 80);
  420. for (int x = 0; x < gv.Bounds.Width; x++) {
  421. for (int y = 0; y < gv.Bounds.Height; y++) {
  422. var graphSpace = gv.ScreenToGraphSpace (x, y);
  423. // See
  424. // https://en.wikipedia.org/wiki/Machine_epsilon
  425. float epsilon = 0.0001f;
  426. var p = gv.GraphSpaceToScreen (new PointF (graphSpace.Left + epsilon, graphSpace.Top + epsilon));
  427. Assert.Equal (x, p.X);
  428. Assert.Equal (y, p.Y);
  429. p = gv.GraphSpaceToScreen (new PointF (graphSpace.Right - epsilon, graphSpace.Top + epsilon));
  430. Assert.Equal (x, p.X);
  431. Assert.Equal (y, p.Y);
  432. p = gv.GraphSpaceToScreen (new PointF (graphSpace.Left + epsilon, graphSpace.Bottom - epsilon));
  433. Assert.Equal (x, p.X);
  434. Assert.Equal (y, p.Y);
  435. p = gv.GraphSpaceToScreen (new PointF (graphSpace.Right - epsilon, graphSpace.Bottom - epsilon));
  436. Assert.Equal (x, p.X);
  437. Assert.Equal (y, p.Y);
  438. }
  439. }
  440. }
  441. }
  442. public class SeriesTests {
  443. [Fact]
  444. public void Series_GetsPassedCorrectBounds_AllAtOnce ()
  445. {
  446. GraphViewTests.InitFakeDriver ();
  447. var gv = new GraphView ();
  448. gv.ColorScheme = new ColorScheme ();
  449. gv.Bounds = new Rect (0, 0, 50, 30);
  450. RectangleF fullGraphBounds = RectangleF.Empty;
  451. Rect graphScreenBounds = Rect.Empty;
  452. var series = new FakeSeries ((v, s, g) => { graphScreenBounds = s; fullGraphBounds = g; });
  453. gv.Series.Add (series);
  454. gv.Redraw (gv.Bounds);
  455. Assert.Equal (new RectangleF (0, 0, 50, 30), fullGraphBounds);
  456. Assert.Equal (new Rect (0, 0, 50, 30), graphScreenBounds);
  457. // Now we put a margin in
  458. // Graph should not spill into the margins
  459. gv.MarginBottom = 2;
  460. gv.MarginLeft = 5;
  461. // Even with a margin the graph should be drawn from
  462. // the origin, we just get less visible width/height
  463. gv.Redraw (gv.Bounds);
  464. Assert.Equal (new RectangleF (0, 0, 45, 28), fullGraphBounds);
  465. // The screen space the graph will be rendered into should
  466. // not overspill the margins
  467. Assert.Equal (new Rect (5, 0, 45, 28), graphScreenBounds);
  468. // Shutdown must be called to safely clean up Application if Init has been called
  469. Application.Shutdown ();
  470. }
  471. /// <summary>
  472. /// Tests that the bounds passed to the ISeries for drawing into are
  473. /// correct even when the <see cref="GraphView.CellSize"/> results in
  474. /// multiple units of graph space being condensed into each cell of
  475. /// console
  476. /// </summary>
  477. [Fact]
  478. public void Series_GetsPassedCorrectBounds_AllAtOnce_LargeCellSize ()
  479. {
  480. GraphViewTests.InitFakeDriver ();
  481. var gv = new GraphView ();
  482. gv.ColorScheme = new ColorScheme ();
  483. gv.Bounds = new Rect (0, 0, 50, 30);
  484. // the larger the cell size the more condensed (smaller) the graph space is
  485. gv.CellSize = new PointF (2, 5);
  486. RectangleF fullGraphBounds = RectangleF.Empty;
  487. Rect graphScreenBounds = Rect.Empty;
  488. var series = new FakeSeries ((v, s, g) => { graphScreenBounds = s; fullGraphBounds = g; });
  489. gv.Series.Add (series);
  490. gv.Redraw (gv.Bounds);
  491. // Since each cell of the console is 2x5 of graph space the graph
  492. // bounds to be rendered are larger
  493. Assert.Equal (new RectangleF (0, 0, 100, 150), fullGraphBounds);
  494. Assert.Equal (new Rect (0, 0, 50, 30), graphScreenBounds);
  495. // Graph should not spill into the margins
  496. gv.MarginBottom = 2;
  497. gv.MarginLeft = 5;
  498. // Even with a margin the graph should be drawn from
  499. // the origin, we just get less visible width/height
  500. gv.Redraw (gv.Bounds);
  501. Assert.Equal (new RectangleF (0, 0, 90, 140), fullGraphBounds);
  502. // The screen space the graph will be rendered into should
  503. // not overspill the margins
  504. Assert.Equal (new Rect (5, 0, 45, 28), graphScreenBounds);
  505. // Shutdown must be called to safely clean up Application if Init has been called
  506. Application.Shutdown ();
  507. }
  508. private class FakeSeries : ISeries {
  509. readonly Action<GraphView, Rect, RectangleF> drawSeries;
  510. public FakeSeries (
  511. Action<GraphView, Rect, RectangleF> drawSeries
  512. )
  513. {
  514. this.drawSeries = drawSeries;
  515. }
  516. public void DrawSeries (GraphView graph, Rect bounds, RectangleF graphBounds)
  517. {
  518. drawSeries (graph, bounds, graphBounds);
  519. }
  520. }
  521. }
  522. public class MultiBarSeriesTests {
  523. readonly ITestOutputHelper output;
  524. public MultiBarSeriesTests (ITestOutputHelper output)
  525. {
  526. this.output = output;
  527. }
  528. [Fact]
  529. public void MultiBarSeries_BarSpacing ()
  530. {
  531. // Creates clusters of 5 adjacent bars with 2 spaces between clusters
  532. var series = new MultiBarSeries (5, 7, 1);
  533. Assert.Equal (5, series.SubSeries.Count);
  534. Assert.Equal (0, series.SubSeries.ElementAt (0).Offset);
  535. Assert.Equal (1, series.SubSeries.ElementAt (1).Offset);
  536. Assert.Equal (2, series.SubSeries.ElementAt (2).Offset);
  537. Assert.Equal (3, series.SubSeries.ElementAt (3).Offset);
  538. Assert.Equal (4, series.SubSeries.ElementAt (4).Offset);
  539. }
  540. [Fact]
  541. public void MultiBarSeriesColors_WrongNumber ()
  542. {
  543. var fake = new FakeDriver ();
  544. var colors = new []{
  545. fake.MakeAttribute(Color.Green,Color.Black)
  546. };
  547. // user passes 1 color only but asks for 5 bars
  548. var ex = Assert.Throws<ArgumentException> (() => new MultiBarSeries (5, 7, 1, colors));
  549. Assert.Equal ("Number of colors must match the number of bars (Parameter 'numberOfBarsPerCategory')", ex.Message);
  550. // Shutdown must be called to safely clean up Application if Init has been called
  551. Application.Shutdown ();
  552. }
  553. [Fact]
  554. public void MultiBarSeriesColors_RightNumber ()
  555. {
  556. var fake = new FakeDriver ();
  557. var colors = new []{
  558. fake.MakeAttribute(Color.Green,Color.Black),
  559. fake.MakeAttribute(Color.Green,Color.White),
  560. fake.MakeAttribute(Color.BrightYellow,Color.White)
  561. };
  562. // user passes 3 colors and asks for 3 bars
  563. var series = new MultiBarSeries (3, 7, 1, colors);
  564. Assert.Equal (series.SubSeries.ElementAt (0).OverrideBarColor, colors [0]);
  565. Assert.Equal (series.SubSeries.ElementAt (1).OverrideBarColor, colors [1]);
  566. Assert.Equal (series.SubSeries.ElementAt (2).OverrideBarColor, colors [2]);
  567. // Shutdown must be called to safely clean up Application if Init has been called
  568. Application.Shutdown ();
  569. }
  570. [Fact]
  571. public void MultiBarSeriesAddValues_WrongNumber ()
  572. {
  573. // user asks for 3 bars per category
  574. var series = new MultiBarSeries (3, 7, 1);
  575. var ex = Assert.Throws<ArgumentException> (() => series.AddBars ("Cars", '#', 1));
  576. Assert.Equal ("Number of values must match the number of bars per category (Parameter 'values')", ex.Message);
  577. }
  578. [Fact]
  579. public void TestRendering_MultibarSeries ()
  580. {
  581. GraphViewTests.InitFakeDriver ();
  582. var gv = new GraphView ();
  583. gv.ColorScheme = new ColorScheme ();
  584. // y axis goes from 0.1 to 1 across 10 console rows
  585. // x axis goes from 0 to 20 across 20 console columns
  586. gv.Bounds = new Rect (0, 0, 20, 10);
  587. gv.CellSize = new PointF (1f, 0.1f);
  588. gv.MarginBottom = 1;
  589. gv.MarginLeft = 1;
  590. var multibarSeries = new MultiBarSeries (2, 4, 1);
  591. //nudge them left to avoid float rounding errors at the boundaries of cells
  592. foreach (var sub in multibarSeries.SubSeries) {
  593. sub.Offset -= 0.001f;
  594. }
  595. gv.Series.Add (multibarSeries);
  596. FakeHAxis fakeXAxis;
  597. // don't show axis labels that means any labels
  598. // that appaer are explicitly from the bars
  599. gv.AxisX = fakeXAxis = new FakeHAxis () { Increment = 0 };
  600. gv.AxisY = new FakeVAxis () { Increment = 0 };
  601. gv.Redraw (gv.Bounds);
  602. // Since bar series has no bars yet no labels should be displayed
  603. Assert.Empty (fakeXAxis.LabelPoints);
  604. multibarSeries.AddBars ("hey", 'M', 0.5001f, 0.5001f);
  605. fakeXAxis.LabelPoints.Clear ();
  606. gv.Redraw (gv.Bounds);
  607. Assert.Equal (4, fakeXAxis.LabelPoints.Single ());
  608. multibarSeries.AddBars ("there", 'M', 0.24999f, 0.74999f);
  609. multibarSeries.AddBars ("bob", 'M', 1, 2);
  610. fakeXAxis.LabelPoints.Clear ();
  611. gv.Redraw (gv.Bounds);
  612. Assert.Equal (3, fakeXAxis.LabelPoints.Count);
  613. Assert.Equal (4, fakeXAxis.LabelPoints [0]);
  614. Assert.Equal (8, fakeXAxis.LabelPoints [1]);
  615. Assert.Equal (12, fakeXAxis.LabelPoints [2]);
  616. string looksLike =
  617. @"
  618. │ MM
  619. │ M MM
  620. │ M MM
  621. │ MM M MM
  622. │ MM M MM
  623. │ MM M MM
  624. │ MM MM MM
  625. │ MM MM MM
  626. ┼──┬M──┬M──┬M──────
  627. heytherebob ";
  628. GraphViewTests.AssertDriverContentsAre (looksLike, output);
  629. // Shutdown must be called to safely clean up Application if Init has been called
  630. Application.Shutdown ();
  631. }
  632. }
  633. public class BarSeriesTests {
  634. private GraphView GetGraph (out FakeBarSeries series, out FakeHAxis axisX, out FakeVAxis axisY)
  635. {
  636. GraphViewTests.InitFakeDriver ();
  637. var gv = new GraphView ();
  638. gv.ColorScheme = new ColorScheme ();
  639. // y axis goes from 0.1 to 1 across 10 console rows
  640. // x axis goes from 0 to 10 across 20 console columns
  641. gv.Bounds = new Rect (0, 0, 20, 10);
  642. gv.CellSize = new PointF (0.5f, 0.1f);
  643. gv.Series.Add (series = new FakeBarSeries ());
  644. // don't show axis labels that means any labels
  645. // that appaer are explicitly from the bars
  646. gv.AxisX = axisX = new FakeHAxis () { Increment = 0 };
  647. gv.AxisY = axisY = new FakeVAxis () { Increment = 0 };
  648. return gv;
  649. }
  650. [Fact]
  651. public void TestZeroHeightBar_WithName ()
  652. {
  653. var graph = GetGraph (out FakeBarSeries barSeries, out FakeHAxis axisX, out FakeVAxis axisY);
  654. graph.Redraw (graph.Bounds);
  655. // no bars
  656. Assert.Empty (barSeries.BarScreenStarts);
  657. Assert.Empty (axisX.LabelPoints);
  658. Assert.Empty (axisY.LabelPoints);
  659. // bar of height 0
  660. barSeries.Bars.Add (new BarSeries.Bar ("hi", new GraphCellToRender ('.'), 0));
  661. barSeries.Orientation = Orientation.Vertical;
  662. // redraw graph
  663. graph.Redraw (graph.Bounds);
  664. // bar should not be drawn
  665. Assert.Empty (barSeries.BarScreenStarts);
  666. Assert.NotEmpty (axisX.LabelPoints);
  667. Assert.Empty (axisY.LabelPoints);
  668. // but bar name should be
  669. // Screen position x=2 because bars are drawn every 1f of
  670. // graph space and CellSize.X is 0.5f
  671. Assert.Contains (2, axisX.LabelPoints);
  672. // Shutdown must be called to safely clean up Application if Init has been called
  673. Application.Shutdown ();
  674. }
  675. [Fact]
  676. public void TestTwoTallBars_WithOffset ()
  677. {
  678. var graph = GetGraph (out FakeBarSeries barSeries, out FakeHAxis axisX, out FakeVAxis axisY);
  679. graph.Redraw (graph.Bounds);
  680. // no bars
  681. Assert.Empty (barSeries.BarScreenStarts);
  682. Assert.Empty (axisX.LabelPoints);
  683. Assert.Empty (axisY.LabelPoints);
  684. // 0.5 units of graph fit every screen cell
  685. // so 1 unit of graph space is 2 screen columns
  686. graph.CellSize = new PointF (0.5f, 0.1f);
  687. // Start bar 1 screen unit along
  688. barSeries.Offset = 0.5f;
  689. barSeries.BarEvery = 1f;
  690. barSeries.Bars.Add (
  691. new BarSeries.Bar ("hi1", new GraphCellToRender ('.'), 100));
  692. barSeries.Bars.Add (
  693. new BarSeries.Bar ("hi2", new GraphCellToRender ('.'), 100));
  694. barSeries.Orientation = Orientation.Vertical;
  695. // redraw graph
  696. graph.Redraw (graph.Bounds);
  697. // bar should be drawn at BarEvery 1f + offset 0.5f = 3 screen units
  698. Assert.Equal (3, barSeries.BarScreenStarts [0].X);
  699. Assert.Equal (3, barSeries.BarScreenEnds [0].X);
  700. // second bar should be BarEveryx2 = 2f + offset 0.5f = 5 screen units
  701. Assert.Equal (5, barSeries.BarScreenStarts [1].X);
  702. Assert.Equal (5, barSeries.BarScreenEnds [1].X);
  703. // both bars should have labels
  704. Assert.Equal (2, axisX.LabelPoints.Count);
  705. Assert.Contains (3, axisX.LabelPoints);
  706. Assert.Contains (5, axisX.LabelPoints);
  707. // bars are very tall but should not draw up off top of screen
  708. Assert.Equal (9, barSeries.BarScreenStarts [0].Y);
  709. Assert.Equal (0, barSeries.BarScreenEnds [0].Y);
  710. Assert.Equal (9, barSeries.BarScreenStarts [1].Y);
  711. Assert.Equal (0, barSeries.BarScreenEnds [1].Y);
  712. // Shutdown must be called to safely clean up Application if Init has been called
  713. Application.Shutdown ();
  714. }
  715. [Fact]
  716. public void TestOneLongOneShortHorizontalBars_WithOffset ()
  717. {
  718. var graph = GetGraph (out FakeBarSeries barSeries, out FakeHAxis axisX, out FakeVAxis axisY);
  719. graph.Redraw (graph.Bounds);
  720. // no bars
  721. Assert.Empty (barSeries.BarScreenStarts);
  722. Assert.Empty (axisX.LabelPoints);
  723. Assert.Empty (axisY.LabelPoints);
  724. // 0.1 units of graph y fit every screen row
  725. // so 1 unit of graph y space is 10 screen rows
  726. graph.CellSize = new PointF (0.5f, 0.1f);
  727. // Start bar 3 screen units up (y = height-3)
  728. barSeries.Offset = 0.25f;
  729. // 1 bar every 3 rows of screen
  730. barSeries.BarEvery = 0.3f;
  731. barSeries.Orientation = Orientation.Horizontal;
  732. // 1 bar that is very wide (100 graph units horizontally = screen pos 50 but bounded by screen)
  733. barSeries.Bars.Add (
  734. new BarSeries.Bar ("hi1", new GraphCellToRender ('.'), 100));
  735. // 1 bar that is shorter
  736. barSeries.Bars.Add (
  737. new BarSeries.Bar ("hi2", new GraphCellToRender ('.'), 5));
  738. // redraw graph
  739. graph.Redraw (graph.Bounds);
  740. // since bars are horizontal all have the same X start cordinates
  741. Assert.Equal (0, barSeries.BarScreenStarts [0].X);
  742. Assert.Equal (0, barSeries.BarScreenStarts [1].X);
  743. // bar goes all the way to the end so bumps up against right screen boundary
  744. // width of graph is 20
  745. Assert.Equal (19, barSeries.BarScreenEnds [0].X);
  746. // shorter bar is 5 graph units wide which is 10 screen units
  747. Assert.Equal (10, barSeries.BarScreenEnds [1].X);
  748. // first bar should be offset 6 screen units (0.25f + 0.3f graph units)
  749. // since height of control is 10 then first bar should be at screen row 4 (10-6)
  750. Assert.Equal (4, barSeries.BarScreenStarts [0].Y);
  751. // second bar should be offset 9 screen units (0.25f + 0.6f graph units)
  752. // since height of control is 10 then second bar should be at screen row 1 (10-9)
  753. Assert.Equal (1, barSeries.BarScreenStarts [1].Y);
  754. // both bars should have labels but on the y axis
  755. Assert.Equal (2, axisY.LabelPoints.Count);
  756. Assert.Empty (axisX.LabelPoints);
  757. // labels should align with the bars (same screen y axis point)
  758. Assert.Contains (4, axisY.LabelPoints);
  759. Assert.Contains (1, axisY.LabelPoints);
  760. // Shutdown must be called to safely clean up Application if Init has been called
  761. Application.Shutdown ();
  762. }
  763. private class FakeBarSeries : BarSeries {
  764. public GraphCellToRender FinalColor { get; private set; }
  765. public List<Point> BarScreenStarts { get; private set; } = new List<Point> ();
  766. public List<Point> BarScreenEnds { get; private set; } = new List<Point> ();
  767. protected override GraphCellToRender AdjustColor (GraphCellToRender graphCellToRender)
  768. {
  769. return FinalColor = base.AdjustColor (graphCellToRender);
  770. }
  771. protected override void DrawBarLine (GraphView graph, Point start, Point end, Bar beingDrawn)
  772. {
  773. base.DrawBarLine (graph, start, end, beingDrawn);
  774. BarScreenStarts.Add (start);
  775. BarScreenEnds.Add (end);
  776. }
  777. }
  778. }
  779. public class AxisTests {
  780. private GraphView GetGraph (out FakeHAxis axis)
  781. {
  782. return GetGraph (out axis, out _);
  783. }
  784. private GraphView GetGraph (out FakeVAxis axis)
  785. {
  786. return GetGraph (out _, out axis);
  787. }
  788. private GraphView GetGraph (out FakeHAxis axisX, out FakeVAxis axisY)
  789. {
  790. GraphViewTests.InitFakeDriver ();
  791. var gv = new GraphView ();
  792. gv.ColorScheme = new ColorScheme ();
  793. gv.Bounds = new Rect (0, 0, 50, 30);
  794. // graph can't be completely empty or it won't draw
  795. gv.Series.Add (new ScatterSeries ());
  796. axisX = new FakeHAxis ();
  797. axisY = new FakeVAxis ();
  798. gv.AxisX = axisX;
  799. gv.AxisY = axisY;
  800. return gv;
  801. }
  802. #region HorizontalAxis Tests
  803. /// <summary>
  804. /// Tests that the horizontal axis is computed correctly and does not over spill
  805. /// it's bounds
  806. /// </summary>
  807. [Fact]
  808. public void TestHAxisLocation_NoMargin ()
  809. {
  810. var gv = GetGraph (out FakeHAxis axis);
  811. gv.Redraw (gv.Bounds);
  812. Assert.DoesNotContain (new Point (-1, 29), axis.DrawAxisLinePoints);
  813. Assert.Contains (new Point (0, 29), axis.DrawAxisLinePoints);
  814. Assert.Contains (new Point (1, 29), axis.DrawAxisLinePoints);
  815. Assert.Contains (new Point (48, 29), axis.DrawAxisLinePoints);
  816. Assert.Contains (new Point (49, 29), axis.DrawAxisLinePoints);
  817. Assert.DoesNotContain (new Point (50, 29), axis.DrawAxisLinePoints);
  818. Assert.InRange (axis.LabelPoints.Max (), 0, 49);
  819. Assert.InRange (axis.LabelPoints.Min (), 0, 49);
  820. // Shutdown must be called to safely clean up Application if Init has been called
  821. Application.Shutdown ();
  822. }
  823. [Fact]
  824. public void TestHAxisLocation_MarginBottom ()
  825. {
  826. var gv = GetGraph (out FakeHAxis axis);
  827. gv.MarginBottom = 10;
  828. gv.Redraw (gv.Bounds);
  829. Assert.DoesNotContain (new Point (-1, 19), axis.DrawAxisLinePoints);
  830. Assert.Contains (new Point (0, 19), axis.DrawAxisLinePoints);
  831. Assert.Contains (new Point (1, 19), axis.DrawAxisLinePoints);
  832. Assert.Contains (new Point (48, 19), axis.DrawAxisLinePoints);
  833. Assert.Contains (new Point (49, 19), axis.DrawAxisLinePoints);
  834. Assert.DoesNotContain (new Point (50, 19), axis.DrawAxisLinePoints);
  835. Assert.InRange (axis.LabelPoints.Max (), 0, 49);
  836. Assert.InRange (axis.LabelPoints.Min (), 0, 49);
  837. // Shutdown must be called to safely clean up Application if Init has been called
  838. Application.Shutdown ();
  839. }
  840. [Fact]
  841. public void TestHAxisLocation_MarginLeft ()
  842. {
  843. var gv = GetGraph (out FakeHAxis axis);
  844. gv.MarginLeft = 5;
  845. gv.Redraw (gv.Bounds);
  846. Assert.DoesNotContain (new Point (4, 29), axis.DrawAxisLinePoints);
  847. Assert.Contains (new Point (5, 29), axis.DrawAxisLinePoints);
  848. Assert.Contains (new Point (6, 29), axis.DrawAxisLinePoints);
  849. Assert.Contains (new Point (48, 29), axis.DrawAxisLinePoints);
  850. Assert.Contains (new Point (49, 29), axis.DrawAxisLinePoints);
  851. Assert.DoesNotContain (new Point (50, 29), axis.DrawAxisLinePoints);
  852. // Axis lables should not be drawn in the margin
  853. Assert.InRange (axis.LabelPoints.Max (), 5, 49);
  854. Assert.InRange (axis.LabelPoints.Min (), 5, 49);
  855. // Shutdown must be called to safely clean up Application if Init has been called
  856. Application.Shutdown ();
  857. }
  858. #endregion
  859. #region VerticalAxisTests
  860. /// <summary>
  861. /// Tests that the horizontal axis is computed correctly and does not over spill
  862. /// it's bounds
  863. /// </summary>
  864. [Fact]
  865. public void TestVAxisLocation_NoMargin ()
  866. {
  867. var gv = GetGraph (out FakeVAxis axis);
  868. gv.Redraw (gv.Bounds);
  869. Assert.DoesNotContain (new Point (0, -1), axis.DrawAxisLinePoints);
  870. Assert.Contains (new Point (0, 1), axis.DrawAxisLinePoints);
  871. Assert.Contains (new Point (0, 2), axis.DrawAxisLinePoints);
  872. Assert.Contains (new Point (0, 28), axis.DrawAxisLinePoints);
  873. Assert.Contains (new Point (0, 29), axis.DrawAxisLinePoints);
  874. Assert.DoesNotContain (new Point (0, 30), axis.DrawAxisLinePoints);
  875. Assert.InRange (axis.LabelPoints.Max (), 0, 29);
  876. Assert.InRange (axis.LabelPoints.Min (), 0, 29);
  877. // Shutdown must be called to safely clean up Application if Init has been called
  878. Application.Shutdown ();
  879. }
  880. [Fact]
  881. public void TestVAxisLocation_MarginBottom ()
  882. {
  883. var gv = GetGraph (out FakeVAxis axis);
  884. gv.MarginBottom = 10;
  885. gv.Redraw (gv.Bounds);
  886. Assert.DoesNotContain (new Point (0, -1), axis.DrawAxisLinePoints);
  887. Assert.Contains (new Point (0, 1), axis.DrawAxisLinePoints);
  888. Assert.Contains (new Point (0, 2), axis.DrawAxisLinePoints);
  889. Assert.Contains (new Point (0, 18), axis.DrawAxisLinePoints);
  890. Assert.Contains (new Point (0, 19), axis.DrawAxisLinePoints);
  891. Assert.DoesNotContain (new Point (0, 20), axis.DrawAxisLinePoints);
  892. // Labels should not be drawn into the axis
  893. Assert.InRange (axis.LabelPoints.Max (), 0, 19);
  894. Assert.InRange (axis.LabelPoints.Min (), 0, 19);
  895. // Shutdown must be called to safely clean up Application if Init has been called
  896. Application.Shutdown ();
  897. }
  898. [Fact]
  899. public void TestVAxisLocation_MarginLeft ()
  900. {
  901. var gv = GetGraph (out FakeVAxis axis);
  902. gv.MarginLeft = 5;
  903. gv.Redraw (gv.Bounds);
  904. Assert.DoesNotContain (new Point (5, -1), axis.DrawAxisLinePoints);
  905. Assert.Contains (new Point (5, 1), axis.DrawAxisLinePoints);
  906. Assert.Contains (new Point (5, 2), axis.DrawAxisLinePoints);
  907. Assert.Contains (new Point (5, 28), axis.DrawAxisLinePoints);
  908. Assert.Contains (new Point (5, 29), axis.DrawAxisLinePoints);
  909. Assert.DoesNotContain (new Point (5, 30), axis.DrawAxisLinePoints);
  910. Assert.InRange (axis.LabelPoints.Max (), 0, 29);
  911. Assert.InRange (axis.LabelPoints.Min (), 0, 29);
  912. // Shutdown must be called to safely clean up Application if Init has been called
  913. Application.Shutdown ();
  914. }
  915. #endregion
  916. }
  917. public class TextAnnotationTests {
  918. readonly ITestOutputHelper output;
  919. public TextAnnotationTests (ITestOutputHelper output)
  920. {
  921. this.output = output;
  922. }
  923. [Fact]
  924. public void TestTextAnnotation_ScreenUnits ()
  925. {
  926. var gv = GraphViewTests.GetGraph ();
  927. gv.Annotations.Add (new TextAnnotation () {
  928. Text = "hey!",
  929. ScreenPosition = new Point (3, 1)
  930. });
  931. gv.Redraw (gv.Bounds);
  932. var expected =
  933. @"
  934. ┤ hey!
  935. 0┼┬┬┬┬┬┬┬┬
  936. 0 5";
  937. GraphViewTests.AssertDriverContentsAre (expected, output);
  938. // user scrolls up one unit of graph space
  939. gv.ScrollOffset = new PointF (0, 1f);
  940. gv.Redraw (gv.Bounds);
  941. // we expect no change in the location of the annotation (only the axis label changes)
  942. // this is because screen units are constant and do not change as the viewport into
  943. // graph space scrolls to different areas of the graph
  944. expected =
  945. @"
  946. ┤ hey!
  947. 1┼┬┬┬┬┬┬┬┬
  948. 0 5";
  949. GraphViewTests.AssertDriverContentsAre (expected, output);
  950. // Shutdown must be called to safely clean up Application if Init has been called
  951. Application.Shutdown ();
  952. }
  953. [Fact]
  954. public void TestTextAnnotation_GraphUnits ()
  955. {
  956. var gv = GraphViewTests.GetGraph ();
  957. gv.Annotations.Add (new TextAnnotation () {
  958. Text = "hey!",
  959. GraphPosition = new PointF (2, 2)
  960. });
  961. gv.Redraw (gv.Bounds);
  962. var expected =
  963. @"
  964. ┤ hey!
  965. 0┼┬┬┬┬┬┬┬┬
  966. 0 5";
  967. GraphViewTests.AssertDriverContentsAre (expected, output);
  968. // user scrolls up one unit of graph space
  969. gv.ScrollOffset = new PointF (0, 1f);
  970. gv.Redraw (gv.Bounds);
  971. // we expect the text annotation to go down one line since
  972. // the scroll offset means that that point of graph space is
  973. // lower down in the view. Note the 1 on the axis too, our viewport
  974. // (excluding margins) now shows y of 1 to 4 (previously 0 to 5)
  975. expected =
  976. @"
  977. ┤ hey!
  978. 1┼┬┬┬┬┬┬┬┬
  979. 0 5";
  980. GraphViewTests.AssertDriverContentsAre (expected, output);
  981. // Shutdown must be called to safely clean up Application if Init has been called
  982. Application.Shutdown ();
  983. }
  984. [Fact]
  985. public void TestTextAnnotation_LongText ()
  986. {
  987. var gv = GraphViewTests.GetGraph ();
  988. gv.Annotations.Add (new TextAnnotation () {
  989. Text = "hey there partner hows it going boy its great",
  990. GraphPosition = new PointF (2, 2)
  991. });
  992. gv.Redraw (gv.Bounds);
  993. // long text should get truncated
  994. // margin takes up 1 units
  995. // the GraphPosition of the anntation is 2
  996. // Leaving 7 characters of the annotation renderable (including space)
  997. var expected =
  998. @"
  999. ┤ hey the
  1000. 0┼┬┬┬┬┬┬┬┬
  1001. 0 5";
  1002. GraphViewTests.AssertDriverContentsAre (expected, output);
  1003. // Shutdown must be called to safely clean up Application if Init has been called
  1004. Application.Shutdown ();
  1005. }
  1006. [Fact]
  1007. public void TestTextAnnotation_Offscreen ()
  1008. {
  1009. var gv = GraphViewTests.GetGraph ();
  1010. gv.Annotations.Add (new TextAnnotation () {
  1011. Text = "hey there partner hows it going boy its great",
  1012. GraphPosition = new PointF (9, 2)
  1013. });
  1014. gv.Redraw (gv.Bounds);
  1015. // Text is off the screen (graph x axis runs to 8 not 9)
  1016. var expected =
  1017. @"
  1018. 0┼┬┬┬┬┬┬┬┬
  1019. 0 5";
  1020. GraphViewTests.AssertDriverContentsAre (expected, output);
  1021. // Shutdown must be called to safely clean up Application if Init has been called
  1022. Application.Shutdown ();
  1023. }
  1024. [Theory]
  1025. [InlineData (null)]
  1026. [InlineData (" ")]
  1027. [InlineData ("\t\t")]
  1028. public void TestTextAnnotation_EmptyText (string whitespace)
  1029. {
  1030. var gv = GraphViewTests.GetGraph ();
  1031. gv.Annotations.Add (new TextAnnotation () {
  1032. Text = whitespace,
  1033. GraphPosition = new PointF (4, 2)
  1034. });
  1035. // add a point a bit further along the graph so if the whitespace were rendered
  1036. // the test would pick it up (AssertDriverContentsAre ignores trailing whitespace on lines)
  1037. var points = new ScatterSeries ();
  1038. points.Points.Add (new PointF (7, 2));
  1039. gv.Series.Add (points);
  1040. gv.Redraw (gv.Bounds);
  1041. var expected =
  1042. @"
  1043. ┤ x
  1044. 0┼┬┬┬┬┬┬┬┬
  1045. 0 5";
  1046. GraphViewTests.AssertDriverContentsAre (expected, output);
  1047. // Shutdown must be called to safely clean up Application if Init has been called
  1048. Application.Shutdown ();
  1049. }
  1050. }
  1051. public class LegendTests {
  1052. readonly ITestOutputHelper output;
  1053. public LegendTests (ITestOutputHelper output)
  1054. {
  1055. this.output = output;
  1056. }
  1057. [Fact]
  1058. public void LegendNormalUsage_WithBorder ()
  1059. {
  1060. var gv = GraphViewTests.GetGraph ();
  1061. var legend = new LegendAnnotation (new Rect (2, 0, 5, 3));
  1062. legend.AddEntry (new GraphCellToRender ('A'), "Ant");
  1063. legend.AddEntry (new GraphCellToRender ('B'), "Bat");
  1064. gv.Annotations.Add (legend);
  1065. gv.Redraw (gv.Bounds);
  1066. var expected =
  1067. @"
  1068. │┌───┐
  1069. ┤│AAn│
  1070. ┤└───┘
  1071. 0┼┬┬┬┬┬┬┬┬
  1072. 0 5";
  1073. GraphViewTests.AssertDriverContentsAre (expected, output);
  1074. // Shutdown must be called to safely clean up Application if Init has been called
  1075. Application.Shutdown ();
  1076. }
  1077. [Fact]
  1078. public void LegendNormalUsage_WithoutBorder ()
  1079. {
  1080. var gv = GraphViewTests.GetGraph ();
  1081. var legend = new LegendAnnotation (new Rect (2, 0, 5, 3));
  1082. legend.AddEntry (new GraphCellToRender ('A'), "Ant");
  1083. legend.AddEntry (new GraphCellToRender ('B'), "?"); // this will exercise pad
  1084. legend.AddEntry (new GraphCellToRender ('C'), "Cat");
  1085. legend.AddEntry (new GraphCellToRender ('H'), "Hattter"); // not enough space for this oen
  1086. legend.Border = false;
  1087. gv.Annotations.Add (legend);
  1088. gv.Redraw (gv.Bounds);
  1089. var expected =
  1090. @"
  1091. │AAnt
  1092. ┤B?
  1093. ┤CCat
  1094. 0┼┬┬┬┬┬┬┬┬
  1095. 0 5";
  1096. GraphViewTests.AssertDriverContentsAre (expected, output);
  1097. // Shutdown must be called to safely clean up Application if Init has been called
  1098. Application.Shutdown ();
  1099. }
  1100. }
  1101. public class PathAnnotationTests {
  1102. readonly ITestOutputHelper output;
  1103. public PathAnnotationTests (ITestOutputHelper output)
  1104. {
  1105. this.output = output;
  1106. }
  1107. [Fact]
  1108. public void PathAnnotation_Box ()
  1109. {
  1110. var gv = GraphViewTests.GetGraph ();
  1111. var path = new PathAnnotation ();
  1112. path.Points.Add (new PointF (1, 1));
  1113. path.Points.Add (new PointF (1, 3));
  1114. path.Points.Add (new PointF (6, 3));
  1115. path.Points.Add (new PointF (6, 1));
  1116. // list the starting point again so that it draws a complete square
  1117. // (otherwise it will miss out the last line along the bottom)
  1118. path.Points.Add (new PointF (1, 1));
  1119. gv.Annotations.Add (path);
  1120. gv.Redraw (gv.Bounds);
  1121. var expected =
  1122. @"
  1123. │......
  1124. ┤. .
  1125. ┤......
  1126. 0┼┬┬┬┬┬┬┬┬
  1127. 0 5";
  1128. GraphViewTests.AssertDriverContentsAre (expected, output);
  1129. // Shutdown must be called to safely clean up Application if Init has been called
  1130. Application.Shutdown ();
  1131. }
  1132. [Fact]
  1133. public void YAxisLabels_With_MarginBottom ()
  1134. {
  1135. GraphViewTests.InitFakeDriver ();
  1136. var gv = new GraphView {
  1137. ColorScheme = new ColorScheme (),
  1138. Bounds = new Rect (0, 0, 10, 7)
  1139. };
  1140. gv.CellSize = new PointF (1, 0.5f);
  1141. gv.AxisY.Increment = 1;
  1142. gv.AxisY.ShowLabelsEvery = 1;
  1143. gv.Series.Add (new ScatterSeries {
  1144. Points = { new PointF (1, 1), new PointF (5, 0) }
  1145. });
  1146. // reserve 3 cells of the console for the margin
  1147. gv.MarginBottom = 3;
  1148. gv.MarginLeft = 1;
  1149. gv.Redraw (gv.Bounds);
  1150. var expected =
  1151. @"
  1152. 1┤x
  1153. 0┼┬┬┬┬x┬┬┬
  1154. 0 5
  1155. ";
  1156. GraphViewTests.AssertDriverContentsAre (expected, output);
  1157. // Shutdown must be called to safely clean up Application if Init has been called
  1158. Application.Shutdown ();
  1159. }
  1160. [Fact]
  1161. public void XAxisLabels_With_MarginLeft ()
  1162. {
  1163. GraphViewTests.InitFakeDriver ();
  1164. var gv = new GraphView {
  1165. ColorScheme = new ColorScheme (),
  1166. Bounds = new Rect (0, 0, 10, 7)
  1167. };
  1168. gv.CellSize = new PointF (1, 0.5f);
  1169. gv.AxisY.Increment = 1;
  1170. gv.AxisY.ShowLabelsEvery = 1;
  1171. gv.Series.Add (new ScatterSeries {
  1172. Points = { new PointF (1, 1), new PointF (5, 0) }
  1173. });
  1174. // reserve 3 cells of the left for the margin
  1175. gv.MarginLeft = 3;
  1176. gv.MarginBottom = 1;
  1177. gv.Redraw (gv.Bounds);
  1178. var expected =
  1179. @"
  1180. 2┤
  1181. 1┤x
  1182. 0┼┬┬┬┬x┬
  1183. 0 5
  1184. ";
  1185. GraphViewTests.AssertDriverContentsAre (expected, output);
  1186. // Shutdown must be called to safely clean up Application if Init has been called
  1187. Application.Shutdown ();
  1188. }
  1189. [Fact]
  1190. public void MarginBottom_BiggerThanHeight_ExpectBlankGraph ()
  1191. {
  1192. var gv = GraphViewTests.GetGraph ();
  1193. gv.Height = 10;
  1194. gv.MarginBottom = 20;
  1195. gv.Series.Add (new ScatterSeries {
  1196. Points = { new PointF (1, 1), new PointF (5, 0) }
  1197. });
  1198. gv.Redraw (gv.Bounds);
  1199. var expected =
  1200. @"
  1201. ";
  1202. GraphViewTests.AssertDriverContentsAre (expected, output);
  1203. // Shutdown must be called to safely clean up Application if Init has been called
  1204. Application.Shutdown ();
  1205. }
  1206. [Fact]
  1207. public void MarginLeft_BiggerThanWidth_ExpectBlankGraph ()
  1208. {
  1209. var gv = GraphViewTests.GetGraph ();
  1210. gv.Width = 10;
  1211. gv.MarginLeft = 20;
  1212. gv.Series.Add (new ScatterSeries {
  1213. Points = { new PointF (1, 1), new PointF (5, 0) }
  1214. });
  1215. gv.Redraw (gv.Bounds);
  1216. var expected =
  1217. @"
  1218. ";
  1219. GraphViewTests.AssertDriverContentsAre (expected, output);
  1220. // Shutdown must be called to safely clean up Application if Init has been called
  1221. Application.Shutdown ();
  1222. }
  1223. [Fact]
  1224. public void PathAnnotation_Diamond ()
  1225. {
  1226. var gv = GraphViewTests.GetGraph ();
  1227. var path = new PathAnnotation ();
  1228. path.Points.Add (new PointF (1, 2));
  1229. path.Points.Add (new PointF (3, 3));
  1230. path.Points.Add (new PointF (6, 2));
  1231. path.Points.Add (new PointF (3, 1));
  1232. // list the starting point again to close the shape
  1233. path.Points.Add (new PointF (1, 2));
  1234. gv.Annotations.Add (path);
  1235. gv.Redraw (gv.Bounds);
  1236. var expected =
  1237. @"
  1238. │ ..
  1239. ┤.. ..
  1240. ┤ ...
  1241. 0┼┬┬┬┬┬┬┬┬
  1242. 0 5";
  1243. GraphViewTests.AssertDriverContentsAre (expected, output);
  1244. // Shutdown must be called to safely clean up Application if Init has been called
  1245. Application.Shutdown ();
  1246. }
  1247. [Theory]
  1248. [InlineData (true)]
  1249. [InlineData (false)]
  1250. public void LabelChangeText_RendersCorrectly (bool useFill)
  1251. {
  1252. var driver = new FakeDriver ();
  1253. Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
  1254. driver.Init (() => { });
  1255. // create a wide window
  1256. var mount = new View () {
  1257. Width = 100,
  1258. Height = 100
  1259. };
  1260. try {
  1261. // Create a label with a short text
  1262. var lbl1 = new Label ("ff");
  1263. // Specify that the label should be very wide
  1264. if (useFill) {
  1265. lbl1.Width = Dim.Fill ();
  1266. } else {
  1267. lbl1.Width = 100;
  1268. }
  1269. //put label into view
  1270. mount.Add (lbl1);
  1271. // render view
  1272. lbl1.ColorScheme = new ColorScheme ();
  1273. Assert.Equal (1, lbl1.Height);
  1274. mount.Redraw (mount.Bounds);
  1275. // should have the initial text
  1276. GraphViewTests.AssertDriverContentsAre ("ff", null);
  1277. // change the text and redraw
  1278. lbl1.Text = "ff1234";
  1279. mount.Redraw (mount.Bounds);
  1280. // should have the new text rendered
  1281. GraphViewTests.AssertDriverContentsAre ("ff1234", null);
  1282. } finally {
  1283. Application.Shutdown ();
  1284. }
  1285. }
  1286. }
  1287. public class AxisIncrementToRenderTests {
  1288. [Fact]
  1289. public void AxisIncrementToRenderTests_Constructor ()
  1290. {
  1291. var render = new AxisIncrementToRender (Orientation.Horizontal, 1, 6.6f);
  1292. Assert.Equal (Orientation.Horizontal, render.Orientation);
  1293. Assert.Equal (1, render.ScreenLocation);
  1294. Assert.Equal (6.6f, render.Value);
  1295. }
  1296. }
  1297. }