From c52ed4a711c7b59b5cd230dd21a9f45e52e4a316 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 11 Jan 2022 20:18:35 +0800 Subject: [PATCH] feature: add simple statistic page --- src/Models/StatisticSample.cs | 15 +++ src/Resources/Icons.xaml | 1 + src/Resources/Locales/en_US.xaml | 9 ++ src/Resources/Locales/zh_CN.xaml | 9 ++ src/Resources/Styles/TabControl.xaml | 33 +++++ src/Views/Controls/Chart.cs | 118 ++++++++++++++++ src/Views/Statistics.xaml | 195 +++++++++++++++++++++++++++ src/Views/Statistics.xaml.cs | 120 +++++++++++++++++ src/Views/Widgets/Dashboard.xaml | 7 + src/Views/Widgets/Dashboard.xaml.cs | 5 + 10 files changed, 512 insertions(+) create mode 100644 src/Models/StatisticSample.cs create mode 100644 src/Views/Controls/Chart.cs create mode 100644 src/Views/Statistics.xaml create mode 100644 src/Views/Statistics.xaml.cs diff --git a/src/Models/StatisticSample.cs b/src/Models/StatisticSample.cs new file mode 100644 index 00000000..1e59371b --- /dev/null +++ b/src/Models/StatisticSample.cs @@ -0,0 +1,15 @@ +namespace SourceGit.Models { + /// + /// 统计图表样品 + /// + public class StatisticSample { + /// + /// 样品名 + /// + public string Name { get; set; } + /// + /// 提交个数 + /// + public int Count { get; set; } + } +} diff --git a/src/Resources/Icons.xaml b/src/Resources/Icons.xaml index 9d50a679..47faa94b 100644 --- a/src/Resources/Icons.xaml +++ b/src/Resources/Icons.xaml @@ -35,6 +35,7 @@ M716.3 383.1c0 38.4-6.5 76-19.4 111.8l-10.7 29.7 229.6 229.5c44.5 44.6 44.5 117.1 0 161.6a113.6 113.6 0 01-80.8 33.5a113.6 113.6 0 01-80.8-33.5L529 694l-32 13a331.6 331.6 0 01-111.9 19.4A333.5 333.5 0 0150 383.1c0-39 6.8-77.2 20-113.6L285 482l194-195-214-210A331 331 0 01383.1 50A333.5 333.5 0 01716.3 383.1zM231.6 31.6l-22.9 9.9a22.2 22.2 0 00-5.9 4.2a19.5 19.5 0 000 27.5l215 215.2L288.4 417.8 77.8 207.1a26 26 0 00-17.2-7.1a22.8 22.8 0 00-21.5 15a400.5 400.5 0 00-7.5 16.6A381.6 381.6 0 000 384c0 211.7 172.2 384 384 384c44.3 0 87.6-7.5 129-22.3L743.1 975.8A163.5 163.5 0 00859.5 1024c43.9 0 85.3-17.1 116.4-48.2a164.8 164.8 0 000-233L745.5 513C760.5 471.5 768 428 768 384C768 172 596 0 384 0c-53 0-104 10.5-152.5 31.5z M928 500a21 21 0 00-19-20L858 472a11 11 0 01-9-9c-1-6-2-13-3-19a11 11 0 015-12l46-25a21 21 0 0010-26l-8-22a21 21 0 00-24-13l-51 10a11 11 0 01-12-6c-3-6-6-11-10-17a11 11 0 011-13l34-39a21 21 0 001-28l-15-18a20 20 0 00-27-4l-45 27a11 11 0 01-13-1c-5-4-10-9-15-12a11 11 0 01-3-12l19-49a21 21 0 00-9-26l-20-12a21 21 0 00-27 6L650 193a9 9 0 01-11 3c-1-1-12-5-20-7a11 11 0 01-7-10l1-52a21 21 0 00-17-22l-23-4a21 21 0 00-24 14L532 164a11 11 0 01-11 7h-20a11 11 0 01-11-7l-17-49a21 21 0 00-24-15l-23 4a21 21 0 00-17 22l1 52a11 11 0 01-8 11c-5 2-15 6-19 7c-4 1-8 0-12-4l-33-40A21 21 0 00313 146l-20 12A21 21 0 00285 184l19 49a11 11 0 01-3 12c-5 4-10 8-15 12a11 11 0 01-13 1L228 231a21 21 0 00-27 4L186 253a21 21 0 001 28L221 320a11 11 0 011 13c-3 5-7 11-10 17a11 11 0 01-12 6l-51-10a21 21 0 00-24 13l-8 22a21 21 0 0010 26l46 25a11 11 0 015 12l0 3c-1 6-2 11-3 16a11 11 0 01-9 9l-51 8A21 21 0 0096 500v23A21 21 0 00114 544l51 8a11 11 0 019 9c1 6 2 13 3 19a11 11 0 01-5 12l-46 25a21 21 0 00-10 26l8 22a21 21 0 0024 13l51-10a11 11 0 0112 6c3 6 6 11 10 17a11 11 0 01-1 13l-34 39a21 21 0 00-1 28l15 18a20 20 0 0027 4l45-27a11 11 0 0113 1c5 4 10 9 15 12a11 11 0 013 12l-19 49a21 21 0 009 26l20 12a21 21 0 0027-6L374 832c3-3 7-5 10-4c7 3 12 5 20 7a11 11 0 018 10l-1 52a21 21 0 0017 22l23 4a21 21 0 0024-14l17-50a11 11 0 0111-7h20a11 11 0 0111 7l17 49a21 21 0 0020 15a19 19 0 004 0l23-4a21 21 0 0017-22l-1-52a11 11 0 018-10c8-3 13-5 18-7l1 0c6-2 9 0 11 3l34 41A21 21 0 00710 878l20-12a21 21 0 009-26l-18-49a11 11 0 013-12c5-4 10-8 15-12a11 11 0 0113-1l45 27a21 21 0 0027-4l15-18a21 21 0 00-1-28l-34-39a11 11 0 01-1-13c3-5 7-11 10-17a11 11 0 0112-6l51 10a21 21 0 0024-13l8-22a21 21 0 00-10-26l-46-25a11 11 0 01-5-12l0-3c1-6 2-11 3-16a11 11 0 019-9l51-8a21 21 0 0018-21v-23zm-565 188a32 32 0 01-51 5a270 270 0 011-363a32 32 0 0151 6l91 161a32 32 0 010 31zM512 782a270 270 0 01-57-6a32 32 0 01-20-47l92-160a32 32 0 0127-16h184a32 32 0 0130 41c-35 109-137 188-257 188zm15-328L436 294a32 32 0 0121-47a268 268 0 0155-6c120 0 222 79 257 188a32 32 0 01-30 41h-184a32 32 0 01-28-16z + M296 912H120c-4.4 0-8-3.6-8-8V520c0-4.4 3.6-8 8-8h176c4.4 0 8 3.6 8 8v384c0 4.4-3.6 8-8 8zM600 912H424c-4.4 0-8-3.6-8-8V121c0-4.4 3.6-8 8-8h176c4.4 0 8 3.6 8 8v783c0 4.4-3.6 8-8 8zM904 912H728c-4.4 0-8-3.6-8-8V280c0-4.4 3.6-8 8-8h176c4.4 0 8 3.6 8 8v624c0 4.4-3.6 8-8 8z M550 627h-81v-21a142 142 0 0113-64a198 198 0 0152-57a390 390 0 0047-42a56 56 0 0012-34a58 58 0 00-21-45a81 81 0 00-56-19a85 85 0 00-57 20a103 103 0 00-32 59l-82-10a136 136 0 0149-96A172 172 0 01512 276a178 178 0 01123 40a122 122 0 0145 94a103 103 0 01-17 56a366 366 0 01-71 72A136 136 0 00556 576a128 128 0 00-6 51zm-81 120v-89h89v89zM512 64a448 448 0 10448 448A448 448 0 00512 64zm0 832a384 384 0 010-768a389 389 0 01384 384a389 389 0 01-384 384z M64 864h896V288h-396a64 64 0 01-57-35L460 160H64v704zm-64 32V128a32 32 0 0132-32h448a32 32 0 0129 18L564 224H992a32 32 0 0132 32v640a32 32 0 01-32 32H32a32 32 0 01-32-32z M448 64l128 128h448v768H0V64z diff --git a/src/Resources/Locales/en_US.xaml b/src/Resources/Locales/en_US.xaml index e6580b6c..53f73427 100644 --- a/src/Resources/Locales/en_US.xaml +++ b/src/Resources/Locales/en_US.xaml @@ -121,6 +121,7 @@ Open In Git Bash Refresh Search Commit + Statistics Configure this repository WORKSPACE LOCAL BRANCHES @@ -487,6 +488,14 @@ To : Reword : + Statistics + WEEK + MONTH + Total Committers: {0} + Total Commits:{0} + COMMITTER + COMMITS + Git has NOT been configured. Please to go [Preference] and configure it first. Path[{0}] not exists! Can NOT locate bash.exe. Make sure bash.exe exists under the same folder with git.exe diff --git a/src/Resources/Locales/zh_CN.xaml b/src/Resources/Locales/zh_CN.xaml index ad67a4b2..7aefa601 100644 --- a/src/Resources/Locales/zh_CN.xaml +++ b/src/Resources/Locales/zh_CN.xaml @@ -120,6 +120,7 @@ 在GIT终端中打开 重新加载 查找提交 + 提交统计 配置本仓库 工作区 本地分支 @@ -486,6 +487,14 @@ 合并到 : 修改提交信息: + 提交统计 + 本周 + 本月 + 提交者人数:{0} + 总计提交次数:{0} + 提交者 + 提交次数 + GIT尚未配置。请打开【偏好设置】配置GIT路径。 路径({0})不存在或不可读取! 无法找到bash.exe,请确保其在git.exe同目录中! diff --git a/src/Resources/Styles/TabControl.xaml b/src/Resources/Styles/TabControl.xaml index 87afce73..69cc26a8 100644 --- a/src/Resources/Styles/TabControl.xaml +++ b/src/Resources/Styles/TabControl.xaml @@ -80,4 +80,37 @@ + + \ No newline at end of file diff --git a/src/Views/Controls/Chart.cs b/src/Views/Controls/Chart.cs new file mode 100644 index 00000000..faa0652d --- /dev/null +++ b/src/Views/Controls/Chart.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Windows; +using System.Windows.Media; + +namespace SourceGit.Views.Controls { + /// + /// 绘制提交频率柱状图 + /// + public class Chart : FrameworkElement { + public static readonly int LABEL_UNIT = 32; + public static readonly double MAX_SHAPE_WIDTH = 24; + + public static readonly DependencyProperty LineBrushProperty = DependencyProperty.Register( + "LineBrush", + typeof(Brush), + typeof(Chart), + new PropertyMetadata(Brushes.White)); + + public Brush LineBrush { + get { return (Brush)GetValue(LineBrushProperty); } + set { SetValue(LineBrushProperty, value); } + } + + public static readonly DependencyProperty ChartBrushProperty = DependencyProperty.Register( + "ChartBrush", + typeof(Brush), + typeof(Chart), + new PropertyMetadata(Brushes.White)); + + public Brush ChartBrush { + get { return (Brush)GetValue(ChartBrushProperty); } + set { SetValue(ChartBrushProperty, value); } + } + + private int maxV = 0; + private List samples = new List(); + + /// + /// 设置绘制数据 + /// + /// 数据源 + public void SetData(List samples) { + this.samples = samples; + maxV = 0; + + foreach (var s in samples) { + if (maxV < s.Count) maxV = s.Count; + } + + maxV = (int)Math.Ceiling(maxV / 10.0) * 10; + InvalidateVisual(); + } + + protected override void OnRender(DrawingContext dc) { + base.OnRender(dc); + + var font = new FontFamily("Consolas"); + var pen = new Pen(LineBrush, 1); + dc.DrawLine(pen, new Point(LABEL_UNIT, 0), new Point(LABEL_UNIT, ActualHeight - LABEL_UNIT)); + dc.DrawLine(pen, new Point(LABEL_UNIT, ActualHeight - LABEL_UNIT), new Point(ActualWidth, ActualHeight - LABEL_UNIT)); + + if (samples.Count == 0) return; + + var stepV = (ActualHeight - LABEL_UNIT) / 5; + var labelStepV = maxV / 5; + var gridPen = new Pen(LineBrush, 1) { DashStyle = DashStyles.Dash }; + for (int i = 1; i < 5; i++) { + var vLabel = new FormattedText( + $"{maxV - i * labelStepV}", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(font, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal), + 12.0, + LineBrush, + VisualTreeHelper.GetDpi(this).PixelsPerDip); + + var dashHeight = i * stepV; + var vy = Math.Max(0, dashHeight - vLabel.Height * 0.5); + dc.DrawLine(gridPen, new Point(LABEL_UNIT + 1, dashHeight), new Point(ActualWidth, dashHeight)); + dc.DrawText(vLabel, new Point(0, vy)); + } + + var stepX = (ActualWidth - LABEL_UNIT) / samples.Count; + var shapeWidth = Math.Min(LABEL_UNIT, stepX - 4); + for (int i = 0; i < samples.Count; i++) { + var h = samples[i].Count * (ActualHeight - LABEL_UNIT) / maxV; + var x = LABEL_UNIT + 1 + stepX * i + (stepX - shapeWidth) * 0.5; + var y = ActualHeight - LABEL_UNIT - h; + var rect = new Rect(x, y, shapeWidth, h); + + var hLabel = new FormattedText( + samples[i].Name, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(font, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal), + 10.0, + LineBrush, + VisualTreeHelper.GetDpi(this).PixelsPerDip); + var xLabel = x - (hLabel.Width - shapeWidth) * 0.5; + var yLabel = ActualHeight - LABEL_UNIT + 4; + + dc.DrawRectangle(ChartBrush, null, rect); + + if (stepX < LABEL_UNIT) { + dc.PushTransform(new TranslateTransform(xLabel, yLabel)); + dc.PushTransform(new RotateTransform(45, hLabel.Width * 0.5, hLabel.Height * 0.5)); + dc.DrawText(hLabel, new Point(0, 0)); + dc.Pop(); + dc.Pop(); + } else { + dc.DrawText(hLabel, new Point(xLabel, yLabel)); + } + } + } + } +} diff --git a/src/Views/Statistics.xaml b/src/Views/Statistics.xaml new file mode 100644 index 00000000..68ab4b15 --- /dev/null +++ b/src/Views/Statistics.xaml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Statistics.xaml.cs b/src/Views/Statistics.xaml.cs new file mode 100644 index 00000000..25bf0062 --- /dev/null +++ b/src/Views/Statistics.xaml.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows; + +namespace SourceGit.Views { + /// + /// 提交统计 + /// + public partial class Statistics : Controls.Window { + private static readonly string[] WEEK_DAYS = new string[] { "一", "二", "三", "四", "五", "六", "日" }; + private string repo = null; + + public Statistics(string repo) { + this.repo = repo; + InitializeComponent(); + Task.Run(Refresh); + } + + private void Quit(object sender, RoutedEventArgs e) { + Close(); + } + + private void Refresh() { + var mapsWeek = new Dictionary(); + for (int i = 0; i < 7; i++) { + mapsWeek.Add(i, new Models.StatisticSample { + Name = $"星期{WEEK_DAYS[i]}", + Count = 0, + }); + } + + var mapsMonth = new Dictionary(); + var today = DateTime.Now; + var maxDays = DateTime.DaysInMonth(today.Year, today.Month); + for (int i = 1; i <= maxDays; i++) { + mapsMonth.Add(i, new Models.StatisticSample { + Name = $"{i}", + Count = 0, + }); + } + + var mapCommitterWeek = new Dictionary(); + var mapCommitterMonth = new Dictionary(); + var week = today.DayOfWeek; + var month = today.Month; + + var limits = $"--since=\"{today.ToString("yyyy-MM-01 00:00:00")}\""; + var commits = new Commands.Commits(repo, limits).Result(); + var totalCommitsMonth = commits.Count; + var totalCommitsWeek = 0; + foreach (var c in commits) { + var commitTime = DateTime.Parse(c.Committer.Time); + if (IsSameWeek(today, commitTime)) { + mapsWeek[(int)commitTime.DayOfWeek].Count++; + if (mapCommitterWeek.ContainsKey(c.Committer.Name)) { + mapCommitterWeek[c.Committer.Name].Count++; + } else { + mapCommitterWeek[c.Committer.Name] = new Models.StatisticSample { + Name = c.Committer.Name, + Count = 1, + }; + } + + totalCommitsWeek++; + } + + mapsMonth[commitTime.Day].Count++; + + if (mapCommitterMonth.ContainsKey(c.Committer.Name)) { + mapCommitterMonth[c.Committer.Name].Count++; + } else { + mapCommitterMonth[c.Committer.Name] = new Models.StatisticSample { + Name = c.Committer.Name, + Count = 1, + }; + } + } + + var samplesChartWeek = new List(); + var samplesChartMonth = new List(); + var samplesCommittersWeek = new List(); + var samplesCommittersMonth = new List(); + for (int i = 0; i < 7; i++) samplesChartWeek.Add(mapsWeek[i]); + for (int i = 1; i <= maxDays; i++) samplesChartMonth.Add(mapsMonth[i]); + foreach (var kv in mapCommitterWeek) samplesCommittersWeek.Add(kv.Value); + foreach (var kv in mapCommitterMonth) samplesCommittersMonth.Add(kv.Value); + mapsMonth.Clear(); + mapsWeek.Clear(); + mapCommitterMonth.Clear(); + mapCommitterWeek.Clear(); + commits.Clear(); + samplesCommittersWeek.Sort((x, y) => y.Count - x.Count); + samplesCommittersMonth.Sort((x, y) => y.Count - x.Count); + + Dispatcher.Invoke(() => { + loading.IsAnimating = false; + loading.Visibility = Visibility.Collapsed; + + chartWeek.SetData(samplesChartWeek); + chartMonth.SetData(samplesChartMonth); + + lstCommitterWeek.ItemsSource = samplesCommittersWeek; + lstCommitterMonth.ItemsSource = samplesCommittersMonth; + + txtMemberCountWeek.Text = App.Text("Statistics.TotalCommitterCount", samplesCommittersWeek.Count); + txtMemberCountMonth.Text = App.Text("Statistics.TotalCommitterCount", samplesCommittersMonth.Count); + txtCommitCountWeek.Text = App.Text("Statistics.TotalCommitsCount", totalCommitsWeek); + txtCommitCountMonth.Text = App.Text("Statistics.TotalCommitsCount", totalCommitsMonth); + }); + } + + private bool IsSameWeek(DateTime t1, DateTime t2) { + double diffDay = t1.Subtract(t2).Duration().TotalDays; + if (diffDay >= 7) return false; + + return t1.CompareTo(t2) > 0 ? (t1.DayOfWeek >= t2.DayOfWeek) : t1.DayOfWeek <= t2.DayOfWeek; + } + } +} diff --git a/src/Views/Widgets/Dashboard.xaml b/src/Views/Widgets/Dashboard.xaml index da779d87..85984014 100644 --- a/src/Views/Widgets/Dashboard.xaml +++ b/src/Views/Widgets/Dashboard.xaml @@ -108,6 +108,13 @@ IsChecked="{Binding Source={x:Static models:Preference.Instance}, Path=Window.MoveCommitInfoRight, Mode=TwoWay, Converter={StaticResource InverseBool}}" Checked="ChangeOrientation" Unchecked="ChangeOrientation"/> + +