PageRenderTime 52ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/Plugins/Statistics/GitImpact/ImpactControl.cs

https://github.com/eisnerd/gitextensions
C# | 508 lines | 349 code | 86 blank | 73 comment | 30 complexity | 163322fcb635902efbb77a41067ec2ce MD5 | raw file
Possible License(s): GPL-3.0, GPL-2.0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Drawing;
  5. using System.Drawing.Drawing2D;
  6. using System.Data;
  7. using System.Linq;
  8. using System.Text;
  9. using System.Windows.Forms;
  10. using GitCommands.Statistics;
  11. using GitUI;
  12. namespace GitImpact
  13. {
  14. public class ImpactControl : GitExtensionsControl
  15. {
  16. private const int block_width = 60;
  17. private const int transition_width = 50;
  18. private const int lines_font_size = 10;
  19. private const int week_font_size = 8;
  20. private Object data_lock = new Object();
  21. private ImpactLoader impact_loader;
  22. // <Author, <Commits, Added Lines, Deleted Lines>>
  23. private Dictionary<string, ImpactLoader.DataPoint> authors;
  24. // <First weekday of commit date, <Author, <Commits, Added Lines, Deleted Lines>>>
  25. private SortedDictionary<DateTime, Dictionary<string, ImpactLoader.DataPoint>> impact;
  26. // List of authors that determines the drawing order
  27. private List<string> author_stack;
  28. // The paths for each author
  29. private Dictionary<string, GraphicsPath> paths;
  30. // The brush for each author
  31. private Dictionary<string, SolidBrush> brushes;
  32. // The changed-lines-labels for each author
  33. private Dictionary<string, List<Tuple<PointF, int>>> line_labels;
  34. // The week-labels
  35. private List<Tuple<PointF, DateTime>> week_labels;
  36. private HScrollBar scrollBar;
  37. public ImpactControl()
  38. {
  39. impact_loader = new ImpactLoader();
  40. impact_loader.RespectMailmap = true; // respect the .mailmap file
  41. impact_loader.Updated += OnImpactUpdate;
  42. Clear();
  43. InitializeComponent();
  44. Translate();
  45. // Set DoubleBuffer flag for flicker-free drawing
  46. this.SetStyle(ControlStyles.AllPaintingInWmPaint |
  47. ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
  48. MouseWheel += ImpactControl_MouseWheel;
  49. }
  50. private void Clear()
  51. {
  52. lock (data_lock)
  53. {
  54. authors = new Dictionary<string, ImpactLoader.DataPoint>();
  55. impact = new SortedDictionary<DateTime, Dictionary<string, ImpactLoader.DataPoint>>();
  56. author_stack = new List<string>();
  57. paths = new Dictionary<string, GraphicsPath>();
  58. brushes = new Dictionary<string, SolidBrush>();
  59. line_labels = new Dictionary<string, List<Tuple<PointF, int>>>();
  60. week_labels = new List<Tuple<PointF, DateTime>>();
  61. }
  62. }
  63. public void Stop()
  64. {
  65. if (impact_loader != null)
  66. {
  67. impact_loader.Dispose();
  68. impact_loader = null;
  69. }
  70. }
  71. void ImpactControl_MouseWheel(object sender, MouseEventArgs e)
  72. {
  73. this.scrollBar.Value = Math.Min(this.scrollBar.Maximum, Math.Max(this.scrollBar.Minimum, this.scrollBar.Value + e.Delta));
  74. // Redraw when we've scrolled
  75. Invalidate();
  76. }
  77. void OnImpactUpdate(ImpactLoader.Commit commit)
  78. {
  79. lock (data_lock)
  80. {
  81. // UPDATE IMPACT
  82. // If week does not exist yet in the impact dictionary
  83. if (!impact.ContainsKey(commit.week))
  84. // Create it
  85. impact.Add(commit.week, new Dictionary<string, ImpactLoader.DataPoint>());
  86. // If author does not exist yet for this week in the impact dictionary
  87. if (!impact[commit.week].ContainsKey(commit.author))
  88. // Create it
  89. impact[commit.week].Add(commit.author, commit.data);
  90. else
  91. // Otherwise just add the changes
  92. impact[commit.week][commit.author] += commit.data;
  93. // UPDATE AUTHORS
  94. // If author does not exist yet in the authors dictionary
  95. if (!authors.ContainsKey(commit.author))
  96. // Create it
  97. authors.Add(commit.author, commit.data);
  98. else
  99. // Otherwise just add the changes
  100. authors[commit.author] += commit.data;
  101. // Add authors to intermediate weeks where they didn't create commits
  102. ImpactLoader.AddIntermediateEmptyWeeks(ref impact, authors);
  103. // UPDATE AUTHORSTACK
  104. // If author does not exist yet in the author_stack
  105. if (!author_stack.Contains(commit.author))
  106. // Add it to the front (drawn first)
  107. author_stack.Insert(0, commit.author);
  108. }
  109. UpdatePathsAndLabels();
  110. Invalidate();
  111. }
  112. public void UpdateData()
  113. {
  114. impact_loader.ShowSubmodules = showSubmodules;
  115. impact_loader.Execute();
  116. }
  117. private bool showSubmodules;
  118. public bool ShowSubmodules
  119. {
  120. get { return showSubmodules; }
  121. set
  122. {
  123. showSubmodules = value;
  124. impact_loader.Dispose();
  125. Clear();
  126. UpdateData();
  127. }
  128. }
  129. private void InitializeComponent()
  130. {
  131. this.scrollBar = new System.Windows.Forms.HScrollBar();
  132. this.SuspendLayout();
  133. //
  134. // scrollBar
  135. //
  136. this.scrollBar.Dock = System.Windows.Forms.DockStyle.Bottom;
  137. this.scrollBar.LargeChange = 0;
  138. this.scrollBar.Location = new System.Drawing.Point(0, 133);
  139. this.scrollBar.Maximum = 0;
  140. this.scrollBar.Name = "scrollBar";
  141. this.scrollBar.Size = new System.Drawing.Size(150, 17);
  142. this.scrollBar.SmallChange = 0;
  143. this.scrollBar.TabIndex = 0;
  144. this.scrollBar.Scroll += this.OnScroll;
  145. //
  146. // ImpactControl
  147. //
  148. this.Controls.Add(this.scrollBar);
  149. this.Name = "ImpactControl";
  150. this.Paint += this.OnPaint;
  151. this.Resize += this.OnResize;
  152. this.ResumeLayout(false);
  153. }
  154. private int GetGraphWidth()
  155. {
  156. lock (data_lock)
  157. {
  158. return Math.Max(0, impact.Count * (block_width + transition_width) - transition_width);
  159. }
  160. }
  161. private void UpdateScrollbar()
  162. {
  163. lock (data_lock)
  164. {
  165. int RightValue = Math.Max(0, scrollBar.Maximum - scrollBar.LargeChange - scrollBar.Value);
  166. scrollBar.Minimum = 0;
  167. scrollBar.Maximum = (int)(Math.Max(0, GetGraphWidth() - ClientSize.Width) * 1.1);
  168. scrollBar.SmallChange = scrollBar.Maximum / 22;
  169. scrollBar.LargeChange = scrollBar.Maximum / 11;
  170. scrollBar.Value = Math.Max(0, scrollBar.Maximum - scrollBar.LargeChange - RightValue);
  171. }
  172. }
  173. private void OnPaint(object sender, PaintEventArgs e)
  174. {
  175. // White background
  176. e.Graphics.Clear(Color.White);
  177. UpdateScrollbar();
  178. lock (data_lock)
  179. {
  180. // Nothing to draw
  181. if (impact.Count == 0)
  182. return;
  183. // Activate AntiAliasing
  184. e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
  185. // "Scroll" to the right position
  186. e.Graphics.TranslateTransform(-scrollBar.Value, 0);
  187. // Draw paths in order of the author_stack
  188. // Default: person with least number of changed lines first, others on top
  189. foreach (var author in author_stack)
  190. {
  191. if (brushes.ContainsKey(author) && paths.ContainsKey(author))
  192. e.Graphics.FillPath(brushes[author], paths[author]);
  193. }
  194. //Draw black border around selected author
  195. string selectedAuthor = author_stack[author_stack.Count - 1];
  196. if (brushes.ContainsKey(selectedAuthor) && paths.ContainsKey(selectedAuthor))
  197. e.Graphics.DrawPath(new Pen(Color.Black, 2), paths[selectedAuthor]);
  198. foreach (var author in author_stack)
  199. DrawAuthorLinesLabels(e.Graphics, author);
  200. }
  201. DrawWeekLabels(e.Graphics);
  202. }
  203. private void DrawAuthorLinesLabels(Graphics g, string author)
  204. {
  205. lock (data_lock)
  206. {
  207. if (!line_labels.ContainsKey(author))
  208. return;
  209. Font font = new Font("Arial", lines_font_size);
  210. Brush brush = new SolidBrush(Color.White);
  211. foreach (var label in line_labels[author])
  212. {
  213. SizeF sz = g.MeasureString(label.Item2.ToString(), font);
  214. PointF pt = new PointF(label.Item1.X - sz.Width / 2, label.Item1.Y - sz.Height / 2);
  215. g.DrawString(label.Item2.ToString(), font, brush, pt);
  216. }
  217. }
  218. }
  219. private void DrawWeekLabels(Graphics g)
  220. {
  221. lock (data_lock)
  222. {
  223. Font font = new Font("Arial", week_font_size);
  224. Brush brush = new SolidBrush(Color.Gray);
  225. foreach (var label in week_labels)
  226. {
  227. SizeF sz = g.MeasureString(label.Item2.ToString("dd. MMM yy"), font);
  228. PointF pt = new PointF(label.Item1.X - sz.Width / 2, label.Item1.Y + sz.Height / 2);
  229. g.DrawString(label.Item2.ToString("dd. MMM yy"), font, brush, pt);
  230. }
  231. }
  232. }
  233. private void OnResize(object sender, EventArgs e)
  234. {
  235. UpdatePathsAndLabels();
  236. UpdateScrollbar();
  237. Invalidate();
  238. }
  239. private void UpdatePathsAndLabels()
  240. {
  241. int h_max = 0;
  242. int x = 0;
  243. Dictionary<string, List<Tuple<Rectangle, int>>> author_points_dict = new Dictionary<string, List<Tuple<Rectangle, int>>>();
  244. lock (data_lock)
  245. {
  246. // Clear previous week labels
  247. week_labels.Clear();
  248. // Iterate through weeks
  249. foreach (var week in impact)
  250. {
  251. int y = 0;
  252. // Iterate through authors
  253. foreach (var pair in (from entry in week.Value orderby entry.Value.ChangedLines descending select entry))
  254. {
  255. string author = pair.Key;
  256. // Calculate week-author-rectangle
  257. int height = Math.Max(1, (int)Math.Round(Math.Pow(Math.Log(pair.Value.ChangedLines), 1.5) * 4));
  258. Rectangle rc = new Rectangle(x, y, block_width, height);
  259. // Add rectangle to temporary list
  260. if (!author_points_dict.ContainsKey(author))
  261. author_points_dict.Add(author, new List<Tuple<Rectangle, int>>());
  262. author_points_dict[author].Add(Tuple.Create(rc, pair.Value.ChangedLines));
  263. // Create a new random brush for the author if none exists yet
  264. if (!brushes.ContainsKey(author))
  265. {
  266. int partLength = author.Length / 3;
  267. brushes.Add(author, new SolidBrush(Color.FromArgb(GenerateIntFromString(author.Substring(0, partLength)) % 255, GenerateIntFromString(author.Substring(partLength, partLength)) % 255, GenerateIntFromString(author.Substring(partLength)) % 255)));
  268. }
  269. // Increase y for next block
  270. y += rc.Height + 2;
  271. }
  272. // Remember total height of largest week
  273. h_max = Math.Max(h_max, y);
  274. // Add week date label
  275. week_labels.Add(Tuple.Create(new PointF(x + block_width / 2, y), week.Key));
  276. // Increase x for next week
  277. x += block_width + transition_width;
  278. }
  279. // Pre-calculate height scale factor
  280. double height_factor = 0.9 * (float)Height / (float)h_max;
  281. // Scale week label coordinates
  282. for (int i = 0; i < week_labels.Count; i++)
  283. week_labels[i] = Tuple.Create(new PointF(week_labels[i].Item1.X, week_labels[i].Item1.Y * (float)height_factor),
  284. week_labels[i].Item2);
  285. // Clear previous paths
  286. paths.Clear();
  287. // Clear previous labels
  288. line_labels.Clear();
  289. // Add points to each author's GraphicsPath
  290. foreach (var author_points in author_points_dict)
  291. {
  292. string author = author_points.Key;
  293. // Scale heights
  294. for (int i = 0; i < author_points.Value.Count; i++)
  295. {
  296. author_points.Value[i] = Tuple.Create(
  297. new Rectangle(author_points.Value[i].Item1.Left, (int)(author_points.Value[i].Item1.Top * height_factor),
  298. author_points.Value[i].Item1.Width, Math.Max(1, (int)(author_points.Value[i].Item1.Height * height_factor))),
  299. author_points.Value[i].Item2);
  300. // Add lines-changed-labels
  301. if (!line_labels.ContainsKey(author))
  302. line_labels.Add(author, new List<Tuple<PointF, int>>());
  303. if (author_points.Value[i].Item1.Height > lines_font_size * 1.5)
  304. line_labels[author].Add(Tuple.Create(new PointF(author_points.Value[i].Item1.Left + block_width / 2,
  305. author_points.Value[i].Item1.Top + author_points.Value[i].Item1.Height / 2),
  306. author_points.Value[i].Item2));
  307. }
  308. paths.Add(author, new GraphicsPath());
  309. // Left border
  310. paths[author].AddLine(author_points.Value[0].Item1.Left, author_points.Value[0].Item1.Bottom,
  311. author_points.Value[0].Item1.Left, author_points.Value[0].Item1.Top);
  312. // Top borders
  313. for (int i = 0; i < author_points.Value.Count; i++)
  314. {
  315. paths[author].AddLine(author_points.Value[i].Item1.Left, author_points.Value[i].Item1.Top,
  316. author_points.Value[i].Item1.Right, author_points.Value[i].Item1.Top);
  317. if (i < author_points.Value.Count - 1)
  318. paths[author].AddBezier(author_points.Value[i].Item1.Right, author_points.Value[i].Item1.Top,
  319. author_points.Value[i].Item1.Right + transition_width / 2, author_points.Value[i].Item1.Top,
  320. author_points.Value[i].Item1.Right + transition_width / 2, author_points.Value[i + 1].Item1.Top,
  321. author_points.Value[i + 1].Item1.Left, author_points.Value[i + 1].Item1.Top);
  322. }
  323. // Right border
  324. paths[author].AddLine(author_points.Value[author_points.Value.Count - 1].Item1.Right,
  325. author_points.Value[author_points.Value.Count - 1].Item1.Top,
  326. author_points.Value[author_points.Value.Count - 1].Item1.Right,
  327. author_points.Value[author_points.Value.Count - 1].Item1.Bottom);
  328. // Bottom borders
  329. for (int i = author_points.Value.Count - 1; i >= 0; i--)
  330. {
  331. paths[author].AddLine(author_points.Value[i].Item1.Right, author_points.Value[i].Item1.Bottom,
  332. author_points.Value[i].Item1.Left, author_points.Value[i].Item1.Bottom);
  333. if (i > 0)
  334. paths[author].AddBezier(author_points.Value[i].Item1.Left, author_points.Value[i].Item1.Bottom,
  335. author_points.Value[i].Item1.Left - transition_width / 2, author_points.Value[i].Item1.Bottom,
  336. author_points.Value[i].Item1.Left - transition_width / 2, author_points.Value[i - 1].Item1.Bottom,
  337. author_points.Value[i - 1].Item1.Right, author_points.Value[i - 1].Item1.Bottom);
  338. }
  339. }
  340. }
  341. }
  342. private int GenerateIntFromString(string text)
  343. {
  344. int number = 0;
  345. foreach (char c in text)
  346. {
  347. number += (int)c;
  348. }
  349. return number;
  350. }
  351. /// <summary>
  352. /// Determines if the given coordinates are belonging to any author
  353. /// </summary>
  354. /// <param name="x">x coordinate</param>
  355. /// <param name="y">y coordinate</param>
  356. /// <returns>Name of the author</returns>
  357. public string GetAuthorByScreenPosition(int x, int y)
  358. {
  359. lock (data_lock)
  360. {
  361. foreach (var author in author_stack.Reverse<string>())
  362. if (paths.ContainsKey(author) && paths[author].IsVisible(x + scrollBar.Value, y))
  363. return author;
  364. }
  365. return "";
  366. }
  367. /// <summary>
  368. /// Pushes the author to the top of the author_stack
  369. /// </summary>
  370. /// <param name="author">Name of the author</param>
  371. public bool SelectAuthor(string author)
  372. {
  373. lock (data_lock)
  374. {
  375. if (!author_stack.Contains(author))
  376. return false;
  377. // Remove author from the stack
  378. author_stack.Remove(author);
  379. // and add it again at the end
  380. author_stack.Add(author);
  381. }
  382. return true;
  383. }
  384. /// <summary>
  385. /// Returns the selected author
  386. /// </summary>
  387. public string GetSelectedAuthor()
  388. {
  389. if (author_stack.Count == 0)
  390. return string.Empty;
  391. lock (data_lock)
  392. {
  393. return author_stack.Last();
  394. }
  395. }
  396. private void OnScroll(object sender, ScrollEventArgs e)
  397. {
  398. // Redraw when we've scrolled
  399. Invalidate();
  400. }
  401. public Color GetAuthorColor(string author)
  402. {
  403. lock (data_lock)
  404. {
  405. if (brushes.ContainsKey(author))
  406. return brushes[author].Color;
  407. }
  408. return Color.Transparent;
  409. }
  410. [Browsable(false)]
  411. public List<string> Authors { get { lock(data_lock) return author_stack; } }
  412. public ImpactLoader.DataPoint GetAuthorInfo(string author)
  413. {
  414. lock (data_lock)
  415. {
  416. if (authors.ContainsKey(author))
  417. return authors[author];
  418. return new ImpactLoader.DataPoint(0, 0, 0);
  419. }
  420. }
  421. }
  422. }